Skip to main content

sim_lib_pitch_serial/
combinatoriality.rs

1//! Combinatorial partner analysis for strict tone rows.
2
3use 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/// One paired source/partner block that covers the full aggregate exactly.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct CombinatorialBlockEvidence {
13    /// Source block index in contiguous partition order.
14    pub source_block_index: usize,
15    /// Partner block index in contiguous partition order.
16    pub partner_block_index: usize,
17    /// Source row ordinals contributing the source block.
18    pub source_ordinals: Vec<u8>,
19    /// Partner row ordinals contributing the complementary block.
20    pub partner_ordinals: Vec<u8>,
21    /// The source block's pitch-class mask.
22    pub source: PitchClassMask,
23    /// The partner block's complementary pitch-class mask.
24    pub complement: PitchClassMask,
25}
26
27/// One successful contiguous equal-cell partition witnessing combinatoriality.
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct CombinatorialPartition {
30    /// The block size in row positions.
31    pub block_size: usize,
32    /// The permutation mapping source blocks onto partner blocks.
33    pub partner_block_order: Vec<usize>,
34    /// Exact complementary evidence for every paired block.
35    pub blocks: Vec<CombinatorialBlockEvidence>,
36}
37
38/// One partner row form that is combinatorial with the source row.
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct CombinatorialPartner {
41    /// The exact row-family operation producing the partner row.
42    pub operation: RowOperation,
43    /// One representative source block mask.
44    pub source: PitchClassMask,
45    /// The complementary partner block mask matching `source`.
46    pub complement: PitchClassMask,
47    /// The validated equal-cell partition witness.
48    pub partition: CombinatorialPartition,
49}
50
51/// Complete combinatorial partner evidence for a strict row.
52#[derive(Clone, Debug, PartialEq, Eq, Default)]
53pub struct CombinatorialityReport {
54    /// Every detected prime, inversional, retrograde, or retrograde-inversional partner.
55    pub partners: Vec<CombinatorialPartner>,
56}
57
58/// Detects combinatorial partners over the supported equal-cell partitions.
59pub 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
74/// Detects combinatoriality for one partner operation and one supported partition size.
75pub 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}