balanced_vrp_clustering 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/balanced_vrp_clustering/ai4r_compat.rb +152 -0
- data/lib/balanced_vrp_clustering/rust_engine.rb +234 -0
- data/lib/balanced_vrp_clustering.rb +1188 -0
- data/lib/balanced_vrp_clustering_native.rb +39 -0
- data/lib/concerns/overloadable_functions.rb +64 -0
- data/lib/helpers/helper.rb +283 -0
- data/lib/helpers/hull.rb +188 -0
- data/rust/Cargo.lock +585 -0
- data/rust/Cargo.toml +3 -0
- data/rust/balanced_vrp_clustering_core/Cargo.toml +25 -0
- data/rust/balanced_vrp_clustering_core/src/clusterer.rs +1280 -0
- data/rust/balanced_vrp_clustering_core/src/compatibility.rs +78 -0
- data/rust/balanced_vrp_clustering_core/src/distance.rs +179 -0
- data/rust/balanced_vrp_clustering_core/src/lib.rs +29 -0
- data/rust/balanced_vrp_clustering_core/src/limits.rs +62 -0
- data/rust/balanced_vrp_clustering_core/src/local_speed.rs +99 -0
- data/rust/balanced_vrp_clustering_core/src/main.rs +46 -0
- data/rust/balanced_vrp_clustering_core/src/relations.rs +143 -0
- data/rust/balanced_vrp_clustering_core/src/rng.rs +50 -0
- data/rust/balanced_vrp_clustering_core/src/types.rs +234 -0
- data/rust/balanced_vrp_clustering_core/tests/integration.rs +71 -0
- data/rust/balanced_vrp_clustering_native/Cargo.toml +33 -0
- data/rust/balanced_vrp_clustering_native/extconf.rb +26 -0
- data/rust/balanced_vrp_clustering_native/src/lib.rs +57 -0
- metadata +102 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Copyright © Cartoway
|
|
2
|
+
//
|
|
3
|
+
// This file is part of balanced_vrp_clustering.
|
|
4
|
+
//
|
|
5
|
+
// Cartoway balanced_vrp_clustering is free software. You can redistribute it and/or
|
|
6
|
+
// modify since you respect the terms of the GNU Affero General
|
|
7
|
+
// Public License as published by the Free Software Foundation,
|
|
8
|
+
// either version 3 of the License, or (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// Cartoway Planner is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
12
|
+
// or FITNESS FOR A PARTICULAR PURPOSE. See the Licenses for more details.
|
|
13
|
+
//
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with Cartoway Planner. If not, see:
|
|
16
|
+
// <http://www.gnu.org/licenses/agpl.html>
|
|
17
|
+
//
|
|
18
|
+
|
|
19
|
+
use crate::types::{Centroid, Item, Vehicle};
|
|
20
|
+
|
|
21
|
+
pub fn compatible_characteristics(item: &Item, vehicle: &Vehicle) -> bool {
|
|
22
|
+
if !item.v_id.is_empty() && item.v_id.iter().all(|v| !vehicle.id.contains(v)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if item.skills.iter().any(|s| !vehicle.skills.contains(s)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if !item
|
|
29
|
+
.day_skills
|
|
30
|
+
.iter()
|
|
31
|
+
.any(|d| vehicle.day_skills.contains(d))
|
|
32
|
+
{
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub fn compatible_item_centroid(item: &Item, centroid: &Centroid) -> bool {
|
|
39
|
+
if !item.v_id.is_empty() && item.v_id.iter().all(|v| !centroid.v_id.contains(v)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if item.skills.iter().any(|s| !centroid.skills.contains(s)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if !item
|
|
46
|
+
.day_skills
|
|
47
|
+
.iter()
|
|
48
|
+
.any(|d| centroid.day_skills.contains(d))
|
|
49
|
+
{
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pub fn vehicle_as_centroid_props(vehicle: &Vehicle) -> Centroid {
|
|
56
|
+
Centroid {
|
|
57
|
+
lat: vehicle.depot_lat,
|
|
58
|
+
lon: vehicle.depot_lon,
|
|
59
|
+
id: vehicle.id.first().cloned().unwrap_or_default(),
|
|
60
|
+
load: Default::default(),
|
|
61
|
+
v_id: vehicle.id.clone(),
|
|
62
|
+
skills: vehicle.skills.clone(),
|
|
63
|
+
day_skills: vehicle.day_skills.clone(),
|
|
64
|
+
matrix_index: vehicle.depot_matrix_index,
|
|
65
|
+
duration_from_and_to_depot: 0.0,
|
|
66
|
+
capacity_offence_coeff: 0.0,
|
|
67
|
+
route_time: 0.0,
|
|
68
|
+
area: 0.0,
|
|
69
|
+
visit_count: 0.0,
|
|
70
|
+
visit_density: 0.0,
|
|
71
|
+
min_speed: 0.0,
|
|
72
|
+
max_speed: 0.0,
|
|
73
|
+
speed: 0.0,
|
|
74
|
+
total_work_days: vehicle.total_work_days,
|
|
75
|
+
vehicle_count: vehicle.vehicle_count,
|
|
76
|
+
capacities: vehicle.capacities.clone(),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Copyright © Cartoway
|
|
2
|
+
//
|
|
3
|
+
// This file is part of balanced_vrp_clustering.
|
|
4
|
+
//
|
|
5
|
+
// Cartoway balanced_vrp_clustering is free software. You can redistribute it and/or
|
|
6
|
+
// modify since you respect the terms of the GNU Affero General
|
|
7
|
+
// Public License as published by the Free Software Foundation,
|
|
8
|
+
// either version 3 of the License, or (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// Cartoway Planner is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
12
|
+
// or FITNESS FOR A PARTICULAR PURPOSE. See the Licenses for more details.
|
|
13
|
+
//
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with Cartoway Planner. If not, see:
|
|
16
|
+
// <http://www.gnu.org/licenses/agpl.html>
|
|
17
|
+
//
|
|
18
|
+
|
|
19
|
+
const R: f64 = 6_378_137.0;
|
|
20
|
+
const L: f64 = 111_321.0;
|
|
21
|
+
const RADIANS_IN_A_DEGREE: f64 = std::f64::consts::PI / 180.0;
|
|
22
|
+
|
|
23
|
+
pub fn deg2rad(degree: f64) -> f64 {
|
|
24
|
+
degree * RADIANS_IN_A_DEGREE
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pub fn euclidean_distance(loc_a: (f64, f64), loc_b: (f64, f64)) -> f64 {
|
|
28
|
+
if loc_a.0 == 0.0 && loc_b.0 == 0.0 {
|
|
29
|
+
return 0.0;
|
|
30
|
+
}
|
|
31
|
+
let delta_lat = loc_a.0 - loc_b.0;
|
|
32
|
+
let delta_lon = (loc_a.1 - loc_b.1) * deg2rad((loc_a.0 + loc_b.0) / 2.0);
|
|
33
|
+
L * (delta_lat * delta_lat + delta_lon * delta_lon).sqrt()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub fn flying_distance(loc_a: (f64, f64), loc_b: (f64, f64)) -> f64 {
|
|
37
|
+
if loc_a.0 == 0.0 || loc_b.0 == 0.0 {
|
|
38
|
+
return 0.0;
|
|
39
|
+
}
|
|
40
|
+
if (loc_a.0 - loc_b.0).abs() < 30.0
|
|
41
|
+
&& loc_a.0.abs().max(loc_b.0.abs()) + (loc_a.1 - loc_b.1).abs() < 100.0
|
|
42
|
+
{
|
|
43
|
+
return euclidean_distance(loc_a, loc_b);
|
|
44
|
+
}
|
|
45
|
+
let deg2rad_loc_a_lat = deg2rad(loc_a.0);
|
|
46
|
+
let deg2rad_loc_b_lat = deg2rad(loc_b.0);
|
|
47
|
+
let intermediate = ((deg2rad_loc_b_lat - deg2rad_loc_a_lat) / 2.0).sin().powi(2)
|
|
48
|
+
+ ((deg2rad(loc_b.1) - deg2rad(loc_a.1)) / 2.0).sin().powi(2)
|
|
49
|
+
* deg2rad_loc_b_lat.cos()
|
|
50
|
+
* deg2rad_loc_a_lat.cos();
|
|
51
|
+
R * 2.0 * (intermediate.sqrt()).atan2((1.0 - intermediate).sqrt())
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
pub fn matrix_distance(matrix: &[Vec<f64>], a_idx: usize, b_idx: usize) -> f64 {
|
|
55
|
+
let d = matrix[a_idx][b_idx];
|
|
56
|
+
let b_dist = matrix[b_idx][a_idx];
|
|
57
|
+
if d <= b_dist { d } else { b_dist }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pub fn compute_approximate_route_time(
|
|
61
|
+
area: f64,
|
|
62
|
+
visit_count: f64,
|
|
63
|
+
speed: f64,
|
|
64
|
+
total_work_days: f64,
|
|
65
|
+
) -> f64 {
|
|
66
|
+
let k0 = 0.765 * 1.45;
|
|
67
|
+
total_work_days * k0 * (area * visit_count / total_work_days.powf(1.5)).sqrt() / speed
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
pub fn approximate_polygon_area(coordinates: &[(f64, f64)]) -> f64 {
|
|
71
|
+
if coordinates.len() <= 2 {
|
|
72
|
+
return 0.0;
|
|
73
|
+
}
|
|
74
|
+
let mut area = 0.0;
|
|
75
|
+
let mut coor_p = coordinates[0];
|
|
76
|
+
for coor in &coordinates[1..] {
|
|
77
|
+
area += deg2rad(coor.1 - coor_p.1)
|
|
78
|
+
* (2.0 + deg2rad(coor_p.0).sin() + deg2rad(coor.0).sin());
|
|
79
|
+
coor_p = *coor;
|
|
80
|
+
}
|
|
81
|
+
(area * R * R / 2.0).abs()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pub fn position_and_distance_to_line(
|
|
85
|
+
l_begin: (f64, f64),
|
|
86
|
+
l_end: (f64, f64),
|
|
87
|
+
point: (f64, f64),
|
|
88
|
+
) -> (f64, f64) {
|
|
89
|
+
let lat_diff = l_end.0 - l_begin.0;
|
|
90
|
+
let lon_diff = l_end.1 - l_begin.1;
|
|
91
|
+
let position = lat_diff * (point.1 - l_begin.1) - lon_diff * (point.0 - l_begin.0);
|
|
92
|
+
let dist = position.abs() / (lat_diff * lat_diff + lon_diff * lon_diff).sqrt();
|
|
93
|
+
(position, dist)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
pub fn approximate_quadrilateral_polygon<F>(
|
|
97
|
+
coordinates: &[(f64, f64)],
|
|
98
|
+
mut rand_index: F,
|
|
99
|
+
) -> Vec<(f64, f64)>
|
|
100
|
+
where
|
|
101
|
+
F: FnMut(usize) -> usize,
|
|
102
|
+
{
|
|
103
|
+
if coordinates.is_empty() {
|
|
104
|
+
return vec![];
|
|
105
|
+
}
|
|
106
|
+
let crand_idx = rand_index(coordinates.len());
|
|
107
|
+
let crand = coordinates[crand_idx];
|
|
108
|
+
let c0 = coordinates
|
|
109
|
+
.iter()
|
|
110
|
+
.max_by(|a, b| {
|
|
111
|
+
flying_distance(crand, **a)
|
|
112
|
+
.partial_cmp(&flying_distance(crand, **b))
|
|
113
|
+
.unwrap()
|
|
114
|
+
})
|
|
115
|
+
.copied()
|
|
116
|
+
.unwrap();
|
|
117
|
+
let c1 = coordinates
|
|
118
|
+
.iter()
|
|
119
|
+
.max_by(|a, b| {
|
|
120
|
+
flying_distance(c0, **a)
|
|
121
|
+
.partial_cmp(&flying_distance(c0, **b))
|
|
122
|
+
.unwrap()
|
|
123
|
+
})
|
|
124
|
+
.copied()
|
|
125
|
+
.unwrap();
|
|
126
|
+
let c2 = coordinates
|
|
127
|
+
.iter()
|
|
128
|
+
.max_by(|a, b| {
|
|
129
|
+
flying_distance(c1, **a)
|
|
130
|
+
.partial_cmp(&flying_distance(c1, **b))
|
|
131
|
+
.unwrap()
|
|
132
|
+
})
|
|
133
|
+
.copied()
|
|
134
|
+
.unwrap();
|
|
135
|
+
let mut c3 = None;
|
|
136
|
+
let mut c4 = None;
|
|
137
|
+
let mut max_up = 0.0;
|
|
138
|
+
let mut max_down = 0.0;
|
|
139
|
+
for c_i in coordinates {
|
|
140
|
+
let (position, distance_to_line) =
|
|
141
|
+
position_and_distance_to_line(c1, c2, *c_i);
|
|
142
|
+
if position > 0.0 && distance_to_line > max_up {
|
|
143
|
+
max_up = distance_to_line;
|
|
144
|
+
c3 = Some(*c_i);
|
|
145
|
+
} else if position < 0.0 && distance_to_line > max_down {
|
|
146
|
+
max_down = distance_to_line;
|
|
147
|
+
c4 = Some(*c_i);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
let mut result = vec![c1];
|
|
151
|
+
if let Some(p) = c3 {
|
|
152
|
+
result.push(p);
|
|
153
|
+
}
|
|
154
|
+
result.push(c2);
|
|
155
|
+
if let Some(p) = c4 {
|
|
156
|
+
result.push(p);
|
|
157
|
+
}
|
|
158
|
+
result.push(c1);
|
|
159
|
+
result
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
pub fn check_if_projection_inside_the_line_segment(
|
|
163
|
+
point: (f64, f64),
|
|
164
|
+
line_beg: (f64, f64),
|
|
165
|
+
line_end: (f64, f64),
|
|
166
|
+
margin: f64,
|
|
167
|
+
) -> bool {
|
|
168
|
+
let line_direction = [line_end.0 - line_beg.0, line_end.1 - line_beg.1];
|
|
169
|
+
let point_direction = [point.0 - line_beg.0, point.1 - line_beg.1];
|
|
170
|
+
let denom: f64 = line_direction.iter().map(|x| x * x).sum();
|
|
171
|
+
if denom == 0.0 {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
let projection_scaler: f64 = (0..2)
|
|
175
|
+
.map(|i| line_direction[i] * point_direction[i])
|
|
176
|
+
.sum::<f64>()
|
|
177
|
+
/ denom;
|
|
178
|
+
projection_scaler >= 0.0 + 0.5 * margin && projection_scaler <= 1.0 - 0.5 * margin
|
|
179
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Copyright © Cartoway
|
|
2
|
+
//
|
|
3
|
+
// This file is part of balanced_vrp_clustering.
|
|
4
|
+
//
|
|
5
|
+
// Cartoway balanced_vrp_clustering is free software. You can redistribute it and/or
|
|
6
|
+
// modify since you respect the terms of the GNU Affero General
|
|
7
|
+
// Public License as published by the Free Software Foundation,
|
|
8
|
+
// either version 3 of the License, or (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// Cartoway Planner is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
12
|
+
// or FITNESS FOR A PARTICULAR PURPOSE. See the Licenses for more details.
|
|
13
|
+
//
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with Cartoway Planner. If not, see:
|
|
16
|
+
// <http://www.gnu.org/licenses/agpl.html>
|
|
17
|
+
//
|
|
18
|
+
|
|
19
|
+
pub mod clusterer;
|
|
20
|
+
pub mod compatibility;
|
|
21
|
+
pub mod distance;
|
|
22
|
+
pub mod limits;
|
|
23
|
+
pub mod local_speed;
|
|
24
|
+
pub mod relations;
|
|
25
|
+
pub mod rng;
|
|
26
|
+
pub mod types;
|
|
27
|
+
|
|
28
|
+
pub use clusterer::build_from_input;
|
|
29
|
+
pub use types::{BuildOutput, InputConfig};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Copyright © Cartoway
|
|
2
|
+
//
|
|
3
|
+
// This file is part of balanced_vrp_clustering.
|
|
4
|
+
//
|
|
5
|
+
// Cartoway balanced_vrp_clustering is free software. You can redistribute it and/or
|
|
6
|
+
// modify since you respect the terms of the GNU Affero General
|
|
7
|
+
// Public License as published by the Free Software Foundation,
|
|
8
|
+
// either version 3 of the License, or (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// Cartoway Planner is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
12
|
+
// or FITNESS FOR A PARTICULAR PURPOSE. See the Licenses for more details.
|
|
13
|
+
//
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with Cartoway Planner. If not, see:
|
|
16
|
+
// <http://www.gnu.org/licenses/agpl.html>
|
|
17
|
+
//
|
|
18
|
+
|
|
19
|
+
use crate::types::{CutLimit, Item, Vehicle};
|
|
20
|
+
use std::collections::HashMap;
|
|
21
|
+
|
|
22
|
+
pub fn compute_limits(
|
|
23
|
+
cut_symbol: Option<&str>,
|
|
24
|
+
cut_ratio: f64,
|
|
25
|
+
vehicles: &[Vehicle],
|
|
26
|
+
items: &[Item],
|
|
27
|
+
) -> (Vec<HashMap<String, f64>>, Vec<CutLimit>) {
|
|
28
|
+
let strict_limits: Vec<HashMap<String, f64>> = vehicles
|
|
29
|
+
.iter()
|
|
30
|
+
.map(|vehicle| {
|
|
31
|
+
let mut s_l = HashMap::new();
|
|
32
|
+
s_l.insert("duration".to_string(), vehicle.duration);
|
|
33
|
+
for (unit, limit) in &vehicle.capacities {
|
|
34
|
+
s_l.insert(unit.clone(), *limit);
|
|
35
|
+
}
|
|
36
|
+
s_l
|
|
37
|
+
})
|
|
38
|
+
.collect();
|
|
39
|
+
|
|
40
|
+
let mut metric_limits = vec![CutLimit { limit: 0.0 }; vehicles.len()];
|
|
41
|
+
|
|
42
|
+
if let Some(cut) = cut_symbol {
|
|
43
|
+
let mut total_quantity = 0.0;
|
|
44
|
+
for item in items {
|
|
45
|
+
total_quantity += item.quantities.get(cut).copied().unwrap_or(0.0);
|
|
46
|
+
}
|
|
47
|
+
let total_capacity: f64 = vehicles
|
|
48
|
+
.iter()
|
|
49
|
+
.map(|v| v.capacities.get(cut).copied().unwrap_or(0.0))
|
|
50
|
+
.sum();
|
|
51
|
+
for (i, vehicle) in vehicles.iter().enumerate() {
|
|
52
|
+
let vehicle_share = if total_capacity > 0.0 {
|
|
53
|
+
vehicle.capacities.get(cut).copied().unwrap_or(0.0) / total_capacity
|
|
54
|
+
} else {
|
|
55
|
+
1.0 / vehicles.len() as f64
|
|
56
|
+
};
|
|
57
|
+
metric_limits[i].limit = cut_ratio * total_quantity * vehicle_share;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
(strict_limits, metric_limits)
|
|
62
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Copyright © Cartoway
|
|
2
|
+
//
|
|
3
|
+
// This file is part of balanced_vrp_clustering.
|
|
4
|
+
//
|
|
5
|
+
// Cartoway balanced_vrp_clustering is free software. You can redistribute it and/or
|
|
6
|
+
// modify since you respect the terms of the GNU Affero General
|
|
7
|
+
// Public License as published by the Free Software Foundation,
|
|
8
|
+
// either version 3 of the License, or (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// Cartoway Planner is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
12
|
+
// or FITNESS FOR A PARTICULAR PURPOSE. See the Licenses for more details.
|
|
13
|
+
//
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with Cartoway Planner. If not, see:
|
|
16
|
+
// <http://www.gnu.org/licenses/agpl.html>
|
|
17
|
+
//
|
|
18
|
+
|
|
19
|
+
use crate::distance::flying_distance;
|
|
20
|
+
use crate::types::Item;
|
|
21
|
+
use std::collections::HashMap;
|
|
22
|
+
|
|
23
|
+
pub const LOCAL_SPEED_CELL_DEG: f64 = 0.01;
|
|
24
|
+
pub const LOCAL_SPEED_NEIGHBOR_LIMIT: usize = 100;
|
|
25
|
+
|
|
26
|
+
pub fn build_spatial_grid(items: &[Item]) -> HashMap<(i32, i32), Vec<usize>> {
|
|
27
|
+
let mut grid: HashMap<(i32, i32), Vec<usize>> = HashMap::new();
|
|
28
|
+
for (idx, item) in items.iter().enumerate() {
|
|
29
|
+
let key = (
|
|
30
|
+
(item.lat / LOCAL_SPEED_CELL_DEG) as i32,
|
|
31
|
+
(item.lon / LOCAL_SPEED_CELL_DEG) as i32,
|
|
32
|
+
);
|
|
33
|
+
grid.entry(key).or_default().push(idx);
|
|
34
|
+
}
|
|
35
|
+
grid
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub fn spatial_neighbors(item: &Item, grid: &HashMap<(i32, i32), Vec<usize>>) -> Vec<usize> {
|
|
39
|
+
let cx = (item.lat / LOCAL_SPEED_CELL_DEG) as i32;
|
|
40
|
+
let cy = (item.lon / LOCAL_SPEED_CELL_DEG) as i32;
|
|
41
|
+
let mut neighbors = Vec::new();
|
|
42
|
+
for dx in -1..=1 {
|
|
43
|
+
for dy in -1..=1 {
|
|
44
|
+
if let Some(bucket) = grid.get(&(cx + dx, cy + dy)) {
|
|
45
|
+
neighbors.extend(bucket.iter().copied());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
neighbors
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
pub fn calculate_local_speeds(items: &mut [Item]) {
|
|
53
|
+
let local_max_speed = 60.0 / 3.6;
|
|
54
|
+
let local_min_speed = 5.0 / 3.6;
|
|
55
|
+
let grid = build_spatial_grid(items);
|
|
56
|
+
|
|
57
|
+
for i in 0..items.len() {
|
|
58
|
+
if items[i].local_speed.is_some() {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
let depot_duration_a = items[i].duration_from_and_to_depot[0];
|
|
62
|
+
let a_lat = items[i].lat;
|
|
63
|
+
let a_lon = items[i].lon;
|
|
64
|
+
|
|
65
|
+
let mut candidates: Vec<(f64, usize)> = Vec::new();
|
|
66
|
+
for j in spatial_neighbors(&items[i], &grid) {
|
|
67
|
+
if i == j {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
let depot_diff = (depot_duration_a - items[j].duration_from_and_to_depot[0]).abs();
|
|
71
|
+
if depot_diff <= 1.0 {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
let flying_dist = flying_distance((a_lat, a_lon), (items[j].lat, items[j].lon));
|
|
75
|
+
if flying_dist <= 5.0 {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
candidates.push((flying_dist, j));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let calculated_speed = if candidates.is_empty() {
|
|
82
|
+
local_min_speed
|
|
83
|
+
} else {
|
|
84
|
+
candidates.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
|
85
|
+
let speeds: Vec<f64> = candidates
|
|
86
|
+
.iter()
|
|
87
|
+
.take(LOCAL_SPEED_NEIGHBOR_LIMIT)
|
|
88
|
+
.map(|(dist, j)| {
|
|
89
|
+
dist / (depot_duration_a - items[*j].duration_from_and_to_depot[0]).abs()
|
|
90
|
+
})
|
|
91
|
+
.collect();
|
|
92
|
+
let sum: f64 = speeds.iter().take(2).sum();
|
|
93
|
+
sum * 1.1
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
let speed = calculated_speed.max(local_min_speed).min(local_max_speed);
|
|
97
|
+
items[i].local_speed = Some(speed);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Copyright © Cartoway
|
|
2
|
+
//
|
|
3
|
+
// This file is part of balanced_vrp_clustering.
|
|
4
|
+
//
|
|
5
|
+
// Cartoway balanced_vrp_clustering is free software. You can redistribute it and/or
|
|
6
|
+
// modify since you respect the terms of the GNU Affero General
|
|
7
|
+
// Public License as published by the Free Software Foundation,
|
|
8
|
+
// either version 3 of the License, or (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// Cartoway Planner is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
12
|
+
// or FITNESS FOR A PARTICULAR PURPOSE. See the Licenses for more details.
|
|
13
|
+
//
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with Cartoway Planner. If not, see:
|
|
16
|
+
// <http://www.gnu.org/licenses/agpl.html>
|
|
17
|
+
//
|
|
18
|
+
|
|
19
|
+
use balanced_vrp_clustering_core::{build_from_input, InputConfig};
|
|
20
|
+
use clap::Parser;
|
|
21
|
+
use std::fs;
|
|
22
|
+
use std::path::PathBuf;
|
|
23
|
+
|
|
24
|
+
#[derive(Parser)]
|
|
25
|
+
#[command(name = "bvrp-cluster")]
|
|
26
|
+
#[command(about = "Balanced VRP clustering (Rust core)")]
|
|
27
|
+
struct Cli {
|
|
28
|
+
#[arg(short, long)]
|
|
29
|
+
input: PathBuf,
|
|
30
|
+
|
|
31
|
+
#[arg(short, long)]
|
|
32
|
+
output: Option<PathBuf>,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn main() {
|
|
36
|
+
let cli = Cli::parse();
|
|
37
|
+
let json = fs::read_to_string(&cli.input).expect("read input");
|
|
38
|
+
let input: InputConfig = serde_json::from_str(&json).expect("parse input");
|
|
39
|
+
let result = build_from_input(&input);
|
|
40
|
+
let out_json = serde_json::to_string_pretty(&result).expect("serialize output");
|
|
41
|
+
if let Some(path) = cli.output {
|
|
42
|
+
fs::write(path, out_json).expect("write output");
|
|
43
|
+
} else {
|
|
44
|
+
println!("{out_json}");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Copyright © Cartoway
|
|
2
|
+
//
|
|
3
|
+
// This file is part of balanced_vrp_clustering.
|
|
4
|
+
//
|
|
5
|
+
// Cartoway balanced_vrp_clustering is free software. You can redistribute it and/or
|
|
6
|
+
// modify since you respect the terms of the GNU Affero General
|
|
7
|
+
// Public License as published by the Free Software Foundation,
|
|
8
|
+
// either version 3 of the License, or (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// Cartoway Planner is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
12
|
+
// or FITNESS FOR A PARTICULAR PURPOSE. See the Licenses for more details.
|
|
13
|
+
//
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with Cartoway Planner. If not, see:
|
|
16
|
+
// <http://www.gnu.org/licenses/agpl.html>
|
|
17
|
+
//
|
|
18
|
+
|
|
19
|
+
use crate::types::Item;
|
|
20
|
+
use std::collections::HashMap;
|
|
21
|
+
|
|
22
|
+
const LINKING_RELATIONS: &[&str] = &[
|
|
23
|
+
"order",
|
|
24
|
+
"same_route",
|
|
25
|
+
"sequence",
|
|
26
|
+
"same_vehicle",
|
|
27
|
+
"shipment",
|
|
28
|
+
];
|
|
29
|
+
const BINDING_RELATIONS: &[&str] = &["order", "same_route", "sequence", "same_vehicle"];
|
|
30
|
+
|
|
31
|
+
pub fn connect_linked_items(
|
|
32
|
+
items: &mut [Item],
|
|
33
|
+
related_item_indices: &HashMap<String, Vec<Vec<usize>>>,
|
|
34
|
+
) {
|
|
35
|
+
for relation in LINKING_RELATIONS {
|
|
36
|
+
if let Some(groups) = related_item_indices.get(*relation) {
|
|
37
|
+
for linked_indices in groups {
|
|
38
|
+
assert_eq!(
|
|
39
|
+
linked_indices.len(),
|
|
40
|
+
linked_indices.iter().collect::<std::collections::HashSet<_>>().len(),
|
|
41
|
+
"Each relation group should contain only unique indices"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let non_binding: Vec<&str> = LINKING_RELATIONS
|
|
48
|
+
.iter()
|
|
49
|
+
.copied()
|
|
50
|
+
.filter(|r| !BINDING_RELATIONS.contains(r))
|
|
51
|
+
.collect();
|
|
52
|
+
|
|
53
|
+
for relation in non_binding {
|
|
54
|
+
if let Some(groups) = related_item_indices.get(relation) {
|
|
55
|
+
for mut linked_indices in groups.clone() {
|
|
56
|
+
if linked_indices
|
|
57
|
+
.iter()
|
|
58
|
+
.any(|&ind| items[ind].linked_next.is_some())
|
|
59
|
+
{
|
|
60
|
+
panic!("A service should not appear in multiple non-binding linking relations");
|
|
61
|
+
}
|
|
62
|
+
linked_indices.push(linked_indices[0]);
|
|
63
|
+
for i in 0..linked_indices.len() - 1 {
|
|
64
|
+
let current = linked_indices[i];
|
|
65
|
+
let next = linked_indices[i + 1];
|
|
66
|
+
items[current].linked_next = Some(next);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for relation in BINDING_RELATIONS {
|
|
73
|
+
if let Some(groups) = related_item_indices.get(*relation) {
|
|
74
|
+
for mut linked_indices in groups.clone() {
|
|
75
|
+
linked_indices.push(linked_indices[0]);
|
|
76
|
+
for i in 0..linked_indices.len() - 1 {
|
|
77
|
+
let mut item_idx = linked_indices[i];
|
|
78
|
+
let mut next_idx = linked_indices[i + 1];
|
|
79
|
+
let item_has = items[item_idx].linked_next.is_some();
|
|
80
|
+
let next_has = items[next_idx].linked_next.is_some();
|
|
81
|
+
|
|
82
|
+
if !item_has && !next_has {
|
|
83
|
+
items[item_idx].linked_next = Some(next_idx);
|
|
84
|
+
} else if item_has && next_has {
|
|
85
|
+
let first_loop_end = item_idx;
|
|
86
|
+
let mut walk = items[item_idx].linked_next.unwrap();
|
|
87
|
+
while walk != first_loop_end && walk != next_idx {
|
|
88
|
+
item_idx = walk;
|
|
89
|
+
walk = items[item_idx].linked_next.unwrap();
|
|
90
|
+
}
|
|
91
|
+
if items[item_idx].linked_next == Some(next_idx) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
let second_loop_end = next_idx;
|
|
95
|
+
let mut walk2 = items[next_idx].linked_next.unwrap();
|
|
96
|
+
while walk2 != second_loop_end {
|
|
97
|
+
next_idx = walk2;
|
|
98
|
+
walk2 = items[next_idx].linked_next.unwrap();
|
|
99
|
+
}
|
|
100
|
+
items[item_idx].linked_next = Some(second_loop_end);
|
|
101
|
+
items[next_idx].linked_next = Some(first_loop_end);
|
|
102
|
+
} else {
|
|
103
|
+
if item_has {
|
|
104
|
+
std::mem::swap(&mut item_idx, &mut next_idx);
|
|
105
|
+
}
|
|
106
|
+
items[item_idx].linked_next = Some(next_idx);
|
|
107
|
+
let loop_end = next_idx;
|
|
108
|
+
let mut walk = items[next_idx].linked_next.unwrap();
|
|
109
|
+
while walk != loop_end {
|
|
110
|
+
next_idx = walk;
|
|
111
|
+
walk = items[next_idx].linked_next.unwrap();
|
|
112
|
+
}
|
|
113
|
+
if next_idx != item_idx {
|
|
114
|
+
items[next_idx].linked_next = Some(item_idx);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Mirrors Ruby `do_forall_linked_items_of`
|
|
124
|
+
pub fn do_forall_linked<F>(items: &[Item], start: usize, mut f: F)
|
|
125
|
+
where
|
|
126
|
+
F: FnMut(usize),
|
|
127
|
+
{
|
|
128
|
+
let mut linked_item: Option<usize> = None;
|
|
129
|
+
loop {
|
|
130
|
+
if linked_item == Some(start) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
let current = if let Some(li) = linked_item {
|
|
134
|
+
items[li].linked_next.unwrap_or(start)
|
|
135
|
+
} else if let Some(next) = items[start].linked_next {
|
|
136
|
+
next
|
|
137
|
+
} else {
|
|
138
|
+
start
|
|
139
|
+
};
|
|
140
|
+
f(current);
|
|
141
|
+
linked_item = Some(current);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Copyright © Cartoway
|
|
2
|
+
//
|
|
3
|
+
// This file is part of balanced_vrp_clustering.
|
|
4
|
+
//
|
|
5
|
+
// Cartoway balanced_vrp_clustering is free software. You can redistribute it and/or
|
|
6
|
+
// modify since you respect the terms of the GNU Affero General
|
|
7
|
+
// Public License as published by the Free Software Foundation,
|
|
8
|
+
// either version 3 of the License, or (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// Cartoway Planner is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
12
|
+
// or FITNESS FOR A PARTICULAR PURPOSE. See the Licenses for more details.
|
|
13
|
+
//
|
|
14
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
// along with Cartoway Planner. If not, see:
|
|
16
|
+
// <http://www.gnu.org/licenses/agpl.html>
|
|
17
|
+
//
|
|
18
|
+
|
|
19
|
+
use rand::rngs::StdRng;
|
|
20
|
+
use rand::{Rng, SeedableRng};
|
|
21
|
+
|
|
22
|
+
/// Seeded RNG for clustering (deterministic per seed).
|
|
23
|
+
pub struct RubyRng {
|
|
24
|
+
inner: StdRng,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl RubyRng {
|
|
28
|
+
pub fn new(seed: u64) -> Self {
|
|
29
|
+
Self {
|
|
30
|
+
inner: StdRng::seed_from_u64(seed),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Ruby `rand` without args: float in [0, 1)
|
|
35
|
+
pub fn rand_f64(&mut self) -> f64 {
|
|
36
|
+
self.inner.gen::<f64>()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Ruby `rand(max)` for positive integer max: 0..max
|
|
40
|
+
pub fn rand_usize(&mut self, max: usize) -> usize {
|
|
41
|
+
if max == 0 {
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
(self.rand_f64() * max as f64) as usize % max
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub fn rand_range_f64(&mut self, min: f64, max: f64) -> f64 {
|
|
48
|
+
min + self.rand_f64() * (max - min)
|
|
49
|
+
}
|
|
50
|
+
}
|