1#[cfg(test)]
2#[path = "../../../../tests/unit/construction/clustering/vicinity/estimations_test.rs"]
3mod estimations_test;
4
5use super::*;
6use crate::models::common::*;
7use crate::models::problem::{Place, Single, TransportCost};
8use crate::models::solution::CommuteInfo;
9use crate::models::GoalContext;
10use rosomaxa::utils::parallel_foreach_mut;
11use std::collections::{HashMap, HashSet};
12
13type PlaceInfo = (PlaceIndex, Location, Duration, Vec<TimeWindow>);
14type PlaceIndex = usize;
15type Reachable = bool;
16type DissimilarityInfo = (Reachable, PlaceIndex, ClusterInfo);
17type DissimilarityIndex = HashMap<Job, Vec<DissimilarityInfo>>;
18
19pub(crate) fn get_clusters(
21 variant: &GoalContext,
22 estimates: HashMap<Job, DissimilarityIndex>,
23 config: &ClusterConfig,
24 check_insertion: &CheckInsertionFn,
25) -> Vec<(Job, Vec<Job>)> {
26 let mut used_jobs = HashSet::new();
27 let mut clusters = Vec::new();
28 let mut cluster_estimates = estimates
29 .iter()
30 .map(|(job, estimate)| {
31 let candidates = estimate
32 .iter()
33 .filter_map(|(job, infos)| {
34 if infos.iter().any(|(reachable, ..)| *reachable) {
36 Some(job.clone())
37 } else {
38 None
39 }
40 })
41 .collect::<HashSet<_>>();
42
43 (job.clone(), (None, candidates))
44 })
45 .collect::<Vec<(_, (Option<Job>, HashSet<_>))>>();
46
47 loop {
48 parallel_foreach_mut(cluster_estimates.as_mut_slice(), |(center_job, (cluster, _))| {
49 if cluster.is_none() {
50 *cluster = build_job_cluster(variant, center_job, &estimates, &used_jobs, config, check_insertion)
51 }
52 });
53
54 cluster_estimates.sort_unstable_by(|(a_job, (_, a_can)), (b_job, (_, b_can))| {
55 (config.building.ordering_global_fn)((b_job, b_can), (a_job, a_can))
56 });
57
58 let new_cluster = cluster_estimates.first().and_then(|(_, (cluster, _))| cluster.as_ref()).cloned();
59
60 if let Some(new_cluster) = new_cluster {
61 let new_cluster_jobs = new_cluster
62 .dimens()
63 .get_cluster_info()
64 .expect("expected to have jobs in a cluster")
65 .iter()
66 .map(|info| info.job.clone())
67 .collect::<Vec<_>>();
68
69 clusters.push((new_cluster.clone(), new_cluster_jobs.clone()));
70 used_jobs.extend(new_cluster_jobs);
71
72 cluster_estimates.retain(|(center, _)| !used_jobs.contains(center));
74 cluster_estimates.iter_mut().for_each(|(_, (cluster, candidates))| {
75 candidates.retain(|job| !used_jobs.contains(job));
76
77 let is_cluster_affected = cluster
78 .as_ref()
79 .and_then(|cluster| cluster.dimens().get_cluster_info())
80 .map_or(false, |cluster_jobs| cluster_jobs.iter().any(|info| used_jobs.contains(&info.job)));
81
82 if is_cluster_affected {
83 *cluster = None;
85 }
86 });
87 cluster_estimates.retain(|(_, (_, candidates))| !candidates.is_empty());
88 } else {
89 break;
90 }
91 }
92
93 clusters
94}
95
96pub(crate) fn get_jobs_dissimilarities(
98 jobs: &[Job],
99 transport: &(dyn TransportCost),
100 config: &ClusterConfig,
101) -> HashMap<Job, DissimilarityIndex> {
102 jobs.iter()
103 .map(|outer| {
104 let dissimilarities = jobs
105 .iter()
106 .filter(|inner| outer != *inner)
107 .filter_map(|inner| {
108 let dissimilarities = get_dissimilarities(outer, inner, transport, config);
109 if dissimilarities.is_empty() {
110 None
111 } else {
112 Some((inner.clone(), dissimilarities))
113 }
114 })
115 .collect::<HashMap<_, _>>();
116 (outer.clone(), dissimilarities)
117 })
118 .collect::<HashMap<_, _>>()
119}
120
121fn get_dissimilarities(
122 outer: &Job,
123 inner: &Job,
124 transport: &(dyn TransportCost),
125 config: &ClusterConfig,
126) -> Vec<DissimilarityInfo> {
127 let min_shared_time = config.threshold.min_shared_time.unwrap_or(0.);
128 outer
129 .to_single()
130 .places
131 .iter()
132 .enumerate()
133 .filter_map(map_place)
134 .flat_map(|(outer_place_idx, outer_loc, _, outer_times)| {
135 inner.to_single().places.iter().enumerate().filter_map(map_place).filter_map(
136 move |(inner_place_idx, inner_loc, inner_duration, inner_times)| {
137 let shared_time = outer_times
138 .iter()
139 .flat_map(|outer_time| {
140 inner_times.iter().filter_map(move |inner_time| {
141 outer_time.overlapping(inner_time).map(|tw| tw.duration())
142 })
143 })
144 .max_by(|a, b| a.total_cmp(b))
145 .unwrap_or(0.);
146
147 if shared_time > min_shared_time {
148 let fwd_distance = transport.distance_approx(&config.profile, outer_loc, inner_loc);
149 let fwd_duration = transport.duration_approx(&config.profile, outer_loc, inner_loc);
150
151 let bck_distance = transport.distance_approx(&config.profile, inner_loc, outer_loc);
152 let bck_duration = transport.duration_approx(&config.profile, inner_loc, outer_loc);
153
154 let reachable = fwd_distance >= 0. && bck_distance >= 0.;
155
156 let reachable = reachable
157 && (fwd_duration - config.threshold.moving_duration < 0.)
158 && (fwd_distance - config.threshold.moving_distance < 0.)
159 && (bck_duration - config.threshold.moving_duration < 0.)
160 && (bck_distance - config.threshold.moving_distance < 0.);
161
162 let (service_time, _) = get_service_time(inner_duration, &config.serving);
163
164 let info = ClusterInfo {
165 job: inner.clone(),
166 service_time,
167 place_idx: inner_place_idx,
168 commute: Commute {
169 forward: CommuteInfo {
170 location: outer_loc,
171 distance: fwd_distance,
172 duration: fwd_duration,
173 },
174 backward: CommuteInfo {
175 location: outer_loc,
176 distance: bck_distance,
177 duration: bck_duration,
178 },
179 },
180 };
181
182 Some((reachable, outer_place_idx, info))
183 } else {
184 None
185 }
186 },
187 )
188 })
189 .collect()
190}
191
192fn build_job_cluster(
193 variant: &GoalContext,
194 center_job: &Job,
195 estimates: &HashMap<Job, DissimilarityIndex>,
196 used_jobs: &HashSet<Job>,
197 config: &ClusterConfig,
198 check_insertion: &CheckInsertionFn,
199) -> Option<Job> {
200 let ordering_fn = config.building.ordering_local_fn.as_ref();
201 let center = center_job.to_single();
202 let center_estimates = estimates.get(center_job).expect("missing job in estimates");
203
204 center
206 .places
207 .iter()
208 .enumerate()
209 .filter_map(map_place)
210 .try_fold(Option::<(Job, usize)>::None, |best_cluster, center_place_info| {
211 let (center_place_idx, center_location, center_duration, center_times) = center_place_info;
212 let (new_duration, parking) = get_service_time(center_duration, &config.serving);
213 let new_duration = new_duration + parking;
214
215 let center_times = if parking > 0. {
217 center_times.into_iter().map(|tw| TimeWindow::new(tw.start, tw.end - parking)).collect()
218 } else {
219 center_times
220 };
221
222 let new_center_job = create_single_job(Some(center_location), new_duration, ¢er_times, ¢er.dimens);
223 let new_visit_info = ClusterInfo {
224 job: center_job.clone(),
225 service_time: new_duration,
226 place_idx: center_place_idx,
227 commute: Commute {
228 forward: CommuteInfo { location: center_place_idx, distance: 0., duration: 0. },
229 backward: CommuteInfo { location: center_place_idx, distance: 0., duration: 0. },
230 },
231 };
232 let center_commute = |original_info: &ClusterInfo| {
233 estimates
234 .get(center_job)
235 .and_then(|index| index.get(&original_info.job))
236 .and_then(|infos| {
237 infos.iter().find(|(_, outer_place_idx, info)| {
238 *outer_place_idx == center_place_idx && info.place_idx == original_info.place_idx
239 })
240 })
241 .map(|(_, _, info)| info.commute.clone())
242 .expect("cannot find movement info")
243 };
244
245 let is_max_jobs = |count| config.threshold.max_jobs_per_cluster.map_or(false, |max| max <= count);
246
247 let mut cluster_candidates = center_estimates
249 .iter()
250 .filter(|(job, ..)| !used_jobs.contains(*job))
251 .filter(|(_, infos)| infos.iter().any(|(reachable, ..)| *reachable))
252 .map(|(candidate, _)| candidate.clone())
253 .collect::<HashSet<_>>();
254
255 let mut cluster = with_cluster_dimension(new_center_job, new_visit_info);
256 let mut last_job = center_job.clone();
257 let mut last_place_idx = center_place_idx;
258 let mut count = 1_usize;
259
260 loop {
261 if cluster_candidates.is_empty() || is_max_jobs(count) {
262 break;
263 }
264
265 let mut job_estimates = estimates
267 .get(&last_job)
268 .iter()
269 .flat_map(|index| index.iter().filter(|(job, _)| cluster_candidates.contains(*job)))
270 .flat_map(|estimate| {
271 let include_unreachable = true;
274 get_cluster_info_sorted(last_place_idx, estimate, include_unreachable, ordering_fn)
275 .into_iter()
276 .next()
277 .map(|visit_info| (estimate.0, estimate.1, visit_info))
278 })
279 .collect::<Vec<_>>();
280 job_estimates.sort_by(|(_, _, a_info), (_, _, b_info)| (ordering_fn)(a_info, b_info));
281
282 let addition_result = job_estimates
284 .iter()
285 .try_fold(None, |_, candidate| {
286 try_add_job(
287 variant,
288 last_place_idx,
289 &cluster,
290 (candidate.0, candidate.1),
291 config,
292 center_commute,
293 check_insertion,
294 )
295 .map_or_else(
296 || {
297 cluster_candidates.remove(candidate.0);
298 ControlFlow::Continue(None)
299 },
300 |data| ControlFlow::Break(Some(data)),
301 )
302 })
303 .unwrap_value();
304
305 match addition_result {
306 Some((new_cluster, visit_info)) => {
307 if !matches!(config.visiting, VisitPolicy::Return) {
308 last_job = visit_info.job.clone();
309 last_place_idx = visit_info.place_idx;
310 }
311
312 count += 1;
313
314 cluster_candidates.remove(&visit_info.job);
315 cluster = with_cluster_dimension(new_cluster, visit_info);
316 }
317 None => cluster_candidates.clear(),
318 }
319 }
320
321 if count > 1 {
322 cluster = finish_cluster(cluster, config, center_commute);
323 }
324
325 match (&best_cluster, count) {
326 (_, count) if is_max_jobs(count) => ControlFlow::Break(Some((cluster, count))),
327 (Some((_, best_count)), _) if *best_count < count => ControlFlow::Continue(Some((cluster, count))),
328 (None, _) if count > 1 => ControlFlow::Continue(Some((cluster, count))),
329 _ => ControlFlow::Continue(best_cluster),
330 }
331 })
332 .unwrap_value()
333 .map(|(cluster, _)| cluster)
334}
335
336fn try_add_job<F>(
337 variant: &GoalContext,
338 center_place_idx: usize,
339 cluster: &Job,
340 candidate: (&Job, &Vec<DissimilarityInfo>),
341 config: &ClusterConfig,
342 center_commute: F,
343 check_insertion_fn: &CheckInsertionFn,
344) -> Option<(Job, ClusterInfo)>
345where
346 F: Fn(&ClusterInfo) -> Commute,
347{
348 let time_window_threshold = config.threshold.smallest_time_window.unwrap_or(0.).max(config.serving.get_parking());
349 let cluster = cluster.to_single();
350 let cluster_place = cluster.places.first().expect("expect one place in cluster");
351 let cluster_times = filter_times(cluster_place.times.as_slice());
352 let cluster_last_duration = cluster
353 .dimens
354 .get_cluster_info()
355 .and_then(|jobs| jobs.last())
356 .and_then(|info| {
357 info.job
358 .as_single()
359 .map(|job| (job, info))
360 .and_then(|(job, info)| job.places.first().map(|place| (place, info)))
361 })
362 .map_or(cluster_place.duration, |(place, info)| {
363 place.duration
364 + if matches!(config.visiting, VisitPolicy::Return) { info.commute.backward.duration } else { 0. }
365 });
366
367 let job = candidate.0.to_single();
368 let ordering = config.building.ordering_local_fn.as_ref();
369 let include_unreachable = true;
370 let dissimilarities = get_cluster_info_sorted(center_place_idx, candidate, include_unreachable, ordering);
371
372 dissimilarities
373 .into_iter()
374 .try_fold(None, |_, info| {
375 let place = job.places.get(info.place_idx).expect("wrong place index");
376 let place_times = filter_times(place.times.as_slice());
377
378 let commute = if matches!(config.visiting, VisitPolicy::Return) {
380 center_commute(&info)
381 } else {
382 Commute {
383 forward: info.commute.forward,
384 backward: CommuteInfo {
385 location: place.location.expect("no location"),
386 distance: 0.,
387 duration: 0.,
388 },
389 }
390 };
391 let info = ClusterInfo { commute, ..info };
392
393 let new_cluster_times = cluster_times
394 .iter()
395 .flat_map(|cluster_time| {
396 place_times.iter().filter_map({
397 let forward_duration = info.commute.forward.duration;
398 move |place_time| {
399 let place_time = TimeWindow::new(place_time.start - forward_duration, place_time.end);
401 let overlap_time = place_time.overlapping(cluster_time);
402
403 let duration = if place_time.end < cluster_time.end {
404 cluster_place.duration
405 } else {
406 cluster_last_duration
407 };
408
409 overlap_time.map(|time| (time, duration))
410 }
411 })
412 })
413 .filter_map(|(overlap_time, duration)| {
414 let end = overlap_time.end - duration - info.commute.forward.duration;
418 if end - overlap_time.start < time_window_threshold {
419 None
420 } else {
421 Some(TimeWindow::new(overlap_time.start, end))
422 }
423 })
424 .collect::<Vec<_>>();
425
426 if new_cluster_times.is_empty() {
428 return ControlFlow::Continue(None);
429 }
430
431 let movement = match config.visiting {
432 VisitPolicy::Return => info.commute.duration(),
433 VisitPolicy::ClosedContinuation | VisitPolicy::OpenContinuation => info.commute.forward.duration,
434 };
435
436 let new_cluster_duration = cluster_place.duration + movement + info.service_time;
437
438 let updated_cluster =
439 create_single_job(cluster_place.location, new_cluster_duration, &new_cluster_times, &cluster.dimens);
440 let updated_candidate =
441 create_single_job(place.location, new_cluster_duration, &new_cluster_times, &job.dimens);
442
443 variant
444 .merge(updated_cluster, updated_candidate)
445 .and_then(|merged_cluster| (check_insertion_fn)(&merged_cluster).map(|_| (merged_cluster, info)))
446 .map(Some)
447 .map_or_else(|_| ControlFlow::Continue(None), ControlFlow::Break)
448 })
449 .unwrap_value()
450}
451
452fn get_cluster_info_sorted(
453 center_place_idx: usize,
454 estimate: (&Job, &Vec<DissimilarityInfo>),
455 include_unreachable: bool,
456 ordering_fn: &(dyn Fn(&ClusterInfo, &ClusterInfo) -> Ordering + Send + Sync),
457) -> Vec<ClusterInfo> {
458 let (_, dissimilarities) = estimate;
459 let mut dissimilarities = dissimilarities
460 .iter()
461 .filter(|(_, outer_place_idx, ..)| *outer_place_idx == center_place_idx)
462 .filter(|(reachable, ..)| include_unreachable || *reachable)
463 .map(|(_, _, info)| info.clone())
464 .collect::<Vec<_>>();
465
466 dissimilarities.sort_by(|a, b| (ordering_fn)(a, b));
468
469 dissimilarities
470}
471
472fn map_place(place_data: (PlaceIndex, &Place)) -> Option<PlaceInfo> {
473 let (idx, place) = place_data;
474 place.location.map(|location| (idx, location, place.duration, filter_times(place.times.as_slice())))
475}
476
477fn filter_times(times: &[TimeSpan]) -> Vec<TimeWindow> {
478 times.iter().filter_map(|time| time.as_time_window()).collect::<Vec<_>>()
479}
480
481fn with_cluster_dimension(cluster: Job, visit_info: ClusterInfo) -> Job {
482 let cluster = cluster.to_single();
483
484 let mut cluster = Single { places: cluster.places.clone(), dimens: cluster.dimens.clone() };
485
486 let mut jobs = cluster.dimens.get_cluster_info().cloned().unwrap_or_default();
487 jobs.push(visit_info);
488
489 cluster.dimens.set_cluster_info(jobs);
490
491 Job::Single(Arc::new(cluster))
492}
493
494fn finish_cluster<F>(cluster: Job, config: &ClusterConfig, center_commute: F) -> Job
495where
496 F: Fn(&ClusterInfo) -> Commute,
497{
498 let clustered_jobs = cluster.dimens().get_cluster_info();
499
500 match (&config.visiting, clustered_jobs) {
501 (VisitPolicy::ClosedContinuation, Some(clustered)) => {
502 let cluster = cluster.to_single();
504 assert_eq!(cluster.places.len(), 1);
505
506 let mut clustered = clustered.clone();
507
508 let last_info = clustered.last_mut().expect("empty cluster");
510 let mut place = cluster.places.first().unwrap().clone();
511
512 let commute = center_commute(last_info);
513 place.duration += commute.backward.duration;
514 last_info.commute.backward = commute.backward;
515
516 let mut dimens = cluster.dimens.clone();
517 dimens.set_cluster_info(clustered);
518
519 Job::Single(Arc::new(Single { places: vec![place], dimens }))
520 }
521 _ => cluster,
522 }
523}
524
525fn create_single_job(location: Option<Location>, duration: Duration, times: &[TimeWindow], dimens: &Dimensions) -> Job {
526 Job::Single(Arc::new(Single {
527 places: vec![Place {
528 location,
529 duration,
530 times: times.iter().map(|time| TimeSpan::Window(time.clone())).collect(),
531 }],
532 dimens: dimens.clone(),
533 }))
534}
535
536fn get_service_time(original: Duration, policy: &ServingPolicy) -> (Duration, Duration) {
537 match *policy {
538 ServingPolicy::Original { parking } => (original, parking),
539 ServingPolicy::Multiplier { multiplier, parking } => (original * multiplier, parking),
540 ServingPolicy::Fixed { value, parking } => (value, parking),
541 }
542}