Skip to content

humdrum: remove unneeded Regexes, fix partial-beam direction#1944

Merged
mscuthbert merged 1 commit into
masterfrom
humdrum-kern-regex
Jun 22, 2026
Merged

humdrum: remove unneeded Regexes, fix partial-beam direction#1944
mscuthbert merged 1 commit into
masterfrom
humdrum-kern-regex

Conversation

@mscuthbert

Copy link
Copy Markdown
Member

Third and last PR broken out from #1706

Reduce the number of regex searches needed to parse a humdrum scope -- Chopin moves from 6000 to 3400. Simplify the code and make it more elegant.

But barely moves on performance. hdStringToNote speeds up by 5% 8.59 µs/call to 8.14. Full parse speeds up by about 1%

Performance/cleanup, with identical output:

  • accidentals: count '#'/'-' directly (the count is the alteration) and build a pitch.Accidental, instead of two re.search calls;
  • rational durations: only run the \d+%\d+ search when '%' is present;
  • pitch: only search when the event is not a rest (walrus in the elif).

Also, from ideas in PR #1706 (Alexander Morgan): comment out the phrase-mark, slur, and arpeggiation blocks that only ran pass, and rename the unused beaming loop variable from i to _.

AI-assisted (Claude). Read closely by Myke -- co-author Alexander Morgan

…ringToNote

Bug fix: a lowercase `k` is a partial beam to the *left* and an uppercase `K` to
the *right*, but both were appended as `'right'`.  `k` now produces a left
partial beam.  Bumps the beta version since this changes kern parsing output
(and so invalidates pickled-stream caches).

Performance/cleanup, with identical output:

- accidentals: count '#'/'-' directly (the count is the alteration) and build a
  pitch.Accidental, instead of two `re.search` calls;
- rational durations: only run the `\d+%\d+` search when '%' is present;
- pitch: only search when the event is not a rest (walrus in the elif).

This cuts re.search calls for a mazurka6 parse from 5974 to 3435 (about -40%).

Also, from ideas in PR #1706 (Alexander Morgan): comment out the phrase-mark,
slur, and arpeggiation blocks that only ran `pass`, and rename the unused
beaming loop variable from `i` to `_`.

AI-assisted (Claude).
@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 93.103% (+0.002%) from 93.101% — humdrum-kern-regex into master

@mscuthbert mscuthbert merged commit a59c66c into master Jun 22, 2026
7 checks passed
@mscuthbert mscuthbert deleted the humdrum-kern-regex branch June 22, 2026 01:11
# than allocating a copy; one that has its own duration replaces it below.
thisObject: note.GeneralNote
if 'r' in contents:
thisObject = note.Rest(duration=defaultDurationOrNone)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if you don't want to factor out duration evaluation to its own function, I would still recommend doing it before anything else in this hdStringToNote function. The reason is that, e.g. for the case of rests here on 2366, you're still creating an initial duration that will just get replaced with the actual duration when you calculate it at the end of the function. So that's still 2 duration objects per rest which might account for the speedup being a little less than what I measured (10-12%). But that will increase the diff size quite a bit.

Comment thread music21/humdrum/tests.py
@@ -82,6 +82,15 @@ def testSingleNote(self):
self.assertEqual(b.duration.dots, 0)
self.assertEqual(b.duration.tuplets[0].durationNormal.dots, 2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had added these 4 tests and I would recommend keeping them to cover the new functionality. I had to add KernSpine.parse() because the new version of the KernSpine class got rid of the __init__ constructor, so some of the class properties were only available if I called .parse() first.
If you don't like calling .parse() there's probably a different way to run these tests, but I would definitely keep the tests in some form.

    def testGraceNote(self):
        ks = KernSpine()
        ks.parse()
        # noinspection SpellCheckingInspection
        a = ks.processNoteEvent('4Cq')
        self.assertEqual(a.duration.type, 'quarter')
        self.assertEqual(a.duration.slash, True)

    def testGraceNote2(self):
        ks = KernSpine()
        ks.parse()
        # noinspection SpellCheckingInspection
        a = ks.processNoteEvent('16Cqq')
        self.assertEqual(a.duration.type, '16th')
        self.assertEqual(a.duration.slash, False)

    def testChord(self):
        ks = KernSpine()
        ks.parse()
        # noinspection SpellCheckingInspection
        c = ks.processChordEvent('8C 8E')
        self.assertEqual(len(c.notes), 2)
        self.assertEqual(c.notes[0].duration, c.duration)

    def testChord2(self):
        # noinspection SpellCheckingInspection
        ks = KernSpine()
        ks.parse()
        c = ks.processChordEvent('8C E')
        self.assertEqual(c.notes[0].duration, c.notes[1].duration)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants