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>>(
66        &'a self,
67        score_director: &'a D,
68    ) -> impl Iterator<Item = Pillar> + 'a;
69
70    fn size<D: Director<S>>(&self, score_director: &D) -> usize;
71
72    // Returns true if this selector may return the same pillar multiple times.
73    fn is_never_ending(&self) -> bool {
74        false
75    }
76
77    fn descriptor_index(&self) -> usize;
78}
79
80// Configuration for sub-pillar selection.
81#[derive(Debug, Clone)]
82pub struct SubPillarConfig {
83    // Whether sub-pillar selection is enabled.
84    pub enabled: bool,
85    // Minimum size of a sub-pillar (default: 1).
86    pub minimum_size: usize,
87    // Maximum size of a sub-pillar (default: usize::MAX).
88    pub maximum_size: usize,
89}
90
91impl Default for SubPillarConfig {
92    fn default() -> Self {
93        Self {
94            enabled: false,
95            minimum_size: 1,
96            maximum_size: usize::MAX,
97        }
98    }
99}
100
101impl SubPillarConfig {
102    pub fn none() -> Self {
103        Self::default()
104    }
105
106    pub fn all() -> Self {
107        Self {
108            enabled: true,
109            minimum_size: 1,
110            maximum_size: usize::MAX,
111        }
112    }
113
114    pub fn with_minimum_size(mut self, size: usize) -> Self {
115        self.minimum_size = size.max(1);
116        self
117    }
118
119    pub fn with_maximum_size(mut self, size: usize) -> Self {
120        self.maximum_size = size;
121        self
122    }
123}
124
125/// A pillar selector that groups entities by their variable value.
126///
127/// This selector uses an entity selector to get entities, then groups them
128/// by the value of a specified variable using a value extractor function.
129///
130/// # Zero-Erasure Design
131///
132/// Both the entity selector `ES` and the extractor function `E` are stored as
133/// concrete generic type parameters, eliminating all virtual dispatch overhead.
134///
135/// **Note**: The value extractor closure uses `&dyn Director<S>` intentionally.
136/// This is a scorer-agnostic boundary - the closure doesn't need the concrete `D` type.
137pub struct DefaultPillarSelector<S, V, ES, E>
138where
139    S: PlanningSolution,
140    V: Clone + Eq + Hash + Send + Sync + 'static,
141    ES: EntitySelector<S>,
142    E: Fn(&dyn Director<S>, usize, usize) -> Option<V> + Send + Sync,
143{
144    // The underlying entity selector (zero-erasure).
145    entity_selector: ES,
146    // The descriptor index.
147    descriptor_index: usize,
148    // The variable name for grouping.
149    variable_name: &'static str,
150    // Function to extract the value from an entity for grouping (zero-erasure).
151    value_extractor: E,
152    // Sub-pillar configuration.
153    sub_pillar_config: SubPillarConfig,
154    // Marker for solution and value types.
155    _phantom: PhantomData<(fn() -> S, fn() -> V)>,
156}
157
158impl<S, V, ES, E> Debug for DefaultPillarSelector<S, V, ES, E>
159where
160    S: PlanningSolution,
161    V: Clone + Eq + Hash + Send + Sync + 'static,
162    ES: EntitySelector<S> + Debug,
163    E: Fn(&dyn Director<S>, usize, usize) -> Option<V> + Send + Sync,
164{
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        f.debug_struct("DefaultPillarSelector")
167            .field("entity_selector", &self.entity_selector)
168            .field("descriptor_index", &self.descriptor_index)
169            .field("variable_name", &self.variable_name)
170            .field("sub_pillar_config", &self.sub_pillar_config)
171            .finish()
172    }
173}
174
175impl<S, V, ES, E> DefaultPillarSelector<S, V, ES, E>
176where
177    S: PlanningSolution,
178    V: Clone + Eq + Hash + Send + Sync + 'static,
179    ES: EntitySelector<S>,
180    E: Fn(&dyn Director<S>, usize, usize) -> Option<V> + Send + Sync,
181{
182    /// Creates a new default pillar selector.
183    ///
184    /// # Arguments
185    ///
186    /// * `entity_selector` - The entity selector to get entities from
187    /// * `descriptor_index` - The entity descriptor index
188    /// * `variable_name` - The variable name for grouping
189    /// * `value_extractor` - Function to extract the grouping value from an entity
190    pub fn new(
191        entity_selector: ES,
192        descriptor_index: usize,
193        variable_name: &'static str,
194        value_extractor: E,
195    ) -> Self {
196        Self {
197            entity_selector,
198            descriptor_index,
199            variable_name,
200            value_extractor,
201            sub_pillar_config: SubPillarConfig::default(),
202            _phantom: PhantomData,
203        }
204    }
205
206    pub fn with_sub_pillar_config(mut self, config: SubPillarConfig) -> Self {
207        self.sub_pillar_config = config;
208        self
209    }
210
211    pub fn variable_name(&self) -> &str {
212        self.variable_name
213    }
214
215    // Builds the pillar list from the current solution state.
216    fn build_pillars<D: Director<S>>(&self, score_director: &D) -> Vec<Pillar> {
217        // Group entities by their value
218        let mut value_to_entities: HashMap<Option<V>, Vec<EntityReference>> = HashMap::new();
219
220        for entity_ref in self.entity_selector.iter(score_director) {
221            // Use dyn Director for the extractor (intentional type-erasure boundary)
222            let value = (self.value_extractor)(
223                score_director,
224                entity_ref.descriptor_index,
225                entity_ref.entity_index,
226            );
227            value_to_entities.entry(value).or_default().push(entity_ref);
228        }
229
230        // Filter by minimum size and create pillars
231        let min_size = self.sub_pillar_config.minimum_size;
232        value_to_entities
233            .into_values()
234            .filter(|entities| entities.len() >= min_size)
235            .map(Pillar::new)
236            .collect()
237    }
238}
239
240impl<S, V, ES, E> PillarSelector<S> for DefaultPillarSelector<S, V, ES, E>
241where
242    S: PlanningSolution,
243    V: Clone + Eq + Hash + Send + Sync + 'static,
244    ES: EntitySelector<S>,
245    E: Fn(&dyn Director<S>, usize, usize) -> Option<V> + Send + Sync,
246{
247    fn iter<'a, D: Director<S>>(
248        &'a self,
249        score_director: &'a D,
250    ) -> impl Iterator<Item = Pillar> + 'a {
251        let pillars = self.build_pillars(score_director);
252        pillars.into_iter()
253    }
254
255    fn size<D: Director<S>>(&self, score_director: &D) -> usize {
256        self.build_pillars(score_director).len()
257    }
258
259    fn descriptor_index(&self) -> usize {
260        self.descriptor_index
261    }
262}
263
264#[cfg(test)]
265#[path = "pillar_tests.rs"]
266mod tests;