Skip to main content

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}