Alignment Bundles

MatchfileLoader, AlignmentBundle, cross-group transfer

Alignment Bundles

An AlignmentBundle manages multiple timelines and the groups that connect them. We load the Vienna 1x22 corpus (one score, 22 performances) using the MatchfileLoader to illustrate the pattern.

from timetoalign import AlignmentBundle, MatchfileLoader, TimeUnit
from timetoalign.core import SupportPolicy
from timetoalign.testdata import ensure_data
from timetoalign.timelines import Timeline

DATA_DIR = ensure_data("vienna_1x22")
/home/laser/miniconda3/envs/timetoalign/lib/python3.11/site-packages/partitura/__init__.py:9: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  import pkg_resources

Load All Match Files at Once

A single MatchfileLoader instance processes all .match files that share the same score.

match_files = sorted(DATA_DIR.glob("*.match"))
loader = MatchfileLoader()
loader.load(*match_files)

{
    "files loaded": len(match_files),
    "timelines": len(loader.create_timelines()),
}
{'files loaded': 22, 'timelines': 23}

Create the Bundle

bundle = loader.create_bundle()
bundle
AlignmentBundle[bundle:AlignmentBundle_1]

  TimelineGroup[score] (1 timelines, 2 timestamps)
  ┌────────────────────────────────────────────────────────────────────────────┐
  │ ContinuousLogicalTimeline[score:clt1] (454 events, 2 cmaps)                │
  │                       0 ____________________________________ 41.5 quarters │
  └────────────────────────────────────────────────────────────────────────────┘
  Timestamps: 2

  Standalone timelines (22):
    Chopin_op...     0 ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 79900 ticks (451 ev)
    Chopin_op...     0 ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 72365 ticks (448 ev)
    Chopin_op...     0 ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 76999 ticks (452 ev)
    ... (16 more)
    Chopin_op...     0 ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 86143 ticks (447 ev)
    Chopin_op...     0 ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 83149 ticks (451 ev)
    Chopin_op...     0 ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 77497 ticks (452 ev)

  MatchClaims: 9988

Explore Timelines

score_tl = bundle.get_timeline("score")
score_tl
ContinuousLogicalTimeline[score:clt1] (454 events, 2 cmaps)
                      0 ________________________________ 41.5 quarters
score_tl.get_events(event_type="Note").to_dataframe().head()
id name temporal_type event_type start end duration

Query Coordinates Across Groups

get_matchstamp_at() is the cross-domain resolution method for an AlignmentBundle: it accepts a coordinate on any timeline and returns a MatchStamp spanning all connected timelines. MatchStamp shares the unified stamp interface used by the other timestamp types.

perf_id = [uid for uid in bundle.timeline_ids if uid != score_tl.id][0]

stamp = bundle.get_matchstamp_at(100.0, "score:clt1")
stamp
MatchStamp 1 timelines, 0 edges
ID Coordinate Type
score:clt1 100
Try: stamp.get(<tl_id>), stamp.get_coordinate(<tl_id>), stamp.get_unit(<unit>)

The returned stamp exposes unit-bearing coordinates through get_coordinate(), and is_interpolated identifies a WarpMap fallback rather than an exact claim anchor.

{
    "score coordinate": stamp.get_coordinate("score:clt1"),
    "interpolated": stamp.is_interpolated,
}
{'score coordinate': Coordinate(100.0, quarters), 'interpolated': True}

Merge Bundles and Bridge Them

Two bundles built separately can be merged into one with AlignmentBundle.from_bundles. Merging registers every group, timeline, and MatchClaim from each source, but does not itself align the two sides — you bridge them afterwards by adding MatchClaims. Everything below is built in memory, with no data files.

The first bundle is a symbolic score, four measures of 4/4:

score = Timeline(length=12, unit=TimeUnit.quarters, uid="score")
score.add_events(
    [
        {
            "id": f"m{m}",
            "temporal_type": "instant",
            "event_type": "Measure",
            "instant": float(4 * m),
        }
        for m in range(4)
    ]
)
symbolic = AlignmentBundle(name="symbolic")
symbolic.add_timeline(score, as_group="score")
AlignmentBundle[bundle:AlignmentBundle_2]

  TimelineGroup[score] (1 timelines, 2 timestamps)
  ┌────────────────────────────────────────────────────────────────────────────┐
  │ Timeline[score] (4 events)                                                 │
  │                       0 ______________________________________ 12 quarters │
  └────────────────────────────────────────────────────────────────────────────┘
  Timestamps: 2

  MatchClaims: 0

