Skip to main content

vrp_pragmatic/format/problem/
job_reader.rs

1use crate::format::coord_index::CoordIndex;
2use crate::format::problem::JobSkills as ApiJobSkills;
3use crate::format::problem::*;
4use crate::format::{JobIndex, Location};
5use crate::utils::VariableJobPermutation;
6use std::collections::HashMap;
7use std::sync::Arc;
8use vrp_core::{
9    construction::features::{
10        BreakPolicy, JobCompatibilityDimension, JobDemandDimension, JobGroupDimension, JobSkills as FeatureJobSkills,
11        JobSkillsDimension,
12    },
13    models::common::*,
14    models::problem::{
15        Actor, Fleet, Job, JobIdDimension, Jobs, Multi, Place, Single, TransportCost, VehicleIdDimension,
16    },
17    models::{Lock, LockDetail, LockOrder, LockPosition},
18};
19
20// TODO configure sample size
21const MULTI_JOB_SAMPLE_SIZE: usize = 3;
22
23type PlaceData = (Option<Location>, Duration, Vec<TimeSpan>, Option<String>);
24type ApiJob = crate::format::problem::Job;
25
26pub(super) fn read_jobs_with_extra_locks(
27    api_problem: &ApiProblem,
28    props: &ProblemProperties,
29    coord_index: &CoordIndex,
30    fleet: &Fleet,
31    transport: &(dyn TransportCost + Sync + Send),
32    job_index: &mut JobIndex,
33    environment: &Environment,
34) -> (Jobs, Vec<Arc<Lock>>) {
35    let random = &environment.random;
36    let logger = &environment.logger;
37
38    let (mut jobs, locks) = read_required_jobs(api_problem, props, coord_index, job_index, random);
39    let conditional_jobs = read_conditional_jobs(api_problem, coord_index, job_index);
40
41    jobs.extend(conditional_jobs);
42
43    (Jobs::new(fleet, jobs, transport, logger).unwrap(), locks)
44}
45
46pub(super) fn read_locks(api_problem: &ApiProblem, job_index: &JobIndex) -> Vec<Arc<Lock>> {
47    if api_problem.plan.relations.as_ref().map_or(true, |r| r.is_empty()) {
48        return vec![];
49    }
50
51    let relations: HashMap<_, Vec<_>> =
52        api_problem.plan.relations.as_ref().unwrap().iter().fold(HashMap::new(), |mut acc, r| {
53            let shift_index = r.shift_index.unwrap_or_default();
54            acc.entry((r.vehicle_id.clone(), shift_index)).or_default().push(r.clone());
55
56            acc
57        });
58
59    relations.into_iter().fold(vec![], |mut acc, ((vehicle_id, shift_index), rels)| {
60        let condition = create_condition(vehicle_id.clone(), shift_index);
61        let details = rels.iter().fold(vec![], |mut acc, rel| {
62            let order = match rel.type_field {
63                RelationType::Any => LockOrder::Any,
64                RelationType::Sequence => LockOrder::Sequence,
65                RelationType::Strict => LockOrder::Strict,
66            };
67
68            let position = match (rel.jobs.first().map(|s| s.as_str()), rel.jobs.last().map(|s| s.as_str())) {
69                (Some("departure"), Some("arrival")) => LockPosition::Fixed,
70                (Some("departure"), _) => LockPosition::Departure,
71                (_, Some("arrival")) => LockPosition::Arrival,
72                _ => LockPosition::Any,
73            };
74
75            let (_, jobs) = rel
76                .jobs
77                .iter()
78                .filter(|job| job.as_str() != "departure" && job.as_str() != "arrival")
79                .fold((HashMap::<String, _>::default(), vec![]), |(mut indexer, mut jobs), job| {
80                    let job_id = match job.as_str() {
81                        "break" | "reload" | "recharge" => {
82                            let entry = indexer.entry(job.clone()).or_insert(1_usize);
83                            let job_index = *entry;
84                            *entry += 1;
85                            format!("{vehicle_id}_{job}_{shift_index}_{job_index}")
86                        }
87                        _ => job.clone(),
88                    };
89                    let job =
90                        job_index.get(&job_id).cloned().unwrap_or_else(|| panic!("cannot find job with id: '{job_id}"));
91
92                    jobs.push(job);
93
94                    (indexer, jobs)
95                });
96
97            acc.push(LockDetail::new(order, position, jobs));
98
99            acc
100        });
101
102        acc.push(Arc::new(Lock::new(condition, details, false)));
103
104        acc
105    })
106}
107
108fn read_required_jobs(
109    api_problem: &ApiProblem,
110    props: &ProblemProperties,
111    coord_index: &CoordIndex,
112    job_index: &mut JobIndex,
113    random: &Arc<dyn Random>,
114) -> (Vec<Job>, Vec<Arc<Lock>>) {
115    let mut jobs = vec![];
116    let has_multi_dimens = props.has_multi_dimen_capacity;
117
118    let get_single_from_task = |task: &JobTask, activity_type: &str, is_static_demand: bool| {
119        let absent = (empty(), empty());
120        let capacity = task.demand.clone().map_or_else(empty, MultiDimLoad::new);
121        let demand = if is_static_demand { (capacity, empty()) } else { (empty(), capacity) };
122
123        let demand = match activity_type {
124            "pickup" => Demand { pickup: demand, delivery: absent },
125            "delivery" => Demand { pickup: absent, delivery: demand },
126            "replacement" => Demand { pickup: demand, delivery: demand },
127            "service" => Demand { pickup: absent, delivery: absent },
128            _ => panic!("invalid activity type."),
129        };
130
131        let places = task
132            .places
133            .iter()
134            .map(|p| (Some(p.location.clone()), p.duration, parse_times(&p.times), p.tag.clone()))
135            .collect();
136
137        get_single_with_dimens(places, demand, &task.order, activity_type, has_multi_dimens, coord_index)
138    };
139
140    api_problem.plan.jobs.iter().for_each(|job| {
141        let pickups = job.pickups.as_ref().map_or(0, |p| p.len());
142        let deliveries = job.deliveries.as_ref().map_or(0, |p| p.len());
143        let is_static_demand = pickups == 0 || deliveries == 0;
144
145        let singles =
146            job.pickups
147                .iter()
148                .flat_map(|tasks| tasks.iter().map(|task| get_single_from_task(task, "pickup", is_static_demand)))
149                .chain(job.deliveries.iter().flat_map(|tasks| {
150                    tasks.iter().map(|task| get_single_from_task(task, "delivery", is_static_demand))
151                }))
152                .chain(
153                    job.replacements
154                        .iter()
155                        .flat_map(|tasks| tasks.iter().map(|task| get_single_from_task(task, "replacement", true))),
156                )
157                .chain(
158                    job.services
159                        .iter()
160                        .flat_map(|tasks| tasks.iter().map(|task| get_single_from_task(task, "service", false))),
161                )
162                .collect::<Vec<_>>();
163
164        assert!(!singles.is_empty());
165
166        let problem_job = if singles.len() > 1 {
167            let deliveries_start_index = job.pickups.as_ref().map_or(0, |p| p.len());
168            get_multi_job(job, singles, deliveries_start_index, random)
169        } else {
170            get_single_job(job, singles.into_iter().next().unwrap())
171        };
172
173        job_index.insert(job.id.clone(), problem_job.clone());
174        jobs.push(problem_job);
175    });
176
177    (jobs, vec![])
178}
179
180fn read_conditional_jobs(api_problem: &ApiProblem, coord_index: &CoordIndex, job_index: &mut JobIndex) -> Vec<Job> {
181    let mut jobs = vec![];
182
183    api_problem.fleet.vehicles.iter().for_each(|vehicle| {
184        for (shift_index, shift) in vehicle.shifts.iter().enumerate() {
185            if let Some(breaks) = &shift.breaks {
186                read_optional_breaks(coord_index, job_index, &mut jobs, vehicle, shift_index, breaks);
187            }
188
189            if let Some(reloads) = &shift.reloads {
190                read_reloads(coord_index, job_index, &mut jobs, vehicle, shift_index, reloads);
191            }
192
193            if let Some(recharges) = &shift.recharges {
194                read_recharges(coord_index, job_index, &mut jobs, vehicle, shift_index, recharges);
195            }
196        }
197    });
198
199    jobs
200}
201
202fn read_optional_breaks(
203    coord_index: &CoordIndex,
204    job_index: &mut JobIndex,
205    jobs: &mut Vec<Job>,
206    vehicle: &VehicleType,
207    shift_index: usize,
208    breaks: &[VehicleBreak],
209) {
210    (1..)
211        .zip(breaks.iter().filter_map(|vehicle_break| match vehicle_break {
212            VehicleBreak::Optional { time, places, policy } => Some((time, places, policy)),
213            VehicleBreak::Required { .. } => None,
214        }))
215        .flat_map(|(break_idx, (break_time, break_places, policy))| {
216            vehicle
217                .vehicle_ids
218                .iter()
219                .map(|vehicle_id| {
220                    let times = match &break_time {
221                        VehicleOptionalBreakTime::TimeWindow(time) if time.len() != 2 => {
222                            panic!("break with invalid time window specified: must have start and end!")
223                        }
224                        VehicleOptionalBreakTime::TimeOffset(offsets) if offsets.len() != 2 => {
225                            panic!("break with invalid offset specified: must have start and end!")
226                        }
227                        VehicleOptionalBreakTime::TimeWindow(time) => vec![TimeSpan::Window(parse_time_window(time))],
228                        VehicleOptionalBreakTime::TimeOffset(offset) => {
229                            vec![TimeSpan::Offset(TimeOffset::new(*offset.first().unwrap(), *offset.last().unwrap()))]
230                        }
231                    };
232
233                    let job_id = format!("{vehicle_id}_break_{shift_index}_{break_idx}");
234                    let places = break_places
235                        .iter()
236                        .map(|place| (place.location.clone(), place.duration, times.clone(), place.tag.clone()))
237                        .collect();
238
239                    let mut job =
240                        get_conditional_job(coord_index, vehicle_id.clone(), &job_id, "break", shift_index, places);
241
242                    if let Some(policy) = policy {
243                        let policy = match policy {
244                            VehicleOptionalBreakPolicy::SkipIfNoIntersection => BreakPolicy::SkipIfNoIntersection,
245                            VehicleOptionalBreakPolicy::SkipIfArrivalBeforeEnd => BreakPolicy::SkipIfArrivalBeforeEnd,
246                        };
247
248                        job.dimens.set_break_policy(policy);
249                    }
250
251                    (job_id, job)
252                })
253                .collect::<Vec<_>>()
254        })
255        .for_each(|(job_id, single)| add_conditional_job(job_index, jobs, job_id, single));
256}
257
258fn read_reloads(
259    coord_index: &CoordIndex,
260    job_index: &mut JobIndex,
261    jobs: &mut Vec<Job>,
262    vehicle: &VehicleType,
263    shift_index: usize,
264    reloads: &[VehicleReload],
265) {
266    read_specific_job_places(
267        "reload",
268        coord_index,
269        job_index,
270        jobs,
271        vehicle,
272        shift_index,
273        reloads.iter().map(|reload| JobPlace {
274            location: reload.location.clone(),
275            duration: reload.duration,
276            times: reload.times.clone(),
277            tag: reload.tag.clone(),
278        }),
279    )
280}
281
282fn read_recharges(
283    coord_index: &CoordIndex,
284    job_index: &mut JobIndex,
285    jobs: &mut Vec<Job>,
286    vehicle: &VehicleType,
287    shift_index: usize,
288    recharges: &VehicleRecharges,
289) {
290    read_specific_job_places(
291        "recharge",
292        coord_index,
293        job_index,
294        jobs,
295        vehicle,
296        shift_index,
297        recharges.stations.iter().cloned(),
298    )
299}
300
301fn read_specific_job_places(
302    job_type: &str,
303    coord_index: &CoordIndex,
304    job_index: &mut JobIndex,
305    jobs: &mut Vec<Job>,
306    vehicle: &VehicleType,
307    shift_index: usize,
308    get_places: impl Iterator<Item = JobPlace>,
309) {
310    (1..)
311        .zip(get_places)
312        .flat_map(|(place_idx, place)| {
313            vehicle
314                .vehicle_ids
315                .iter()
316                .map(|vehicle_id| {
317                    let job_id = format!("{vehicle_id}_{job_type}_{shift_index}_{place_idx}");
318                    let times = parse_times(&place.times);
319
320                    let job = get_conditional_job(
321                        coord_index,
322                        vehicle_id.clone(),
323                        &job_id,
324                        job_type,
325                        shift_index,
326                        vec![(Some(place.location.clone()), place.duration, times, place.tag.clone())],
327                    );
328
329                    (job_id, job)
330                })
331                .collect::<Vec<_>>()
332        })
333        .for_each(|(job_id, single)| add_conditional_job(job_index, jobs, job_id, single));
334}
335
336fn get_conditional_job(
337    coord_index: &CoordIndex,
338    vehicle_id: String,
339    job_id: &str,
340    job_type: &str,
341    shift_index: usize,
342    places: Vec<PlaceData>,
343) -> Single {
344    let mut single = get_single(places, coord_index);
345    single
346        .dimens
347        .set_job_id(job_id.to_string())
348        .set_job_type(job_type.to_string())
349        .set_shift_index(shift_index)
350        .set_vehicle_id(vehicle_id);
351
352    single
353}
354
355fn add_conditional_job(job_index: &mut JobIndex, jobs: &mut Vec<Job>, job_id: String, single: Single) {
356    let job = Job::Single(Arc::new(single));
357    job_index.insert(job_id, job.clone());
358    jobs.push(job);
359}
360
361fn get_single(places: Vec<PlaceData>, coord_index: &CoordIndex) -> Single {
362    let tags = places
363        .iter()
364        .map(|(_, _, _, tag)| tag)
365        .enumerate()
366        .filter_map(|(idx, tag)| tag.as_ref().map(|tag| (idx, tag.clone())))
367        .collect::<Vec<_>>();
368
369    let places = places
370        .into_iter()
371        .map(|(location, duration, times, _)| Place {
372            location: location.as_ref().and_then(|l| coord_index.get_by_loc(l)),
373            duration,
374            times,
375        })
376        .collect();
377
378    let mut dimens = Dimensions::default();
379
380    dimens.set_place_tags(tags);
381
382    Single { places, dimens }
383}
384
385fn get_single_with_dimens(
386    places: Vec<PlaceData>,
387    demand: Demand<MultiDimLoad>,
388    order: &Option<i32>,
389    activity_type: &str,
390    has_multi_dimens: bool,
391    coord_index: &CoordIndex,
392) -> Single {
393    let mut single = get_single(places, coord_index);
394    let dimens = &mut single.dimens;
395
396    if has_multi_dimens {
397        dimens.set_job_demand(demand)
398    } else {
399        dimens.set_job_demand(Demand {
400            pickup: (SingleDimLoad::new(demand.pickup.0.load[0]), SingleDimLoad::new(demand.pickup.1.load[0])),
401            delivery: (SingleDimLoad::new(demand.delivery.0.load[0]), SingleDimLoad::new(demand.delivery.1.load[0])),
402        })
403    }
404    .set_job_type(activity_type.to_string());
405
406    if let Some(order) = order {
407        dimens.set_job_order(*order);
408    }
409
410    single
411}
412
413fn fill_dimens(job: &ApiJob, dimens: &mut Dimensions) {
414    dimens.set_job_id(job.id.clone());
415
416    if let Some(value) = job.value {
417        dimens.set_job_value(value);
418    }
419
420    if let Some(group) = job.group.clone() {
421        dimens.set_job_group(group);
422    }
423
424    if let Some(compat) = job.compatibility.clone() {
425        dimens.set_job_compatibility(compat);
426    }
427
428    if let Some(skills) = get_skills(&job.skills) {
429        dimens.set_job_skills(skills);
430    }
431}
432
433fn get_single_job(job: &ApiJob, single: Single) -> Job {
434    let mut single = single;
435    fill_dimens(job, &mut single.dimens);
436
437    Job::Single(Arc::new(single))
438}
439
440fn get_multi_job(job: &ApiJob, singles: Vec<Single>, deliveries_start_index: usize, random: &Arc<dyn Random>) -> Job {
441    let mut dimens: Dimensions = Default::default();
442    fill_dimens(job, &mut dimens);
443
444    let singles = singles.into_iter().map(Arc::new).collect::<Vec<_>>();
445
446    let multi = if singles.len() == 2 && deliveries_start_index == 1 {
447        Multi::new_shared(singles, dimens)
448    } else {
449        let jobs_len = singles.len();
450        Multi::new_shared_with_permutator(
451            singles,
452            dimens,
453            Box::new(VariableJobPermutation::new(
454                jobs_len,
455                deliveries_start_index,
456                MULTI_JOB_SAMPLE_SIZE,
457                random.clone(),
458            )),
459        )
460    };
461
462    Job::Multi(multi)
463}
464
465fn create_condition(vehicle_id: String, shift_index: usize) -> Arc<dyn Fn(&Actor) -> bool + Sync + Send> {
466    Arc::new(move |actor: &Actor| {
467        *actor.vehicle.dimens.get_vehicle_id().unwrap() == vehicle_id
468            && actor.vehicle.dimens.get_shift_index().copied().unwrap() == shift_index
469    })
470}
471
472fn get_skills(skills: &Option<ApiJobSkills>) -> Option<FeatureJobSkills> {
473    skills
474        .as_ref()
475        .map(|skills| FeatureJobSkills::new(skills.all_of.clone(), skills.one_of.clone(), skills.none_of.clone()))
476}
477
478fn empty() -> MultiDimLoad {
479    MultiDimLoad::default()
480}
481
482fn parse_times(times: &Option<Vec<Vec<String>>>) -> Vec<TimeSpan> {
483    times.as_ref().map_or(vec![TimeSpan::Window(TimeWindow::max())], |tws| {
484        tws.iter().map(|tw| TimeSpan::Window(parse_time_window(tw))).collect()
485    })
486}