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