solverforge_solver/heuristic/selector/
nearby.rs

1//! Nearby selection for distance-based filtering of candidates.
2//!
3//! Nearby selection improves move quality by preferring destinations that are
4//! geographically or otherwise "close" to an origin. This is critical for
5//! vehicle routing problems (VRP) where swapping with nearby customers
6//! is more likely to improve the solution than swapping with distant ones.
7//!
8//! # Architecture
9//!
10//! - [`NearbyDistanceMeter`]: User-defined function to measure distance between elements
11//! - [`NearbySelectionConfig`]: Configuration for nearby selection behavior
12//! - [`NearbyEntitySelector`]: Selects entities nearby to a reference entity
13
14use std::cmp::Ordering;
15use std::fmt::Debug;
16
17use solverforge_core::domain::PlanningSolution;
18use solverforge_scoring::ScoreDirector;
19
20use super::entity::{EntityReference, EntitySelector};
21use super::mimic::MimicRecorder;
22
23/// Trait for measuring distance between an origin and a destination.
24///
25/// Implementations should be stateless. The solver may reuse instances.
26///
27/// # Type Parameters
28///
29/// - `Origin`: The type of the origin element (usually an entity or value)
30/// - `Destination`: The type of the destination element (usually an entity or value)
31///
32/// # Example
33///
34/// ```
35/// use solverforge_solver::heuristic::selector::NearbyDistanceMeter;
36///
37/// #[derive(Debug)]
38/// struct Location { x: f64, y: f64 }
39///
40/// #[derive(Debug)]
41/// struct EuclideanMeter;
42///
43/// impl NearbyDistanceMeter<Location, Location> for EuclideanMeter {
44///     fn distance(&self, origin: &Location, dest: &Location) -> f64 {
45///         let dx = origin.x - dest.x;
46///         let dy = origin.y - dest.y;
47///         (dx * dx + dy * dy).sqrt()
48///     }
49/// }
50/// ```
51pub trait NearbyDistanceMeter<Origin, Destination>: Send + Sync + Debug {
52    /// Measures the distance from the origin to the destination.
53    ///
54    /// The distance can be in any unit (meters, seconds, cost, etc.).
55    /// Distances can be asymmetrical: the distance from A to B may differ
56    /// from the distance from B to A.
57    ///
58    /// Returns a value >= 0.0. If origin == destination, typically returns 0.0.
59    fn distance(&self, origin: &Origin, destination: &Destination) -> f64;
60}
61
62/// Distribution type for nearby selection.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
64pub enum NearbyDistributionType {
65    /// Select all candidates sorted by distance (up to a maximum).
66    #[default]
67    Linear,
68    /// Select candidates with probability proportional to distance (closer = more likely).
69    Parabolic,
70    /// Use a block distribution for k-opt style moves.
71    Block,
72}
73
74/// Configuration for nearby selection.
75#[derive(Debug, Clone)]
76pub struct NearbySelectionConfig {
77    /// The distribution type to use.
78    pub distribution_type: NearbyDistributionType,
79    /// Maximum number of nearby candidates to consider.
80    /// If None, considers all candidates (sorted by distance).
81    pub max_nearby_size: Option<usize>,
82    /// Minimum distance to include a candidate (exclusive of origin).
83    pub min_distance: f64,
84}
85
86impl Default for NearbySelectionConfig {
87    fn default() -> Self {
88        Self {
89            distribution_type: NearbyDistributionType::Linear,
90            max_nearby_size: None,
91            min_distance: 0.0,
92        }
93    }
94}
95
96impl NearbySelectionConfig {
97    /// Creates a new configuration with default values.
98    pub fn new() -> Self {
99        Self::default()
100    }
101
102    /// Sets the distribution type.
103    pub fn with_distribution_type(mut self, distribution_type: NearbyDistributionType) -> Self {
104        self.distribution_type = distribution_type;
105        self
106    }
107
108    /// Sets the maximum number of nearby candidates.
109    pub fn with_max_nearby_size(mut self, max_size: usize) -> Self {
110        self.max_nearby_size = Some(max_size);
111        self
112    }
113
114    /// Sets the minimum distance threshold.
115    pub fn with_min_distance(mut self, min_distance: f64) -> Self {
116        self.min_distance = min_distance;
117        self
118    }
119}
120
121/// Type-erased distance meter for dynamic dispatch.
122pub trait DynDistanceMeter: Send + Sync + Debug {
123    /// Measures distance between two entity references.
124    fn distance_between<S: PlanningSolution>(
125        &self,
126        score_director: &dyn ScoreDirector<S>,
127        origin: EntityReference,
128        destination: EntityReference,
129    ) -> f64;
130}
131
132/// An entity selector that returns entities nearby to an origin entity.
133///
134/// The origin entity is obtained from a mimic recorder, allowing this selector
135/// to be synchronized with another selector that picks the "current" entity.
136///
137/// # Zero-Erasure Design
138///
139/// The child entity selector `ES` is stored as a concrete generic type parameter,
140/// eliminating virtual dispatch overhead when iterating over candidate entities.
141pub struct NearbyEntitySelector<S, M, ES> {
142    /// The child selector providing all candidate entities (zero-erasure).
143    child: ES,
144    /// The recorder providing the origin entity.
145    origin_recorder: MimicRecorder,
146    /// The distance meter for measuring nearness.
147    distance_meter: M,
148    /// Configuration for nearby selection.
149    config: NearbySelectionConfig,
150    /// Marker for solution type.
151    _phantom: std::marker::PhantomData<fn() -> S>,
152}
153
154impl<S, M, ES> NearbyEntitySelector<S, M, ES> {
155    /// Creates a new nearby entity selector.
156    pub fn new(
157        child: ES,
158        origin_recorder: MimicRecorder,
159        distance_meter: M,
160        config: NearbySelectionConfig,
161    ) -> Self {
162        Self {
163            child,
164            origin_recorder,
165            distance_meter,
166            config,
167            _phantom: std::marker::PhantomData,
168        }
169    }
170}
171
172impl<S: PlanningSolution, M: DynDistanceMeter, ES: Debug> Debug for NearbyEntitySelector<S, M, ES> {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        f.debug_struct("NearbyEntitySelector")
175            .field("child", &self.child)
176            .field("origin_recorder_id", &self.origin_recorder.id())
177            .field("distance_meter", &self.distance_meter)
178            .field("config", &self.config)
179            .finish()
180    }
181}
182
183impl<S, M, ES> EntitySelector<S> for NearbyEntitySelector<S, M, ES>
184where
185    S: PlanningSolution,
186    M: DynDistanceMeter + 'static,
187    ES: EntitySelector<S>,
188{
189    fn iter<'a, D: ScoreDirector<S>>(
190        &'a self,
191        score_director: &'a D,
192    ) -> Box<dyn Iterator<Item = EntityReference> + 'a> {
193        // Get the origin entity from the recorder
194        let origin = match self.origin_recorder.get_recorded_entity() {
195            Some(e) => e,
196            None => {
197                // No origin recorded yet, return empty
198                return Box::new(std::iter::empty());
199            }
200        };
201
202        // Collect all candidate entities with their distances
203        let mut candidates: Vec<(EntityReference, f64)> = self
204            .child
205            .iter(score_director)
206            .filter(|&dest| dest != origin) // Exclude the origin itself
207            .map(|dest| {
208                let dist = self
209                    .distance_meter
210                    .distance_between(score_director, origin, dest);
211                (dest, dist)
212            })
213            .filter(|(_, dist)| *dist >= self.config.min_distance)
214            .collect();
215
216        // Sort by distance (closest first)
217        candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
218
219        // Apply max size limit
220        if let Some(max_size) = self.config.max_nearby_size {
221            candidates.truncate(max_size);
222        }
223
224        Box::new(candidates.into_iter().map(|(entity, _)| entity))
225    }
226
227    fn size<D: ScoreDirector<S>>(&self, score_director: &D) -> usize {
228        // This is an estimate; the actual size depends on the origin
229        let child_size = self.child.size(score_director);
230        match self.config.max_nearby_size {
231            Some(max) => child_size.min(max),
232            None => child_size,
233        }
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use crate::heuristic::selector::entity::FromSolutionEntitySelector;
241    use crate::heuristic::selector::mimic::{MimicRecorder, MimicRecordingEntitySelector};
242    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
243    use solverforge_core::score::SimpleScore;
244    use solverforge_scoring::SimpleScoreDirector;
245    use std::any::TypeId;
246
247    #[derive(Clone, Debug)]
248    struct Location {
249        id: i64,
250        x: f64,
251        y: f64,
252    }
253
254    #[derive(Clone, Debug)]
255    struct RoutingSolution {
256        locations: Vec<Location>,
257        score: Option<SimpleScore>,
258    }
259
260    impl PlanningSolution for RoutingSolution {
261        type Score = SimpleScore;
262
263        fn score(&self) -> Option<Self::Score> {
264            self.score
265        }
266
267        fn set_score(&mut self, score: Option<Self::Score>) {
268            self.score = score;
269        }
270    }
271
272    fn get_locations(s: &RoutingSolution) -> &Vec<Location> {
273        &s.locations
274    }
275
276    fn get_locations_mut(s: &mut RoutingSolution) -> &mut Vec<Location> {
277        &mut s.locations
278    }
279
280    /// Distance meter that uses Euclidean distance.
281    #[derive(Debug)]
282    struct EuclideanDistanceMeter {
283        /// Cached locations for quick lookup.
284        locations: Vec<(f64, f64)>,
285    }
286
287    impl EuclideanDistanceMeter {
288        fn new(locations: &[Location]) -> Self {
289            Self {
290                locations: locations.iter().map(|l| (l.x, l.y)).collect(),
291            }
292        }
293    }
294
295    impl DynDistanceMeter for EuclideanDistanceMeter {
296        fn distance_between<S: PlanningSolution>(
297            &self,
298            _score_director: &dyn ScoreDirector<S>,
299            origin: EntityReference,
300            destination: EntityReference,
301        ) -> f64 {
302            let (ox, oy) = self.locations[origin.entity_index];
303            let (dx, dy) = self.locations[destination.entity_index];
304            let delta_x = ox - dx;
305            let delta_y = oy - dy;
306            (delta_x * delta_x + delta_y * delta_y).sqrt()
307        }
308    }
309
310    fn create_test_director(
311    ) -> SimpleScoreDirector<RoutingSolution, impl Fn(&RoutingSolution) -> SimpleScore> {
312        // Create a grid of locations: (0,0), (1,0), (2,0), (0,1), (1,1), (2,1)
313        let locations = vec![
314            Location {
315                id: 0,
316                x: 0.0,
317                y: 0.0,
318            },
319            Location {
320                id: 1,
321                x: 1.0,
322                y: 0.0,
323            },
324            Location {
325                id: 2,
326                x: 2.0,
327                y: 0.0,
328            },
329            Location {
330                id: 3,
331                x: 0.0,
332                y: 1.0,
333            },
334            Location {
335                id: 4,
336                x: 1.0,
337                y: 1.0,
338            },
339            Location {
340                id: 5,
341                x: 2.0,
342                y: 1.0,
343            },
344        ];
345
346        let solution = RoutingSolution {
347            locations,
348            score: None,
349        };
350
351        let extractor = Box::new(TypedEntityExtractor::new(
352            "Location",
353            "locations",
354            get_locations,
355            get_locations_mut,
356        ));
357        let entity_desc = EntityDescriptor::new("Location", TypeId::of::<Location>(), "locations")
358            .with_extractor(extractor);
359
360        let descriptor =
361            SolutionDescriptor::new("RoutingSolution", TypeId::of::<RoutingSolution>())
362                .with_entity(entity_desc);
363
364        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
365    }
366
367    #[test]
368    fn test_nearby_selector_sorts_by_distance() {
369        let director = create_test_director();
370
371        // Verify entity IDs match indices
372        let solution = director.working_solution();
373        for (i, loc) in solution.locations.iter().enumerate() {
374            assert_eq!(loc.id, i as i64);
375        }
376
377        // Create mimic recorder and recording selector for origin
378        let recorder = MimicRecorder::new("origin");
379        let origin_child = FromSolutionEntitySelector::new(0);
380        let origin_selector = MimicRecordingEntitySelector::new(origin_child, recorder.clone());
381
382        // Create nearby selector for destinations
383        let dest_child = FromSolutionEntitySelector::new(0);
384        let distance_meter = EuclideanDistanceMeter::new(&director.working_solution().locations);
385        let nearby_config = NearbySelectionConfig::default();
386        let nearby_selector =
387            NearbyEntitySelector::new(dest_child, recorder.clone(), distance_meter, nearby_config);
388
389        // Select origin entity (location 0 at 0,0)
390        let mut origin_iter = origin_selector.iter(&director);
391        let _origin = origin_iter.next().unwrap();
392
393        // Get nearby entities (should be sorted by distance from 0,0)
394        let nearby: Vec<_> = nearby_selector.iter(&director).collect();
395
396        // Expected order: 1 (dist 1), 3 (dist 1), 2 (dist 2), 4 (dist √2 ≈ 1.41), 5 (dist √5 ≈ 2.24)
397        // Actually: 1 at (1,0) dist 1, 3 at (0,1) dist 1, 4 at (1,1) dist √2, 2 at (2,0) dist 2, 5 at (2,1) dist √5
398        assert_eq!(nearby.len(), 5); // 6 locations - 1 (origin) = 5
399
400        // First two should be at distance 1 (locations 1 and 3)
401        assert!(
402            nearby[0].entity_index == 1 || nearby[0].entity_index == 3,
403            "Expected location 1 or 3, got {}",
404            nearby[0].entity_index
405        );
406    }
407
408    #[test]
409    fn test_nearby_selector_with_max_size() {
410        let director = create_test_director();
411
412        let recorder = MimicRecorder::new("origin");
413        let origin_child = FromSolutionEntitySelector::new(0);
414        let origin_selector = MimicRecordingEntitySelector::new(origin_child, recorder.clone());
415
416        let dest_child = FromSolutionEntitySelector::new(0);
417        let distance_meter = EuclideanDistanceMeter::new(&director.working_solution().locations);
418        let nearby_config = NearbySelectionConfig::default().with_max_nearby_size(2);
419        let nearby_selector =
420            NearbyEntitySelector::new(dest_child, recorder.clone(), distance_meter, nearby_config);
421
422        // Select origin
423        let mut origin_iter = origin_selector.iter(&director);
424        origin_iter.next();
425
426        // Should only get 2 nearest
427        let nearby: Vec<_> = nearby_selector.iter(&director).collect();
428        assert_eq!(nearby.len(), 2);
429    }
430
431    #[test]
432    fn test_nearby_selector_excludes_origin() {
433        let director = create_test_director();
434
435        let recorder = MimicRecorder::new("origin");
436        let origin_child = FromSolutionEntitySelector::new(0);
437        let origin_selector = MimicRecordingEntitySelector::new(origin_child, recorder.clone());
438
439        let dest_child = FromSolutionEntitySelector::new(0);
440        let distance_meter = EuclideanDistanceMeter::new(&director.working_solution().locations);
441        let nearby_config = NearbySelectionConfig::default();
442        let nearby_selector =
443            NearbyEntitySelector::new(dest_child, recorder.clone(), distance_meter, nearby_config);
444
445        // Select origin (entity 0)
446        let mut origin_iter = origin_selector.iter(&director);
447        let origin = origin_iter.next().unwrap();
448
449        // Nearby should not include the origin
450        let nearby: Vec<_> = nearby_selector.iter(&director).collect();
451        assert!(!nearby.contains(&origin));
452    }
453}