Skip to main content

sim_lib_music_serial/
plan.rs

1//! Immutable serial-plan construction and validation.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use sim_lib_pitch_serial::RowForm;
6
7use crate::{
8    OrdinalRef, PlannedSerialEvent, PrecedenceGraph, RowInstanceId, SerialEventId, SerialOrigin,
9    SerialPlanError, SerialRole, SimultaneousGroupId,
10};
11
12/// Immutable structural source for serial practice.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct SerialPlan {
15    rows: BTreeMap<RowInstanceId, RowForm>,
16    events: BTreeMap<SerialEventId, PlannedSerialEvent>,
17    precedence: PrecedenceGraph<SerialEventId>,
18}
19
20impl SerialPlan {
21    /// Validates and freezes one complete serial plan.
22    pub fn try_new(
23        rows: BTreeMap<RowInstanceId, RowForm>,
24        events: BTreeMap<SerialEventId, PlannedSerialEvent>,
25        precedence_edges: impl IntoIterator<Item = (SerialEventId, SerialEventId)>,
26    ) -> Result<Self, SerialPlanError> {
27        let event_ids = events.keys().cloned().collect::<BTreeSet<_>>();
28        let precedence = PrecedenceGraph::try_new(precedence_edges, &event_ids)?;
29        validate_events(&rows, &events)?;
30        validate_parent_graph(&events)?;
31        validate_simultaneous_constraints(&events, &precedence)?;
32        validate_structural_coverage(&rows, &events)?;
33        Ok(Self {
34            rows,
35            events,
36            precedence,
37        })
38    }
39
40    /// Returns the immutable row instances by stable id.
41    pub fn rows(&self) -> &BTreeMap<RowInstanceId, RowForm> {
42        &self.rows
43    }
44
45    /// Returns the immutable planned events by stable id.
46    pub fn events(&self) -> &BTreeMap<SerialEventId, PlannedSerialEvent> {
47        &self.events
48    }
49
50    /// Returns one immutable planned event by stable id.
51    pub fn event(&self, event_id: &SerialEventId) -> Option<&PlannedSerialEvent> {
52        self.events.get(event_id)
53    }
54
55    /// Returns one immutable row form by stable id.
56    pub fn row(&self, row_id: &RowInstanceId) -> Option<&RowForm> {
57        self.rows.get(row_id)
58    }
59
60    /// Returns the validated precedence DAG.
61    pub fn precedence(&self) -> &PrecedenceGraph<SerialEventId> {
62        &self.precedence
63    }
64
65    /// Returns every simultaneous placement group and its canonical event members.
66    pub fn simultaneous_groups(&self) -> BTreeMap<SimultaneousGroupId, Vec<&PlannedSerialEvent>> {
67        let mut groups: BTreeMap<SimultaneousGroupId, Vec<&PlannedSerialEvent>> = BTreeMap::new();
68        for event in self.events.values() {
69            if let Some(group) = event.placement.simultaneous_group() {
70                groups.entry(group.clone()).or_default().push(event);
71            }
72        }
73        groups
74    }
75}
76
77fn validate_events(
78    rows: &BTreeMap<RowInstanceId, RowForm>,
79    events: &BTreeMap<SerialEventId, PlannedSerialEvent>,
80) -> Result<(), SerialPlanError> {
81    for (event_id, event) in events {
82        if &event.id != event_id {
83            return Err(SerialPlanError::InvalidId {
84                kind: "serial-event-key",
85                value: event.id.as_str().to_owned(),
86                reason: "map key and event id must match",
87            });
88        }
89        if event.ordinals.is_empty() {
90            return Err(SerialPlanError::EmptyOrdinalSet(event.id.clone()));
91        }
92        if event.licenses.is_empty() {
93            return Err(SerialPlanError::MissingStructuralLicenses(event.id.clone()));
94        }
95        let mut seen_ordinals = BTreeSet::new();
96        for ordinal in &event.ordinals {
97            if !seen_ordinals.insert(ordinal.clone()) {
98                return Err(SerialPlanError::DuplicateOrdinal {
99                    event_id: event.id.clone(),
100                    ordinal: ordinal.clone(),
101                });
102            }
103            let Some(row) = rows.get(&ordinal.row_id) else {
104                return Err(SerialPlanError::UnknownRow {
105                    event_id: event.id.clone(),
106                    row_id: ordinal.row_id.clone(),
107                });
108            };
109            let row_len = row.classes().len();
110            if ordinal.ordinal >= row_len {
111                return Err(SerialPlanError::OrdinalOutOfRange {
112                    event_id: event.id.clone(),
113                    row_id: ordinal.row_id.clone(),
114                    ordinal: ordinal.ordinal,
115                    row_len,
116                });
117            }
118        }
119        validate_role_origin(event_id, event)?;
120    }
121    Ok(())
122}
123
124fn validate_role_origin(
125    event_id: &SerialEventId,
126    event: &PlannedSerialEvent,
127) -> Result<(), SerialPlanError> {
128    let parents_empty = event.parents.is_empty();
129    match (event.role, &event.origin, parents_empty) {
130        (SerialRole::Structural, SerialOrigin::Structural { rationale }, true)
131            if !rationale.trim().is_empty() =>
132        {
133            Ok(())
134        }
135        (SerialRole::Structural, SerialOrigin::Structural { .. }, false) => {
136            Err(SerialPlanError::RoleOriginMismatch {
137                event_id: event_id.clone(),
138                reason: "structural events cannot name parents",
139            })
140        }
141        (SerialRole::Structural, _, _) => Err(SerialPlanError::RoleOriginMismatch {
142            event_id: event_id.clone(),
143            reason: "structural role requires structural origin",
144        }),
145        (SerialRole::Derived, SerialOrigin::Derived { technique }, false)
146            if !technique.trim().is_empty() =>
147        {
148            Ok(())
149        }
150        (SerialRole::Derived, SerialOrigin::Derived { .. }, true) => {
151            Err(SerialPlanError::MissingParents {
152                event_id: event_id.clone(),
153                role: SerialRole::Derived.as_str(),
154            })
155        }
156        (SerialRole::Derived, _, _) => Err(SerialPlanError::RoleOriginMismatch {
157            event_id: event_id.clone(),
158            reason: "derived role requires derived origin",
159        }),
160        (SerialRole::Ornamental, SerialOrigin::Ornamental { technique }, false)
161            if !technique.trim().is_empty() =>
162        {
163            Ok(())
164        }
165        (SerialRole::Ornamental, SerialOrigin::Ornamental { .. }, true) => {
166            Err(SerialPlanError::MissingParents {
167                event_id: event_id.clone(),
168                role: SerialRole::Ornamental.as_str(),
169            })
170        }
171        (SerialRole::Ornamental, _, _) => Err(SerialPlanError::RoleOriginMismatch {
172            event_id: event_id.clone(),
173            reason: "ornamental role requires ornamental origin",
174        }),
175        (SerialRole::External, SerialOrigin::External { source }, false)
176            if !source.trim().is_empty() =>
177        {
178            Ok(())
179        }
180        (SerialRole::External, SerialOrigin::External { .. }, true) => {
181            Err(SerialPlanError::MissingParents {
182                event_id: event_id.clone(),
183                role: SerialRole::External.as_str(),
184            })
185        }
186        (SerialRole::External, _, _) => Err(SerialPlanError::RoleOriginMismatch {
187            event_id: event_id.clone(),
188            reason: "external role requires external origin",
189        }),
190    }
191}
192
193fn validate_parent_graph(
194    events: &BTreeMap<SerialEventId, PlannedSerialEvent>,
195) -> Result<(), SerialPlanError> {
196    #[derive(Copy, Clone, PartialEq, Eq)]
197    enum Mark {
198        Visiting,
199        Done,
200    }
201
202    fn visit(
203        id: &SerialEventId,
204        events: &BTreeMap<SerialEventId, PlannedSerialEvent>,
205        marks: &mut BTreeMap<SerialEventId, Mark>,
206    ) -> Result<(), SerialPlanError> {
207        match marks.get(id) {
208            Some(Mark::Done) => return Ok(()),
209            Some(Mark::Visiting) => return Err(SerialPlanError::ParentCycle(id.clone())),
210            None => {}
211        }
212        marks.insert(id.clone(), Mark::Visiting);
213        let event = events.get(id).expect("known event");
214        for parent in &event.parents {
215            if parent == id {
216                return Err(SerialPlanError::SelfParent(id.clone()));
217            }
218            if !events.contains_key(parent) {
219                return Err(SerialPlanError::UnknownParent {
220                    event_id: id.clone(),
221                    parent_id: parent.clone(),
222                });
223            }
224            visit(parent, events, marks)?;
225        }
226        marks.insert(id.clone(), Mark::Done);
227        Ok(())
228    }
229
230    let mut marks = BTreeMap::new();
231    for event_id in events.keys() {
232        visit(event_id, events, &mut marks)?;
233    }
234    Ok(())
235}
236
237fn validate_simultaneous_constraints(
238    events: &BTreeMap<SerialEventId, PlannedSerialEvent>,
239    precedence: &PrecedenceGraph<SerialEventId>,
240) -> Result<(), SerialPlanError> {
241    let mut by_group: BTreeMap<&SimultaneousGroupId, Vec<&PlannedSerialEvent>> = BTreeMap::new();
242    for event in events.values() {
243        if let Some(group) = event.placement.simultaneous_group() {
244            by_group.entry(group).or_default().push(event);
245        }
246    }
247    for (group_id, members) in by_group {
248        for left in 0..members.len() {
249            for right in (left + 1)..members.len() {
250                let before = &members[left].id;
251                let after = &members[right].id;
252                if precedence.contains_edge(before, after)
253                    || precedence.contains_edge(after, before)
254                {
255                    return Err(SerialPlanError::SimultaneousPrecedenceConflict {
256                        group_id: group_id.clone(),
257                        before: before.clone(),
258                        after: after.clone(),
259                    });
260                }
261            }
262        }
263    }
264    Ok(())
265}
266
267fn validate_structural_coverage(
268    rows: &BTreeMap<RowInstanceId, RowForm>,
269    events: &BTreeMap<SerialEventId, PlannedSerialEvent>,
270) -> Result<(), SerialPlanError> {
271    let mut covered: BTreeMap<RowInstanceId, BTreeSet<usize>> = BTreeMap::new();
272    for event in events
273        .values()
274        .filter(|event| event.role == SerialRole::Structural)
275    {
276        for OrdinalRef { row_id, ordinal } in &event.ordinals {
277            covered.entry(row_id.clone()).or_default().insert(*ordinal);
278        }
279    }
280    for (row_id, row) in rows {
281        let row_len = row.classes().len();
282        let missing = (0..row_len)
283            .filter(|ordinal| !covered.get(row_id).is_some_and(|set| set.contains(ordinal)))
284            .collect::<Vec<_>>();
285        if !missing.is_empty() {
286            return Err(SerialPlanError::MissingStructuralCoverage {
287                row_id: row_id.clone(),
288                ordinals: missing,
289            });
290        }
291    }
292    Ok(())
293}