Skip to main content

sim_lib_music_serial/
derived.rs

1//! Derived-cell deployment and invariant-form search with explicit certificates.
2
3use sim_lib_pitch_core::PitchClass;
4use sim_lib_pitch_serial::{
5    DerivationKind, RowError, RowForm, RowOperation, RowSegment, SegmentInvariant, ToneRow,
6    analyze_derivation_partition, analyze_invariance,
7};
8
9use crate::techniques::derived_cells::build_derived_cell_plan;
10use crate::{
11    RowInstanceId, SerialDeployError, SerialEventId, SerialPlan, StructuralLicense, VoiceId,
12};
13
14/// One deployed derived-cell occurrence with preserved generator and transform evidence.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct DerivedCellOccurrence {
17    /// Stable event identity for the emitted occurrence.
18    pub event_id: SerialEventId,
19    /// Stable voice receiving the occurrence.
20    pub voice: VoiceId,
21    /// Zero-based occurrence index in source-row order.
22    pub occurrence_index: usize,
23    /// The source ordinals realized by this occurrence.
24    pub source_ordinals: Vec<u8>,
25    /// The generator-cell ordinals repeated by the derivation.
26    pub generator_ordinals: Vec<u8>,
27    /// The generator-cell pitch classes.
28    pub generator_classes: Vec<PitchClass>,
29    /// The exact row operation deriving this occurrence from the generator.
30    pub operation: RowOperation,
31}
32
33/// One inspectable derived-cell deployment with immutable plan output.
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct DerivedCellDeployment {
36    /// Immutable structural plan produced by the deployment.
37    pub plan: SerialPlan,
38    /// The detected derivation family that licensed the deployment.
39    pub kind: DerivationKind,
40    /// The generator-cell size in ordinals.
41    pub generator_size: usize,
42    /// Occurrence-by-occurrence derivation evidence.
43    pub occurrences: Vec<DerivedCellOccurrence>,
44}
45
46/// Deploys a derivation-supported row as ordered derived-cell occurrences.
47pub fn deploy_derived_cells(
48    row_id: RowInstanceId,
49    row_form: RowForm,
50    generator_size: usize,
51    voices: Vec<VoiceId>,
52    event_prefix: impl AsRef<str>,
53    rationale: impl AsRef<str>,
54    license: StructuralLicense,
55) -> Result<DerivedCellDeployment, SerialDeployError> {
56    let derivation = analyze_derivation_partition(row_form.row(), generator_size)
57        .map_err(|error| SerialDeployError::Plan(error.to_string()))?
58        .ok_or_else(|| {
59            SerialDeployError::Plan(format!(
60                "row {} is not derivational at generator size {generator_size}",
61                row_id.as_str()
62            ))
63        })?;
64    let deployed = build_derived_cell_plan(
65        row_id,
66        row_form,
67        derivation.clone(),
68        voices,
69        event_prefix.as_ref(),
70        rationale.as_ref(),
71        license,
72    )?;
73    Ok(DerivedCellDeployment {
74        plan: deployed.plan,
75        kind: derivation.kind,
76        generator_size: derivation.generator_size,
77        occurrences: deployed
78            .occurrences
79            .into_iter()
80            .map(|occurrence| DerivedCellOccurrence {
81                event_id: occurrence.event_id,
82                voice: occurrence.voice,
83                occurrence_index: occurrence.occurrence_index,
84                source_ordinals: occurrence.source_ordinals,
85                generator_ordinals: occurrence.generator_ordinals,
86                generator_classes: occurrence.generator_classes,
87                operation: occurrence.operation,
88            })
89            .collect(),
90    })
91}
92
93/// Additional symmetry constraint layered onto invariant matching.
94#[derive(Copy, Clone, Debug, PartialEq, Eq)]
95pub enum SymmetryRequirement {
96    /// No extra symmetry requirement.
97    None,
98    /// Candidate pitches must read the same forwards and backwards.
99    PitchPalindrome,
100    /// Candidate must be the exact retrograde of the source segment.
101    RetrogradeSource,
102}
103
104/// One inspectable invariant requirement.
105#[derive(Copy, Clone, Debug, PartialEq, Eq)]
106pub struct InvariantRequirement {
107    /// Preserve the source ordinals exactly.
108    pub preserve_source_ordinals: bool,
109    /// Preserve ordered pitch identity exactly.
110    pub preserve_pitch_identity: bool,
111    /// Require a transposition witness.
112    pub require_transposition: bool,
113    /// Require an inversion witness.
114    pub require_inversion: bool,
115    /// Preserve directed adjacent intervals exactly.
116    pub preserve_interval_order: bool,
117    /// Preserve the unordered set class.
118    pub preserve_set_class: bool,
119    /// Extra symmetry requirement.
120    pub symmetry: SymmetryRequirement,
121}
122
123impl InvariantRequirement {
124    /// Returns one permissive requirement that only asks for an explicit witness.
125    pub const fn any() -> Self {
126        Self {
127            preserve_source_ordinals: false,
128            preserve_pitch_identity: false,
129            require_transposition: false,
130            require_inversion: false,
131            preserve_interval_order: false,
132            preserve_set_class: false,
133            symmetry: SymmetryRequirement::None,
134        }
135    }
136}
137
138/// One symmetry witness attached to an invariant candidate.
139#[derive(Clone, Debug, PartialEq, Eq)]
140pub struct SymmetryCertificate {
141    /// Whether the candidate segment is a pitch palindrome.
142    pub pitch_palindrome: bool,
143    /// Whether the candidate is the exact retrograde of the source segment.
144    pub retrograde_source: bool,
145}
146
147/// One invariant witness proving why a form candidate matches.
148#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct InvariantCertificate {
150    /// Operation producing the candidate row form.
151    pub operation: RowOperation,
152    /// Candidate segment ordinals inside that form.
153    pub candidate_ordinals: Vec<u8>,
154    /// Ordered invariance facts for the comparison.
155    pub invariant: SegmentInvariant,
156    /// Any extra symmetry evidence.
157    pub symmetry: SymmetryCertificate,
158}
159
160impl InvariantCertificate {
161    /// Returns whether this certificate satisfies the selected requirement.
162    pub fn satisfies(&self, requirement: &InvariantRequirement) -> bool {
163        (!requirement.preserve_source_ordinals || self.invariant.ordinal_identity)
164            && (!requirement.preserve_pitch_identity || self.invariant.pitch_identity)
165            && (!requirement.require_transposition || self.invariant.transposition.is_some())
166            && (!requirement.require_inversion || self.invariant.inversion.is_some())
167            && (!requirement.preserve_interval_order || self.invariant.interval_order_identity)
168            && (!requirement.preserve_set_class || self.invariant.set_class_identity)
169            && match requirement.symmetry {
170                SymmetryRequirement::None => true,
171                SymmetryRequirement::PitchPalindrome => self.symmetry.pitch_palindrome,
172                SymmetryRequirement::RetrogradeSource => self.symmetry.retrograde_source,
173            }
174    }
175}
176
177/// One candidate row form whose certificate satisfies the requirement.
178#[derive(Clone, Debug, PartialEq, Eq)]
179pub struct InvariantFormCandidate {
180    /// Matching row form.
181    pub form: RowForm,
182    /// Inspectable evidence for the match.
183    pub certificate: InvariantCertificate,
184}
185
186/// Explicit unsatisfied evidence when no candidate meets the requirement.
187#[derive(Clone, Debug, PartialEq, Eq)]
188pub struct UnsatisfiedInvariantRequest {
189    /// Requirement that could not be satisfied.
190    pub requirement: InvariantRequirement,
191    /// Number of row forms inspected.
192    pub forms_checked: usize,
193    /// Number of same-length segments inspected.
194    pub segments_checked: usize,
195}
196
197/// Invariant search result that preserves both matching candidates and explicit failure evidence.
198#[derive(Clone, Debug, PartialEq, Eq)]
199pub struct InvariantFormCandidates {
200    candidates: Vec<InvariantFormCandidate>,
201    unsatisfied: Option<UnsatisfiedInvariantRequest>,
202}
203
204impl InvariantFormCandidates {
205    /// Returns the matching candidates in stable search order.
206    pub fn iter(&self) -> impl Iterator<Item = &InvariantFormCandidate> {
207        self.candidates.iter()
208    }
209
210    /// Returns the matching candidates as a slice.
211    pub fn as_slice(&self) -> &[InvariantFormCandidate] {
212        &self.candidates
213    }
214
215    /// Returns explicit unsatisfied evidence when no candidate matched.
216    pub fn unsatisfied(&self) -> Option<&UnsatisfiedInvariantRequest> {
217        self.unsatisfied.as_ref()
218    }
219}
220
221/// Searches every row form for same-length segments satisfying the requirement.
222pub fn forms_with_invariant(
223    row: &ToneRow,
224    segment: &RowSegment,
225    requirement: InvariantRequirement,
226) -> Result<InvariantFormCandidates, RowError> {
227    let segment_len = segment.classes().len();
228    let mut candidates = Vec::new();
229    let mut forms_checked = 0usize;
230    let mut segments_checked = 0usize;
231
232    for family in [
233        sim_lib_pitch_serial::RowFamily::P,
234        sim_lib_pitch_serial::RowFamily::I,
235        sim_lib_pitch_serial::RowFamily::R,
236        sim_lib_pitch_serial::RowFamily::RI,
237    ] {
238        for addend in 0..12 {
239            forms_checked += 1;
240            let form = row.apply(RowOperation::new(family, addend));
241            for start in 0..=(form.classes().len() - segment_len) {
242                segments_checked += 1;
243                let candidate_segment = form.row().segment(start, segment_len)?;
244                let certificate = build_certificate(segment, &candidate_segment, form.operation());
245                if certificate.satisfies(&requirement) {
246                    candidates.push(InvariantFormCandidate {
247                        form: form.clone(),
248                        certificate,
249                    });
250                }
251            }
252        }
253    }
254
255    let unsatisfied = candidates
256        .is_empty()
257        .then_some(UnsatisfiedInvariantRequest {
258            requirement,
259            forms_checked,
260            segments_checked,
261        });
262    Ok(InvariantFormCandidates {
263        candidates,
264        unsatisfied,
265    })
266}
267
268fn build_certificate(
269    source: &RowSegment,
270    candidate: &RowSegment,
271    operation: RowOperation,
272) -> InvariantCertificate {
273    InvariantCertificate {
274        operation,
275        candidate_ordinals: candidate.ordinals().to_vec(),
276        invariant: analyze_invariance(source, candidate),
277        symmetry: SymmetryCertificate {
278            pitch_palindrome: is_pitch_palindrome(candidate.classes()),
279            retrograde_source: candidate.classes().iter().copied().eq(source
280                .classes()
281                .iter()
282                .rev()
283                .copied()),
284        },
285    }
286}
287
288fn is_pitch_palindrome(classes: &[PitchClass]) -> bool {
289    classes.iter().eq(classes.iter().rev())
290}