Skip to main content

sim_lib_music_serial/
referential.rs

1//! Bounded referential and tonal-anchor analysis for serial subsets.
2
3use sim_lib_pitch_chord::ChordTemplate;
4use sim_lib_pitch_core::PitchClass;
5use sim_lib_pitch_namer_roman::label_roman;
6use sim_lib_pitch_ratio::{PitchRatio, RatioPolicy, analyze_ratio_chord};
7use sim_lib_pitch_scale::{Key, Scale};
8use sim_lib_pitch_serial::RowForm;
9use sim_lib_pitch_set::PitchClassMask;
10
11use crate::{ReferentialEmphasis, techniques::referential::build_referential_report};
12
13/// Bounded referential claims made over one row subset.
14#[derive(Copy, Clone, Debug, PartialEq, Eq)]
15pub enum ReferentialClaim {
16    /// The named evidence is embedded in the requested subset.
17    EmbeddedSubset,
18    /// The named evidence does not match the requested subset.
19    NoReferentialSubset,
20}
21
22/// The evidence category backing a referential claim.
23#[derive(Copy, Clone, Debug, PartialEq, Eq)]
24pub enum ReferentialEvidenceKind {
25    /// Named chord evidence.
26    Chord,
27    /// Named scale evidence.
28    Scale,
29    /// Named key-region evidence.
30    KeyRegion,
31    /// Exact pitch-ratio evidence.
32    PitchRatio,
33    /// Caller-supplied pitch-class mask evidence.
34    Mask,
35}
36
37/// Supported named evidence for one referential subset.
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub enum ReferentialEvidence {
40    /// A named chord-template target.
41    Chord {
42        /// Stable caller-facing label for the chord evidence.
43        label: String,
44        /// Exact chord template used to name the subset.
45        chord: ChordTemplate,
46    },
47    /// A named scale target.
48    Scale {
49        /// Stable caller-facing label for the scale evidence.
50        label: String,
51        /// Exact scale used to test subset membership.
52        scale: Scale,
53    },
54    /// A named key-region target with optional preferred root for labeling.
55    KeyRegion {
56        /// Stable caller-facing label for the key-region evidence.
57        label: String,
58        /// Key context used for membership and roman-numeral naming.
59        key: Key,
60        /// Preferred root used when the subset needs an explicit harmonic root.
61        preferred_root: Option<PitchClass>,
62    },
63    /// Exact ratio evidence for the subset's voices.
64    PitchRatio {
65        /// Stable caller-facing label for the ratio evidence.
66        label: String,
67        /// Exact root-relative ratios attached to the subset.
68        ratios: Vec<PitchRatio>,
69        /// Canonicalization policy used while evaluating the ratios.
70        policy: RatioPolicy,
71    },
72    /// Caller-declared mask evidence.
73    Mask {
74        /// Stable caller-facing label for the mask evidence.
75        label: String,
76        /// Exact unordered pitch-class evidence supplied by the caller.
77        mask: PitchClassMask,
78    },
79}
80
81impl ReferentialEvidence {
82    /// Returns the stable evidence label supplied by the caller.
83    pub fn label(&self) -> &str {
84        match self {
85            Self::Chord { label, .. }
86            | Self::Scale { label, .. }
87            | Self::KeyRegion { label, .. }
88            | Self::PitchRatio { label, .. }
89            | Self::Mask { label, .. } => label,
90        }
91    }
92
93    /// Returns the evidence kind.
94    pub const fn kind(&self) -> ReferentialEvidenceKind {
95        match self {
96            Self::Chord { .. } => ReferentialEvidenceKind::Chord,
97            Self::Scale { .. } => ReferentialEvidenceKind::Scale,
98            Self::KeyRegion { .. } => ReferentialEvidenceKind::KeyRegion,
99            Self::PitchRatio { .. } => ReferentialEvidenceKind::PitchRatio,
100            Self::Mask { .. } => ReferentialEvidenceKind::Mask,
101        }
102    }
103}
104
105/// Exact ratio/sonance summary attached to one claim.
106#[derive(Clone, Debug, PartialEq)]
107pub struct ReferentialRatioSummary {
108    /// Policy used to admit and normalize ratios.
109    pub policy: RatioPolicy,
110    /// Number of admitted tones.
111    pub admitted_tones: usize,
112    /// Number of rejected tones.
113    pub rejected_tones: usize,
114    /// Generalized-mean complexity cost.
115    pub cost: f64,
116}
117
118/// Exact naming and sonance context behind one claim.
119#[derive(Clone, Debug, PartialEq)]
120pub struct ReferentialContextReport {
121    /// Evidence category backing the analysis.
122    pub evidence_kind: ReferentialEvidenceKind,
123    /// Caller-supplied evidence label.
124    pub evidence_label: String,
125    /// Exact text claim, when one can be named.
126    pub harmonic_claim: Option<String>,
127    /// Exact pitch classes at the requested ordinals.
128    pub subset_classes: Vec<PitchClass>,
129    /// Unordered subset projection.
130    pub subset_mask: PitchClassMask,
131    /// One-based scale degrees reached under scale/key evidence.
132    pub scale_degrees: Vec<usize>,
133    /// Optional exact ratio/sonance summary.
134    pub ratio_summary: Option<ReferentialRatioSummary>,
135}
136
137/// One request to analyze a row subset as bounded referential evidence.
138#[derive(Clone, Debug, PartialEq)]
139pub struct ReferentialRequest {
140    /// Zero-based source ordinals whose subset is being named.
141    pub ordinals: Vec<usize>,
142    /// Named evidence attached to the subset.
143    pub evidence: ReferentialEvidence,
144    /// Optional non-pitch emphasis around the subset.
145    pub emphasis: ReferentialEmphasis,
146}
147
148/// Full bounded report over one referential subset.
149#[derive(Clone, Debug, PartialEq)]
150pub struct ReferentialReport {
151    /// The bounded claim result.
152    pub claim: ReferentialClaim,
153    /// Requested source ordinals echoed exactly.
154    pub ordinals: Vec<usize>,
155    /// Exact context behind the claim.
156    pub context: ReferentialContextReport,
157    /// Non-pitch emphasis retained around the subset.
158    pub emphasis: ReferentialEmphasis,
159    /// Always `false`: this surface never escalates one subset into whole-passage tonality.
160    pub claims_whole_passage_tonality: bool,
161}
162
163/// Analyzes one row subset against named evidence without asserting global tonality.
164pub fn analyze_referential_subset(
165    row_form: &RowForm,
166    request: ReferentialRequest,
167) -> Result<ReferentialReport, String> {
168    build_referential_report(row_form, request)
169}
170
171pub(crate) fn roman_claim(
172    subset_mask: PitchClassMask,
173    key: Key,
174    preferred_root: Option<PitchClass>,
175) -> Option<String> {
176    label_roman(subset_mask, Some(key), preferred_root).ok()
177}
178
179pub(crate) fn scale_degrees(scale: Scale, classes: &[PitchClass]) -> Vec<usize> {
180    classes
181        .iter()
182        .filter_map(|class| scale.degree_of(*class))
183        .collect()
184}
185
186pub(crate) fn ratio_summary(
187    ratios: &[PitchRatio],
188    policy: RatioPolicy,
189) -> Result<ReferentialRatioSummary, String> {
190    let report = analyze_ratio_chord(ratios, policy).map_err(|error| error.to_string())?;
191    Ok(ReferentialRatioSummary {
192        policy,
193        admitted_tones: report.covered.admitted_tones,
194        rejected_tones: report.covered.rejected_tones,
195        cost: report.cost,
196    })
197}