1use std::fmt;
2use std::marker::PhantomData;
3
4use solverforge_core::domain::{
5 DynamicListVariableSlot, DynamicScalarVariableSlot, SolutionDescriptor,
6};
7
8use super::{ConflictRepair, ListVariableSlot, ScalarGroupBinding, ScalarVariableSlot};
9
10pub enum VariableSlot<S, V, DM, IDM> {
11 Scalar(ScalarVariableSlot<S>),
12 List(ListVariableSlot<S, V, DM, IDM>),
13 DynamicScalar(DynamicScalarVariableSlot<S>),
14 DynamicList(DynamicListVariableSlot<S>),
15}
16
17impl<S, V, DM: Clone, IDM: Clone> Clone for VariableSlot<S, V, DM, IDM> {
18 fn clone(&self) -> Self {
19 match self {
20 Self::Scalar(variable) => Self::Scalar(*variable),
21 Self::List(variable) => Self::List(variable.clone()),
22 Self::DynamicScalar(variable) => Self::DynamicScalar(variable.clone()),
23 Self::DynamicList(variable) => Self::DynamicList(variable.clone()),
24 }
25 }
26}
27
28impl<S, V, DM: fmt::Debug, IDM: fmt::Debug> fmt::Debug for VariableSlot<S, V, DM, IDM> {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 Self::Scalar(variable) => variable.fmt(f),
32 Self::List(variable) => variable.fmt(f),
33 Self::DynamicScalar(variable) => variable.fmt(f),
34 Self::DynamicList(variable) => variable.fmt(f),
35 }
36 }
37}
38
39pub struct RuntimeModel<S, V, DM, IDM> {
40 variables: Vec<VariableSlot<S, V, DM, IDM>>,
41 scalar_groups: Vec<ScalarGroupBinding<S>>,
42 conflict_repairs: Vec<ConflictRepair<S>>,
43 _phantom: PhantomData<(fn() -> S, fn() -> V)>,
44}
45
46impl<S, V, DM: Clone, IDM: Clone> Clone for RuntimeModel<S, V, DM, IDM> {
47 fn clone(&self) -> Self {
48 Self {
49 variables: self.variables.clone(),
50 scalar_groups: self.scalar_groups.clone(),
51 conflict_repairs: self.conflict_repairs.clone(),
52 _phantom: PhantomData,
53 }
54 }
55}
56
57impl<S, V, DM, IDM> RuntimeModel<S, V, DM, IDM> {
58 pub fn new(variables: Vec<VariableSlot<S, V, DM, IDM>>) -> Self {
59 Self {
60 variables,
61 scalar_groups: Vec::new(),
62 conflict_repairs: Vec::new(),
63 _phantom: PhantomData,
64 }
65 }
66
67 pub fn with_scalar_groups(mut self, groups: Vec<ScalarGroupBinding<S>>) -> Self {
68 self.scalar_groups = groups;
69 self
70 }
71
72 pub fn with_conflict_repairs(mut self, repairs: Vec<ConflictRepair<S>>) -> Self {
73 self.conflict_repairs = repairs;
74 self
75 }
76
77 pub fn resolve_dynamic_descriptor_indexes(
78 mut self,
79 descriptor: &SolutionDescriptor,
80 ) -> Result<Self, String> {
81 for variable in &mut self.variables {
82 match variable {
83 VariableSlot::DynamicScalar(slot) => slot.resolve_descriptor_index(descriptor)?,
84 VariableSlot::DynamicList(slot) => slot.resolve_descriptor_index(descriptor)?,
85 VariableSlot::Scalar(_) | VariableSlot::List(_) => {}
86 }
87 }
88 Ok(self)
89 }
90
91 pub fn assert_dynamic_descriptor_indexes_resolved(&self) {
92 for variable in &self.variables {
93 match variable {
94 VariableSlot::DynamicScalar(slot) => {
95 let _ = slot.descriptor_index();
96 }
97 VariableSlot::DynamicList(slot) => {
98 let _ = slot.descriptor_index();
99 }
100 VariableSlot::Scalar(_) | VariableSlot::List(_) => {}
101 }
102 }
103 }
104
105 pub fn variables(&self) -> &[VariableSlot<S, V, DM, IDM>] {
106 &self.variables
107 }
108
109 pub fn scalar_groups(&self) -> &[ScalarGroupBinding<S>] {
110 &self.scalar_groups
111 }
112
113 pub fn conflict_repairs(&self) -> &[ConflictRepair<S>] {
114 &self.conflict_repairs
115 }
116
117 pub fn is_empty(&self) -> bool {
118 self.variables.is_empty()
119 }
120
121 pub fn has_list_variables(&self) -> bool {
122 self.variables.iter().any(|variable| {
123 matches!(
124 variable,
125 VariableSlot::List(_) | VariableSlot::DynamicList(_)
126 )
127 })
128 }
129
130 pub fn has_scalar_variables(&self) -> bool {
131 self.variables.iter().any(|variable| {
132 matches!(
133 variable,
134 VariableSlot::Scalar(_) | VariableSlot::DynamicScalar(_)
135 )
136 })
137 }
138
139 pub fn has_dynamic_variables(&self) -> bool {
140 self.variables.iter().any(|variable| {
141 matches!(
142 variable,
143 VariableSlot::DynamicScalar(_) | VariableSlot::DynamicList(_)
144 )
145 })
146 }
147
148 pub fn has_dynamic_list_variables(&self) -> bool {
149 self.variables
150 .iter()
151 .any(|variable| matches!(variable, VariableSlot::DynamicList(_)))
152 }
153
154 pub fn is_scalar_only(&self) -> bool {
155 self.has_scalar_variables() && !self.has_list_variables()
156 }
157
158 pub fn has_nearby_scalar_change_variables(&self) -> bool {
159 self.scalar_variables()
160 .any(ScalarVariableSlot::supports_nearby_change)
161 }
162
163 pub fn has_nearby_scalar_swap_variables(&self) -> bool {
164 self.scalar_variables()
165 .any(ScalarVariableSlot::supports_nearby_swap)
166 }
167
168 pub fn assignment_scalar_groups(
169 &self,
170 ) -> impl Iterator<Item = (usize, &ScalarGroupBinding<S>)> {
171 self.scalar_groups
172 .iter()
173 .enumerate()
174 .filter(|(_, group)| group.is_assignment())
175 }
176
177 pub fn assignment_group_covers_scalar_variable(
178 &self,
179 variable: &ScalarVariableSlot<S>,
180 ) -> bool {
181 self.assignment_scalar_groups().any(|(_, group)| {
182 group.members.iter().any(|member| {
183 member.descriptor_index == variable.descriptor_index
184 && member.variable_index == variable.variable_index
185 })
186 })
187 }
188
189 pub fn has_scalar_groups(&self) -> bool {
190 !self.scalar_groups.is_empty()
191 }
192
193 pub fn has_assignment_scalar_groups(&self) -> bool {
194 self.assignment_scalar_groups().next().is_some()
195 }
196
197 pub fn candidate_scalar_groups(&self) -> impl Iterator<Item = (usize, &ScalarGroupBinding<S>)> {
198 self.scalar_groups
199 .iter()
200 .enumerate()
201 .filter(|(_, group)| group.is_candidate_group())
202 }
203
204 pub fn has_candidate_scalar_groups(&self) -> bool {
205 self.candidate_scalar_groups().next().is_some()
206 }
207
208 pub fn has_list_ruin_variables(&self) -> bool {
209 self.list_variables().any(ListVariableSlot::supports_ruin)
210 }
211
212 pub fn list_precedence_variables(
213 &self,
214 ) -> impl Iterator<Item = &ListVariableSlot<S, V, DM, IDM>> {
215 self.list_variables()
216 .filter(|variable| variable.supports_precedence_moves())
217 }
218
219 pub fn has_list_precedence_variables(&self) -> bool {
220 self.list_precedence_variables().next().is_some()
221 }
222
223 pub fn has_k_opt_variables(&self) -> bool {
224 self.list_variables().any(ListVariableSlot::supports_k_opt)
225 }
226
227 pub fn has_conflict_repairs(&self) -> bool {
228 !self.conflict_repairs.is_empty()
229 }
230
231 pub fn scalar_variables(&self) -> impl Iterator<Item = &ScalarVariableSlot<S>> {
232 self.variables.iter().filter_map(|variable| match variable {
233 VariableSlot::Scalar(ctx) => Some(ctx),
234 VariableSlot::List(_)
235 | VariableSlot::DynamicScalar(_)
236 | VariableSlot::DynamicList(_) => None,
237 })
238 }
239
240 pub fn dynamic_scalar_variables(&self) -> impl Iterator<Item = &DynamicScalarVariableSlot<S>> {
241 self.variables.iter().filter_map(|variable| match variable {
242 VariableSlot::DynamicScalar(ctx) => Some(ctx),
243 VariableSlot::Scalar(_) | VariableSlot::List(_) | VariableSlot::DynamicList(_) => None,
244 })
245 }
246
247 pub fn scalar_variable_target(
248 &self,
249 entity_class: Option<&str>,
250 variable_name: Option<&str>,
251 ) -> Result<ScalarVariableSlot<S>, String> {
252 let mut matches = self
253 .scalar_variables()
254 .filter(|slot| slot.matches_target(entity_class, variable_name));
255 let Some(first) = matches.next().copied() else {
256 return Err(match (entity_class, variable_name) {
257 (Some(entity), Some(variable)) => {
258 format!("no scalar variable `{entity}.{variable}` exists in the runtime model")
259 }
260 (Some(entity), None) => {
261 format!("no scalar variable for entity `{entity}` exists in the runtime model")
262 }
263 (None, Some(variable)) => {
264 format!("no scalar variable named `{variable}` exists in the runtime model")
265 }
266 (None, None) => {
267 "exhaustive search requires exactly one scalar variable or an explicit target"
268 .to_string()
269 }
270 });
271 };
272 if matches.next().is_some() {
273 return Err(
274 "exhaustive search target is ambiguous; specify entity_class and variable_name"
275 .to_string(),
276 );
277 }
278 Ok(first)
279 }
280
281 pub fn finite_scalar_candidate_space_estimate(
282 &self,
283 solution: &S,
284 slot: ScalarVariableSlot<S>,
285 value_candidate_limit: Option<usize>,
286 ) -> Option<usize> {
287 let entity_count = (slot.entity_count)(solution);
288 let mut total: usize = 1;
289 for entity_index in 0..entity_count {
290 let candidate_count = slot
291 .candidate_values_for_entity(solution, entity_index, value_candidate_limit)
292 .len();
293 if candidate_count == 0 {
294 return Some(0);
295 }
296 total = total.checked_mul(candidate_count)?;
297 }
298 Some(total)
299 }
300
301 pub fn list_variables(&self) -> impl Iterator<Item = &ListVariableSlot<S, V, DM, IDM>> {
302 self.variables.iter().filter_map(|variable| match variable {
303 VariableSlot::List(ctx) => Some(ctx),
304 VariableSlot::Scalar(_)
305 | VariableSlot::DynamicScalar(_)
306 | VariableSlot::DynamicList(_) => None,
307 })
308 }
309
310 pub fn dynamic_list_variables(&self) -> impl Iterator<Item = &DynamicListVariableSlot<S>> {
311 self.variables.iter().filter_map(|variable| match variable {
312 VariableSlot::DynamicList(ctx) => Some(ctx),
313 VariableSlot::Scalar(_) | VariableSlot::List(_) | VariableSlot::DynamicScalar(_) => {
314 None
315 }
316 })
317 }
318}
319
320impl<S, V, DM: fmt::Debug, IDM: fmt::Debug> fmt::Debug for RuntimeModel<S, V, DM, IDM> {
321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 f.debug_struct("RuntimeModel")
323 .field("variables", &self.variables)
324 .field("scalar_groups", &self.scalar_groups)
325 .field("conflict_repairs", &self.conflict_repairs)
326 .finish()
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use solverforge_core::domain::{
333 DynamicListVariableSlot, DynamicModelBackend, DynamicScalarVariableSlot, EntityClassId,
334 VariableId,
335 };
336 use solverforge_core::score::SoftScore;
337
338 use super::{RuntimeModel, VariableSlot};
339
340 #[derive(Clone)]
341 struct DynamicRows;
342
343 impl DynamicModelBackend for DynamicRows {
344 type Score = SoftScore;
345
346 fn entity_count(&self, _entity: EntityClassId) -> usize {
347 0
348 }
349
350 fn get_scalar(
351 &self,
352 _entity: EntityClassId,
353 _row: usize,
354 _variable: VariableId,
355 ) -> Option<usize> {
356 None
357 }
358
359 fn set_scalar(
360 &mut self,
361 _entity: EntityClassId,
362 _row: usize,
363 _variable: VariableId,
364 _value: Option<usize>,
365 ) {
366 }
367
368 fn list_len(&self, _entity: EntityClassId, _row: usize, _variable: VariableId) -> usize {
369 0
370 }
371
372 fn list_get(
373 &self,
374 _entity: EntityClassId,
375 _row: usize,
376 _variable: VariableId,
377 _pos: usize,
378 ) -> Option<usize> {
379 None
380 }
381
382 fn list_insert(
383 &mut self,
384 _entity: EntityClassId,
385 _row: usize,
386 _variable: VariableId,
387 _pos: usize,
388 _value: usize,
389 ) {
390 }
391
392 fn list_remove(
393 &mut self,
394 _entity: EntityClassId,
395 _row: usize,
396 _variable: VariableId,
397 _pos: usize,
398 ) -> Option<usize> {
399 None
400 }
401
402 fn candidate_values(
403 &self,
404 _entity: EntityClassId,
405 _row: usize,
406 _variable: VariableId,
407 ) -> &[usize] {
408 &[]
409 }
410 }
411
412 #[test]
413 fn runtime_model_tracks_dynamic_variable_slots() {
414 let scalar =
415 DynamicScalarVariableSlot::new(EntityClassId(0), VariableId(0), "Task", "worker", true);
416 let list =
417 DynamicListVariableSlot::new(EntityClassId(1), VariableId(0), "Vehicle", "visits");
418
419 let model: RuntimeModel<DynamicRows, usize, (), ()> = RuntimeModel::new(vec![
420 VariableSlot::DynamicScalar(scalar.clone()),
421 VariableSlot::DynamicList(list.clone()),
422 ]);
423
424 assert!(model.has_scalar_variables());
425 assert!(model.has_list_variables());
426 assert_eq!(
427 model
428 .dynamic_scalar_variables()
429 .map(|slot| slot.variable_name)
430 .collect::<Vec<_>>(),
431 vec!["worker"]
432 );
433 assert_eq!(
434 model
435 .dynamic_list_variables()
436 .map(|slot| slot.variable_name)
437 .collect::<Vec<_>>(),
438 vec!["visits"]
439 );
440 assert_eq!(model.scalar_variables().count(), 0);
441 assert_eq!(model.list_variables().count(), 0);
442 }
443
444 #[test]
445 #[should_panic(
446 expected = "dynamic scalar variable Task.worker has not been resolved against a SolutionDescriptor"
447 )]
448 fn runtime_model_rejects_unresolved_dynamic_slots_for_selector_use() {
449 let scalar =
450 DynamicScalarVariableSlot::new(EntityClassId(0), VariableId(0), "Task", "worker", true);
451 let model: RuntimeModel<DynamicRows, usize, (), ()> =
452 RuntimeModel::new(vec![VariableSlot::DynamicScalar(scalar)]);
453
454 model.assert_dynamic_descriptor_indexes_resolved();
455 }
456}