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
True

remove_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)
False

correspondence

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}
True

diff

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 differences

add_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]
True

add_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]
True

insert_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))
1

Raises

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]
True

connect_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']