Skip to main content

sim_lib_music_serial/deploy/
components.rs

1use std::collections::BTreeMap;
2
3use sim_lib_pitch_serial::{
4    RowForm, RowPartition, analyze_combinatoriality_partition, analyze_interlocking_partitions,
5};
6
7use crate::{
8    EventPlacement, RowInstanceId, SerialEventId, SimultaneousGroupId, StructuralLicense, VoiceId,
9};
10
11use super::core::{
12    PlanBuilder, SerialDeployError, SerialDeployer, SerialDeployerInner, require_row,
13    require_voices, structural_event,
14};
15
16/// Complete horizontal statement deployment for one row.
17#[derive(Clone)]
18pub struct HorizontalStatementSpec {
19    /// Source row to present.
20    pub row_id: RowInstanceId,
21    /// Stable event id for the emitted statement.
22    pub event_id: SerialEventId,
23    /// Voice receiving the statement.
24    pub voice: VoiceId,
25    /// Human-facing structural rationale.
26    pub rationale: String,
27    /// Structural reading that licenses the statement.
28    pub license: StructuralLicense,
29}
30
31impl HorizontalStatementSpec {
32    pub(crate) fn apply(
33        &self,
34        rows: &BTreeMap<RowInstanceId, RowForm>,
35        builder: &mut PlanBuilder,
36    ) -> Result<(), SerialDeployError> {
37        require_row("complete-horizontal-statement", rows, &self.row_id)?;
38        builder.add_event(
39            "complete-horizontal-statement",
40            structural_event(
41                self.event_id.clone(),
42                &(0..12).map(|ordinal| ordinal as u8).collect::<Vec<_>>(),
43                self.row_id.clone(),
44                self.voice.clone(),
45                self.rationale.clone(),
46                self.license.clone(),
47                EventPlacement::independent(),
48            ),
49        )
50    }
51}
52
53/// Creates one complete horizontal row statement deployer.
54pub fn complete_horizontal_statement(
55    row_id: RowInstanceId,
56    event_id: SerialEventId,
57    voice: VoiceId,
58    rationale: impl Into<String>,
59    license: StructuralLicense,
60) -> SerialDeployer {
61    SerialDeployer {
62        inner: SerialDeployerInner::Horizontal(HorizontalStatementSpec {
63            row_id,
64            event_id,
65            voice,
66            rationale: rationale.into(),
67            license,
68        }),
69    }
70}
71
72/// Sequential motivic partition deployment over one row.
73#[derive(Clone)]
74pub struct MotivicPartitionSpec {
75    /// Source row to partition.
76    pub row_id: RowInstanceId,
77    /// Validated partition to emit.
78    pub partition: RowPartition,
79    /// One voice per partition block.
80    pub voices: Vec<VoiceId>,
81    /// Stable event id prefix.
82    pub event_prefix: String,
83    /// Human-facing structural rationale.
84    pub rationale: String,
85    /// Structural reading that licenses the partition.
86    pub license: StructuralLicense,
87}
88
89impl MotivicPartitionSpec {
90    pub(crate) fn apply(
91        &self,
92        rows: &BTreeMap<RowInstanceId, RowForm>,
93        builder: &mut PlanBuilder,
94    ) -> Result<(), SerialDeployError> {
95        require_row("motivic-partition", rows, &self.row_id)?;
96        require_voices(
97            "motivic-partition",
98            self.partition.block_count(),
99            self.voices.len(),
100        )?;
101        let mut previous = None::<SerialEventId>;
102        for (index, (block, voice)) in self.partition.blocks().iter().zip(&self.voices).enumerate()
103        {
104            let event_id = SerialEventId::new(format!("{}/block-{}", self.event_prefix, index))
105                .map_err(|error| SerialDeployError::Plan(error.to_string()))?;
106            builder.add_event(
107                "motivic-partition",
108                structural_event(
109                    event_id.clone(),
110                    block.ordinals(),
111                    self.row_id.clone(),
112                    voice.clone(),
113                    self.rationale.clone(),
114                    self.license.clone(),
115                    EventPlacement::independent(),
116                ),
117            )?;
118            if let Some(previous_id) = previous.as_ref() {
119                builder.add_precedence(previous_id, &event_id);
120            }
121            previous = Some(event_id);
122        }
123        Ok(())
124    }
125}
126
127/// Creates one sequential motivic partition deployer.
128pub fn motivic_partition(
129    row_id: RowInstanceId,
130    partition: RowPartition,
131    voices: Vec<VoiceId>,
132    event_prefix: impl Into<String>,
133    rationale: impl Into<String>,
134    license: StructuralLicense,
135) -> SerialDeployer {
136    SerialDeployer {
137        inner: SerialDeployerInner::Motivic(MotivicPartitionSpec {
138            row_id,
139            partition,
140            voices,
141            event_prefix: event_prefix.into(),
142            rationale: rationale.into(),
143            license,
144        }),
145    }
146}
147
148/// Chordal vertical-block deployment over one row.
149#[derive(Clone)]
150pub struct VerticalBlocksSpec {
151    /// Source row to partition.
152    pub row_id: RowInstanceId,
153    /// Validated partition supplying vertical blocks.
154    pub partition: RowPartition,
155    /// Block indices that should sound as chordal events.
156    pub selected_blocks: Vec<usize>,
157    /// Voice receiving the chordal material.
158    pub voice: VoiceId,
159    /// Stable event id prefix.
160    pub event_prefix: String,
161    /// Human-facing structural rationale.
162    pub rationale: String,
163    /// Structural reading that licenses the vertical reading.
164    pub license: StructuralLicense,
165}
166
167impl VerticalBlocksSpec {
168    pub(crate) fn apply(
169        &self,
170        rows: &BTreeMap<RowInstanceId, RowForm>,
171        builder: &mut PlanBuilder,
172    ) -> Result<(), SerialDeployError> {
173        require_row("vertical-blocks", rows, &self.row_id)?;
174        let mut previous = None::<SerialEventId>;
175        for (position, block_index) in self.selected_blocks.iter().copied().enumerate() {
176            let block = self
177                .partition
178                .blocks()
179                .get(block_index)
180                .expect("validated block index selected by caller");
181            let event_id =
182                SerialEventId::new(format!("{}/vertical-{}", self.event_prefix, position))
183                    .map_err(|error| SerialDeployError::Plan(error.to_string()))?;
184            builder.add_event(
185                "vertical-blocks",
186                structural_event(
187                    event_id.clone(),
188                    block.ordinals(),
189                    self.row_id.clone(),
190                    self.voice.clone(),
191                    self.rationale.clone(),
192                    self.license.clone(),
193                    EventPlacement::independent(),
194                ),
195            )?;
196            if let Some(previous_id) = previous.as_ref() {
197                builder.add_precedence(previous_id, &event_id);
198            }
199            previous = Some(event_id);
200        }
201        Ok(())
202    }
203}
204
205/// Creates one chordal vertical-block deployer.
206pub fn verticalize_selected_blocks(spec: VerticalBlocksSpec) -> SerialDeployer {
207    SerialDeployer {
208        inner: SerialDeployerInner::Vertical(spec),
209    }
210}
211
212/// Interlocking partition deployment over one row.
213#[derive(Clone)]
214pub struct InterlockingPartitionSpec {
215    /// Source row to partition.
216    pub row_id: RowInstanceId,
217    /// Primary partition to emit.
218    pub partition: RowPartition,
219    /// Counter-partition used as the interlocking witness.
220    pub counter_partition: RowPartition,
221    /// One voice per emitted block.
222    pub voices: Vec<VoiceId>,
223    /// Stable event id prefix.
224    pub event_prefix: String,
225    /// Human-facing structural rationale.
226    pub rationale: String,
227    /// Structural reading that licenses the interlocking reading.
228    pub license: StructuralLicense,
229}
230
231impl InterlockingPartitionSpec {
232    pub(crate) fn apply(
233        &self,
234        rows: &BTreeMap<RowInstanceId, RowForm>,
235        builder: &mut PlanBuilder,
236    ) -> Result<(), SerialDeployError> {
237        require_row("interlocking-partition", rows, &self.row_id)?;
238        require_voices(
239            "interlocking-partition",
240            self.partition.block_count(),
241            self.voices.len(),
242        )?;
243        let report = analyze_interlocking_partitions(&self.partition, &self.counter_partition);
244        if !report.is_interlocking {
245            return Err(SerialDeployError::NotInterlocking {
246                deployer: "interlocking-partition".to_owned(),
247            });
248        }
249        let mut previous = None::<SerialEventId>;
250        for (index, (block, voice)) in self.partition.blocks().iter().zip(&self.voices).enumerate()
251        {
252            let event_id = SerialEventId::new(format!("{}/exchange-{}", self.event_prefix, index))
253                .map_err(|error| SerialDeployError::Plan(error.to_string()))?;
254            builder.add_event(
255                "interlocking-partition",
256                structural_event(
257                    event_id.clone(),
258                    block.ordinals(),
259                    self.row_id.clone(),
260                    voice.clone(),
261                    format!("{}; interlocking witness", self.rationale),
262                    self.license.clone(),
263                    EventPlacement::independent(),
264                ),
265            )?;
266            if let Some(previous_id) = previous.as_ref() {
267                builder.add_precedence(previous_id, &event_id);
268            }
269            previous = Some(event_id);
270        }
271        Ok(())
272    }
273}
274
275/// Creates one interlocking partition deployer.
276pub fn interlocking_partition(spec: InterlockingPartitionSpec) -> SerialDeployer {
277    SerialDeployer {
278        inner: SerialDeployerInner::Interlocking(spec),
279    }
280}
281
282/// Melody/accompaniment distribution deployment over one row.
283#[derive(Clone)]
284pub struct MelodyAccompanimentSpec {
285    /// Source row to partition.
286    pub row_id: RowInstanceId,
287    /// Validated partition supplying blocks.
288    pub partition: RowPartition,
289    /// Lead melody voice.
290    pub melody_voice: VoiceId,
291    /// Accompaniment voice.
292    pub accompaniment_voice: VoiceId,
293    /// Stable event id prefix.
294    pub event_prefix: String,
295    /// Human-facing structural rationale.
296    pub rationale: String,
297    /// Structural reading that licenses the split.
298    pub license: StructuralLicense,
299}
300
301impl MelodyAccompanimentSpec {
302    pub(crate) fn apply(
303        &self,
304        rows: &BTreeMap<RowInstanceId, RowForm>,
305        builder: &mut PlanBuilder,
306    ) -> Result<(), SerialDeployError> {
307        require_row("melody-accompaniment", rows, &self.row_id)?;
308        let mut previous = None::<SerialEventId>;
309        for (index, block) in self.partition.blocks().iter().enumerate() {
310            let melody_id = SerialEventId::new(format!("{}/melody-{}", self.event_prefix, index))
311                .map_err(|error| SerialDeployError::Plan(error.to_string()))?;
312            builder.add_event(
313                "melody-accompaniment",
314                structural_event(
315                    melody_id.clone(),
316                    &[block.ordinals()[0]],
317                    self.row_id.clone(),
318                    self.melody_voice.clone(),
319                    self.rationale.clone(),
320                    self.license.clone(),
321                    EventPlacement::independent(),
322                ),
323            )?;
324            if let Some(previous_id) = previous.as_ref() {
325                builder.add_precedence(previous_id, &melody_id);
326            }
327            previous = Some(melody_id.clone());
328            if block.ordinals().len() > 1 {
329                let accompaniment_id =
330                    SerialEventId::new(format!("{}/accompaniment-{}", self.event_prefix, index))
331                        .map_err(|error| SerialDeployError::Plan(error.to_string()))?;
332                builder.add_event(
333                    "melody-accompaniment",
334                    structural_event(
335                        accompaniment_id.clone(),
336                        &block.ordinals()[1..],
337                        self.row_id.clone(),
338                        self.accompaniment_voice.clone(),
339                        format!("{}; accompaniment residue", self.rationale),
340                        self.license.clone(),
341                        EventPlacement::independent(),
342                    ),
343                )?;
344                builder.add_precedence(&melody_id, &accompaniment_id);
345                previous = Some(accompaniment_id);
346            }
347        }
348        Ok(())
349    }
350}
351
352/// Creates one melody/accompaniment deployer.
353pub fn melody_accompaniment_distribution(spec: MelodyAccompanimentSpec) -> SerialDeployer {
354    SerialDeployer {
355        inner: SerialDeployerInner::MelodyAccompaniment(spec),
356    }
357}
358
359/// Aggregate-rotation deployment over one row.
360#[derive(Clone)]
361pub struct AggregateRotationSpec {
362    /// Source row to rotate.
363    pub row_id: RowInstanceId,
364    /// Rotation offset in row ordinals.
365    pub rotation: usize,
366    /// Block lengths after rotation.
367    pub block_lengths: Vec<usize>,
368    /// One voice per rotated block.
369    pub voices: Vec<VoiceId>,
370    /// Stable event id prefix.
371    pub event_prefix: String,
372    /// Human-facing structural rationale.
373    pub rationale: String,
374    /// Structural reading that licenses the rotation.
375    pub license: StructuralLicense,
376}
377
378impl AggregateRotationSpec {
379    pub(crate) fn apply(
380        &self,
381        rows: &BTreeMap<RowInstanceId, RowForm>,
382        builder: &mut PlanBuilder,
383    ) -> Result<(), SerialDeployError> {
384        require_row("aggregate-rotation", rows, &self.row_id)?;
385        require_voices(
386            "aggregate-rotation",
387            self.block_lengths.len(),
388            self.voices.len(),
389        )?;
390        let total: usize = self.block_lengths.iter().sum();
391        if total != 12 {
392            return Err(SerialDeployError::InvalidRotationCoverage(total));
393        }
394        let ordinals = (0..12)
395            .map(|offset| ((self.rotation + offset) % 12) as u8)
396            .collect::<Vec<_>>();
397        let mut start = 0usize;
398        let mut previous = None::<SerialEventId>;
399        for (index, (len, voice)) in self
400            .block_lengths
401            .iter()
402            .copied()
403            .zip(&self.voices)
404            .enumerate()
405        {
406            let event_id = SerialEventId::new(format!("{}/rotation-{}", self.event_prefix, index))
407                .map_err(|error| SerialDeployError::Plan(error.to_string()))?;
408            builder.add_event(
409                "aggregate-rotation",
410                structural_event(
411                    event_id.clone(),
412                    &ordinals[start..start + len],
413                    self.row_id.clone(),
414                    voice.clone(),
415                    self.rationale.clone(),
416                    self.license.clone(),
417                    EventPlacement::independent(),
418                ),
419            )?;
420            if let Some(previous_id) = previous.as_ref() {
421                builder.add_precedence(previous_id, &event_id);
422            }
423            previous = Some(event_id);
424            start += len;
425        }
426        Ok(())
427    }
428}
429
430/// Creates one aggregate-rotation deployer.
431#[allow(clippy::missing_const_for_fn)]
432pub fn aggregate_rotation(spec: AggregateRotationSpec) -> SerialDeployer {
433    SerialDeployer {
434        inner: SerialDeployerInner::AggregateRotation(spec),
435    }
436}
437
438/// Simultaneous multi-form deployment aligned by contiguous blocks.
439#[derive(Clone)]
440pub struct SimultaneousFormsSpec {
441    /// Row ids to align simultaneously.
442    pub row_ids: Vec<RowInstanceId>,
443    /// One voice per row form.
444    pub voices: Vec<VoiceId>,
445    /// Supported contiguous block size.
446    pub block_size: usize,
447    /// Stable event id prefix.
448    pub event_prefix: String,
449    /// Human-facing structural rationale.
450    pub rationale: String,
451    /// Structural reading that licenses the simultaneity.
452    pub license: StructuralLicense,
453}
454
455impl SimultaneousFormsSpec {
456    pub(crate) fn apply(
457        &self,
458        rows: &BTreeMap<RowInstanceId, RowForm>,
459        builder: &mut PlanBuilder,
460    ) -> Result<(), SerialDeployError> {
461        require_voices("simultaneous-forms", self.row_ids.len(), self.voices.len())?;
462        for row_id in &self.row_ids {
463            require_row("simultaneous-forms", rows, row_id)?;
464        }
465        let source_id = self.row_ids.first().expect("at least one row");
466        let source_row = rows.get(source_id).expect("validated row").row().clone();
467        for partner_id in self.row_ids.iter().skip(1) {
468            let partner = rows.get(partner_id).expect("validated row");
469            let witness = analyze_combinatoriality_partition(
470                &source_row,
471                partner.operation(),
472                self.block_size,
473            )
474            .map_err(|error| SerialDeployError::Partition(error.to_string()))?;
475            let Some(partner_witness) = witness else {
476                return Err(SerialDeployError::NotCombinatorial {
477                    source_row_id: source_id.clone(),
478                    partner_row_id: partner_id.clone(),
479                    block_size: self.block_size,
480                });
481            };
482            if partner_witness.operation != partner.operation() {
483                return Err(SerialDeployError::NotCombinatorial {
484                    source_row_id: source_id.clone(),
485                    partner_row_id: partner_id.clone(),
486                    block_size: self.block_size,
487                });
488            }
489        }
490        let block_count = 12 / self.block_size;
491        let mut previous_group = Vec::<SerialEventId>::new();
492        for block_index in 0..block_count {
493            let group =
494                SimultaneousGroupId::new(format!("{}/simul-{}", self.event_prefix, block_index))
495                    .map_err(|error| SerialDeployError::Plan(error.to_string()))?;
496            let mut current_group = Vec::new();
497            for (row_id, voice) in self.row_ids.iter().zip(&self.voices) {
498                let event_id = SerialEventId::new(format!(
499                    "{}/form-{}/block-{}",
500                    self.event_prefix,
501                    row_id.as_str().replace('/', "_"),
502                    block_index
503                ))
504                .map_err(|error| SerialDeployError::Plan(error.to_string()))?;
505                let start = block_index * self.block_size;
506                let ordinals = (start..start + self.block_size)
507                    .map(|ordinal| ordinal as u8)
508                    .collect::<Vec<_>>();
509                builder.add_event(
510                    "simultaneous-forms",
511                    structural_event(
512                        event_id.clone(),
513                        &ordinals,
514                        row_id.clone(),
515                        voice.clone(),
516                        self.rationale.clone(),
517                        self.license.clone(),
518                        EventPlacement::simultaneous(group.clone()),
519                    ),
520                )?;
521                current_group.push(event_id);
522            }
523            for previous_id in &previous_group {
524                for current_id in &current_group {
525                    builder.add_precedence(previous_id, current_id);
526                }
527            }
528            previous_group = current_group;
529        }
530        Ok(())
531    }
532}
533
534/// Creates one simultaneous-forms deployer.
535pub fn simultaneous_forms(spec: SimultaneousFormsSpec) -> SerialDeployer {
536    SerialDeployer {
537        inner: SerialDeployerInner::SimultaneousForms(spec),
538    }
539}