1use crate::result::StopReason;
43use crate::timing::Timer;
44use std::fmt::Debug;
45use std::sync::atomic::{AtomicBool, Ordering};
46use std::sync::Arc;
47use u_metaheur::TemperatureScale;
48
49#[derive(Debug, Clone)]
51pub struct AlnsConfig {
52 pub max_iterations: usize,
54 pub time_limit_ms: u64,
56 pub segment_size: usize,
58 pub score_best: f64,
60 pub score_better: f64,
62 pub score_accepted: f64,
64 pub reaction_factor: f64,
66 pub min_weight: f64,
68 pub temperature_scale: TemperatureScale,
75 pub initial_temperature: f64,
77 pub cooling_rate: f64,
79 pub final_temperature: f64,
81 pub seed: Option<u64>,
83}
84
85impl Default for AlnsConfig {
86 fn default() -> Self {
87 Self {
88 max_iterations: 10000,
89 time_limit_ms: 60000, segment_size: 100,
91 score_best: 33.0,
92 score_better: 9.0,
93 score_accepted: 3.0,
94 reaction_factor: 0.1,
95 min_weight: 0.1,
96 temperature_scale: TemperatureScale::default(),
97 initial_temperature: 1.0,
98 cooling_rate: 0.9995,
99 final_temperature: 1e-3,
100 seed: None,
101 }
102 }
103}
104
105impl AlnsConfig {
106 pub fn new() -> Self {
108 Self::default()
109 }
110
111 pub fn with_max_iterations(mut self, iterations: usize) -> Self {
113 self.max_iterations = iterations;
114 self
115 }
116
117 pub fn with_time_limit_ms(mut self, ms: u64) -> Self {
119 self.time_limit_ms = ms;
120 self
121 }
122
123 pub fn with_segment_size(mut self, size: usize) -> Self {
125 self.segment_size = size;
126 self
127 }
128
129 pub fn with_scores(mut self, best: f64, better: f64, accepted: f64) -> Self {
131 self.score_best = best;
132 self.score_better = better;
133 self.score_accepted = accepted;
134 self
135 }
136
137 pub fn with_reaction_factor(mut self, factor: f64) -> Self {
139 self.reaction_factor = factor;
140 self
141 }
142
143 pub fn with_temperature(mut self, initial: f64, cooling_rate: f64, final_temp: f64) -> Self {
146 self.temperature_scale = TemperatureScale::Absolute;
147 self.initial_temperature = initial;
148 self.cooling_rate = cooling_rate;
149 self.final_temperature = final_temp;
150 self
151 }
152
153 pub fn with_relative_temperature(
159 mut self,
160 initial: f64,
161 cooling_rate: f64,
162 final_temp: f64,
163 samples: usize,
164 ) -> Self {
165 self.temperature_scale = TemperatureScale::Calibrated { samples };
166 self.initial_temperature = initial;
167 self.cooling_rate = cooling_rate;
168 self.final_temperature = final_temp;
169 self
170 }
171
172 pub fn validate(&self) -> crate::error::Result<()> {
182 use crate::error::{check_at_least, check_range};
183 check_at_least("segment_size", self.segment_size as f64, 1.0)?;
184 check_range("reaction_factor", self.reaction_factor, 0.0, 1.0)?;
185 check_range("cooling_rate", self.cooling_rate, 0.9, 1.0)?;
186 match self.temperature_scale {
187 TemperatureScale::Absolute => {
188 check_at_least("initial_temperature", self.initial_temperature, 0.01)?;
189 check_at_least("final_temperature", self.final_temperature, 0.001)?;
190 }
191 TemperatureScale::Calibrated { samples } => {
192 crate::error::check_above("initial_temperature", self.initial_temperature, 0.0)?;
193 crate::error::check_above("final_temperature", self.final_temperature, 0.0)?;
194 check_at_least("temperature_scale.samples", samples as f64, 1.0)?;
195 }
196 }
197 check_at_least("min_weight", self.min_weight, 0.0)?;
198 Ok(())
199 }
200
201 pub fn with_seed(mut self, seed: u64) -> Self {
203 self.seed = Some(seed);
204 self
205 }
206}
207
208#[derive(Debug, Clone)]
210pub struct OperatorStats {
211 pub weight: f64,
213 pub times_used: usize,
215 pub total_score: f64,
217 pub segment_score: f64,
219 pub segment_uses: usize,
221}
222
223impl Default for OperatorStats {
224 fn default() -> Self {
225 Self {
226 weight: 1.0,
227 times_used: 0,
228 total_score: 0.0,
229 segment_score: 0.0,
230 segment_uses: 0,
231 }
232 }
233}
234
235impl OperatorStats {
236 pub fn new(initial_weight: f64) -> Self {
238 Self {
239 weight: initial_weight,
240 ..Default::default()
241 }
242 }
243
244 pub fn record_use(&mut self, score: f64) {
246 self.times_used += 1;
247 self.total_score += score;
248 self.segment_score += score;
249 self.segment_uses += 1;
250 }
251
252 pub fn update_weight(&mut self, reaction_factor: f64, min_weight: f64) {
254 if self.segment_uses > 0 {
255 let segment_avg = self.segment_score / self.segment_uses as f64;
256 self.weight = self.weight * (1.0 - reaction_factor) + segment_avg * reaction_factor;
257 self.weight = self.weight.max(min_weight);
258 }
259 self.segment_score = 0.0;
261 self.segment_uses = 0;
262 }
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
267pub enum DestroyOperatorId {
268 Random,
270 Worst,
272 Shaw,
274 Custom(usize),
276}
277
278#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
280pub enum RepairOperatorId {
281 Greedy,
283 Random,
285 Longest,
287 Custom(usize),
289}
290
291#[derive(Debug, Clone)]
293pub struct DestroyResult {
294 pub removed_indices: Vec<usize>,
296 pub operator: DestroyOperatorId,
298}
299
300#[derive(Debug, Clone)]
302pub struct RepairResult {
303 pub placed_count: usize,
305 pub unplaced_count: usize,
307 pub operator: RepairOperatorId,
309}
310
311#[derive(Debug, Clone)]
313pub struct AlnsProgress {
314 pub iteration: usize,
316 pub best_fitness: f64,
318 pub current_fitness: f64,
320 pub temperature: f64,
322 pub segment: usize,
324 pub elapsed_ms: u64,
326 pub acceptance_rate: f64,
328 pub best_destroy: DestroyOperatorId,
330 pub best_repair: RepairOperatorId,
332}
333
334#[derive(Debug, Clone)]
336pub struct AlnsResult<S> {
337 pub best_solution: S,
339 pub best_fitness: f64,
341 pub iterations: usize,
343 pub elapsed_ms: u64,
345 pub stop_reason: StopReason,
347 pub improvements: usize,
349 pub final_temperature: f64,
351 pub temperature_unit: f64,
354 pub destroy_weights: Vec<(DestroyOperatorId, f64)>,
356 pub repair_weights: Vec<(RepairOperatorId, f64)>,
358}
359
360pub trait AlnsSolution: Clone + Debug {
362 fn fitness(&self) -> f64;
364
365 fn placed_count(&self) -> usize;
367
368 fn total_count(&self) -> usize;
370}
371
372pub trait AlnsProblem {
374 type Solution: AlnsSolution;
376
377 fn create_initial_solution(&mut self) -> Self::Solution;
379
380 fn clone_solution(&self, solution: &Self::Solution) -> Self::Solution;
382
383 fn destroy_operators(&self) -> Vec<DestroyOperatorId>;
385
386 fn repair_operators(&self) -> Vec<RepairOperatorId>;
388
389 fn destroy(
391 &mut self,
392 solution: &mut Self::Solution,
393 operator: DestroyOperatorId,
394 degree: f64,
395 rng: &mut rand::rngs::StdRng,
396 ) -> DestroyResult;
397
398 fn repair(
401 &mut self,
402 solution: &mut Self::Solution,
403 destroyed: &DestroyResult,
404 operator: RepairOperatorId,
405 rng: &mut rand::rngs::StdRng,
406 ) -> RepairResult;
407
408 fn relatedness(&self, solution: &Self::Solution, i: usize, j: usize) -> f64 {
410 let _ = (solution, i, j);
412 0.0
413 }
414}
415
416pub struct AlnsRunner {
418 config: AlnsConfig,
419 cancelled: Arc<AtomicBool>,
420}
421
422impl AlnsRunner {
423 pub fn new(config: AlnsConfig) -> crate::error::Result<Self> {
428 Self::with_cancellation(config, Arc::new(AtomicBool::new(false)))
429 }
430
431 pub fn with_cancellation(
437 config: AlnsConfig,
438 cancelled: Arc<AtomicBool>,
439 ) -> crate::error::Result<Self> {
440 config.validate()?;
441 Ok(Self { config, cancelled })
442 }
443
444 pub fn run<P, F>(&self, problem: &mut P, mut progress_callback: F) -> AlnsResult<P::Solution>
446 where
447 P: AlnsProblem,
448 F: FnMut(&AlnsProgress),
449 {
450 use rand::prelude::*;
451 use rand::SeedableRng;
452
453 let mut rng = match self.config.seed {
455 Some(seed) => rand::rngs::StdRng::seed_from_u64(seed),
456 None => rand::make_rng::<rand::rngs::StdRng>(),
457 };
458
459 let start_time = Timer::now();
460
461 let mut current = problem.create_initial_solution();
463 let mut best = problem.clone_solution(¤t);
464 let mut best_fitness = best.fitness();
465
466 let destroy_ops = problem.destroy_operators();
468 let repair_ops = problem.repair_operators();
469
470 let mut destroy_stats: Vec<(DestroyOperatorId, OperatorStats)> = destroy_ops
472 .iter()
473 .map(|&op| (op, OperatorStats::new(1.0)))
474 .collect();
475
476 let mut repair_stats: Vec<(RepairOperatorId, OperatorStats)> = repair_ops
477 .iter()
478 .map(|&op| (op, OperatorStats::new(1.0)))
479 .collect();
480
481 let scale = match self.config.temperature_scale {
485 TemperatureScale::Absolute => 1.0,
486 TemperatureScale::Calibrated { samples } => {
487 let from = current.fitness();
488 let mut worsenings = Vec::with_capacity(samples);
489 for _ in 0..samples {
490 if self.cancelled.load(Ordering::Relaxed)
491 || (self.config.time_limit_ms > 0
492 && start_time.elapsed().as_millis() as u64 >= self.config.time_limit_ms)
493 {
494 break;
495 }
496 let destroy_op =
497 destroy_stats[self.select_operator_by_weight(&destroy_stats, &mut rng)].0;
498 let repair_op =
499 repair_stats[self.select_operator_by_weight(&repair_stats, &mut rng)].0;
500 let mut candidate = problem.clone_solution(¤t);
501 let degree = rng.random_range(0.1..=0.4);
502 let destroyed = problem.destroy(&mut candidate, destroy_op, degree, &mut rng);
503 problem.repair(&mut candidate, &destroyed, repair_op, &mut rng);
504 worsenings.push(candidate.fitness() - from);
505 }
506 TemperatureScale::cost_scale(worsenings)
507 }
508 };
509 let final_temperature = self.config.final_temperature * scale;
510 let mut temperature = self.config.initial_temperature * scale;
511
512 let mut iteration = 0;
514 let mut segment = 0;
515 let mut improvements = 0;
516 let mut segment_accepts = 0;
517 let mut segment_total = 0;
518
519 let stop_reason = loop {
521 let elapsed = start_time.elapsed();
523 let elapsed_ms = elapsed.as_millis() as u64;
524
525 if iteration >= self.config.max_iterations {
526 break StopReason::IterationLimit;
527 }
528
529 if self.cancelled.load(Ordering::Relaxed) {
530 break StopReason::Cancelled;
531 }
532
533 if self.config.time_limit_ms > 0 && elapsed_ms >= self.config.time_limit_ms {
534 break StopReason::TimeLimit;
535 }
536
537 let destroy_idx = self.select_operator_by_weight(&destroy_stats, &mut rng);
539 let destroy_op = destroy_stats[destroy_idx].0;
540
541 let repair_idx = self.select_operator_by_weight(&repair_stats, &mut rng);
543 let repair_op = repair_stats[repair_idx].0;
544
545 let mut candidate = problem.clone_solution(¤t);
547
548 let degree = rng.random_range(0.1..=0.4);
550 let destroy_result = problem.destroy(&mut candidate, destroy_op, degree, &mut rng);
551
552 let _repair_result =
554 problem.repair(&mut candidate, &destroy_result, repair_op, &mut rng);
555
556 let candidate_fitness = candidate.fitness();
557 let current_fitness = current.fitness();
558
559 let (accepted, score) = if candidate_fitness < best_fitness {
561 best = problem.clone_solution(&candidate);
563 best_fitness = candidate_fitness;
564 improvements += 1;
565 (true, self.config.score_best)
566 } else if candidate_fitness < current_fitness {
567 (true, self.config.score_better)
569 } else {
570 let delta = candidate_fitness - current_fitness;
572 let accept_prob = (-delta / temperature).exp();
573 if rng.random::<f64>() < accept_prob {
574 (true, self.config.score_accepted)
575 } else {
576 (false, 0.0)
577 }
578 };
579
580 if accepted {
582 current = candidate;
583 segment_accepts += 1;
584 }
585
586 destroy_stats[destroy_idx].1.record_use(score);
588 repair_stats[repair_idx].1.record_use(score);
589
590 segment_total += 1;
591
592 temperature *= self.config.cooling_rate;
594 temperature = temperature.max(final_temperature);
595
596 if iteration > 0 && iteration % self.config.segment_size == 0 {
598 for (_, stats) in &mut destroy_stats {
600 stats.update_weight(self.config.reaction_factor, self.config.min_weight);
601 }
602 for (_, stats) in &mut repair_stats {
603 stats.update_weight(self.config.reaction_factor, self.config.min_weight);
604 }
605
606 segment += 1;
607 segment_accepts = 0;
608 segment_total = 0;
609 }
610
611 let acceptance_rate = if segment_total > 0 {
613 segment_accepts as f64 / segment_total as f64
614 } else {
615 0.0
616 };
617
618 let best_destroy = destroy_stats
619 .iter()
620 .max_by(|a, b| {
621 a.1.weight
622 .partial_cmp(&b.1.weight)
623 .unwrap_or(std::cmp::Ordering::Equal)
624 })
625 .map(|(op, _)| *op)
626 .unwrap_or(DestroyOperatorId::Random);
627
628 let best_repair = repair_stats
629 .iter()
630 .max_by(|a, b| {
631 a.1.weight
632 .partial_cmp(&b.1.weight)
633 .unwrap_or(std::cmp::Ordering::Equal)
634 })
635 .map(|(op, _)| *op)
636 .unwrap_or(RepairOperatorId::Greedy);
637
638 let progress = AlnsProgress {
639 iteration,
640 best_fitness,
641 current_fitness: current.fitness(),
642 temperature,
643 segment,
644 elapsed_ms,
645 acceptance_rate,
646 best_destroy,
647 best_repair,
648 };
649
650 progress_callback(&progress);
651
652 iteration += 1;
653 };
654
655 let elapsed_ms = start_time.elapsed_ms();
656
657 AlnsResult {
658 best_solution: best,
659 best_fitness,
660 iterations: iteration,
661 elapsed_ms,
662 stop_reason,
663 improvements,
664 final_temperature: temperature,
665 temperature_unit: scale,
666 destroy_weights: destroy_stats
667 .iter()
668 .map(|(op, stats)| (*op, stats.weight))
669 .collect(),
670 repair_weights: repair_stats
671 .iter()
672 .map(|(op, stats)| (*op, stats.weight))
673 .collect(),
674 }
675 }
676
677 fn select_operator_by_weight<T>(
679 &self,
680 stats: &[(T, OperatorStats)],
681 rng: &mut rand::rngs::StdRng,
682 ) -> usize {
683 use rand::prelude::*;
684
685 let total_weight: f64 = stats.iter().map(|(_, s)| s.weight).sum();
686 if total_weight <= 0.0 || stats.is_empty() {
687 return 0;
688 }
689
690 let mut roll = rng.random::<f64>() * total_weight;
691 for (i, (_, stat)) in stats.iter().enumerate() {
692 roll -= stat.weight;
693 if roll <= 0.0 {
694 return i;
695 }
696 }
697
698 stats.len() - 1
699 }
700}
701
702#[cfg(test)]
703mod tests {
704 use super::*;
705
706 #[test]
707 fn test_alns_config_default() {
708 let config = AlnsConfig::default();
709 assert_eq!(config.max_iterations, 10000);
710 assert_eq!(config.time_limit_ms, 60000);
711 assert_eq!(config.segment_size, 100);
712 assert!((config.score_best - 33.0).abs() < 1e-9);
713 }
714
715 #[test]
716 fn test_alns_config_builder() {
717 let config = AlnsConfig::new()
718 .with_max_iterations(5000)
719 .with_time_limit_ms(30000)
720 .with_segment_size(50)
721 .with_scores(10.0, 5.0, 1.0)
722 .with_reaction_factor(0.2)
723 .with_temperature(50.0, 0.999, 0.001)
724 .with_seed(42);
725
726 assert_eq!(config.max_iterations, 5000);
727 assert_eq!(config.time_limit_ms, 30000);
728 assert_eq!(config.segment_size, 50);
729 assert!((config.score_best - 10.0).abs() < 1e-9);
730 assert!((config.score_better - 5.0).abs() < 1e-9);
731 assert!((config.score_accepted - 1.0).abs() < 1e-9);
732 assert!((config.reaction_factor - 0.2).abs() < 1e-9);
733 assert!((config.initial_temperature - 50.0).abs() < 1e-9);
734 assert!((config.cooling_rate - 0.999).abs() < 1e-9);
735 assert!((config.final_temperature - 0.001).abs() < 1e-9);
736 assert_eq!(config.seed, Some(42));
737 }
738
739 #[test]
740 fn test_operator_stats() {
741 let mut stats = OperatorStats::new(1.0);
742
743 stats.record_use(10.0);
744 stats.record_use(20.0);
745
746 assert_eq!(stats.times_used, 2);
747 assert!((stats.total_score - 30.0).abs() < 1e-9);
748 assert!((stats.segment_score - 30.0).abs() < 1e-9);
749 assert_eq!(stats.segment_uses, 2);
750
751 stats.update_weight(0.5, 0.1);
752
753 assert!((stats.weight - 8.0).abs() < 1e-9);
755 assert!((stats.segment_score - 0.0).abs() < 1e-9);
756 assert_eq!(stats.segment_uses, 0);
757 }
758
759 #[test]
760 fn test_destroy_operator_ids() {
761 let ops = [
762 DestroyOperatorId::Random,
763 DestroyOperatorId::Worst,
764 DestroyOperatorId::Shaw,
765 DestroyOperatorId::Custom(0),
766 ];
767
768 assert_eq!(ops.len(), 4);
769 assert_eq!(DestroyOperatorId::Random, DestroyOperatorId::Random);
770 assert_ne!(DestroyOperatorId::Random, DestroyOperatorId::Worst);
771 }
772
773 #[test]
774 fn test_repair_operator_ids() {
775 let ops = [
776 RepairOperatorId::Greedy,
777 RepairOperatorId::Random,
778 RepairOperatorId::Custom(0),
779 ];
780
781 assert_eq!(ops.len(), 3);
782 assert_eq!(RepairOperatorId::Greedy, RepairOperatorId::Greedy);
783 assert_ne!(RepairOperatorId::Greedy, RepairOperatorId::Random);
784 }
785
786 #[test]
787 fn test_destroy_result() {
788 let result = DestroyResult {
789 removed_indices: vec![0, 3, 5],
790 operator: DestroyOperatorId::Random,
791 };
792
793 assert_eq!(result.removed_indices.len(), 3);
794 assert_eq!(result.operator, DestroyOperatorId::Random);
795 }
796
797 #[test]
798 fn test_repair_result() {
799 let result = RepairResult {
800 placed_count: 8,
801 unplaced_count: 2,
802 operator: RepairOperatorId::Greedy,
803 };
804
805 assert_eq!(result.placed_count, 8);
806 assert_eq!(result.unplaced_count, 2);
807 assert_eq!(result.operator, RepairOperatorId::Greedy);
808 }
809
810 #[test]
811 fn test_alns_progress() {
812 let progress = AlnsProgress {
813 iteration: 100,
814 best_fitness: 0.85,
815 current_fitness: 0.90,
816 temperature: 50.0,
817 segment: 1,
818 elapsed_ms: 5000,
819 acceptance_rate: 0.45,
820 best_destroy: DestroyOperatorId::Worst,
821 best_repair: RepairOperatorId::Greedy,
822 };
823
824 assert_eq!(progress.iteration, 100);
825 assert!((progress.best_fitness - 0.85).abs() < 1e-9);
826 assert_eq!(progress.segment, 1);
827 assert_eq!(progress.best_destroy, DestroyOperatorId::Worst);
828 assert_eq!(progress.best_repair, RepairOperatorId::Greedy);
829 }
830
831 #[derive(Clone, Debug)]
833 struct MockSolution {
834 fitness: f64,
835 placed: usize,
836 total: usize,
837 }
838
839 impl AlnsSolution for MockSolution {
840 fn fitness(&self) -> f64 {
841 self.fitness
842 }
843
844 fn placed_count(&self) -> usize {
845 self.placed
846 }
847
848 fn total_count(&self) -> usize {
849 self.total
850 }
851 }
852
853 struct MockProblem {
854 improvement_per_iteration: f64,
855 }
856
857 impl AlnsProblem for MockProblem {
858 type Solution = MockSolution;
859
860 fn create_initial_solution(&mut self) -> MockSolution {
861 MockSolution {
862 fitness: 1.0,
863 placed: 8,
864 total: 10,
865 }
866 }
867
868 fn clone_solution(&self, solution: &MockSolution) -> MockSolution {
869 solution.clone()
870 }
871
872 fn destroy_operators(&self) -> Vec<DestroyOperatorId> {
873 vec![DestroyOperatorId::Random, DestroyOperatorId::Worst]
874 }
875
876 fn repair_operators(&self) -> Vec<RepairOperatorId> {
877 vec![RepairOperatorId::Greedy, RepairOperatorId::Random]
878 }
879
880 fn destroy(
881 &mut self,
882 _solution: &mut MockSolution,
883 operator: DestroyOperatorId,
884 _degree: f64,
885 _rng: &mut rand::rngs::StdRng,
886 ) -> DestroyResult {
887 DestroyResult {
888 removed_indices: vec![0, 1, 2],
889 operator,
890 }
891 }
892
893 fn repair(
894 &mut self,
895 solution: &mut MockSolution,
896 _destroyed: &DestroyResult,
897 operator: RepairOperatorId,
898 _rng: &mut rand::rngs::StdRng,
899 ) -> RepairResult {
900 solution.fitness -= self.improvement_per_iteration;
902 solution.fitness = solution.fitness.max(0.1);
903 RepairResult {
904 placed_count: solution.placed,
905 unplaced_count: 0,
906 operator,
907 }
908 }
909 }
910
911 #[test]
912 fn test_alns_runner_basic() {
913 let config = AlnsConfig::new()
914 .with_max_iterations(100)
915 .with_time_limit_ms(5000)
916 .with_seed(42);
917
918 let mut problem = MockProblem {
919 improvement_per_iteration: 0.01,
920 };
921
922 let runner = AlnsRunner::new(config).expect("valid config");
923 let mut last_progress: Option<AlnsProgress> = None;
924
925 let result = runner.run(&mut problem, |progress| {
926 last_progress = Some(progress.clone());
927 });
928
929 assert!(result.best_fitness <= 1.0);
930 assert_eq!(result.iterations, 100);
931 assert!(last_progress.is_some());
932 assert!(!result.destroy_weights.is_empty());
933 assert!(!result.repair_weights.is_empty());
934 }
935
936 #[test]
937 fn test_alns_runner_time_limit() {
938 let config = AlnsConfig::new()
939 .with_max_iterations(usize::MAX)
944 .with_time_limit_ms(100)
945 .with_seed(42);
946
947 let mut problem = MockProblem {
948 improvement_per_iteration: 0.001,
949 };
950
951 let runner = AlnsRunner::new(config).expect("valid config");
952 let result = runner.run(&mut problem, |_| {});
953
954 assert!(result.iterations < usize::MAX);
956 assert!(result.elapsed_ms >= 100);
957 }
958
959 #[test]
960 fn test_alns_weight_adaptation() {
961 let config = AlnsConfig::new()
962 .with_max_iterations(200)
963 .with_segment_size(50)
964 .with_reaction_factor(0.5)
965 .with_seed(42);
966
967 let mut problem = MockProblem {
968 improvement_per_iteration: 0.01,
969 };
970
971 let runner = AlnsRunner::new(config).expect("valid config");
972 let result = runner.run(&mut problem, |_| {});
973
974 let _initial_weight_sum: f64 = 2.0; let _final_destroy_sum: f64 = result.destroy_weights.iter().map(|(_, w)| *w).sum();
977
978 let max_destroy_weight = result
981 .destroy_weights
982 .iter()
983 .map(|(_, w)| *w)
984 .fold(0.0, f64::max);
985
986 assert!(max_destroy_weight >= 0.1); assert!(result.iterations == 200);
988 }
989}