vrp_core/construction/clustering/vicinity/
estimations.rs

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
19/// Gets job clusters.
20pub(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                    // get only reachable estimates
35                    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            // remove used jobs from analysis
73            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                    // NOTE force to rebuild cluster on next iteration
84                    *cluster = None;
85                }
86            });
87            cluster_estimates.retain(|(_, (_, candidates))| !candidates.is_empty());
88        } else {
89            break;
90        }
91    }
92
93    clusters
94}
95
96/// Gets jobs dissimilarities.
97pub(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    // iterate through all places and choose the one with most jobs clustered
205    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            // NOTE as parking time is part of service time in the cluster, we need to shrink time window
216            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, &center_times, &center.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            // allow jobs only from reachable candidates
248            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                // get job estimates specific for the last visited place
266                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                        // embed the first visit info to sort estimates of all candidate jobs later
272                        // we allow unreachable from the last job candidates as they must be reachable from the center
273                        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                // try to find the first successful addition to the cluster from job estimates
283                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            // override backward movement costs in case of return
379            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                            // NOTE travel duration to the place can be deducted from its time window requirement
400                            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                    // TODO adapt service time from last cluster job to avoid time window violation of
415                    //      a next job in case of last time arrival. However, this can be too restrictive
416                    //      in some cases and can be improved to keep time window a bit wider.
417                    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            // no time window intersection: cannot be clustered
427            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    // sort dissimilarities based on user provided ordering function
467    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            // add extra duration from last clustered job to finish cluster visiting
503            let cluster = cluster.to_single();
504            assert_eq!(cluster.places.len(), 1);
505
506            let mut clustered = clustered.clone();
507
508            // NOTE add a return duration back to the cluster duration and modify backward info
509            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}