Skip to main content

sim_lib_pitch_serial/
derivation.rs

1//! Generator-cell derivation analysis for strict tone rows.
2
3use crate::{RowError, RowFamily, RowOperation, ToneRow};
4
5const DERIVATION_PARTITIONS: [usize; 4] = [2, 3, 4, 6];
6
7/// One detected derivation family for a fixed generator-cell size.
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct DerivationMatch {
10    /// The generator-cell size in row positions.
11    pub generator_size: usize,
12    /// The detected named derivation kind.
13    pub kind: DerivationKind,
14    /// The operation relating each partition cell back to the generator cell.
15    pub cells: Vec<DerivationCellRelation>,
16}
17
18/// Stable derivation names for the classical equal-cell partitions of a row.
19#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub enum DerivationKind {
21    /// Two-note generator cells.
22    Dyadic,
23    /// Three-note generator cells.
24    Trichordal,
25    /// Four-note generator cells.
26    Tetrachordal,
27    /// Six-note generator cells.
28    Hexachordal,
29}
30
31impl DerivationKind {
32    fn of_size(size: usize) -> Option<Self> {
33        match size {
34            2 => Some(Self::Dyadic),
35            3 => Some(Self::Trichordal),
36            4 => Some(Self::Tetrachordal),
37            6 => Some(Self::Hexachordal),
38            _ => None,
39        }
40    }
41}
42
43/// One partition cell together with the operation that derives it from the generator.
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct DerivationCellRelation {
46    /// Zero-based contiguous cell index in source-row order.
47    pub cell_index: usize,
48    /// The contiguous row ordinals belonging to this cell.
49    pub ordinals: Vec<u8>,
50    /// The exact affine/reversal operation mapping the generator onto this cell.
51    pub operation: RowOperation,
52}
53
54/// Complete derivation evidence for one strict row.
55#[derive(Clone, Debug, PartialEq, Eq, Default)]
56pub struct DerivationReport {
57    /// The largest detected generator-cell size, when any derivation is present.
58    pub generator_size: Option<usize>,
59    /// Every detected derivation family in ascending partition-size order.
60    pub matches: Vec<DerivationMatch>,
61}
62
63/// Detects dyadic, trichordal, tetrachordal, and hexachordal derivation.
64pub fn analyze_derivation(row: &ToneRow) -> DerivationReport {
65    let matches = DERIVATION_PARTITIONS
66        .into_iter()
67        .filter_map(|size| analyze_derivation_partition(row, size).ok().flatten())
68        .collect::<Vec<_>>();
69    let generator_size = matches.iter().map(|entry| entry.generator_size).max();
70    DerivationReport {
71        generator_size,
72        matches,
73    }
74}
75
76/// Detects derivation for one supported generator-cell size.
77pub fn analyze_derivation_partition(
78    row: &ToneRow,
79    generator_size: usize,
80) -> Result<Option<DerivationMatch>, RowError> {
81    let Some(kind) = DerivationKind::of_size(generator_size) else {
82        return Err(RowError::InvalidPartitionSize {
83            size: generator_size,
84        });
85    };
86    let generator = &row.classes()[0..generator_size];
87    let mut cells = Vec::with_capacity(12 / generator_size);
88    for (cell_index, chunk) in row.classes().chunks(generator_size).enumerate() {
89        let Some(operation) = related_operation(generator, chunk) else {
90            return Ok(None);
91        };
92        let start = cell_index * generator_size;
93        cells.push(DerivationCellRelation {
94            cell_index,
95            ordinals: (start..start + generator_size)
96                .map(|ordinal| ordinal as u8)
97                .collect(),
98            operation,
99        });
100    }
101    Ok(Some(DerivationMatch {
102        generator_size,
103        kind,
104        cells,
105    }))
106}
107
108fn related_operation(
109    left: &[sim_lib_pitch_core::PitchClass],
110    right: &[sim_lib_pitch_core::PitchClass],
111) -> Option<RowOperation> {
112    [RowFamily::P, RowFamily::I, RowFamily::R, RowFamily::RI]
113        .into_iter()
114        .flat_map(|family| (0..12).map(move |addend| RowOperation::new(family, addend)))
115        .find(|operation| apply_operation(left, *operation) == right)
116}
117
118fn apply_operation(
119    source: &[sim_lib_pitch_core::PitchClass],
120    operation: RowOperation,
121) -> Vec<sim_lib_pitch_core::PitchClass> {
122    let mut classes = source.to_vec();
123    if operation.family.is_retrograde() {
124        classes.reverse();
125    }
126    classes
127        .into_iter()
128        .map(|class| {
129            let class = if operation.family.is_inverted() {
130                class.invert(sim_lib_pitch_core::PitchClass::C)
131            } else {
132                class
133            };
134            class.transpose(i32::from(operation.addend))
135        })
136        .collect()
137}