Source code for music21.tree.trees

# -----------------------------------------------------------------------------
# Name:         tree/trees.py
# Purpose:      Subclasses of tree.core.AVLTree for different purposes
#
# Authors:      Joséphine Wolf Oberholtzer
#               Michael Scott Asato Cuthbert
#
# Copyright:    Copyright © 2013-2016 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# -----------------------------------------------------------------------------
'''
Tools for grouping elements, timespans, and especially
pitched elements into kinds of searchable tree organized by start and stop offsets
and other positions.
'''
from __future__ import annotations

from math import inf
import unittest
import weakref

from music21 import common
from music21 import environment
from music21 import exceptions21
from music21.sorting import SortTuple

from music21.tree import core
from music21.tree import node as nodeModule

environLocal = environment.Environment('tree.trees')

INFINITY = inf
NEGATIVE_INFINITY = -inf

# -----------------------------------------------------------------------------


class ElementTreeException(exceptions21.TreeException):
    pass

# -----------------------------------------------------------------------------


[docs] class ElementTree(core.AVLTree): r''' A data structure for efficiently storing a score: flat or recursed or normal. This data structure has no connection to the XML ElementTree. This data structure stores ElementNodes: objects which implement both a `position` and `endTime` property. It provides fast lookups of such objects. >>> et = tree.trees.ElementTree() >>> et <ElementTree {0} (-inf to inf)> >>> s = stream.Stream() >>> for i in range(100): ... n = note.Note() ... n.duration.quarterLength = 2.0 ... s.insert(i * 2, n) >>> for n in s: ... et.insert(n) >>> et <ElementTree {100} (0.0 <0.20...> to 200.0)> >>> et.rootNode <ElementNode: Start:126.0 <0.20...> Indices:(l:0 *63* r:100) Payload:<music21.note.Note C>> >>> n2 = s[-1] These operations are very fast >>> et.index(n2, n2.sortTuple()) 99 Get a position after a certain position: >>> st = s[40].sortTuple() >>> st SortTuple(atEnd=0, offset=80.0, priority=0, classSortOrder=20, isNotGrace=1, insertIndex=...) >>> st2 = et.getPositionAfter(st) >>> st2.shortRepr() '82.0 <0.20...>' >>> st2.offset 82.0 >>> st3 = et.getPositionAfter(5.0) >>> st3.offset 6.0 >>> et.getPositionAfter(4.0).offset 6.0 ''' # TYPING # rootNode: nodeModule.ElementNode|None # CLASS VARIABLES # nodeClass = nodeModule.ElementNode __slots__ = ( '_source', 'parentTrees', ) def __init__(self, elements=None, source=None): super().__init__() self.parentTrees = weakref.WeakSet() self._source = None if elements and elements is not None: self.insert(elements) self.source = source # Special Methods # def __contains__(self, element): # noinspection PyShadowingNames r''' Is true when the ElementTree contains the object within it If element.sortTuple(self.source) returns the right information, it's a fast O(log n) search. If not his is an O(n log n) operation in python not C, so slow. >>> score = tree.examples.makeExampleScore() >>> scoreTree = score.asTree(flatten=True) >>> lastNote = score.flatten().notes[-1] >>> lastNote in scoreTree True >>> n = note.Note('E--') >>> n in scoreTree False >>> s = stream.Stream(id='tinyStream') >>> s.insert(0, n) >>> st = s.asTree(flatten=False) >>> n in st True ''' s = self.source sourcePosition = element.sortTuple(s) # might be wrong if element not in s or s is None nodeAtPosition = self.getNodeByPosition(sourcePosition) if nodeAtPosition is not None: if nodeAtPosition.payload is element: return True # not found, do slow search. for pl in self: if pl is element: return True return False
[docs] def __eq__(self, expr): r''' Two ElementTrees are equal only if they are the same object. >>> et1 = tree.trees.ElementTree() >>> et2 = tree.trees.ElementTree() >>> et3 = et1 >>> et1 == et2 False >>> et1 == et3 True >>> et2 != et1 True ''' return self is expr
[docs] def __getitem__(self, i): # noinspection PyShadowingNames r''' Gets elements by integer index or slice. This is pretty fast in computational time (O(log n)), but it's O(log n) in Python while normal list slicing is O(1) in C, so don't use trees for __getitem__ searching if you don't have to. >>> score = tree.examples.makeExampleScore() >>> scoreTree = score.asTree(flatten=True) >>> scoreTree <ElementTree {20} (0.0 <0.-25...> to 8.0) <music21.stream.Score exampleScore>> >>> scoreTree[0] <music21.instrument.Instrument 'PartA: : '> >>> scoreTree[-1] <music21.bar.Barline type=final> >>> scoreTree[2000] is None True Slices work >>> scoreTree[2:5] [<music21.clef.BassClef>, <music21.clef.BassClef>, <music21.meter.TimeSignature 2/4>] >>> scoreTree[-6:-3] [<music21.note.Note A>, <music21.note.Note B>, <music21.note.Note D#>] >>> scoreTree[-100:-200] [] >>> for x in scoreTree[:]: ... x <music21.instrument.Instrument 'PartA: : '> ... <music21.bar.Barline type=final> These should all be the same as the flat version: >>> scoreFlat = score.flatten() >>> for i in (0, -1, 10): ... if scoreFlat[i] is not scoreTree[i]: ... print('false!') >>> for i, j in ((2, 5), (-6, -3)): ... sfSlice = scoreFlat[i:j] ... for n in range(i, j): ... sliceOffset = n - i ... if sfSlice[sliceOffset] is not scoreFlat[n]: ... print('false!') ''' try: nodeOrNodeList = self.getNodeByIndex(i) except IndexError: return None if nodeOrNodeList is None: return nodeOrNodeList elif not isinstance(nodeOrNodeList, list): return nodeOrNodeList.payload else: return [n.payload for n in nodeOrNodeList]
def __hash__(self): return hash((type(self), id(self))) def __len__(self): r''' Gets the length of the ElementTree, i.e., the number of elements enclosed. This is a very very fast O(1). >>> score = tree.examples.makeExampleScore() >>> scoreTree = score.asTree(flatten=True) >>> len(scoreTree) 20 Works well on OffsetTrees also, which are more complex, because they can have multiple elements per Node. >>> offTree = tree.trees.OffsetTree() >>> len(offTree) 0 >>> tsList = [(0, 2), (0, 9), (1, 1), (2, 3), (3, 4), ... (4, 9), (5, 6), (5, 8), (6, 8), (7, 7)] >>> noteList = [note.Note() for _ in tsList] >>> for i,n in enumerate(noteList): ... n.offset, n.quarterLength = tsList[i] >>> offTree.insert(noteList) >>> len(offTree) 10 >>> len(offTree) == len(noteList) True >>> offTree.removeElements(noteList) >>> len(offTree) 0 ''' if self.rootNode is None: return 0 return self.rootNode.subtreeElementsStopIndex def __repr__(self): o = self.source pos = self.lowestPosition() endTime = self.endTime if hasattr(pos, 'shortRepr'): # sortTuple pos = pos.shortRepr() if hasattr(endTime, 'shortRepr'): # sortTuple endTime = endTime.shortRepr() className = type(self).__name__ lenEnclosed = '{' + str(len(self)) + '}' msg = f'<{className} {lenEnclosed} ({pos} to {endTime})' if o is not None: msg += f' {o!r}' msg += '>' return msg def __setitem__(self, i, new): # noinspection PyShadowingNames r''' Sets elements at index `i` to `new`, but keeping the old position of the element there. (This is different from OffsetTrees, where things can move around). >>> score = tree.examples.makeExampleScore() >>> scoreTree = score.asTree(flatten=True) >>> n = scoreTree[10] >>> n <music21.note.Note G#> >>> scoreTree.getNodeByIndex(10) <ElementNode: Start:2.0 <0.20...> Indices:(l:10 *10* r:11) Payload:<music21.note.Note G#>> >>> scoreTree[10] = note.Note('F#') >>> scoreTree[10] <music21.note.Note F#> >>> scoreTree.getNodeByIndex(10) <ElementNode: Start:2.0 <0.20...> Indices:(l:10 *10* r:11) Payload:<music21.note.Note F#>> >>> scoreTree[10:13] [<music21.note.Note F#>, <music21.note.Note F>, <music21.note.Note G>] >>> scoreTree[10:14:2] = [note.Note('E#'), note.Note('F-')] >>> scoreTree[10:13] [<music21.note.Note E#>, <music21.note.Note F>, <music21.note.Note F->] ''' if isinstance(i, int): n = self.getNodeByIndex(i) if n is None: message = f'Index must be less than {len(self)}' raise TypeError(message) n.payload = new elif isinstance(i, slice): if not isinstance(new, list): message = f'If {i} is a slice, then {new} must be a list' raise TypeError(message) sliceLen = (i.stop - i.start) / i.step if sliceLen != len(new): message = f'{i} is a slice of len {sliceLen}, so {new} cannot have len {len(new)}' raise TypeError(message) for j, sliceIter in enumerate(range(i.start, i.stop, i.step)): self[sliceIter] = new[j] # recursive. else: message = f'Indices must be ints or slices, got {i}' raise TypeError(message) def __str__(self): ''' Print the whole contents of the tree. Slow: O(n log n) time, but it's just for debugging >>> score = tree.examples.makeExampleScore() >>> scoreTree = score.asTree(flatten=True) >>> print(scoreTree) <ElementTree {20} (0.0 <0.-25...> to 8.0) <music21.stream.Score exampleScore>> PartA: : PartB: : <music21.clef.BassClef> <music21.clef.BassClef> <music21.meter.TimeSignature 2/4> <music21.meter.TimeSignature 2/4> <music21.note.Note C> <music21.note.Note C#> <music21.note.Note D> <music21.note.Note E> <music21.note.Note G#> <music21.note.Note F> <music21.note.Note G> <music21.note.Note E#> <music21.note.Note A> <music21.note.Note B> <music21.note.Note D#> <music21.note.Note C> <music21.bar.Barline type=final> <music21.bar.Barline type=final> ''' result = [] result.append(repr(self)) for x in self: subResult = str(x).splitlines() subResult = ['\t' + x for x in subResult] result.extend(subResult) result = '\n'.join(result) return result def __iter__(self): r''' Iterates through all the nodes in the offset tree and returns each node's payload Not an especially efficient way of using this beautiful tree object! But useful for debugging or a final iteration for conversion. >>> score = tree.examples.makeExampleScore() >>> scoreTree = score.asTree(flatten=True) >>> for x in scoreTree: ... print(x) PartA: : PartB: : <music21.clef.BassClef> <music21.clef.BassClef> <music21.meter.TimeSignature 2/4> <music21.meter.TimeSignature 2/4> <music21.note.Note C> <music21.note.Note C#> ... ''' for node in self.iterNodes(): yield node.payload # PRIVATE METHODS # def _updateNodes(self, initialPosition=None, initialEndTime=None, visitedParents=None): ''' runs updateIndices and updateEndTimes on the rootNode and if the offset or endTime of the tree differs from `initialPosition` or `initialEndTime` will run _updateParents() as well. Called by insert() and remove(). ''' if self.rootNode is not None: self.rootNode.updateIndices() self.rootNode.updateEndTimes() if (self.lowestPosition() != initialPosition or self.endTime != initialEndTime): self._updateParents(initialPosition, visitedParents=visitedParents) def _updateParents(self, oldPosition, visitedParents=None): ''' Tells all parents that the position of this tree has changed. Not currently used. ''' if visitedParents is None: visitedParents = set() for parent in self.parentTrees: if parent is None or parent in visitedParents: continue visitedParents.add(parent) parent._removeElementAtPosition(self, oldPosition) # Trees don't have offsets currently raise NotImplementedError # # pylint: disable=all # parent._insertCore(self.offset, self) # # parent._updateNodes(parentPosition, visitedParents=visitedParents) def _removeElementAtPosition(self, element, position): ''' removes an element or ElementTree from a position (either its current .offset or its oldPosition) without updating the indices, endTimes, etc. ''' node = self.getNodeByPosition(position) if node is None: return if isinstance(node.payload, list): # OffsetTree if element in node.payload: node.payload.remove(element) if not node.payload: self.removeNode(position) else: if node.payload is element: node.payload = None if node.payload is None: self.removeNode(position) # PUBLIC METHODS #
[docs] def getPositionFromElementUnsafe(self, el): ''' A quick but dirty method for getting the likely position (or offset) of an element within the elementTree from the element itself. Such as calling el.getOffsetBySite(tree.source) or something like that. Pulled out for subclassing ''' return el.sortTuple(self.source)
[docs] def populateFromSortedList(self, listOfTuples): # noinspection PyShadowingNames ''' This method assumes that the current tree is empty (or will be wiped) and that listOfTuples is a non-empty list where the first element is a unique position to insert, and the second is the complete payload for that node, and that the positions are strictly increasing in order. This is about an order of magnitude faster (3ms vs 21ms for 1000 items; 31 vs. 30ms for 10,000 items) than running createNodeAtPosition() for each element in a list if it is already sorted. Thus, it should be used when converting a Stream where .isSorted is True into a tree. If any of the conditions is not true, expect to get a dangerously badly sorted tree that will be useless. >>> bFlat = corpus.parse('bwv66.6').flatten() >>> bFlat.isSorted True >>> listOfTuples = [(e.sortTuple(bFlat), e) for e in bFlat] >>> listOfTuples[14] (SortTuple(atEnd=0, offset=0.0, priority=0, ...), <music21.key.Key of f# minor>) >>> et = tree.trees.ElementTree() >>> et.rootNode is None True >>> et.populateFromSortedList(listOfTuples) >>> et.rootNode <ElementNode: Start:14.5 <0.20...> Indices:(l:0 *99* r:199) Payload:<music21.note.Note A>> >>> n = et.rootNode >>> while n is not None: ... print(n) ... n = n.leftChild <ElementNode: Start:14.5 <0.20...> Indices:(l:0 *99* r:199) Payload:<music21.note.Note A>> <ElementNode: Start:5.5 <0.20...> Indices:(l:0 *49* r:99) Payload:<music21.note.Note A>> <ElementNode: Start:0.0 <0.20...> Indices:(l:0 *24* r:49) Payload:<music21.note.Note A>> <ElementNode: Start:0.0 <0.1...> Indices:(l:0 *12* r:24) Payload:<music21.tempo.MetronomeMark Quarter=96 (playback only)>> <ElementNode: Start:0.0 <0.0...> Indices:(l:0 *6* r:12) Payload:<music21.clef.TrebleClef>> <ElementNode: Start:0.0 <0.-25...> Indices:(l:0 *3* r:6) Payload:<music21.instrument.Instrument 'P3: Tenor: Instrument 3'>> <ElementNode: Start:0.0 <0.-25...> Indices:(l:0 *1* r:3) Payload:<music21.instrument.Instrument 'P1: Soprano: Instrument 1'>> <ElementNode: Start:0.0 <0.-30...> Indices:(l:0 *0* r:1) Payload:<music21.metadata.Metadata object at 0x104adbdd8>> >>> n = et.rootNode >>> while n is not None: ... print(n) ... n = n.rightChild <ElementNode: Start:14.5 <0.20...> Indices:(l:0 *99* r:199) Payload:<music21.note.Note A>> <ElementNode: Start:25.0 <0.20...> Indices:(l:100 *149* r:199) Payload:<music21.note.Note G#>> <ElementNode: Start:31.0 <0.20...> Indices:(l:150 *174* r:199) Payload:<music21.note.Note B>> <ElementNode: Start:34.0 <0.20...> Indices:(l:175 *187* r:199) Payload:<music21.note.Note D>> <ElementNode: Start:35.0 <0.20...> Indices:(l:188 *193* r:199) Payload:<music21.note.Note A#>> <ElementNode: Start:36.0 <0.-5...> Indices:(l:194 *196* r:199) Payload:<music21.bar.Barline type=final>> <ElementNode: Start:36.0 <0.-5...> Indices:(l:197 *198* r:199) Payload:<music21.bar.Barline type=final>> ''' def recurse(subListOfTuples, globalStartOffset) -> core.AVLNode|None: ''' Divide and conquer. ''' lenL = len(subListOfTuples) if lenL == 0: return None midpoint = lenL // 2 midtuple = subListOfTuples[midpoint] n = NodeClass(midtuple[0], midtuple[1]) n.payloadElementIndex = globalStartOffset + midpoint n.subtreeElementsStartIndex = globalStartOffset n.subtreeElementsStopIndex = globalStartOffset + lenL n.leftChild = recurse(subListOfTuples[:midpoint], globalStartOffset) n.rightChild = recurse(subListOfTuples[midpoint + 1:], globalStartOffset + midpoint + 1) n.update() return n NodeClass = self.nodeClass self.rootNode = recurse(listOfTuples, 0)
[docs] def getNodeByIndex(self, i): ''' Get a node whose element is at a particular index (not position). Works with slices too See __getitem__ for caveats about speed. >>> score = tree.examples.makeExampleScore() >>> scoreTree = score.asTree(flatten=True) >>> scoreTree <ElementTree {20} (0.0 <0.-25...> to 8.0) <music21.stream.Score exampleScore>> >>> scoreTree.getNodeByIndex(0) <ElementNode: Start:0.0 <0.-25...> Indices:(l:0 *0* r:2) Payload:<music21.instrument.Instrument 'PartA: : '>> >>> scoreTree.getNodeByIndex(-1) <ElementNode: Start:End <0.-5...> Indices:(l:19 *19* r:20) Payload:<music21.bar.Barline type=final>> >>> scoreTree.getNodeByIndex(slice(2, 5)) [<ElementNode: Start:0.0 <0.0...> Indices:(l:0 *2* r:4) Payload:<music21.clef.BassClef>>, <ElementNode: Start:0.0 <0.0...> Indices:(l:3 *3* r:4) Payload:<music21.clef.BassClef>>, <ElementNode: Start:0.0 <0.4...> Indices:(l:0 *4* r:8) Payload:<music21.meter.TimeSignature 2/4>>] >>> scoreTree.getNodeByIndex(slice(-6, -3)) [<ElementNode: Start:5.0 <0.20...> Indices:(l:9 *14* r:20) Payload:<music21.note.Note A>>, <ElementNode: Start:6.0 <0.20...> Indices:(l:15 *15* r:17) Payload:<music21.note.Note B>>, <ElementNode: Start:6.0 <0.20...> Indices:(l:16 *16* r:17) Payload:<music21.note.Note D#>>] >>> scoreTree.getNodeByIndex(slice(-100, -200)) [] ''' def recurseByIndex(node, index): ''' Return the node element at a given index ''' if node.payloadElementIndex == index: return node elif node.leftChild and index < node.payloadElementIndex: return recurseByIndex(node.leftChild, index) elif node.rightChild and node.payloadElementIndex <= index: return recurseByIndex(node.rightChild, index) def recurseBySlice(node, start, stop): ''' Return a slice of the nodes (plural) whose indices are between start <= index < stop. ''' result = [] if node is None: return result if start < node.payloadElementIndex and node.leftChild: result.extend(recurseBySlice(node.leftChild, start, stop)) if start <= node.payloadElementIndex < stop: result.append(node) if node.payloadElementIndex < stop and node.rightChild: result.extend(recurseBySlice(node.rightChild, start, stop)) return result if isinstance(i, int): if self.rootNode is None: raise IndexError if i < 0: i = self.rootNode.subtreeElementsStopIndex + i if i < 0 or self.rootNode.subtreeElementsStopIndex <= i: raise IndexError return recurseByIndex(self.rootNode, i) elif isinstance(i, slice): if self.rootNode is None: return [] indices = i.indices(self.rootNode.subtreeElementsStopIndex) outer_start, outer_stop = indices[0], indices[1] return recurseBySlice(self.rootNode, outer_start, outer_stop) else: raise TypeError(f'Indices must be integers or slices, got {i}')
[docs] def iterNodes(self): ''' Identical to the iterating on a core.AVLTree -- yields each node in order Slow: O(n log n) time so don't make this your main thing. >>> score = tree.examples.makeExampleScore() >>> scoreTree = score.asTree(flatten=True) >>> scoreTree <ElementTree {20} (0.0 <0.-25...> to 8.0) <music21.stream.Score exampleScore>> >>> for node in scoreTree.iterNodes(): ... print(node) <ElementNode: Start:0.0 <0.-25...> Indices:(l:0 *0* r:2) Payload:<music21.instrument.Instrument 'PartA: : '>> <ElementNode: Start:0.0 <0.-25...> Indices:(l:1 *1* r:2) Payload:<music21.instrument.Instrument 'PartB: : '>> <ElementNode: Start:0.0 <0.0...> Indices:(l:0 *2* r:4) Payload:<music21.clef.BassClef>> <ElementNode: Start:0.0 <0.0...> Indices:(l:3 *3* r:4) Payload:<music21.clef.BassClef>> <ElementNode: Start:0.0 <0.4...> Indices:(l:0 *4* r:8) Payload:<music21.meter.TimeSignature 2/4>> <ElementNode: Start:0.0 <0.4...> Indices:(l:5 *5* r:6) Payload:<music21.meter.TimeSignature 2/4>> <ElementNode: Start:0.0 <0.20...> Indices:(l:5 *6* r:8) Payload:<music21.note.Note C>> <ElementNode: Start:0.0 <0.20...> Indices:(l:7 *7* r:8) Payload:<music21.note.Note C#>> <ElementNode: Start:1.0 <0.20...> Indices:(l:0 *8* r:20) Payload:<music21.note.Note D>> <ElementNode: Start:2.0 <0.20...> Indices:(l:9 *9* r:11) Payload:<music21.note.Note E>> ... <ElementNode: Start:7.0 <0.20...> Indices:(l:15 *17* r:20) Payload:<music21.note.Note C>> <ElementNode: Start:End <0.-5...> Indices:(l:18 *18* r:20) Payload:<music21.bar.Barline type=final>> <ElementNode: Start:End <0.-5...> Indices:(l:19 *19* r:20) Payload:<music21.bar.Barline type=final>> ''' # py 3 only # yield from super().__iter__() for n in super().__iter__(): yield n
[docs] def index(self, element, position=None): # noinspection PyShadowingNames r''' Gets index of `element` in tree. position could be none. If the element is in the original score, then it should be very fast (O(log n)) >>> score = tree.examples.makeExampleScore() >>> scoreFlat = score.flatten() >>> n = scoreFlat.notes[-1] >>> flatTree = scoreFlat.asTree() >>> flatTree.index(n) 17 If it's not in the original stream, then it should be slower than doing it on a stream (O (n log n)). >>> scoreTree = score.asTree(flatten=True) >>> n = score.flatten().notes[-1] >>> scoreTree.index(n) 17 And if it's nowhere at all, you get a ValueError! >>> scoreTree.index(note.Note('F-')) Traceback (most recent call last): ValueError: <music21.note.Note F-> not in Tree at position SortTuple(atEnd=0, offset=0.0, priority=0, ...). ''' if position is None: position = self.getPositionFromElementUnsafe(element) node = self.getNodeByPosition(position) if node is None or node.payload is not element: for i, n in enumerate(self): if n is element: return i raise ValueError(f'{element} not in Tree at position {position}.') return node.payloadElementIndex
def _getPositionsFromElements(self, elements): ''' takes a list of elements and returns a list of positions. In an ElementTree, this will be a list of .sortTuple() calls. In an OffsetTree, this will be a list of .offset calls ''' return [self.getPositionFromElementUnsafe(el) for el in elements]
[docs] def insert(self, positionsOrElements, elements=None): r''' Inserts elements or `Timespans` into this tree. >>> n = note.Note() >>> ot = tree.trees.OffsetTree() >>> ot <OffsetTree {0} (-inf to inf)> >>> ot.insert(10.0, n) >>> ot <OffsetTree {1} (10.0 to 11.0)> >>> n2 = note.Note('D') >>> n2.offset = 20 >>> n3 = note.Note('E') >>> n3.offset = 5 >>> ot.insert([n2, n3]) >>> ot <OffsetTree {3} (5.0 to 21.0)> ''' initialPosition = self.lowestPosition() initialEndTime = self.endTime if elements is None: elements = positionsOrElements positions = None else: positions = positionsOrElements if not common.isListLike(positions) or hasattr(positions, 'shortRepr'): # is not a list and not a sortTuple positions = [positions] if (not common.isListLike(elements) and not isinstance(elements, (set, frozenset))): # not a list. a single element or timespan elements = [elements] if positions is None: positions = self._getPositionsFromElements(elements) for i, el in enumerate(elements): pos = positions[i] self._insertCore(pos, el) self._updateNodes(initialPosition, initialEndTime)
def _insertCore(self, position, el): ''' Inserts a single element at an offset, creating new nodes as necessary, but does not updateIndices or updateEndTimes or updateParents ''' self.createNodeAtPosition(position) node = self.getNodeByPosition(position) node.payload = el
[docs] def highestPosition(self): r''' Gets the latest position in this tree. Keep as a property, because a similar property exists on streams. >>> score = corpus.parse('bwv66.6') >>> tsTree = score.asTimespans(classList=(note.Note,)) >>> tsTree.highestPosition() 35.0 ''' def recurse(node): if node.rightChild is not None: return recurse(node.rightChild) else: return node.position if self.rootNode is not None: return recurse(self.rootNode) else: return NEGATIVE_INFINITY
[docs] def lowestPosition(self): r''' Gets the earliest position in this tree. >>> score = tree.examples.makeExampleScore() >>> elTree = score.asTree() >>> elTree.lowestPosition().shortRepr() '0.0 <0.-20...>' >>> tsTree = score.asTimespans() >>> tsTree.lowestPosition() 0.0 ''' def recurse(node): if node.leftChild is not None: return recurse(node.leftChild) return node.position if self.rootNode is not None: return recurse(self.rootNode) else: return NEGATIVE_INFINITY
# PROPERTIES # @property def source(self): ''' the original stream. (stored as a weakref but returned unwrapped) >>> example = tree.examples.makeExampleScore() >>> eTree = example.asTree() >>> eTree.source is example True >>> s = stream.Stream() >>> eTree.source = s >>> eTree.source is s True ''' return common.unwrapWeakref(self._source) @source.setter def source(self, expr): # uses weakrefs so that garbage collection on the stream cache is possible self._source = common.wrapWeakref(expr) @property def endTime(self): r''' Gets the latest stop position in this element-tree. This is cast as a property so that it can be used like a TimeSpan in a TimeSpanTree >>> score = corpus.parse('bwv66.6') >>> tsTree = score.asTree() >>> tsTree.endTime 36.0 Returns infinity if no elements exist: >>> et = tree.trees.ElementTree() >>> et.endTime inf ''' if self.rootNode is not None: return self.rootNode.endTimeHigh return INFINITY
# ---------------------------------------------------------------
[docs] class OffsetTree(ElementTree): ''' A tree representation where positions are offsets in the score and each node has a payload which is a list of elements at that offset (unsorted by sort order). ''' __slots__ = () # TYPING # rootNode: nodeModule.OffsetNode|None nodeClass = nodeModule.OffsetNode # SPECIAL METHODS # def __contains__(self, element): r''' Is true when the ElementTree contains the object within it; TRUE IF and ONLY if the .offset of the element matches the position in the tree -- thus it is very fast! >>> score = tree.examples.makeExampleScore() >>> scoreTree = score.asTree(flatten=True, groupOffsets=True) >>> score.flatten()[5] in scoreTree True Note that this way of finding an item won't work because the offset is different from the flat offset: >>> n = score.parts[0].measure(2).notes[1] >>> n <music21.note.Note F> >>> n.offset 1.0 >>> n in scoreTree False ''' try: offset = element.offset except AttributeError as ae: raise ElementTreeException( 'element must be a Music21Object, i.e., must have offset' ) from ae candidates = self.elementsStartingAt(offset) if element in candidates: return True else: return False
[docs] def __getitem__(self, i): r''' Gets elements by integer index or slice. >>> score = tree.examples.makeExampleScore() >>> scoreTree = score.asTree(flatten=True, groupOffsets=True) >>> scoreTree[0] <music21.instrument.Instrument 'PartA: : '> >>> scoreTree[-1] <music21.bar.Barline type=final> >>> scoreTree[2:5] [<music21.clef.BassClef>, <music21.clef.BassClef>, <music21.meter.TimeSignature 2/4>] >>> scoreTree[-6:-3] [<music21.note.Note A>, <music21.note.Note B>, <music21.note.Note D#>] >>> scoreTree[-100:-200] [] ''' def recurseByIndex(node, index): ''' Return the payload element at a given index ''' if node.payloadElementsStartIndex <= index < node.payloadElementsStopIndex: return node.payload[index - node.payloadElementsStartIndex] elif node.leftChild and index < node.payloadElementsStartIndex: return recurseByIndex(node.leftChild, index) elif node.rightChild and node.payloadElementsStopIndex <= index: return recurseByIndex(node.rightChild, index) def recurseBySlice(node: nodeModule.OffsetNode, start, stop): ''' Return a slice of the payload elements (plural) where start <= index < stop. ''' result = [] if node is None: return result if start < node.payloadElementsStartIndex and node.leftChild: result.extend(recurseBySlice(node.leftChild, start, stop)) if start < node.payloadElementsStopIndex and node.payloadElementsStartIndex < stop: indexStart = start - node.payloadElementsStartIndex indexStart = max(indexStart, 0) indexStop = stop - node.payloadElementsStartIndex result.extend(node.payload[indexStart:indexStop]) if node.payloadElementsStopIndex <= stop and node.rightChild: result.extend(recurseBySlice(node.rightChild, start, stop)) return result if isinstance(i, int): if self.rootNode is None: raise IndexError if i < 0: i = self.rootNode.subtreeElementsStopIndex + i if i < 0 or self.rootNode.subtreeElementsStopIndex <= i: raise IndexError return recurseByIndex(self.rootNode, i) elif isinstance(i, slice): if self.rootNode is None: return [] indices = i.indices(self.rootNode.subtreeElementsStopIndex) outer_start, outer_stop = indices[0], indices[1] return recurseBySlice(self.rootNode, outer_start, outer_stop) else: raise TypeError(f'Indices must be integers or slices, got {i}')
# def __setitem__(self, i, new): # r''' # Sets elements or timespans at index `i` to `new`. # TODO: this should be a bit different for OffsetTrees, probably more like ElementTrees # >>> tss = [ # ... tree.spans.Timespan(0, 2), # ... tree.spans.Timespan(0, 9), # ... tree.spans.Timespan(1, 1), # ... ] # >>> tsTree = tree.timespanTree.TimespanTree() # >>> tsTree.insert(tss) # >>> tsTree[0] = tree.spans.Timespan(-1, 6) # >>> for x in tsTree: # ... x # <Timespan -1.0 6.0> # <Timespan 0.0 9.0> # <Timespan 1.0 1.0> # Note however, that calling __getitem__ after __setitem__ will not return # what you just set if the timing is wrong. This is different from the # behavior on ElementTree which assumes that the new element wants to be # at the old element's offset. # >>> tsTree[2] = tree.spans.Timespan(-0.5, 4) # >>> tsTree[2] # <Timespan 0.0 9.0> # >>> for x in tsTree: # ... x # <Timespan -1.0 6.0> # <Timespan -0.5 4.0> # <Timespan 0.0 9.0> # Works with slices too. # >>> tsTree[1:] = [tree.spans.Timespan(10, 20)] # >>> for x in tsTree: # ... x # <Timespan -1.0 6.0> # <Timespan 10.0 20.0> # ''' # raise NotImplementedError # # pylint: disable=all # if isinstance(i, (int, slice)): # old = self[i] # self.removeTimespan(old) # self.insert(new) # else: # message = f'Indices must be ints or slices, got {i}' # raise TypeError(message) def __iter__(self): r''' Iterates through all the nodes in the offset tree and returns each thing in the payload. Not an especially efficient way of using this beautiful tree object. >>> score = tree.examples.makeExampleScore() >>> scoreTree = score.asTree(flatten=True, groupOffsets=True) >>> for x in scoreTree: ... print(x) PartA: : PartB: : <music21.clef.BassClef> <music21.clef.BassClef> <music21.meter.TimeSignature 2/4> <music21.meter.TimeSignature 2/4> <music21.note.Note C> <music21.note.Note C#> ... ''' for node in self.iterNodes(): for el in node.payload: yield el # ---------static methods ------------------------
[docs] @staticmethod def elementEndTime(el, node): ''' Use so that both OffsetTrees, which have elements which do not have a .endTime, and TimespanTrees, which have element that have an .endTime but not a duration, can use most of the same code. ''' return node.position + el.duration.quarterLength
# ---------public methods ------------------------
[docs] def getPositionFromElementUnsafe(self, el): ''' A quick but dirty method for getting the likely position (or offset) of an element within the elementTree from the element itself. Such as calling el.getOffsetBySite(tree.source) or something like that. Pulled out for subclassing ''' return el.offset
[docs] def append(self, el): ''' Add an element to the end, making certain speed savings. ''' initialPosition = self.lowestPosition() # will only change if is empty endTime = self.endTime if endTime == INFINITY: endTime = 0 self._insertCore(endTime, el) self._updateNodes(initialPosition, initialEndTime=None)
@staticmethod def _insertCorePayloadSortKey(x): ''' Payload sorting is done the old-fashioned way, because the number of elements at a single offset should be few enough that it is not a problem ''' return x.sortTuple()[2:] # cut off atEnd and offset def _insertCore(self, position, el): ''' Inserts a single element at an offset, creating new nodes as necessary, but does not updateIndices or updateEndTimes or updateParents ''' self.createNodeAtPosition(position) node = self.getNodeByPosition(position) node.payload.append(el) node.payload.sort(key=self._insertCorePayloadSortKey)
[docs] def copy(self): # noinspection PyShadowingNames r''' Creates a new tree with the same payload as this tree. This is analogous to `dict.copy()`. Much, much faster than creating a new tree; creating one with 3600 items took 500ms. Creating the tree the first time was 40 seconds, so about an 80x speedup. >>> score = tree.examples.makeExampleScore() >>> scoreTree = score.asTimespans() >>> newTree = scoreTree.copy() >>> newTree <TimespanTree {20} (0.0 to 8.0) <music21.stream.Score exampleScore>> >>> scoreTree[16] <PitchedTimespan (6.0 to 8.0) <music21.note.Note D#>> >>> newTree[16] <PitchedTimespan (6.0 to 8.0) <music21.note.Note D#>> >>> scoreTree[16] is newTree[16] True ''' newTree = type(self)() # this is just as efficient as ._insertCore, since it's given a list. newTree.insert(list(self)) newTree.source = self.source newTree.parentTrees = self.parentTrees.copy() return newTree
[docs] def elementsStartingAt(self, position): r''' Finds elements or timespans in this tree which start at `position`. >>> score = corpus.parse('bwv66.6') >>> scoreTree = score.asTimespans() >>> for timespan in scoreTree.elementsStartingAt(0.5): ... timespan ... <PitchedTimespan (0.5 to 1.0) <music21.note.Note B>> <PitchedTimespan (0.5 to 1.0) <music21.note.Note B>> <PitchedTimespan (0.5 to 1.0) <music21.note.Note G#>> ''' results = [] node = self.getNodeByPosition(position) if node is not None: if isinstance(node.payload, list): results.extend(node.payload) elif node.payload is not None: results.append(node.payload) return tuple(results)
[docs] def elementsStoppingAt(self, offset): r''' Finds elements in this OffsetTree which stop at `offset`. Elements are ordered according to (start) offset. >>> score = corpus.parse('bwv66.6') >>> scoreTree = score.asTree(flatten=True, groupOffsets=True) >>> for el in scoreTree.elementsStoppingAt(0.5): ... el <music21.note.Note C#> <music21.note.Note A> <music21.note.Note A> Works also on timespans for TimespanTrees: >>> scoreTree = score.asTimespans() >>> for el in scoreTree.elementsStoppingAt(0.5): ... el <PitchedTimespan (0.0 to 0.5) <music21.note.Note C#>> <PitchedTimespan (0.0 to 0.5) <music21.note.Note A>> <PitchedTimespan (0.0 to 0.5) <music21.note.Note A>> ''' def recurse(node): result = [] if node is not None: # could happen in an empty TimespanTree if node.endTimeLow <= offset <= node.endTimeHigh: if node.leftChild is not None: result.extend(recurse(node.leftChild)) for el in node.payload: if self.elementEndTime(el, node) == offset: result.append(el) if node.rightChild is not None: result.extend(recurse(node.rightChild)) return result results = recurse(self.rootNode) return tuple(results)
[docs] def elementsOverlappingOffset(self, offset): r''' Finds elements in this ElementTree which overlap `offset`. >>> score = corpus.parse('bwv66.6') >>> scoreTree = score.asTree(flatten=True, groupOffsets=True) >>> for el in scoreTree.elementsOverlappingOffset(0.5): ... el ... <music21.note.Note E> Works with Timespans in TimespanTrees as well. >>> scoreTree = score.asTimespans() >>> for el in scoreTree.elementsOverlappingOffset(0.5): ... el ... <PitchedTimespan (0.0 to 1.0) <music21.note.Note E>> ''' def recurse(node): result = [] # collections.deque() if node is not None: if node.position < offset < node.endTimeHigh: result.extend(recurse(node.leftChild)) # This currently requires timespans not elements, and list payloads # TODO: Fix/disambiguate. for el in node.payload: if offset < self.elementEndTime(el, node): result.append(el) result.extend(recurse(node.rightChild)) elif offset <= node.position: result.extend(recurse(node.leftChild)) return result results = recurse(self.rootNode) return tuple(results)
[docs] def removeElements(self, elements, offsets=None, runUpdate=True): r''' Removes `elements` which can be Music21Objects or Timespans (a single one or a list) from this Tree. Much safer (for non-timespans) if a list of offsets is used, but it is optional. If runUpdate is False then the tree will be left with incorrect indices and endTimes; but it can speed up operations where an element is going to be removed and then immediately replaced: i.e., where the position of an element has changed. ''' initialPosition = self.lowestPosition() initialEndTime = self.endTime if hasattr(elements, 'offset'): # a music21 object or an PitchedTimespan elements = [elements] if offsets is not None and not common.isListLike(offsets): offsets = [offsets] if offsets is not None and len(elements) != len(offsets): raise ElementTreeException( 'Number of elements and number of offsets must be the same') for i, el in enumerate(elements): if offsets is not None: self._removeElementAtPosition(el, offsets[i]) else: self._removeElementAtPosition(el, el.offset) if runUpdate: self._updateNodes(initialPosition, initialEndTime)
[docs] def allOffsets(self): r''' Gets all unique offsets of all timespans in this offset-tree. >>> score = corpus.parse('bwv66.6') >>> tsTree = score.asTimespans() >>> for offset in tsTree.allOffsets()[:10]: ... offset ... 0.0 0.5 1.0 2.0 3.0 4.0 5.0 5.5 6.0 6.5 ''' def recurse(node): result = [] if node is not None: if node.leftChild is not None: result.extend(recurse(node.leftChild)) pos = node.position if isinstance(pos, SortTuple): result.append(pos.offset) else: result.append(pos) if node.rightChild is not None: result.extend(recurse(node.rightChild)) return result return tuple(recurse(self.rootNode))
[docs] def allTimePoints(self): r''' Gets all unique offsets (both starting and stopping) of all elements/timespans in this offset-tree. >>> score = corpus.parse('bwv66.6') >>> scoreTree = score.asTimespans() >>> for offset in scoreTree.allTimePoints()[:10]: ... offset ... 0.0 0.5 1.0 2.0 3.0 4.0 5.0 5.5 6.0 6.5 ''' def recurse(node): result = set() if node is not None: if node.leftChild is not None: result.update(recurse(node.leftChild)) result.add(node.position) result.update(node.payloadEndTimes()) if node.rightChild is not None: result.update(recurse(node.rightChild)) return result return tuple(sorted(recurse(self.rootNode)))
[docs] def overlapTimePoints(self, includeStopPoints=False, returnVerticality=False): ''' Gets all time-points where some element is starting (or if includeStopPoints is True, where some element is starting or stopping) while some other element is still continuing onward. >>> score = corpus.parse('bwv66.6') >>> scoreOffsetTree = score.asTree(flatten=True, groupOffsets=True) >>> scoreOffsetTree.overlapTimePoints() [0.5, 5.5, 6.5, 10.5, 13.5, 14.5, 15.5...] if returnVerticality is True, then a mapping of time point to elements is returned. How cool is that? >>> otp = scoreOffsetTree.overlapTimePoints(returnVerticality=True) >>> otp[0] {0.5: <music21.tree.verticality.Verticality 0.5 {G#3 B3 E4 B4}>} ''' checkPoints = self.allOffsets() if includeStopPoints is False else self.allTimePoints() overlaps = [] for cp in checkPoints: overlappingElements = self.elementsOverlappingOffset(cp) if not overlappingElements: continue if returnVerticality is False: overlaps.append(cp) else: overlaps.append({cp: self.getVerticalityAt(cp)}) return overlaps
[docs] def getVerticalityAt(self, offset): r''' Gets the verticality in this offset-tree which starts at `offset`. >>> bach = corpus.parse('bwv66.6') >>> scoreTree = bach.asTimespans() >>> scoreTree.getVerticalityAt(2.5) <music21.tree.verticality.Verticality 2.5 {G#3 B3 E4 B4}> Verticalities outside the range still return a Verticality, but it might be empty: >>> scoreTree.getVerticalityAt(2000) <music21.tree.verticality.Verticality 2000 {}> Test that it still works if the tree is empty: >>> scoreTree = bach.asTimespans(classList=(instrument.Tuba,)) >>> scoreTree <TimespanTree {0} (-inf to inf) <music21.stream.Score ...>> >>> scoreTree.getVerticalityAt(5.0) <music21.tree.verticality.Verticality 5.0 {}> Returns a verticality.Verticality object. ''' from music21.tree.verticality import Verticality startTimespans = self.elementsStartingAt(offset) stopTimespans = self.elementsStoppingAt(offset) overlapTimespans = self.elementsOverlappingOffset(offset) verticality = Verticality( overlapTimespans=overlapTimespans, startTimespans=startTimespans, offset=offset, stopTimespans=stopTimespans, timespanTree=self, ) return verticality
[docs] def simultaneityDict(self): ''' Creates a dictionary of offsets that have more than one element starting at that time, where the keys are offset times and the values are lists of elements at that moment. >>> score = tree.examples.makeExampleScore() >>> scoreTree = score.asTree(flatten=True, groupOffsets=True) >>> scoreTree <OffsetTree {20} (0.0 to 8.0) <music21.stream.Score exampleScore>> >>> sd = scoreTree.simultaneityDict() >>> len(sd) 5 >>> list(sorted(sd.keys())) [0.0, 2.0, 4.0, 6.0, 8.0] >>> sd[0.0] [<music21.instrument.Instrument 'PartA: : '>, <music21.instrument.Instrument 'PartB: : '>, <music21.clef.BassClef>, <music21.clef.BassClef>, <music21.meter.TimeSignature 2/4>, <music21.meter.TimeSignature 2/4>, <music21.note.Note C>, <music21.note.Note C#>] >>> sd[2.0] [<music21.note.Note E>, <music21.note.Note G#>] ''' simultaneityDict = {} for node in self.iterNodes(): pl = node.payload if len(pl) > 1: simultaneityDict[node.position] = pl[:] return simultaneityDict
# --------------------------------------------------------------- # ----------------------------------------------------------------------------- class Test(unittest.TestCase): def testGetPositionAfterOffset(self): ''' test that get position after works with an offset when the tree is built on SortTuples. ''' from music21 import note from music21 import stream et = ElementTree() s = stream.Stream() for i in range(100): n = note.Note() n.duration.quarterLength = 2.0 s.insert(i * 2, n) for n in s: et.insert(n) self.assertTrue(repr(et).startswith('<ElementTree {100} (0.0 <0.20')) n2 = s[-1] self.assertEqual(et.index(n2, n2.sortTuple()), 99) st3 = et.getPositionAfter(5.0) self.assertIsNotNone(st3) # def testBachDoctest(self): # from music21 import corpus, note, chord, tree # bach = corpus.parse('bwv66.6') # tree = tree.fromStream.asTimespans(bach, flatten=True, # classList=(note.Note, chord.Chord)) # for verticalities in tree.iterateVerticalitiesNwise(n=3): # print(verticalities) # if verticalities[-1].offset == 25: # pass # horizontalities = tree.unwrapVerticalities(verticalities) # for unused_part, horizontality in horizontalities.items(): # if horizontality.hasNeighborTone: # merged = horizontality[0].new(endTime=horizontality[2].endTime,) # # tree.remove(horizontality[0]) # # tree.remove(horizontality[1]) # # tree.remove(horizontality[2]) # # tree.insert(merged) # # # newBach = tree.toStream.partwise(tree, templateStream=bach,) # newBach.parts[1].measure(7).show('text') # # {0.0} <music21.chord.Chord F#4> # # {1.5} <music21.chord.Chord F#3> # # {2.0} <music21.chord.Chord C#4> # def testElementsStoppingAt(self): ''' this was reporting: <music21.note.Note G#> <music21.note.Note C#> <music21.note.Note A> <music21.note.Note A> G# was coming from an incorrect activeSite. activeSite should not be used! ''' from music21 import corpus from music21 import stream from music21 import note s = stream.Stream() n0 = note.Note('A') n0.duration.quarterLength = 3.0 s.insert(0, n0) n1 = note.Note('B') n1.duration.quarterLength = 2.0 s.insert(1, n1) n2 = note.Note('C') n2.duration.quarterLength = 1.0 s.insert(2, n2) # and one later to be sure that order is right n3 = note.Note('A#') n3.duration.quarterLength = 2.5 s.insert(0.5, n3) st = s.asTree(groupOffsets=True) stList = st.elementsStoppingAt(3.0) self.assertEqual(len(stList), 4) self.assertEqual([n.name for n in stList], ['A', 'A#', 'B', 'C']) # making the tree more complex does not change anything, I hope? for i in range(30): s.insert(0, note.Rest()) for i in range(22): s.insert(10 + i, note.Rest()) st = s.asTree(groupOffsets=True) stList = st.elementsStoppingAt(3.0) self.assertEqual(len(stList), 4) self.assertEqual([n.name for n in stList], ['A', 'A#', 'B', 'C']) # real world example score = corpus.parse('bwv66.6') scoreTree = score.asTree(flatten=True, groupOffsets=True) elementList = scoreTree.elementsStoppingAt(0.5) self.assertEqual(len(elementList), 3) self.assertEqual(elementList[0].name, 'C#') self.assertEqual(elementList[1].name, 'A') self.assertEqual(elementList[2].name, 'A') # def testBachDoctest(self): # from music21 import corpus, note, chord, tree # bach = corpus.parse('bwv66.6') # scoreTree = tree.fromStream.asTimespans(bach, flatten=True, # classList=(note.Note, chord.Chord)) # print(scoreTree) # for verticalities in scoreTree.iterateVerticalitiesNwise(n=3): # if verticalities[-1].offset == 25: # pass # horizontalities = scoreTree.unwrapVerticalities(verticalities) # for unused_part, horizontality in horizontalities.items(): # if horizontality.hasNeighborTone: # merged = horizontality[0].new(endTime=horizontality[2].endTime,) # scoreTree.remove(horizontality[0]) # scoreTree.remove(horizontality[1]) # scoreTree.remove(horizontality[2]) # scoreTree.insert(merged) # # # newBach = tree.toStream.partwise(scoreTree, templateStream=bach,) # newBach.show() # newBach.parts[1].measure(7).show('text') # # {0.0} <music21.chord.Chord F#4> # # {1.5} <music21.chord.Chord F#3> # # {2.0} <music21.chord.Chord C#4> # ----------------------------------------------------------------------------- _DOC_ORDER = ( ElementTree, OffsetTree, ) # ----------------------------------------------------------------------------- if __name__ == '__main__': import music21 music21.mainTest(Test) # , runTest='testElementsStoppingAt')