Developers’ Reference
This page is the canonical reference for contributing to TimeToAlign!. The conceptual model lives in Concepts and the per-class API lives under API Reference; everything you need to build, test, document, and release the library is here.
The package is named timetoalign for machines (imports, PyPI) and written TimeToAlign! for humans (prose, headings, comments).
1. Repository Layout
The git repository is a multi-project workspace; the library itself lives in timetoalign/.
tta/ # Repository root
├── timetoalign/ # The Python library — the focus of this doc
│ ├── timetoalign/ # Importable package
│ ├── tests/ # pytest suite (mirrors the package layout)
│ ├── docs/ # Documentation source
│ │ ├── page/ # Quarto site (this site)
│ │ ├── tuto-notebooks/ # Tutorial notebooks (jupytext .py + paired .ipynb)
│ │ └── howto-notebooks/ # How-to notebooks (jupytext .py + paired .ipynb)
│ ├── pyproject.toml # PEP 621 project metadata + ruff/coverage/pytest config
│ ├── tox.ini # Build/test/lint/publish orchestrator
│ ├── .pre-commit-config.yaml # Pinned formatter/linter versions
│ ├── .github/workflows/ # CI (release-please)
│ └── CHANGELOG.md # Auto-generated by release-please — DO NOT edit by hand
├── tta_article/ # TISMIR manuscript (LaTeX, submitted) — out of scope here
├── pyMeasureMap/ # Sibling project; standalone
└── dashboard/ # Sibling project; standalone
Sibling projects (tta_article/, pyMeasureMap/, dashboard/) have their own build infrastructure and are not governed by the rules below.
2. Architecture Overview
2.1 Subpackages
The importable package timetoalign/ is organised into focused subpackages, each with a single responsibility:
| Subpackage | Responsibility |
|---|---|
core/ |
Foundational types: enums (Domain, TimeUnit, …), Coordinate / IdCoordinate, TimeStamp, IdGenerator; core/fields.py contains the SemanticField bases plus the Arrow translator/builder. |
display/ |
ASCII rendering helpers for timelines, groups, bundles, flows. |
maps/ |
ConversionMap family (LinearMap, TableMap, ChainMap, PiecewiseMap, …). |
timelines/ |
Timeline classes for all six domain × modality combinations, TimelineGroup, regions, flow control, beat grids. |
alignment/ |
MatchClaim, AlignmentAnchor, AlignmentBundle, MatchGraph, MatchLine, WarpMap. |
storage/ |
PyArrow-backed EventData / EventStore storage primitives. |
loader/ |
File-format-specific ingestion. |
2.2 Import Direction (MANDATORY)
Subpackages are arranged in strict layers. Imports may only flow downward, from a higher-numbered layer to a lower-numbered one. This is a hard rule: violations create circular-import landmines and are refused at review.
┌──────────────────────────────┐
Layer 5 ──▶ │ loader │
└──────────────┬───────────────┘
│ may import from layers 0–4
┌──────────────▼───────────────┐
Layer 4 ──▶ │ alignment │
└──────────────┬───────────────┘
│ may import from layers 0–3
┌──────────────▼───────────────┐
Layer 3 ──▶ │ timelines │
└──────────────┬───────────────┘
│ may import from layers 0–2
┌──────────────▼───────────────┐
Layer 2 ──▶ │ storage │
└──────────────┬───────────────┘
│ may import from layers 0–1
┌──────────────▼───────────────┐
Layer 1 ──▶ │ maps │
└──────────────┬───────────────┘
│ may import from layer 0
┌──────────────▼───────────────┐
Layer 0 ──▶ │ core display │
└──────────────────────────────┘
(no internal dependencies)
Equivalently:
| Subpackage | May import from |
|---|---|
core/ |
nothing internal |
display/ |
nothing internal at runtime (TYPE_CHECKING only) |
maps/ |
core |
storage/ |
lower layers as needed; owns EventData and EventStore |
timelines/ |
core, maps, storage |
alignment/ |
lower layers, including timelines |
loader/ |
all lower layers |
Storage ownership and timeline dependencies
storage/ is a first-class layer that owns EventData and EventStore, the PyArrow-backed event storage used by timelines and loaders. Timeline instances therefore import storage types at runtime as part of the normal layering:
from timetoalign.storage import EventDataThere is no timelines–loader import cycle: loader/ is the upper layer and imports downward to construct storage-backed timelines. Runtime imports from timelines/ to storage/ are legitimate dependencies; keep all imports flowing from a higher layer to a lower one.
2.3 Stable Architectural Invariants
These invariants are tested at the unit level and enforced at review. If you find yourself wanting to break one, raise it as a discussion before coding.
- Coordinate resolution returns a shared stamp type.
TimeStamp,GroupTimestamp, andMatchStampimplement the commonStampinterface; each exposes raw and unit-bearing coordinate access consistently. CoordinateandIdCoordinateare the canonical input types for every method that takes a coordinate value. Raw numbers are accepted for ergonomics; raw numbers are not acceptable as return types — every getter returns aCoordinate.ConversionMapproperties belong to a singleTimeline. They are not graph edges between timelines.MatchGraphis on-demand and analytical. It is built from claims when needed, not maintained as a system-wide structure.- Loaders use a strict two-phase contract.
loader.load(*sources)ingests files;loader.create_*()produces domain objects and never takes file paths. Thefrom_file()classmethod is sugar for the common case. - All enums live in
core/enums.pyand subclassFancyStrEnum. Member names are lowercase. The single documented exception isNumberType, whose values are Pythontypeobjects. - Timeline IDs are systematic:
clt/dlt/cpt/dpt/cgt/dgtprefixes for the six domain × modality combinations, with optional role prefixes (score:clt1,perf:dlt1).
For the full conceptual rationale see Concepts and the Glossary.
2.4 Architectural decision log
The invariants below are load-bearing design rules. They are not auto-discoverable from any single docstring; new code is expected to respect them, and breaking changes are expected to be discussed and recorded here.
Pydantic-derived schemas + value projectors. Every scalar (Coordinate, Duration, SpecificPitch, Note, Measure, the pitch / harmony hierarchies, …) is a pydantic BaseModel subclass with model_config = ConfigDict(frozen=True). The corresponding PyArrow pa.StructType is derived once at class-definition time via derive_arrow_struct(model_cls) (see timetoalign/core/fields.py). Computed pydantic fields are deliberately excluded from the derived struct. Individual fields may be denormalised or dropped via register_value_projector(model_cls, field_name, projector): for example, Coordinate.value projects to {value, numerator, denominator} so rationals survive Parquet, and Note.pitch is dropped entirely so pitch lives in its own field (midi_pitch / specific_pitch) rather than as a sub-field of Note.
Union-of-BaseModel rejected. Distinct scalar types live in distinct PyArrow fields. The translator deliberately raises on BaseModel | BaseModel unions: storing them as PyArrow dense_union is forbidden because consumers (DuckDB, polars, pandas) cannot project the union arms cleanly. The required pattern is columnar separation — drop the polymorphic field with register_value_projector(..., lambda *_: []) and expose each scalar type in its own field.
Bulk construction path. The canonical bulk constructor is StructArrayBuilder.from_model(model_cls).build(objects), with build_struct_array(model_cls, objects) as the public facade and build_coordinate_struct_array as the specialised facade for the rational denormalisation of Coordinate / Duration. The builder traverses each scalar once and accumulates per-field arrays. Never model_dump row-wise in production paths. The microbenchmark gate requires the store-builder to be ≥ 2× faster than row-wise model_dump; on Coordinate the observed ratio is ~5×, and the underlying load-bearing claim — vectorised pa.compute operations on a 1M-element field being ~150–210× faster than the equivalent per-object Python loop — is the justification for the columnar architecture as a whole.
Paired scalar + field convention. Every scalar X is followed in the same source file by its paired XField(SemanticField[X]). core/time.py cohabits the time scalars (Coordinate, Duration, IdCoordinate, IdDuration) and their paired fields under a single TimeScalarField parent that consolidates from_field / from_table / metadata plumbing; core/events.py cohabits pitch, harmony, Note, and Measure with their paired fields. Object + ObjectField is the unit of code organisation and must never be split across files.
Raw vs semantic DataField branch. DataField is the abstract base for field wrappers. Raw subclasses (NumericField, StringField, StructField, MapField, plus the struct-shaped numeric branch NumberField → RationalField → DenominateNumberField) wrap PyArrow data with type checks and accessors. SemanticField[T] is the strictly-typed bridge between a pydantic scalar T and a struct-shaped raw field; it pins the inner raw type via the _raw_cls ClassVar (default StructField, refined to DenominateNumberField by TimeScalarField).
Two-step typing/grouping algorithm. FlowController converts raw MeasureUnits into typed measures and groups via two passes:
- Typing step. Each
MeasureUnitis classified asIncompleteMeasure/CompleteMeasure/OverlengthMeasureby comparing its actual duration to the expected time-signature duration, with local context (first/last measure) refining the incomplete position. - Grouping step. Typed measures are consolidated into
MeasureGroupsubclasses (CompleteMeasureGroup,SplitMeasure,VoltaGroup,IncompleteGroup,OverlengthGroup) so every measure belongs to exactly one group.
Do not use the word “Phase” to refer to these passes in shipped code or docs — say “Typing step” / “Grouping step”.
Unified TimeStamp architecture. TimeStamp, GroupTimestamp, and MatchStamp are the canonical results of coordinate resolution for timelines, groups, and bundles respectively. They share the same Stamp interface for timeline presence, raw coordinates, and unit-bearing coordinate access. MatchGraph remains on-demand (see §2.3).
3. Environment & Installation
3.1 Python
- Supported: Python 3.11, 3.12, 3.13 (CI matrix).
- Required:
>=3.11perpyproject.toml.
3.2 Install
From the timetoalign/ directory:
pip install -e ".[dev]"dev is the convenience extra that pulls in everything: all loaders, plotting, the tutorial Jupyter stack, the docs build chain, the test stack, and the formatter/linter tooling. See pyproject.toml for the full extras hierarchy (midi, partitura, music21, ms3, audio, graphical, plot, scores, loaders, tutorial, docs, testing, all, dev).
3.3 Pre-commit Hook
After installing, register the git hook once per clone:
pre-commit installThe pinned hook versions in .pre-commit-config.yaml are:
| Hook | Version | Settings |
|---|---|---|
black |
26.1.0 | language_version: python3.11 |
isort |
7.0.0 | --profile black --filter-files |
flake8 |
7.3.0 | max-line-length=120, __init__.py:F401 ignored, docs/howto-notebooks/*.py:E402 ignored |
seed-isort-config |
2.2.0 | |
pre-commit-hooks |
6.0.0 | trailing-whitespace, check-ast/json/xml/yaml, end-of-file-fixer, mixed-line-ending (auto), debug-statements, requirements-txt-fixer |
These are pinned deliberately — bump them in a dedicated chore: commit so the diff is isolated.
4. Build, Test, Publish (tox)
tox is the canonical entry point for everything CI-relevant. It uses tox-uv for fast environment creation. All commands run from timetoalign/.
| Command | Effect |
|---|---|
tox |
Runs the full pytest suite under py311, py312, py313 (skips missing interpreters). |
tox -e lint |
pre-commit run --all-files --show-diff-on-failure. |
tox -e build |
PEP 517 sdist + wheel into ./dist/ via python -m build. |
tox -e clean |
Removes ./build/ and ./dist/. |
tox -e publish |
twine check dist/* then twine upload dist/* (testpypi by default). |
To publish to production PyPI explicitly:
tox -e publish -- --repository pypiThe publish env passes TWINE_USERNAME / TWINE_PASSWORD / TWINE_REPOSITORY / TWINE_REPOSITORY_URL through from the environment.
There is no tox -e docs env; documentation is built directly with quartodoc + quarto (see §6).
4.1 Direct pytest for Iteration
Inside the active venv, plain pytest is faster than tox for tight edit-test loops:
pytest # full suite, parallel via pytest-xdist (-n auto)
pytest -n 0 # serial — debugging only
pytest tests/timelines # one subtree
pytest tests/timelines/test_base.py::test_lockCoverage is on by default (configured in pyproject.toml [tool.pytest.ini_options]).
4.2 Test Discipline (MANDATORY)
- Parallel-safe. No global mutable state, no inter-test ordering, use
tmp_pathnot hardcoded/tmp/paths, no port collisions. - Exact assertions. Use exact gold-standard counts, never
>=, never “approximate”. If floating-point tolerance is required, document the mathematical reason in the test and the test data README. - Cover happy path + edge cases. Boundaries, empty inputs, type mismatches.
- Property tests for math. ConversionMap-style code uses
hypothesisto verify invariants such asinverse(forward(x)) == x. - Test data needs a README. Every directory under
tests/data/documents provenance, validation logic, and known discrepancies between loaders.
5. Coding Standards
5.1 Style and Formatting
- Line length 120 (enforced by black + flake8).
from __future__ import annotationsis the first non-comment line.- Imports grouped: standard library, third-party, local — handled by isort with the black profile.
- Module logger:
module_logger = logging.getLogger(__name__)immediately after imports. - Use
# region <name>/# endregion <name>to group related class / function definitions inside a module.
5.2 Class Member Order
Inside a class, declare members in this order:
- Class variables.
@classmethod @propertyaccessors.@classmethodfactories / helpers.- Nested classes (especially
Schema). __init__.- Magic methods (
__eq__,__hash__,__repr__,__str__). @propertyaccessors.- Public and private methods.
5.3 Typing
- Every public function signature and class attribute is type-hinted.
- Use modern generics (
list[str],dict[str, int],X | None) — the__future__ annotationsimport makes these free at runtime on 3.11+. - Use
typing_extensions.Selffor methods returning the instance type. - Break circular dependencies with
if TYPE_CHECKING:blocks and string annotations, not by collapsing modules.
5.4 Docstrings (Google Style)
Every public module, class, function, and method needs a Google-style docstring. Quartodoc renders them into the API reference site.
def lookup(self, coord: CoordinateSpec) -> TimeStamp:
"""Return the timestamp at ``coord``.
Args:
coord: A raw value, ``Coordinate``, or ``IdCoordinate``.
Returns:
A `timetoalign.TimeStamp` populated with all synchronous child
coordinates and conversion-map results.
Raises:
ValueError: If ``coord`` is outside the timeline length.
"""Cross-linking
- In docstrings: wrap fully-qualified names in backticks (
`timetoalign.ConversionMap`); theinterlinksfilter resolves them. - In
.qmdpages and notebook markdown: every model term goes through the glossary shortcode:. Bare uses without the shortcode are documentation bugs. - When you add a new concept: update
glossary.ymlandglossary.qmdin the same commit as the implementation.
5.5 Error Handling
- Never use
assertfor runtime validation — it disappears under-O. ValueErrorfor invalid arguments.TypeErrorfor unit / type mismatches.RuntimeError(or a custom subclass) for invalid state, e.g. mutating a locked timeline.
5.6 Pitch Spelling
Normalise all input to canonical Unicode characters:
- Sharp:
♯(U+266F) - Flat:
♭(U+266D)
#, b, -, etc. are converted at the boundary.
6. Documentation Site
The docs site is built with Quarto + quartodoc and lives at https://timetoalign.github.io/.
6.1 Building Locally
cd docs/page
quartodoc build # regenerate the API reference pages from docstrings
quarto render # build the full site into _site/The Quarto pre-render hook automatically runs sync_notebooks.py (see §6.3) to refresh tutorial and how-to notebooks before rendering.
6.2 Site Structure (Diátaxis)
The site follows the Diátaxis framework:
- Tutorials (
tutorials.qmd+tutorials/): learning-oriented, step-by-step. - How-to Guides (
howto.qmd+howto/): task-oriented, focused recipes. - Explanation (
concepts.qmd,glossary.qmd): understanding-oriented. - API Reference (
reference.qmd+ auto-generatedreference/): information-oriented; this Developers’ Reference lives here too.
The navigation (top navbar + per-section sidebar) is configured in _quarto.yml.
6.3 Notebook Integration Pipeline
Tutorial and how-to notebooks live as jupytext py:percent files in docs/tuto-notebooks/ and docs/howto-notebooks/. The paired .ipynb files are regenerated mechanically; the .py is the source of truth in git.
The pipeline is driven by docs/page/notebooks.csv, with one row per notebook:
| Column | Meaning |
|---|---|
section |
Either tutorials or howto. Selects the source directory and target subdirectory of the rendered site. |
source |
Filename of the .py jupytext source. |
slug |
Stem used for the rendered .ipynb (without extension). |
title |
Title injected as Quarto YAML front matter. |
description |
Short description used in listing pages. |
docs/page/sync_notebooks.py (run as the Quarto pre-render step) does three things per row:
- Runs
jupytext --sync --executeon the.pysource, regenerating the paired.ipynbwith fresh outputs. - Copies the resulting
.ipynbintodocs/page/{tutorials,howto}/<slug>.ipynb. - Replaces the notebook metadata with a Quarto-friendly YAML cell built from the CSV row.
A SHA-256 cache (docs/page/.sync_state.json) skips notebooks whose .py source has not changed.
Adding a new how-to / tutorial notebook
- Drop the new
.py(jupytextpy:percent) intodocs/tuto-notebooks/ordocs/howto-notebooks/. - Add a row to
docs/page/notebooks.csv. - Add a sidebar entry under the appropriate section in
docs/page/_quarto.yml. - If the notebook introduces a new model term, also update
glossary.ymlandglossary.qmd. - Run
cd docs/page && python sync_notebooks.py --verboseonce locally to confirm it executes cleanly. - Run
quartodoc build && quarto renderto confirm it renders.
Notebook style rules
- Markdown cells use the
shortcode for every model term. A bare “Timeline” / “ConversionMap” / etc. is a documentation bug. - Use
Loader.from_file()rather than the two-phaseload()+create_*()pattern in tutorials. The two-phase form is library-internal. - Coordinate conversion is demonstrated through a
TimeStamporMatchStamp, which expose the resolved coordinates directly.
6.4 Jupytext and Quarto: Upgrade-Only
Jupytext and Quarto versions may only ever be upgraded, never downgraded.
Both tools rewrite notebook structure on save; downgrading silently introduces incompatible changes that break the diff history and may corrupt outputs across the entire notebook corpus. If a contributor’s local install is older than the version that last touched the notebooks, they must upgrade before running sync_notebooks.py.
This rule applies equally to CI environments — pin versions upward only.
6.5 quartodoc API Reference
The API reference under docs/page/reference/ is fully generated from the live docstrings. Do not hand-edit those files; they are overwritten by quartodoc build. To add a class to the reference:
- Make sure the class is exported from
timetoalign/__init__.py(or list it with an explicitname:/package:entry). - Add it to the appropriate
contents:list underquartodoc:indocs/page/_quarto.yml. quartodoc buildregenerates the page.
7. Conventional Commits & Releases
The library is released by release-please, configured in .github/workflows/release-please.yml with release-type: python and package-name: timetoalign. The action runs on every push to main and:
- Reads commit messages since the last release tag.
- Computes the next semantic version from those messages.
- Opens (or updates) a release PR that bumps
pyproject.toml’sversionand rewritesCHANGELOG.md. - When the release PR is merged, tags the commit and creates a GitHub release.
Because the version bump is computed from commit messages, every commit that lands on main must follow Conventional Commits.
7.1 Commit Types
| Prefix | Effect on version | Use for |
|---|---|---|
feat: |
minor bump | New user-facing capability. |
fix: |
patch bump | Bug fix. |
perf: |
patch bump | Performance improvement (no behaviour change). |
docs: |
none | Documentation only. |
style: |
none | Formatting / whitespace. |
refactor: |
none | Restructuring with no behaviour change. |
test: |
none | Adding or correcting tests. |
build: |
none | Build system or dependency changes. |
ci: |
none | CI configuration changes. |
chore: |
none | Repo housekeeping with no src/test impact. |
revert: |
depends on reverted | Revert of a previous commit. |
Optional scopes are encouraged for clarity: feat(loader): …, fix(maps): ….
7.2 Breaking Changes (MAJOR Bump)
A breaking change triggers a major version bump. Mark it both ways:
- Add
!after the type, e.g.refactor!:,feat!:. - Include a
BREAKING CHANGE:footer describing the migration.
refactor!: rename TimelineGroup.get_timestamp row lookup
BREAKING CHANGE: row-index lookups now use get_timestamp_at_index(index).
The coordinate-resolving get_timestamp_at(coordinate, timeline_id) method
is unchanged and returns a GroupTimestamp.
The ! is what release-please reads to compute the bump; the BREAKING CHANGE: footer is what readers of the changelog rely on. Use both.
7.3 Manual Edits Are Forbidden
- Do not edit
pyproject.toml’sversionby hand. - Do not edit
CHANGELOG.mdby hand.
Both are owned by release-please. Manual edits will be overwritten or, worse, will fight the next release PR.
8. Branching and Pull Requests
mainis the release branch; release-please runs against it.- Feature work happens on topic branches; squash-merge into
mainwith a Conventional Commit message that becomes the merge commit. - The PR title also follows Conventional Commits — release-please reads the merge commit, but a consistent title makes the PR list scannable.
- Run
tox -e lintandtox(or at least targetedpytest) locally before opening a PR.
9. IDE
PyCharm is the preferred IDE. The repo is set up to work cleanly with it out of the box:
- Import the
timetoalign/directory as a PyCharm project (not the repo root — sibling projects have their own settings). - Configure the project interpreter to a venv with
pip install -e ".[dev]"applied. - Enable the Black and isort integrations and point them at the project tools (PyCharm reads
.pre-commit-config.yamlfor the pinned versions; mirror them in Settings → Tools → Black and Settings → Editor → Code Style → Python → Imports). - Enable Settings → Editor → Inspections → Python → Type checker with strict mode if you want the same level of typing the codebase already enforces.
- The flake8 line length is 120; set Settings → Editor → Code Style → Python → Hard wrap at to 120 to match.
VS Code, Neovim, and other editors all work — the formatters and linters are externally driven by pre-commit and tox, so editor choice is not load-bearing — but PyCharm is what the maintainers use day-to-day.
10. Where to Look Next
- The conceptual model: Concepts.
- Term definitions: Glossary.
- Per-class API: API Reference.
- Worked examples: Tutorials and How-To Guides.