from __future__ import annotations
import pyarrow.compute as pc
from timetoalign import IntervalToConstantMap
from timetoalign.alignment.claims import MatchClaim
from timetoalign.loader.alignment.ieee1599 import Ieee1599Loader
from timetoalign.testdata import ensure_data
base = ensure_data("ieee1599")
document_path = base / "SatiePetriNets" / "ieee1599" / "gymnopedie_01.xml"How to Load an IEEE 1599 Document
How to Load an IEEE 1599 Document
IEEE 1599 is a multi-layer XML standard for encoding a single musical work once and relating every representation of it — the score, engraved page images, audio recordings, analytical annotations — back to a common axis. That axis is the <spine>: a flat, ordered list of abstract events in virtual time units (VTU), unit-less by design. Every other layer states where in its own coordinate space one of those spine events falls, via an event_ref. The spine is the hub; the layers are projections onto it; event_ref is the correspondence — exactly the shape of an alignment.
Ieee1599Loader reads one such document into a single multimodal : the spine as one in ticks, the logically-organised-symbols (LOS) layer of notes, rests and lyrics on another, one graphical timeline per engraved edition, one physical timeline per audio track, and the whole set of projections as one columnar . The document’s own analytical layer — a segmentation of the spine resolving to places of a Petri net — is carried too, not as timing but as an external reference.
We load an existing document; nothing here runs an aligner. The correspondences are the document’s own event_ref cross-references; the loader’s job is to read them faithfully.
The work is Erik Satie’s Gymnopédie No. 1.
The arc:
- Load the document in one call and read its title.
- The spine, in ticks — a cumulative virtual-time axis — and the LOS layer of notes and rests sitting at spine coordinates.
- The graphical editions as nested
SegmentLines of pages and accolades, in pixels, and the audio tracks, in seconds. - The projections, reached through the uniform API, and the cross-section they describe over spine coordinates.
- The structural layer: a Petri-net analysis carried as external references on the spine.
Setup
1. Load the document in one call
Ieee1599Loader parses the whole document — spine, LOS, every graphical edition, every audio track, and the structural analysis — into curated tables, then assembles them into timelines and a claim field. from_file() is the one-line form of the standard two-step loader pattern: parse, then build.
loader = Ieee1599Loader.from_file(document_path)
loaderIeee1599Loader
| Sources | 1 file(s): gymnopedie_01.xml |
| Claims | 2085 |
| Unit | ticks |
| File | gymnopedie_01 |
| Title | Gymnopédie No. 1 |
| Spine events | 382 |
| LOS events | 557 |
| Editions | 2 |
| Tracks | 2 |
| Claims | 2085 |
| Try | create_timeline(), create_bundle(), get_events(...) |
The document states its own bibliographic metadata, read straight off <general><description>:
{
"title": loader.file_metadata.get("title"),
"work_title": loader.file_metadata.get("work_title"),
"authors": loader.file_metadata.get("authors"),
}{'title': 'Gymnopédie No. 1',
'work_title': '3 Gymnopédies',
'authors': [{'name': 'Erik Satie', 'type': 'composer'}]}
create_bundle() builds the : six standalone timelines — the spine, the LOS layer, two engraved editions and two audio tracks — tied together by the MatchClaims. Each timeline lives in its own ; MatchClaims carry the connections between those groups.
bundle = loader.create_bundle()
print(bundle.diagram())AlignmentBundle[bundle:AlignmentBundle_1]
TimelineGroup[spine:dlt1] (1 timelines, 2 timestamps)
┌────────────────────────────────────────────────────────────────────────────┐
│ DiscreteLogicalTimeline[spine:dlt1] (382 events) │
│ 0 ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 236544 ticks │
└────────────────────────────────────────────────────────────────────────────┘
Timestamps: 2
TimelineGroup[los:dlt2] (1 timelines, 2 timestamps)
┌────────────────────────────────────────────────────────────────────────────┐
│ DiscreteLogicalTimeline[los:dlt2] (557 events) │
│ 0 ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 236544 ticks │
└────────────────────────────────────────────────────────────────────────────┘
Timestamps: 2
TimelineGroup[eng_montreal_les_editions_outremontaises_2006:dgt1] (1 timelines, 2 timestamps)
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ SegmentLine[SegmentLine[DiscreteGraphicalTimeline]][eng_montreal_les_editions_outremontaises_2006:dgt1] (4 children, 1 cmaps) │
│ 0 ∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶ 7686 pixels │
│ ├─ page_1 0 ∶∶∶∶∶∶∶∶ 1694 │
│ │ ├─ accolade_1 0 ∶∶ 426 (22 events) │
│ │ ├─ accolade_2 426 ∶∶ 846 (22 events) │
│ │ ├─ accolade_3 846 ∶∶ 1262 (16 events) │
│ │ └─ accolade_4 1262 ∶∶ 1694 (26 events) │
│ ├─ page_2 1694 ∶∶∶∶∶∶∶∶∶∶ 3846 │
│ │ ├─ accolade_1 1694 ∶∶ 2111 (16 events) │
│ │ ├─ accolade_2 2111 ∶∶ 2533 (22 events) │
│ │ ├─ accolade_3 2533 ∶∶ 2976 (23 events) │
│ │ ├─ accolade_4 2976 ∶∶ 3423 (23 events) │
│ │ └─ accolade_5 3423 ∶∶ 3846 (24 events) │
│ ├─ page_3 3846 ∶∶∶∶∶∶∶∶∶∶ 5951 │
│ │ ├─ accolade_1 3846 ∶∶ 4266 (16 events) │
│ │ ├─ accolade_2 4266 ∶∶ 4687 (22 events) │
│ │ ├─ accolade_3 4687 ∶∶ 5102 (16 events) │
│ │ ├─ accolade_4 5102 ∶∶ 5523 (22 events) │
│ │ └─ accolade_5 5523 ∶∶ 5951 (20 events) │
│ └─ page_4 5951 ∶∶∶∶∶∶∶∶∶ 7686 │
│ ├─ accolade_1 5951 ∶∶ 6372 (22 events) │
│ ├─ accolade_2 6372 ∶∶ 6813 (23 events) │
│ ├─ accolade_3 6813 ∶∶ 7260 (23 events) │
│ └─ accolade_4 7260 ∶∶∶ 7686 (24 events) │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Timestamps: 2
TimelineGroup[eng_transcription_2012:dgt2] (1 timelines, 2 timestamps)
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ SegmentLine[SegmentLine[DiscreteGraphicalTimeline]][eng_transcription_2012:dgt2] (4 children, 1 cmaps) │
│ 0 ∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶ 7356 pixels │
│ ├─ page_1 0 ∶∶∶∶∶∶∶∶ 1625 │
│ │ ├─ accolade_1 0 ∶∶ 408 (22 events) │
│ │ ├─ accolade_2 408 ∶∶ 812 (22 events) │
│ │ ├─ accolade_3 812 ∶∶ 1211 (16 events) │
│ │ └─ accolade_4 1211 ∶∶ 1625 (26 events) │
│ ├─ page_2 1625 ∶∶∶∶∶∶∶∶∶∶ 3681 │
│ │ ├─ accolade_1 1625 ∶∶ 2024 (16 events) │
│ │ ├─ accolade_2 2024 ∶∶ 2428 (22 events) │
│ │ ├─ accolade_3 2428 ∶∶ 2851 (23 events) │
│ │ ├─ accolade_4 2851 ∶∶ 3279 (23 events) │
│ │ └─ accolade_5 3279 ∶∶ 3681 (24 events) │
│ ├─ page_3 3681 ∶∶∶∶∶∶∶∶∶∶ 5700 │
│ │ ├─ accolade_1 3681 ∶∶ 4084 (16 events) │
│ │ ├─ accolade_2 4084 ∶∶ 4487 (22 events) │
│ │ ├─ accolade_3 4487 ∶∶ 4887 (16 events) │
│ │ ├─ accolade_4 4887 ∶∶ 5291 (22 events) │
│ │ └─ accolade_5 5291 ∶∶ 5700 (20 events) │
│ └─ page_4 5700 ∶∶∶∶∶∶∶∶∶ 7356 │
│ ├─ accolade_1 5700 ∶∶ 6105 (22 events) │
│ ├─ accolade_2 6105 ∶∶ 6528 (23 events) │
│ ├─ accolade_3 6528 ∶∶ 6954 (23 events) │
│ └─ accolade_4 6954 ∶∶∶ 7356 (24 events) │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Timestamps: 2
TimelineGroup[satie_gymnopedie1_coleman:cpt1] (1 timelines, 2 timestamps)
┌────────────────────────────────────────────────────────────────────────────┐
│ ContinuousPhysicalTimeline[satie_gymnopedie1_coleman:cpt1] (382 events) │
│ 0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 188.8 seconds │
└────────────────────────────────────────────────────────────────────────────┘
Timestamps: 2
TimelineGroup[satie_gymnopedie1_pfaul:cpt2] (1 timelines, 2 timestamps)
┌────────────────────────────────────────────────────────────────────────────┐
│ ContinuousPhysicalTimeline[satie_gymnopedie1_pfaul:cpt2] (382 events) │
│ 0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 192.2 seconds │
└────────────────────────────────────────────────────────────────────────────┘
Timestamps: 2
MatchClaims: 2085
2. The spine and the LOS layer
The spine: a cumulative virtual-time axis
The spine’s timing attribute is a relative integer delta against the previous <event>; the loader accumulates it into an absolute coordinate, so the stored value is the running sum — the cumulative VTU. Events notated as simultaneous carry timing="0" and land on the same coordinate. The spine is a DiscreteLogicalTimeline in ticks:
spine = bundle.get_timeline(loader.spine_uid)
spineDiscreteLogicalTimeline[spine:dlt1] (382 events)
0 ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 236544 ticks
spine.get_events().head(5)| id | name | temporal_type | event_type | start | end | duration | hpos | |
|---|---|---|---|---|---|---|---|---|
| 0 | Clef_part_1_1 | NaN | instant | SpineEvent | 0 | None | None | 0 |
| 1 | Clef_part_2_1 | NaN | instant | SpineEvent | 0 | None | None | 0 |
| 2 | KeySignature_part_1_1 | NaN | instant | SpineEvent | 0 | None | None | 0 |
| 3 | KeySignature_part_2_1 | NaN | instant | SpineEvent | 0 | None | None | 0 |
| 4 | TimeSignature_part_1_1 | NaN | instant | SpineEvent | 0 | None | None | 0 |
The LOS layer: notes and rests at spine coordinates
Every LOS event — a Note, a Rest, or a lyric Syllable — sits at the VTU coordinate of the spine event its event_ref names; that reference is its temporal position; the LOS layer carries no timing of its own. It is a second DiscreteLogicalTimeline, sharing the spine’s unit and length but its own event set — 557 LOS events against 382 spine events, since a chord’s several noteheads and a measure’s clefs and key signatures do not correspond one-to-one.
los = bundle.get_timeline(loader.los_uid)
losDiscreteLogicalTimeline[los:dlt2] (557 events)
0 ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 236544 ticks
Notated durations are kept as the verbatim num/den pair rather than a reduced fraction, so a duration notated 1/4 never silently becomes something else; the exact value is Fraction(duration_num, duration_den). The opening melodic notes — each a quarter note (duration_num=1, duration_den=4) — carry their pitch as a step, octave and accidental, e.g. the first is F♯6:
notes = los.get_events(event_type="Note").to_dataframe()
notes.head(5)| id | name | temporal_type | event_type | start | end | duration | actual_accidental | augmentation_dots | duration_den | ... | event_ref | measure | notehead_index | octave | part | printed_accidental | staff | step | tie | voice | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | note:000001 | NaN | instant | Note | 13312 | None | None | sharp | NaN | 4 | ... | part_1_voice0_measure5_ev1 | 5 | 0 | 6 | part_1 | NaN | part_1_staff | F | False | part_1_0_voice |
| 1 | note:000002 | NaN | instant | Note | 14336 | None | None | natural | NaN | 4 | ... | part_1_voice0_measure5_ev2 | 5 | 0 | 6 | part_1 | NaN | part_1_staff | A | False | part_1_0_voice |
| 2 | note:000003 | NaN | instant | Note | 15360 | None | None | natural | NaN | 4 | ... | part_1_voice0_measure6_ev0 | 6 | 0 | 6 | part_1 | NaN | part_1_staff | G | False | part_1_0_voice |
| 3 | note:000004 | NaN | instant | Note | 16384 | None | None | sharp | NaN | 4 | ... | part_1_voice0_measure6_ev1 | 6 | 0 | 6 | part_1 | NaN | part_1_staff | F | False | part_1_0_voice |
| 4 | note:000005 | NaN | instant | Note | 17408 | None | None | sharp | NaN | 4 | ... | part_1_voice0_measure6_ev2 | 6 | 0 | 6 | part_1 | NaN | part_1_staff | C | False | part_1_0_voice |
5 rows × 21 columns
3. Editions in pixels, tracks in seconds
Per-edition graphical SegmentLines
Each <graphic_instance_group> — one engraved edition of the score — is its own nested SegmentLine, in unit pixels. An edition is printed on pages, and a page is one image file with its own pixel origin, so the edition’s segments are its pages and a page’s segments are the accolades engraved on it. Within a page, x coordinates zig-zag along the spine: when a new system begins beyond half of that page’s x-span, the reset starts a new accolade. Both gymnopédie editions have four pages carrying 4 + 5 + 5 + 4 accolades.
for uid in loader.edition_uids:
edition = bundle.get_timeline(uid)
print(f"{edition.id}: {edition.name!r}, {edition.length}")eng_montreal_les_editions_outremontaises_2006:dgt1: 'eng:Montréal: Les Éditions Outremontaises (2006)', 7686 pixels
eng_transcription_2012:dgt2: 'eng:Transcription (2012)', 7356 pixels
edition = bundle.get_timeline(loader.edition_uids[0])
graphical_events = edition.get_events().to_dataframe()
graphical_events.head()| id | name | temporal_type | event_type | start | end | duration | bbox | event_ref | file_name | position_in_group | source_timeline | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | graphicevent:000001 | NaN | interval | GraphicEvent | 50 | 64 | 14 | {'ul': {'x': 50, 'y': 132}, 'lr': {'x': 64, 'y... | Clef_part_1_1 | score/Montreal/IMSLP01599-Satie_Gymnopedies-1.png | 1 | eng_montreal_les_editions_outremontaises_2006:... |
| 1 | graphicevent:000002 | NaN | interval | GraphicEvent | 49 | 64 | 15 | {'ul': {'x': 49, 'y': 192}, 'lr': {'x': 64, 'y... | Clef_part_2_1 | score/Montreal/IMSLP01599-Satie_Gymnopedies-1.png | 1 | eng_montreal_les_editions_outremontaises_2006:... |
| 2 | graphicevent:000003 | NaN | interval | GraphicEvent | 66 | 79 | 13 | {'ul': {'x': 66, 'y': 131}, 'lr': {'x': 79, 'y... | KeySignature_part_1_1 | score/Montreal/IMSLP01599-Satie_Gymnopedies-1.png | 1 | eng_montreal_les_editions_outremontaises_2006:... |
| 3 | graphicevent:000004 | NaN | interval | GraphicEvent | 66 | 79 | 13 | {'ul': {'x': 66, 'y': 190}, 'lr': {'x': 79, 'y... | KeySignature_part_2_1 | score/Montreal/IMSLP01599-Satie_Gymnopedies-1.png | 1 | eng_montreal_les_editions_outremontaises_2006:... |
| 4 | graphicevent:000005 | NaN | interval | GraphicEvent | 84 | 94 | 10 | {'ul': {'x': 84, 'y': 139}, 'lr': {'x': 94, 'y... | TimeSignature_part_1_1 | score/Montreal/IMSLP01599-Satie_Gymnopedies-1.png | 1 | eng_montreal_les_editions_outremontaises_2006:... |
The edition’s segments are its pages; each page’s segments are its accolades. iter_segments() yields both levels in spine order, together with each child’s offset in its parent’s coordinates. A page carries its <graphic_instance> attributes — the image file_name above all — in meta["page"]:
print(f"{edition.class_name}: {edition.n_segments} pages")
for _, page_offset, page in edition.iter_segments():
print(f"{page.name} at x={page_offset.value}: {page.meta['page']['file_name']}")
for _, offset, accolade in page.iter_segments():
print(f" {accolade.name} at x={offset.value}, length={accolade.length}")SegmentLine[SegmentLine[DiscreteGraphicalTimeline]]: 4 pages
page_1 at x=0: score/Montreal/IMSLP01599-Satie_Gymnopedies-1.png
accolade_1 at x=0, length=426 pixels
accolade_2 at x=426, length=420 pixels
accolade_3 at x=846, length=416 pixels
accolade_4 at x=1262, length=432 pixels
page_2 at x=1694: score/Montreal/IMSLP01599-Satie_Gymnopedies-2.png
accolade_1 at x=0, length=417 pixels
accolade_2 at x=417, length=422 pixels
accolade_3 at x=839, length=443 pixels
accolade_4 at x=1282, length=447 pixels
accolade_5 at x=1729, length=423 pixels
page_3 at x=3846: score/Montreal/IMSLP01599-Satie_Gymnopedies-3.png
accolade_1 at x=0, length=420 pixels
accolade_2 at x=420, length=421 pixels
accolade_3 at x=841, length=415 pixels
accolade_4 at x=1256, length=421 pixels
accolade_5 at x=1677, length=428 pixels
page_4 at x=5951: score/Montreal/IMSLP01599-Satie_Gymnopedies-4.png
accolade_1 at x=0, length=421 pixels
accolade_2 at x=421, length=441 pixels
accolade_3 at x=862, length=447 pixels
accolade_4 at x=1309, length=426 pixels
An IntervalToConstantMap on the edition resolves any edition-wide x coordinate to the page-image file_name that contains it, without descending the hierarchy. The first graphical event provides one such coordinate:
page_image_map = next(
cmap
for cmap in edition._conversion_maps.values()
if isinstance(cmap, IntervalToConstantMap)
)
first_graphical_event = graphical_events.iloc[0]
page_image_map(first_graphical_event["start"])'score/Montreal/IMSLP01599-Satie_Gymnopedies-1.png'
Graphical geometry is kept as one nested bbox struct rather than split coordinate columns. Its ul and lr members preserve the upper-left and lower-right pixel coordinates of the graphical event:
bbox = first_graphical_event["bbox"]
{
"bbox": bbox,
"ul": bbox["ul"],
"lr": bbox["lr"],
}{'bbox': {'ul': {'x': 50, 'y': 132}, 'lr': {'x': 64, 'y': 164}},
'ul': {'x': 50, 'y': 132},
'lr': {'x': 64, 'y': 164}}
Per-track audio timelines
Each <track> — one recording — is its own ContinuousPhysicalTimeline in seconds. The performers and the media file name the document states ride along in meta; the media file itself is never opened:
for uid in loader.track_uids:
track = bundle.get_timeline(uid)
print(f"{track.id}: {track.meta['performers']}, {track.length}")satie_gymnopedie1_coleman:cpt1: [{'name': 'Chase Coleman', 'type': 'piano'}], 188.77 seconds
satie_gymnopedie1_pfaul:cpt2: [{'name': 'Andreas Pfaul', 'type': 'piano'}], 192.22 seconds
track = bundle.get_timeline(loader.track_uids[0])
track.get_events().head(3)| id | name | temporal_type | event_type | start | end | duration | event_ref | file_name | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | trackevent:000001 | NaN | instant | TrackEvent | 2.46 | None | None | Clef_part_1_1 | audio/ChaseColeman/satie-gymnopedie1-coleman.mp3 |
| 1 | trackevent:000002 | NaN | instant | TrackEvent | 2.46 | None | None | Clef_part_2_1 | audio/ChaseColeman/satie-gymnopedie1-coleman.mp3 |
| 2 | trackevent:000003 | NaN | instant | TrackEvent | 2.46 | None | None | KeySignature_part_1_1 | audio/ChaseColeman/satie-gymnopedie1-coleman.mp3 |
4. The projections, as one columnar claim field
Every LOS, graphical and audio event contributes one synchronous tying its own coordinate to the spine event it references. All three layers go into one field — the alignment they express is one alignment, hub-and-spoke around the spine — reached through the uniform field API:
field = loader.get_field(MatchClaim)
{
"field type": type(field).__name__,
"claims": len(field),
}{'field type': 'MatchClaimField', 'claims': 2085}
The MatchClaimField remains columnar; its table has one struct column. Showing its head makes that stored representation visible before any individual MatchClaim is materialised:
field.table.to_pandas().head()| match_claim | |
|---|---|
| 0 | {'timeline_a_id': 'los:dlt2', 'timeline_b_id':... |
| 1 | {'timeline_a_id': 'los:dlt2', 'timeline_b_id':... |
| 2 | {'timeline_a_id': 'los:dlt2', 'timeline_b_id':... |
| 3 | {'timeline_a_id': 'los:dlt2', 'timeline_b_id':... |
| 4 | {'timeline_a_id': 'los:dlt2', 'timeline_b_id':... |
Indexing the field materialises one MatchClaim on demand. This mid-document row is more illustrative than the first row because it shows an ordinary in-document correspondence rather than the special opening coordinate:
field[len(field) // 2]| Timeline A | eng_transcription_2012:dgt2 | @2102 pixels |
| Timeline B | spine:dlt1 | @64512 ticks |
| Metadata | agent=Finale Plugin | |
claim.get_matchstamp()The cross-section over spine coordinates
get_matchstamp_table(from_graph=True) collapses the claims into one row per connected component of the alignment graph — one row per spine coordinate that at least one layer reaches, every participating layer filled in the same row. It is the cross-section of all six timelines over the spine:
cross_section = bundle.get_matchstamp_table(from_graph=True).to_pandas()
{"rows": len(cross_section), "columns": list(cross_section.columns)}{'rows': 188,
'columns': ['eng_montreal_les_editions_outremontaises_2006:dgt1',
'eng_transcription_2012:dgt2',
'los:dlt2',
'satie_gymnopedie1_coleman:cpt1',
'satie_gymnopedie1_pfaul:cpt2',
'spine:dlt1']}
cross_section.head(5)| eng_montreal_les_editions_outremontaises_2006:dgt1 | eng_transcription_2012:dgt2 | los:dlt2 | satie_gymnopedie1_coleman:cpt1 | satie_gymnopedie1_pfaul:cpt2 | spine:dlt1 | |
|---|---|---|---|---|---|---|
| 0 | {'value': 49.0, 'numerator': 49, 'denominator'... | {'value': 75.0, 'numerator': 75, 'denominator'... | {'value': 0.0, 'numerator': 0, 'denominator': 1} | {'value': 2.46, 'numerator': 2769713770832855,... | {'value': 0.5, 'numerator': 1, 'denominator': 2} | {'value': 0.0, 'numerator': 0, 'denominator': 1} |
| 1 | {'value': 133.0, 'numerator': 133, 'denominato... | {'value': 148.0, 'numerator': 148, 'denominato... | {'value': 1024.0, 'numerator': 1024, 'denomina... | {'value': 3.48, 'numerator': 7836263351624663,... | {'value': 1.3, 'numerator': 5854679515581645, ... | {'value': 1024.0, 'numerator': 1024, 'denomina... |
| 2 | {'value': 192.0, 'numerator': 192, 'denominato... | {'value': 201.0, 'numerator': 201, 'denominato... | {'value': 3072.0, 'numerator': 3072, 'denomina... | {'value': 5.47, 'numerator': 6158672490429153,... | {'value': 3.14, 'numerator': 7070651414971679,... | {'value': 3072.0, 'numerator': 3072, 'denomina... |
| 3 | {'value': 224.0, 'numerator': 224, 'denominato... | {'value': 231.0, 'numerator': 231, 'denominato... | {'value': 4096.0, 'numerator': 4096, 'denomina... | {'value': 6.41, 'numerator': 1804254600715305,... | {'value': 4.0, 'numerator': 4, 'denominator': 1} | {'value': 4096.0, 'numerator': 4096, 'denomina... |
| 4 | {'value': 286.0, 'numerator': 286, 'denominato... | {'value': 284.0, 'numerator': 284, 'denominato... | {'value': 6144.0, 'numerator': 6144, 'denomina... | {'value': 8.24, 'numerator': 4638707616191611,... | {'value': 5.8, 'numerator': 6530219459687219, ... | {'value': 6144.0, 'numerator': 6144, 'denomina... |
5. The structural layer: an analysis resolved to a Petri net
An <analysis> partitions the spine into <segment> elements, each listing the spine events it covers; a sibling <petri_nets> block names .pnml files and binds one place of one net to one segment. That is a reference into an external resource, not a timing statement, so it is carried as spine.external_references — one row per (segment_event, place) pair — rather than as events or claims:
refs = spine.external_references
refs.num_rows1795
Filtering to one segment shows the resolution end to end: every spine event of segment Analisi_1_L1_A resolves to place p2 of the Petri net stored in Analisi_1/L1.pnml — read straight off the document, with the .pnml file itself never opened:
segment_a = refs.filter(pc.equal(refs["comment"], "Analisi_1_L1_A"))
segment_a.to_pandas()| event_id | external_id | access_points | comment | |
|---|---|---|---|---|
| 0 | part_1_voice0_measure1_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 1 | part_2_voice0_measure1_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 2 | part_2_voice1_measure1_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 3 | part_2_voice1_measure1_ev1 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 4 | part_1_voice0_measure2_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 5 | part_2_voice0_measure2_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 6 | part_2_voice1_measure2_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 7 | part_2_voice1_measure2_ev1 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 8 | part_1_voice0_measure3_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 9 | part_2_voice0_measure3_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 10 | part_2_voice1_measure3_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 11 | part_2_voice1_measure3_ev1 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 12 | part_1_voice0_measure4_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 13 | part_2_voice0_measure4_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 14 | part_2_voice1_measure4_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 15 | part_2_voice1_measure4_ev1 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 16 | part_1_voice0_measure40_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 17 | part_2_voice0_measure40_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 18 | part_2_voice1_measure40_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 19 | part_2_voice1_measure40_ev1 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 20 | part_1_voice0_measure41_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 21 | part_2_voice0_measure41_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 22 | part_2_voice1_measure41_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 23 | part_2_voice1_measure41_ev1 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 24 | part_1_voice0_measure42_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 25 | part_2_voice0_measure42_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 26 | part_2_voice1_measure42_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 27 | part_2_voice1_measure42_ev1 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 28 | part_1_voice0_measure43_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 29 | part_2_voice0_measure43_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 30 | part_2_voice1_measure43_ev0 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
| 31 | part_2_voice1_measure43_ev1 | p2 | [{'uri': 'Analisi_1/L1.pnml', 'kind': 'relativ... | Analisi_1_L1_A |
A segment no <place> names keeps its row rather than being dropped: external_id falls back to the segment id, access_points is empty, and the comment records why. Exactly one segment of this analysis is unmapped:
unmapped = refs.filter(pc.equal(pc.list_value_length(refs["access_points"]), 0))
unmapped.to_pandas()| event_id | external_id | access_points | comment | |
|---|---|---|---|---|
| 0 | part_1_voice0_measure72_ev0 | Analisi_1_L3_RS_I_23 | [] | segment without petri-net node |
Recap
| What the bundle expresses | How |
|---|---|
| The spine, a cumulative VTU axis | spine:dlt1, DiscreteLogicalTimeline, ticks |
| Notes, rests, lyrics at spine coordinates | los:dlt2, verbatim duration_num/duration_den |
| Engraved editions, page-image boxes | one nested SegmentLine per edition: |
4 page segments carrying 18 accolades, an IntervalToConstantMap to page images, pixels |
|
| Audio recordings | one ContinuousPhysicalTimeline per track, seconds |
| Every projection onto the spine | one columnar via loader.get_field(MatchClaim) |
| The cross-section over spine coordinates | bundle.get_matchstamp_table(from_graph=True) |
| The Petri-net analysis | spine.external_references — segment → place, resolved without opening any .pnml |
One IEEE 1599 document — one spine, several projections, one analytical annotation layer — loaded into a single in which every representation of the work stays reachable from the axis the document itself defines.