Skip to main content

sim_lib_pitch_serial/
invariance.rs

1//! Independent ordered and unordered invariance comparisons for row segments.
2
3use sim_lib_pitch_core::PitchClass;
4use sim_lib_pitch_set::analyze_set_relations;
5
6use crate::RowSegment;
7
8/// Independent invariance facts relating two ordered row segments.
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct SegmentInvariant {
11    /// Whether the compared segments use the same source ordinals in the same order.
12    pub ordinal_identity: bool,
13    /// Whether the compared segments contain the same pitch classes in the same order.
14    pub pitch_identity: bool,
15    /// Exact `Tn` preserving the segment's ordered pitch classes, when present.
16    pub transposition: Option<u8>,
17    /// Exact `TnI` preserving the segment's ordered pitch classes, when present.
18    pub inversion: Option<u8>,
19    /// Whether the directed ordered-interval strings match exactly.
20    pub interval_order_identity: bool,
21    /// Whether the unordered pitch-class projections share one set class.
22    pub set_class_identity: bool,
23}
24
25/// Compares two ordered row segments without conflating ordered and unordered facts.
26pub fn analyze_invariance(left: &RowSegment, right: &RowSegment) -> SegmentInvariant {
27    let relation = analyze_set_relations(left.mask(), right.mask());
28    SegmentInvariant {
29        ordinal_identity: left.ordinals() == right.ordinals(),
30        pitch_identity: left.classes() == right.classes(),
31        transposition: ordered_transposition(left.classes(), right.classes()),
32        inversion: ordered_inversion(left.classes(), right.classes()),
33        interval_order_identity: left.ordered_intervals() == right.ordered_intervals(),
34        set_class_identity: relation.transposition_inversion_equivalent,
35    }
36}
37
38fn ordered_transposition(left: &[PitchClass], right: &[PitchClass]) -> Option<u8> {
39    if left.len() != right.len() {
40        return None;
41    }
42    let Some((first_left, first_right)) = left.first().zip(right.first()) else {
43        return Some(0);
44    };
45    let shift = (12 + i16::from(first_right.value()) - i16::from(first_left.value())) as u8 % 12;
46    left.iter()
47        .zip(right)
48        .all(|(source, target)| source.transpose(i32::from(shift)) == *target)
49        .then_some(shift)
50}
51
52fn ordered_inversion(left: &[PitchClass], right: &[PitchClass]) -> Option<u8> {
53    if left.len() != right.len() {
54        return None;
55    }
56    let Some((first_left, first_right)) = left.first().zip(right.first()) else {
57        return Some(0);
58    };
59    let index = (first_left.value() + first_right.value()) % 12;
60    left.iter()
61        .zip(right)
62        .all(|(source, target)| target.value() == (12 + index - source.value()) % 12)
63        .then_some(index)
64}