Skip to main content

solverforge_solver/builder/context/
model.rs

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