sim_lib_music_consonance/
completion.rs1use std::cmp::Ordering;
2
3use sim_lib_discrete_search::{
4 SearchControl, SearchInterrupt, SearchProblem, SearchReceipt, SearchStep, solve,
5};
6use sim_lib_music_core::{ObjectId, Staff};
7use thiserror::Error;
8
9use crate::constraints::changed_spans;
10use crate::patch::addition_ids;
11use crate::{
12 Addition, CompletionConstraints, ConsonanceError, ConsonancePatch, ConsonancePolicy,
13 ConsonanceReport, ConstraintError, PatchError, TimeSpan, apply_patch, evaluate_staff,
14};
15
16#[derive(Clone, Debug, Default, PartialEq)]
18pub struct CompletionRequest {
19 pub candidates: Vec<Addition>,
21 pub constraints: CompletionConstraints,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct CompletionProvenance {
28 pub selected_candidates: Vec<usize>,
30 pub preserved_ids: Vec<ObjectId>,
32 pub added_ids: Vec<ObjectId>,
34 pub facts: Vec<String>,
36}
37
38#[derive(Clone, Debug, PartialEq)]
40pub struct CompletionResult {
41 pub patch: ConsonancePatch,
43 pub before: ConsonanceReport,
45 pub after: ConsonanceReport,
47 pub changed_windows: Vec<TimeSpan>,
49 pub provenance: CompletionProvenance,
51 pub search: SearchReceipt,
53}
54
55#[derive(Clone, Debug, Error, PartialEq)]
57pub enum CompletionError {
58 #[error(transparent)]
60 Consonance(#[from] ConsonanceError),
61 #[error(transparent)]
63 Patch(#[from] PatchError),
64 #[error(transparent)]
66 Constraint(#[from] ConstraintError),
67 #[error("bounded completion produced no feasible patch")]
69 NoCompletion {
70 before: Box<ConsonanceReport>,
72 search: Box<SearchReceipt>,
74 },
75}
76
77pub fn complete_staff(
83 source: &Staff,
84 policy: &ConsonancePolicy,
85 request: &CompletionRequest,
86 control: SearchControl,
87 interrupt: &dyn SearchInterrupt,
88) -> Result<CompletionResult, CompletionError> {
89 request.constraints.validate(source)?;
90 for candidate in &request.candidates {
91 ConsonancePatch::new(source, vec![candidate.clone()])?;
92 }
93 let before = evaluate_staff(source, policy)?;
94 let problem = CompletionProblem {
95 source,
96 policy,
97 request,
98 };
99 let run = solve(&problem, control, interrupt);
100 let receipt = run.receipt;
101 let Some(candidate) = run.outputs.into_iter().min_by(compare_outputs) else {
102 return Err(CompletionError::NoCompletion {
103 before: Box::new(before),
104 search: Box::new(receipt),
105 });
106 };
107 let restored = crate::remove_patch(&candidate.completed, &candidate.patch)?;
108 if restored != *source {
109 return Err(PatchError::InvalidInverse(
110 "remove(apply(source, patch), patch) changed source identities or values".to_owned(),
111 )
112 .into());
113 }
114 let preserved_ids = source.object_ids();
115 let added_ids = addition_ids(&candidate.patch.additions);
116 let changed_windows = changed_spans(&candidate.after, &candidate.patch.additions);
117 Ok(CompletionResult {
118 patch: candidate.patch,
119 before,
120 after: candidate.after,
121 changed_windows,
122 provenance: CompletionProvenance {
123 selected_candidates: candidate.selected,
124 preserved_ids,
125 added_ids,
126 facts: vec![
127 "source-material=immutable".to_owned(),
128 "patch-operation=additions-only".to_owned(),
129 "inverse=remove(apply(source,patch),patch)==source".to_owned(),
130 "metric-thresholds=checked-per-intersecting-window".to_owned(),
131 ],
132 },
133 search: receipt,
134 })
135}
136
137struct CompletionProblem<'a> {
138 source: &'a Staff,
139 policy: &'a ConsonancePolicy,
140 request: &'a CompletionRequest,
141}
142
143#[derive(Clone, Debug)]
144struct CompletionState {
145 cursor: usize,
146 selected: Vec<usize>,
147}
148
149#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
150enum CompletionChoice {
151 Skip,
152 Include,
153}
154
155#[derive(Clone, Debug)]
156struct CompletionOutput {
157 selected: Vec<usize>,
158 patch: ConsonancePatch,
159 completed: Staff,
160 after: ConsonanceReport,
161}
162
163impl SearchProblem for CompletionProblem<'_> {
164 type State = CompletionState;
165 type Choice = CompletionChoice;
166 type Output = CompletionOutput;
167
168 fn initial_state(&self) -> Self::State {
169 CompletionState {
170 cursor: 0,
171 selected: Vec::new(),
172 }
173 }
174
175 fn expand(&self, state: &Self::State, out: &mut Vec<Self::Choice>) {
176 if state.cursor < self.request.candidates.len() {
177 out.extend([CompletionChoice::Skip, CompletionChoice::Include]);
178 }
179 }
180
181 fn apply(&self, state: &Self::State, choice: &Self::Choice) -> SearchStep<Self::State> {
182 let mut next = state.clone();
183 if *choice == CompletionChoice::Include {
184 next.selected.push(state.cursor);
185 }
186 next.cursor += 1;
187 let additions = self.selected_additions(&next.selected);
188 match self
189 .request
190 .constraints
191 .accepts_partial(self.source, &additions)
192 {
193 Ok(true) => SearchStep::Continue(next),
194 Ok(false) => SearchStep::pruned("completion constraints rejected candidate prefix"),
195 Err(error) => SearchStep::pruned(error.to_string()),
196 }
197 }
198
199 fn finish(&self, state: &Self::State) -> Option<Self::Output> {
200 if state.cursor != self.request.candidates.len() {
201 return None;
202 }
203 let additions = self.selected_additions(&state.selected);
204 let patch = ConsonancePatch::new(self.source, additions.clone()).ok()?;
205 let completed = apply_patch(self.source, &patch).ok()?;
206 let after = evaluate_staff(&completed, self.policy).ok()?;
207 self.request
208 .constraints
209 .accepts_complete(self.source, &additions, &after)
210 .ok()?
211 .then(|| CompletionOutput {
212 selected: state.selected.clone(),
213 patch,
214 completed,
215 after,
216 })
217 }
218
219 fn score_state(&self, state: &Self::State) -> i64 {
220 i64::try_from(state.selected.len()).unwrap_or(i64::MAX)
221 }
222
223 fn bound(&self, state: &Self::State) -> Option<i64> {
224 Some(i64::try_from(state.selected.len()).unwrap_or(i64::MAX))
225 }
226
227 fn output_score(&self, output: &Self::Output) -> Option<i64> {
228 Some(i64::try_from(output.selected.len()).unwrap_or(i64::MAX))
229 }
230}
231
232impl CompletionProblem<'_> {
233 fn selected_additions(&self, selected: &[usize]) -> Vec<Addition> {
234 selected
235 .iter()
236 .map(|index| self.request.candidates[*index].clone())
237 .collect()
238 }
239}
240
241fn compare_outputs(left: &CompletionOutput, right: &CompletionOutput) -> Ordering {
242 left.selected
243 .len()
244 .cmp(&right.selected.len())
245 .then_with(|| left.selected.cmp(&right.selected))
246}