sim_lib_pitch_serial/
combinatoriality.rs1use sim_lib_discrete_comb::permutations;
4use sim_lib_pitch_set::PitchClassMask;
5
6use crate::{RowError, RowFamilySet, RowOperation, ToneRow};
7
8const COMBINATORIAL_PARTITIONS: [usize; 4] = [2, 3, 4, 6];
9
10#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct CombinatorialBlockEvidence {
13 pub source_block_index: usize,
15 pub partner_block_index: usize,
17 pub source_ordinals: Vec<u8>,
19 pub partner_ordinals: Vec<u8>,
21 pub source: PitchClassMask,
23 pub complement: PitchClassMask,
25}
26
27#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct CombinatorialPartition {
30 pub block_size: usize,
32 pub partner_block_order: Vec<usize>,
34 pub blocks: Vec<CombinatorialBlockEvidence>,
36}
37
38#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct CombinatorialPartner {
41 pub operation: RowOperation,
43 pub source: PitchClassMask,
45 pub complement: PitchClassMask,
47 pub partition: CombinatorialPartition,
49}
50
51#[derive(Clone, Debug, PartialEq, Eq, Default)]
53pub struct CombinatorialityReport {
54 pub partners: Vec<CombinatorialPartner>,
56}
57
58pub fn analyze_combinatoriality(row: &ToneRow) -> CombinatorialityReport {
60 let family = RowFamilySet::of(row);
61 let mut partners = Vec::new();
62 for alias in family.aliases() {
63 for partition_size in COMBINATORIAL_PARTITIONS {
64 if let Ok(Some(partner)) =
65 analyze_combinatoriality_partition(row, alias.operation, partition_size)
66 {
67 partners.push(partner);
68 }
69 }
70 }
71 CombinatorialityReport { partners }
72}
73
74pub fn analyze_combinatoriality_partition(
76 row: &ToneRow,
77 operation: RowOperation,
78 partition_size: usize,
79) -> Result<Option<CombinatorialPartner>, RowError> {
80 validate_partition_size(partition_size)?;
81 let source_blocks = contiguous_masks(row, partition_size);
82 let partner_row = row.apply(operation).into_row();
83 let partner_blocks = contiguous_masks(&partner_row, partition_size);
84 let block_count = source_blocks.len();
85 let aggregate = PitchClassMask::from_pitch_classes(row.classes());
86
87 for partner_block_order in permutations(block_count) {
88 let blocks = source_blocks
89 .iter()
90 .enumerate()
91 .map(|(source_block_index, source)| {
92 let partner_block_index = partner_block_order[source_block_index];
93 let complement = partner_blocks[partner_block_index];
94 let start = source_block_index * partition_size;
95 let partner_start = partner_block_index * partition_size;
96 CombinatorialBlockEvidence {
97 source_block_index,
98 partner_block_index,
99 source_ordinals: (start..start + partition_size)
100 .map(|ordinal| ordinal as u8)
101 .collect(),
102 partner_ordinals: (partner_start..partner_start + partition_size)
103 .map(|ordinal| ordinal as u8)
104 .collect(),
105 source: *source,
106 complement,
107 }
108 })
109 .collect::<Vec<_>>();
110 let exact_cover = blocks.iter().all(|block| {
111 block.source.is_disjoint_from(block.complement)
112 && block.source.union(block.complement) == aggregate
113 });
114 if exact_cover {
115 let partition = CombinatorialPartition {
116 block_size: partition_size,
117 partner_block_order,
118 blocks,
119 };
120 return Ok(Some(CombinatorialPartner {
121 operation,
122 source: partition.blocks[0].source,
123 complement: partition.blocks[0].complement,
124 partition,
125 }));
126 }
127 }
128
129 Ok(None)
130}
131
132fn validate_partition_size(size: usize) -> Result<(), RowError> {
133 if COMBINATORIAL_PARTITIONS.contains(&size) {
134 Ok(())
135 } else {
136 Err(RowError::InvalidPartitionSize { size })
137 }
138}
139
140fn contiguous_masks(row: &ToneRow, partition_size: usize) -> Vec<PitchClassMask> {
141 row.classes()
142 .chunks(partition_size)
143 .map(PitchClassMask::from_pitch_classes)
144 .collect()
145}