Skip to main content

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}