vrp_core/models/problem/
jobs.rs

1#[cfg(test)]
2#[path = "../../../tests/unit/models/problem/jobs_test.rs"]
3mod jobs_test;
4
5use crate::construction::clustering::dbscan::create_job_clusters;
6use crate::models::common::*;
7use crate::models::problem::{Costs, Fleet, TransportCost};
8use crate::utils::{short_type_name, Either};
9use rosomaxa::prelude::{Float, GenericResult, InfoLogger};
10use rosomaxa::utils::{parallel_collect, Timer};
11use std::cmp::Ordering::Less;
12use std::collections::{HashMap, HashSet};
13use std::fmt::{Debug, Formatter};
14use std::hash::{Hash, Hasher};
15use std::sync::{Arc, Weak};
16
17custom_dimension!(JobId typeof String);
18
19/// Represents a job variant.
20#[derive(Clone)]
21pub enum Job {
22    /// Single job.
23    Single(Arc<Single>),
24    /// MultiJob with multiple dependent jobs.
25    Multi(Arc<Multi>),
26}
27
28impl Job {
29    /// Considers job as [`Single`].
30    pub fn as_single(&self) -> Option<&Arc<Single>> {
31        match &self {
32            Job::Single(job) => Some(job),
33            _ => None,
34        }
35    }
36
37    /// Considers job as [`Single`]. Panics if it is [`Multi`].
38    pub fn to_single(&self) -> &Arc<Single> {
39        self.as_single().expect("Unexpected job type: multi")
40    }
41
42    /// Considers job as [`Multi`].
43    pub fn as_multi(&self) -> Option<&Arc<Multi>> {
44        match &self {
45            Job::Multi(job) => Some(job),
46            _ => None,
47        }
48    }
49
50    /// Considers job as [`Multi`]. Panics if it is [`Multi`]
51    pub fn to_multi(&self) -> &Arc<Multi> {
52        self.as_multi().expect("Unexpected job type: single")
53    }
54
55    /// Returns dimensions collection.
56    pub fn dimens(&self) -> &Dimensions {
57        match &self {
58            Job::Single(single) => &single.dimens,
59            Job::Multi(multi) => &multi.dimens,
60        }
61    }
62
63    /// Get all places from the job.
64    pub fn places(&self) -> impl Iterator<Item = &Place> + '_ {
65        match &self {
66            Job::Single(single) => Either::Left(single.places.iter()),
67            Job::Multi(multi) => Either::Right(multi.jobs.iter().flat_map(|single| single.places.iter())),
68        }
69    }
70}
71
72impl Debug for Job {
73    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
74        match self {
75            Job::Single(single) => single.fmt(f),
76            Job::Multi(multi) => multi.fmt(f),
77        }
78    }
79}
80
81/// Represents a job place details where and/or when work has to be performed.
82#[derive(Clone)]
83pub struct Place {
84    /// Location where work has to be performed.
85    pub location: Option<Location>,
86    /// Time has to be spend performing work.
87    pub duration: Duration,
88    /// Time data which specifies when work can be started.
89    pub times: Vec<TimeSpan>,
90}
91
92/// Represents a job which should be performed once but actual place/time might vary.
93pub struct Single {
94    /// Specifies job details: where and when it can be performed.
95    pub places: Vec<Place>,
96    /// Dimensions which contains extra work requirements.
97    pub dimens: Dimensions,
98}
99
100impl Debug for Single {
101    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
102        f.debug_struct(short_type_name::<Single>())
103            .field("id", &self.dimens.get_job_id().map(|id| id.as_str()).unwrap_or("undef"))
104            .finish_non_exhaustive()
105    }
106}
107
108/// Represents a job which consists of multiple sub jobs.
109/// All of these jobs must be performed or none of them. Order can be controlled
110/// via specific dimension value.
111pub struct Multi {
112    /// A list of jobs which must be performed.
113    pub jobs: Vec<Arc<Single>>,
114    /// Dimensions which contains extra work requirements.
115    pub dimens: Dimensions,
116    /// Permutation generator.
117    permutator: Box<dyn JobPermutation>,
118}
119
120impl Debug for Multi {
121    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
122        f.debug_struct(short_type_name::<Multi>())
123            .field("id", &self.dimens.get_job_id().map(|id| id.as_str()).unwrap_or("undef"))
124            .field("jobs", &self.jobs.len())
125            .finish_non_exhaustive()
126    }
127}
128
129/// Defines a trait to work with multi job's permutations. Essentially, it specifies valid combinations
130/// of sub-jobs inside multi-job.
131pub trait JobPermutation: Send + Sync {
132    // TODO fix all implementations to support returning reference
133    /// Returns a valid permutation.
134    fn get(&self) -> Vec<Vec<usize>>;
135
136    /// Validates given permutation.
137    fn validate(&self, permutation: &[usize]) -> bool;
138}
139
140/// Specifies job permutation generator which allows only fixed set of permutations.
141pub struct FixedJobPermutation {
142    permutations: Vec<Vec<usize>>,
143}
144
145impl FixedJobPermutation {
146    /// Creates a new instance of `StrictJobPermutation`.
147    pub fn new(permutations: Vec<Vec<usize>>) -> Self {
148        Self { permutations }
149    }
150}
151
152impl JobPermutation for FixedJobPermutation {
153    fn get(&self) -> Vec<Vec<usize>> {
154        self.permutations.clone()
155    }
156
157    fn validate(&self, permutation: &[usize]) -> bool {
158        self.permutations
159            .iter()
160            .any(|prm| prm.len() == permutation.len() && prm.iter().zip(permutation.iter()).all(|(&a, &b)| a == b))
161    }
162}
163
164impl Multi {
165    /// Creates a new multi job from given 'dimens' and `jobs` assuming that jobs has to be
166    /// inserted in order they specified.
167    pub fn new_shared(jobs: Vec<Arc<Single>>, dimens: Dimensions) -> Arc<Self> {
168        let permutations = vec![(0..jobs.len()).collect()];
169        Self::bind(Self { jobs, dimens, permutator: Box::new(FixedJobPermutation::new(permutations)) })
170    }
171
172    /// Creates a new multi job from given 'dimens' and `jobs` using `permutator` to control insertion order.
173    pub fn new_shared_with_permutator(
174        jobs: Vec<Arc<Single>>,
175        dimens: Dimensions,
176        permutator: Box<dyn JobPermutation>,
177    ) -> Arc<Self> {
178        Self::bind(Self { jobs, dimens, permutator })
179    }
180
181    /// Returns all sub-jobs permutations.
182    pub fn permutations(&self) -> Vec<Vec<Arc<Single>>> {
183        self.permutator
184            .get()
185            .iter()
186            .map(|perm| perm.iter().map(|&i| self.jobs.get(i).unwrap().clone()).collect())
187            .collect()
188    }
189
190    /// Validates given set of permutations.
191    pub fn validate(&self, permutations: &[usize]) -> bool {
192        self.permutator.validate(permutations)
193    }
194
195    /// Returns parent multi job for given sub-job.
196    pub fn roots(single: &Single) -> Option<Arc<Multi>> {
197        single.dimens.get_value::<JobLink, Weak<Multi>>().and_then(|w| w.upgrade())
198    }
199
200    /// Wraps given multi job into [`Arc`] adding reference to it from all sub-jobs.
201    fn bind(mut multi: Self) -> Arc<Self> {
202        Arc::new_cyclic(|weak_multi| {
203            multi.jobs.iter_mut().for_each(|single| {
204                Arc::get_mut(single)
205                    .expect("Single from Multi should not be shared before binding")
206                    .dimens
207                    .set_value::<JobLink, _>(weak_multi.clone());
208            });
209
210            multi
211        })
212    }
213}
214
215/// A private type to get/set link between multi job and its children single jobs.
216struct JobLink;
217
218/// Floating type wit less precision, but lower impact on memory footprint.
219type LowPrecisionCost = f32;
220type JobIndex = HashMap<Job, (Vec<(Job, LowPrecisionCost)>, LowPrecisionCost)>;
221
222// TODO: we don't know actual departure and zero-cost when we create job index.
223const DEFAULT_COST: LowPrecisionCost = 0.;
224
225/// A big enough value to mark unreachable cost.
226const UNREACHABLE_COST: LowPrecisionCost = f32::MAX;
227
228/// Maximum amount of job's neighbours stored in index. We restrict this side to lower impact on
229/// memory footprint. It is unlikely that more than 100 neighbours needed to be processed in reality,
230/// but we keep it 2x times more.
231const MAX_NEIGHBOURS: usize = 256;
232
233/// Stores all jobs taking into account their neighborhood.
234pub struct Jobs {
235    jobs: Vec<Job>,
236    index: HashMap<usize, JobIndex>,
237    clusters: Vec<HashSet<Job>>,
238}
239
240impl Jobs {
241    /// Creates a new instance of [`Jobs`].
242    pub fn new(
243        fleet: &Fleet,
244        jobs: Vec<Job>,
245        transport: &(dyn TransportCost),
246        logger: &InfoLogger,
247    ) -> GenericResult<Jobs> {
248        let index = create_index(fleet, jobs.clone(), transport, logger);
249        let clusters =
250            create_job_clusters(&jobs, fleet, Some(3), None, |profile, job| neighbors(&index, profile, job))?;
251
252        Ok(Jobs { jobs, index, clusters })
253    }
254
255    /// Returns all jobs in the original order as a slice.
256    pub fn all(&self) -> &[Job] {
257        &self.jobs
258    }
259
260    /// Returns range of jobs "near" to given one. Near is defined by costs with relation
261    /// transport profile and departure time.
262    pub fn neighbors(&self, profile: &Profile, job: &Job, _: Timestamp) -> impl Iterator<Item = (&Job, Cost)> {
263        neighbors(&self.index, profile, job)
264    }
265
266    /// Returns job clusters based on their neighborhood approximation.
267    pub fn clusters(&self) -> &[HashSet<Job>] {
268        &self.clusters
269    }
270
271    /// Returns job rank as relative cost from any vehicle's start position.
272    /// Returns `None` if a job is not found in index.
273    pub fn rank(&self, profile: &Profile, job: &Job) -> Option<Cost> {
274        self.index.get(&profile.index).and_then(|index| index.get(job)).map(|(_, cost)| *cost as Cost)
275    }
276
277    /// Returns number of jobs.
278    pub fn size(&self) -> usize {
279        self.jobs.len()
280    }
281}
282
283impl PartialEq<Job> for Job {
284    fn eq(&self, other: &Job) -> bool {
285        match (&self, other) {
286            (Job::Single(_), Job::Multi(_)) | (Job::Multi(_), Job::Single(_)) => false,
287            (Job::Single(lhs), Job::Single(rhs)) => Arc::ptr_eq(lhs, rhs),
288            (Job::Multi(lhs), Job::Multi(rhs)) => Arc::ptr_eq(lhs, rhs),
289        }
290    }
291}
292
293impl Eq for Job {}
294
295impl Hash for Job {
296    fn hash<H: Hasher>(&self, state: &mut H) {
297        match self {
298            Job::Single(single) => {
299                Arc::as_ptr(single).hash(state);
300            }
301            Job::Multi(multi) => {
302                Arc::as_ptr(multi).hash(state);
303            }
304        }
305    }
306}
307
308fn neighbors<'a>(
309    index: &'a HashMap<usize, JobIndex>,
310    profile: &Profile,
311    job: &Job,
312) -> impl Iterator<Item = (&'a Job, Cost)> {
313    index
314        .get(&profile.index)
315        .and_then(|index| index.get(job))
316        .into_iter()
317        .flat_map(|(info, _)| info.iter().map(|(job, cost)| (job, *cost as Float)))
318}
319
320/// Returns job locations.
321pub fn get_job_locations(job: &Job) -> impl Iterator<Item = Option<Location>> + '_ {
322    match job {
323        Job::Single(single) => Either::Left(single.places.iter().map(|p| p.location)),
324        Job::Multi(multi) => Either::Right(multi.jobs.iter().flat_map(|j| j.places.iter().map(|p| p.location))),
325    }
326}
327
328/// Creates job index.
329fn create_index(
330    fleet: &Fleet,
331    jobs: Vec<Job>,
332    transport: &(dyn TransportCost),
333    logger: &InfoLogger,
334) -> HashMap<usize, JobIndex> {
335    let avg_profile_costs = get_avg_profile_costs(fleet);
336
337    Timer::measure_duration_with_callback(
338        || {
339            fleet.profiles.iter().fold(HashMap::new(), |mut acc, profile| {
340                let avg_costs = avg_profile_costs.get(&profile.index).unwrap();
341                // get all possible start positions for given profile
342                let starts: Vec<Location> = fleet
343                    .vehicles
344                    .iter()
345                    .filter(|v| v.profile.index == profile.index)
346                    .flat_map(|v| v.details.iter().map(|d| d.start.as_ref().map(|s| s.location)))
347                    .flatten()
348                    .collect();
349
350                // create job index
351                let item = parallel_collect(&jobs, |job| {
352                    let mut sorted_job_costs: Vec<(Job, LowPrecisionCost)> = jobs
353                        .iter()
354                        .filter(|j| **j != *job)
355                        .map(|j| (j.clone(), get_cost_between_jobs(profile, avg_costs, transport, job, j)))
356                        .collect();
357                    sorted_job_costs.sort_unstable_by(|(_, a), (_, b)| a.total_cmp(b));
358
359                    sorted_job_costs.truncate(MAX_NEIGHBOURS);
360                    sorted_job_costs.shrink_to_fit();
361
362                    let fleet_costs = starts
363                        .iter()
364                        .cloned()
365                        .map(|s| get_cost_between_job_and_location(profile, avg_costs, transport, job, s))
366                        .min_by(|a, b| a.partial_cmp(b).unwrap_or(Less))
367                        .unwrap_or(DEFAULT_COST);
368
369                    (job.clone(), (sorted_job_costs, fleet_costs))
370                })
371                .into_iter()
372                .collect::<HashMap<_, _>>();
373
374                acc.insert(profile.index, item);
375                acc
376            })
377        },
378        |duration| (logger)(format!("job index created in {}ms", duration.as_millis()).as_str()),
379    )
380}
381
382fn get_cost_between_locations(
383    profile: &Profile,
384    costs: &Costs,
385    transport: &(dyn TransportCost),
386    from: Location,
387    to: Location,
388) -> LowPrecisionCost {
389    let distance = transport.distance_approx(profile, from, to);
390    let duration = transport.duration_approx(profile, from, to);
391
392    if distance < 0. || duration < 0. {
393        // NOTE this happens if matrix uses negative values as a marker of unreachable location
394        UNREACHABLE_COST
395    } else {
396        (distance * costs.per_distance + duration * costs.per_driving_time) as LowPrecisionCost
397    }
398}
399
400/// Returns min cost between job and location.
401fn get_cost_between_job_and_location(
402    profile: &Profile,
403    costs: &Costs,
404    transport: &(dyn TransportCost),
405    job: &Job,
406    to: Location,
407) -> LowPrecisionCost {
408    get_job_locations(job)
409        .flatten()
410        .map(|from| get_cost_between_locations(profile, costs, transport, from, to))
411        .min_by(|a, b| a.partial_cmp(b).unwrap_or(Less))
412        .unwrap_or(UNREACHABLE_COST)
413}
414
415/// Returns minimal cost between jobs.
416fn get_cost_between_jobs(
417    profile: &Profile,
418    costs: &Costs,
419    transport: &(dyn TransportCost),
420    lhs: &Job,
421    rhs: &Job,
422) -> LowPrecisionCost {
423    let outer: Vec<Option<Location>> = get_job_locations(lhs).collect();
424    let inner: Vec<Option<Location>> = get_job_locations(rhs).collect();
425
426    let routing_cost = outer
427        .iter()
428        .flat_map(|o| inner.iter().map(move |i| (*o, *i)))
429        .map(|pair| match pair {
430            (Some(from), Some(to)) => get_cost_between_locations(profile, costs, transport, from, to),
431            _ => DEFAULT_COST,
432        })
433        .min_by(|a, b| a.total_cmp(b))
434        .unwrap_or(DEFAULT_COST);
435
436    // NOTE: ignore time window difference costs as it is hard to balance with routing costs
437
438    routing_cost
439}
440
441fn get_avg_profile_costs(fleet: &Fleet) -> HashMap<usize, Costs> {
442    let get_avg_by = |costs: &Vec<Costs>, map_cost_fn: fn(&Costs) -> Float| -> Float {
443        costs.iter().map(map_cost_fn).sum::<Float>() / (costs.len() as Float)
444    };
445    fleet
446        .vehicles
447        .iter()
448        .fold(HashMap::<_, Vec<_>>::new(), |mut acc, vehicle| {
449            acc.entry(vehicle.profile.index).or_default().push(vehicle.costs.clone());
450            acc
451        })
452        .iter()
453        .map(|(&profile_idx, costs)| {
454            (
455                profile_idx,
456                Costs {
457                    fixed: get_avg_by(costs, |c| c.fixed),
458                    per_distance: get_avg_by(costs, |c| c.per_distance),
459                    per_driving_time: get_avg_by(costs, |c| c.per_driving_time),
460                    per_waiting_time: get_avg_by(costs, |c| c.per_waiting_time),
461                    per_service_time: get_avg_by(costs, |c| c.per_service_time),
462                },
463            )
464        })
465        .collect()
466}