solverforge_solver/heuristic/selector/
pillar.rs

1//! Pillar selector for selecting groups of entities with the same variable value.
2//!
3//! A pillar is a group of entities that share the same planning variable value.
4//! Pillar moves operate on entire pillars, changing or swapping all entities
5//! in the pillar atomically.
6
7use std::collections::HashMap;
8use std::fmt::Debug;
9use std::hash::Hash;
10use std::marker::PhantomData;
11
12use solverforge_core::domain::PlanningSolution;
13use solverforge_scoring::ScoreDirector;
14
15use super::entity::{EntityReference, EntitySelector};
16
17/// A pillar is a group of entity references that share the same variable value.
18///
19/// All entities in a pillar have the same value for a specific planning variable,
20/// which allows them to be moved together atomically.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Pillar {
23    /// The entity references in this pillar.
24    pub entities: Vec<EntityReference>,
25}
26
27impl Pillar {
28    /// Creates a new pillar with the given entities.
29    pub fn new(entities: Vec<EntityReference>) -> Self {
30        Self { entities }
31    }
32
33    /// Returns the number of entities in this pillar.
34    pub fn size(&self) -> usize {
35        self.entities.len()
36    }
37
38    /// Returns true if this pillar is empty.
39    pub fn is_empty(&self) -> bool {
40        self.entities.is_empty()
41    }
42
43    /// Returns the first entity reference in this pillar.
44    pub fn first(&self) -> Option<&EntityReference> {
45        self.entities.first()
46    }
47
48    /// Returns an iterator over the entity references.
49    pub fn iter(&self) -> impl Iterator<Item = &EntityReference> {
50        self.entities.iter()
51    }
52}
53
54/// Trait for selecting pillars of entities.
55///
56/// A pillar selector groups entities by their variable values and returns
57/// groups (pillars) that can be moved together.
58///
59/// # Type Parameters
60/// * `S` - The planning solution type
61pub trait PillarSelector<S: PlanningSolution>: Send + Debug {
62    /// Returns an iterator over pillars.
63    ///
64    /// Each pillar contains entity references for entities that share
65    /// the same variable value.
66    fn iter<'a, D: ScoreDirector<S>>(
67        &'a self,
68        score_director: &'a D,
69    ) -> Box<dyn Iterator<Item = Pillar> + 'a>;
70
71    /// Returns the approximate number of pillars.
72    fn size<D: ScoreDirector<S>>(&self, score_director: &D) -> usize;
73
74    /// Returns true if this selector may return the same pillar multiple times.
75    fn is_never_ending(&self) -> bool {
76        false
77    }
78
79    /// Returns the descriptor index this selector operates on.
80    fn descriptor_index(&self) -> usize;
81}
82
83/// Configuration for sub-pillar selection.
84#[derive(Debug, Clone)]
85pub struct SubPillarConfig {
86    /// Whether sub-pillar selection is enabled.
87    pub enabled: bool,
88    /// Minimum size of a sub-pillar (default: 1).
89    pub minimum_size: usize,
90    /// Maximum size of a sub-pillar (default: usize::MAX).
91    pub maximum_size: usize,
92}
93
94impl Default for SubPillarConfig {
95    fn default() -> Self {
96        Self {
97            enabled: false,
98            minimum_size: 1,
99            maximum_size: usize::MAX,
100        }
101    }
102}
103
104impl SubPillarConfig {
105    /// Creates a new sub-pillar config with sub-pillars disabled.
106    pub fn none() -> Self {
107        Self::default()
108    }
109
110    /// Creates a new sub-pillar config with sub-pillars enabled.
111    pub fn all() -> Self {
112        Self {
113            enabled: true,
114            minimum_size: 1,
115            maximum_size: usize::MAX,
116        }
117    }
118
119    /// Sets the minimum sub-pillar size.
120    pub fn with_minimum_size(mut self, size: usize) -> Self {
121        self.minimum_size = size.max(1);
122        self
123    }
124
125    /// Sets the maximum sub-pillar size.
126    pub fn with_maximum_size(mut self, size: usize) -> Self {
127        self.maximum_size = size;
128        self
129    }
130}
131
132/// A pillar selector that groups entities by their variable value.
133///
134/// This selector uses an entity selector to get entities, then groups them
135/// by the value of a specified variable using a value extractor function.
136///
137/// # Zero-Erasure Design
138///
139/// Both the entity selector `ES` and the extractor function `E` are stored as
140/// concrete generic type parameters, eliminating all virtual dispatch overhead.
141///
142/// **Note**: The value extractor closure uses `&dyn ScoreDirector<S>` intentionally.
143/// This is a scorer-agnostic boundary - the closure doesn't need the concrete `D` type.
144pub struct DefaultPillarSelector<S, V, ES, E>
145where
146    S: PlanningSolution,
147    V: Clone + Eq + Hash + Send + Sync + 'static,
148    ES: EntitySelector<S>,
149    E: Fn(&dyn ScoreDirector<S>, usize, usize) -> Option<V> + Send + Sync,
150{
151    /// The underlying entity selector (zero-erasure).
152    entity_selector: ES,
153    /// The descriptor index.
154    descriptor_index: usize,
155    /// The variable name for grouping.
156    variable_name: &'static str,
157    /// Function to extract the value from an entity for grouping (zero-erasure).
158    value_extractor: E,
159    /// Sub-pillar configuration.
160    sub_pillar_config: SubPillarConfig,
161    /// Marker for solution and value types.
162    _phantom: PhantomData<(fn() -> S, V)>,
163}
164
165impl<S, V, ES, E> Debug for DefaultPillarSelector<S, V, ES, E>
166where
167    S: PlanningSolution,
168    V: Clone + Eq + Hash + Send + Sync + 'static,
169    ES: EntitySelector<S> + Debug,
170    E: Fn(&dyn ScoreDirector<S>, usize, usize) -> Option<V> + Send + Sync,
171{
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        f.debug_struct("DefaultPillarSelector")
174            .field("entity_selector", &self.entity_selector)
175            .field("descriptor_index", &self.descriptor_index)
176            .field("variable_name", &self.variable_name)
177            .field("sub_pillar_config", &self.sub_pillar_config)
178            .finish()
179    }
180}
181
182impl<S, V, ES, E> DefaultPillarSelector<S, V, ES, E>
183where
184    S: PlanningSolution,
185    V: Clone + Eq + Hash + Send + Sync + 'static,
186    ES: EntitySelector<S>,
187    E: Fn(&dyn ScoreDirector<S>, usize, usize) -> Option<V> + Send + Sync,
188{
189    /// Creates a new default pillar selector.
190    ///
191    /// # Arguments
192    ///
193    /// * `entity_selector` - The entity selector to get entities from
194    /// * `descriptor_index` - The entity descriptor index
195    /// * `variable_name` - The variable name for grouping
196    /// * `value_extractor` - Function to extract the grouping value from an entity
197    pub fn new(
198        entity_selector: ES,
199        descriptor_index: usize,
200        variable_name: &'static str,
201        value_extractor: E,
202    ) -> Self {
203        Self {
204            entity_selector,
205            descriptor_index,
206            variable_name,
207            value_extractor,
208            sub_pillar_config: SubPillarConfig::default(),
209            _phantom: PhantomData,
210        }
211    }
212
213    /// Sets the sub-pillar configuration.
214    pub fn with_sub_pillar_config(mut self, config: SubPillarConfig) -> Self {
215        self.sub_pillar_config = config;
216        self
217    }
218
219    /// Returns the variable name.
220    pub fn variable_name(&self) -> &str {
221        self.variable_name
222    }
223
224    /// Builds the pillar list from the current solution state.
225    fn build_pillars<D: ScoreDirector<S>>(&self, score_director: &D) -> Vec<Pillar> {
226        // Group entities by their value
227        let mut value_to_entities: HashMap<Option<V>, Vec<EntityReference>> = HashMap::new();
228
229        for entity_ref in self.entity_selector.iter(score_director) {
230            // Use dyn ScoreDirector for the extractor (intentional type-erasure boundary)
231            let value = (self.value_extractor)(
232                score_director,
233                entity_ref.descriptor_index,
234                entity_ref.entity_index,
235            );
236            value_to_entities.entry(value).or_default().push(entity_ref);
237        }
238
239        // Filter by minimum size and create pillars
240        let min_size = self.sub_pillar_config.minimum_size;
241        value_to_entities
242            .into_values()
243            .filter(|entities| entities.len() >= min_size)
244            .map(Pillar::new)
245            .collect()
246    }
247}
248
249impl<S, V, ES, E> PillarSelector<S> for DefaultPillarSelector<S, V, ES, E>
250where
251    S: PlanningSolution,
252    V: Clone + Eq + Hash + Send + Sync + 'static,
253    ES: EntitySelector<S>,
254    E: Fn(&dyn ScoreDirector<S>, usize, usize) -> Option<V> + Send + Sync,
255{
256    fn iter<'a, D: ScoreDirector<S>>(
257        &'a self,
258        score_director: &'a D,
259    ) -> Box<dyn Iterator<Item = Pillar> + 'a> {
260        let pillars = self.build_pillars(score_director);
261        Box::new(pillars.into_iter())
262    }
263
264    fn size<D: ScoreDirector<S>>(&self, score_director: &D) -> usize {
265        self.build_pillars(score_director).len()
266    }
267
268    fn descriptor_index(&self) -> usize {
269        self.descriptor_index
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use crate::heuristic::selector::entity::FromSolutionEntitySelector;
277    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
278    use solverforge_core::score::SimpleScore;
279    use solverforge_scoring::SimpleScoreDirector;
280    use std::any::TypeId;
281
282    #[derive(Clone, Debug)]
283    struct Employee {
284        id: usize,
285        shift: Option<i32>,
286    }
287
288    #[derive(Clone, Debug)]
289    struct ScheduleSolution {
290        employees: Vec<Employee>,
291        score: Option<SimpleScore>,
292    }
293
294    impl PlanningSolution for ScheduleSolution {
295        type Score = SimpleScore;
296
297        fn score(&self) -> Option<Self::Score> {
298            self.score
299        }
300
301        fn set_score(&mut self, score: Option<Self::Score>) {
302            self.score = score;
303        }
304    }
305
306    fn get_employees(s: &ScheduleSolution) -> &Vec<Employee> {
307        &s.employees
308    }
309
310    fn get_employees_mut(s: &mut ScheduleSolution) -> &mut Vec<Employee> {
311        &mut s.employees
312    }
313
314    fn create_test_director(
315        employees: Vec<Employee>,
316    ) -> SimpleScoreDirector<ScheduleSolution, impl Fn(&ScheduleSolution) -> SimpleScore> {
317        let solution = ScheduleSolution {
318            employees,
319            score: None,
320        };
321
322        let extractor = Box::new(TypedEntityExtractor::new(
323            "Employee",
324            "employees",
325            get_employees,
326            get_employees_mut,
327        ));
328        let entity_desc = EntityDescriptor::new("Employee", TypeId::of::<Employee>(), "employees")
329            .with_extractor(extractor);
330
331        let descriptor =
332            SolutionDescriptor::new("ScheduleSolution", TypeId::of::<ScheduleSolution>())
333                .with_entity(entity_desc);
334
335        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
336    }
337
338    #[test]
339    fn test_pillar_new() {
340        let pillar = Pillar::new(vec![EntityReference::new(0, 0), EntityReference::new(0, 1)]);
341
342        assert_eq!(pillar.size(), 2);
343        assert!(!pillar.is_empty());
344        assert_eq!(pillar.first(), Some(&EntityReference::new(0, 0)));
345    }
346
347    #[test]
348    fn test_pillar_empty() {
349        let pillar = Pillar::new(vec![]);
350        assert!(pillar.is_empty());
351        assert_eq!(pillar.first(), None);
352    }
353
354    #[test]
355    fn test_default_pillar_selector_groups_by_value() {
356        // Create employees with shifts: [1, 1, 2, 2, 2, 3]
357        let employees = vec![
358            Employee {
359                id: 0,
360                shift: Some(1),
361            },
362            Employee {
363                id: 1,
364                shift: Some(1),
365            },
366            Employee {
367                id: 2,
368                shift: Some(2),
369            },
370            Employee {
371                id: 3,
372                shift: Some(2),
373            },
374            Employee {
375                id: 4,
376                shift: Some(2),
377            },
378            Employee {
379                id: 5,
380                shift: Some(3),
381            },
382        ];
383        let director = create_test_director(employees);
384
385        // Verify entity IDs
386        let solution = director.working_solution();
387        for (i, emp) in solution.employees.iter().enumerate() {
388            assert_eq!(emp.id, i);
389        }
390
391        let entity_selector = FromSolutionEntitySelector::new(0);
392        let selector = DefaultPillarSelector::<ScheduleSolution, i32, _, _>::new(
393            entity_selector,
394            0,
395            "shift",
396            |sd: &dyn ScoreDirector<ScheduleSolution>, _desc_idx, entity_idx| {
397                let solution = sd.working_solution();
398                solution.employees.get(entity_idx).and_then(|e| e.shift)
399            },
400        );
401
402        let pillars: Vec<_> = selector.iter(&director).collect();
403
404        // Should have 3 pillars (for shift values 1, 2, 3)
405        assert_eq!(pillars.len(), 3);
406
407        // Find pillar sizes
408        let mut sizes: Vec<_> = pillars.iter().map(|p| p.size()).collect();
409        sizes.sort();
410
411        // Should have pillars of size 1, 2, and 3
412        assert_eq!(sizes, vec![1, 2, 3]);
413    }
414
415    #[test]
416    fn test_pillar_selector_with_minimum_size() {
417        // Create employees with shifts: [1, 1, 2, 2, 2, 3]
418        let employees = vec![
419            Employee {
420                id: 0,
421                shift: Some(1),
422            },
423            Employee {
424                id: 1,
425                shift: Some(1),
426            },
427            Employee {
428                id: 2,
429                shift: Some(2),
430            },
431            Employee {
432                id: 3,
433                shift: Some(2),
434            },
435            Employee {
436                id: 4,
437                shift: Some(2),
438            },
439            Employee {
440                id: 5,
441                shift: Some(3),
442            },
443        ];
444        let director = create_test_director(employees);
445
446        // Verify entity IDs
447        let solution = director.working_solution();
448        for (i, emp) in solution.employees.iter().enumerate() {
449            assert_eq!(emp.id, i);
450        }
451
452        let entity_selector = FromSolutionEntitySelector::new(0);
453        let selector = DefaultPillarSelector::<ScheduleSolution, i32, _, _>::new(
454            entity_selector,
455            0,
456            "shift",
457            |sd: &dyn ScoreDirector<ScheduleSolution>, _desc_idx, entity_idx| {
458                let solution = sd.working_solution();
459                solution.employees.get(entity_idx).and_then(|e| e.shift)
460            },
461        )
462        .with_sub_pillar_config(SubPillarConfig::none().with_minimum_size(2));
463
464        let pillars: Vec<_> = selector.iter(&director).collect();
465
466        // Should only have 2 pillars (shift 1 has 2 entities, shift 2 has 3 entities)
467        // Shift 3 only has 1 entity, so it's filtered out
468        assert_eq!(pillars.len(), 2);
469    }
470
471    #[test]
472    fn test_pillar_selector_with_none_values() {
473        // Create employees with some unassigned
474        let employees = vec![
475            Employee {
476                id: 0,
477                shift: Some(1),
478            },
479            Employee { id: 1, shift: None },
480            Employee { id: 2, shift: None },
481            Employee {
482                id: 3,
483                shift: Some(1),
484            },
485        ];
486        let director = create_test_director(employees);
487
488        // Verify entity IDs
489        let solution = director.working_solution();
490        for (i, emp) in solution.employees.iter().enumerate() {
491            assert_eq!(emp.id, i);
492        }
493
494        let entity_selector = FromSolutionEntitySelector::new(0);
495        let selector = DefaultPillarSelector::<ScheduleSolution, i32, _, _>::new(
496            entity_selector,
497            0,
498            "shift",
499            |sd: &dyn ScoreDirector<ScheduleSolution>, _desc_idx, entity_idx| {
500                let solution = sd.working_solution();
501                solution.employees.get(entity_idx).and_then(|e| e.shift)
502            },
503        );
504
505        let pillars: Vec<_> = selector.iter(&director).collect();
506
507        // Should have 2 pillars: one for shift 1 (2 entities), one for None (2 entities)
508        assert_eq!(pillars.len(), 2);
509    }
510
511    #[test]
512    fn test_pillar_selector_empty_solution() {
513        let director = create_test_director(vec![]);
514
515        let entity_selector = FromSolutionEntitySelector::new(0);
516        let selector = DefaultPillarSelector::<ScheduleSolution, i32, _, _>::new(
517            entity_selector,
518            0,
519            "shift",
520            |sd: &dyn ScoreDirector<ScheduleSolution>, _desc_idx, entity_idx| {
521                let solution = sd.working_solution();
522                solution.employees.get(entity_idx).and_then(|e| e.shift)
523            },
524        );
525
526        let pillars: Vec<_> = selector.iter(&director).collect();
527        assert!(pillars.is_empty());
528        assert_eq!(selector.size(&director), 0);
529    }
530
531    #[test]
532    fn test_sub_pillar_config() {
533        let config = SubPillarConfig::none();
534        assert!(!config.enabled);
535        assert_eq!(config.minimum_size, 1);
536
537        let config = SubPillarConfig::all();
538        assert!(config.enabled);
539
540        let config = SubPillarConfig::none()
541            .with_minimum_size(2)
542            .with_maximum_size(5);
543        assert_eq!(config.minimum_size, 2);
544        assert_eq!(config.maximum_size, 5);
545    }
546}