Skip to main content

sim_lib_pitch_set/
relation.rs

1//! Typed relations between canonical pitch-class set forms.
2
3use crate::{IntervalVector, PitchClassMask, SetClass, SetEquivalence, classify_set};
4
5/// Exact inclusion relation between two pitch-class masks.
6#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
7pub enum SetInclusion {
8    /// The masks contain exactly the same pitch classes.
9    Equal,
10    /// Every source pitch class occurs in the target, and the target has more.
11    ProperSubset,
12    /// Every target pitch class occurs in the source, and the source has more.
13    ProperSuperset,
14    /// The masks share pitch classes, but neither contains the other.
15    Overlap,
16    /// The masks have no pitch classes in common.
17    Disjoint,
18}
19
20/// Reproducible set-theory evidence relating two pitch-class masks.
21///
22/// Operator lists describe exact mask-to-mask transformations. Equivalence
23/// booleans are instead derived from the conventional phase-08 canonical forms,
24/// so callers never need to compare display labels or numeric normalization.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct SetRelationAnalysis {
27    /// Source set class under transposition-and-inversion equivalence.
28    pub source_class: SetClass,
29    /// Target set class under transposition-and-inversion equivalence.
30    pub target_class: SetClass,
31    /// Exact inclusion relation between the source and target masks.
32    pub inclusion: SetInclusion,
33    /// Pitch classes present in both masks.
34    pub common_tones: PitchClassMask,
35    /// Transposition amounts `Tn` that map the source mask exactly to the target.
36    pub transpositions: Vec<u8>,
37    /// Conventional inversion indices `TnI` mapping source exactly to target.
38    pub inversion_indices: Vec<u8>,
39    /// Whether conventional transposition-only prime forms agree.
40    pub transposition_equivalent: bool,
41    /// Whether conventional transposition-and-inversion prime forms agree.
42    pub transposition_inversion_equivalent: bool,
43    /// Whether the target is the source's exact aggregate complement.
44    pub exact_complement: bool,
45    /// Whether the target is Tn/TnI-equivalent to the source's complement.
46    pub complement_equivalent: bool,
47    /// Whether the masks have equal interval vectors but distinct Tn/TnI forms.
48    pub z_related: bool,
49    /// Source interval-class census.
50    pub source_interval_vector: IntervalVector,
51    /// Target interval-class census.
52    pub target_interval_vector: IntervalVector,
53}
54
55/// Analyzes exact and canonical relations between two pitch-class masks.
56///
57/// This reports every exact `Tn` and `TnI` operator rather than choosing one,
58/// which preserves symmetric-set evidence. Conventional equivalence and
59/// complement equivalence are computed from [`SetEquivalence`] prime forms.
60pub fn analyze_set_relations(
61    source: PitchClassMask,
62    target: PitchClassMask,
63) -> SetRelationAnalysis {
64    let common_tones = source.intersection(target);
65    let inclusion = if source == target {
66        SetInclusion::Equal
67    } else if source.is_subset_of(target) {
68        SetInclusion::ProperSubset
69    } else if source.is_superset_of(target) {
70        SetInclusion::ProperSuperset
71    } else if source.is_disjoint_from(target) {
72        SetInclusion::Disjoint
73    } else {
74        SetInclusion::Overlap
75    };
76
77    let transpositions = (0..12)
78        .filter(|shift| source.rotate(i32::from(*shift)) == target)
79        .collect();
80    let inversion_indices = (0..12)
81        .filter(|index| source.invert_tni(*index) == target)
82        .collect();
83
84    let source_t = classify_set(source, SetEquivalence::Transposition);
85    let target_t = classify_set(target, SetEquivalence::Transposition);
86    let source_class = classify_set(source, SetEquivalence::TranspositionInversion);
87    let target_class = classify_set(target, SetEquivalence::TranspositionInversion);
88    let complement_class =
89        classify_set(source.complement(), SetEquivalence::TranspositionInversion);
90    let source_interval_vector = source.interval_vector();
91    let target_interval_vector = target.interval_vector();
92
93    SetRelationAnalysis {
94        source_class,
95        target_class: target_class.clone(),
96        inclusion,
97        common_tones,
98        transpositions,
99        inversion_indices,
100        transposition_equivalent: source_t.prime == target_t.prime,
101        transposition_inversion_equivalent: source_class_prime_eq(source, target),
102        exact_complement: source.complement() == target,
103        complement_equivalent: complement_class.prime == target_class.prime,
104        z_related: source.is_z_related_to(target),
105        source_interval_vector,
106        target_interval_vector,
107    }
108}
109
110fn source_class_prime_eq(source: PitchClassMask, target: PitchClassMask) -> bool {
111    classify_set(source, SetEquivalence::TranspositionInversion).prime
112        == classify_set(target, SetEquivalence::TranspositionInversion).prime
113}