OmkGraph
OmkGraph(meta: GraphMeta, graph_engine: GraphAdapter | None = None)A graph representation of music: objects (see objects) as nodes, related by typed edges (see graph.edge). See the graph package docstring for lines, branches, pins, groups, parts and stints.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.temporal.symbols import quarter
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}, duration=quarter) for t in (C, D, E))
>>> graph.add_line([c, d, e])
>>> [format(next(iter(n.tones))) for n in graph.walk_line(c)]
['C', 'D', 'E']
>>> graph.relative_onset(c, e)
MetricalDuration(1, 2)Attributes
| Name | Description |
|---|---|
| meta | The graph’s own metadata (see GraphMeta); a work’s is on its Score. |
Methods
| Name | Description |
|---|---|
__init__ |
|
| from_json | Returns the graph an OMK JSON document holds, with its ids and metadata |
| from_file | Returns an OmkGraph read from a file, by its suffix: .musicxml, |
| import_json_graph | Adds the nodes and edges of an OMK JSON document to this graph, with their ids. |
| import_file | Adds the contents of a file to the current graph, by its suffix |
| export_to_json | The canonical OMK JSON document of this graph (see io.omk_json); |
| export_json_to_file | Writes export_to_json to a file, as UTF-8 with LF line endings. |
| export_file | Writes a file, by its suffix: .musicxml, .xml and .mxl through |
| get_node | Returns an OmkObj based on id. Returns None if no such object exists. |
| add_node | Adds node to the graph. If the node is already on the graph, does nothing. |
| remove_node | Removes a node and all related edges from the graph. |
| get_edge | Returns the edge of edge_type from from_obj to to_obj. |
| edges_between | Iterates over the edges from from_obj to to_obj, optionally filtered by edge_type. |
| edges | Iterates over all edges, optionally filtered by edge_type. |
| nodes | Iterates over all nodes, optionally filtered by node_type and/or predicate, |
| edge_endpoints | The source and target of edge, as a tuple. |
| is_equivalent | Whether other holds the same music as this graph: its nodes pair |
| correspondence | The id of each node here paired with the id of its counterpart in |
| diff | What changes from this graph to other, counted by content: the |
| add_edge | Adds a plain edge of edge_type; the semantic methods (add_next, |
| remove_edge | Removes an edge, found with get_edge or by iterating edges, |
| add_next | Places a SequentialEvent after another SequentialEvent. |
| get_next | The event that follows current in its line, or None at the end of the line. |
| get_previous | The event before current in its line, or None at the head of the line. |
| insert_event | Insert a SequentialEvent between two SequentialEvents. |
| add_line | Create a new linear subgraph from a list of objects. |
| insert_line_from_list | Create a new linear subgraph from a list of objects and insert it between prev and next. |
| walk_line | Yields the events from start to end (inclusive) along NEXT edges; |
| walk_span | Yields the events from start to end (inclusive) along NEXT edges, |
| transform_tones | Applies operation to the tonal content of every TonalObject from |
| remove_derivable_divisions | Removes from the line starting at head every division that the |
| add_branch | Hangs the line starting at head off parent: the same performer |
| branches_from | The heads of the lines branched from event. |
| add_simultaneous | Pins other to event: it starts at the anchor of event plus |
| relative_onset | How long after the onset of reference the onset of event falls |
| onsets | The onset of every node a timing walk reaches from reference, |
| check_alignment | Checks every pin against the rest of the timing graph. |
| add_group | Puts group on the graph holding the lines that start at heads, |
| add_to_group | Adds the line starting at head to group, starting displacement |
| group_members | The heads of the lines in group, in no particular order. |
| groups_of | The groups whose lines start at head. |
| walk_group | Every event of every line in group, line by line (the lines in |
| add_stint | Records that part performs the line from start to end |
| stints | The stints part performs, in no particular order. |
| add_performs | Records that part plays every line of group: one instrument |
| groups | The groups part plays every line of (see add_performs). |
| walk_stint | The events stint covers (walk_span from its start to its end). |
| materialize | Forks a doubled line: copies everything that defines the music |
| add_line_to_score | Records that the line starting at head is one of score’s. |
| add_part_to_score | Records that part is one of score’s parts. A part with no |
| score_lines | The heads of the top-level lines of score, in no particular order. |
| score_parts | The parts of score, in no particular order. |
| add_sync_line_to_score | Records that sync_line is one of score’s: a timeline the work |
| score_sync_lines | The sync lines of score, in no particular order. |
| scores_of | The scores that hold node, a line head, a Part or a SyncLine. |
| walk_score | Every event of every line of score, line by line (the lines in |
| add_stacked | Places lower directly below upper: one STACKS_ABOVE edge. |
| add_stack | Stacks members from the top down, each directly below the one |
| insert_stacked | Puts node between upper and lower, which are directly stacked. |
| stacked_below | What is directly below node in its stack, or None at the bottom. |
| stacked_above | What is directly above node in its stack, or None at the top. |
| walk_stack | node and everything stacked below it, top to bottom. |
| remove_from_stack | Takes node out of its stack, joining what was above it to what |
| add_stack_group | Places group over a run of a stack, from first down to last |
| stack_group_members | The members of group, from first to last. |
| stack_groups_of | The groups whose run includes node, in no particular order. |
| add_sync_line | Puts sync_line on the graph, with no points yet. |
| add_sync_point | Puts point on sync_line at the point’s own offset. |
| create_sync_point | The point of sync_line at offset, made if it is not there yet. |
| sync_event | Says that event happens at offset on sync_line, and returns |
| sync_points | The points of sync_line, earliest offset first. |
| sync_line_of | The sync line point is on, or None if it is on none. |
| remove_sync_line | Takes sync_line off the graph with its points and every pin to |
| synced_events | The events pinned to point, in no particular order. |
| check_sync_alignment | Checks that the points of a sync line run in the same order as the |
| add_articulation | Places a mark on one event, with a MARKS edge from the mark to the event. |
| add_annotation | Attaches any object to another as an annotation (a memo, an |
| add_spanner | Places a spanner over the events from start to end, with |
| spanner_start | The event spanner starts at. |
| spanner_end | The event spanner ends at, or None for an open spanner |
| add_lyrics | Adds a line of lyric syllables, joined by NEXT edges. |
| connect_lyric_to_object | Records that obj begins lyric_syllable: a LYRIC edge marks a |
| zip_lyrics_to_objects | Attaches a line of syllables to a line of events, one syllable per |
| unlink_lyric_from_object | The inverse of connect_lyric_to_object: removes the LYRIC edge from obj to the syllable. |
| unlink_lyric_sequence | Unlink a sequence of lyric syllables from their associated musical objects, |
__init__
__init__(meta: GraphMeta, graph_engine: GraphAdapter | None = None)from_json
from_json(
text: str,
graph_engine: GraphAdapter | None = None,
vocabularies: Iterable[Vocabulary] = (),
)Returns the graph an OMK JSON document holds, with its ids and metadata (see io.omk_json); vocabularies are any beyond the core and WSMN that the document requires.
>>> from openmusickit.objects.note_event import NoteEvent
>>> graph = OmkGraph(GraphMeta(name="Sketch"))
>>> graph.add_node(NoteEvent(tones=set()))
>>> again = OmkGraph.from_json(graph.export_to_json())
>>> again.meta.name, [node.id for node in again.nodes()] == [node.id for node in graph.nodes()]
('Sketch', True)Raises
| Name | Type | Description |
|---|---|---|
| OmkJsonError | if text is not an OMK JSON document, or breaks the specification. |
from_file
from_file(f: Path, graph_engine: GraphAdapter | None = None)Returns an OmkGraph read from a file, by its suffix: .musicxml, .xml and .mxl through io.musicxml.read, and .json through io.omk_json.read (which keeps the document’s graph metadata).
Raises ValueError for a suffix no reader claims.
import_json_graph
import_json_graph(text: str, vocabularies: Iterable[Vocabulary] = ())Adds the nodes and edges of an OMK JSON document to this graph, with their ids. The document’s graph metadata is not merged into this graph’s. A document that cannot be read, or holds an id already on this graph, adds nothing.
Raises
| Name | Type | Description |
|---|---|---|
| OmkJsonError | if text is not an OMK JSON document, or breaks the specification. |
|
| GraphError | if a node or edge of the document has an id already on this graph. |
import_file
import_file(f: Path)Adds the contents of a file to the current graph, by its suffix (see from_file); a MusicXML file becomes a new Score on the graph, and a JSON document’s nodes and edges are added with their ids (import_json_graph).
export_to_json
export_to_json(vocabularies: Iterable[Vocabulary] = ())The canonical OMK JSON document of this graph (see io.omk_json); vocabularies are any beyond the core and WSMN that its types need.
>>> OmkGraph(GraphMeta()).export_to_json().splitlines()[:3]
['{', ' "edges": [],', ' "format": "omk-json",']Raises
| Name | Type | Description |
|---|---|---|
| UnexportableError | if the graph holds something no vocabulary can write. |
export_json_to_file
export_json_to_file(f: Path, vocabularies: Iterable[Vocabulary] = ())Writes export_to_json to a file, as UTF-8 with LF line endings.
Raises
| Name | Type | Description |
|---|---|---|
| UnexportableError | as export_to_json does. |
export_file
export_file(f: Path, score: Score | None = None)Writes a file, by its suffix: .musicxml, .xml and .mxl through io.musicxml.write, which writes one Score (the graph’s only one when score is None), and .json through io.omk_json.write, which writes the whole graph.
Raises ValueError for a suffix no writer claims, or for a score given with .json; and UnexportableError when the format cannot write the graph (from io.musicxml.write, when there is no score).
get_node
get_node(node_id: UUID | str)Returns an OmkObj based on id. Returns None if no such object exists.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C
>>> graph = OmkGraph(GraphMeta())
>>> note = NoteEvent(tones={C})
>>> graph.add_node(note)
>>> graph.get_node(note.id) is note, graph.get_node(str(note.id)) is note
(True, True)add_node
add_node(obj: OmkObject)Adds node to the graph. If the node is already on the graph, does nothing.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C
>>> graph = OmkGraph(GraphMeta())
>>> note = NoteEvent(tones={C})
>>> graph.add_node(note)
>>> graph.add_node(note) # a second add is a no-op
>>> graph.get_node(note.id) is note
Trueremove_node
remove_node(obj: OmkObject)Removes a node and all related edges from the graph.
This does not automatically remove the object and edges from memory; if you retain a reference to the object and edges, they remain accessible until they are garbage collected.
Removing an event from the middle of a line takes its NEXT edges with it:
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d, e])
>>> graph.remove_node(d)
>>> graph.get_next(c) is None, list(graph.edges())
(True, [])Raises an exception if the node does not exist.
get_edge
get_edge(from_obj: OmkObject, to_obj: OmkObject, edge_type: EdgeType)Returns the edge of edge_type from from_obj to to_obj.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d])
>>> graph.get_edge(c, d, EdgeType.NEXT).type
<EdgeType.NEXT: 'next'>
>>> graph.get_edge(c, d, EdgeType.MARKS)
Traceback (most recent call last):
...
openmusickit.errors.GraphError: No edge of type <EdgeType.MARKS: 'marks'> between ...Raises
| Name | Type | Description |
|---|---|---|
| GraphError | if there is no such edge. |
edges_between
edges_between(
from_obj: OmkObject,
to_obj: OmkObject,
edge_type: EdgeType | None = None,
)Iterates over the edges from from_obj to to_obj, optionally filtered by edge_type.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d])
>>> [edge.type for edge in graph.edges_between(c, d)]
[<EdgeType.NEXT: 'next'>]
>>> list(graph.edges_between(d, c)) # edges are directed
[]edges
edges(edge_type: EdgeType | None = None)Iterates over all edges, optionally filtered by edge_type.
>>> from openmusickit.objects.marking import Marking
>>> from openmusickit.systems.wsmn.scoring.symbols import staccato
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d, e])
>>> graph.add_articulation(Marking(mark=staccato), c)
>>> sorted(edge.type.name for edge in graph.edges())
['MARKS', 'NEXT', 'NEXT']
>>> [edge.type.name for edge in graph.edges(EdgeType.MARKS)]
['MARKS']nodes
nodes(
node_type: type[OmkObject] | None = None,
predicate: Callable[[OmkObject], bool] | None = None,
)Iterates over all nodes, optionally filtered by node_type and/or predicate, in no particular order.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D
>>> graph = OmkGraph(GraphMeta())
>>> c, d = (NoteEvent(tones={t}) for t in (C, D))
>>> graph.add_line([c, d])
>>> graph.add_stint(Part(name="Flute"), Stint(), c)
>>> sorted(type(node).__name__ for node in graph.nodes())
['NoteEvent', 'NoteEvent', 'Part', 'Stint']
>>> [part.name for part in graph.nodes(Part)]
['Flute']
>>> [node is d for node in graph.nodes(NoteEvent, lambda n: D in n.tones)]
[True]edge_endpoints
edge_endpoints(edge: OmkEdge)The source and target of edge, as a tuple.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D
>>> graph = OmkGraph(GraphMeta())
>>> c, d = (NoteEvent(tones={t}) for t in (C, D))
>>> graph.add_line([c, d])
>>> source, target = graph.edge_endpoints(graph.get_edge(c, d, EdgeType.NEXT))
>>> source is c, target is d
(True, True)Raises
| Name | Type | Description |
|---|---|---|
| GraphError | if edge is not on the graph. |
is_equivalent
is_equivalent(other: OmkGraph)Whether other holds the same music as this graph: its nodes pair off one to one with equal nodes here (==, so ids and meta aside), joined by equal edges. GraphMeta is not compared, and the graphs may sit on different adapters. See graph.equivalence for how.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph, rebuilt = OmkGraph(GraphMeta()), OmkGraph(GraphMeta(name="copy"))
>>> graph.add_line([NoteEvent(tones={C}), NoteEvent(tones={D})])
>>> rebuilt.add_line([NoteEvent(tones={C}), NoteEvent(tones={D})])
>>> graph.is_equivalent(rebuilt)
True
>>> rebuilt.add_line([NoteEvent(tones={E})])
>>> graph.is_equivalent(rebuilt)
Falsecorrespondence
correspondence(other: OmkGraph)The id of each node here paired with the id of its counterpart in other, or None if the graphs are not equivalent (is_equivalent). Where the music repeats itself, several pairings may do, and this is one of them.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D
>>> c, d, c2, d2 = (NoteEvent(tones={t}) for t in (C, D, C, D))
>>> graph, rebuilt = OmkGraph(GraphMeta()), OmkGraph(GraphMeta())
>>> graph.add_line([c, d])
>>> rebuilt.add_line([c2, d2])
>>> graph.correspondence(rebuilt) == {c.id: c2.id, d.id: d2.id}
Truediff
diff(other: OmkGraph)What changes from this graph to other, counted by content: the nodes, and the edges (with their ends), that one has more of than the other. See GraphDiff.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D
>>> graph, rebuilt = OmkGraph(GraphMeta()), OmkGraph(GraphMeta())
>>> graph.add_node(NoteEvent(tones={C}))
>>> rebuilt.add_node(NoteEvent(tones={D}))
>>> print(graph.diff(rebuilt))
- NoteEvent(tones=[TonalVector((0, 0))], duration=None)
+ NoteEvent(tones=[TonalVector((1, 2))], duration=None)
>>> print(graph.diff(graph))
no differencesadd_edge
add_edge(from_obj: OmkObject, to_obj: OmkObject, edge_type: EdgeType)Adds a plain edge of edge_type; the semantic methods (add_next, add_branch, add_articulation, …) build on this and are usually what a caller wants.
>>> from openmusickit.objects.marking import Marking
>>> from openmusickit.systems.wsmn.scoring.symbols import staccato
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> dot = Marking(mark=staccato)
>>> graph.add_line([c, d])
>>> graph.add_node(dot)
>>> graph.add_edge(dot, c, EdgeType.MARKS)
>>> graph.get_edge(dot, c, EdgeType.MARKS).type
<EdgeType.MARKS: 'marks'>
>>> graph.add_edge(dot, c, EdgeType.MARKS) # one edge of a type between two nodes
Traceback (most recent call last):
...
openmusickit.errors.GraphError: An edge of type <EdgeType.MARKS: 'marks'> already exists ...Raises
| Name | Type | Description |
|---|---|---|
| GraphError | if an edge of that type already exists between the two nodes, or if a NEXT edge would give a node a second NEXT in or out. |
remove_edge
remove_edge(edge: OmkEdge)Removes an edge, found with get_edge or by iterating edges, and returns it, so an undo has what it removed (its type, displacement and metadata) to hand.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d])
>>> removed = graph.remove_edge(graph.get_edge(c, d, EdgeType.NEXT))
>>> removed, graph.get_next(c) is None, graph.get_previous(d) is None
(Next(next, origin=asserted), True, True)add_next
add_next(current: SequentialEvent, following: SequentialEvent)Places a SequentialEvent after another SequentialEvent.
Current node must be on the graph before adding a next node. Next node can be on the graph or not.
A line is a chain: an event has at most one NEXT out and one NEXT in. Music that runs alongside a line goes on a line of its own, joined to this one by add_branch or add_simultaneous.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d])
>>> graph.add_next(c, e)
Traceback (most recent call last):
...
openmusickit.errors.GraphError: Source NoteEvent(...) already has an outgoing edge of type NEXT.
>>> graph.add_next(e, d)
Traceback (most recent call last):
...
openmusickit.errors.GraphError: Target NoteEvent(...) already has an incoming edge of type NEXT.get_next
get_next(current: SequentialEvent)The event that follows current in its line, or None at the end of the line.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d, e])
>>> graph.get_next(c) is d, graph.get_next(e) is None
(True, True)get_previous
get_previous(current: SequentialEvent)The event before current in its line, or None at the head of the line.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d, e])
>>> graph.get_previous(d) is c, graph.get_previous(c) is None
(True, True)insert_event
insert_event(
event: SequentialEvent,
prev: SequentialEvent,
following: SequentialEvent,
)Insert a SequentialEvent between two SequentialEvents.
It makes no difference if the inserted object was already part of the graph.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, e])
>>> graph.insert_event(d, c, e)
>>> [format(next(iter(n.tones))) for n in graph.walk_line(c)]
['C', 'D', 'E']add_line
add_line(line: list[SequentialEvent])Create a new linear subgraph from a list of objects.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d, e])
>>> [format(next(iter(n.tones))) for n in graph.walk_line(c)]
['C', 'D', 'E']
>>> graph.get_previous(c) is None, graph.get_next(e) is None
(True, True)insert_line_from_list
insert_line_from_list(
line: list[SequentialEvent],
prev: SequentialEvent,
following: SequentialEvent,
)Create a new linear subgraph from a list of objects and insert it between prev and next.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E, F
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e, f = (NoteEvent(tones={t}) for t in (C, D, E, F))
>>> graph.add_line([c, f])
>>> graph.insert_line_from_list([d, e], c, f)
>>> [format(next(iter(n.tones))) for n in graph.walk_line(c)]
['C', 'D', 'E', 'F']walk_line
walk_line(start: SequentialEvent, end: SequentialEvent | None = None)Yields the events from start to end (inclusive) along NEXT edges; end=None runs to the end of the line. Never leaves the line, and yields each event once: a cyclic line (a gamelan cycle, whose last event is followed by its first) comes out as one pass from start.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d, e])
>>> [format(next(iter(n.tones))) for n in graph.walk_line(d)]
['D', 'E']
>>> [format(next(iter(n.tones))) for n in graph.walk_line(c, d)]
['C', 'D']
>>> list(graph.walk_line(e, c))
Traceback (most recent call last):
...
ValueError: NoteEvent(...) was not reached: the line starting at NoteEvent(...) ended first.
>>> graph.add_next(e, c) # close the cycle
>>> [format(next(iter(n.tones))) for n in graph.walk_line(d)]
['D', 'E', 'C']Raises ValueError if the line ends, or comes back round to an event already yielded, before end is reached.
walk_span
walk_span(start: SequentialEvent, end: SequentialEvent | None = None)Yields the events from start to end (inclusive) along NEXT edges, and, after each, the whole of every line branched from it: everything the performer of this line does over that stretch. Pinned lines (add_simultaneous) are never entered. Each event is yielded once, however the lines cycle or branch back into each other.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E, A, B
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e, a, b = (NoteEvent(tones={t}) for t in (C, D, E, A, B))
>>> graph.add_line([c, d, e])
>>> graph.add_line([a, b])
>>> graph.add_branch(d, a) # a second voice under d-e
>>> [format(next(iter(n.tones))) for n in graph.walk_span(c)]
['C', 'D', 'A', 'B', 'E']
>>> [format(next(iter(n.tones))) for n in graph.walk_line(c)]
['C', 'D', 'E']transform_tones
transform_tones(
start: SequentialEvent,
end: SequentialEvent | None,
operation: Callable[..., Tone],
*args,
**kwargs,
)Applies operation to the tonal content of every TonalObject from start to end (inclusive), over the span (walk_span): along NEXT edges and into every line branched from them, never into a pinned line. end=None runs to the end of the line. Objects without tonal content are passed over.
Raises ValueError if the line ends before end is reached.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.objects.chord_event import ChordEvent
>>> from openmusickit.objects.context_event import ModalContextEvent
>>> from openmusickit.systems.wsmn.tonal.key import Key
>>> from openmusickit.systems.wsmn.tonal.tonal_vector import TonalVector
>>> from openmusickit.systems.wsmn.tonal.symbols import C, E, G, Gx, B, maj, M3, Major
>>> key, note, chord = (ModalContextEvent(modal_context=Key.of(C, Major)),
... NoteEvent(tones={C, E, G}), ChordEvent(chord=C(maj)))
>>> graph = OmkGraph(GraphMeta())
>>> graph.add_line([key, note, chord])
>>> graph.transform_tones(key, chord, TonalVector.transpose, M3)
>>> key.modal_context.name, note.tones == {E, Gx, B}, str(chord.chord)
('E Major', True, 'E')remove_derivable_divisions
remove_derivable_divisions(head: SequentialEvent)Removes from the line starting at head every division that the meter in force would put back, and returns them in line order.
A division is derivable when it falls exactly one meter after the previous boundary (the previous division, or the head), has no shape, carries nothing (no mark, annotation or spanner end), and is not the last event of the line. A pickup arrives early, so it stays and the count restarts from it; a split bar, a repeat, a double or final bar, a bar line with a fermata, and every division of a line with no meter stay too. Pins to a removed division move to the event after it, which starts at the same instant. Past an event of unknown duration nothing can be judged, so the rest of the line is left as it is.
insert_event puts a returned division back where it was.
>>> from openmusickit.objects.context_event import MeterEvent
>>> from openmusickit.objects.division_event import DivisionEvent
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.scoring.symbols import final_bar
>>> from openmusickit.systems.wsmn.temporal.symbols import four_four, quarter
>>> from openmusickit.systems.wsmn.tonal.symbols import C
>>> graph = OmkGraph(GraphMeta())
>>> meter = MeterEvent(meter=four_four)
>>> notes = [NoteEvent(tones={C}, duration=quarter) for _ in range(9)]
>>> pickup, bar, end = DivisionEvent(), DivisionEvent(), DivisionEvent(shape=final_bar)
>>> graph.add_line([meter, notes[0], pickup, *notes[1:5], bar, *notes[5:], end])
>>> removed = graph.remove_derivable_divisions(meter)
>>> removed[0] is bar, len(removed)
(True, 1)
>>> [n is pickup or n is end for n in graph.walk_line(meter) if isinstance(n, DivisionEvent)]
[True, True]add_branch
add_branch(
parent: SequentialEvent,
head: SequentialEvent,
anchor: TimingAnchor = TimingAnchor.ONSET,
displacement: Duration | None = None,
)Hangs the line starting at head off parent: the same performer does both. The child starts at the anchor of parent (its onset by default; its offset for a voice that begins after this event) plus the signed displacement.
A branch is a second voice within one instrument’s line: a one-measure second voice is a short line branched from the event it starts under; a voice that runs the whole piece is branched head from head. Either way it just ends when it ends; there is no join. Lines that are their own things done together (the pianist’s hands, the drums of a kit) are not branches but the lines of a LineGroup.
parent must be on the graph; head may or may not be. head must be the head of a line (no NEXT in), not already branched, not a line of a group or a score, and not the start of a Stint (whoever performs the parent performs the branch).
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.temporal.symbols import quarter, half
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E, A, B
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}, duration=half) for t in (C, D, E))
>>> a, b = (NoteEvent(tones={t}, duration=quarter) for t in (A, B))
>>> graph.add_line([c, d, e])
>>> graph.add_line([a, b])
>>> graph.add_branch(c, a, anchor=TimingAnchor.OFFSET) # a-b fills the time of d
>>> graph.relative_onset(c, b)
MetricalDuration(1, 2, dots=1)
>>> graph.relative_onset(b, e)
MetricalDuration(1, 4)
>>> graph.add_branch(d, b)
Traceback (most recent call last):
...
openmusickit.errors.GraphError: NoteEvent(...) is not the head of a line.branches_from
branches_from(event: SequentialEvent)The heads of the lines branched from event.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, A
>>> graph = OmkGraph(GraphMeta())
>>> c, d, a = (NoteEvent(tones={t}) for t in (C, D, A))
>>> graph.add_line([c, d])
>>> graph.add_branch(d, a)
>>> list(graph.branches_from(d)) == [a], list(graph.branches_from(c))
(True, [])add_simultaneous
add_simultaneous(
event: SequentialEvent,
other: SequentialEvent | SyncPoint,
anchor: TimingAnchor = TimingAnchor.ONSET,
displacement: Duration | None = None,
)Pins other to event: it starts at the anchor of event plus the signed displacement. The two lines stay independent; nothing is said about who performs either, and span walks never cross a pin.
This is how incomplete music is joined without a score origin: a three-bar chord line pinned to bar 1 of a melody, a countermelody pinned under a held note with a displacement.
event must be on the graph; other may or may not be.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.objects.chord_event import ChordEvent
>>> from openmusickit.systems.wsmn.temporal.symbols import quarter, whole
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E, F, maj
>>> graph = OmkGraph(GraphMeta())
>>> melody = [NoteEvent(tones={t}, duration=quarter) for t in (C, D, E, F)]
>>> chords = [ChordEvent(chord=C(maj), duration=whole), ChordEvent(chord=F(maj), duration=whole)]
>>> graph.add_line(melody)
>>> graph.add_line(chords)
>>> graph.add_simultaneous(melody[1], chords[0], displacement=-quarter) # chords start with the melody
>>> graph.relative_onset(melody[0], chords[1])
MetricalDuration(1, 1)
>>> [n is melody[0] for n in graph.walk_span(chords[0])] # pins are not entered
[False, False]relative_onset
relative_onset(reference: OmkObject, event: OmkObject)How long after the onset of reference the onset of event falls (negative if before), found by following NEXT edges through event durations, and branches, pins and groups through their anchors and displacements.
There is no score origin; a position only ever means “relative to”. Either end may be any node of the timing graph, not only an event: a LineGroup and a SyncLine are origins, and a SyncPoint is an instant on one. An event with duration=None is opaque: nothing is known about what follows it or hangs from its offset. Where pins disagree with the lines’ own durations, the first path found wins; see check_alignment. A sync line is met but not crossed, so a distance through the music is never answered in the timeline’s units (sync_event).
Raises GraphError if no timing path connects the two events.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.temporal.symbols import quarter, half
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}, duration=quarter) for t in (C, D, E))
>>> graph.add_line([c, d, e])
>>> graph.relative_onset(c, e), graph.relative_onset(e, c), graph.relative_onset(c, c)
(MetricalDuration(1, 2), MetricalDuration(-1, 2), ZeroDuration())
>>> d.duration = None
>>> graph.relative_onset(c, e)
Traceback (most recent call last):
...
openmusickit.errors.GraphError: No timing path connects NoteEvent(...) to NoteEvent(...).onsets
onsets(reference: OmkObject)The onset of every node a timing walk reaches from reference, keyed by node id: how long after the onset of reference each one starts (negative if before), the reference itself at ZeroDuration().
The walk is the one behind relative_onset, run to exhaustion: NEXT edges through known durations; branches, pins and groups through their anchors and displacements; a sync line met but not crossed. A node that is not in the result has no known timing relation to the reference: it lies beyond an event of unknown duration, or in an unrelated part of the graph, or at a distance measured in another temporal system.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.temporal.symbols import quarter
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}, duration=quarter) for t in (C, D, E))
>>> graph.add_line([c, d, e])
>>> onsets = graph.onsets(d)
>>> onsets[c.id], onsets[d.id], onsets[e.id]
(MetricalDuration(-1, 4), ZeroDuration(), MetricalDuration(1, 4))
>>> d.duration = None
>>> [node.id in graph.onsets(c) for node in (c, d, e)] # e lies beyond the unknown duration
[True, True, False]Raises
| Name | Type | Description |
|---|---|---|
| GraphError | if reference is not on the graph. |
check_alignment
check_alignment()Checks every pin against the rest of the timing graph.
Branches and groups alone keep timing a tree, always consistent. Pins make it a graph: a pin between two lines of one group, or two pins between the same lines, can disagree with the lines’ own durations. That is a musical fact to report, not an error: each pin whose claim differs from the offset found by another path is returned, with an OmkWarning. A pin with no other path is unchecked.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.temporal.symbols import quarter, whole
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E, A, B
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}, duration=quarter) for t in (C, D, E))
>>> a, b = (NoteEvent(tones={t}, duration=whole) for t in (A, B))
>>> graph.add_line([c, d, e])
>>> graph.add_line([a, b])
>>> graph.add_simultaneous(c, a)
>>> graph.add_simultaneous(e, b) # but b is a whole after a, e only a half after c
>>> import warnings
>>> with warnings.catch_warnings(record=True) as caught:
... warnings.simplefilter("always")
... conflicts = graph.check_alignment()
>>> len(conflicts), str(caught[1].message)
(2, 'NoteEvent(tones=[TonalVector((6, 11))], duration=MetricalDuration(1, 1)) is pinned ZeroDuration() after NoteEvent(tones=[TonalVector((2, 4))], duration=MetricalDuration(1, 4)), but lies MetricalDuration(1, 2) after it by another path.')add_group
add_group(group: LineGroup, heads: Iterable[SequentialEvent] = ())Puts group on the graph holding the lines that start at heads, each starting at the group’s origin (add_to_group places a line later than that).
The hands of a pianist, the drums of a kit, the three instruments of one percussionist: lines that are their own things done together. Nothing is said about who plays them; a Part points at the group (add_performs) or at a line (add_stint).
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.temporal.symbols import quarter, half
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E, F, G, A
>>> graph = OmkGraph(GraphMeta())
>>> rh = [NoteEvent(tones={t}, duration=quarter) for t in (C, D, E, F)]
>>> lh = [NoteEvent(tones={t}, duration=half) for t in (G, A)]
>>> graph.add_line(rh)
>>> graph.add_line(lh)
>>> piano = LineGroup(name="Piano")
>>> graph.add_group(piano, [rh[0], lh[0]])
>>> graph.relative_onset(rh[0], lh[1]), graph.relative_onset(lh[1], rh[3])
(MetricalDuration(1, 2), MetricalDuration(1, 4))
>>> sorted(format(next(iter(n.tones))) for n in graph.walk_group(piano))
['A', 'C', 'D', 'E', 'F', 'G']add_to_group
add_to_group(
group: LineGroup,
head: SequentialEvent,
displacement: Duration | None = None,
)Adds the line starting at head to group, starting displacement after the group’s origin (at the origin if None).
head must be the head of a line (no NEXT in) and not branched: a branched line belongs to its parent’s line, which is in the group through its own head.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.temporal.symbols import quarter, half
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E, F, G, A
>>> graph = OmkGraph(GraphMeta())
>>> tune = [NoteEvent(tones={t}, duration=quarter) for t in (C, D, E, F)]
>>> late = [NoteEvent(tones={t}, duration=quarter) for t in (G, A)]
>>> graph.add_line(tune)
>>> graph.add_line(late)
>>> duet = LineGroup()
>>> graph.add_group(duet, [tune[0]])
>>> graph.add_to_group(duet, late[0], displacement=half) # enters two beats in
>>> graph.relative_onset(tune[0], late[1])
MetricalDuration(1, 2, dots=1)
>>> graph.add_to_group(duet, tune[1])
Traceback (most recent call last):
...
openmusickit.errors.GraphError: NoteEvent(...) is not the head of a line.Raises
| Name | Type | Description |
|---|---|---|
| GraphError | if head is not the head of a line, is branched, or is already in group. |
group_members
group_members(group: LineGroup)The heads of the lines in group, in no particular order.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D
>>> graph = OmkGraph(GraphMeta())
>>> c, d = (NoteEvent(tones={t}) for t in (C, D))
>>> group = LineGroup()
>>> graph.add_group(group, [c, d])
>>> sorted(format(next(iter(n.tones))) for n in graph.group_members(group))
['C', 'D']groups_of
groups_of(head: SequentialEvent)The groups whose lines start at head.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D
>>> graph = OmkGraph(GraphMeta())
>>> c, d = (NoteEvent(tones={t}) for t in (C, D))
>>> graph.add_line([c, d])
>>> group = LineGroup(name="Duo")
>>> graph.add_group(group, [c])
>>> list(graph.groups_of(c)) == [group], list(graph.groups_of(d))
(True, [])walk_group
walk_group(group: LineGroup)Every event of every line in group, line by line (the lines in no particular order): walk_span from each head, so second voices come along and pins are not entered.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E, F
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e, f = (NoteEvent(tones={t}) for t in (C, D, E, F))
>>> graph.add_line([c, d])
>>> graph.add_line([e, f])
>>> group = LineGroup()
>>> graph.add_group(group, [c, e])
>>> sorted(format(next(iter(n.tones))) for n in graph.walk_group(group))
['C', 'D', 'E', 'F']add_stint
add_stint(
part: Part,
stint: Stint,
start: SequentialEvent,
end: SequentialEvent | None = None,
)Records that part performs the line from start to end (inclusive), or to the end of the line, however long it grows, if end is None. Everything branched from those events is covered too.
start must not be a branched head: a branched line is performed by whoever performs its parent.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E, P5
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d, e])
>>> flute, oboe = Part(name="Flute"), Part(name="Oboe")
>>> graph.add_stint(flute, Stint(), c)
>>> graph.add_stint(oboe, Stint(transposition=P5), c, d) # doubling, a fifth up, for two notes
>>> [len(list(graph.walk_stint(s))) for s in graph.stints(oboe)]
[2]stints
stints(part: Part)The stints part performs, in no particular order.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d, e])
>>> flute, stint = Part(name="Flute"), Stint()
>>> graph.add_stint(flute, stint, c)
>>> list(graph.stints(flute)) == [stint]
Trueadd_performs
add_performs(part: Part, group: LineGroup)Records that part plays every line of group: one instrument with several lines, as a pianist has two hands. A group whose lines are different instruments gets a stint per line instead.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, G
>>> graph = OmkGraph(GraphMeta())
>>> rh, lh = (NoteEvent(tones={t}) for t in (C, G))
>>> piano_lines = LineGroup()
>>> graph.add_group(piano_lines, [rh, lh])
>>> piano = Part(name="Piano")
>>> graph.add_performs(piano, piano_lines)
>>> list(graph.groups(piano)) == [piano_lines], list(graph.stints(piano))
(True, [])groups
groups(part: Part)The groups part plays every line of (see add_performs).
walk_stint
walk_stint(stint: Stint)The events stint covers (walk_span from its start to its end). The events are yielded as they are; stint.transposition is the consumer’s to apply.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d, e])
>>> stint = Stint()
>>> graph.add_stint(Part(name="Oboe"), stint, c, d)
>>> [format(next(iter(n.tones))) for n in graph.walk_stint(stint)]
['C', 'D']materialize
materialize(stint: Stint)Forks a doubled line: copies everything that defines the music stint covers into a new line, points the stint at the copy, and returns the copy’s head. The copy starts exact, so the two parts can now diverge; the original events, and any other stint over them, are untouched.
Copied: the events (walk_span, so branches come along), and every mark, spanner, annotation and lyric attached to them, with all the edges among those things. Not copied: relationships to other lines (pins to events outside the span), other Parts’ stints, and any spanner (a slur, a lyric section) that reaches beyond the covered events – such a spanner is left out with an OmkWarning, since the copy has no event for its other end.
>>> from openmusickit.objects.marking import Marking, MarkSpanner
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.scoring.symbols import slur, staccato
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E, P5
>>> from openmusickit.systems.wsmn.tonal.tonal_vector import TonalVector
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d, e])
>>> graph.add_articulation(Marking(mark=staccato), c)
>>> graph.add_spanner(MarkSpanner(mark=slur), c, d)
>>> flute, oboe = Part(name="Flute"), Part(name="Oboe")
>>> graph.add_stint(flute, Stint(), c)
>>> oboe_stint = Stint()
>>> graph.add_stint(oboe, oboe_stint, c, d)
>>> head = graph.materialize(oboe_stint)
>>> head == c, head is c, graph.get_previous(head) is None
(True, False, True)
>>> sorted(type(n).__name__ for n in graph._graph.predecessors(head))
['MarkSpanner', 'Marking', 'Stint']
>>> graph.transform_tones(head, None, TonalVector.transpose, P5)
>>> [format(next(iter(n.tones))) for n in graph.walk_stint(oboe_stint)]
['G', 'A']
>>> [format(next(iter(n.tones))) for n in graph.walk_span(c)]
['C', 'D', 'E']add_line_to_score
add_line_to_score(score: Score, head: SequentialEvent)Records that the line starting at head is one of score’s.
head must be the head of a line (no NEXT in) and not branched: a branched line is in the score through its parent’s head, the same rule as add_to_group. The edge is a plain CONTAINS: a Score is not a timing origin, so nothing is said about when the line starts (pins and groups say that). Once per score.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.objects.score import Score
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E, F
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e, f = (NoteEvent(tones={t}) for t in (C, D, E, F))
>>> graph.add_line([c, d])
>>> graph.add_line([e, f])
>>> score = Score(title="Two lines")
>>> graph.add_line_to_score(score, c)
>>> graph.add_line_to_score(score, e)
>>> sorted(format(next(iter(n.tones))) for n in graph.score_lines(score))
['C', 'E']
>>> list(graph.scores_of(c)) == [score]
True
>>> graph.relative_onset(score, c) # a Score is not a timing node
Traceback (most recent call last):
...
openmusickit.errors.GraphError: No timing path connects Score(...) to NoteEvent(...).
>>> graph.add_line_to_score(score, d)
Traceback (most recent call last):
...
openmusickit.errors.GraphError: NoteEvent(...) is not the head of a line.Raises
| Name | Type | Description |
|---|---|---|
| GraphError | if head is not the head of a line, is branched, or is already in score. |
add_part_to_score
add_part_to_score(score: Score, part: Part)Records that part is one of score’s parts. A part with no music yet (an orchestration sketch) is allowed.
>>> from openmusickit.objects.score import Score
>>> graph = OmkGraph(GraphMeta())
>>> score, flute = Score(title="Sketch"), Part(name="Flute")
>>> graph.add_part_to_score(score, flute)
>>> list(graph.score_parts(score)) == [flute], list(graph.scores_of(flute)) == [score]
(True, True)
>>> list(graph.stints(flute))
[]Raises
| Name | Type | Description |
|---|---|---|
| GraphError | if part is already in score. |
score_lines
score_lines(score: Score)The heads of the top-level lines of score, in no particular order.
score_parts
score_parts(score: Score)The parts of score, in no particular order.
add_sync_line_to_score
add_sync_line_to_score(score: Score, sync_line: SyncLine)Records that sync_line is one of score’s: a timeline the work is measured against, such as a bar grid that every part’s bar lines are pinned to, or the film a cue score is written to. The edge is a plain CONTAINS, as for the score’s lines and parts.
>>> from openmusickit.objects.score import Score
>>> from openmusickit.systems.wsmn.temporal.wsmn import WSMN_TEMPORAL
>>> graph = OmkGraph(GraphMeta())
>>> score, bars = Score(title="Quartet"), SyncLine(temporal_system=WSMN_TEMPORAL, name="bars")
>>> graph.add_sync_line_to_score(score, bars)
>>> list(graph.score_sync_lines(score)) == [bars], list(graph.scores_of(bars)) == [score]
(True, True)Raises
| Name | Type | Description |
|---|---|---|
| GraphError | if sync_line is already in score. |
score_sync_lines
score_sync_lines(score: Score)The sync lines of score, in no particular order.
scores_of
scores_of(node: OmkObject)The scores that hold node, a line head, a Part or a SyncLine.
walk_score
walk_score(score: Score)Every event of every line of score, line by line (the lines in no particular order): walk_span from each head, so second voices come along and pins are not entered.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.objects.score import Score
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E, A
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e, a = (NoteEvent(tones={t}) for t in (C, D, E, A))
>>> graph.add_line([c, d])
>>> graph.add_line([e])
>>> graph.add_line([a])
>>> graph.add_branch(d, a) # a second voice under d
>>> score = Score()
>>> graph.add_line_to_score(score, c)
>>> graph.add_line_to_score(score, e)
>>> sorted(format(next(iter(n.tones))) for n in graph.walk_score(score))
['A', 'C', 'D', 'E']add_stacked
add_stacked(upper: Stackable, lower: Stackable)Places lower directly below upper: one STACKS_ABOVE edge.
A stack is a chain, as a line is: a node has at most one thing directly above it and one directly below. What stacks is a Part, a LineGroup, the head of a line (not branched: a branched line is stacked through its parent) or a SyncLine. A stack says only the order in which things are drawn; it says nothing about time, who plays what, or which lines share a staff. Several unconnected stacks are allowed: an order that is only partly known.
>>> graph = OmkGraph(GraphMeta())
>>> violin, viola, cello = (Part(name=n) for n in ("Violin", "Viola", "Cello"))
>>> graph.add_stacked(violin, viola)
>>> graph.stacked_below(violin) is viola, graph.stacked_above(violin)
(True, None)
>>> graph.add_stacked(cello, viola)
Traceback (most recent call last):
...
openmusickit.errors.GraphError: Part(name='Viola') already has something stacked above it.
>>> graph.add_stacked(viola, violin)
Traceback (most recent call last):
...
openmusickit.errors.GraphError: Stacking Part(name='Violin') below Part(name='Viola') would close a cycle.Raises
| Name | Type | Description |
|---|---|---|
| GraphError | if upper already has something below it, lower already has something above it, the edge would close a cycle, or either node is not something that stacks. |
add_stack
add_stack(members: Iterable[Stackable])Stacks members from the top down, each directly below the one before (see add_stacked).
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, G
>>> graph = OmkGraph(GraphMeta())
>>> rh, lh = NoteEvent(tones={C}), NoteEvent(tones={G})
>>> graph.add_stack([rh, lh]) # the right hand's line above the left's
>>> list(graph.walk_stack(rh)) == [rh, lh]
Trueinsert_stacked
insert_stacked(node: Stackable, upper: Stackable, lower: Stackable)Puts node between upper and lower, which are directly stacked.
>>> graph = OmkGraph(GraphMeta())
>>> flute, oboe, clarinet = (Part(name=n) for n in ("Flute", "Oboe", "Clarinet"))
>>> graph.add_stack([flute, clarinet])
>>> graph.insert_stacked(oboe, flute, clarinet)
>>> [part.name for part in graph.walk_stack(flute)]
['Flute', 'Oboe', 'Clarinet']Raises
| Name | Type | Description |
|---|---|---|
| GraphError | if lower is not directly below upper, node is already in a stack, or node is not something that stacks. |
stacked_below
stacked_below(node: Stackable)What is directly below node in its stack, or None at the bottom.
stacked_above
stacked_above(node: Stackable)What is directly above node in its stack, or None at the top.
walk_stack
walk_stack(node: Stackable)node and everything stacked below it, top to bottom.
remove_from_stack
remove_from_stack(node: Stackable)Takes node out of its stack, joining what was above it to what was below. A StackGroup that started or ended at node now starts or ends at its neighbour inside the group; a group of node alone is removed. node stays on the graph.
>>> from openmusickit.systems.wsmn.scoring.symbols import bracket
>>> graph = OmkGraph(GraphMeta())
>>> flute, oboe, violin, viola = (Part(name=n) for n in ("Flute", "Oboe", "Violin", "Viola"))
>>> graph.add_stack([flute, oboe, violin, viola])
>>> strings = StackGroup(name="Strings", shape=bracket)
>>> graph.add_stack_group(strings, violin, viola)
>>> graph.remove_from_stack(violin)
>>> [part.name for part in graph.walk_stack(flute)]
['Flute', 'Oboe', 'Viola']
>>> [part.name for part in graph.stack_group_members(strings)]
['Viola']add_stack_group
add_stack_group(group: StackGroup, first: Stackable, last: Stackable)Places group over a run of a stack, from first down to last (inclusive), with STARTS_AT and ENDS_AT edges.
>>> from openmusickit.systems.wsmn.scoring.symbols import brace, bracket
>>> graph = OmkGraph(GraphMeta())
>>> oboe, violin_1, violin_2, viola = (
... Part(name=n) for n in ("Oboe", "Violin 1", "Violin 2", "Viola"))
>>> graph.add_stack([oboe, violin_1, violin_2, viola])
>>> strings, violins = StackGroup(name="Strings", shape=bracket), StackGroup(shape=brace)
>>> graph.add_stack_group(strings, violin_1, viola)
>>> graph.add_stack_group(violins, violin_1, violin_2) # nested
>>> [part.name for part in graph.stack_group_members(violins)]
['Violin 1', 'Violin 2']
>>> graph.add_stack_group(StackGroup(), viola, violin_1)
Traceback (most recent call last):
...
openmusickit.errors.GraphError: Part(name='Violin 1') is not at or below Part(name='Viola') in a stack.Raises
| Name | Type | Description |
|---|---|---|
| GraphError | if last is neither first nor stacked somewhere below it, or first is not something that stacks. |
stack_group_members
stack_group_members(group: StackGroup)The members of group, from first to last.
stack_groups_of
stack_groups_of(node: Stackable)The groups whose run includes node, in no particular order.
>>> from openmusickit.systems.wsmn.scoring.symbols import bracket
>>> graph = OmkGraph(GraphMeta())
>>> oboe, violin, viola = (Part(name=n) for n in ("Oboe", "Violin", "Viola"))
>>> graph.add_stack([oboe, violin, viola])
>>> strings = StackGroup(name="Strings", shape=bracket)
>>> graph.add_stack_group(strings, violin, viola)
>>> list(graph.stack_groups_of(viola)) == [strings], list(graph.stack_groups_of(oboe))
(True, [])add_sync_line
add_sync_line(sync_line: SyncLine)Puts sync_line on the graph, with no points yet.
create_sync_point and sync_event add it for you, so this is only needed for a timeline that has no points so far.
Removing a sync line is not the inverse of building one: unlike a group, whose member heads exist before they join it, a sync line’s points are created by create_sync_point, so remove_node(sync_line) leaves them behind. Undo a sync line by removing each pin, then each point, then the line.
>>> from openmusickit.values.time.clock_time import CLOCK_TIME
>>> graph = OmkGraph(GraphMeta())
>>> film = SyncLine(temporal_system=CLOCK_TIME, name="Reel 1")
>>> graph.add_sync_line(film)
>>> graph.sync_points(film)
[]add_sync_point
add_sync_point(sync_line: SyncLine, point: SyncPoint)Puts point on sync_line at the point’s own offset.
Use this for a point you have built yourself, to give it a name or a description; create_sync_point is the usual way in.
The offset must be measurable, because the line’s points are ordered by it, and must belong to the line’s temporal system (or be a ZeroDuration, which belongs to every system). A point is on at most one sync line, and no two points on one line share an offset.
>>> from openmusickit.values.time.clock_time import CLOCK_TIME, ClockDuration
>>> graph = OmkGraph(GraphMeta())
>>> film = SyncLine(temporal_system=CLOCK_TIME)
>>> slam = SyncPoint(offset=ClockDuration.from_seconds(5), name="door slams")
>>> graph.add_sync_point(film, slam)
>>> graph.sync_points(film) == [slam], graph.sync_line_of(slam) is film
(True, True)Raises
| Name | Type | Description |
|---|---|---|
| TypeError | if point.offset is not a Measurable. |
|
| TemporalCompatibilityError | if point.offset is not in sync_line’s temporal system. |
|
| GraphError | if point is already on another sync line, if another point of sync_line has an equal offset, or if point is already on sync_line. |
create_sync_point
create_sync_point(sync_line: SyncLine, offset: Duration)The point of sync_line at offset, made if it is not there yet.
This may return a point it did not create: offsets are compared by length, as durations always are, so MetricalDuration(1, 2) finds a point built as MetricalDuration(2, 4). A caller that needs to know whether anything was added should consult sync_points first.
>>> from openmusickit.values.time.clock_time import CLOCK_TIME, ClockDuration
>>> graph = OmkGraph(GraphMeta())
>>> film = SyncLine(temporal_system=CLOCK_TIME)
>>> first = graph.create_sync_point(film, ClockDuration.from_seconds(5))
>>> graph.create_sync_point(film, ClockDuration.from_milliseconds(5000)) is first
True
>>> len(graph.sync_points(film))
1Raises
| Name | Type | Description |
|---|---|---|
| (TypeError, TemporalCompatibilityError) | as add_sync_point does, for an offset that cannot be placed. |
sync_event
sync_event(
event: SequentialEvent,
sync_line: SyncLine,
offset: Duration,
anchor: TimingAnchor = TimingAnchor.ONSET,
)Says that event happens at offset on sync_line, and returns that point.
The point is found or made (create_sync_point) and the event is pinned to it with an ordinary SIMULTANEOUS edge. There is no displacement: the point is where the event falls. With anchor=OFFSET it is the event’s release that lands on the point, which is how “this note ends on the cue” is said; as with any offset-anchored edge, that contributes nothing while the event’s duration is unknown.
Several events may share one point, and that is how they are asserted simultaneous with each other.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.values.time.clock_time import CLOCK_TIME, ClockDuration
>>> from openmusickit.systems.wsmn.temporal.symbols import whole
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> line = [NoteEvent(tones={t}, duration=whole) for t in (C, D, E)]
>>> graph.add_line(line)
>>> film = SyncLine(temporal_system=CLOCK_TIME, name="Reel 1")
>>> start = graph.sync_event(line[0], film, ClockDuration.from_seconds(5))
>>> cue = graph.sync_event(line[2], film, ClockDuration.from_seconds(9))
>>> graph.relative_onset(start, cue) # along the timeline
ClockDuration(microseconds=4000000)
>>> graph.relative_onset(line[0], line[2]) # and through the music, in its own units
MetricalDuration(2, 1)
>>> graph.relative_onset(line[0], start)
ZeroDuration()sync_points
sync_points(sync_line: SyncLine)The points of sync_line, earliest offset first.
Unlike the graph’s other readers this one is ordered and eager: a timeline’s points have a meaningful order, and sorting them means holding them all.
sync_line_of
sync_line_of(point: SyncPoint)The sync line point is on, or None if it is on none.
remove_sync_line
remove_sync_line(sync_line: SyncLine)Takes sync_line off the graph with its points and every pin to them, and out of any Score that holds it: the undo of building one (add_sync_line explains why remove_node alone is not). The music the points were pinned to is untouched.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.values.time.clock_time import CLOCK_TIME, ClockDuration
>>> from openmusickit.systems.wsmn.tonal.symbols import C
>>> graph = OmkGraph(GraphMeta())
>>> note = NoteEvent(tones={C})
>>> graph.add_line([note])
>>> film = SyncLine(temporal_system=CLOCK_TIME)
>>> _ = graph.sync_event(note, film, ClockDuration.from_seconds(5))
>>> graph.remove_sync_line(film)
>>> [type(n).__name__ for n in graph.nodes()], list(graph.edges())
(['NoteEvent'], [])synced_events
synced_events(point: SyncPoint)The events pinned to point, in no particular order.
Both directions of pin are read, so an event pinned by hand the other way round is still found.
check_sync_alignment
check_sync_alignment(sync_line: SyncLine | None = None)Checks that the points of a sync line run in the same order as the music pinned to them.
For each neighbouring pair of points, the events pinned to them are compared: whichever way round the offsets say they fall, the music must agree, and events the music makes simultaneous must not be pinned to two different points. Each disagreeing pair is returned, with an OmkWarning; like check_alignment this reports rather than raises, because a source that says two contradictory things is a fact about the source.
Only the order is checked, never the distance. Comparing the interval between two offsets against the elapsed music would need both to be paced the same way, and a temporal system does not say that: two stretches of metrical music at different tempos cover the same real time in different numbers of quarter notes, so a metrical sync line’s offsets could only ever agree with one of them. Order survives any tempo, which is why it is what can be checked – across temporal systems as much as within one.
Nothing is reported where nothing can be: a distance that does not reduce to a number has no direction to compare, and two events on different points that are not otherwise related have no relative onset at all (the walk does not cross a sync line), so a freshly pinned timeline is silent. What this catches is a timeline that contradicts music related some other way – one long line, a group, a chain of pins, a shared point.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.values.time.clock_time import CLOCK_TIME, ClockDuration
>>> from openmusickit.systems.wsmn.temporal.symbols import whole
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> line = [NoteEvent(tones={t}, duration=whole) for t in (C, D, E)]
>>> graph.add_line(line)
>>> film = SyncLine(temporal_system=CLOCK_TIME)
>>> _ = graph.sync_event(line[0], film, ClockDuration.from_seconds(9))
>>> _ = graph.sync_event(line[2], film, ClockDuration.from_seconds(5))
>>> import warnings
>>> with warnings.catch_warnings(record=True) as caught:
... warnings.simplefilter("always")
... conflicts = graph.check_sync_alignment(film)
>>> len(conflicts), "runs the other way" in str(caught[0].message)
(1, True)add_articulation
add_articulation(articulation: Marking, obj: OmkObject)Places a mark on one event, with a MARKS edge from the mark to the event.
>>> from openmusickit.objects.marking import Marking
>>> from openmusickit.systems.wsmn.scoring.symbols import staccato
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d])
>>> dot = Marking(mark=staccato)
>>> graph.add_articulation(dot, c)
>>> graph.get_edge(dot, c, EdgeType.MARKS).type
<EdgeType.MARKS: 'marks'>add_annotation
add_annotation(annotation: OmkObject, obj: OmkObject)Attaches any object to another as an annotation (a memo, an analysis figure), with an ANNOTATES edge from the annotation to the object.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d])
>>> memo = OmkObject()
>>> graph.add_annotation(memo, c)
>>> graph.get_edge(memo, c, EdgeType.ANNOTATES).type
<EdgeType.ANNOTATES: 'annotates'>add_spanner
add_spanner(
spanner: Spanner,
start: SequentialEvent,
end: SequentialEvent | None = None,
)Places a spanner over the events from start to end, with STARTS_AT and ENDS_AT edges from the spanner to them.
With end=None the spanner is open: it runs from start to the end of the line, however long it grows, as a Stint with no end does. That is how a pedal held to the last bar, a rit. with no a tempo, or an una corda that is never lifted is recorded.
>>> from openmusickit.objects.marking import MarkSpanner
>>> from openmusickit.systems.wsmn.scoring.symbols import slur, una_corda
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d, e])
>>> arc = MarkSpanner(mark=slur)
>>> graph.add_spanner(arc, c, d)
>>> graph.get_edge(arc, c, EdgeType.STARTS_AT).type, graph.get_edge(arc, d, EdgeType.ENDS_AT).type
(<EdgeType.STARTS_AT: 'starts_at'>, <EdgeType.ENDS_AT: 'ends_at'>)
>>> soft = MarkSpanner(mark=una_corda)
>>> graph.add_spanner(soft, d)
>>> graph.spanner_end(soft) is None, graph.spanner_end(arc) is d
(True, True)spanner_start
spanner_start(spanner: Spanner)The event spanner starts at.
Raises
| Name | Type | Description |
|---|---|---|
| GraphError | if spanner has no STARTS_AT edge. |
spanner_end
spanner_end(spanner: Spanner)The event spanner ends at, or None for an open spanner (one that runs to the end of its line).
add_lyrics
add_lyrics(
lyrics: str | Iterable[LyricSyllable],
section: LyricSection | None = None,
after: LyricSyllable | None = None,
)Adds a line of lyric syllables, joined by NEXT edges.
A string is parsed with parse_lyrics. section, if given, is added as a Spanner over the first and last syllable. after appends the line to an existing one. Returns the syllables in order, ready for zip_lyrics_to_objects.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E, F
>>> graph = OmkGraph(GraphMeta())
>>> notes = [NoteEvent(tones={tone}) for tone in (C, D, E, F)]
>>> graph.add_line(notes)
>>> verse = LyricSection(section_type="verse", section_number=1, language="la")
>>> syllables = graph.add_lyrics("Al-le-lu-ia", section=verse)
>>> graph.zip_lyrics_to_objects(syllables[0], notes[0])
>>> " ".join(str(s) for s in syllables)
'Al - - le - - lu - - ia'
>>> graph.get_next(syllables[0]) is syllables[1], graph.get_next(syllables[-1])
(True, None)
>>> graph.get_edge(verse, syllables[0], EdgeType.STARTS_AT).type
<EdgeType.STARTS_AT: 'starts_at'>
>>> graph.get_edge(verse, syllables[-1], EdgeType.ENDS_AT).type
<EdgeType.ENDS_AT: 'ends_at'>
>>> graph.get_edge(notes[2], syllables[2], EdgeType.LYRIC).type
<EdgeType.LYRIC: 'lyric'>Appending continues the line:
>>> more = graph.add_lyrics("A-men", after=syllables[-1])
>>> graph.get_next(syllables[-1]) is more[0]
Trueconnect_lyric_to_object
connect_lyric_to_object(lyric_syllable: LyricSyllable, obj: OmkObject)Records that obj begins lyric_syllable: a LYRIC edge marks a syllable’s onset. The notes that go on sustaining it (a melisma) get no edge of their own; a sung note without one continues the previous syllable, and a rest ends it.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d, e])
>>> a, men = graph.add_lyrics("A-men")
>>> graph.connect_lyric_to_object(men, d)
>>> graph.get_edge(d, men, EdgeType.LYRIC).type
<EdgeType.LYRIC: 'lyric'>zip_lyrics_to_objects
zip_lyrics_to_objects(
start_syllable: LyricSyllable,
start_object: SequentialEvent,
)Attaches a line of syllables to a line of events, one syllable per syllable onset, until either line runs out.
Walking the events from start_object, a syllable begins on every event except: a rest; anything of zero duration (a context event or a division, which have nothing to sing, and a grace note, sung on the syllable of the note it decorates; to put a syllable on one, use connect_lyric_to_object); and an event that lies under a binding MarkSpanner (mark.binds: a slur or a tie) without starting it – those continue the syllable begun on the span’s first note. An event that ends one binding span and starts another is inside the first, so it continues rather than begins.
>>> from openmusickit.objects.marking import MarkSpanner
>>> from openmusickit.objects.note_event import Rest
>>> from openmusickit.systems.wsmn.scoring.symbols import slur, crescendo
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E, F, G
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e, rest, f, g = (NoteEvent(tones={C}), NoteEvent(tones={D}),
... NoteEvent(tones={E}), Rest(None),
... NoteEvent(tones={F}), NoteEvent(tones={G}))
>>> graph.add_line([c, d, e, rest, f, g])
>>> graph.add_spanner(MarkSpanner(mark=slur), d, e) # d-e is a melisma
>>> graph.add_spanner(MarkSpanner(mark=crescendo), f, g) # a hairpin binds nothing
>>> syllables = graph.add_lyrics("Al-le-lu-ia")
>>> graph.zip_lyrics_to_objects(syllables[0], c)
>>> for note in (c, d, e, rest, f, g):
... sung = [str(s) for s in graph._graph.successors(note, LyricSyllable, EdgeType.LYRIC)]
... print(note, sung)
NoteEvent(tones=[TonalVector((0, 0))], duration=None) ['Al -']
NoteEvent(tones=[TonalVector((1, 2))], duration=None) ['- le -']
NoteEvent(tones=[TonalVector((2, 4))], duration=None) []
Rest(duration=None) []
NoteEvent(tones=[TonalVector((3, 5))], duration=None) ['- lu -']
NoteEvent(tones=[TonalVector((4, 7))], duration=None) ['- ia']unlink_lyric_from_object
unlink_lyric_from_object(lyric_syllable: LyricSyllable, obj: OmkObject)The inverse of connect_lyric_to_object: removes the LYRIC edge from obj to the syllable.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d, e])
>>> a, men = graph.add_lyrics("A-men")
>>> graph.connect_lyric_to_object(men, d)
>>> graph.unlink_lyric_from_object(men, d)
>>> list(graph.edges_between(d, men))
[]unlink_lyric_sequence
unlink_lyric_sequence(
start_syllable: LyricSyllable,
stop_syllable: LyricSyllable | None = None,
)Unlink a sequence of lyric syllables from their associated musical objects, stopping at the specified stop syllable if provided. If no stop syllable is provided, the sequence will be unlinked until the end. The syllables stay on the graph, in their NEXT line.
>>> from openmusickit.objects.note_event import NoteEvent
>>> from openmusickit.systems.wsmn.tonal.symbols import C, D, E
>>> graph = OmkGraph(GraphMeta())
>>> c, d, e = (NoteEvent(tones={t}) for t in (C, D, E))
>>> graph.add_line([c, d, e])
>>> a, men = graph.add_lyrics("A-men")
>>> graph.zip_lyrics_to_objects(a, c)
>>> len(list(graph.edges(EdgeType.LYRIC)))
2
>>> graph.unlink_lyric_sequence(a, stop_syllable=men) # only "A" is unlinked
>>> len(list(graph.edges(EdgeType.LYRIC)))
1
>>> graph.unlink_lyric_sequence(a)
>>> list(graph.edges(EdgeType.LYRIC)), graph.get_next(a) is men
([], True)