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}