sim_lib_pitch_serial/reservoir.rs
1//! Ordered pitch reservoirs used when strict row invariants are relaxed.
2
3use sim_lib_pitch_core::PitchClass;
4use sim_lib_pitch_set::PitchClassMask;
5
6/// One ordered pitch block inside a reservoir.
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct OrderedPitchBlock {
9 /// Exact pitch classes in retained presentation order.
10 pub pitch_classes: Vec<PitchClass>,
11 /// Unordered pitch-class content of the block.
12 pub mask: PitchClassMask,
13}
14
15/// A source invariant that no longer holds after a transform.
16#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub enum PitchInvariant {
18 /// The result is no longer one strict twelve-position row.
19 TotalOrder,
20 /// The result no longer contains each pitch class exactly once.
21 AggregateIdentity,
22}
23
24/// Which row invariants a transform preserved or relaxed.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct InvariantDelta {
27 /// Whether the result remains one strict total ordering of twelve positions.
28 pub retains_total_order: bool,
29 /// Whether the result still contains each pitch class exactly once.
30 pub retains_aggregate_identity: bool,
31 /// Named invariants intentionally relaxed by the transform.
32 pub relaxed_invariants: Vec<PitchInvariant>,
33}
34
35/// Provenance for one reservoir block.
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct BlockProjection {
38 /// Result block index within the reservoir.
39 pub block_index: usize,
40 /// How this block was derived from source material.
41 pub source: BlockProjectionSource,
42}
43
44/// The source relationship that produced one reservoir block.
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub enum BlockProjectionSource {
47 /// Several source ordinals collapsed onto one pitch class under a non-bijective affine map.
48 OrdinalCollapse {
49 /// Source row ordinals contributing to the collapsed block.
50 source_ordinals: Vec<u8>,
51 /// The common mapped pitch class.
52 target_pitch_class: PitchClass,
53 },
54 /// One block's interval content was projected onto another block's pitches.
55 BlockMultiplication {
56 /// Source block index providing anchor pitches.
57 anchor_block_index: usize,
58 /// Source row ordinals providing anchor pitches.
59 anchor_ordinals: Vec<u8>,
60 /// Source block index providing interval content.
61 interval_block_index: usize,
62 /// Source row ordinals providing interval content.
63 interval_ordinals: Vec<u8>,
64 /// Ordered intervals projected from the interval block's first pitch.
65 interval_content: Vec<u8>,
66 },
67}
68
69/// An ordered collection of pitch blocks whose result is not a strict tone row.
70#[derive(Clone, Debug, PartialEq, Eq)]
71pub struct PitchReservoir {
72 /// Result blocks in retained presentation order.
73 pub blocks: Vec<OrderedPitchBlock>,
74 /// Provenance for each result block.
75 pub provenance: Vec<BlockProjection>,
76 /// Which strict-row invariants no longer hold.
77 pub invariant_delta: InvariantDelta,
78}
79
80impl PitchReservoir {
81 /// Constructs a reservoir and derives its invariant delta from the supplied blocks.
82 pub fn new(blocks: Vec<OrderedPitchBlock>, provenance: Vec<BlockProjection>) -> Self {
83 let mut counts = [0u8; 12];
84 for pitch_class in blocks.iter().flat_map(|block| block.pitch_classes.iter()) {
85 counts[usize::from(pitch_class.value())] += 1;
86 }
87 let retains_aggregate_identity = counts.iter().all(|count| *count == 1);
88 let retains_total_order = blocks.len() == 1
89 && blocks
90 .first()
91 .is_some_and(|block| block.pitch_classes.len() == 12 && retains_aggregate_identity);
92 let mut relaxed_invariants = Vec::new();
93 if !retains_total_order {
94 relaxed_invariants.push(PitchInvariant::TotalOrder);
95 }
96 if !retains_aggregate_identity {
97 relaxed_invariants.push(PitchInvariant::AggregateIdentity);
98 }
99 Self {
100 blocks,
101 provenance,
102 invariant_delta: InvariantDelta {
103 retains_total_order,
104 retains_aggregate_identity,
105 relaxed_invariants,
106 },
107 }
108 }
109}