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#[derive(Clone)]
18pub struct HorizontalStatementSpec {
19 pub row_id: RowInstanceId,
21 pub event_id: SerialEventId,
23 pub voice: VoiceId,
25 pub rationale: String,
27 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
53pub 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#[derive(Clone)]
74pub struct MotivicPartitionSpec {
75 pub row_id: RowInstanceId,
77 pub partition: RowPartition,
79 pub voices: Vec<VoiceId>,
81 pub event_prefix: String,
83 pub rationale: String,
85 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
127pub 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#[derive(Clone)]
150pub struct VerticalBlocksSpec {
151 pub row_id: RowInstanceId,
153 pub partition: RowPartition,
155 pub selected_blocks: Vec<usize>,
157 pub voice: VoiceId,
159 pub event_prefix: String,
161 pub rationale: String,
163 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
205pub fn verticalize_selected_blocks(spec: VerticalBlocksSpec) -> SerialDeployer {
207 SerialDeployer {
208 inner: SerialDeployerInner::Vertical(spec),
209 }
210}
211
212#[derive(Clone)]
214pub struct InterlockingPartitionSpec {
215 pub row_id: RowInstanceId,
217 pub partition: RowPartition,
219 pub counter_partition: RowPartition,
221 pub voices: Vec<VoiceId>,
223 pub event_prefix: String,
225 pub rationale: String,
227 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
275pub fn interlocking_partition(spec: InterlockingPartitionSpec) -> SerialDeployer {
277 SerialDeployer {
278 inner: SerialDeployerInner::Interlocking(spec),
279 }
280}
281
282#[derive(Clone)]
284pub struct MelodyAccompanimentSpec {
285 pub row_id: RowInstanceId,
287 pub partition: RowPartition,
289 pub melody_voice: VoiceId,
291 pub accompaniment_voice: VoiceId,
293 pub event_prefix: String,
295 pub rationale: String,
297 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
352pub fn melody_accompaniment_distribution(spec: MelodyAccompanimentSpec) -> SerialDeployer {
354 SerialDeployer {
355 inner: SerialDeployerInner::MelodyAccompaniment(spec),
356 }
357}
358
359#[derive(Clone)]
361pub struct AggregateRotationSpec {
362 pub row_id: RowInstanceId,
364 pub rotation: usize,
366 pub block_lengths: Vec<usize>,
368 pub voices: Vec<VoiceId>,
370 pub event_prefix: String,
372 pub rationale: String,
374 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#[allow(clippy::missing_const_for_fn)]
432pub fn aggregate_rotation(spec: AggregateRotationSpec) -> SerialDeployer {
433 SerialDeployer {
434 inner: SerialDeployerInner::AggregateRotation(spec),
435 }
436}
437
438#[derive(Clone)]
440pub struct SimultaneousFormsSpec {
441 pub row_ids: Vec<RowInstanceId>,
443 pub voices: Vec<VoiceId>,
445 pub block_size: usize,
447 pub event_prefix: String,
449 pub rationale: String,
451 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 ¤t_group {
525 builder.add_precedence(previous_id, current_id);
526 }
527 }
528 previous_group = current_group;
529 }
530 Ok(())
531 }
532}
533
534pub fn simultaneous_forms(spec: SimultaneousFormsSpec) -> SerialDeployer {
536 SerialDeployer {
537 inner: SerialDeployerInner::SimultaneousForms(spec),
538 }
539}