use std::collections::BTreeMap;
use sim_lib_pitch_core::PitchClass;
use sim_lib_pitch_set::PitchClassMask;
use crate::{RowError, ToneRow};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum OrderKind {
Total,
Partial,
Absent,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct BlockOrder {
pub within_blocks: OrderKind,
pub between_blocks: OrderKind,
}
impl BlockOrder {
pub const fn new(within_blocks: OrderKind, between_blocks: OrderKind) -> Self {
Self {
within_blocks,
between_blocks,
}
}
pub const fn total() -> Self {
Self::new(OrderKind::Total, OrderKind::Total)
}
pub const fn partially_ordered_blocks() -> Self {
Self::new(OrderKind::Total, OrderKind::Partial)
}
pub const fn unordered() -> Self {
Self::new(OrderKind::Absent, OrderKind::Absent)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RowPartitionBlock {
ordinals: Vec<u8>,
}
impl RowPartitionBlock {
pub fn ordinals(&self) -> &[u8] {
&self.ordinals
}
pub fn pitch_classes(&self, row: &ToneRow) -> Vec<PitchClass> {
self.ordinals
.iter()
.map(|ordinal| row.classes()[usize::from(*ordinal)])
.collect()
}
pub fn mask(&self, row: &ToneRow) -> PitchClassMask {
PitchClassMask::from_pitch_classes(&self.pitch_classes(row))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RowPartition {
blocks: Vec<RowPartitionBlock>,
order: BlockOrder,
}
impl RowPartition {
pub fn blocks(&self) -> &[RowPartitionBlock] {
&self.blocks
}
pub const fn order(&self) -> BlockOrder {
self.order
}
pub fn block_count(&self) -> usize {
self.blocks.len()
}
pub fn block_sizes(&self) -> Vec<usize> {
self.blocks
.iter()
.map(|block| block.ordinals.len())
.collect()
}
pub fn ordinals(&self) -> Vec<u8> {
self.blocks
.iter()
.flat_map(|block| block.ordinals.iter().copied())
.collect()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PartitionBlockMatch {
pub left_block_index: usize,
pub right_block_index: usize,
pub ordinals: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PartitionSimilarityReport {
pub left_block_sizes: Vec<usize>,
pub right_block_sizes: Vec<usize>,
pub same_order_contract: bool,
pub same_block_size_multiset: bool,
pub exact_block_matches: Vec<PartitionBlockMatch>,
pub overlap_matrix: Vec<Vec<usize>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AggregateCoverageReport {
pub aggregate: PitchClassMask,
pub covered: PitchClassMask,
pub missing: PitchClassMask,
pub complete: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InterlockingPartitionReport {
pub overlap_matrix: Vec<Vec<usize>>,
pub left_to_right_links: Vec<Vec<usize>>,
pub right_to_left_links: Vec<Vec<usize>>,
pub is_interlocking: bool,
}
pub fn try_partition(blocks: Vec<Vec<u8>>, order: BlockOrder) -> Result<RowPartition, RowError> {
let mut seen = BTreeMap::new();
let mut validated = Vec::with_capacity(blocks.len());
for (block_index, ordinals) in blocks.into_iter().enumerate() {
if ordinals.is_empty() {
return Err(RowError::EmptyPartitionBlock { block_index });
}
for ordinal in &ordinals {
if usize::from(*ordinal) >= 12 {
return Err(RowError::InvalidOrdinal {
ordinal: usize::from(*ordinal),
});
}
if let Some(first_block_index) = seen.insert(*ordinal, block_index) {
return Err(RowError::DuplicatePartitionOrdinal {
ordinal: *ordinal,
first_block_index,
second_block_index: block_index,
});
}
}
validated.push(RowPartitionBlock { ordinals });
}
let missing = (0u8..12)
.filter(|ordinal| !seen.contains_key(ordinal))
.collect::<Vec<_>>();
if !missing.is_empty() {
return Err(RowError::PartitionCoverageMismatch { missing });
}
Ok(RowPartition {
blocks: validated,
order,
})
}
pub fn analyze_partition_similarity(
left: &RowPartition,
right: &RowPartition,
) -> PartitionSimilarityReport {
let left_block_sizes = left.block_sizes();
let right_block_sizes = right.block_sizes();
let overlap_matrix = left
.blocks()
.iter()
.map(|left_block| {
right
.blocks()
.iter()
.map(|right_block| {
left_block
.ordinals()
.iter()
.filter(|ordinal| right_block.ordinals().contains(ordinal))
.count()
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
let mut left_sizes = left_block_sizes.clone();
let mut right_sizes = right_block_sizes.clone();
left_sizes.sort_unstable();
right_sizes.sort_unstable();
let exact_block_matches = left
.blocks()
.iter()
.enumerate()
.flat_map(|(left_block_index, left_block)| {
right
.blocks()
.iter()
.enumerate()
.filter(move |(_, right_block)| left_block.ordinals() == right_block.ordinals())
.map(
move |(right_block_index, _right_block)| PartitionBlockMatch {
left_block_index,
right_block_index,
ordinals: left_block.ordinals().to_vec(),
},
)
})
.collect();
PartitionSimilarityReport {
left_block_sizes,
right_block_sizes,
same_order_contract: left.order() == right.order(),
same_block_size_multiset: left_sizes == right_sizes,
exact_block_matches,
overlap_matrix,
}
}
pub fn analyze_partition_aggregate_coverage(
row: &ToneRow,
partition: &RowPartition,
) -> AggregateCoverageReport {
let masks = partition
.blocks()
.iter()
.map(|block| block.mask(row))
.collect::<Vec<_>>();
analyze_aggregate_coverage(PitchClassMask::from_pitch_classes(row.classes()), &masks)
}
pub fn analyze_aggregate_coverage(
aggregate: PitchClassMask,
masks: &[PitchClassMask],
) -> AggregateCoverageReport {
let covered = masks
.iter()
.copied()
.fold(PitchClassMask::default(), PitchClassMask::union);
let missing = aggregate.difference(covered);
AggregateCoverageReport {
aggregate,
covered,
missing,
complete: missing.bits() == 0,
}
}
pub fn analyze_interlocking_partitions(
left: &RowPartition,
right: &RowPartition,
) -> InterlockingPartitionReport {
let overlap_matrix = analyze_partition_similarity(left, right).overlap_matrix;
let left_to_right_links = overlap_matrix
.iter()
.map(|row| {
row.iter()
.enumerate()
.filter_map(|(index, overlap)| (*overlap > 0).then_some(index))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
let right_to_left_links = (0..right.block_count())
.map(|right_index| {
overlap_matrix
.iter()
.enumerate()
.filter_map(|(left_index, row)| (row[right_index] > 0).then_some(left_index))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
let is_interlocking = left_to_right_links.iter().all(|links| links.len() > 1)
&& right_to_left_links.iter().all(|links| links.len() > 1);
InterlockingPartitionReport {
overlap_matrix,
left_to_right_links,
right_to_left_links,
is_interlocking,
}
}