1#[cfg(test)]
2#[path = "../../../tests/unit/format/solution/writer_test.rs"]
3mod writer_test;
4
5use crate::format::solution::activity_matcher::get_job_tag;
6use crate::format::solution::model::Timing;
7use crate::format::solution::*;
8use crate::format::CoordIndex;
9use vrp_core::construction::enablers::{get_route_intervals, ReservedTimesIndex};
10use vrp_core::construction::features::JobDemandDimension;
11use vrp_core::construction::heuristics::UnassignmentInfo;
12use vrp_core::models::common::*;
13use vrp_core::models::problem::{JobIdDimension, Multi, TravelTime, VehicleIdDimension};
14use vrp_core::models::solution::{Activity, Route};
15use vrp_core::prelude::Float;
16use vrp_core::rosomaxa::evolution::TelemetryMetrics;
17use vrp_core::solver::processing::{ClusterConfigExtraProperty, ReservedTimesExtraProperty};
18use vrp_core::utils::CollectGroupBy;
19
20struct Leg {
21 pub last_detail: Option<(DomainLocation, Timestamp)>,
22 pub load: Option<MultiDimLoad>,
23 pub statistic: Statistic,
24}
25
26impl Leg {
27 fn new(last_detail: Option<(DomainLocation, Timestamp)>, load: Option<MultiDimLoad>, statistic: Statistic) -> Self {
28 Self { last_detail, load, statistic }
29 }
30
31 fn empty() -> Self {
32 Self { last_detail: None, load: None, statistic: Statistic::default() }
33 }
34}
35
36pub(crate) fn create_solution(
38 problem: &DomainProblem,
39 solution: &DomainSolution,
40 output_type: &PragmaticOutputType,
41) -> ApiSolution {
42 let coord_index = problem.extras.get_coord_index().expect("no coord index");
43
44 let empty_reserved_times = Default::default();
45 let reserved_times_index = problem.extras.get_reserved_times();
46 let reserved_times_index = reserved_times_index.as_ref().unwrap_or(&empty_reserved_times);
47
48 let tours = solution
49 .routes
50 .iter()
51 .map(|r| create_tour(problem, r, &coord_index, reserved_times_index))
52 .collect::<Vec<Tour>>();
53
54 let statistic = tours.iter().fold(Statistic::default(), |acc, tour| acc + tour.statistic.clone());
55
56 let unassigned = create_unassigned(solution);
57 let violations = create_violations(solution);
58
59 let api_solution = ApiSolution { statistic, tours, unassigned, violations, extras: None };
60
61 let extras = create_extras(problem, &api_solution, solution.telemetry.as_ref(), output_type);
62
63 ApiSolution { extras, ..api_solution }
64}
65
66fn create_tour(
67 problem: &DomainProblem,
68 route: &Route,
69 coord_index: &CoordIndex,
70 reserved_times_index: &ReservedTimesIndex,
71) -> Tour {
72 let parking = get_parking_time(problem.extras.as_ref());
74
75 let actor = route.actor.as_ref();
76 let vehicle = actor.vehicle.as_ref();
77 let transport = problem.transport.as_ref();
78
79 let mut tour = Tour {
80 vehicle_id: vehicle.dimens.get_vehicle_id().unwrap().clone(),
81 type_id: vehicle.dimens.get_vehicle_type().unwrap().clone(),
82 shift_index: vehicle.dimens.get_shift_index().copied().unwrap(),
83 stops: vec![],
84 statistic: Statistic::default(),
85 };
86
87 let intervals = get_route_intervals(route, |a| get_activity_type(a).map_or(false, |t| t == "reload"));
88
89 let mut leg = intervals.into_iter().fold(Leg::empty(), |leg, (start_idx, end_idx)| {
90 let (start_delivery, end_pickup) = route.tour.activities_slice(start_idx, end_idx).iter().fold(
91 (leg.load.unwrap_or_default(), MultiDimLoad::default()),
92 |acc, activity| {
93 let (delivery, pickup) = activity
94 .job
95 .as_ref()
96 .and_then(|job| get_capacity(&job.dimens).map(|d| (d.delivery.0, d.pickup.0)))
97 .unwrap_or((MultiDimLoad::default(), MultiDimLoad::default()));
98 (acc.0 + delivery, acc.1 + pickup)
99 },
100 );
101
102 let (start_idx, start) = if start_idx == 0 {
103 let start = route.tour.start().unwrap();
104 let is_same_location =
105 route.tour.get(1).map_or(false, |activity| start.place.location == activity.place.location);
106
107 tour.stops.push(Stop::Point(PointStop {
108 location: coord_index.get_by_idx(start.place.location).unwrap(),
109 time: format_schedule(&start.schedule),
110 load: start_delivery.as_vec(),
111 distance: 0,
112 activities: vec![ApiActivity {
113 job_id: "departure".to_string(),
114 activity_type: "departure".to_string(),
115 location: None,
116 time: if is_same_location {
117 Some(Interval {
118 start: format_time(start.schedule.arrival),
119 end: format_time(start.schedule.departure),
120 })
121 } else {
122 None
123 },
124 job_tag: None,
125 commute: None,
126 }],
127 parking: None,
128 }));
129 (start_idx + 1, start)
130 } else {
131 (start_idx, route.tour.get(start_idx - 1).unwrap())
132 };
133
134 let mut leg = route.tour.activities_slice(start_idx, end_idx).iter().fold(
135 Leg::new(Some((start.place.location, start.schedule.departure)), Some(start_delivery), leg.statistic),
136 |leg, act| {
137 let activity_type = get_activity_type(act).cloned();
138 let (prev_location, prev_departure) = leg.last_detail.unwrap();
139 let prev_load = if activity_type.is_some() {
140 leg.load.unwrap()
141 } else {
142 let dimen_size = leg.load.unwrap().size;
144 MultiDimLoad::new(vec![0; dimen_size])
145 };
146
147 let activity_type = activity_type.unwrap_or_else(|| "arrival".to_string());
148 let is_break = activity_type == "break";
149
150 let job_tag = act.job.as_ref().and_then(|single| {
151 get_job_tag(single, (act.place.location, (act.place.time.clone(), start.schedule.departure)))
152 .cloned()
153 });
154 let job_id = match activity_type.as_str() {
155 "pickup" | "delivery" | "replacement" | "service" => {
156 let single = act.job.as_ref().unwrap();
157 let id = single.dimens.get_job_id().cloned();
158 id.unwrap_or_else(|| Multi::roots(single).unwrap().dimens.get_job_id().unwrap().clone())
159 }
160 _ => activity_type.clone(),
161 };
162
163 let commute = act.commute.clone().unwrap_or_default();
164 let commuting = commute.duration();
165
166 let (driving, transport_cost) = if commute.is_zero_distance() {
167 let prev_departure = TravelTime::Departure(prev_departure);
169 let duration = transport.duration(route, prev_location, act.place.location, prev_departure);
170 let transport_cost = transport.cost(route, prev_location, act.place.location, prev_departure);
171 (duration, transport_cost)
172 } else {
173 (0., commuting * vehicle.costs.per_service_time)
175 };
176
177 let parking =
179 match (prev_location == act.place.location, act.commute.is_some(), commute.is_zero_distance()) {
180 (false, true, true) => parking,
181 _ => 0.,
182 };
183
184 let activity_arrival = parking + act.schedule.arrival + commute.forward.duration;
185 let service_start = activity_arrival.max(act.place.time.start);
186 let waiting = service_start - activity_arrival;
187 let serving = act.place.duration - parking;
188 let service_end = service_start + serving;
189 let activity_departure = service_end;
190
191 let serving_cost = problem.activity.cost(route, act, service_start);
193 let total_cost = serving_cost + transport_cost + waiting * vehicle.costs.per_waiting_time;
194
195 let location_distance =
196 transport.distance(route, prev_location, act.place.location, TravelTime::Departure(prev_departure))
197 as i64;
198 let distance = leg.statistic.distance + location_distance - commute.forward.distance as i64;
199
200 let is_new_stop = match (act.commute.as_ref(), prev_location == act.place.location) {
201 (Some(commute), false) if commute.is_zero_distance() => true,
202 (Some(_), _) => false,
203 (None, is_same_location) => !is_same_location,
204 };
205
206 if is_new_stop {
207 tour.stops.push(Stop::Point(PointStop {
208 location: coord_index.get_by_idx(act.place.location).unwrap(),
209 time: format_schedule(&act.schedule),
210 load: prev_load.as_vec(),
211 distance,
212 parking: if parking > 0. {
213 Some(Interval {
214 start: format_time(act.schedule.arrival),
215 end: format_time(act.schedule.arrival + parking),
216 })
217 } else {
218 None
219 },
220 activities: vec![],
221 }));
222 }
223
224 let load = calculate_load(prev_load, act);
225
226 let last = tour.stops.len() - 1;
227 let last = match tour.stops.get_mut(last).unwrap() {
228 Stop::Point(point) => point,
229 Stop::Transit(_) => unreachable!(),
230 };
231
232 last.time.departure = format_time(act.schedule.departure);
233 last.load = load.as_vec();
234 last.activities.push(ApiActivity {
235 job_id,
236 activity_type: activity_type.clone(),
237 location: Some(coord_index.get_by_idx(act.place.location).unwrap()),
238 time: Some(Interval {
239 start: format_time(activity_arrival.max(act.place.time.start)),
240 end: format_time(activity_departure),
241 }),
242 job_tag,
243 commute: act
244 .commute
245 .as_ref()
246 .map(|commute| Commute::new(commute, act.schedule.arrival, activity_departure, coord_index)),
247 });
248
249 let end_location = if commute.backward.is_zero_distance() {
251 act.place.location
252 } else {
253 tour.stops
254 .last()
255 .and_then(|stop| stop.as_point())
256 .and_then(|stop| coord_index.get_by_loc(&stop.location))
257 .expect("expect to have at least one stop")
258 };
259
260 Leg {
261 last_detail: Some((end_location, act.schedule.departure)),
262 statistic: Statistic {
263 cost: leg.statistic.cost + total_cost,
264 distance,
265 duration: leg.statistic.duration + act.schedule.departure as i64 - prev_departure as i64,
266 times: Timing {
267 driving: leg.statistic.times.driving + driving as i64,
268 serving: leg.statistic.times.serving + (if is_break { 0 } else { serving as i64 }),
269 waiting: leg.statistic.times.waiting + waiting as i64,
270 break_time: leg.statistic.times.break_time + (if is_break { serving as i64 } else { 0 }),
271 commuting: leg.statistic.times.commuting + commuting as i64,
272 parking: leg.statistic.times.parking + parking as i64,
273 },
274 },
275 load: Some(load),
276 }
277 },
278 );
279
280 leg.load = Some(leg.load.unwrap() - end_pickup);
281
282 leg
283 });
284
285 leg.statistic.cost += vehicle.costs.fixed;
286 tour.statistic = leg.statistic;
287
288 insert_reserved_times_as_breaks(route, &mut tour, reserved_times_index);
289
290 tour.stops
292 .iter_mut()
293 .filter(|stop| stop.activities().len() == 1)
294 .flat_map(|stop| {
295 let schedule = stop.schedule().clone();
296 let location = stop.location().cloned();
297 stop.activities_mut().first_mut().map(|activity| (location, schedule, activity))
298 })
299 .for_each(|(location, schedule, activity)| {
300 let is_same_schedule = activity.time.as_ref().map_or(true, |time| schedule.arrival == time.start);
301 let is_same_location = activity.location.clone().zip(location).map_or(true, |(lhs, rhs)| lhs == rhs);
302
303 if is_same_schedule {
304 activity.time = None;
305 }
306
307 if is_same_location {
308 activity.location = None;
309 }
310 });
311
312 tour.vehicle_id.clone_from(vehicle.dimens.get_vehicle_id().unwrap());
313 tour.type_id.clone_from(vehicle.dimens.get_vehicle_type().unwrap());
314
315 tour
316}
317
318fn format_schedule(schedule: &DomainSchedule) -> ApiSchedule {
319 ApiSchedule { arrival: format_time(schedule.arrival), departure: format_time(schedule.departure) }
320}
321
322fn calculate_load(current: MultiDimLoad, act: &Activity) -> MultiDimLoad {
323 let job = act.job.as_ref();
324 let demand = job.and_then(|job| get_capacity(&job.dimens)).unwrap_or_default();
325 current - demand.delivery.0 - demand.delivery.1 + demand.pickup.0 + demand.pickup.1
326}
327
328fn create_unassigned(solution: &DomainSolution) -> Option<Vec<UnassignedJob>> {
329 let create_simple_reasons = |code: ViolationCode| {
330 let (code, reason) = map_code_reason(code);
331 vec![UnassignedJobReason { code: code.to_string(), description: reason.to_string(), details: None }]
332 };
333
334 let unassigned = solution
335 .unassigned
336 .iter()
337 .filter(|(job, _)| job.dimens().get_vehicle_id().is_none())
338 .map(|(job, code)| {
339 let job_id = job.dimens().get_job_id().expect("job id expected").clone();
340
341 let reasons = match code {
342 UnassignmentInfo::Simple(code) => create_simple_reasons(*code),
343 UnassignmentInfo::Detailed(details) if !details.is_empty() => details
344 .iter()
345 .collect_group_by_key(|(_, code)| *code)
346 .into_iter()
347 .map(|(code, group)| {
348 let (code, reason) = map_code_reason(code);
349 let mut vehicle_details = group
350 .iter()
351 .map(|(actor, _)| {
352 let dimens = &actor.vehicle.dimens;
353 let vehicle_id = dimens.get_vehicle_id().cloned().unwrap();
354 let shift_index = dimens.get_shift_index().copied().unwrap();
355 (vehicle_id, shift_index)
356 })
357 .collect::<Vec<_>>();
358 vehicle_details.sort();
360
361 UnassignedJobReason {
362 details: Some(
363 vehicle_details
364 .into_iter()
365 .map(|(vehicle_id, shift_index)| UnassignedJobDetail { vehicle_id, shift_index })
366 .collect(),
367 ),
368 code: code.to_string(),
369 description: reason.to_string(),
370 }
371 })
372 .collect(),
373 _ => create_simple_reasons(ViolationCode(0)),
374 };
375
376 UnassignedJob { job_id, reasons }
377 })
378 .collect::<Vec<_>>();
379
380 if unassigned.is_empty() {
381 None
382 } else {
383 Some(unassigned)
384 }
385}
386
387fn create_violations(solution: &DomainSolution) -> Option<Vec<Violation>> {
388 let violations = solution
390 .unassigned
391 .iter()
392 .filter(|(job, _)| job.dimens().get_job_type().map_or(false, |t| t == "break"))
393 .map(|(job, _)| Violation::Break {
394 vehicle_id: job.dimens().get_vehicle_id().expect("vehicle id").clone(),
395 shift_index: job.dimens().get_shift_index().copied().expect("shift index"),
396 })
397 .collect::<Vec<_>>();
398
399 if violations.is_empty() {
400 None
401 } else {
402 Some(violations)
403 }
404}
405
406fn get_activity_type(activity: &Activity) -> Option<&String> {
407 activity.job.as_ref().and_then(|single| single.dimens.get_job_type())
408}
409
410fn get_capacity(dimens: &Dimensions) -> Option<Demand<MultiDimLoad>> {
411 let demand: Option<Demand<MultiDimLoad>> = dimens.get_job_demand().cloned();
413 if let Some(demand) = demand {
414 return Some(demand);
415 }
416
417 let create_capacity = |capacity: SingleDimLoad| {
418 if capacity.value == 0 {
419 MultiDimLoad::default()
420 } else {
421 MultiDimLoad::new(vec![capacity.value])
422 }
423 };
424 dimens.get_job_demand().map(|demand: &Demand<SingleDimLoad>| Demand {
425 pickup: (create_capacity(demand.pickup.0), create_capacity(demand.pickup.1)),
426 delivery: (create_capacity(demand.delivery.0), create_capacity(demand.delivery.1)),
427 })
428}
429
430fn get_parking_time(extras: &DomainExtras) -> Float {
431 extras.get_cluster_config().map_or(0., |config| config.serving.get_parking())
432}
433
434fn create_extras(
435 problem: &DomainProblem,
436 solution: &ApiSolution,
437 metrics: Option<&TelemetryMetrics>,
438 output_type: &PragmaticOutputType,
439) -> Option<Extras> {
440 match output_type {
441 PragmaticOutputType::OnlyPragmatic => {
442 get_api_metrics(metrics).map(|metrics| Extras { metrics: Some(metrics), features: None })
443 }
444 PragmaticOutputType::OnlyGeoJson => None,
445 PragmaticOutputType::Combined => {
446 Some(Extras {
447 metrics: get_api_metrics(metrics),
448 features: create_feature_collection(problem, solution).ok(),
450 })
451 }
452 }
453}
454
455fn get_api_metrics(metrics: Option<&TelemetryMetrics>) -> Option<ApiMetrics> {
456 metrics.as_ref().map(|metrics| ApiMetrics {
457 duration: metrics.duration,
458 generations: metrics.generations,
459 speed: metrics.speed,
460 evolution: metrics
461 .evolution
462 .iter()
463 .map(|g| ApiGeneration {
464 number: g.number,
465 timestamp: g.timestamp,
466 i_all_ratio: g.i_all_ratio,
467 i_1000_ratio: g.i_1000_ratio,
468 is_improvement: g.is_improvement,
469 population: AppPopulation {
470 individuals: g
471 .population
472 .individuals
473 .iter()
474 .map(|i| ApiIndividual { difference: i.difference, fitness: i.fitness.clone() })
475 .collect(),
476 },
477 })
478 .collect(),
479 })
480}