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.
136pub struct NearbyEntitySelector<S: PlanningSolution, M: DynDistanceMeter> {
137    /// The child selector providing all candidate entities.
138    child: Box<dyn EntitySelector<S>>,
139    /// The recorder providing the origin entity.
140    origin_recorder: MimicRecorder,
141    /// The distance meter for measuring nearness.
142    distance_meter: M,
143    /// Configuration for nearby selection.
144    config: NearbySelectionConfig,
145}
146
147impl<S: PlanningSolution, M: DynDistanceMeter> NearbyEntitySelector<S, M> {
148    /// Creates a new nearby entity selector.
149    pub fn new(
150        child: Box<dyn EntitySelector<S>>,
151        origin_recorder: MimicRecorder,
152        distance_meter: M,
153        config: NearbySelectionConfig,
154    ) -> Self {
155        Self {
156            child,
157            origin_recorder,
158            distance_meter,
159            config,
160        }
161    }
162}
163
164impl<S: PlanningSolution, M: DynDistanceMeter> Debug for NearbyEntitySelector<S, M> {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        f.debug_struct("NearbyEntitySelector")
167            .field("child", &self.child)
168            .field("origin_recorder_id", &self.origin_recorder.id())
169            .field("distance_meter", &self.distance_meter)
170            .field("config", &self.config)
171            .finish()
172    }
173}
174
175impl<S: PlanningSolution, M: DynDistanceMeter + 'static> EntitySelector<S>
176    for NearbyEntitySelector<S, M>
177{
178    fn iter<'a>(
179        &'a self,
180        score_director: &'a dyn ScoreDirector<S>,
181    ) -> Box<dyn Iterator<Item = EntityReference> + 'a> {
182        // Get the origin entity from the recorder
183        let origin = match self.origin_recorder.get_recorded_entity() {
184            Some(e) => e,
185            None => {
186                // No origin recorded yet, return empty
187                return Box::new(std::iter::empty());
188            }
189        };
190
191        // Collect all candidate entities with their distances
192        let mut candidates: Vec<(EntityReference, f64)> = self
193            .child
194            .iter(score_director)
195            .filter(|&dest| dest != origin) // Exclude the origin itself
196            .map(|dest| {
197                let dist = self
198                    .distance_meter
199                    .distance_between(score_director, origin, dest);
200                (dest, dist)
201            })
202            .filter(|(_, dist)| *dist >= self.config.min_distance)
203            .collect();
204
205        // Sort by distance (closest first)
206        candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
207
208        // Apply max size limit
209        if let Some(max_size) = self.config.max_nearby_size {
210            candidates.truncate(max_size);
211        }
212
213        Box::new(candidates.into_iter().map(|(entity, _)| entity))
214    }
215
216    fn size(&self, score_director: &dyn ScoreDirector<S>) -> usize {
217        // This is an estimate; the actual size depends on the origin
218        let child_size = self.child.size(score_director);
219        match self.config.max_nearby_size {
220            Some(max) => child_size.min(max),
221            None => child_size,
222        }
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use crate::heuristic::selector::entity::FromSolutionEntitySelector;
230    use crate::heuristic::selector::mimic::{MimicRecorder, MimicRecordingEntitySelector};
231    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
232    use solverforge_core::score::SimpleScore;
233    use solverforge_scoring::SimpleScoreDirector;
234    use std::any::TypeId;
235
236    #[allow(dead_code)]
237    #[derive(Clone, Debug)]
238    struct Location {
239        id: i64,
240        x: f64,
241        y: f64,
242        assigned_to: Option<i64>,
243    }
244
245    #[derive(Clone, Debug)]
246    struct RoutingSolution {
247        locations: Vec<Location>,
248        score: Option<SimpleScore>,
249    }
250
251    impl PlanningSolution for RoutingSolution {
252        type Score = SimpleScore;
253
254        fn score(&self) -> Option<Self::Score> {
255            self.score
256        }
257
258        fn set_score(&mut self, score: Option<Self::Score>) {
259            self.score = score;
260        }
261    }
262
263    fn get_locations(s: &RoutingSolution) -> &Vec<Location> {
264        &s.locations
265    }
266
267    fn get_locations_mut(s: &mut RoutingSolution) -> &mut Vec<Location> {
268        &mut s.locations
269    }
270
271    /// Distance meter that uses Euclidean distance.
272    #[derive(Debug)]
273    struct EuclideanDistanceMeter {
274        /// Cached locations for quick lookup.
275        locations: Vec<(f64, f64)>,
276    }
277
278    impl EuclideanDistanceMeter {
279        fn new(locations: &[Location]) -> Self {
280            Self {
281                locations: locations.iter().map(|l| (l.x, l.y)).collect(),
282            }
283        }
284    }
285
286    impl DynDistanceMeter for EuclideanDistanceMeter {
287        fn distance_between<S: PlanningSolution>(
288            &self,
289            _score_director: &dyn ScoreDirector<S>,
290            origin: EntityReference,
291            destination: EntityReference,
292        ) -> f64 {
293            let (ox, oy) = self.locations[origin.entity_index];
294            let (dx, dy) = self.locations[destination.entity_index];
295            let delta_x = ox - dx;
296            let delta_y = oy - dy;
297            (delta_x * delta_x + delta_y * delta_y).sqrt()
298        }
299    }
300
301    fn create_test_director(
302    ) -> SimpleScoreDirector<RoutingSolution, impl Fn(&RoutingSolution) -> SimpleScore> {
303        // Create a grid of locations: (0,0), (1,0), (2,0), (0,1), (1,1), (2,1)
304        let locations = vec![
305            Location {
306                id: 0,
307                x: 0.0,
308                y: 0.0,
309                assigned_to: None,
310            },
311            Location {
312                id: 1,
313                x: 1.0,
314                y: 0.0,
315                assigned_to: None,
316            },
317            Location {
318                id: 2,
319                x: 2.0,
320                y: 0.0,
321                assigned_to: None,
322            },
323            Location {
324                id: 3,
325                x: 0.0,
326                y: 1.0,
327                assigned_to: None,
328            },
329            Location {
330                id: 4,
331                x: 1.0,
332                y: 1.0,
333                assigned_to: None,
334            },
335            Location {
336                id: 5,
337                x: 2.0,
338                y: 1.0,
339                assigned_to: None,
340            },
341        ];
342
343        let solution = RoutingSolution {
344            locations,
345            score: None,
346        };
347
348        let extractor = Box::new(TypedEntityExtractor::new(
349            "Location",
350            "locations",
351            get_locations,
352            get_locations_mut,
353        ));
354        let entity_desc = EntityDescriptor::new("Location", TypeId::of::<Location>(), "locations")
355            .with_extractor(extractor);
356
357        let descriptor =
358            SolutionDescriptor::new("RoutingSolution", TypeId::of::<RoutingSolution>())
359                .with_entity(entity_desc);
360
361        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
362    }
363
364    #[test]
365    fn test_nearby_selector_sorts_by_distance() {
366        let director = create_test_director();
367
368        // Verify entity IDs match indices
369        let solution = director.working_solution();
370        for (i, loc) in solution.locations.iter().enumerate() {
371            assert_eq!(loc.id, i as i64);
372        }
373
374        // Create mimic recorder and recording selector for origin
375        let recorder = MimicRecorder::new("origin");
376        let origin_child = Box::new(FromSolutionEntitySelector::new(0));
377        let origin_selector = MimicRecordingEntitySelector::new(origin_child, recorder.clone());
378
379        // Create nearby selector for destinations
380        let dest_child = Box::new(FromSolutionEntitySelector::new(0));
381        let distance_meter = EuclideanDistanceMeter::new(&director.working_solution().locations);
382        let nearby_config = NearbySelectionConfig::default();
383        let nearby_selector =
384            NearbyEntitySelector::new(dest_child, recorder.clone(), distance_meter, nearby_config);
385
386        // Select origin entity (location 0 at 0,0)
387        let mut origin_iter = origin_selector.iter(&director);
388        let _origin = origin_iter.next().unwrap();
389
390        // Get nearby entities (should be sorted by distance from 0,0)
391        let nearby: Vec<_> = nearby_selector.iter(&director).collect();
392
393        // Expected order: 1 (dist 1), 3 (dist 1), 2 (dist 2), 4 (dist √2 ≈ 1.41), 5 (dist √5 ≈ 2.24)
394        // 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
395        assert_eq!(nearby.len(), 5); // 6 locations - 1 (origin) = 5
396
397        // First two should be at distance 1 (locations 1 and 3)
398        assert!(
399            nearby[0].entity_index == 1 || nearby[0].entity_index == 3,
400            "Expected location 1 or 3, got {}",
401            nearby[0].entity_index
402        );
403    }
404
405    #[test]
406    fn test_nearby_selector_with_max_size() {
407        let director = create_test_director();
408
409        let recorder = MimicRecorder::new("origin");
410        let origin_child = Box::new(FromSolutionEntitySelector::new(0));
411        let origin_selector = MimicRecordingEntitySelector::new(origin_child, recorder.clone());
412
413        let dest_child = Box::new(FromSolutionEntitySelector::new(0));
414        let distance_meter = EuclideanDistanceMeter::new(&director.working_solution().locations);
415        let nearby_config = NearbySelectionConfig::default().with_max_nearby_size(2);
416        let nearby_selector =
417            NearbyEntitySelector::new(dest_child, recorder.clone(), distance_meter, nearby_config);
418
419        // Select origin
420        let mut origin_iter = origin_selector.iter(&director);
421        origin_iter.next();
422
423        // Should only get 2 nearest
424        let nearby: Vec<_> = nearby_selector.iter(&director).collect();
425        assert_eq!(nearby.len(), 2);
426    }
427
428    #[test]
429    fn test_nearby_selector_excludes_origin() {
430        let director = create_test_director();
431
432        let recorder = MimicRecorder::new("origin");
433        let origin_child = Box::new(FromSolutionEntitySelector::new(0));
434        let origin_selector = MimicRecordingEntitySelector::new(origin_child, recorder.clone());
435
436        let dest_child = Box::new(FromSolutionEntitySelector::new(0));
437        let distance_meter = EuclideanDistanceMeter::new(&director.working_solution().locations);
438        let nearby_config = NearbySelectionConfig::default();
439        let nearby_selector =
440            NearbyEntitySelector::new(dest_child, recorder.clone(), distance_meter, nearby_config);
441
442        // Select origin (entity 0)
443        let mut origin_iter = origin_selector.iter(&director);
444        let origin = origin_iter.next().unwrap();
445
446        // Nearby should not include the origin
447        let nearby: Vec<_> = nearby_selector.iter(&director).collect();
448        assert!(!nearby.contains(&origin));
449    }
450}