solverforge_solver/heuristic/selector/
nearby.rs1use 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
23pub trait NearbyDistanceMeter<Origin, Destination>: Send + Sync + Debug {
52 fn distance(&self, origin: &Origin, destination: &Destination) -> f64;
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
64pub enum NearbyDistributionType {
65 #[default]
67 Linear,
68 Parabolic,
70 Block,
72}
73
74#[derive(Debug, Clone)]
76pub struct NearbySelectionConfig {
77 pub distribution_type: NearbyDistributionType,
79 pub max_nearby_size: Option<usize>,
82 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 pub fn new() -> Self {
99 Self::default()
100 }
101
102 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 {
110 self.max_nearby_size = Some(max_size);
111 self
112 }
113
114 pub fn with_min_distance(mut self, min_distance: f64) -> Self {
116 self.min_distance = min_distance;
117 self
118 }
119}
120
121pub trait DynDistanceMeter: Send + Sync + Debug {
123 fn distance_between<S: PlanningSolution>(
125 &self,
126 score_director: &dyn ScoreDirector<S>,
127 origin: EntityReference,
128 destination: EntityReference,
129 ) -> f64;
130}
131
132pub struct NearbyEntitySelector<S: PlanningSolution, M: DynDistanceMeter> {
137 child: Box<dyn EntitySelector<S>>,
139 origin_recorder: MimicRecorder,
141 distance_meter: M,
143 config: NearbySelectionConfig,
145}
146
147impl<S: PlanningSolution, M: DynDistanceMeter> NearbyEntitySelector<S, M> {
148 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 let origin = match self.origin_recorder.get_recorded_entity() {
184 Some(e) => e,
185 None => {
186 return Box::new(std::iter::empty());
188 }
189 };
190
191 let mut candidates: Vec<(EntityReference, f64)> = self
193 .child
194 .iter(score_director)
195 .filter(|&dest| dest != origin) .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 candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
207
208 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 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 #[derive(Debug)]
273 struct EuclideanDistanceMeter {
274 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 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 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 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 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 let mut origin_iter = origin_selector.iter(&director);
388 let _origin = origin_iter.next().unwrap();
389
390 let nearby: Vec<_> = nearby_selector.iter(&director).collect();
392
393 assert_eq!(nearby.len(), 5); 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 let mut origin_iter = origin_selector.iter(&director);
421 origin_iter.next();
422
423 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 let mut origin_iter = origin_selector.iter(&director);
444 let origin = origin_iter.next().unwrap();
445
446 let nearby: Vec<_> = nearby_selector.iter(&director).collect();
448 assert!(!nearby.contains(&origin));
449 }
450}