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,1280 @@
|
|
|
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::compatibility::{compatible_item_centroid, vehicle_as_centroid_props};
|
|
20
|
+
use crate::distance::{
|
|
21
|
+
approximate_polygon_area, approximate_quadrilateral_polygon, check_if_projection_inside_the_line_segment,
|
|
22
|
+
compute_approximate_route_time, euclidean_distance, flying_distance, matrix_distance,
|
|
23
|
+
};
|
|
24
|
+
use crate::limits::compute_limits;
|
|
25
|
+
use crate::local_speed::calculate_local_speeds;
|
|
26
|
+
use crate::relations::{connect_linked_items, do_forall_linked};
|
|
27
|
+
use crate::rng::RubyRng;
|
|
28
|
+
use crate::types::{
|
|
29
|
+
BuildOutput, Centroid, CentroidWeights, CutLimit, InputConfig, Item, Vehicle,
|
|
30
|
+
INCOMPATIBILITY_DISTANCE_PENALTY,
|
|
31
|
+
};
|
|
32
|
+
use std::collections::HashMap;
|
|
33
|
+
use std::time::Instant;
|
|
34
|
+
|
|
35
|
+
pub struct Clusterer {
|
|
36
|
+
pub rng: RubyRng,
|
|
37
|
+
pub vehicles: Vec<Vehicle>,
|
|
38
|
+
pub items: Vec<Item>,
|
|
39
|
+
pub item_order: Vec<usize>,
|
|
40
|
+
pub distance_matrix: Option<Vec<Vec<f64>>>,
|
|
41
|
+
pub cut_symbol: Option<String>,
|
|
42
|
+
pub cut_ratio: f64,
|
|
43
|
+
pub max_iterations: usize,
|
|
44
|
+
pub centroid_indices: Vec<usize>,
|
|
45
|
+
pub related_item_indices: HashMap<String, Vec<Vec<usize>>>,
|
|
46
|
+
|
|
47
|
+
pub number_of_clusters: usize,
|
|
48
|
+
pub unit_symbols: Vec<String>,
|
|
49
|
+
pub iteration: usize,
|
|
50
|
+
pub centroids: Vec<Centroid>,
|
|
51
|
+
pub clusters: Vec<Vec<usize>>,
|
|
52
|
+
pub strict_limitations: Vec<HashMap<String, f64>>,
|
|
53
|
+
pub cut_limit: Vec<CutLimit>,
|
|
54
|
+
pub balance_coeff: Vec<f64>,
|
|
55
|
+
pub cluster_load: Vec<HashMap<String, f64>>,
|
|
56
|
+
pub evaluate_distance_buffer: Vec<f64>,
|
|
57
|
+
pub expected_n_visits: f64,
|
|
58
|
+
pub total_cut_load: f64,
|
|
59
|
+
pub approximate_total_route_time: f64,
|
|
60
|
+
pub density_cap: f64,
|
|
61
|
+
pub max_balance_violation: f64,
|
|
62
|
+
pub limit_violation_count: usize,
|
|
63
|
+
pub needs_to_stay_at_the_top: usize,
|
|
64
|
+
pub old_centroids_lat_lon: Vec<(f64, f64)>,
|
|
65
|
+
pub last_n_average_diffs: Vec<f64>,
|
|
66
|
+
items_with_limit_violation: Vec<LimitViolationRecord>,
|
|
67
|
+
pub clusters_with_limit_violation: Vec<Vec<usize>>,
|
|
68
|
+
pub already_assigned: Vec<bool>,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[derive(Clone)]
|
|
72
|
+
struct LimitViolationRecord {
|
|
73
|
+
item_index: usize,
|
|
74
|
+
diff: f64,
|
|
75
|
+
ratio: f64,
|
|
76
|
+
closest_cluster: usize,
|
|
77
|
+
closest_wo_violation: usize,
|
|
78
|
+
min_with_violation: f64,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
impl Clusterer {
|
|
82
|
+
pub fn from_input(input: &InputConfig) -> Self {
|
|
83
|
+
let vehicles: Vec<Vehicle> = input
|
|
84
|
+
.vehicles
|
|
85
|
+
.iter()
|
|
86
|
+
.map(|v| {
|
|
87
|
+
let coords = v.depot.coordinates.as_ref();
|
|
88
|
+
Vehicle {
|
|
89
|
+
id: v.id.clone(),
|
|
90
|
+
depot_lat: coords.map(|c| c[0]).unwrap_or(0.0),
|
|
91
|
+
depot_lon: coords.map(|c| c[1]).unwrap_or(0.0),
|
|
92
|
+
depot_matrix_index: v.depot.matrix_index,
|
|
93
|
+
capacities: v.capacities.clone(),
|
|
94
|
+
skills: v.skills.clone(),
|
|
95
|
+
day_skills: if v.day_skills.is_empty() {
|
|
96
|
+
default_day_skills()
|
|
97
|
+
} else {
|
|
98
|
+
v.day_skills.clone()
|
|
99
|
+
},
|
|
100
|
+
duration: v.duration,
|
|
101
|
+
total_work_days: v.total_work_days,
|
|
102
|
+
vehicle_count: v.vehicle_count,
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
.collect();
|
|
106
|
+
|
|
107
|
+
let k = vehicles.len();
|
|
108
|
+
let items: Vec<Item> = input
|
|
109
|
+
.items
|
|
110
|
+
.iter()
|
|
111
|
+
.map(|it| {
|
|
112
|
+
let mut durations = it.duration_from_and_to_depot.clone();
|
|
113
|
+
if durations.len() < k {
|
|
114
|
+
durations.resize(k, 0.0);
|
|
115
|
+
}
|
|
116
|
+
Item {
|
|
117
|
+
id: it.id.clone(),
|
|
118
|
+
lat: it.lat,
|
|
119
|
+
lon: it.lon,
|
|
120
|
+
quantities: it.quantities.clone(),
|
|
121
|
+
v_id: it.v_id.clone(),
|
|
122
|
+
skills: it.skills.clone(),
|
|
123
|
+
day_skills: if it.day_skills.is_empty() {
|
|
124
|
+
default_day_skills()
|
|
125
|
+
} else {
|
|
126
|
+
it.day_skills.clone()
|
|
127
|
+
},
|
|
128
|
+
matrix_index: it.matrix_index,
|
|
129
|
+
duration_from_and_to_depot: durations,
|
|
130
|
+
linked_next: None,
|
|
131
|
+
centroid_weights: CentroidWeights {
|
|
132
|
+
limit: vec![1.0; k],
|
|
133
|
+
compatibility: 1.0,
|
|
134
|
+
},
|
|
135
|
+
needs_to_stay_at_the_top: false,
|
|
136
|
+
moved_up: false,
|
|
137
|
+
moved_down: false,
|
|
138
|
+
weighted_visit_distance: 0.0,
|
|
139
|
+
local_speed: None,
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
.collect();
|
|
143
|
+
|
|
144
|
+
let item_order: Vec<usize> = (0..items.len()).collect();
|
|
145
|
+
let max_iterations = if input.max_iterations > 0 {
|
|
146
|
+
input.max_iterations
|
|
147
|
+
} else {
|
|
148
|
+
((0.5 * items.len() as f64) as usize).max(100)
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
Self {
|
|
152
|
+
rng: RubyRng::new(input.seed),
|
|
153
|
+
vehicles,
|
|
154
|
+
items,
|
|
155
|
+
item_order,
|
|
156
|
+
distance_matrix: input.distance_matrix.clone(),
|
|
157
|
+
cut_symbol: input.cut_symbol.clone(),
|
|
158
|
+
cut_ratio: input.cut_ratio,
|
|
159
|
+
max_iterations,
|
|
160
|
+
centroid_indices: input.centroid_indices.clone(),
|
|
161
|
+
related_item_indices: input.related_item_indices.clone(),
|
|
162
|
+
number_of_clusters: k,
|
|
163
|
+
unit_symbols: Vec::new(),
|
|
164
|
+
iteration: 0,
|
|
165
|
+
centroids: Vec::new(),
|
|
166
|
+
clusters: vec![Vec::new(); k],
|
|
167
|
+
strict_limitations: Vec::new(),
|
|
168
|
+
cut_limit: Vec::new(),
|
|
169
|
+
balance_coeff: vec![1.0; k],
|
|
170
|
+
cluster_load: vec![HashMap::new(); k],
|
|
171
|
+
evaluate_distance_buffer: vec![0.0; k],
|
|
172
|
+
expected_n_visits: 0.0,
|
|
173
|
+
total_cut_load: 0.0,
|
|
174
|
+
approximate_total_route_time: 0.0,
|
|
175
|
+
density_cap: f64::EPSILON,
|
|
176
|
+
max_balance_violation: 0.0,
|
|
177
|
+
limit_violation_count: 0,
|
|
178
|
+
needs_to_stay_at_the_top: 0,
|
|
179
|
+
old_centroids_lat_lon: Vec::new(),
|
|
180
|
+
last_n_average_diffs: Vec::new(),
|
|
181
|
+
items_with_limit_violation: Vec::new(),
|
|
182
|
+
clusters_with_limit_violation: vec![Vec::new(); k],
|
|
183
|
+
already_assigned: Vec::new(),
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
pub fn build(mut self) -> BuildOutput {
|
|
188
|
+
let started = Instant::now();
|
|
189
|
+
connect_linked_items(&mut self.items, &self.related_item_indices);
|
|
190
|
+
|
|
191
|
+
self.init_unit_symbols();
|
|
192
|
+
self.init_item_defaults();
|
|
193
|
+
self.init_compatibility_weights();
|
|
194
|
+
let (strict, cut_lim) = compute_limits(
|
|
195
|
+
self.cut_symbol.as_deref(),
|
|
196
|
+
self.cut_ratio,
|
|
197
|
+
&self.vehicles,
|
|
198
|
+
&self.items,
|
|
199
|
+
);
|
|
200
|
+
self.strict_limitations = strict;
|
|
201
|
+
self.cut_limit = cut_lim;
|
|
202
|
+
|
|
203
|
+
self.calc_initial_centroids();
|
|
204
|
+
self.prepare_balancing();
|
|
205
|
+
|
|
206
|
+
self.mark_items_at_top();
|
|
207
|
+
|
|
208
|
+
while !self.stop_criteria_met() && self.iteration < self.max_iterations {
|
|
209
|
+
self.calculate_membership_clusters();
|
|
210
|
+
self.update_balance_coefficients();
|
|
211
|
+
self.recompute_centroids();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let clusters: Vec<Vec<String>> = self
|
|
215
|
+
.clusters
|
|
216
|
+
.iter()
|
|
217
|
+
.map(|c| c.iter().map(|&i| self.items[i].id.clone()).collect())
|
|
218
|
+
.collect();
|
|
219
|
+
|
|
220
|
+
BuildOutput {
|
|
221
|
+
iterations: self.iteration,
|
|
222
|
+
clusters,
|
|
223
|
+
elapsed_ms: started.elapsed().as_millis() as u64,
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fn init_unit_symbols(&mut self) {
|
|
228
|
+
let mut symbols = self.cut_symbol.clone().into_iter().collect::<Vec<_>>();
|
|
229
|
+
for v in &self.vehicles {
|
|
230
|
+
for unit in v.capacities.keys() {
|
|
231
|
+
if !symbols.contains(unit) {
|
|
232
|
+
symbols.push(unit.clone());
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
self.unit_symbols = symbols;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
fn init_item_defaults(&mut self) {
|
|
240
|
+
for item in &mut self.items {
|
|
241
|
+
if item.quantities.get("visits").is_none() {
|
|
242
|
+
item.quantities.insert("visits".to_string(), 1.0);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
for vehicle in &mut self.vehicles {
|
|
246
|
+
if vehicle.capacities.is_empty() {
|
|
247
|
+
vehicle
|
|
248
|
+
.capacities
|
|
249
|
+
.insert("duration".to_string(), vehicle.duration);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
fn init_compatibility_weights(&mut self) {
|
|
255
|
+
let k = self.number_of_clusters;
|
|
256
|
+
let total_visits: f64 = self.items.iter().map(|i| i.quantities.get("visits").copied().unwrap_or(1.0)).sum();
|
|
257
|
+
self.expected_n_visits = total_visits / k as f64;
|
|
258
|
+
|
|
259
|
+
let mut groups: HashMap<String, Vec<usize>> = HashMap::new();
|
|
260
|
+
for (idx, item) in self.items.iter().enumerate() {
|
|
261
|
+
let key = format!(
|
|
262
|
+
"{:?}|{:?}|{:?}",
|
|
263
|
+
item.v_id, item.skills, item.day_skills
|
|
264
|
+
);
|
|
265
|
+
groups.entry(key).or_default().push(idx);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
for indices in groups.values() {
|
|
269
|
+
let item = &self.items[indices[0]];
|
|
270
|
+
let mut compatibility: Vec<u8> = Vec::new();
|
|
271
|
+
for vehicle in &self.vehicles {
|
|
272
|
+
let c = vehicle_as_centroid_props(vehicle);
|
|
273
|
+
compatibility.push(if compatible_item_centroid(item, &c) { 1 } else { 0 });
|
|
274
|
+
}
|
|
275
|
+
let compatible_vehicle_count = compatibility.iter().map(|&x| x as usize).sum::<usize>().max(1);
|
|
276
|
+
let incompatible_vehicle_count = k - compatible_vehicle_count;
|
|
277
|
+
let compat_weight = (self.expected_n_visits
|
|
278
|
+
.powf(
|
|
279
|
+
(1.0
|
|
280
|
+
+ ((incompatible_vehicle_count as f64 / k as f64) + 0.1).ln())
|
|
281
|
+
/ (1.0 + 1.1_f64.ln()),
|
|
282
|
+
))
|
|
283
|
+
/ (indices.len() as f64 / compatible_vehicle_count as f64).max(1.0);
|
|
284
|
+
let weight = compat_weight.ceil();
|
|
285
|
+
for &idx in indices {
|
|
286
|
+
self.items[idx].centroid_weights.compatibility = weight;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
fn prepare_balancing(&mut self) {
|
|
292
|
+
let Some(ref cut) = self.cut_symbol.clone() else {
|
|
293
|
+
return;
|
|
294
|
+
};
|
|
295
|
+
let cut = cut.clone();
|
|
296
|
+
self.total_cut_load = self
|
|
297
|
+
.items
|
|
298
|
+
.iter()
|
|
299
|
+
.map(|i| i.quantities.get(&cut).copied().unwrap_or(0.0))
|
|
300
|
+
.sum();
|
|
301
|
+
|
|
302
|
+
if self.total_cut_load == 0.0 {
|
|
303
|
+
self.cut_symbol = None;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let centroid_len = self.centroids.len();
|
|
308
|
+
let data_length = self.item_order.len();
|
|
309
|
+
let tail_start = centroid_len.min(data_length);
|
|
310
|
+
let tail_end = data_length;
|
|
311
|
+
if tail_start < tail_end {
|
|
312
|
+
let mut tail: Vec<usize> = self.item_order[tail_start..tail_end].to_vec();
|
|
313
|
+
tail.sort_by(|&a, &b| {
|
|
314
|
+
let qa = self.items[b].quantities.get(&cut).copied().unwrap_or(0.0);
|
|
315
|
+
let qb = self.items[a].quantities.get(&cut).copied().unwrap_or(0.0);
|
|
316
|
+
qa.partial_cmp(&qb).unwrap()
|
|
317
|
+
});
|
|
318
|
+
let range_end = (data_length as f64 * 0.9) as usize;
|
|
319
|
+
let range_begin = (centroid_len + (data_length as f64 * 0.1) as usize).min(range_end);
|
|
320
|
+
if range_begin < range_end {
|
|
321
|
+
let slice = &mut tail[range_begin - tail_start..range_end - tail_start];
|
|
322
|
+
self.shuffle_slice(slice);
|
|
323
|
+
}
|
|
324
|
+
self.item_order[tail_start..tail_end].copy_from_slice(&tail);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
calculate_local_speeds(&mut self.items);
|
|
328
|
+
|
|
329
|
+
let coords: Vec<(f64, f64)> = self
|
|
330
|
+
.items
|
|
331
|
+
.iter()
|
|
332
|
+
.map(|i| (i.lat, i.lon))
|
|
333
|
+
.collect();
|
|
334
|
+
let quad = approximate_quadrilateral_polygon(&coords, |max| self.rng.rand_usize(max));
|
|
335
|
+
let area_per_cluster =
|
|
336
|
+
approximate_polygon_area(&quad) / self.vehicles.len() as f64;
|
|
337
|
+
let speed: f64 = self
|
|
338
|
+
.items
|
|
339
|
+
.iter()
|
|
340
|
+
.filter_map(|i| i.local_speed)
|
|
341
|
+
.sum::<f64>()
|
|
342
|
+
/ self.items.len() as f64;
|
|
343
|
+
let visit_count_per_cluster: f64 = self
|
|
344
|
+
.items
|
|
345
|
+
.iter()
|
|
346
|
+
.map(|i| i.quantities.get("visits").copied().unwrap_or(1.0))
|
|
347
|
+
.sum::<f64>()
|
|
348
|
+
/ self.vehicles.len() as f64;
|
|
349
|
+
let total_work_days_per_cluster: f64 = self
|
|
350
|
+
.vehicles
|
|
351
|
+
.iter()
|
|
352
|
+
.map(|v| v.total_work_days)
|
|
353
|
+
.sum::<f64>()
|
|
354
|
+
/ self.vehicles.len() as f64;
|
|
355
|
+
|
|
356
|
+
self.approximate_total_route_time = compute_approximate_route_time(
|
|
357
|
+
area_per_cluster,
|
|
358
|
+
visit_count_per_cluster,
|
|
359
|
+
speed,
|
|
360
|
+
total_work_days_per_cluster,
|
|
361
|
+
) * self.vehicles.len() as f64;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fn shuffle_slice(&mut self, slice: &mut [usize]) {
|
|
365
|
+
for i in (1..slice.len()).rev() {
|
|
366
|
+
let j = self.rng.rand_usize(i + 1);
|
|
367
|
+
slice.swap(i, j);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
fn mark_items_at_top(&mut self) {
|
|
372
|
+
for item in &mut self.items {
|
|
373
|
+
item.needs_to_stay_at_the_top = false;
|
|
374
|
+
}
|
|
375
|
+
let units: Vec<String> = self
|
|
376
|
+
.vehicles
|
|
377
|
+
.iter()
|
|
378
|
+
.flat_map(|v| v.capacities.keys().cloned())
|
|
379
|
+
.collect::<std::collections::HashSet<_>>()
|
|
380
|
+
.into_iter()
|
|
381
|
+
.collect();
|
|
382
|
+
|
|
383
|
+
let top_count = (self.items.len() as f64 * 0.005 + 1.0) as usize;
|
|
384
|
+
for unit in units {
|
|
385
|
+
let mut ranked: Vec<usize> = (0..self.items.len()).collect();
|
|
386
|
+
ranked.sort_by(|&a, &b| {
|
|
387
|
+
let va = self.items[b].quantities.get(&unit).copied().unwrap_or(0.0);
|
|
388
|
+
let vb = self.items[a].quantities.get(&unit).copied().unwrap_or(0.0);
|
|
389
|
+
va.partial_cmp(&vb).unwrap()
|
|
390
|
+
});
|
|
391
|
+
for &idx in ranked.iter().take(top_count) {
|
|
392
|
+
if self.items[idx].quantities.get(&unit).copied().unwrap_or(0.0) > 0.0 {
|
|
393
|
+
self.items[idx].needs_to_stay_at_the_top = true;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
self.needs_to_stay_at_the_top = self.items.iter().filter(|i| i.needs_to_stay_at_the_top).count();
|
|
398
|
+
let mut top_items: Vec<usize> = self
|
|
399
|
+
.item_order
|
|
400
|
+
.iter()
|
|
401
|
+
.copied()
|
|
402
|
+
.filter(|&i| self.items[i].needs_to_stay_at_the_top)
|
|
403
|
+
.collect();
|
|
404
|
+
let rest: Vec<usize> = self
|
|
405
|
+
.item_order
|
|
406
|
+
.iter()
|
|
407
|
+
.copied()
|
|
408
|
+
.filter(|&i| !self.items[i].needs_to_stay_at_the_top)
|
|
409
|
+
.collect();
|
|
410
|
+
top_items.extend(rest);
|
|
411
|
+
self.item_order = top_items;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
fn distance_between(&self, item_idx: usize, centroid: &Centroid) -> f64 {
|
|
415
|
+
let item = &self.items[item_idx];
|
|
416
|
+
if let Some(ref matrix) = self.distance_matrix {
|
|
417
|
+
let a_idx = item.matrix_index.unwrap();
|
|
418
|
+
let b_idx = centroid.matrix_index.unwrap();
|
|
419
|
+
matrix_distance(matrix, a_idx, b_idx)
|
|
420
|
+
} else {
|
|
421
|
+
flying_distance((item.lat, item.lon), (centroid.lat, centroid.lon))
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
fn distance(&self, item_idx: usize, cluster_index: usize) -> f64 {
|
|
426
|
+
let centroid = &self.centroids[cluster_index];
|
|
427
|
+
let mut total_dist = 0.0;
|
|
428
|
+
do_forall_linked(&self.items, item_idx, |linked_idx| {
|
|
429
|
+
total_dist += self.distance_between(linked_idx, centroid);
|
|
430
|
+
});
|
|
431
|
+
total_dist * self.balance_coeff[cluster_index]
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
fn get_min_index(distances: &[f64]) -> usize {
|
|
435
|
+
distances
|
|
436
|
+
.iter()
|
|
437
|
+
.enumerate()
|
|
438
|
+
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
|
|
439
|
+
.map(|(i, _)| i)
|
|
440
|
+
.unwrap_or(0)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
fn capacity_violation(&self, item_idx: usize, cluster_index: usize) -> bool {
|
|
444
|
+
for (unit, &limit) in &self.strict_limitations[cluster_index] {
|
|
445
|
+
// Match Ruby: `next unless @strict_limitations[cluster_index][unit]` (0 is truthy in Ruby)
|
|
446
|
+
let added = self.linked_quantity(item_idx, unit);
|
|
447
|
+
let load = self.cluster_load[cluster_index].get(unit).copied().unwrap_or(0.0);
|
|
448
|
+
if load + added > limit {
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
false
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
fn linked_quantity(&self, item_idx: usize, unit: &str) -> f64 {
|
|
456
|
+
let mut total = 0.0;
|
|
457
|
+
do_forall_linked(&self.items, item_idx, |idx| {
|
|
458
|
+
total += self.items[idx].quantities.get(unit).copied().unwrap_or(0.0);
|
|
459
|
+
});
|
|
460
|
+
total
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
fn evaluate(&mut self, item_idx: usize) -> usize {
|
|
464
|
+
let k = self.number_of_clusters;
|
|
465
|
+
let mut distances = vec![0.0; k];
|
|
466
|
+
for i in 0..k {
|
|
467
|
+
let mut dist = self.distance(item_idx, i);
|
|
468
|
+
if !compatible_item_centroid(&self.items[item_idx], &self.centroids[i]) {
|
|
469
|
+
dist += INCOMPATIBILITY_DISTANCE_PENALTY;
|
|
470
|
+
}
|
|
471
|
+
distances[i] = dist;
|
|
472
|
+
}
|
|
473
|
+
self.evaluate_distance_buffer.copy_from_slice(&distances);
|
|
474
|
+
let closest_cluster_index = Self::get_min_index(&distances);
|
|
475
|
+
|
|
476
|
+
if self.capacity_violation(item_idx, closest_cluster_index) {
|
|
477
|
+
let mut min_wo = INCOMPATIBILITY_DISTANCE_PENALTY;
|
|
478
|
+
let mut closest_wo = None;
|
|
479
|
+
for k in 0..self.number_of_clusters {
|
|
480
|
+
if distances[k] < min_wo && !self.capacity_violation(item_idx, k) {
|
|
481
|
+
closest_wo = Some(k);
|
|
482
|
+
min_wo = distances[k];
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if let Some(wo_idx) = closest_wo {
|
|
486
|
+
let min_with = distances.iter().copied().fold(f64::INFINITY, f64::min);
|
|
487
|
+
let mut ratio = min_wo / min_with;
|
|
488
|
+
if ratio.is_nan() {
|
|
489
|
+
ratio = 1.0;
|
|
490
|
+
}
|
|
491
|
+
self.items_with_limit_violation.push(LimitViolationRecord {
|
|
492
|
+
item_index: item_idx,
|
|
493
|
+
diff: min_wo - min_with,
|
|
494
|
+
ratio,
|
|
495
|
+
closest_cluster: closest_cluster_index,
|
|
496
|
+
closest_wo_violation: wo_idx,
|
|
497
|
+
min_with_violation: min_with,
|
|
498
|
+
});
|
|
499
|
+
self.clusters_with_limit_violation[closest_cluster_index].push(wo_idx);
|
|
500
|
+
return wo_idx;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
closest_cluster_index
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
fn calculate_membership_clusters(&mut self) {
|
|
507
|
+
self.update_strict_duration_limitation();
|
|
508
|
+
self.items_with_limit_violation.clear();
|
|
509
|
+
for c in &mut self.centroids {
|
|
510
|
+
c.load.clear();
|
|
511
|
+
}
|
|
512
|
+
for load in &mut self.cluster_load {
|
|
513
|
+
load.clear();
|
|
514
|
+
}
|
|
515
|
+
self.clusters = vec![Vec::new(); self.number_of_clusters];
|
|
516
|
+
self.already_assigned = vec![false; self.items.len()];
|
|
517
|
+
|
|
518
|
+
let order = self.item_order.clone();
|
|
519
|
+
for item_idx in order {
|
|
520
|
+
if self.already_assigned[item_idx] {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
let cluster_index = self.evaluate(item_idx);
|
|
524
|
+
let mut linked = Vec::new();
|
|
525
|
+
do_forall_linked(&self.items, item_idx, |linked_idx| linked.push(linked_idx));
|
|
526
|
+
for linked_idx in linked {
|
|
527
|
+
self.assign_item(linked_idx, cluster_index);
|
|
528
|
+
self.update_metrics(linked_idx, cluster_index);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
self.manage_empty_clusters();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
fn assign_item(&mut self, item_idx: usize, cluster_index: usize) {
|
|
535
|
+
self.already_assigned[item_idx] = true;
|
|
536
|
+
if !self.clusters[cluster_index].contains(&item_idx) {
|
|
537
|
+
self.clusters[cluster_index].push(item_idx);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
fn update_metrics(&mut self, item_idx: usize, cluster_index: usize) {
|
|
542
|
+
for (unit, &value) in &self.items[item_idx].quantities {
|
|
543
|
+
let q = value;
|
|
544
|
+
*self.centroids[cluster_index].load.entry(unit.clone()).or_insert(0.0) += q;
|
|
545
|
+
*self.cluster_load[cluster_index].entry(unit.clone()).or_insert(0.0) += q;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
fn update_strict_duration_limitation(&mut self) {
|
|
550
|
+
if self.cut_symbol.is_none() {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
for (index, centroid) in self.centroids.iter_mut().enumerate() {
|
|
554
|
+
if let Some(limit) = self.strict_limitations[index].get("duration").copied() {
|
|
555
|
+
self.strict_limitations[index].insert(
|
|
556
|
+
"duration".to_string(),
|
|
557
|
+
limit - centroid.duration_from_and_to_depot * self.vehicles[index].total_work_days,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
fn calc_initial_centroids(&mut self) {
|
|
564
|
+
self.centroids.clear();
|
|
565
|
+
self.old_centroids_lat_lon.clear();
|
|
566
|
+
let mut remaining_skills: Vec<Vehicle> = self.vehicles.clone();
|
|
567
|
+
|
|
568
|
+
if self.centroid_indices.is_empty() {
|
|
569
|
+
while self.centroids.len() < self.number_of_clusters {
|
|
570
|
+
let skills = remaining_skills.remove(0);
|
|
571
|
+
let mut available: Vec<usize> = self.item_order.clone();
|
|
572
|
+
let vehicle_centroid = vehicle_as_centroid_props(&skills);
|
|
573
|
+
let mut compatible = self.compatible_items_multi_depot(
|
|
574
|
+
available
|
|
575
|
+
.iter()
|
|
576
|
+
.copied()
|
|
577
|
+
.filter(|&idx| {
|
|
578
|
+
let item = &self.items[idx];
|
|
579
|
+
!item.v_id.is_empty() || !item.skills.is_empty()
|
|
580
|
+
})
|
|
581
|
+
.filter(|&idx| compatible_item_centroid(&self.items[idx], &vehicle_centroid))
|
|
582
|
+
.collect(),
|
|
583
|
+
);
|
|
584
|
+
if compatible.is_empty() {
|
|
585
|
+
compatible = self.compatible_items_multi_depot(
|
|
586
|
+
available
|
|
587
|
+
.iter()
|
|
588
|
+
.copied()
|
|
589
|
+
.filter(|&idx| compatible_item_centroid(&self.items[idx], &vehicle_centroid))
|
|
590
|
+
.collect(),
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
if compatible.is_empty() {
|
|
594
|
+
compatible = self.compatible_items_multi_depot(available.iter().copied().collect());
|
|
595
|
+
}
|
|
596
|
+
if compatible.is_empty() {
|
|
597
|
+
compatible = self.compatible_items_multi_depot((0..self.items.len()).collect());
|
|
598
|
+
}
|
|
599
|
+
let pick = compatible[self.rng.rand_usize(compatible.len())];
|
|
600
|
+
let item = &self.items[pick];
|
|
601
|
+
let mut centroid = vehicle_as_centroid_props(&skills);
|
|
602
|
+
centroid.lat = item.lat;
|
|
603
|
+
centroid.lon = item.lon;
|
|
604
|
+
centroid.id = item.id.clone();
|
|
605
|
+
centroid.matrix_index = item.matrix_index;
|
|
606
|
+
centroid.duration_from_and_to_depot =
|
|
607
|
+
item.duration_from_and_to_depot[self.centroids.len()];
|
|
608
|
+
self.centroids.push(centroid);
|
|
609
|
+
let mut to_remove = Vec::new();
|
|
610
|
+
do_forall_linked(&self.items, pick, |li| to_remove.push(li));
|
|
611
|
+
available.retain(|i| !to_remove.contains(i));
|
|
612
|
+
self.move_item_to_front(pick);
|
|
613
|
+
}
|
|
614
|
+
} else {
|
|
615
|
+
let mut insert_at_beginning = Vec::new();
|
|
616
|
+
for (ind, &index) in self.centroid_indices.iter().enumerate() {
|
|
617
|
+
let skills = remaining_skills.remove(0);
|
|
618
|
+
let item = &self.items[index];
|
|
619
|
+
do_forall_linked(&self.items, index, |li| {
|
|
620
|
+
if insert_at_beginning.contains(&li) {
|
|
621
|
+
panic!("Centroid linked conflict");
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
let vehicle_centroid = vehicle_as_centroid_props(&skills);
|
|
625
|
+
if !compatible_item_centroid(item, &vehicle_centroid) {
|
|
626
|
+
panic!("Incompatible centroid init");
|
|
627
|
+
}
|
|
628
|
+
let mut centroid = vehicle_centroid;
|
|
629
|
+
centroid.lat = item.lat;
|
|
630
|
+
centroid.lon = item.lon;
|
|
631
|
+
centroid.id = item.id.clone();
|
|
632
|
+
centroid.matrix_index = item.matrix_index;
|
|
633
|
+
centroid.duration_from_and_to_depot = item.duration_from_and_to_depot[ind];
|
|
634
|
+
self.centroids.push(centroid);
|
|
635
|
+
insert_at_beginning.push(index);
|
|
636
|
+
}
|
|
637
|
+
for idx in insert_at_beginning {
|
|
638
|
+
self.move_item_to_front(idx);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
self.number_of_clusters = self.centroids.len();
|
|
642
|
+
for c in &mut self.centroids {
|
|
643
|
+
c.capacity_offence_coeff = 0.0;
|
|
644
|
+
c.route_time = 0.0;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
fn move_item_to_front(&mut self, item_idx: usize) {
|
|
649
|
+
if let Some(pos) = self.item_order.iter().position(|&i| i == item_idx) {
|
|
650
|
+
self.item_order.remove(pos);
|
|
651
|
+
self.item_order.insert(0, item_idx);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
fn compatible_items_multi_depot(&self, items: Vec<usize>) -> Vec<usize> {
|
|
656
|
+
let mut closest_items = Vec::new();
|
|
657
|
+
for margin in 0..self.vehicles.len() {
|
|
658
|
+
let c_len = self.centroids.len();
|
|
659
|
+
closest_items = items
|
|
660
|
+
.iter()
|
|
661
|
+
.copied()
|
|
662
|
+
.filter(|&idx| {
|
|
663
|
+
let d = &self.items[idx].duration_from_and_to_depot;
|
|
664
|
+
let mut sorted: Vec<f64> = d.to_vec();
|
|
665
|
+
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
666
|
+
d[c_len] <= sorted[margin.min(sorted.len() - 1)]
|
|
667
|
+
})
|
|
668
|
+
.collect();
|
|
669
|
+
if !closest_items.is_empty() {
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
closest_items
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
fn move_limit_violating_dataitems(&mut self) {
|
|
677
|
+
self.limit_violation_count = self.items_with_limit_violation.len();
|
|
678
|
+
if self.items_with_limit_violation.is_empty() {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
let mean_distance_diff: f64 = self.items_with_limit_violation.iter().map(|d| d.diff).sum::<f64>()
|
|
682
|
+
/ self.limit_violation_count as f64;
|
|
683
|
+
let mean_ratio: f64 = self.items_with_limit_violation.iter().map(|d| d.ratio).sum::<f64>()
|
|
684
|
+
/ self.limit_violation_count as f64;
|
|
685
|
+
|
|
686
|
+
self.items_with_limit_violation
|
|
687
|
+
.sort_by(|a, b| b.min_with_violation.partial_cmp(&a.min_with_violation).unwrap());
|
|
688
|
+
|
|
689
|
+
while let Some(data) = self.items_with_limit_violation.pop() {
|
|
690
|
+
let item_idx = data.item_index;
|
|
691
|
+
if !self.items[item_idx].needs_to_stay_at_the_top
|
|
692
|
+
&& (data.ratio > (2.0 * mean_ratio).max(5.0)
|
|
693
|
+
|| self.rng.rand_f64() < (data.diff / (3.0 * mean_distance_diff + 1e-10)))
|
|
694
|
+
{
|
|
695
|
+
let point = (self.items[item_idx].lat, self.items[item_idx].lon);
|
|
696
|
+
let centroid_violation = (
|
|
697
|
+
self.centroids[data.closest_cluster].lat,
|
|
698
|
+
self.centroids[data.closest_cluster].lon,
|
|
699
|
+
);
|
|
700
|
+
let centroid_ok = (
|
|
701
|
+
self.centroids[data.closest_wo_violation].lat,
|
|
702
|
+
self.centroids[data.closest_wo_violation].lon,
|
|
703
|
+
);
|
|
704
|
+
if check_if_projection_inside_the_line_segment(point, centroid_violation, centroid_ok, 0.1)
|
|
705
|
+
{
|
|
706
|
+
let max_w = (self.expected_n_visits / 10.0).max(5.0);
|
|
707
|
+
for w in &mut self.items[item_idx].centroid_weights.limit {
|
|
708
|
+
*w = (*w * 2.0).min(max_w);
|
|
709
|
+
}
|
|
710
|
+
self.items[item_idx].centroid_weights.limit[data.closest_cluster] = 1.0;
|
|
711
|
+
self.items[item_idx].moved_down = true;
|
|
712
|
+
if let Some(pos) = self.item_order.iter().position(|&i| i == item_idx) {
|
|
713
|
+
self.item_order.remove(pos);
|
|
714
|
+
self.item_order.push(item_idx);
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
self.items[item_idx].moved_up = true;
|
|
718
|
+
if let Some(pos) = self.item_order.iter().position(|&i| i == item_idx) {
|
|
719
|
+
self.item_order.remove(pos);
|
|
720
|
+
self.item_order.insert(self.needs_to_stay_at_the_top, item_idx);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
fn recompute_centroids(&mut self) {
|
|
728
|
+
self.move_limit_violating_dataitems();
|
|
729
|
+
self.old_centroids_lat_lon = self
|
|
730
|
+
.centroids
|
|
731
|
+
.iter()
|
|
732
|
+
.map(|c| (c.lat, c.lon))
|
|
733
|
+
.collect();
|
|
734
|
+
let smoothing = 0.1 + 0.9 * (self.iteration as f64 / self.max_iterations as f64).powf(0.5);
|
|
735
|
+
|
|
736
|
+
for index in 0..self.number_of_clusters {
|
|
737
|
+
if self.clusters[index].is_empty() {
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
let centroid = &self.centroids[index];
|
|
741
|
+
let c_lat = centroid.lat;
|
|
742
|
+
let c_lon = centroid.lon;
|
|
743
|
+
let mut total_weighted = 0.0;
|
|
744
|
+
let cluster_items: Vec<usize> = self.clusters[index].clone();
|
|
745
|
+
|
|
746
|
+
for &item_idx in &cluster_items {
|
|
747
|
+
let item = &mut self.items[item_idx];
|
|
748
|
+
let distance_weight = if item.moved_up {
|
|
749
|
+
0.1
|
|
750
|
+
} else if item.moved_down {
|
|
751
|
+
10.0
|
|
752
|
+
} else {
|
|
753
|
+
1.0
|
|
754
|
+
};
|
|
755
|
+
let w = distance_weight
|
|
756
|
+
* (flying_distance((c_lat, c_lon), (item.lat, item.lon)) + 1.0).powf(0.2)
|
|
757
|
+
* item.quantities.get("visits").copied().unwrap_or(1.0).powf(0.2)
|
|
758
|
+
* item.centroid_weights.compatibility
|
|
759
|
+
* item.centroid_weights.limit[index];
|
|
760
|
+
item.weighted_visit_distance = w;
|
|
761
|
+
item.moved_up = false;
|
|
762
|
+
item.moved_down = false;
|
|
763
|
+
total_weighted += w;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
{
|
|
767
|
+
let centroid = &mut self.centroids[index];
|
|
768
|
+
let sum_lat: f64 = cluster_items
|
|
769
|
+
.iter()
|
|
770
|
+
.map(|&i| self.items[i].lat * self.items[i].weighted_visit_distance)
|
|
771
|
+
.sum();
|
|
772
|
+
let sum_lon: f64 = cluster_items
|
|
773
|
+
.iter()
|
|
774
|
+
.map(|&i| self.items[i].lon * self.items[i].weighted_visit_distance)
|
|
775
|
+
.sum();
|
|
776
|
+
centroid.lat = smoothing * centroid.lat + (1.0 - smoothing) * sum_lat / total_weighted;
|
|
777
|
+
centroid.lon = smoothing * centroid.lon + (1.0 - smoothing) * sum_lon / total_weighted;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
let c_lat = self.centroids[index].lat;
|
|
781
|
+
let c_lon = self.centroids[index].lon;
|
|
782
|
+
let sample_size = ((cluster_items.len() as f64 / 10.0).ceil() as usize)
|
|
783
|
+
.max(5)
|
|
784
|
+
.min(cluster_items.len())
|
|
785
|
+
.max(2);
|
|
786
|
+
let mut nearest: Vec<usize> = cluster_items.clone();
|
|
787
|
+
nearest.sort_by(|&a, &b| {
|
|
788
|
+
flying_distance((c_lat, c_lon), (self.items[a].lat, self.items[a].lon))
|
|
789
|
+
.partial_cmp(&flying_distance(
|
|
790
|
+
(c_lat, c_lon),
|
|
791
|
+
(self.items[b].lat, self.items[b].lon),
|
|
792
|
+
))
|
|
793
|
+
.unwrap()
|
|
794
|
+
});
|
|
795
|
+
nearest.truncate(sample_size);
|
|
796
|
+
let centroid_ref = self.centroids[index].clone();
|
|
797
|
+
let representative = nearest
|
|
798
|
+
.iter()
|
|
799
|
+
.min_by(|&&a, &&b| {
|
|
800
|
+
let sum_a: f64 = cluster_items
|
|
801
|
+
.iter()
|
|
802
|
+
.map(|&i| {
|
|
803
|
+
self.distance_between(a, ¢roid_ref)
|
|
804
|
+
* self.items[i].quantities.get("visits").copied().unwrap_or(1.0)
|
|
805
|
+
})
|
|
806
|
+
.sum();
|
|
807
|
+
let sum_b: f64 = cluster_items
|
|
808
|
+
.iter()
|
|
809
|
+
.map(|&i| {
|
|
810
|
+
self.distance_between(b, ¢roid_ref)
|
|
811
|
+
* self.items[i].quantities.get("visits").copied().unwrap_or(1.0)
|
|
812
|
+
})
|
|
813
|
+
.sum();
|
|
814
|
+
sum_a.partial_cmp(&sum_b).unwrap()
|
|
815
|
+
})
|
|
816
|
+
.copied()
|
|
817
|
+
.unwrap_or(cluster_items[0]);
|
|
818
|
+
|
|
819
|
+
let rep = self.items[representative].clone();
|
|
820
|
+
let centroid = &mut self.centroids[index];
|
|
821
|
+
centroid.id = rep.id.clone();
|
|
822
|
+
if centroid.matrix_index.is_some() {
|
|
823
|
+
centroid.matrix_index = rep.matrix_index;
|
|
824
|
+
}
|
|
825
|
+
if centroid.duration_from_and_to_depot > 0.0 || rep.duration_from_and_to_depot[index] > 0.0 {
|
|
826
|
+
centroid.duration_from_and_to_depot = cluster_items
|
|
827
|
+
.iter()
|
|
828
|
+
.map(|&i| self.items[i].duration_from_and_to_depot[index])
|
|
829
|
+
.sum::<f64>()
|
|
830
|
+
/ cluster_items.len() as f64;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
self.swap_centroid_with_limit_violation();
|
|
835
|
+
self.iteration += 1;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
fn swap_centroid_with_limit_violation(&mut self) {
|
|
839
|
+
let mut already_swapped = false;
|
|
840
|
+
let mut order: Vec<(usize, usize)> = self
|
|
841
|
+
.clusters_with_limit_violation
|
|
842
|
+
.iter()
|
|
843
|
+
.enumerate()
|
|
844
|
+
.map(|(i, v)| (i, v.len()))
|
|
845
|
+
.collect();
|
|
846
|
+
order.sort_by(|a, b| b.1.cmp(&a.1));
|
|
847
|
+
|
|
848
|
+
for (violated_cluster, _) in order {
|
|
849
|
+
let preferred = &self.clusters_with_limit_violation[violated_cluster];
|
|
850
|
+
if preferred.is_empty() {
|
|
851
|
+
if self.centroids[violated_cluster].capacity_offence_coeff > 0.3 {
|
|
852
|
+
self.centroids[violated_cluster].capacity_offence_coeff -= 0.3;
|
|
853
|
+
}
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
let units_that_matter: Vec<String> = self.centroids[violated_cluster]
|
|
858
|
+
.load
|
|
859
|
+
.iter()
|
|
860
|
+
.filter(|(_, v)| **v > 0.0)
|
|
861
|
+
.map(|(k, _)| k.clone())
|
|
862
|
+
.filter(|u| {
|
|
863
|
+
self.strict_limitations[violated_cluster]
|
|
864
|
+
.get(u)
|
|
865
|
+
.copied()
|
|
866
|
+
.unwrap_or(0.0) > 0.0
|
|
867
|
+
})
|
|
868
|
+
.collect();
|
|
869
|
+
|
|
870
|
+
let mut favorite_clusters: Vec<usize> = Vec::new();
|
|
871
|
+
for i in 0..self.number_of_clusters {
|
|
872
|
+
let c = &self.centroids[i];
|
|
873
|
+
let cap_ok = units_that_matter.iter().any(|unit| {
|
|
874
|
+
let limit = c.capacities.get(unit).copied();
|
|
875
|
+
let v_cap = self.centroids[violated_cluster].capacities.get(unit).copied();
|
|
876
|
+
v_cap.is_some() && (limit.is_none() || limit.unwrap() > v_cap.unwrap())
|
|
877
|
+
});
|
|
878
|
+
if !cap_ok {
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
let swap_ok = units_that_matter.iter().all(|unit| {
|
|
882
|
+
let v_lim = self.strict_limitations[violated_cluster].get(unit).copied();
|
|
883
|
+
let i_lim = self.strict_limitations[i].get(unit).copied();
|
|
884
|
+
let c_load = c.load.get(unit).copied().unwrap_or(0.0);
|
|
885
|
+
let v_cap_val = self.centroids[violated_cluster].load.get(unit).copied().unwrap_or(0.0);
|
|
886
|
+
(v_lim.is_none() || c_load < 0.98 * v_lim.unwrap())
|
|
887
|
+
&& (i_lim.is_none() || i_lim.unwrap() >= 1.02 * v_cap_val)
|
|
888
|
+
&& self.clusters[violated_cluster].iter().all(|&idx| {
|
|
889
|
+
compatible_item_centroid(&self.items[idx], &self.centroids[i])
|
|
890
|
+
})
|
|
891
|
+
&& self.clusters[i].iter().all(|&idx| {
|
|
892
|
+
compatible_item_centroid(&self.items[idx], &self.centroids[violated_cluster])
|
|
893
|
+
})
|
|
894
|
+
});
|
|
895
|
+
if !swap_ok {
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
let v_dur: f64 = self.clusters[violated_cluster]
|
|
899
|
+
.iter()
|
|
900
|
+
.map(|&idx| self.items[idx].duration_from_and_to_depot[i])
|
|
901
|
+
.sum::<f64>()
|
|
902
|
+
/ self.clusters[violated_cluster].len().max(1) as f64;
|
|
903
|
+
let f_dur: f64 = self.clusters[i]
|
|
904
|
+
.iter()
|
|
905
|
+
.map(|&idx| self.items[idx].duration_from_and_to_depot[violated_cluster])
|
|
906
|
+
.sum::<f64>()
|
|
907
|
+
/ self.clusters[i].len().max(1) as f64;
|
|
908
|
+
let v_cent = self.centroids[violated_cluster].duration_from_and_to_depot;
|
|
909
|
+
let f_cent = self.centroids[i].duration_from_and_to_depot;
|
|
910
|
+
if (v_dur < v_cent || f_dur < f_cent)
|
|
911
|
+
|| (v_dur < 2.0 * v_cent && f_dur < 2.0 * f_cent)
|
|
912
|
+
{
|
|
913
|
+
favorite_clusters.push(i);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if favorite_clusters.is_empty() {
|
|
918
|
+
self.balance_coeff[violated_cluster] /= 0.95;
|
|
919
|
+
self.centroids[violated_cluster].capacity_offence_coeff += 1.0;
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
if already_swapped {
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
let favorite = favorite_clusters
|
|
927
|
+
.iter()
|
|
928
|
+
.min_by(|&&a, &&b| {
|
|
929
|
+
let score_a: f64 = units_that_matter
|
|
930
|
+
.iter()
|
|
931
|
+
.map(|unit| {
|
|
932
|
+
let lim = self.strict_limitations[a].get(unit).copied();
|
|
933
|
+
if lim.is_none() || lim.unwrap() == 0.0 {
|
|
934
|
+
0.0
|
|
935
|
+
} else {
|
|
936
|
+
self.centroids[a].load.get(unit).copied().unwrap_or(0.0)
|
|
937
|
+
/ lim.unwrap()
|
|
938
|
+
* (self.rng.rand_range_f64(0.0, 0.90) + 0.1)
|
|
939
|
+
}
|
|
940
|
+
})
|
|
941
|
+
.sum();
|
|
942
|
+
let score_b: f64 = units_that_matter
|
|
943
|
+
.iter()
|
|
944
|
+
.map(|unit| {
|
|
945
|
+
let lim = self.strict_limitations[b].get(unit).copied();
|
|
946
|
+
if lim.is_none() || lim.unwrap() == 0.0 {
|
|
947
|
+
0.0
|
|
948
|
+
} else {
|
|
949
|
+
self.centroids[b].load.get(unit).copied().unwrap_or(0.0)
|
|
950
|
+
/ lim.unwrap()
|
|
951
|
+
* (self.rng.rand_range_f64(0.0, 0.90) + 0.1)
|
|
952
|
+
}
|
|
953
|
+
})
|
|
954
|
+
.sum();
|
|
955
|
+
score_a.partial_cmp(&score_b).unwrap()
|
|
956
|
+
})
|
|
957
|
+
.copied()
|
|
958
|
+
.unwrap();
|
|
959
|
+
|
|
960
|
+
if favorite == violated_cluster {
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
let v = violated_cluster;
|
|
965
|
+
let f = favorite;
|
|
966
|
+
let swap_lat = self.centroids[v].lat;
|
|
967
|
+
let swap_lon = self.centroids[v].lon;
|
|
968
|
+
let swap_id = self.centroids[v].id.clone();
|
|
969
|
+
let swap_matrix = self.centroids[v].matrix_index;
|
|
970
|
+
let swap_depot = self.centroids[v].duration_from_and_to_depot;
|
|
971
|
+
let swap_balance = self.balance_coeff[v];
|
|
972
|
+
|
|
973
|
+
self.centroids[v].lat = self.centroids[f].lat;
|
|
974
|
+
self.centroids[v].lon = self.centroids[f].lon;
|
|
975
|
+
self.centroids[v].id = self.centroids[f].id.clone();
|
|
976
|
+
self.centroids[v].matrix_index = self.centroids[f].matrix_index;
|
|
977
|
+
self.centroids[v].duration_from_and_to_depot = self.centroids[f].duration_from_and_to_depot;
|
|
978
|
+
self.balance_coeff[v] = self.balance_coeff[f];
|
|
979
|
+
|
|
980
|
+
self.centroids[f].lat = swap_lat;
|
|
981
|
+
self.centroids[f].lon = swap_lon;
|
|
982
|
+
self.centroids[f].id = swap_id;
|
|
983
|
+
self.centroids[f].matrix_index = swap_matrix;
|
|
984
|
+
self.centroids[f].duration_from_and_to_depot = swap_depot;
|
|
985
|
+
self.balance_coeff[f] = swap_balance;
|
|
986
|
+
|
|
987
|
+
already_swapped = true;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
for v in &mut self.clusters_with_limit_violation {
|
|
991
|
+
v.clear();
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
fn manage_empty_clusters(&mut self) {
|
|
996
|
+
if !self.clusters.iter().any(|c| c.is_empty()) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
for ind in 0..self.number_of_clusters {
|
|
1000
|
+
if !self.clusters[ind].is_empty() {
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
let empty_centroid = self.centroids[ind].clone();
|
|
1004
|
+
let mut best: Option<(f64, usize, usize)> = None;
|
|
1005
|
+
for (cluster_idx, cluster) in self.clusters.iter().enumerate() {
|
|
1006
|
+
if cluster.len() <= 1 {
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
let mut min_dist = f64::INFINITY;
|
|
1010
|
+
let mut closest_item = None;
|
|
1011
|
+
for &item_idx in cluster {
|
|
1012
|
+
if !compatible_item_centroid(&self.items[item_idx], &empty_centroid) {
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
let mut total_dist = 0.0;
|
|
1016
|
+
do_forall_linked(&self.items, item_idx, |li| {
|
|
1017
|
+
total_dist += self.distance_between(li, &empty_centroid);
|
|
1018
|
+
});
|
|
1019
|
+
if total_dist < min_dist {
|
|
1020
|
+
min_dist = total_dist;
|
|
1021
|
+
closest_item = Some(item_idx);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if let Some(item_idx) = closest_item {
|
|
1025
|
+
if best.is_none() || min_dist < best.unwrap().0 {
|
|
1026
|
+
best = Some((min_dist, item_idx, cluster_idx));
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if let Some((_, item_idx, from_cluster)) = best {
|
|
1031
|
+
let mut linked = Vec::new();
|
|
1032
|
+
do_forall_linked(&self.items, item_idx, |li| linked.push(li));
|
|
1033
|
+
for li in linked {
|
|
1034
|
+
self.clusters[from_cluster].retain(|&i| i != li);
|
|
1035
|
+
self.clusters[ind].push(li);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
fn update_balance_coefficients(&mut self) {
|
|
1042
|
+
let Some(ref cut) = self.cut_symbol.clone() else {
|
|
1043
|
+
return;
|
|
1044
|
+
};
|
|
1045
|
+
let cut = cut.clone();
|
|
1046
|
+
self.update_cut_limit_wrt_depot_and_route_time();
|
|
1047
|
+
|
|
1048
|
+
let mut balance_violations = Vec::new();
|
|
1049
|
+
for index in 0..self.number_of_clusters {
|
|
1050
|
+
let route_time = if cut == "duration" {
|
|
1051
|
+
self.centroids[index].route_time
|
|
1052
|
+
} else {
|
|
1053
|
+
0.0
|
|
1054
|
+
};
|
|
1055
|
+
let load = self.centroids[index].load.get(&cut).copied().unwrap_or(0.0);
|
|
1056
|
+
let violation = (load + route_time) / self.cut_limit[index].limit - 1.0;
|
|
1057
|
+
balance_violations.push(violation);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
let stepsize = 0.2 - 0.1 * self.iteration as f64 / self.max_iterations as f64;
|
|
1061
|
+
let max_correction = 1.05 + 0.95 * (self.max_iterations - self.iteration) as f64 / self.max_iterations as f64;
|
|
1062
|
+
let min_correction = 1.0 / max_correction;
|
|
1063
|
+
self.max_balance_violation = 0.0;
|
|
1064
|
+
|
|
1065
|
+
for index in 0..self.number_of_clusters {
|
|
1066
|
+
if !self.clusters_with_limit_violation[index].is_empty() && balance_violations[index] < 0.0 {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
self.max_balance_violation = self
|
|
1070
|
+
.max_balance_violation
|
|
1071
|
+
.max(balance_violations[index].abs());
|
|
1072
|
+
let balance_correction = ((1.0 + balance_violations[index]).powf(stepsize))
|
|
1073
|
+
.max(min_correction)
|
|
1074
|
+
.min(max_correction);
|
|
1075
|
+
self.balance_coeff[index] *= balance_correction;
|
|
1076
|
+
for bc in &mut self.balance_coeff {
|
|
1077
|
+
*bc /= balance_correction;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
let mean: f64 = self.balance_coeff.iter().sum::<f64>() / self.balance_coeff.len() as f64;
|
|
1081
|
+
for bc in &mut self.balance_coeff {
|
|
1082
|
+
*bc /= mean;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
fn update_cut_limit_wrt_depot_and_route_time(&mut self) {
|
|
1087
|
+
self.update_approximate_route_times();
|
|
1088
|
+
if self.cut_symbol.as_deref() != Some("duration") {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
let vehicle_work_times = self.compute_vehicle_work_time_with_depot_and_capacity();
|
|
1092
|
+
let total: f64 = vehicle_work_times.iter().sum();
|
|
1093
|
+
for index in 0..self.number_of_clusters {
|
|
1094
|
+
self.cut_limit[index].limit = (self.total_cut_load + self.approximate_total_route_time)
|
|
1095
|
+
* vehicle_work_times[index]
|
|
1096
|
+
/ total;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
fn compute_vehicle_work_time_with_depot_and_capacity(&self) -> Vec<f64> {
|
|
1101
|
+
let coef = self
|
|
1102
|
+
.centroids
|
|
1103
|
+
.iter()
|
|
1104
|
+
.enumerate()
|
|
1105
|
+
.map(|(index, c)| {
|
|
1106
|
+
self.vehicles[index].duration
|
|
1107
|
+
/ (c.duration_from_and_to_depot.max(1.0) * self.vehicles[index].total_work_days)
|
|
1108
|
+
})
|
|
1109
|
+
.fold(f64::INFINITY, f64::min);
|
|
1110
|
+
let coef = (coef * 0.9).min(1.0);
|
|
1111
|
+
let min_offence = self
|
|
1112
|
+
.centroids
|
|
1113
|
+
.iter()
|
|
1114
|
+
.map(|c| c.capacity_offence_coeff)
|
|
1115
|
+
.fold(f64::INFINITY, f64::min);
|
|
1116
|
+
self.centroids
|
|
1117
|
+
.iter()
|
|
1118
|
+
.enumerate()
|
|
1119
|
+
.map(|(index, c)| {
|
|
1120
|
+
let offence = c.capacity_offence_coeff - min_offence;
|
|
1121
|
+
(self.vehicles[index].duration
|
|
1122
|
+
- coef * c.duration_from_and_to_depot * self.vehicles[index].total_work_days)
|
|
1123
|
+
* 0.98_f64.powf(offence)
|
|
1124
|
+
})
|
|
1125
|
+
.collect()
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
fn update_approximate_route_times(&mut self) {
|
|
1129
|
+
if self.cut_symbol.is_none() {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
self.update_approximate_area_and_speeds();
|
|
1133
|
+
let mut approximate_total = 0.0;
|
|
1134
|
+
for centroid in &mut self.centroids {
|
|
1135
|
+
if centroid.speed == 0.0 {
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
centroid.route_time = compute_approximate_route_time(
|
|
1139
|
+
centroid.area,
|
|
1140
|
+
centroid.visit_count,
|
|
1141
|
+
centroid.speed,
|
|
1142
|
+
centroid.total_work_days / centroid.vehicle_count,
|
|
1143
|
+
) * centroid.vehicle_count;
|
|
1144
|
+
approximate_total += centroid.route_time;
|
|
1145
|
+
}
|
|
1146
|
+
if approximate_total < self.approximate_total_route_time {
|
|
1147
|
+
self.approximate_total_route_time =
|
|
1148
|
+
(9.0 * self.approximate_total_route_time + approximate_total) / 10.0;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
fn update_approximate_area_and_speeds(&mut self) {
|
|
1153
|
+
let densities: Vec<f64> = self
|
|
1154
|
+
.centroids
|
|
1155
|
+
.iter()
|
|
1156
|
+
.enumerate()
|
|
1157
|
+
.filter_map(|(index, _)| {
|
|
1158
|
+
if self.clusters[index].is_empty() {
|
|
1159
|
+
None
|
|
1160
|
+
} else {
|
|
1161
|
+
let coords: Vec<(f64, f64)> = self
|
|
1162
|
+
.clusters[index]
|
|
1163
|
+
.iter()
|
|
1164
|
+
.map(|&i| (self.items[i].lat, self.items[i].lon))
|
|
1165
|
+
.collect();
|
|
1166
|
+
let quad = approximate_quadrilateral_polygon(&coords, |m| self.rng.rand_usize(m));
|
|
1167
|
+
let area = approximate_polygon_area(&quad).max(1.0);
|
|
1168
|
+
let visit_count: f64 = self
|
|
1169
|
+
.clusters[index]
|
|
1170
|
+
.iter()
|
|
1171
|
+
.map(|&i| self.items[i].quantities.get("visits").copied().unwrap_or(1.0))
|
|
1172
|
+
.sum();
|
|
1173
|
+
Some(visit_count / area)
|
|
1174
|
+
}
|
|
1175
|
+
})
|
|
1176
|
+
.collect();
|
|
1177
|
+
if let Some(median) = median(&densities) {
|
|
1178
|
+
self.density_cap = self.density_cap.max(median * 6.0);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
for index in 0..self.number_of_clusters {
|
|
1182
|
+
if self.clusters[index].is_empty() {
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
let cluster = &self.clusters[index];
|
|
1186
|
+
let min_size = (0.95 * cluster.len() as f64).ceil() as usize;
|
|
1187
|
+
let max_size = (0.15 * cluster.len() as f64).ceil() as usize;
|
|
1188
|
+
let mut speeds: Vec<f64> = cluster
|
|
1189
|
+
.iter()
|
|
1190
|
+
.map(|&i| self.items[i].local_speed.unwrap_or(0.0))
|
|
1191
|
+
.collect();
|
|
1192
|
+
speeds.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
1193
|
+
let min_speed: f64 = speeds.iter().take(min_size).sum::<f64>() / min_size as f64;
|
|
1194
|
+
speeds.sort_by(|a, b| b.partial_cmp(a).unwrap());
|
|
1195
|
+
let max_speed: f64 = speeds.iter().take(max_size).sum::<f64>() / max_size.max(1) as f64;
|
|
1196
|
+
let cp = &mut self.centroids[index];
|
|
1197
|
+
let coords: Vec<(f64, f64)> = cluster
|
|
1198
|
+
.iter()
|
|
1199
|
+
.map(|&i| (self.items[i].lat, self.items[i].lon))
|
|
1200
|
+
.collect();
|
|
1201
|
+
let quad = approximate_quadrilateral_polygon(&coords, |m| self.rng.rand_usize(m));
|
|
1202
|
+
cp.area = approximate_polygon_area(&quad).max(1.0);
|
|
1203
|
+
cp.visit_count = cluster
|
|
1204
|
+
.iter()
|
|
1205
|
+
.map(|&i| self.items[i].quantities.get("visits").copied().unwrap_or(1.0))
|
|
1206
|
+
.sum();
|
|
1207
|
+
cp.visit_density = cp.visit_count / cp.area;
|
|
1208
|
+
cp.min_speed = min_speed;
|
|
1209
|
+
cp.max_speed = max_speed;
|
|
1210
|
+
let ratio = ((self.density_cap - cp.visit_density).max(0.0) / self.density_cap).powf(6.0);
|
|
1211
|
+
cp.speed = cp.min_speed + (cp.max_speed - cp.min_speed).max(0.0) * ratio;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
fn centroids_converged_or_in_loop(&mut self, last_n_iterations: usize) -> bool {
|
|
1216
|
+
if self.iteration == 0 {
|
|
1217
|
+
self.last_n_average_diffs = vec![0.0; 2 * last_n_iterations + 1];
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1220
|
+
let mut total_movement = 0.0;
|
|
1221
|
+
for i in 0..self.number_of_clusters {
|
|
1222
|
+
total_movement +=
|
|
1223
|
+
euclidean_distance(self.old_centroids_lat_lon[i], (self.centroids[i].lat, self.centroids[i].lon));
|
|
1224
|
+
}
|
|
1225
|
+
self.last_n_average_diffs.push(total_movement);
|
|
1226
|
+
if self.last_n_average_diffs.last().copied().unwrap_or(0.0)
|
|
1227
|
+
< self.number_of_clusters as f64
|
|
1228
|
+
* (20.0 + 80.0 * self.iteration as f64 / self.max_iterations as f64)
|
|
1229
|
+
{
|
|
1230
|
+
return true;
|
|
1231
|
+
}
|
|
1232
|
+
for n in 1..=last_n_iterations {
|
|
1233
|
+
let len = self.last_n_average_diffs.len();
|
|
1234
|
+
if len < 2 * n {
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
1237
|
+
let curr: f64 = self.last_n_average_diffs[len - n..].iter().sum();
|
|
1238
|
+
let prev: f64 = self.last_n_average_diffs[len - n - n..len - n].iter().sum();
|
|
1239
|
+
if (curr - prev).abs() < 1e-5 {
|
|
1240
|
+
return true;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if self.last_n_average_diffs.len() > 2 * last_n_iterations + 1 {
|
|
1244
|
+
self.last_n_average_diffs.remove(0);
|
|
1245
|
+
}
|
|
1246
|
+
false
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
fn stop_criteria_met(&mut self) -> bool {
|
|
1250
|
+
let last_n = (self.iteration as f64).sqrt() as usize;
|
|
1251
|
+
self.centroids_converged_or_in_loop(last_n)
|
|
1252
|
+
&& self.limit_violation_count == 0
|
|
1253
|
+
&& self.max_balance_violation
|
|
1254
|
+
<= 0.05 + (self.iteration as f64 / self.max_iterations as f64).powi(8)
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
fn median(values: &[f64]) -> Option<f64> {
|
|
1259
|
+
if values.is_empty() {
|
|
1260
|
+
return None;
|
|
1261
|
+
}
|
|
1262
|
+
let mut v = values.to_vec();
|
|
1263
|
+
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
1264
|
+
let m = v.len() / 2;
|
|
1265
|
+
if v.len() % 2 == 1 {
|
|
1266
|
+
Some(v[m])
|
|
1267
|
+
} else {
|
|
1268
|
+
Some((v[m - 1] + v[m]) / 2.0)
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
fn default_day_skills() -> Vec<String> {
|
|
1273
|
+
(0..7)
|
|
1274
|
+
.map(|i| format!("{}_day_skill", i))
|
|
1275
|
+
.collect()
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
pub fn build_from_input(input: &InputConfig) -> BuildOutput {
|
|
1279
|
+
Clusterer::from_input(input).build()
|
|
1280
|
+
}
|