1use super::tsp_instance::{TspInstanceError, TspInstanceYaml};
40use super::{CriterionStatus, EddDemo, FalsificationStatus};
41use crate::edd::audit::{
42 Decision, EquationEval, SimulationAuditLog, StepEntry, TspStateSnapshot, TspStepType,
43};
44use crate::engine::rng::SimRng;
45use crate::engine::SimTime;
46use serde::{Deserialize, Serialize};
47
48#[cfg(not(target_arch = "wasm32"))]
51use std::time::Instant;
52
53#[cfg(target_arch = "wasm32")]
54#[derive(Clone, Copy)]
55struct Instant(f64);
56
57#[cfg(target_arch = "wasm32")]
58impl Instant {
59 fn now() -> Self {
60 #[cfg(feature = "wasm")]
62 {
63 if let Some(window) = web_sys::window() {
64 if let Some(perf) = window.performance() {
65 return Self(perf.now());
66 }
67 }
68 }
69 Self(0.0)
70 }
71
72 fn elapsed(&self) -> std::time::Duration {
73 #[cfg(feature = "wasm")]
74 {
75 if let Some(window) = web_sys::window() {
76 if let Some(perf) = window.performance() {
77 let now = perf.now();
78 let elapsed_ms = now - self.0;
79 return std::time::Duration::from_micros((elapsed_ms * 1000.0) as u64);
80 }
81 }
82 }
83 std::time::Duration::ZERO
84 }
85}
86
87fn gen_range_usize(rng: &mut SimRng, max: usize) -> usize {
89 if max == 0 {
90 return 0;
91 }
92 (rng.gen_u64() as usize) % max
93}
94
95#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
97pub struct City {
98 pub x: f64,
99 pub y: f64,
100}
101
102impl City {
103 #[must_use]
105 pub const fn new(x: f64, y: f64) -> Self {
106 Self { x, y }
107 }
108
109 #[must_use]
111 pub fn distance_to(&self, other: &Self) -> f64 {
112 let dx = self.x - other.x;
113 let dy = self.y - other.y;
114 (dx * dx + dy * dy).sqrt()
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
120pub enum ConstructionMethod {
121 #[default]
123 RandomizedGreedy,
124 NearestNeighbor,
126 Random,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct TspGraspDemo {
133 pub cities: Vec<City>,
135 pub tour: Vec<usize>,
137 pub tour_length: f64,
139 pub best_tour: Vec<usize>,
141 pub best_tour_length: f64,
143 pub n: usize,
145 pub rcl_size: usize,
147 pub construction_method: ConstructionMethod,
149 pub two_opt_iterations: u64,
151 pub two_opt_improvements: u64,
153 pub restarts: u64,
155 pub restart_history: Vec<f64>,
157 pub lower_bound: f64,
159 pub area: f64,
161 pub seed: u64,
163 #[serde(default = "default_units")]
165 pub units: String,
166 #[serde(default)]
168 pub optimal_known: Option<u32>,
169 #[serde(default)]
171 pub stagnation_count: u64,
172 #[serde(default = "default_stagnation_threshold")]
174 pub stagnation_threshold: u64,
175 #[serde(default)]
177 pub converged: bool,
178 #[serde(default = "default_euclidean")]
181 pub is_euclidean: bool,
182 #[serde(skip)]
184 rng: Option<SimRng>,
185 #[serde(skip)]
187 distance_matrix: Vec<Vec<f64>>,
188 #[serde(skip)]
190 audit_log: Vec<StepEntry<TspStateSnapshot>>,
191 #[serde(skip)]
193 step_counter: u64,
194 #[serde(default = "default_audit_enabled")]
196 pub audit_enabled: bool,
197}
198
199fn default_units() -> String {
200 "units".to_string()
201}
202
203fn default_audit_enabled() -> bool {
204 true
205}
206
207fn default_stagnation_threshold() -> u64 {
209 10
210}
211
212fn default_euclidean() -> bool {
214 true
215}
216
217impl Default for TspGraspDemo {
218 fn default() -> Self {
219 Self::new(42, 20)
220 }
221}
222
223impl TspGraspDemo {
224 pub fn reinitialize(&mut self) {
227 if self.rng.is_none() {
228 self.rng = Some(SimRng::new(self.seed));
229 }
230 if self.distance_matrix.is_empty() && !self.cities.is_empty() {
231 self.precompute_distances();
232 }
233 if self.lower_bound == 0.0 && !self.cities.is_empty() {
234 self.compute_lower_bound();
235 }
236 }
237}
238
239impl TspGraspDemo {
240 #[must_use]
242 pub fn new(seed: u64, n: usize) -> Self {
243 let mut rng = SimRng::new(seed);
244 let n = n.max(4); let mut cities = Vec::with_capacity(n);
248 for _ in 0..n {
249 let x = rng.gen_range_f64(0.0, 1.0);
250 let y = rng.gen_range_f64(0.0, 1.0);
251 cities.push(City::new(x, y));
252 }
253
254 let mut demo = Self {
255 cities,
256 tour: Vec::new(),
257 tour_length: 0.0,
258 best_tour: Vec::new(),
259 best_tour_length: 0.0,
260 n,
261 rcl_size: 3, construction_method: ConstructionMethod::RandomizedGreedy,
263 two_opt_iterations: 0,
264 two_opt_improvements: 0,
265 restarts: 0,
266 restart_history: Vec::new(),
267 lower_bound: 0.0,
268 area: 1.0, seed,
270 units: "units".to_string(), optimal_known: None, stagnation_count: 0, stagnation_threshold: 10, converged: false, is_euclidean: true, rng: Some(rng),
277 distance_matrix: Vec::new(),
278 audit_log: Vec::new(),
279 step_counter: 0,
280 audit_enabled: true,
281 };
282
283 demo.precompute_distances();
284 demo.compute_lower_bound();
285 demo
286 }
287
288 #[must_use]
290 pub fn with_cities(seed: u64, cities: Vec<City>) -> Self {
291 let n = cities.len();
292 let rng = SimRng::new(seed);
293
294 let (min_x, max_x, min_y, max_y) = cities.iter().fold(
296 (
297 f64::INFINITY,
298 f64::NEG_INFINITY,
299 f64::INFINITY,
300 f64::NEG_INFINITY,
301 ),
302 |(min_x, max_x, min_y, max_y), c| {
303 (
304 min_x.min(c.x),
305 max_x.max(c.x),
306 min_y.min(c.y),
307 max_y.max(c.y),
308 )
309 },
310 );
311 let area = (max_x - min_x) * (max_y - min_y);
312
313 let mut demo = Self {
314 cities,
315 tour: Vec::new(),
316 tour_length: 0.0,
317 best_tour: Vec::new(),
318 best_tour_length: 0.0,
319 n,
320 rcl_size: 3,
321 construction_method: ConstructionMethod::RandomizedGreedy,
322 two_opt_iterations: 0,
323 two_opt_improvements: 0,
324 restarts: 0,
325 restart_history: Vec::new(),
326 lower_bound: 0.0,
327 area: area.max(0.001), seed,
329 units: "units".to_string(), optimal_known: None, stagnation_count: 0, stagnation_threshold: 10, converged: false, is_euclidean: true, rng: Some(rng),
336 distance_matrix: Vec::new(),
337 audit_log: Vec::new(),
338 step_counter: 0,
339 audit_enabled: true,
340 };
341
342 demo.precompute_distances();
343 demo.compute_lower_bound();
344 demo
345 }
346
347 #[must_use]
384 pub fn from_instance(instance: &TspInstanceYaml) -> Self {
385 let cities: Vec<City> = instance
388 .cities
389 .iter()
390 .map(|c| City::new(c.coords.lon, c.coords.lat))
391 .collect();
392
393 let seed = instance.algorithm.params.seed;
394 let mut demo = Self::with_cities(seed, cities);
395
396 demo.set_rcl_size(instance.algorithm.params.rcl_size);
398
399 let method = match instance.algorithm.method.as_str() {
401 "nearest_neighbor" | "nn" => ConstructionMethod::NearestNeighbor,
402 "random" => ConstructionMethod::Random,
403 _ => ConstructionMethod::RandomizedGreedy, };
405 demo.set_construction_method(method);
406
407 demo.distance_matrix = instance
410 .matrix
411 .iter()
412 .map(|row| row.iter().map(|&d| f64::from(d)).collect())
413 .collect();
414
415 demo.is_euclidean = false;
418
419 demo.units.clone_from(&instance.meta.units);
421 demo.optimal_known = instance.meta.optimal_known;
422
423 demo.compute_lower_bound();
425
426 demo
427 }
428
429 pub fn from_yaml(yaml: &str) -> Result<Self, TspInstanceError> {
449 let instance = TspInstanceYaml::from_yaml(yaml)?;
450 instance.validate()?;
451 Ok(Self::from_instance(&instance))
452 }
453
454 pub fn from_yaml_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, TspInstanceError> {
460 let instance = TspInstanceYaml::from_yaml_file(path)?;
461 instance.validate()?;
462 Ok(Self::from_instance(&instance))
463 }
464
465 pub fn set_construction_method(&mut self, method: ConstructionMethod) {
467 self.construction_method = method;
468 }
469
470 pub fn set_rcl_size(&mut self, size: usize) {
472 self.rcl_size = size.max(1).min(self.n);
473 }
474
475 fn precompute_distances(&mut self) {
477 self.distance_matrix = vec![vec![0.0; self.n]; self.n];
478 for i in 0..self.n {
479 for j in (i + 1)..self.n {
480 let d = self.cities[i].distance_to(&self.cities[j]);
481 self.distance_matrix[i][j] = d;
482 self.distance_matrix[j][i] = d;
483 }
484 }
485 }
486
487 #[must_use]
489 pub fn distance(&self, i: usize, j: usize) -> f64 {
490 if self.distance_matrix.is_empty() {
491 self.cities[i].distance_to(&self.cities[j])
492 } else {
493 self.distance_matrix[i][j]
494 }
495 }
496
497 #[must_use]
499 pub fn compute_tour_length(&self, tour: &[usize]) -> f64 {
500 if tour.len() < 2 {
501 return 0.0;
502 }
503
504 let mut length = 0.0;
505 for i in 0..tour.len() {
506 let j = (i + 1) % tour.len();
507 length += self.distance(tour[i], tour[j]);
508 }
509 length
510 }
511
512 fn compute_mst_excluding(&self, exclude_vertex: usize) -> f64 {
514 if self.n < 3 {
515 return 0.0;
516 }
517
518 let mut in_mst = vec![false; self.n];
520 let mut min_edge = vec![f64::INFINITY; self.n];
521
522 let start = (0..self.n).find(|&v| v != exclude_vertex).unwrap_or(0);
524 min_edge[start] = 0.0;
525 in_mst[exclude_vertex] = true; let mut mst_weight = 0.0;
528 let vertices_to_add = self.n - 1; for _ in 0..vertices_to_add {
531 let mut u = None;
533 let mut min_val = f64::INFINITY;
534 for (i, (&in_tree, &edge)) in in_mst.iter().zip(min_edge.iter()).enumerate() {
535 if !in_tree && edge < min_val {
536 min_val = edge;
537 u = Some(i);
538 }
539 }
540
541 let Some(u) = u else { break };
542
543 in_mst[u] = true;
544 mst_weight += min_val;
545
546 for v in 0..self.n {
548 if !in_mst[v] && v != exclude_vertex {
549 let d = self.distance(u, v);
550 if d < min_edge[v] {
551 min_edge[v] = d;
552 }
553 }
554 }
555 }
556
557 mst_weight
558 }
559
560 fn compute_one_tree_bound(&self) -> f64 {
569 if self.n < 3 {
570 return 0.0;
571 }
572
573 let mut best_bound = 0.0;
574
575 for exclude in 0..self.n {
576 let mst_weight = self.compute_mst_excluding(exclude);
578
579 let mut edges: Vec<f64> = (0..self.n)
581 .filter(|&v| v != exclude)
582 .map(|v| self.distance(exclude, v))
583 .collect();
584 edges.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
585
586 let one_tree =
588 mst_weight + edges.first().unwrap_or(&0.0) + edges.get(1).unwrap_or(&0.0);
589
590 if one_tree > best_bound {
591 best_bound = one_tree;
592 }
593 }
594
595 best_bound
596 }
597
598 fn compute_lower_bound(&mut self) {
600 if self.n < 2 {
601 self.lower_bound = 0.0;
602 return;
603 }
604
605 self.lower_bound = self.compute_one_tree_bound();
606 }
607
608 fn segments_intersect(p1: &City, p2: &City, p3: &City, p4: &City) -> bool {
610 fn cross(o: &City, a: &City, b: &City) -> f64 {
613 (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x)
614 }
615
616 let d1 = cross(p3, p4, p1);
617 let d2 = cross(p3, p4, p2);
618 let d3 = cross(p1, p2, p3);
619 let d4 = cross(p1, p2, p4);
620
621 if ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0))
623 && ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0))
624 {
625 return true;
626 }
627
628 false
629 }
630
631 #[must_use]
634 pub fn count_crossings(&self) -> usize {
635 let n = self.tour.len();
636 if n < 4 {
637 return 0;
638 }
639
640 let mut crossings = 0;
641
642 for i in 0..n {
643 let i_next = (i + 1) % n;
644 let p1 = &self.cities[self.tour[i]];
645 let p2 = &self.cities[self.tour[i_next]];
646
647 for j in (i + 2)..n {
649 let j_next = (j + 1) % n;
650
651 if j_next == i {
653 continue;
654 }
655
656 let p3 = &self.cities[self.tour[j]];
657 let p4 = &self.cities[self.tour[j_next]];
658
659 if Self::segments_intersect(p1, p2, p3, p4) {
660 crossings += 1;
661 }
662 }
663 }
664
665 crossings
666 }
667
668 #[must_use]
670 pub fn is_two_opt_optimal(&self) -> bool {
671 let n = self.tour.len();
672 if n < 4 {
673 return true;
674 }
675
676 for i in 0..(n - 2) {
677 for j in (i + 2)..n {
678 if i == 0 && j == n - 1 {
679 continue;
680 }
681 if self.two_opt_improvement(i, j) > f64::EPSILON {
682 return false;
683 }
684 }
685 }
686
687 true
688 }
689
690 fn construct_randomized_greedy(&mut self) {
692 let n = self.n;
693 let rcl_size_param = self.rcl_size;
694
695 let Some(mut rng) = self.rng.take() else {
697 return;
698 };
699
700 let mut visited = vec![false; n];
701 let mut tour = Vec::with_capacity(n);
702
703 let start = gen_range_usize(&mut rng, n);
705 tour.push(start);
706 visited[start] = true;
707
708 while tour.len() < n {
710 let Some(¤t) = tour.last() else {
711 break;
712 };
713
714 let mut candidates: Vec<(usize, f64)> = (0..n)
716 .filter(|&i| !visited[i])
717 .map(|i| (i, self.distance_matrix[current][i]))
718 .collect();
719
720 candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
721
722 let rcl_size = rcl_size_param.min(candidates.len());
724 let rcl = &candidates[..rcl_size];
725
726 let idx = gen_range_usize(&mut rng, rcl_size);
728 let next = rcl[idx].0;
729
730 tour.push(next);
731 visited[next] = true;
732 }
733
734 self.rng = Some(rng);
736
737 self.tour = tour;
738 self.tour_length = self.compute_tour_length(&self.tour);
739 }
740
741 fn construct_nearest_neighbor(&mut self) {
743 let n = self.n;
744
745 let Some(mut rng) = self.rng.take() else {
747 return;
748 };
749
750 let mut visited = vec![false; n];
751 let mut tour = Vec::with_capacity(n);
752
753 let start = gen_range_usize(&mut rng, n);
755 tour.push(start);
756 visited[start] = true;
757
758 while tour.len() < n {
759 let Some(¤t) = tour.last() else {
760 break;
761 };
762
763 let mut best_next = None;
765 let mut best_dist = f64::INFINITY;
766
767 for i in 0..n {
768 if !visited[i] {
769 let d = self.distance_matrix[current][i];
770 if d < best_dist {
771 best_dist = d;
772 best_next = Some(i);
773 }
774 }
775 }
776
777 if let Some(next) = best_next {
778 tour.push(next);
779 visited[next] = true;
780 }
781 }
782
783 self.rng = Some(rng);
785
786 self.tour = tour;
787 self.tour_length = self.compute_tour_length(&self.tour);
788 }
789
790 fn construct_random(&mut self) {
792 let n = self.n;
793
794 let Some(mut rng) = self.rng.take() else {
796 return;
797 };
798
799 let mut tour: Vec<usize> = (0..n).collect();
801 for i in (1..n).rev() {
802 let j = gen_range_usize(&mut rng, i + 1);
803 tour.swap(i, j);
804 }
805
806 self.rng = Some(rng);
808
809 self.tour = tour;
810 self.tour_length = self.compute_tour_length(&self.tour);
811 }
812
813 pub fn construct_tour(&mut self) {
815 match self.construction_method {
816 ConstructionMethod::RandomizedGreedy => self.construct_randomized_greedy(),
817 ConstructionMethod::NearestNeighbor => self.construct_nearest_neighbor(),
818 ConstructionMethod::Random => self.construct_random(),
819 }
820 }
821
822 #[must_use]
825 pub fn two_opt_improvement(&self, i: usize, j: usize) -> f64 {
826 let n = self.tour.len();
827 if n < 4 || i >= j || j >= n {
828 return 0.0;
829 }
830
831 let i_next = (i + 1) % n;
832 let j_next = (j + 1) % n;
833
834 let d_current = self.distance(self.tour[i], self.tour[i_next])
837 + self.distance(self.tour[j], self.tour[j_next]);
838
839 let d_new = self.distance(self.tour[i], self.tour[j])
840 + self.distance(self.tour[i_next], self.tour[j_next]);
841
842 d_current - d_new
843 }
844
845 fn apply_two_opt(&mut self, i: usize, j: usize) {
847 let mut left = i + 1;
849 let mut right = j;
850 while left < right {
851 self.tour.swap(left, right);
852 left += 1;
853 right -= 1;
854 }
855 self.tour_length = self.compute_tour_length(&self.tour);
856 }
857
858 pub fn two_opt_pass(&mut self) -> bool {
861 let n = self.tour.len();
862 if n < 4 {
863 return false;
864 }
865
866 self.two_opt_iterations += 1;
867
868 let mut best_improvement = 0.0;
870 let mut best_i = 0;
871 let mut best_j = 0;
872
873 for i in 0..(n - 2) {
874 for j in (i + 2)..n {
875 if i == 0 && j == n - 1 {
877 continue;
878 }
879
880 let improvement = self.two_opt_improvement(i, j);
881 if improvement > best_improvement {
882 best_improvement = improvement;
883 best_i = i;
884 best_j = j;
885 }
886 }
887 }
888
889 if best_improvement > f64::EPSILON {
890 self.apply_two_opt(best_i, best_j);
891 self.two_opt_improvements += 1;
892 true
893 } else {
894 false
895 }
896 }
897
898 pub fn two_opt_to_local_optimum(&mut self) {
900 while self.two_opt_pass() {}
901 }
902
903 pub fn grasp_iteration(&mut self) {
905 let start_time = Instant::now();
906 let input_snapshot = self.snapshot();
907 let rng_before = self.rng_state_hash();
908
909 self.construct_tour();
910 self.two_opt_to_local_optimum();
911
912 self.restarts += 1;
914 self.restart_history.push(self.tour_length);
915
916 let equations = vec![
918 EquationEval::new("tour_length", self.tour_length).with_input("n", self.n as f64),
919 EquationEval::new("optimality_gap", self.optimality_gap())
920 .with_input("tour_length", self.tour_length)
921 .with_input("lower_bound", self.lower_bound),
922 ];
923
924 let decisions = vec![Decision::new(
925 "construction_method",
926 format!("{:?}", self.construction_method),
927 )
928 .with_rationale("rcl_size", self.rcl_size as f64)];
929
930 self.log_audit_step(
931 TspStepType::GraspIteration,
932 input_snapshot,
933 rng_before,
934 start_time,
935 equations,
936 decisions,
937 );
938
939 if self.best_tour_length == 0.0 || self.tour_length < self.best_tour_length {
942 self.best_tour_length = self.tour_length;
943 self.best_tour = self.tour.clone();
944 self.stagnation_count = 0; } else {
946 self.stagnation_count += 1;
947 if self.stagnation_count >= self.stagnation_threshold {
949 self.converged = true;
950 }
951 }
952 }
953
954 pub fn run_grasp(&mut self, iterations: usize) {
956 for _ in 0..iterations {
957 if self.converged {
959 break;
960 }
961 self.grasp_iteration();
962 }
963 }
964
965 #[must_use]
967 pub const fn is_converged(&self) -> bool {
968 self.converged
969 }
970
971 pub fn set_stagnation_threshold(&mut self, threshold: u64) {
973 self.stagnation_threshold = threshold;
974 }
975
976 #[must_use]
978 pub fn optimality_gap(&self) -> f64 {
979 if self.lower_bound > f64::EPSILON {
980 (self.best_tour_length - self.lower_bound) / self.lower_bound
981 } else {
982 0.0
983 }
984 }
985
986 #[must_use]
988 pub fn bhh_expected_length(&self) -> f64 {
989 0.7124 * (self.n as f64 * self.area).sqrt()
991 }
992
993 #[must_use]
995 pub fn restart_variance(&self) -> f64 {
996 if self.restart_history.len() < 2 {
997 return 0.0;
998 }
999
1000 let n = self.restart_history.len() as f64;
1001 let mean: f64 = self.restart_history.iter().sum::<f64>() / n;
1002 let variance: f64 = self
1003 .restart_history
1004 .iter()
1005 .map(|x| (x - mean).powi(2))
1006 .sum::<f64>()
1007 / n;
1008
1009 variance
1010 }
1011
1012 #[must_use]
1014 pub fn restart_cv(&self) -> f64 {
1015 if self.restart_history.is_empty() {
1016 return 0.0;
1017 }
1018
1019 let mean: f64 =
1020 self.restart_history.iter().sum::<f64>() / self.restart_history.len() as f64;
1021 if mean > f64::EPSILON {
1022 self.restart_variance().sqrt() / mean
1023 } else {
1024 0.0
1025 }
1026 }
1027
1028 #[must_use]
1034 fn snapshot(&self) -> TspStateSnapshot {
1035 TspStateSnapshot {
1036 tour: self.tour.clone(),
1037 tour_length: self.tour_length,
1038 best_tour: self.best_tour.clone(),
1039 best_tour_length: self.best_tour_length,
1040 restarts: self.restarts,
1041 two_opt_iterations: self.two_opt_iterations,
1042 two_opt_improvements: self.two_opt_improvements,
1043 }
1044 }
1045
1046 fn rng_state_hash(&self) -> [u8; 32] {
1048 self.rng.as_ref().map_or([0u8; 32], |rng| {
1049 let state_bytes = rng.state_bytes();
1051 *blake3::hash(&state_bytes).as_bytes()
1052 })
1053 }
1054
1055 fn log_audit_step(
1057 &mut self,
1058 step_type: TspStepType,
1059 input_snapshot: TspStateSnapshot,
1060 rng_before: [u8; 32],
1061 start_time: Instant,
1062 equations: Vec<EquationEval>,
1063 decisions: Vec<Decision>,
1064 ) {
1065 if !self.audit_enabled {
1066 return;
1067 }
1068
1069 let rng_after = self.rng_state_hash();
1070 let output_snapshot = self.snapshot();
1071 let duration_us = start_time.elapsed().as_micros() as u64;
1072
1073 let mut entry = StepEntry::new(
1074 self.step_counter,
1075 SimTime::from_secs(self.step_counter as f64 * 0.001), step_type.to_string(),
1077 input_snapshot,
1078 output_snapshot,
1079 )
1080 .with_rng_states(rng_before, rng_after)
1081 .with_duration(duration_us);
1082
1083 for eq in equations {
1084 entry.add_equation_eval(eq);
1085 }
1086
1087 for dec in decisions {
1088 entry.add_decision(dec);
1089 }
1090
1091 self.audit_log.push(entry);
1092 self.step_counter += 1;
1093 }
1094
1095 pub fn set_audit_enabled(&mut self, enabled: bool) {
1097 self.audit_enabled = enabled;
1098 }
1099}
1100
1101impl SimulationAuditLog for TspGraspDemo {
1106 type StateSnapshot = TspStateSnapshot;
1107
1108 fn log_step(&mut self, entry: StepEntry<Self::StateSnapshot>) {
1109 self.audit_log.push(entry);
1110 }
1111
1112 fn audit_log(&self) -> &[StepEntry<Self::StateSnapshot>] {
1113 &self.audit_log
1114 }
1115
1116 fn audit_log_mut(&mut self) -> &mut Vec<StepEntry<Self::StateSnapshot>> {
1117 &mut self.audit_log
1118 }
1119
1120 fn clear_audit_log(&mut self) {
1121 self.audit_log.clear();
1122 self.step_counter = 0;
1123 }
1124}
1125
1126impl EddDemo for TspGraspDemo {
1127 fn name(&self) -> &'static str {
1128 "TSP Randomized Greedy Start with 2-Opt"
1129 }
1130
1131 fn emc_ref(&self) -> &'static str {
1132 "optimization/tsp_grasp_2opt"
1133 }
1134
1135 fn step(&mut self, _dt: f64) {
1136 self.grasp_iteration();
1138 }
1139
1140 fn verify_equation(&self) -> bool {
1141 let gap_ok = self.optimality_gap() < 0.20;
1143
1144 let cv_ok = self.restart_cv() < 0.05 || self.restart_history.len() < 5;
1146
1147 let no_crossings = !self.is_euclidean || self.best_tour.is_empty() || {
1150 let mut temp = self.clone();
1152 temp.tour.clone_from(&self.best_tour);
1153 temp.count_crossings() == 0
1154 };
1155
1156 gap_ok && cv_ok && no_crossings
1157 }
1158
1159 fn get_falsification_status(&self) -> FalsificationStatus {
1160 let gap = self.optimality_gap();
1161 let cv = self.restart_cv();
1162
1163 let crossings = if self.best_tour.is_empty() {
1165 0
1166 } else {
1167 let mut temp = self.clone();
1168 temp.tour.clone_from(&self.best_tour);
1169 temp.count_crossings()
1170 };
1171
1172 let gap_passed = gap < 0.20;
1173 let cv_passed = cv < 0.05 || self.restart_history.len() < 5;
1174
1175 let crossings_passed = !self.is_euclidean || crossings == 0;
1179
1180 FalsificationStatus {
1181 verified: gap_passed && cv_passed && crossings_passed,
1182 criteria: vec![
1183 CriterionStatus {
1185 id: "TSP-CROSSINGS".to_string(),
1186 name: if self.is_euclidean {
1187 "Edge crossings (Jidoka)".to_string()
1188 } else {
1189 "Edge crossings (N/A: non-Euclidean)".to_string()
1190 },
1191 passed: crossings_passed,
1192 value: crossings as f64,
1193 threshold: 0.0,
1194 },
1195 CriterionStatus {
1196 id: "TSP-GAP".to_string(),
1197 name: "Optimality gap (1-tree, Held-Karp 1970)".to_string(),
1198 passed: gap_passed,
1199 value: gap,
1200 threshold: 0.20,
1201 },
1202 CriterionStatus {
1203 id: "TSP-VARIANCE".to_string(),
1204 name: "Restart consistency (CV)".to_string(),
1205 passed: cv_passed,
1206 value: cv,
1207 threshold: 0.05,
1208 },
1209 ],
1210 message: if gap_passed && cv_passed && crossings_passed {
1211 if self.is_euclidean {
1212 format!(
1213 "TSP GRASP verified: crossings=0, gap={:.1}%, CV={:.1}%, best={:.1}",
1214 gap * 100.0,
1215 cv * 100.0,
1216 self.best_tour_length
1217 )
1218 } else {
1219 format!(
1220 "TSP GRASP verified (non-Euclidean): gap={:.1}%, CV={:.1}%, best={:.1}",
1221 gap * 100.0,
1222 cv * 100.0,
1223 self.best_tour_length
1224 )
1225 }
1226 } else if !crossings_passed && self.is_euclidean {
1227 format!(
1229 "JIDOKA STOP: Tour has {crossings} edge crossing(s) - \
1230 Euclidean TSP tours MUST have 0 (Lin & Kernighan, 1973)"
1231 )
1232 } else if !gap_passed {
1233 format!(
1234 "Optimality gap too large: {:.1}% (expected <20%, Held-Karp 1970)",
1235 gap * 100.0
1236 )
1237 } else {
1238 format!(
1239 "Restart variance too high: CV={:.1}% (expected <5%)",
1240 cv * 100.0
1241 )
1242 },
1243 }
1244 }
1245
1246 fn reset(&mut self) {
1247 let seed = self.seed;
1248 let n = self.n;
1249 let method = self.construction_method;
1250 let rcl_size = self.rcl_size;
1251 let cities = self.cities.clone();
1252
1253 *self = Self::with_cities(seed, cities);
1254 self.construction_method = method;
1255 self.rcl_size = rcl_size;
1256 self.n = n;
1257 }
1258}
1259
1260#[cfg(feature = "wasm")]
1265mod wasm {
1266 use super::{City, ConstructionMethod, EddDemo, TspGraspDemo};
1267 use wasm_bindgen::prelude::*;
1268
1269 #[wasm_bindgen]
1270 pub struct WasmTspGrasp {
1271 inner: TspGraspDemo,
1272 }
1273
1274 #[wasm_bindgen]
1275 impl WasmTspGrasp {
1276 #[wasm_bindgen(constructor)]
1281 pub fn new(seed: u32, n: usize) -> Self {
1282 Self {
1283 inner: TspGraspDemo::new(u64::from(seed), n),
1284 }
1285 }
1286
1287 #[wasm_bindgen(js_name = fromYaml)]
1313 pub fn from_yaml(yaml: &str) -> Result<Self, String> {
1314 let inner = TspGraspDemo::from_yaml(yaml).map_err(|e| e.to_string())?;
1315 Ok(Self { inner })
1316 }
1317
1318 #[wasm_bindgen(js_name = withCities)]
1321 pub fn with_cities_js(seed: u32, cities_js: &JsValue) -> Result<Self, JsValue> {
1322 let cities_array: Vec<Vec<f64>> = serde_wasm_bindgen::from_value(cities_js.clone())
1323 .map_err(|e| JsValue::from_str(&format!("Invalid cities format: {e}")))?;
1324
1325 let cities: Vec<City> = cities_array
1326 .into_iter()
1327 .filter_map(|coords| {
1328 if coords.len() >= 2 {
1329 Some(City::new(coords[0], coords[1]))
1330 } else {
1331 None
1332 }
1333 })
1334 .collect();
1335
1336 if cities.len() < 3 {
1337 return Err(JsValue::from_str("Need at least 3 cities"));
1338 }
1339
1340 Ok(Self {
1341 inner: TspGraspDemo::with_cities(u64::from(seed), cities),
1342 })
1343 }
1344
1345 pub fn step(&mut self) {
1350 self.inner.step(0.0);
1351 }
1352
1353 #[wasm_bindgen(js_name = graspIteration)]
1355 pub fn grasp_iteration(&mut self) {
1356 self.inner.grasp_iteration();
1357 }
1358
1359 #[wasm_bindgen(js_name = runGrasp)]
1361 pub fn run_grasp(&mut self, iterations: usize) {
1362 self.inner.run_grasp(iterations);
1363 }
1364
1365 #[wasm_bindgen(js_name = constructTour)]
1367 pub fn construct_tour(&mut self) {
1368 self.inner.construct_tour();
1369 }
1370
1371 #[wasm_bindgen(js_name = twoOptPass)]
1373 pub fn two_opt_pass(&mut self) -> bool {
1374 self.inner.two_opt_pass()
1375 }
1376
1377 #[wasm_bindgen(js_name = twoOptToLocalOptimum)]
1379 pub fn two_opt_to_local_optimum(&mut self) {
1380 self.inner.two_opt_to_local_optimum();
1381 }
1382
1383 pub fn reset(&mut self) {
1384 self.inner.reset();
1385 }
1386
1387 #[wasm_bindgen(js_name = setConstructionMethod)]
1393 pub fn set_construction_method(&mut self, method: u8) {
1394 self.inner.construction_method = match method {
1395 1 => ConstructionMethod::NearestNeighbor,
1396 2 => ConstructionMethod::Random,
1397 _ => ConstructionMethod::RandomizedGreedy, };
1399 }
1400
1401 #[wasm_bindgen(js_name = setRclSize)]
1403 pub fn set_rcl_size(&mut self, size: usize) {
1404 self.inner.set_rcl_size(size);
1405 }
1406
1407 #[wasm_bindgen(js_name = getTourLength)]
1412 pub fn get_tour_length(&self) -> f64 {
1413 self.inner.tour_length
1414 }
1415
1416 #[wasm_bindgen(js_name = getBestTourLength)]
1417 pub fn get_best_tour_length(&self) -> f64 {
1418 self.inner.best_tour_length
1419 }
1420
1421 #[wasm_bindgen(js_name = getOptimalityGap)]
1422 pub fn get_optimality_gap(&self) -> f64 {
1423 self.inner.optimality_gap()
1424 }
1425
1426 #[wasm_bindgen(js_name = getLowerBound)]
1427 pub fn get_lower_bound(&self) -> f64 {
1428 self.inner.lower_bound
1429 }
1430
1431 #[wasm_bindgen(js_name = getRestarts)]
1432 pub fn get_restarts(&self) -> u64 {
1433 self.inner.restarts
1434 }
1435
1436 #[wasm_bindgen(js_name = getTwoOptIterations)]
1437 pub fn get_two_opt_iterations(&self) -> u64 {
1438 self.inner.two_opt_iterations
1439 }
1440
1441 #[wasm_bindgen(js_name = getTwoOptImprovements)]
1442 pub fn get_two_opt_improvements(&self) -> u64 {
1443 self.inner.two_opt_improvements
1444 }
1445
1446 #[wasm_bindgen(js_name = getN)]
1447 pub fn get_n(&self) -> usize {
1448 self.inner.n
1449 }
1450
1451 #[wasm_bindgen(js_name = getUnits)]
1453 pub fn get_units(&self) -> String {
1454 self.inner.units.clone()
1455 }
1456
1457 #[wasm_bindgen(js_name = getOptimalKnown)]
1459 pub fn get_optimal_known(&self) -> Option<u32> {
1460 self.inner.optimal_known
1461 }
1462
1463 #[wasm_bindgen(js_name = getRclSize)]
1464 pub fn get_rcl_size(&self) -> usize {
1465 self.inner.rcl_size
1466 }
1467
1468 #[wasm_bindgen(js_name = getConstructionMethod)]
1469 pub fn get_construction_method(&self) -> u8 {
1470 match self.inner.construction_method {
1471 ConstructionMethod::RandomizedGreedy => 0,
1472 ConstructionMethod::NearestNeighbor => 1,
1473 ConstructionMethod::Random => 2,
1474 }
1475 }
1476
1477 #[wasm_bindgen(js_name = getRestartVariance)]
1478 pub fn get_restart_variance(&self) -> f64 {
1479 self.inner.restart_variance()
1480 }
1481
1482 #[wasm_bindgen(js_name = getRestartCv)]
1483 pub fn get_restart_cv(&self) -> f64 {
1484 self.inner.restart_cv()
1485 }
1486
1487 #[wasm_bindgen(js_name = getCities)]
1493 pub fn get_cities_js(&self) -> JsValue {
1494 let cities: Vec<[f64; 2]> = self.inner.cities.iter().map(|c| [c.x, c.y]).collect();
1495 serde_wasm_bindgen::to_value(&cities).unwrap_or(JsValue::NULL)
1496 }
1497
1498 #[wasm_bindgen(js_name = getTour)]
1500 pub fn get_tour_js(&self) -> JsValue {
1501 serde_wasm_bindgen::to_value(&self.inner.tour).unwrap_or(JsValue::NULL)
1502 }
1503
1504 #[wasm_bindgen(js_name = getBestTour)]
1506 pub fn get_best_tour_js(&self) -> JsValue {
1507 serde_wasm_bindgen::to_value(&self.inner.best_tour).unwrap_or(JsValue::NULL)
1508 }
1509
1510 #[wasm_bindgen(js_name = getRestartHistory)]
1512 pub fn get_restart_history_js(&self) -> JsValue {
1513 serde_wasm_bindgen::to_value(&self.inner.restart_history).unwrap_or(JsValue::NULL)
1514 }
1515
1516 #[wasm_bindgen(js_name = getState)]
1518 pub fn get_state_js(&self) -> JsValue {
1519 serde_wasm_bindgen::to_value(&self.inner).unwrap_or(JsValue::NULL)
1520 }
1521
1522 pub fn verify(&self) -> bool {
1527 self.inner.verify_equation()
1528 }
1529
1530 #[wasm_bindgen(js_name = getFalsificationStatus)]
1532 pub fn get_falsification_status_js(&self) -> JsValue {
1533 let status = self.inner.get_falsification_status();
1534 serde_wasm_bindgen::to_value(&status).unwrap_or(JsValue::NULL)
1535 }
1536
1537 #[wasm_bindgen(js_name = getName)]
1539 pub fn get_name(&self) -> String {
1540 self.inner.name().to_string()
1541 }
1542
1543 #[wasm_bindgen(js_name = getEmcRef)]
1545 pub fn get_emc_ref(&self) -> String {
1546 self.inner.emc_ref().to_string()
1547 }
1548 }
1549}
1550
1551#[cfg(test)]
1552mod quality_tests;
1553#[cfg(test)]
1554mod tests;