Skip to main content

sim_lib_pitch_serial/
partition.rs

1//! Validated row partitions and ordinal overlap analysis.
2
3use std::collections::BTreeMap;
4
5use sim_lib_pitch_core::PitchClass;
6use sim_lib_pitch_set::PitchClassMask;
7
8use crate::{RowError, ToneRow};
9
10/// How strongly a partition treats order at one structural level.
11#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
12pub enum OrderKind {
13    /// Relative order is fully significant.
14    Total,
15    /// Some order matters, but the structure is not a strict sequence.
16    Partial,
17    /// Order is intentionally not part of the claim.
18    Absent,
19}
20
21/// The ordering contract for a row partition within and between its blocks.
22#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
23pub struct BlockOrder {
24    /// Whether ordinal order matters inside each block.
25    pub within_blocks: OrderKind,
26    /// Whether the block list itself is ordered.
27    pub between_blocks: OrderKind,
28}
29
30impl BlockOrder {
31    /// Creates an explicit ordering contract.
32    pub const fn new(within_blocks: OrderKind, between_blocks: OrderKind) -> Self {
33        Self {
34            within_blocks,
35            between_blocks,
36        }
37    }
38
39    /// Marks both block-internal and block-to-block order as total.
40    pub const fn total() -> Self {
41        Self::new(OrderKind::Total, OrderKind::Total)
42    }
43
44    /// Marks block-internal order as total but block-to-block order as partial.
45    pub const fn partially_ordered_blocks() -> Self {
46        Self::new(OrderKind::Total, OrderKind::Partial)
47    }
48
49    /// Marks both block-internal and block-to-block order as intentionally absent.
50    pub const fn unordered() -> Self {
51        Self::new(OrderKind::Absent, OrderKind::Absent)
52    }
53}
54
55/// One nonempty block of row ordinals in caller-declared order.
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct RowPartitionBlock {
58    ordinals: Vec<u8>,
59}
60
61impl RowPartitionBlock {
62    /// Returns the block's row ordinals in the caller-declared order.
63    pub fn ordinals(&self) -> &[u8] {
64        &self.ordinals
65    }
66
67    /// Returns the pitch classes reached by this block on `row`.
68    pub fn pitch_classes(&self, row: &ToneRow) -> Vec<PitchClass> {
69        self.ordinals
70            .iter()
71            .map(|ordinal| row.classes()[usize::from(*ordinal)])
72            .collect()
73    }
74
75    /// Returns the unordered pitch-class mask reached by this block on `row`.
76    pub fn mask(&self, row: &ToneRow) -> PitchClassMask {
77        PitchClassMask::from_pitch_classes(&self.pitch_classes(row))
78    }
79}
80
81/// A validated partition of all twelve row ordinals into disjoint nonempty blocks.
82#[derive(Clone, Debug, PartialEq, Eq)]
83pub struct RowPartition {
84    blocks: Vec<RowPartitionBlock>,
85    order: BlockOrder,
86}
87
88impl RowPartition {
89    /// Returns the validated blocks in caller order.
90    pub fn blocks(&self) -> &[RowPartitionBlock] {
91        &self.blocks
92    }
93
94    /// Returns the partition's ordering contract.
95    pub const fn order(&self) -> BlockOrder {
96        self.order
97    }
98
99    /// Returns the number of validated blocks.
100    pub fn block_count(&self) -> usize {
101        self.blocks.len()
102    }
103
104    /// Returns the size of each block in caller order.
105    pub fn block_sizes(&self) -> Vec<usize> {
106        self.blocks
107            .iter()
108            .map(|block| block.ordinals.len())
109            .collect()
110    }
111
112    /// Returns all row ordinals in block-major caller order.
113    pub fn ordinals(&self) -> Vec<u8> {
114        self.blocks
115            .iter()
116            .flat_map(|block| block.ordinals.iter().copied())
117            .collect()
118    }
119}
120
121/// One exact block match shared by two validated partitions.
122#[derive(Clone, Debug, PartialEq, Eq)]
123pub struct PartitionBlockMatch {
124    /// Matching block index in the left partition.
125    pub left_block_index: usize,
126    /// Matching block index in the right partition.
127    pub right_block_index: usize,
128    /// Shared row ordinals, preserved in the left block's order.
129    pub ordinals: Vec<u8>,
130}
131
132/// Similarity evidence between two validated row partitions.
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct PartitionSimilarityReport {
135    /// Block sizes from the left partition.
136    pub left_block_sizes: Vec<usize>,
137    /// Block sizes from the right partition.
138    pub right_block_sizes: Vec<usize>,
139    /// Whether the two partitions use the same ordering contract.
140    pub same_order_contract: bool,
141    /// Whether the two partitions have the same block-size multiset.
142    pub same_block_size_multiset: bool,
143    /// Exact block matches regardless of block position.
144    pub exact_block_matches: Vec<PartitionBlockMatch>,
145    /// Cardinality of every left/right block overlap.
146    pub overlap_matrix: Vec<Vec<usize>>,
147}
148
149/// Aggregate pitch-class coverage assembled from partition-derived blocks.
150#[derive(Clone, Debug, PartialEq, Eq)]
151pub struct AggregateCoverageReport {
152    /// The target aggregate expected by the caller.
153    pub aggregate: PitchClassMask,
154    /// The union of every supplied block mask.
155    pub covered: PitchClassMask,
156    /// Pitch classes still missing from `covered`.
157    pub missing: PitchClassMask,
158    /// Whether `covered` equals the requested aggregate exactly.
159    pub complete: bool,
160}
161
162/// Interlocking evidence between two validated row partitions.
163#[derive(Clone, Debug, PartialEq, Eq)]
164pub struct InterlockingPartitionReport {
165    /// Cardinality of every left/right block overlap.
166    pub overlap_matrix: Vec<Vec<usize>>,
167    /// For each left block, the right blocks with nonzero overlap.
168    pub left_to_right_links: Vec<Vec<usize>>,
169    /// For each right block, the left blocks with nonzero overlap.
170    pub right_to_left_links: Vec<Vec<usize>>,
171    /// Whether every block on both sides overlaps more than one opposite block.
172    pub is_interlocking: bool,
173}
174
175/// Validates a caller-declared row partition over ordinals `0..12`.
176pub fn try_partition(blocks: Vec<Vec<u8>>, order: BlockOrder) -> Result<RowPartition, RowError> {
177    let mut seen = BTreeMap::new();
178    let mut validated = Vec::with_capacity(blocks.len());
179    for (block_index, ordinals) in blocks.into_iter().enumerate() {
180        if ordinals.is_empty() {
181            return Err(RowError::EmptyPartitionBlock { block_index });
182        }
183        for ordinal in &ordinals {
184            if usize::from(*ordinal) >= 12 {
185                return Err(RowError::InvalidOrdinal {
186                    ordinal: usize::from(*ordinal),
187                });
188            }
189            if let Some(first_block_index) = seen.insert(*ordinal, block_index) {
190                return Err(RowError::DuplicatePartitionOrdinal {
191                    ordinal: *ordinal,
192                    first_block_index,
193                    second_block_index: block_index,
194                });
195            }
196        }
197        validated.push(RowPartitionBlock { ordinals });
198    }
199    let missing = (0u8..12)
200        .filter(|ordinal| !seen.contains_key(ordinal))
201        .collect::<Vec<_>>();
202    if !missing.is_empty() {
203        return Err(RowError::PartitionCoverageMismatch { missing });
204    }
205    Ok(RowPartition {
206        blocks: validated,
207        order,
208    })
209}
210
211/// Compares two validated partitions without collapsing their block order contracts.
212pub fn analyze_partition_similarity(
213    left: &RowPartition,
214    right: &RowPartition,
215) -> PartitionSimilarityReport {
216    let left_block_sizes = left.block_sizes();
217    let right_block_sizes = right.block_sizes();
218    let overlap_matrix = left
219        .blocks()
220        .iter()
221        .map(|left_block| {
222            right
223                .blocks()
224                .iter()
225                .map(|right_block| {
226                    left_block
227                        .ordinals()
228                        .iter()
229                        .filter(|ordinal| right_block.ordinals().contains(ordinal))
230                        .count()
231                })
232                .collect::<Vec<_>>()
233        })
234        .collect::<Vec<_>>();
235
236    let mut left_sizes = left_block_sizes.clone();
237    let mut right_sizes = right_block_sizes.clone();
238    left_sizes.sort_unstable();
239    right_sizes.sort_unstable();
240
241    let exact_block_matches = left
242        .blocks()
243        .iter()
244        .enumerate()
245        .flat_map(|(left_block_index, left_block)| {
246            right
247                .blocks()
248                .iter()
249                .enumerate()
250                .filter(move |(_, right_block)| left_block.ordinals() == right_block.ordinals())
251                .map(
252                    move |(right_block_index, _right_block)| PartitionBlockMatch {
253                        left_block_index,
254                        right_block_index,
255                        ordinals: left_block.ordinals().to_vec(),
256                    },
257                )
258        })
259        .collect();
260
261    PartitionSimilarityReport {
262        left_block_sizes,
263        right_block_sizes,
264        same_order_contract: left.order() == right.order(),
265        same_block_size_multiset: left_sizes == right_sizes,
266        exact_block_matches,
267        overlap_matrix,
268    }
269}
270
271/// Computes aggregate pitch-class coverage for one validated partition on `row`.
272pub fn analyze_partition_aggregate_coverage(
273    row: &ToneRow,
274    partition: &RowPartition,
275) -> AggregateCoverageReport {
276    let masks = partition
277        .blocks()
278        .iter()
279        .map(|block| block.mask(row))
280        .collect::<Vec<_>>();
281    analyze_aggregate_coverage(PitchClassMask::from_pitch_classes(row.classes()), &masks)
282}
283
284/// Computes aggregate pitch-class coverage for any caller-supplied block masks.
285pub fn analyze_aggregate_coverage(
286    aggregate: PitchClassMask,
287    masks: &[PitchClassMask],
288) -> AggregateCoverageReport {
289    let covered = masks
290        .iter()
291        .copied()
292        .fold(PitchClassMask::default(), PitchClassMask::union);
293    let missing = aggregate.difference(covered);
294    AggregateCoverageReport {
295        aggregate,
296        covered,
297        missing,
298        complete: missing.bits() == 0,
299    }
300}
301
302/// Reports how two validated partitions weave their ordinals across one another.
303pub fn analyze_interlocking_partitions(
304    left: &RowPartition,
305    right: &RowPartition,
306) -> InterlockingPartitionReport {
307    let overlap_matrix = analyze_partition_similarity(left, right).overlap_matrix;
308    let left_to_right_links = overlap_matrix
309        .iter()
310        .map(|row| {
311            row.iter()
312                .enumerate()
313                .filter_map(|(index, overlap)| (*overlap > 0).then_some(index))
314                .collect::<Vec<_>>()
315        })
316        .collect::<Vec<_>>();
317    let right_to_left_links = (0..right.block_count())
318        .map(|right_index| {
319            overlap_matrix
320                .iter()
321                .enumerate()
322                .filter_map(|(left_index, row)| (row[right_index] > 0).then_some(left_index))
323                .collect::<Vec<_>>()
324        })
325        .collect::<Vec<_>>();
326    let is_interlocking = left_to_right_links.iter().all(|links| links.len() > 1)
327        && right_to_left_links.iter().all(|links| links.len() > 1);
328    InterlockingPartitionReport {
329        overlap_matrix,
330        left_to_right_links,
331        right_to_left_links,
332        is_interlocking,
333    }
334}