Skip to main content

sim_lib_music_serial/deploy/
core.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use sim_lib_pitch_serial::RowForm;
5use thiserror::Error;
6
7use crate::{
8    BuiltInPracticeRule, EventPlacement, OrdinalRef, PlannedSerialEvent, PracticeRule,
9    PracticeRuleId, RowInstanceId, SerialEventId, SerialOrigin, SerialPlan, SerialPractice,
10    SerialRole, StructuralLicense, VoiceId,
11};
12
13/// Failure while composing one inspectable serial deployment plan.
14#[derive(Clone, Debug, PartialEq, Eq, Error)]
15pub enum SerialDeployError {
16    /// The supplied technique id was empty or used an invalid character.
17    #[error("{0}")]
18    InvalidTechniqueId(String),
19    /// The technique attempted to build without any deployers.
20    #[error("technique {0} must contain at least one deployer")]
21    EmptyTechnique(String),
22    /// A deployer named a row instance missing from the deployment input.
23    #[error("deployer {deployer} references unknown row {row_id}")]
24    UnknownRow {
25        /// Stable deployer label.
26        deployer: String,
27        /// Missing row id.
28        row_id: RowInstanceId,
29    },
30    /// A deployer attempted to reuse an event id already emitted earlier.
31    #[error("deployer {deployer} attempted to reuse event id {event_id}")]
32    DuplicateEventId {
33        /// Stable deployer label.
34        deployer: String,
35        /// Duplicate event id.
36        event_id: SerialEventId,
37    },
38    /// A deployer requiring one voice per block or form received the wrong count.
39    #[error("deployer {deployer} expected {expected} voices but received {actual}")]
40    VoiceCountMismatch {
41        /// Stable deployer label.
42        deployer: String,
43        /// Required voice count.
44        expected: usize,
45        /// Received voice count.
46        actual: usize,
47    },
48    /// Aggregate-rotation block lengths failed to cover exactly one row.
49    #[error("aggregate rotation block lengths must sum to 12, received {0}")]
50    InvalidRotationCoverage(usize),
51    /// The caller requested an interlocking deployment without an interlocking witness.
52    #[error("deployer {deployer} requires an interlocking partition witness")]
53    NotInterlocking {
54        /// Stable deployer label.
55        deployer: String,
56    },
57    /// The requested simultaneous forms were not combinatorial at the block size.
58    #[error(
59        "rows {source_row_id} and {partner_row_id} are not combinatorial at block size {block_size}"
60    )]
61    NotCombinatorial {
62        /// Source row id.
63        source_row_id: RowInstanceId,
64        /// Partner row id.
65        partner_row_id: RowInstanceId,
66        /// Requested contiguous block size.
67        block_size: usize,
68    },
69    /// Building or validating a pitch-serial partition failed.
70    #[error("partition build failed: {0}")]
71    Partition(String),
72    /// Final immutable serial-plan validation failed.
73    #[error("serial plan validation failed: {0}")]
74    Plan(String),
75}
76
77pub(crate) fn validate_technique_id(value: impl Into<String>) -> Result<String, SerialDeployError> {
78    let value = value.into();
79    if value.trim().is_empty() {
80        return Err(SerialDeployError::InvalidTechniqueId(
81            "technique id cannot be empty".to_owned(),
82        ));
83    }
84    if value
85        .chars()
86        .any(|ch| !(ch.is_ascii_alphanumeric() || matches!(ch, '/' | '-' | '_' | '.')))
87    {
88        return Err(SerialDeployError::InvalidTechniqueId(
89            "technique id must use ASCII letters, digits, /, -, _, or .".to_owned(),
90        ));
91    }
92    Ok(value)
93}
94
95/// Inspectable parameter attached to one deployer.
96#[derive(Clone, Debug, PartialEq, Eq)]
97pub struct SerialDeployerParameter {
98    /// Stable parameter name.
99    pub name: String,
100    /// Stable printable value.
101    pub value: String,
102}
103
104/// Public category of one serial deployer.
105#[derive(Copy, Clone, Debug, PartialEq, Eq)]
106pub enum SerialDeployerKind {
107    /// Present one complete row in sequence.
108    CompleteHorizontalStatement,
109    /// Present a caller-declared partition sequentially.
110    MotivicPartition,
111    /// Present selected blocks vertically as chordal events.
112    VerticalBlocks,
113    /// Require an interlocking partition witness before deployment.
114    InterlockingPartition,
115    /// Distribute each block between melody and accompaniment.
116    MelodyAccompanimentDistribution,
117    /// Rotate the aggregate before reblocking it.
118    AggregateRotation,
119    /// Present several forms simultaneously by aligned blocks.
120    SimultaneousForms,
121}
122
123/// Inspectable public description of one deployment component.
124#[derive(Clone, Debug, PartialEq, Eq)]
125pub struct SerialDeployerSpec {
126    /// Public deployer category.
127    pub kind: SerialDeployerKind,
128    /// Stable deployer label.
129    pub label: String,
130    /// Human-facing expected fact or usage.
131    pub expected_fact: String,
132    /// Inspectable deployer parameters.
133    pub parameters: Vec<SerialDeployerParameter>,
134}
135
136#[derive(Clone)]
137pub(crate) enum SerialDeployerInner {
138    Horizontal(super::components::HorizontalStatementSpec),
139    Motivic(super::components::MotivicPartitionSpec),
140    Vertical(super::components::VerticalBlocksSpec),
141    Interlocking(super::components::InterlockingPartitionSpec),
142    MelodyAccompaniment(super::components::MelodyAccompanimentSpec),
143    AggregateRotation(super::components::AggregateRotationSpec),
144    SimultaneousForms(super::components::SimultaneousFormsSpec),
145}
146
147/// One reusable inspectable deployment component.
148#[derive(Clone)]
149pub struct SerialDeployer {
150    pub(crate) inner: SerialDeployerInner,
151}
152
153impl SerialDeployer {
154    pub(crate) fn spec(&self) -> SerialDeployerSpec {
155        match &self.inner {
156            SerialDeployerInner::Horizontal(spec) => SerialDeployerSpec {
157                kind: SerialDeployerKind::CompleteHorizontalStatement,
158                label: spec.event_id.as_str().to_owned(),
159                expected_fact: "one complete row sounds in sequence".to_owned(),
160                parameters: vec![
161                    param("row", spec.row_id.as_str()),
162                    param("voice", &spec.voice.to_string()),
163                    param("reading", spec.license.reading_id.as_str()),
164                ],
165            },
166            SerialDeployerInner::Motivic(spec) => SerialDeployerSpec {
167                kind: SerialDeployerKind::MotivicPartition,
168                label: spec.event_prefix.clone(),
169                expected_fact: "partition blocks sound sequentially as inspectable motives"
170                    .to_owned(),
171                parameters: vec![
172                    param("row", spec.row_id.as_str()),
173                    param("blocks", &spec.partition.block_count().to_string()),
174                    param("reading", spec.license.reading_id.as_str()),
175                ],
176            },
177            SerialDeployerInner::Vertical(spec) => SerialDeployerSpec {
178                kind: SerialDeployerKind::VerticalBlocks,
179                label: spec.event_prefix.clone(),
180                expected_fact: "selected blocks sound as chordal vertical events".to_owned(),
181                parameters: vec![
182                    param("row", spec.row_id.as_str()),
183                    param("blocks", &format!("{:?}", spec.selected_blocks)),
184                    param("reading", spec.license.reading_id.as_str()),
185                ],
186            },
187            SerialDeployerInner::Interlocking(spec) => SerialDeployerSpec {
188                kind: SerialDeployerKind::InterlockingPartition,
189                label: spec.event_prefix.clone(),
190                expected_fact: "interlocking block evidence licenses sequential partition exchange"
191                    .to_owned(),
192                parameters: vec![
193                    param("row", spec.row_id.as_str()),
194                    param("blocks", &spec.partition.block_count().to_string()),
195                    param("reading", spec.license.reading_id.as_str()),
196                ],
197            },
198            SerialDeployerInner::MelodyAccompaniment(spec) => SerialDeployerSpec {
199                kind: SerialDeployerKind::MelodyAccompanimentDistribution,
200                label: spec.event_prefix.clone(),
201                expected_fact: "each block splits into melody lead and accompaniment residue"
202                    .to_owned(),
203                parameters: vec![
204                    param("row", spec.row_id.as_str()),
205                    param("blocks", &spec.partition.block_count().to_string()),
206                    param("reading", spec.license.reading_id.as_str()),
207                ],
208            },
209            SerialDeployerInner::AggregateRotation(spec) => SerialDeployerSpec {
210                kind: SerialDeployerKind::AggregateRotation,
211                label: spec.event_prefix.clone(),
212                expected_fact: "a rotated aggregate is reblocked without losing row coverage"
213                    .to_owned(),
214                parameters: vec![
215                    param("row", spec.row_id.as_str()),
216                    param("rotation", &spec.rotation.to_string()),
217                    param("reading", spec.license.reading_id.as_str()),
218                ],
219            },
220            SerialDeployerInner::SimultaneousForms(spec) => SerialDeployerSpec {
221                kind: SerialDeployerKind::SimultaneousForms,
222                label: spec.event_prefix.clone(),
223                expected_fact: "simultaneous form blocks preserve each form identity and alignment"
224                    .to_owned(),
225                parameters: vec![
226                    param(
227                        "forms",
228                        &spec
229                            .row_ids
230                            .iter()
231                            .map(RowInstanceId::as_str)
232                            .collect::<Vec<_>>()
233                            .join(","),
234                    ),
235                    param("block-size", &spec.block_size.to_string()),
236                    param("reading", spec.license.reading_id.as_str()),
237                ],
238            },
239        }
240    }
241
242    pub(crate) fn apply(
243        &self,
244        rows: &BTreeMap<RowInstanceId, RowForm>,
245        builder: &mut PlanBuilder,
246    ) -> Result<(), SerialDeployError> {
247        match &self.inner {
248            SerialDeployerInner::Horizontal(spec) => spec.apply(rows, builder),
249            SerialDeployerInner::Motivic(spec) => spec.apply(rows, builder),
250            SerialDeployerInner::Vertical(spec) => spec.apply(rows, builder),
251            SerialDeployerInner::Interlocking(spec) => spec.apply(rows, builder),
252            SerialDeployerInner::MelodyAccompaniment(spec) => spec.apply(rows, builder),
253            SerialDeployerInner::AggregateRotation(spec) => spec.apply(rows, builder),
254            SerialDeployerInner::SimultaneousForms(spec) => spec.apply(rows, builder),
255        }
256    }
257}
258
259pub(crate) fn param(name: &str, value: &str) -> SerialDeployerParameter {
260    SerialDeployerParameter {
261        name: name.to_owned(),
262        value: value.to_owned(),
263    }
264}
265
266#[derive(Clone, Debug)]
267pub(crate) struct PlanBuilder {
268    pub(crate) events: BTreeMap<SerialEventId, PlannedSerialEvent>,
269    pub(crate) precedence: Vec<(SerialEventId, SerialEventId)>,
270}
271
272impl PlanBuilder {
273    pub(crate) fn add_event(
274        &mut self,
275        deployer: &str,
276        event: PlannedSerialEvent,
277    ) -> Result<(), SerialDeployError> {
278        if self.events.contains_key(&event.id) {
279            return Err(SerialDeployError::DuplicateEventId {
280                deployer: deployer.to_owned(),
281                event_id: event.id,
282            });
283        }
284        self.events.insert(event.id.clone(), event);
285        Ok(())
286    }
287
288    pub(crate) fn add_precedence(&mut self, before: &SerialEventId, after: &SerialEventId) {
289        self.precedence.push((before.clone(), after.clone()));
290    }
291}
292
293/// Inspectable deployment plan composed from ordinary practice rules and deployers.
294#[derive(Clone)]
295pub struct TechniquePlan {
296    id: String,
297    rules: Vec<Arc<dyn PracticeRule>>,
298    deployers: Vec<SerialDeployer>,
299}
300
301impl TechniquePlan {
302    /// Starts a builder for one inspectable deployment technique.
303    pub fn builder(id: impl Into<String>) -> Result<TechniquePlanBuilder, SerialDeployError> {
304        Ok(TechniquePlanBuilder {
305            id: validate_technique_id(id)?,
306            rules: Vec::new(),
307            deployers: Vec::new(),
308        })
309    }
310
311    /// Returns the stable technique id.
312    pub fn id(&self) -> &str {
313        &self.id
314    }
315
316    /// Returns the inspectable practice rule specifications.
317    pub fn rule_specs(&self) -> Vec<crate::PracticeRuleSpec> {
318        self.practice().rule_specs()
319    }
320
321    /// Returns the inspectable deployer specifications.
322    pub fn deployer_specs(&self) -> Vec<SerialDeployerSpec> {
323        self.deployers.iter().map(SerialDeployer::spec).collect()
324    }
325
326    /// Returns the inspectable practice carried alongside the deployers.
327    pub fn practice(&self) -> SerialPractice {
328        SerialPractice::new(
329            crate::PracticeId::new(self.id.clone()).expect("validated technique id"),
330            self.rules.clone(),
331        )
332    }
333
334    /// Deploys the supplied row instances into one validated immutable serial plan.
335    pub fn deploy(
336        &self,
337        rows: BTreeMap<RowInstanceId, RowForm>,
338    ) -> Result<SerialPlan, SerialDeployError> {
339        let mut builder = PlanBuilder {
340            events: BTreeMap::new(),
341            precedence: Vec::new(),
342        };
343        for deployer in &self.deployers {
344            deployer.apply(&rows, &mut builder)?;
345        }
346        SerialPlan::try_new(rows, builder.events, builder.precedence)
347            .map_err(|error| SerialDeployError::Plan(error.to_string()))
348    }
349}
350
351/// Builder for one inspectable technique plan.
352pub struct TechniquePlanBuilder {
353    id: String,
354    rules: Vec<Arc<dyn PracticeRule>>,
355    deployers: Vec<SerialDeployer>,
356}
357
358impl TechniquePlanBuilder {
359    /// Adds one inspectable practice rule.
360    pub fn rule(mut self, rule: Arc<dyn PracticeRule>) -> Self {
361        self.rules.push(rule);
362        self
363    }
364
365    /// Adds one inspectable deployment component.
366    pub fn deployer(mut self, deployer: SerialDeployer) -> Self {
367        self.deployers.push(deployer);
368        self
369    }
370
371    /// Finishes the technique plan after validating its minimum structure.
372    pub fn build(self) -> Result<TechniquePlan, SerialDeployError> {
373        if self.deployers.is_empty() {
374            return Err(SerialDeployError::EmptyTechnique(self.id));
375        }
376        Ok(TechniquePlan {
377            id: self.id,
378            rules: self.rules,
379            deployers: self.deployers,
380        })
381    }
382}
383
384/// Convenience helper for the built-in strict aggregate rule.
385pub fn strict_aggregate() -> Arc<dyn PracticeRule> {
386    Arc::new(BuiltInPracticeRule::aggregate(
387        PracticeRuleId::new("rule/aggregate").expect("static rule id"),
388    ))
389}
390
391pub(crate) fn require_row(
392    deployer: &str,
393    rows: &BTreeMap<RowInstanceId, RowForm>,
394    row_id: &RowInstanceId,
395) -> Result<(), SerialDeployError> {
396    if rows.contains_key(row_id) {
397        Ok(())
398    } else {
399        Err(SerialDeployError::UnknownRow {
400            deployer: deployer.to_owned(),
401            row_id: row_id.clone(),
402        })
403    }
404}
405
406pub(crate) fn require_voices(
407    deployer: &str,
408    expected: usize,
409    actual: usize,
410) -> Result<(), SerialDeployError> {
411    if expected == actual {
412        Ok(())
413    } else {
414        Err(SerialDeployError::VoiceCountMismatch {
415            deployer: deployer.to_owned(),
416            expected,
417            actual,
418        })
419    }
420}
421
422pub(crate) fn structural_event(
423    event_id: SerialEventId,
424    ordinals: &[u8],
425    row_id: RowInstanceId,
426    voice: VoiceId,
427    rationale: String,
428    license: StructuralLicense,
429    placement: EventPlacement,
430) -> PlannedSerialEvent {
431    PlannedSerialEvent {
432        id: event_id,
433        ordinals: ordinals
434            .iter()
435            .copied()
436            .map(|ordinal| OrdinalRef::new(row_id.clone(), usize::from(ordinal)))
437            .collect(),
438        role: SerialRole::Structural,
439        origin: SerialOrigin::Structural { rationale },
440        voice,
441        placement,
442        parents: Vec::new(),
443        licenses: vec![license],
444    }
445}