The second bundle is one performance, its audio (seconds) and MIDI (ticks) placed in a single group, so a coordinate transfers between them by interpolation:

perf = Timeline(length=6.0, unit=TimeUnit.seconds, uid="perf")
midi = Timeline(length=2880, unit=TimeUnit.ticks, uid="midi")
audio = AlignmentBundle(name="audio")
audio.add_timeline(perf, as_group="performance")
audio.add_timeline(midi, aligned_to="perf")

merged = AlignmentBundle.from_bundles([symbolic, audio], name="merged")
merged
AlignmentBundle[bundle:AlignmentBundle_4]

  TimelineGroup[score] (1 timelines, 2 timestamps)
  ┌────────────────────────────────────────────────────────────────────────────┐
  │ Timeline[score] (4 events)                                                 │
  │                       0 ______________________________________ 12 quarters │
  └────────────────────────────────────────────────────────────────────────────┘
  Timestamps: 2

  TimelineGroup[performance] (2 timelines, 2 timestamps)
  ┌────────────────────────────────────────────────────────────────────────────┐
  │ Timeline[perf]                                                             │
  │                       0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 seconds │
  │                                                                            │
  │ Timeline[midi]                                                             │
  │                       0 _______________________________________ 2880 ticks │
  └────────────────────────────────────────────────────────────────────────────┘
  Timestamps: 2

  MatchClaims: 0

The merged bundle holds all three timelines but no connection between the two sides yet. create_match_claims bridges the score to the performance at the measure downbeats — quarters on score to seconds on perf:

merged.create_match_claims(
    [
        ({"start": q}, "score", {"start": s}, "perf")
        for q, s in [(4.0, 1.0), (8.0, 3.0), (12.0, 5.0)]
    ]
)
sorted(merged.timeline_ids)
['midi', 'perf', 'score']

The Transitive Union Across Both Bundles

A single get_matchstamp_at() on score now resolves the whole chain: the bridge warps score to perf, and perf’s own group carries the reach on to midi. The stamp spans all three timelines even though no claim ties score directly to midi.

merged.get_matchstamp_at(8.0, "score")
MatchStamp 3 timelines, 2 edges
ID Coordinate Type
perf 3 anchor
score 8 anchor
midi 1440 inferred
Try: stamp.get(<tl_id>), stamp.get_coordinate(<tl_id>), stamp.get_unit(<unit>)

Out-of-Support Coordinates

The bridge’s earliest anchor is measure 2 (quarter 4). A query below it — quarter 0 — lies outside the WarpMap’s support, so perf (and the midi reached through it) has no defined position there. support_policy decides what happens. The default, SupportPolicy.omit, drops the unsupported timelines; the queried timeline’s own coordinate always stays:

merged.get_matchstamp_at(0.0, "score")  # support_policy defaults to omit
MatchStamp 1 timelines, 0 edges
ID Coordinate Type
score 0
Try: stamp.get(<tl_id>), stamp.get_coordinate(<tl_id>), stamp.get_unit(<unit>)

clamp reports the nearest in-support boundary coordinate instead of dropping the timeline; extrapolate keeps the linear extrapolation but clips it to each timeline’s [0, length] span. No policy ever yields a negative coordinate. The per-call support_policy argument overrides the bundle-wide AlignmentBundle.support_policy default:

{
    "clamp": merged.get_matchstamp_at(0.0, "score", support_policy="clamp"),
    "extrapolate": merged.get_matchstamp_at(
        0.0, "score", support_policy=SupportPolicy.extrapolate
    ),
}
{'clamp': MatchStamp(score=0.00, perf=1.00, midi=480.00),
 'extrapolate': MatchStamp(score=0.00, perf=0.00, midi=0.00)}

Next: Flow Control and Grids