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 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}