sim_lib_music_serial/hypothesis.rs
1//! Ranked serial-row extraction hypotheses and stable evidence records.
2
3use sim_lib_music_core::{ObjectId, Time};
4use sim_lib_pitch_serial::{RowClassAlias, ToneRow};
5
6/// Exact half-open span used by extracted serial evidence.
7#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
8pub struct SerialTimeSpan {
9 /// Inclusive start.
10 pub start: Time,
11 /// Exclusive end.
12 pub end: Time,
13}
14
15impl SerialTimeSpan {
16 /// Builds an ordered half-open span.
17 pub fn new(start: Time, end: Time) -> Self {
18 debug_assert!(start <= end);
19 Self { start, end }
20 }
21
22 /// Returns the exact duration.
23 pub fn duration(&self) -> Time {
24 self.end - self.start
25 }
26}
27
28/// Stable sort key used when ranking extraction hypotheses.
29#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
30pub struct SerialStableRank {
31 /// Count of missing pitch classes from a complete twelve-tone aggregate.
32 pub omissions: usize,
33 /// Count of repeated pitch classes observed before aggregate completion.
34 pub duplicates_before_completion: usize,
35 /// Count of repeated pitch classes observed after aggregate completion.
36 pub order_errors: usize,
37 /// Exact span from the first contributing attack to the last contributing release.
38 pub occupied_span: Time,
39 /// Stable tie-breaker derived from the chosen attack ordering.
40 pub stable_key: String,
41}
42
43/// Ordering policy selected for one same-onset attack block.
44#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
45pub enum SerialReadingOrder {
46 /// Preserve the canonical exact-window note order.
47 WindowOrder,
48 /// Sort equal-onset attacks by pitch, low to high.
49 PitchAscending,
50 /// Sort equal-onset attacks by pitch, high to low.
51 PitchDescending,
52 /// Sort equal-onset attacks by voice id, low to high.
53 VoiceAscending,
54 /// Sort equal-onset attacks by voice id, high to low.
55 VoiceDescending,
56}
57
58impl SerialReadingOrder {
59 /// Stable machine-readable name.
60 pub const fn as_str(self) -> &'static str {
61 match self {
62 Self::WindowOrder => "window-order",
63 Self::PitchAscending => "pitch-ascending",
64 Self::PitchDescending => "pitch-descending",
65 Self::VoiceAscending => "voice-ascending",
66 Self::VoiceDescending => "voice-descending",
67 }
68 }
69}
70
71/// One note attack cited by a ranked hypothesis.
72#[derive(Clone, Debug, PartialEq, Eq)]
73pub struct SerialObservation {
74 /// Voice identity.
75 pub voice_id: ObjectId,
76 /// Source note identity.
77 pub note_id: ObjectId,
78 /// Source event identity.
79 pub event_id: ObjectId,
80 /// Zero-based pitch-class ordinal in the hypothesis row.
81 pub ordinal: usize,
82 /// Exact source attack span inherited from the sounding-window partition.
83 pub span: SerialTimeSpan,
84}
85
86/// One exact same-onset block chosen during extraction.
87#[derive(Clone, Debug, PartialEq, Eq)]
88pub struct SerialObservationBlock {
89 /// Exact span for the block's source sounding window.
90 pub span: SerialTimeSpan,
91 /// Ordering policy chosen for this block.
92 pub order: SerialReadingOrder,
93 /// Notes contributing new or repeated row evidence in chosen order.
94 pub observations: Vec<SerialObservation>,
95}
96
97/// Alias evidence attached to a ranked row hypothesis.
98#[derive(Clone, Debug, PartialEq, Eq)]
99pub struct SerialAliasEvidence {
100 /// The alias operation preserved by the observed row class.
101 pub alias: RowClassAlias,
102 /// Stable printable form such as `P0` or `RI11`.
103 pub label: String,
104}
105
106/// One ranked hypothesis extracted from exact source attacks.
107#[derive(Clone, Debug, PartialEq, Eq)]
108pub struct RankedSerialHypothesis {
109 /// Stable ranking key; lower values are stronger.
110 pub stable_rank: SerialStableRank,
111 /// Observed row order inferred from first pitch-class occurrence.
112 pub row: ToneRow,
113 /// Exact same-onset blocks cited by the hypothesis.
114 pub blocks: Vec<SerialObservationBlock>,
115 /// Count of repeated pitch classes seen before aggregate completion.
116 pub duplicates_before_completion: usize,
117 /// Count of repeated pitch classes seen after aggregate completion.
118 pub order_errors: usize,
119 /// Missing pitch classes relative to an exact twelve-tone aggregate.
120 pub omissions: usize,
121 /// Exact span covered by the hypothesis.
122 pub span: SerialTimeSpan,
123 /// P/I/R/RI alias evidence for the observed row class.
124 pub aliases: Vec<SerialAliasEvidence>,
125}