Repository navigation
Conversation
qstamps.py moves desktop#621's quarter stamps out of its BeatTimeline (beat_quarter_lengths, _build_quarter_stamps, _build_time_stamp_table, get_quarter_stamp_by_time and get_time_by_quarter_stamp, at 0ee8ea4), with the same rules: a beat lasts its value x 4 / denominator in quarter notes, each beat's stamp is the sum of the lengths before it from 0, and times and stamps convert by interpolating between beats and extrapolating beyond them with the tempo of the nearest gap. The stamps are exact inside and floats where they meet times, so at a beat the time map gives float() of the exact stamp, as #621 does. Each MeasureRow gains qstamp, its downbeat's, and quarters, its length, both exact Fractions; quarters is computed from the meter, like the time signature. The rows of a folded table get them too, in the timeline's order. TimeMap gains qstamp(t), time_at_qstamp(q), quarters(start, end) and assumed_between(start, end). It builds its stamp table the first time a qstamp is asked for, not with the map: the index reads every file's positions and never asks, and exact sums over a long piece take a while. assumed_between reads a span as positions do (snapped, the end point excluded), and a part off the map touches the measure at that end of it, whose tempo its stamps are extrapolated with. test_timemap_qstamps.py ports #621's test_quarter_stamps.py case for case, and adds the spec's scenarios. test_timemap_shared.py now compares every shared example's quarter stamps, at each beat and on each row. Part of TimeLineAnnotator/project-management#215 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TimeMap gains position(t, unit), length(start, end, unit) and unit_seconds(t, unit), with unit "bar" or "beat": the prototype's position, length, beat_seconds and bar_seconds (ql_timemap.py). A position runs from 0 at the first beat, a bar counts 1 whatever its metre and the fraction inside it counts beats, and the closing barline ends the last bar. A length is the difference of two positions, rounded to a millionth, and None when either end is off the map; an end on the closing barline is on it, a start isn't. unit_seconds is in the file's unit. Both ends are snapped first, as for positions. A unit other than "bar" or "beat" gives None and logs an error, rather than counting beats as the prototype does: no file can give one, only a caller. The prototype's distance assertions in test_timemap_prototype.py now pass, unchanged, so their strict xfail mark is gone. test_timemap_distances.py adds the spec's scenarios: eight bars at any tempo, a 3/4 bar inside 4/4, how long a beat and a bar last, the media's end, and a quarters file. Part of TimeLineAnnotator/project-management#215 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
assumed_between picked its measures as positions read a span: snapped, the end point excluded, an end before the start in the start's bar, and past the last beat the last bar. quarters() reads none of these ways, so a length could count an assumed meter that the flag missed: - past the last beat, quarters() extrapolates with the last gap's tempo, which is the bar before's when the last bar has a single tap; - an end before the start still counts every bar between the two; - an end that snaps onto a downbeat still counts the time after it. beats_between, beside the conversions in qstamps.py, now gives the beats whose lengths the quarter notes between two times count, extrapolating as quarter_stamp_by_time does, and assumed_between checks their measures. A test checks over a grid of spans, with and without a one-tap last bar, that wherever the flag says no, another meter for the assumed bar leaves quarters() unchanged. quarters() is rounded to a millionth, as length() is: under 1/3 units, two beats 8 quarter notes apart gave 7.999999999999999. The stamp table is cached in the instance's __dict__ rather than with functools.cached_property, which on Python 3.10 and 3.11 holds one lock for every instance, so maps building their first table waited for each other. Part of TimeLineAnnotator/project-management#215 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
🤖 Claude A review of this PR before anyone else's found that
The description is updated: what changed, the test counts, and the guarantee table. One question is left for the plan: TimeLineAnnotator/project-management#325, the length of a last beat whose value differs from the beat before it. Its answer may change |
azfoo
added a commit
that referenced
this pull request
Oct 19, 2026
Copied from tilia/timelines/beat/units.py (desktop#621 at e5da86a), unchanged: the file is the same blob as there (2ef3be7). Since 0ee8ea4, the commit copied before, parse_units refuses a fraction whose denominator, in lowest terms, is above MAX_DENOMINATOR (128), with the message of any unreadable unit. Whole numbers are unchanged. Sums of units, such as a row's ts_numerator and the running quarter stamps of #747 (quarter stamps), then have denominators that divide one fixed number, where units such as 1/(10**29 + i) made them grow without limit. The new cases of #621's tests are ported. Part of TimeLineAnnotator/project-management#247 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
azfoo
added a commit
that referenced
this pull request
Oct 19, 2026
Copied from tilia/timelines/beat/units.py (desktop#621 at e5da86a), unchanged: the file is the same blob as there (2ef3be7). Since 0ee8ea4, the commit copied before, parse_units refuses a fraction whose denominator, in lowest terms, is above MAX_DENOMINATOR (128), with the message of any unreadable unit. Whole numbers are unchanged. Sums of units, such as a row's ts_numerator and the running quarter stamps of #747 (quarter stamps), then have denominators that divide one fixed number, where units such as 1/(10**29 + i) made them grow without limit. The new cases of #621's tests are ported. Part of TimeLineAnnotator/project-management#247 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Claude
The time map learns quarter notes, and distances and lengths in bars and beats.
What changed
Quarter stamps (first and third commits).
tilia_core/timemap/qstamps.pymoves the quarter stamps of #621 (beat units and time signatures) out of itsBeatTimeline, at0ee8ea45:beat_quarter_lengths,_build_quarter_stamps,_build_time_stamp_table,get_quarter_stamp_by_timeandget_time_by_quarter_stamp, with the same rules.Fractions inside and floats where they meet times, so at a beatTimeMap.qstampgivesfloat()of the exact stamp, as feat: beat patterns, beat units, time signatures and quarter stamps #621 does.MeasureRowgains two exact fields, which a folded table's rows get too:qstamp, its downbeat's stamp;quarters, its length, computed from the meter like the time signature.TimeMapgains four methods:qstamp(t)andtime_at_qstamp(q);quarters(start, end): the difference of two qstamps, rounded to a millionth aslength()is;assumed_between(start, end): whether an assumed meter went intoquarters(start, end).To answer that,
qstamps.pygainsbeats_between, which gives the beats whose lengths a span's quarter notes count. It sits beside the conversions and extrapolates the same way:The map builds its stamp table the first time a qstamp is asked for, not when the map itself is built. The index reads every file's positions and never asks for qstamps, and exact sums over a long piece take a while. The table is cached in the instance's
__dict__rather than withfunctools.cached_property, which on Python 3.10 and 3.11 holds one lock shared by every instance.On a synthetic 93,000-beat timeline the map takes 0.55 s to build, against 0.44 s on
v1-prerelease(best of 7, Python 3.11). The difference comes from the rows' exactqstampandquarters. Building the stamp table with the map made it 0.73 s.Distances and lengths (second commit).
TimeMap.position(t, unit),length(start, end, unit)andunit_seconds(t, unit), withuniteither"bar"or"beat", port the prototype'sposition,length,beat_secondsandbar_seconds(ql_timemap.py).Nonewhen either end is off the map. An end on the closing barline is on the map; a start there isn't.unit_secondsis in the file's unit.Noneand logs an error. The prototype counts beats for any unit other than"bar", but no file can give another unit: only a caller can.Tests
test_timemap_qstamps.pyports feat: beat patterns, beat units, time signatures and quarter stamps #621'stest_quarter_stamps.pycase for case, and adds the spec's User Story 3, scenarios 1–3 and 5.assumed_betweensays no, giving the assumed bar another meter must leavequarters()unchanged.test_timemap_distances.pyadds scenarios 4 and 6: eight bars at any tempo, a 3/4 bar inside 4/4, how long a beat and a bar last, the end of the media, and a quarters file.test_timemap_shared.pyno longer skipstest_qstamps. It compares the quarter stamps of all 18 shared examples, at every beat and on every row, without changing any expected value.test_timemap_prototype.py: the 7 tests markedneeds_distancespass unchanged, so the mark and its definition are gone.Open question
The last beat's length when its value differs from the beat before's (an uneven pattern, or a last bar under a unit of its own) is open as TimeLineAnnotator/project-management#325. Until it is answered:
For such a last bar,
quarters(row.start, row.end)differs from the row'squarters(1.5 against 1 under 2+3 with a one-tap last bar). No migrated file has such a last beat.Tests run
uv sync --all-packagesuv run --package tilia-core pytest packages/tilia-core -q(Python 3.11): 878 passed, 3 skipped, 1 xfailed. The xfail isneeds_find_time_map, which feat(core): choose a document's time map by role #733 (choosing the time map) removes.uv run --isolated --python 3.10 --package tilia-core pytest packages/tilia-core -q: 881 passed, 1 xfailed.python scripts/check_packages.py(Python 3.14): all package checks passed.Guarantees, and what removing their code did
For each guarantee, its code was removed or neutralised, the time map's tests (
tests/timemap: 400 tests and 1 xfail, on Python 3.11) were run, and the code was restored.test_triplets_are_exact, shared "1/3 over four taps"test_past_last_beat_keeps_last_gap_tempoqstampis the sum of the measures before itquartersis the sum of its beats' lengthsquartersequal to the numeratortest_setting_a_beat_unit_changes_every_qstamp_after_itisfinitechecktest_conversions_reuse_one_tablefunctools.cached_propertytest_first_qstamps_on_two_maps_dont_wait_for_each_otherquarters()is rounded to a millionthassumed_between: past the last beat, the last gap's beatassumed_between: an end before the start covers the bars betweenassumed_between: an end exactly on a beat takes nothing from itbisectforbisect_leftassumed_between: not snapped, as qstamps aren'tassumed_between: False for a missing timeunit_secondsof a bar runs from its downbeat to the nextunit_secondssnaps firstNone, loggedUntested, or tested only in part:
cached_propertyhas no lock, so the test passes either way.Other open PRs on the same files
#733 (choosing the time map) changes
table.py,timemap.pyandtest_timemap_prototype.pytoo. Lookups (TimeLineAnnotator/project-management#218) will changetimemap.pyandfind_rows. Whichever merges later rebases. Intest_timemap_prototype.py, this PR removesneeds_distancesand #733 removesneeds_find_time_map, and each rewords the comment on the other's mark. Whoever rebases drops both marks and both comments.__init__.pyand its__all__are left alone. Nothing new needs exporting once the others have landed: the new names are methods ofTimeMapand fields ofMeasureRow, which are already exported.qstamps.py's functions stay internal, as the contract lists none of them.Part of TimeLineAnnotator/project-management#215
🤖 Generated with Claude Code