From 509f6aec6562312f908dc2ef8556c71f905e7820 Mon Sep 17 00:00:00 2001 From: hill Date: Mon, 2 Nov 2026 08:41:28 +0100 Subject: [PATCH 1/5] Remove MeterCore._reprInternal ProtoM21Object comes before MeterCore in the MRO of both subclasses, and each subclass defines its own _reprInternal, so this one never ran. --- music21/meter/core.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/music21/meter/core.py b/music21/meter/core.py index df9bc39d2b..6d8a22890e 100644 --- a/music21/meter/core.py +++ b/music21/meter/core.py @@ -118,9 +118,6 @@ def duration(self) -> FrozenDuration: ''' return self._durationFromNumeratorDenominator(self.numerator, self.denominator) - def _reprInternal(self): - return str(self) - def ratioEqual(self, other) -> bool: ''' Compare the numerator and denominator of another object. From 35f6b1e7994e68ea2b72bf3a3352260e051a0c4d Mon Sep 17 00:00:00 2001 From: hill Date: Mon, 2 Nov 2026 08:42:08 +0100 Subject: [PATCH 2/5] Set summedNumerator with MeterSequence.modify The setter rebuilt the display sequence through makeSequence and the private _partition; modify(summedNumerator=value) does the same through the public method. --- music21/meter/base.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/music21/meter/base.py b/music21/meter/base.py index 673e750b87..cef1115500 100644 --- a/music21/meter/base.py +++ b/music21/meter/base.py @@ -35,7 +35,7 @@ from music21.meter.tools import slashToTuple, proportionToFraction from music21.meter.core import ( - MeterSequence, getMeterSequence, getMeterTerminal, makeSequence, + MeterSequence, getMeterSequence, getMeterTerminal, ) environLocal = environment.Environment('meter') @@ -1176,11 +1176,7 @@ def summedNumerator(self) -> bool: @summedNumerator.setter def summedNumerator(self, value: bool): if self.displaySequence is not None: - # displaySequence is immutable; swap in a copy with the new flag - ds = self.displaySequence - self.displaySequence = makeSequence( - ds.numerator, ds.denominator, ds._partition, - parenthesis=ds.parenthesis, summedNumerator=value) + self.displaySequence = self.displaySequence.modify(summedNumerator=value) # -------------------------------------------------------------------------- # private methods -- most to be put into the various sequences. From 879ebe1dd8423e16f67e02aec3479feb169796c2 Mon Sep 17 00:00:00 2001 From: hill Date: Mon, 2 Nov 2026 08:43:05 +0100 Subject: [PATCH 3/5] Weight the default accents in one rebuild _setDefaultAccentWeights called setIndex once per accent. Each call copied and hashed the whole partition and left an intermediate sequence in _meterSequenceCache, so a meter with n accents did O(n^2) work and cached n sequences that nothing uses. Build the weighted terminals in a list and call modify(partition=...) once. Accent, beat and beam sequences for 153 meters match master (apart from the int/float default weight this branch already changes). --- music21/meter/base.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/music21/meter/base.py b/music21/meter/base.py index cef1115500..ab2ed1edcf 100644 --- a/music21/meter/base.py +++ b/music21/meter/base.py @@ -1337,14 +1337,13 @@ def _setDefaultAccentWeights(self, depth: int = 3) -> None: weightValues[x + 1] = weightValueMin * pow(2, x) # set weights on accent partitions - self.accentSequence = self.accentSequence.partition([1] * accentCount) - for i in range(accentCount): - # get values from weightValues dictionary; terminals are - # immutable, so replace each with a re-weighted cached one - mt = self.accentSequence[i] - self.accentSequence = self.accentSequence.setIndex( - i, getMeterTerminal( - mt.numerator, mt.denominator, weight=weightValues[weightInts[i]])) + accentSequence = self.accentSequence.partition([1] * accentCount) + weighted = [] + for i, mt in enumerate(accentSequence): + # get values from weightValues dictionary + weighted.append(getMeterTerminal( + mt.numerator, mt.denominator, weight=weightValues[weightInts[i]])) + self.accentSequence = accentSequence.modify(partition=weighted) # -------------------------------------------------------------------------- # access data for other processing From 3f99b5a0a0a4d2ae4b50ab10742d982f7265509f Mon Sep 17 00:00:00 2001 From: hill Date: Mon, 2 Nov 2026 08:48:00 +0100 Subject: [PATCH 4/5] Walk frames directly in FrozenObject._check_init inspect.stack() reads the source file and line for every frame, which made each attribute set inside FrozenDuration.__init__ take milliseconds. With MeterTerminal.duration returning a FrozenDuration, the first TimeSignature of each meter took about 30 ms here (master: 1.5 ms). Following f_back finds the same __init__/__new__/__setstate__ frame without reading files: about 6 ms, and music21/meter tests run in 2 s instead of 11 s. --- music21/common/objects.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/music21/common/objects.py b/music21/common/objects.py index f6993e92fb..43fc7dc169 100644 --- a/music21/common/objects.py +++ b/music21/common/objects.py @@ -295,11 +295,13 @@ def _check_init(self, key: str|None = None) -> bool: if not getattr(self, 'frozen', True): return True - for st in inspect.stack(): - if (st.frame.f_code.co_name in ('__init__', '__new__', '__setstate__') - and 'self' in st.frame.f_locals - and st.frame.f_locals['self'].__class__ == self.__class__): + frame = inspect.currentframe() + while frame is not None: + if (frame.f_code.co_name in ('__init__', '__new__', '__setstate__') + and 'self' in frame.f_locals + and frame.f_locals['self'].__class__ == self.__class__): return True + frame = frame.f_back raise TypeError(f'This {self.__class__.__name__} instance is immutable.') def __setattr__(self, key: str, value: t.Any) -> None: From 36b6ee6518fc5144aea4eb30949449c3b3c95ba2 Mon Sep 17 00:00:00 2001 From: hill Date: Mon, 2 Nov 2026 08:50:37 +0100 Subject: [PATCH 5/5] Rebuild the tree once per level in subdivideNestedHierarchy _subdivideNestedByPaths replaced one path at a time through setIndex, so each replacement copied and hashed every sequence above it, and cached each intermediate root. For n beats that is O(n^2) per level. Collect the replacements and rebuild only the sequences above them, once. subdivideNestedHierarchy gives the same sequences and weights for 108 cases (12 meters, depths 1-4, both normalizeDenominators settings, and a beatSequence as firstPartitionForm). --- music21/meter/core.py | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/music21/meter/core.py b/music21/meter/core.py index 6d8a22890e..64b189488f 100644 --- a/music21/meter/core.py +++ b/music21/meter/core.py @@ -2250,19 +2250,26 @@ def _getAtPath(root: MeterSequence, path: tuple[int, ...]) -> MeterNode: return node -def _replaceAtPath(root: MeterSequence, path: tuple[int, ...], newNode) -> MeterSequence: +def _replaceAtPaths( + node: MeterNode, + replacements: dict[tuple[int, ...], MeterNode], + ancestors: set[tuple[int, ...]], + path: tuple[int, ...] = (), +) -> MeterNode: ''' - Return a new root MeterSequence with the node at `path` replaced by - `newNode`, rebuilding each ancestor functionally. + Return `node` with each node at a path in `replacements` replaced, rebuilding + only the sequences in `ancestors`, the paths above a replacement. AI-assisted (Claude). ''' - idx = path[0] - if len(path) == 1: - return root.setIndex(idx, newNode) - child = t.cast('MeterSequence', root[idx]) - newChild = _replaceAtPath(child, path[1:], newNode) - return root.setIndex(idx, newChild) + if path in replacements: + return replacements[path] + if path not in ancestors: + return node + sequence = t.cast('MeterSequence', node) + newPartition = [_replaceAtPaths(child, replacements, ancestors, path + (i,)) + for i, child in enumerate(sequence)] + return sequence._rebuild(newPartition) def _subdivideNestedByPaths( @@ -2271,20 +2278,22 @@ def _subdivideNestedByPaths( divisions, ) -> tuple[MeterSequence, list[tuple[int, ...]]]: ''' - Subdivide (equally) each node reached by a path in `paths`, rebuilding the - root functionally, and return the new root plus the list of paths to the - newly created child nodes. + Subdivide each node at `paths` equally. No path may lie inside another. + Return the new root and the paths to the new child nodes. AI-assisted (Claude). ''' + replacements: dict[tuple[int, ...], MeterNode] = {} newPaths: list[tuple[int, ...]] = [] for p in paths: node = t.cast('MeterSequence', _getAtPath(root, p)) newNode = node.subdividePartitionsEqual(divisions) - root = _replaceAtPath(root, p, newNode) + replacements[p] = newNode for j in range(len(newNode)): newPaths.append(p + (j,)) - return root, newPaths + ancestors = {p[:k] for p in paths for k in range(len(p))} + newRoot = t.cast('MeterSequence', _replaceAtPaths(root, replacements, ancestors)) + return newRoot, newPaths if __name__ == '__main__':