1use std::collections::BTreeMap;
4
5use sim_lib_pitch_core::PitchClass;
6use sim_lib_pitch_set::PitchClassMask;
7
8use crate::{RowError, ToneRow};
9
10#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
12pub enum OrderKind {
13 Total,
15 Partial,
17 Absent,
19}
20
21#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
23pub struct BlockOrder {
24 pub within_blocks: OrderKind,
26 pub between_blocks: OrderKind,
28}
29
30impl BlockOrder {
31 pub const fn new(within_blocks: OrderKind, between_blocks: OrderKind) -> Self {
33 Self {
34 within_blocks,
35 between_blocks,
36 }
37 }
38
39 pub const fn total() -> Self {
41 Self::new(OrderKind::Total, OrderKind::Total)
42 }
43
44 pub const fn partially_ordered_blocks() -> Self {
46 Self::new(OrderKind::Total, OrderKind::Partial)
47 }
48
49 pub const fn unordered() -> Self {
51 Self::new(OrderKind::Absent, OrderKind::Absent)
52 }
53}
54
55#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct RowPartitionBlock {
58 ordinals: Vec<u8>,
59}
60
61impl RowPartitionBlock {
62 pub fn ordinals(&self) -> &[u8] {
64 &self.ordinals
65 }
66
67 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 pub fn mask(&self, row: &ToneRow) -> PitchClassMask {
77 PitchClassMask::from_pitch_classes(&self.pitch_classes(row))
78 }
79}
80
81#[derive(Clone, Debug, PartialEq, Eq)]
83pub struct RowPartition {
84 blocks: Vec<RowPartitionBlock>,
85 order: BlockOrder,
86}
87
88impl RowPartition {
89 pub fn blocks(&self) -> &[RowPartitionBlock] {
91 &self.blocks
92 }
93
94 pub const fn order(&self) -> BlockOrder {
96 self.order
97 }
98
99 pub fn block_count(&self) -> usize {
101 self.blocks.len()
102 }
103
104 pub fn block_sizes(&self) -> Vec<usize> {
106 self.blocks
107 .iter()
108 .map(|block| block.ordinals.len())
109 .collect()
110 }
111
112 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#[derive(Clone, Debug, PartialEq, Eq)]
123pub struct PartitionBlockMatch {
124 pub left_block_index: usize,
126 pub right_block_index: usize,
128 pub ordinals: Vec<u8>,
130}
131
132#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct PartitionSimilarityReport {
135 pub left_block_sizes: Vec<usize>,
137 pub right_block_sizes: Vec<usize>,
139 pub same_order_contract: bool,
141 pub same_block_size_multiset: bool,
143 pub exact_block_matches: Vec<PartitionBlockMatch>,
145 pub overlap_matrix: Vec<Vec<usize>>,
147}
148
149#[derive(Clone, Debug, PartialEq, Eq)]
151pub struct AggregateCoverageReport {
152 pub aggregate: PitchClassMask,
154 pub covered: PitchClassMask,
156 pub missing: PitchClassMask,
158 pub complete: bool,
160}
161
162#[derive(Clone, Debug, PartialEq, Eq)]
164pub struct InterlockingPartitionReport {
165 pub overlap_matrix: Vec<Vec<usize>>,
167 pub left_to_right_links: Vec<Vec<usize>>,
169 pub right_to_left_links: Vec<Vec<usize>>,
171 pub is_interlocking: bool,
173}
174
175pub 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
211pub 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
271pub 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
284pub 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
302pub 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}