Skip to main content

vrp_core/construction/heuristics/
metrics.rs

1#[cfg(test)]
2#[path = "../../../tests/unit/construction/heuristics/metrics_test.rs"]
3mod metrics_test;
4
5use crate::construction::enablers::{TotalDistanceTourState, TotalDurationTourState, WaitingTimeActivityState};
6use crate::construction::features::MaxVehicleLoadTourState;
7use crate::construction::heuristics::{InsertionContext, RouteState};
8use crate::models::common::Distance;
9use crate::models::problem::TravelTime;
10use rosomaxa::algorithms::math::*;
11use rosomaxa::prelude::*;
12use rosomaxa::utils::{parallel_collect, SelectionSamplingIterator};
13use std::cmp::Ordering;
14
15/// Gets max load variance in tours.
16pub fn get_max_load_variance(insertion_ctx: &InsertionContext) -> Float {
17    get_variance(
18        get_values_from_route_state(insertion_ctx, |state| state.get_max_vehicle_load()).collect::<Vec<_>>().as_slice(),
19    )
20}
21
22/// Gets max load mean in tours.
23pub fn get_max_load_mean(insertion_ctx: &InsertionContext) -> Float {
24    get_mean_iter(get_values_from_route_state(insertion_ctx, |state| state.get_max_vehicle_load()))
25}
26
27/// Gets tours with max_load at least 0.9.
28pub fn get_full_load_ratio(insertion_ctx: &InsertionContext) -> Float {
29    let total = insertion_ctx.solution.routes.len();
30    if total == 0 {
31        0.
32    } else {
33        let full_capacity = get_values_from_route_state(insertion_ctx, |state| state.get_max_vehicle_load())
34            .filter(|&max_load| max_load > 0.9)
35            .count();
36
37        full_capacity as Float / total as Float
38    }
39}
40
41/// Gets standard deviation of the number of customer per tour.
42pub fn get_customers_deviation(insertion_ctx: &InsertionContext) -> Float {
43    let values = insertion_ctx
44        .solution
45        .routes
46        .iter()
47        .map(|route_ctx| route_ctx.route().tour.job_count() as Float)
48        .collect::<Vec<_>>();
49
50    get_stdev(values.as_slice())
51}
52
53/// Gets mean of route durations.
54pub fn get_duration_mean(insertion_ctx: &InsertionContext) -> Float {
55    get_mean_iter(get_values_from_route_state(insertion_ctx, |state| state.get_total_duration()))
56}
57
58/// Gets mean of route distances.
59pub fn get_distance_mean(insertion_ctx: &InsertionContext) -> Float {
60    get_mean_iter(get_values_from_route_state(insertion_ctx, |state| state.get_total_distance()))
61}
62
63/// Gets mean of future waiting time.
64pub fn get_waiting_mean(insertion_ctx: &InsertionContext) -> Float {
65    get_mean_iter(
66        insertion_ctx
67            .solution
68            .routes
69            .iter()
70            .filter(|route_ctx| route_ctx.route().tour.get(1).is_some())
71            .map(|route_ctx| route_ctx.state().get_waiting_time_at(1).copied().unwrap_or(0.)),
72    )
73}
74/// Gets longest distance between two connected customers (mean, S2).
75pub fn get_longest_distance_between_customers_mean(insertion_ctx: &InsertionContext) -> Float {
76    let transport = insertion_ctx.problem.transport.as_ref();
77    get_mean_iter(insertion_ctx.solution.routes.iter().map(|route_ctx| {
78        route_ctx.route().tour.legs().fold(0., |acc, (activities, _)| match activities {
79            [_] => acc,
80            [prev, next] => transport
81                .distance(
82                    route_ctx.route(),
83                    prev.place.location,
84                    next.place.location,
85                    TravelTime::Departure(prev.schedule.departure),
86                )
87                .max(acc),
88            _ => panic!("Unexpected route leg configuration."),
89        })
90    }))
91}
92
93/// Gets average distance between depot to directly-connected customers (mean, S3).
94pub fn get_average_distance_between_depot_customer_mean(insertion_ctx: &InsertionContext) -> Float {
95    let transport = insertion_ctx.problem.transport.as_ref();
96
97    get_mean_iter(insertion_ctx.solution.routes.iter().map(|route_ctx| {
98        let depot = route_ctx.route().tour.start().expect("empty tour");
99
100        get_mean_iter(route_ctx.route().tour.all_activities().skip(1).map(|activity| {
101            transport.distance(
102                route_ctx.route(),
103                depot.place.location,
104                activity.place.location,
105                TravelTime::Departure(depot.schedule.departure),
106            )
107        }))
108    }))
109}
110
111/// Gets the largest distance between a customer on the route and the depot (mean, S3).
112pub fn get_longest_distance_between_depot_customer_mean(insertion_ctx: &InsertionContext) -> Float {
113    let transport = insertion_ctx.problem.transport.as_ref();
114
115    get_mean_iter(insertion_ctx.solution.routes.iter().filter_map(|route_ctx| {
116        let depot = route_ctx.route().tour.start().expect("empty tour");
117
118        route_ctx
119            .route()
120            .tour
121            .all_activities()
122            .skip(1)
123            .map(|activity| {
124                transport.distance(
125                    route_ctx.route(),
126                    depot.place.location,
127                    activity.place.location,
128                    TravelTime::Departure(depot.schedule.departure),
129                )
130            })
131            .max_by(|a, b| a.total_cmp(b))
132    }))
133}
134
135/// Gets the distance between a first job and the depot (mean).
136pub fn get_first_distance_customer_mean(insertion_ctx: &InsertionContext) -> Float {
137    let transport = insertion_ctx.problem.transport.as_ref();
138
139    get_mean_iter(insertion_ctx.solution.routes.iter().filter_map(|route_ctx| {
140        let route = route_ctx.route();
141        route.tour.get(1).zip(route.tour.start()).map(|(activity, depot)| {
142            transport.distance(
143                route,
144                depot.place.location,
145                activity.place.location,
146                TravelTime::Departure(depot.schedule.departure),
147            )
148        })
149    }))
150}
151
152/// Gets the distance between a last job and the depot (mean).
153pub fn get_last_distance_customer_mean(insertion_ctx: &InsertionContext) -> Float {
154    let transport = insertion_ctx.problem.transport.as_ref();
155
156    let distances = insertion_ctx.solution.routes.iter().filter_map(|route_ctx| {
157        let tour = &route_ctx.route().tour;
158        if tour.total() < 2 {
159            return None;
160        }
161
162        // NOTE if it is open VRP, we get a distance between two last customers
163        let last_idx = tour.total() - 1;
164        let before_last_idx = last_idx - 1;
165
166        tour.get(before_last_idx).zip(tour.get(last_idx)).map(|(activity, depot)| {
167            transport.distance(
168                route_ctx.route(),
169                activity.place.location,
170                depot.place.location,
171                TravelTime::Departure(depot.schedule.departure),
172            )
173        })
174    });
175
176    get_mean_iter(distances)
177}
178
179/// Estimates distances between all routes by sampling locations from routes and measuring
180/// average distance between them.
181pub fn group_routes_by_proximity(insertion_ctx: &InsertionContext) -> Vec<Vec<usize>> {
182    const LOCATION_SAMPLE_SIZE: usize = 4;
183
184    let routes = &insertion_ctx.solution.routes;
185    let transport = insertion_ctx.problem.transport.as_ref();
186    let random = &insertion_ctx.environment.random;
187
188    // get routes with sampled locations and index them
189    let indexed_route_clusters = routes
190        .iter()
191        .map(|route_ctx| {
192            SelectionSamplingIterator::new(
193                route_ctx.route().tour.all_activities(),
194                LOCATION_SAMPLE_SIZE,
195                random.clone(),
196            )
197            .map(|activity| activity.place.location)
198            .collect::<Vec<_>>()
199        })
200        .enumerate()
201        .collect::<Vec<_>>();
202
203    parallel_collect(&indexed_route_clusters, |(outer_idx, outer_clusters)| {
204        let mut route_distances = indexed_route_clusters
205            .iter()
206            .filter(move |(inner_idx, _)| *outer_idx != *inner_idx)
207            .map(move |(inner_idx, inner_clusters)| {
208                // get a sum of distances between all pairs of sampled locations
209                let pair_distance = outer_clusters
210                    .iter()
211                    .flat_map(|outer| inner_clusters.iter().map(move |inner| (inner, outer)))
212                    .map(|(&o, &i)| {
213                        // NOTE use outer and inner route profiles to estimate distance
214                        let inner_profile = &routes[*inner_idx].route().actor.vehicle.profile;
215                        let outer_profile = &routes[*outer_idx].route().actor.vehicle.profile;
216                        transport.distance_approx(inner_profile, o, i).max(0.)
217                            + transport.distance_approx(outer_profile, o, i).max(0.)
218                    })
219                    .sum::<Distance>()
220                    / 2.;
221
222                let total_pairs = outer_clusters.len() * inner_clusters.len();
223                let distance = if total_pairs == 0 {
224                    None
225                } else {
226                    // get average distance between clusters
227                    Some(pair_distance / total_pairs as Float)
228                };
229
230                (*inner_idx, distance)
231            })
232            .collect::<Vec<_>>();
233
234        route_distances.sort_unstable_by(|(_, a_distance), (_, b_distance)| match (a_distance, b_distance) {
235            (Some(a_distance), Some(b_distance)) => a_distance.total_cmp(b_distance),
236            (Some(_), None) => Ordering::Less,
237            _ => Ordering::Greater,
238        });
239
240        let (indices, _): (Vec<_>, Vec<_>) = route_distances.into_iter().unzip();
241
242        indices
243    })
244}
245
246fn get_values_from_route_state<'a>(
247    insertion_ctx: &'a InsertionContext,
248    state_value_fn: impl Fn(&'a RouteState) -> Option<&Float> + 'a,
249) -> impl Iterator<Item = Float> + 'a {
250    insertion_ctx
251        .solution
252        .routes
253        .iter()
254        .map(move |route_ctx| state_value_fn(route_ctx.state()).copied().unwrap_or_default())
255}