Skip to main content

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    ) -> impl Iterator<Item = EntityReference> + 'a {
193        // Get the origin entity from the recorder
194        let origin = self.origin_recorder.get_recorded_entity();
195
196        // Collect all candidate entities with their distances (empty if no origin)
197        let mut candidates: Vec<(EntityReference, f64)> = match origin {
198            Some(origin) => self
199                .child
200                .iter(score_director)
201                .filter(move |&dest| dest != origin) // Exclude the origin itself
202                .map(move |dest| {
203                    let dist = self
204                        .distance_meter
205                        .distance_between(score_director, origin, dest);
206                    (dest, dist)
207                })
208                .filter(|(_, dist)| *dist >= self.config.min_distance)
209                .collect(),
210            None => Vec::new(),
211        };
212
213        // Sort by distance (closest first)
214        candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
215
216        // Apply max size limit
217        if let Some(max_size) = self.config.max_nearby_size {
218            candidates.truncate(max_size);
219        }
220
221        candidates.into_iter().map(|(entity, _)| entity)
222    }
223
224    fn size<D: ScoreDirector<S>>(&self, score_director: &D) -> usize {
225        // This is an estimate; the actual size depends on the origin
226        let child_size = self.child.size(score_director);
227        match self.config.max_nearby_size {
228            Some(max) => child_size.min(max),
229            None => child_size,
230        }
231    }
232}