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.
@@ -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, &centroid_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, &centroid_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
+ }