solverforge_solver/heuristic/selector/nearby.rs
1/* Nearby selection for distance-based filtering of candidates.
2
3Nearby selection improves move quality by preferring destinations that are
4geographically or otherwise "close" to an origin. This is critical for
5vehicle routing problems (VRP) where swapping with nearby customers
6is 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*/
14
15use std::cmp::Ordering;
16use std::fmt::Debug;
17
18use solverforge_core::domain::PlanningSolution;
19use solverforge_scoring::Director;
20
21use super::entity::{EntityReference, EntitySelector};
22use super::mimic::MimicRecorder;
23
24/// Trait for measuring distance between an origin and a destination.
25///
26/// Implementations should be stateless. The solver may reuse instances.
27///
28/// # Type Parameters
29///
30/// - `Origin`: The type of the origin element (usually an entity or value)
31/// - `Destination`: The type of the destination element (usually an entity or value)
32///
33/// # Example
34///
35/// ```
36/// use solverforge_solver::heuristic::selector::NearbyDistanceMeter;
37///
38/// #[derive(Debug)]
39/// struct Location { x: f64, y: f64 }
40///
41/// #[derive(Debug)]
42/// struct EuclideanMeter;
43///
44/// impl NearbyDistanceMeter<Location, Location> for EuclideanMeter {
45/// fn distance(&self, origin: &Location, dest: &Location) -> f64 {
46/// let dx = origin.x - dest.x;
47/// let dy = origin.y - dest.y;
48/// (dx * dx + dy * dy).sqrt()
49/// }
50/// }
51/// ```
52pub trait NearbyDistanceMeter<Origin, Destination>: Send + Sync + Debug {
53 /* Measures the distance from the origin to the destination.
54
55 The distance can be in any unit (meters, seconds, cost, etc.).
56 Distances can be asymmetrical: the distance from A to B may differ
57 from the distance from B to A.
58
59 Returns a value >= 0.0. If origin == destination, typically returns 0.0.
60 */
61 fn distance(&self, origin: &Origin, destination: &Destination) -> f64;
62}
63
64// Distribution type for nearby selection.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
66pub enum NearbyDistributionType {
67 // Select all candidates sorted by distance (up to a maximum).
68 #[default]
69 Linear,
70 // Select candidates with probability proportional to distance (closer = more likely).
71 Parabolic,
72 // Use a block distribution for k-opt style moves.
73 Block,
74}
75
76// Configuration for nearby selection.
77#[derive(Debug, Clone)]
78pub struct NearbySelectionConfig {
79 // The distribution type to use.
80 pub distribution_type: NearbyDistributionType,
81 // Maximum number of nearby candidates to consider.
82 // If None, considers all candidates (sorted by distance).
83 pub max_nearby_size: Option<usize>,
84 // Minimum distance to include a candidate (exclusive of origin).
85 pub min_distance: f64,
86}
87
88impl Default for NearbySelectionConfig {
89 fn default() -> Self {
90 Self {
91 distribution_type: NearbyDistributionType::Linear,
92 max_nearby_size: None,
93 min_distance: 0.0,
94 }
95 }
96}
97
98impl NearbySelectionConfig {
99 pub fn new() -> Self {
100 Self::default()
101 }
102
103 pub fn with_distribution_type(mut self, distribution_type: NearbyDistributionType) -> Self {
104 self.distribution_type = distribution_type;
105 self
106 }
107
108 pub fn with_max_nearby_size(mut self, max_size: usize) -> Self {
109 self.max_nearby_size = Some(max_size);
110 self
111 }
112
113 pub fn with_min_distance(mut self, min_distance: f64) -> Self {
114 self.min_distance = min_distance;
115 self
116 }
117}
118
119/// Type-erased distance meter for dynamic dispatch.
120pub trait DynDistanceMeter: Send + Sync + Debug {
121 // Measures distance between two entity references.
122 fn distance_between<S: PlanningSolution>(
123 &self,
124 score_director: &dyn Director<S>,
125 origin: EntityReference,
126 destination: EntityReference,
127 ) -> f64;
128}
129
130/// An entity selector that returns entities nearby to an origin entity.
131///
132/// The origin entity is obtained from a mimic recorder, allowing this selector
133/// to be synchronized with another selector that picks the "current" entity.
134///
135/// # Zero-Erasure Design
136///
137/// The child entity selector `ES` is stored as a concrete generic type parameter,
138/// eliminating virtual dispatch overhead when iterating over candidate entities.
139pub struct NearbyEntitySelector<S, M, ES> {
140 // The child selector providing all candidate entities (zero-erasure).
141 child: ES,
142 // The recorder providing the origin entity.
143 origin_recorder: MimicRecorder,
144 // The distance meter for measuring nearness.
145 distance_meter: M,
146 // Configuration for nearby selection.
147 config: NearbySelectionConfig,
148 // Marker for solution type.
149 _phantom: std::marker::PhantomData<fn() -> S>,
150}
151
152impl<S, M, ES> NearbyEntitySelector<S, M, ES> {
153 pub fn new(
154 child: ES,
155 origin_recorder: MimicRecorder,
156 distance_meter: M,
157 config: NearbySelectionConfig,
158 ) -> Self {
159 Self {
160 child,
161 origin_recorder,
162 distance_meter,
163 config,
164 _phantom: std::marker::PhantomData,
165 }
166 }
167}
168
169impl<S: PlanningSolution, M: DynDistanceMeter, ES: Debug> Debug for NearbyEntitySelector<S, M, ES> {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 f.debug_struct("NearbyEntitySelector")
172 .field("child", &self.child)
173 .field("origin_recorder_id", &self.origin_recorder.id())
174 .field("distance_meter", &self.distance_meter)
175 .field("config", &self.config)
176 .finish()
177 }
178}
179
180impl<S, M, ES> EntitySelector<S> for NearbyEntitySelector<S, M, ES>
181where
182 S: PlanningSolution,
183 M: DynDistanceMeter + 'static,
184 ES: EntitySelector<S>,
185{
186 fn iter<'a, D: Director<S>>(
187 &'a self,
188 score_director: &'a D,
189 ) -> impl Iterator<Item = EntityReference> + 'a {
190 // Get the origin entity from the recorder
191 let origin = self.origin_recorder.get_recorded_entity();
192
193 // Collect all candidate entities with their distances (empty if no origin)
194 let mut candidates: Vec<(EntityReference, f64)> = match origin {
195 Some(origin) => self
196 .child
197 .iter(score_director)
198 .filter(move |&dest| dest != origin) // Exclude the origin itself
199 .map(move |dest| {
200 let dist = self
201 .distance_meter
202 .distance_between(score_director, origin, dest);
203 (dest, dist)
204 })
205 .filter(|(_, dist)| *dist >= self.config.min_distance)
206 .collect(),
207 None => Vec::new(),
208 };
209
210 // Sort by distance (closest first)
211 candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
212
213 // Apply max size limit
214 if let Some(max_size) = self.config.max_nearby_size {
215 candidates.truncate(max_size);
216 }
217
218 candidates.into_iter().map(|(entity, _)| entity)
219 }
220
221 fn size<D: Director<S>>(&self, score_director: &D) -> usize {
222 // This is an estimate; the actual size depends on the origin
223 let child_size = self.child.size(score_director);
224 match self.config.max_nearby_size {
225 Some(max) => child_size.min(max),
226 None => child_size,
227 }
228 }
229}