Skip to main content

solverforge_solver/builder/context/
model.rs

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}