use sim_lib_pitch_core::PitchClass;
use sim_lib_pitch_serial::{
DerivationKind, RowError, RowForm, RowOperation, RowSegment, SegmentInvariant, ToneRow,
analyze_derivation_partition, analyze_invariance,
};
use crate::techniques::derived_cells::build_derived_cell_plan;
use crate::{
RowInstanceId, SerialDeployError, SerialEventId, SerialPlan, StructuralLicense, VoiceId,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DerivedCellOccurrence {
pub event_id: SerialEventId,
pub voice: VoiceId,
pub occurrence_index: usize,
pub source_ordinals: Vec<u8>,
pub generator_ordinals: Vec<u8>,
pub generator_classes: Vec<PitchClass>,
pub operation: RowOperation,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DerivedCellDeployment {
pub plan: SerialPlan,
pub kind: DerivationKind,
pub generator_size: usize,
pub occurrences: Vec<DerivedCellOccurrence>,
}
pub fn deploy_derived_cells(
row_id: RowInstanceId,
row_form: RowForm,
generator_size: usize,
voices: Vec<VoiceId>,
event_prefix: impl AsRef<str>,
rationale: impl AsRef<str>,
license: StructuralLicense,
) -> Result<DerivedCellDeployment, SerialDeployError> {
let derivation = analyze_derivation_partition(row_form.row(), generator_size)
.map_err(|error| SerialDeployError::Plan(error.to_string()))?
.ok_or_else(|| {
SerialDeployError::Plan(format!(
"row {} is not derivational at generator size {generator_size}",
row_id.as_str()
))
})?;
let deployed = build_derived_cell_plan(
row_id,
row_form,
derivation.clone(),
voices,
event_prefix.as_ref(),
rationale.as_ref(),
license,
)?;
Ok(DerivedCellDeployment {
plan: deployed.plan,
kind: derivation.kind,
generator_size: derivation.generator_size,
occurrences: deployed
.occurrences
.into_iter()
.map(|occurrence| DerivedCellOccurrence {
event_id: occurrence.event_id,
voice: occurrence.voice,
occurrence_index: occurrence.occurrence_index,
source_ordinals: occurrence.source_ordinals,
generator_ordinals: occurrence.generator_ordinals,
generator_classes: occurrence.generator_classes,
operation: occurrence.operation,
})
.collect(),
})
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum SymmetryRequirement {
None,
PitchPalindrome,
RetrogradeSource,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct InvariantRequirement {
pub preserve_source_ordinals: bool,
pub preserve_pitch_identity: bool,
pub require_transposition: bool,
pub require_inversion: bool,
pub preserve_interval_order: bool,
pub preserve_set_class: bool,
pub symmetry: SymmetryRequirement,
}
impl InvariantRequirement {
pub const fn any() -> Self {
Self {
preserve_source_ordinals: false,
preserve_pitch_identity: false,
require_transposition: false,
require_inversion: false,
preserve_interval_order: false,
preserve_set_class: false,
symmetry: SymmetryRequirement::None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SymmetryCertificate {
pub pitch_palindrome: bool,
pub retrograde_source: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InvariantCertificate {
pub operation: RowOperation,
pub candidate_ordinals: Vec<u8>,
pub invariant: SegmentInvariant,
pub symmetry: SymmetryCertificate,
}
impl InvariantCertificate {
pub fn satisfies(&self, requirement: &InvariantRequirement) -> bool {
(!requirement.preserve_source_ordinals || self.invariant.ordinal_identity)
&& (!requirement.preserve_pitch_identity || self.invariant.pitch_identity)
&& (!requirement.require_transposition || self.invariant.transposition.is_some())
&& (!requirement.require_inversion || self.invariant.inversion.is_some())
&& (!requirement.preserve_interval_order || self.invariant.interval_order_identity)
&& (!requirement.preserve_set_class || self.invariant.set_class_identity)
&& match requirement.symmetry {
SymmetryRequirement::None => true,
SymmetryRequirement::PitchPalindrome => self.symmetry.pitch_palindrome,
SymmetryRequirement::RetrogradeSource => self.symmetry.retrograde_source,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InvariantFormCandidate {
pub form: RowForm,
pub certificate: InvariantCertificate,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnsatisfiedInvariantRequest {
pub requirement: InvariantRequirement,
pub forms_checked: usize,
pub segments_checked: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InvariantFormCandidates {
candidates: Vec<InvariantFormCandidate>,
unsatisfied: Option<UnsatisfiedInvariantRequest>,
}
impl InvariantFormCandidates {
pub fn iter(&self) -> impl Iterator<Item = &InvariantFormCandidate> {
self.candidates.iter()
}
pub fn as_slice(&self) -> &[InvariantFormCandidate] {
&self.candidates
}
pub fn unsatisfied(&self) -> Option<&UnsatisfiedInvariantRequest> {
self.unsatisfied.as_ref()
}
}
pub fn forms_with_invariant(
row: &ToneRow,
segment: &RowSegment,
requirement: InvariantRequirement,
) -> Result<InvariantFormCandidates, RowError> {
let segment_len = segment.classes().len();
let mut candidates = Vec::new();
let mut forms_checked = 0usize;
let mut segments_checked = 0usize;
for family in [
sim_lib_pitch_serial::RowFamily::P,
sim_lib_pitch_serial::RowFamily::I,
sim_lib_pitch_serial::RowFamily::R,
sim_lib_pitch_serial::RowFamily::RI,
] {
for addend in 0..12 {
forms_checked += 1;
let form = row.apply(RowOperation::new(family, addend));
for start in 0..=(form.classes().len() - segment_len) {
segments_checked += 1;
let candidate_segment = form.row().segment(start, segment_len)?;
let certificate = build_certificate(segment, &candidate_segment, form.operation());
if certificate.satisfies(&requirement) {
candidates.push(InvariantFormCandidate {
form: form.clone(),
certificate,
});
}
}
}
}
let unsatisfied = candidates
.is_empty()
.then_some(UnsatisfiedInvariantRequest {
requirement,
forms_checked,
segments_checked,
});
Ok(InvariantFormCandidates {
candidates,
unsatisfied,
})
}
fn build_certificate(
source: &RowSegment,
candidate: &RowSegment,
operation: RowOperation,
) -> InvariantCertificate {
InvariantCertificate {
operation,
candidate_ordinals: candidate.ordinals().to_vec(),
invariant: analyze_invariance(source, candidate),
symmetry: SymmetryCertificate {
pitch_palindrome: is_pitch_palindrome(candidate.classes()),
retrograde_source: candidate.classes().iter().copied().eq(source
.classes()
.iter()
.rev()
.copied()),
},
}
}
fn is_pitch_palindrome(classes: &[PitchClass]) -> bool {
classes.iter().eq(classes.iter().rev())
}