Skip to main content

solverforge_solver/heuristic/selector/
pillar.rs

1/* Pillar selector for selecting groups of entities with the same variable value.
2
3A pillar is a group of entities that share the same planning variable value.
4Pillar moves operate on entire pillars, changing or swapping all entities
5in the pillar atomically.
6*/
7
8use std::collections::HashMap;
9use std::fmt::Debug;
10use std::hash::Hash;
11use std::marker::PhantomData;
12
13use solverforge_core::domain::PlanningSolution;
14use solverforge_scoring::Director;
15
16use super::entity::{EntityReference, EntitySelector};
17
18/* A pillar is a group of entity references that share the same variable value.
19
20All entities in a pillar have the same value for a specific planning variable,
21which allows them to be moved together atomically.
22*/
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Pillar {
25    // The entity references in this pillar.
26    pub entities: Vec<EntityReference>,
27}
28
29impl Pillar {
30    pub fn new(entities: Vec<EntityReference>) -> Self {
31        Self { entities }
32    }
33
34    pub fn size(&self) -> usize {
35        self.entities.len()
36    }
37
38    pub fn is_empty(&self) -> bool {
39        self.entities.is_empty()
40    }
41
42    pub fn first(&self) -> Option<&EntityReference> {
43        self.entities.first()
44    }
45
46    /// Returns an iterator over the entity references.
47    pub fn iter(&self) -> impl Iterator<Item = &EntityReference> {
48        self.entities.iter()
49    }
50}
51
52/// Trait for selecting pillars of entities.
53///
54/// A pillar selector groups entities by their variable values and returns
55/// groups (pillars) that can be moved together.
56///
57/// # Type Parameters
58/// * `S` - The planning solution type
59pub trait PillarSelector<S: PlanningSolution>: Send + Debug {
60    /* Returns an iterator over pillars.
61
62    Each pillar contains entity references for entities that share
63    the same variable value.
64    */
65    fn iter<'a, D: Director<S>>(&'a self, score_director: &D) -> impl Iterator<Item = Pillar> + 'a;
66
67    fn size<D: Director<S>>(&self, score_director: &D) -> usize;
68
69    // Returns true if this selector may return the same pillar multiple times.
70    fn is_never_ending(&self) -> bool {
71        false
72    }
73
74    fn descriptor_index(&self) -> usize;
75}
76
77// Configuration for sub-pillar selection.
78#[derive(Debug, Clone)]
79pub struct SubPillarConfig {
80    // Whether sub-pillar selection is enabled.
81    pub enabled: bool,
82    // Minimum size of a sub-pillar (default: 1).
83    pub minimum_size: usize,
84    // Maximum size of a sub-pillar (default: usize::MAX).
85    pub maximum_size: usize,
86}
87
88impl Default for SubPillarConfig {
89    fn default() -> Self {
90        Self {
91            enabled: false,
92            minimum_size: 1,
93            maximum_size: usize::MAX,
94        }
95    }
96}
97
98impl SubPillarConfig {
99    pub fn none() -> Self {
100        Self::default()
101    }
102
103    pub fn all() -> Self {
104        Self {
105            enabled: true,
106            minimum_size: 1,
107            maximum_size: usize::MAX,
108        }
109    }
110
111    pub fn with_minimum_size(mut self, size: usize) -> Self {
112        self.minimum_size = size.max(1);
113        self
114    }
115
116    pub fn with_maximum_size(mut self, size: usize) -> Self {
117        self.maximum_size = size;
118        self
119    }
120}
121
122/// A pillar selector that groups entities by their variable value.
123///
124/// This selector uses an entity selector to get entities, then groups them
125/// by the value of a specified variable using a value extractor function.
126///
127/// # Zero-Erasure Design
128///
129/// Both the entity selector `ES` and the extractor function `E` are stored as
130/// concrete generic type parameters, eliminating all virtual dispatch overhead.
131///
132/// **Note**: The value extractor closure uses `&dyn Director<S>` intentionally.
133/// This is a scorer-agnostic boundary - the closure doesn't need the concrete `D` type.
134pub struct DefaultPillarSelector<S, V, ES, E>
135where
136    S: PlanningSolution,
137    V: Clone + Eq + Hash + Send + Sync + 'static,
138    ES: EntitySelector<S>,
139    E: Fn(&dyn Director<S>, usize, usize) -> Option<V> + Send + Sync,
140{
141    // The underlying entity selector (zero-erasure).
142    entity_selector: ES,
143    // The descriptor index.
144    descriptor_index: usize,
145    // The variable name for grouping.
146    variable_name: &'static str,
147    // Function to extract the value from an entity for grouping (zero-erasure).
148    value_extractor: E,
149    // Sub-pillar configuration.
150    sub_pillar_config: SubPillarConfig,
151    // Marker for solution and value types.
152    _phantom: PhantomData<(fn() -> S, fn() -> V)>,
153}
154
155impl<S, V, ES, E> Debug for DefaultPillarSelector<S, V, ES, E>
156where
157    S: PlanningSolution,
158    V: Clone + Eq + Hash + Send + Sync + 'static,
159    ES: EntitySelector<S> + Debug,
160    E: Fn(&dyn Director<S>, usize, usize) -> Option<V> + Send + Sync,
161{
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        f.debug_struct("DefaultPillarSelector")
164            .field("entity_selector", &self.entity_selector)
165            .field("descriptor_index", &self.descriptor_index)
166            .field("variable_name", &self.variable_name)
167            .field("sub_pillar_config", &self.sub_pillar_config)
168            .finish()
169    }
170}
171
172impl<S, V, ES, E> DefaultPillarSelector<S, V, ES, E>
173where
174    S: PlanningSolution,
175    V: Clone + Eq + Hash + Send + Sync + 'static,
176    ES: EntitySelector<S>,
177    E: Fn(&dyn Director<S>, usize, usize) -> Option<V> + Send + Sync,
178{
179    /// Creates a new default pillar selector.
180    ///
181    /// # Arguments
182    ///
183    /// * `entity_selector` - The entity selector to get entities from
184    /// * `descriptor_index` - The entity descriptor index
185    /// * `variable_name` - The variable name for grouping
186    /// * `value_extractor` - Function to extract the grouping value from an entity
187    pub fn new(
188        entity_selector: ES,
189        descriptor_index: usize,
190        variable_name: &'static str,
191        value_extractor: E,
192    ) -> Self {
193        Self {
194            entity_selector,
195            descriptor_index,
196            variable_name,
197            value_extractor,
198            sub_pillar_config: SubPillarConfig::default(),
199            _phantom: PhantomData,
200        }
201    }
202
203    pub fn with_sub_pillar_config(mut self, config: SubPillarConfig) -> Self {
204        self.sub_pillar_config = config;
205        self
206    }
207
208    pub fn variable_name(&self) -> &str {
209        self.variable_name
210    }
211
212    // Builds the pillar list from the current solution state.
213    fn build_pillars<D: Director<S>>(&self, score_director: &D) -> Vec<Pillar> {
214        // Group entities by their value
215        let mut value_to_entities: HashMap<Option<V>, Vec<EntityReference>> = HashMap::new();
216
217        for entity_ref in self.entity_selector.iter(score_director) {
218            // Use dyn Director for the extractor (intentional type-erasure boundary)
219            let value = (self.value_extractor)(
220                score_director,
221                entity_ref.descriptor_index,
222                entity_ref.entity_index,
223            );
224            value_to_entities.entry(value).or_default().push(entity_ref);
225        }
226
227        // Filter by minimum size and create pillars
228        let min_size = self.sub_pillar_config.minimum_size;
229        value_to_entities
230            .into_values()
231            .filter(|entities| entities.len() >= min_size)
232            .map(Pillar::new)
233            .collect()
234    }
235}
236
237impl<S, V, ES, E> PillarSelector<S> for DefaultPillarSelector<S, V, ES, E>
238where
239    S: PlanningSolution,
240    V: Clone + Eq + Hash + Send + Sync + 'static,
241    ES: EntitySelector<S>,
242    E: Fn(&dyn Director<S>, usize, usize) -> Option<V> + Send + Sync,
243{
244    fn iter<'a, D: Director<S>>(&'a self, score_director: &D) -> impl Iterator<Item = Pillar> + 'a {
245        let pillars = self.build_pillars(score_director);
246        pillars.into_iter()
247    }
248
249    fn size<D: Director<S>>(&self, score_director: &D) -> 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)]
259#[path = "pillar_tests.rs"]
260mod tests;