Skip to main content

simular/demos/tsp_grasp/
mod.rs

1//! Demo 6: TSP Randomized Greedy Start with 2-Opt
2//!
3//! Demonstrates the GRASP (Greedy Randomized Adaptive Search Procedure) methodology
4//! for the Traveling Salesman Problem.
5//!
6//! # Governing Equations
7//!
8//! ```text
9//! Tour Length:           L(π) = Σᵢ d(π(i), π(i+1)) + d(π(n), π(1))
10//! 2-Opt Improvement:     Δ = d(i,i+1) + d(j,j+1) - d(i,j) - d(i+1,j+1)
11//! Expected Greedy Tour:  E[L_greedy] ≈ 0.7124·√(n·A)  [BHH 1959]
12//! 1-Tree Lower Bound:    L* ≥ MST(G\{v₀}) + 2·min_edges(v₀)  [Held-Karp 1970]
13//! ```
14//!
15//! # EDD Cycle
16//!
17//! 1. **Equation**: Greedy + 2-opt achieves tours within ~5% of optimal [JM 1997]
18//! 2. **Failing Test**: Assert `tour_length` / `lower_bound` < 1.10 (10% optimality gap)
19//! 3. **Implementation**: Randomized nearest-neighbor + exhaustive 2-opt local search
20//! 4. **Verification**: Multiple random starts converge to similar tour lengths
21//! 5. **Falsification**: Pure random start (no greedy) yields significantly worse results
22//!
23//! # Toyota Production System Compliance
24//!
25//! - **Jidoka**: Edge crossing detection (Lin-Kernighan 1973: Euclidean TSP optimal has 0 crossings)
26//! - **Muda**: Stagnation detection eliminates restart waste (early termination)
27//! - **Heijunka**: Adaptive tick rate based on convergence status
28//! - **Poka-Yoke**: RCL size clamping, input validation
29//!
30//! # References (IEEE Style)
31//!
32//! - \[51\] Feo & Resende (1995). GRASP. *J. Global Optimization*, 6(2), 109-133.
33//! - \[52\] Lin & Kernighan (1973). TSP Heuristics. *Operations Research*, 21(2), 498-516.
34//! - \[53\] Johnson & `McGeoch` (1997). TSP Case Study. *Local Search in Comb. Opt.*
35//! - \[54\] Christofides (1976). TSP Heuristic. *CMU Report 388*.
36//! - \[55\] Beardwood, Halton & Hammersley (1959). Shortest Path. *Proc. Cambridge*, 55, 299-327.
37//! - \[56\] Held & Karp (1970). TSP and MST. *Operations Research*, 18(6), 1138-1162.
38
39use 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// WASM-compatible timing abstraction
49// std::time::Instant::now() panics in WASM - use web_sys::Performance or dummy
50#[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        // Use performance.now() if available, otherwise 0
61        #[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
87/// Helper to generate random usize in range [0, max).
88fn 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/// A 2D point representing a city.
96#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
97pub struct City {
98    pub x: f64,
99    pub y: f64,
100}
101
102impl City {
103    /// Create a new city at coordinates (x, y).
104    #[must_use]
105    pub const fn new(x: f64, y: f64) -> Self {
106        Self { x, y }
107    }
108
109    /// Euclidean distance to another city.
110    #[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/// Construction method for initial tour.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
120pub enum ConstructionMethod {
121    /// Randomized greedy (GRASP-style RCL selection).
122    #[default]
123    RandomizedGreedy,
124    /// Pure nearest neighbor (deterministic greedy).
125    NearestNeighbor,
126    /// Pure random tour (for falsification).
127    Random,
128}
129
130/// TSP GRASP Demo state.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct TspGraspDemo {
133    /// Cities to visit.
134    pub cities: Vec<City>,
135    /// Current tour (indices into cities).
136    pub tour: Vec<usize>,
137    /// Current tour length.
138    pub tour_length: f64,
139    /// Best tour found across restarts.
140    pub best_tour: Vec<usize>,
141    /// Best tour length found.
142    pub best_tour_length: f64,
143    /// Number of cities.
144    pub n: usize,
145    /// RCL size for randomized greedy (k nearest candidates).
146    pub rcl_size: usize,
147    /// Construction method.
148    pub construction_method: ConstructionMethod,
149    /// Number of 2-opt iterations performed.
150    pub two_opt_iterations: u64,
151    /// Number of 2-opt improvements made.
152    pub two_opt_improvements: u64,
153    /// Number of GRASP restarts performed.
154    pub restarts: u64,
155    /// History of tour lengths from each restart.
156    pub restart_history: Vec<f64>,
157    /// Lower bound estimate (MST-based).
158    pub lower_bound: f64,
159    /// Area of bounding box (for BHH constant).
160    pub area: f64,
161    /// Seed for reproducibility.
162    pub seed: u64,
163    /// Unit label for distances (e.g., "miles", "km").
164    #[serde(default = "default_units")]
165    pub units: String,
166    /// Known optimal tour length (if available from YAML).
167    #[serde(default)]
168    pub optimal_known: Option<u32>,
169    /// MUDA: Consecutive restarts without improvement (stagnation detection).
170    #[serde(default)]
171    pub stagnation_count: u64,
172    /// MUDA: Threshold for early termination (default: 10 restarts).
173    #[serde(default = "default_stagnation_threshold")]
174    pub stagnation_threshold: u64,
175    /// MUDA: Flag indicating convergence (no further improvement expected).
176    #[serde(default)]
177    pub converged: bool,
178    /// Flag indicating if distances are Euclidean (computed from coords).
179    /// Lin-Kernighan crossing theorem ONLY applies to Euclidean instances.
180    #[serde(default = "default_euclidean")]
181    pub is_euclidean: bool,
182    /// RNG for stochastic elements.
183    #[serde(skip)]
184    rng: Option<SimRng>,
185    /// Distance matrix (precomputed for efficiency).
186    #[serde(skip)]
187    distance_matrix: Vec<Vec<f64>>,
188    /// Audit log for step-by-step tracking (EDD-16).
189    #[serde(skip)]
190    audit_log: Vec<StepEntry<TspStateSnapshot>>,
191    /// Step counter for audit logging.
192    #[serde(skip)]
193    step_counter: u64,
194    /// Enable audit logging (can be disabled for performance).
195    #[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
207/// MUDA: Default stagnation threshold (10 restarts without improvement).
208fn default_stagnation_threshold() -> u64 {
209    10
210}
211
212/// Default: assume Euclidean (computed from coordinates).
213fn 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    /// Ensure demo is properly initialized after deserialization.
225    /// Call this after deserializing to rebuild RNG and distance matrix.
226    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    /// Create a new TSP GRASP demo with random cities.
241    #[must_use]
242    pub fn new(seed: u64, n: usize) -> Self {
243        let mut rng = SimRng::new(seed);
244        let n = n.max(4); // Minimum 4 cities for meaningful TSP
245
246        // Generate random cities in unit square
247        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, // Default RCL size
262            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, // Unit square
269            seed,
270            units: "units".to_string(), // Default for random cities
271            optimal_known: None,        // Unknown for random instances
272            stagnation_count: 0,        // MUDA: Start fresh
273            stagnation_threshold: 10,   // MUDA: 10 restarts without improvement
274            converged: false,           // MUDA: Not yet converged
275            is_euclidean: true,         // Random cities use Euclidean distances
276            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    /// Create demo with specific cities.
289    #[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        // Compute bounding box area
295        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), // Avoid zero area
328            seed,
329            units: "units".to_string(), // Default for custom cities
330            optimal_known: None,        // Unknown for custom instances
331            stagnation_count: 0,        // MUDA: Start fresh
332            stagnation_threshold: 10,   // MUDA: 10 restarts without improvement
333            converged: false,           // MUDA: Not yet converged
334            is_euclidean: true,         // Custom cities use Euclidean distances
335            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    /// Create demo from a YAML instance configuration.
348    ///
349    /// This enables the YAML-first architecture where users can modify
350    /// `bay_area_tsp.yaml` and run the same demo in TUI or WASM.
351    ///
352    /// # Arguments
353    ///
354    /// * `instance` - Parsed YAML configuration
355    ///
356    /// # Example
357    ///
358    /// ```
359    /// use simular::demos::{TspGraspDemo, TspInstanceYaml};
360    ///
361    /// let yaml = r#"
362    /// meta:
363    ///   id: "TEST"
364    ///   description: "Test"
365    /// cities:
366    ///   - id: 0
367    ///     name: "A"
368    ///     alias: "A"
369    ///     coords: { lat: 0.0, lon: 0.0 }
370    ///   - id: 1
371    ///     name: "B"
372    ///     alias: "B"
373    ///     coords: { lat: 1.0, lon: 1.0 }
374    /// matrix:
375    ///   - [0, 10]
376    ///   - [10, 0]
377    /// "#;
378    ///
379    /// let instance = TspInstanceYaml::from_yaml(yaml).unwrap();
380    /// let demo = TspGraspDemo::from_instance(&instance);
381    /// assert_eq!(demo.n, 2);
382    /// ```
383    #[must_use]
384    pub fn from_instance(instance: &TspInstanceYaml) -> Self {
385        // Convert TspCity coords (lat/lon) to City (x/y)
386        // Use lon as x, lat as y (standard mapping)
387        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        // Apply algorithm configuration
397        demo.set_rcl_size(instance.algorithm.params.rcl_size);
398
399        // Set construction method based on YAML config
400        let method = match instance.algorithm.method.as_str() {
401            "nearest_neighbor" | "nn" => ConstructionMethod::NearestNeighbor,
402            "random" => ConstructionMethod::Random,
403            _ => ConstructionMethod::RandomizedGreedy, // "grasp" is default
404        };
405        demo.set_construction_method(method);
406
407        // Override distance matrix with YAML-provided distances
408        // This allows users to specify exact driving distances rather than Euclidean
409        demo.distance_matrix = instance
410            .matrix
411            .iter()
412            .map(|row| row.iter().map(|&d| f64::from(d)).collect())
413            .collect();
414
415        // YAML instances with distance matrices are NON-EUCLIDEAN
416        // Lin-Kernighan crossing theorem does NOT apply
417        demo.is_euclidean = false;
418
419        // Copy metadata from YAML instance
420        demo.units.clone_from(&instance.meta.units);
421        demo.optimal_known = instance.meta.optimal_known;
422
423        // Recompute lower bound with actual distances
424        demo.compute_lower_bound();
425
426        demo
427    }
428
429    /// Load demo from YAML string.
430    ///
431    /// This is the primary entry point for YAML-first configuration.
432    ///
433    /// # Errors
434    ///
435    /// Returns error if:
436    /// - YAML parsing fails
437    /// - Validation fails (matrix size, symmetry, etc.)
438    ///
439    /// # Example
440    ///
441    /// ```
442    /// use simular::demos::TspGraspDemo;
443    ///
444    /// let yaml = include_str!("../../../examples/experiments/bay_area_tsp.yaml");
445    /// let demo = TspGraspDemo::from_yaml(yaml).expect("YAML should parse");
446    /// assert_eq!(demo.n, 20); // 20-city California instance
447    /// ```
448    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    /// Load demo from YAML file.
455    ///
456    /// # Errors
457    ///
458    /// Returns error if file cannot be read or YAML is invalid.
459    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    /// Set construction method.
466    pub fn set_construction_method(&mut self, method: ConstructionMethod) {
467        self.construction_method = method;
468    }
469
470    /// Set RCL size for randomized greedy.
471    pub fn set_rcl_size(&mut self, size: usize) {
472        self.rcl_size = size.max(1).min(self.n);
473    }
474
475    /// Precompute distance matrix for O(1) lookups.
476    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    /// Get distance between two cities (O(1) with precomputed matrix).
488    #[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    /// Compute tour length for a given tour.
498    #[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    /// Compute MST on vertices excluding `exclude_vertex`.
513    fn compute_mst_excluding(&self, exclude_vertex: usize) -> f64 {
514        if self.n < 3 {
515            return 0.0;
516        }
517
518        // Prim's MST algorithm on all vertices except exclude_vertex
519        let mut in_mst = vec![false; self.n];
520        let mut min_edge = vec![f64::INFINITY; self.n];
521
522        // Find first vertex that's not excluded
523        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; // Mark excluded vertex as "in MST" to skip it
526
527        let mut mst_weight = 0.0;
528        let vertices_to_add = self.n - 1; // All except excluded
529
530        for _ in 0..vertices_to_add {
531            // Find minimum edge to non-MST vertex
532            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            // Update minimum edges
547            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    /// Compute 1-tree lower bound (Held-Karp style).
561    ///
562    /// For each vertex v:
563    /// 1. Compute MST on remaining n-1 vertices
564    /// 2. Add two smallest edges from v to the MST
565    /// 3. This gives a lower bound for TSP
566    ///
567    /// The maximum over all vertices is the 1-tree bound.
568    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            // Compute MST excluding this vertex
577            let mst_weight = self.compute_mst_excluding(exclude);
578
579            // Find two smallest edges from excluded vertex to MST
580            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            // Add two smallest edges
587            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    /// Compute lower bound using 1-tree (tighter than MST).
599    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    /// Check if two line segments intersect (for crossing detection).
609    fn segments_intersect(p1: &City, p2: &City, p3: &City, p4: &City) -> bool {
610        // Returns true if segment p1-p2 intersects segment p3-p4
611        // Using cross product method
612        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        // Segments intersect if points are on opposite sides
622        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    /// Count edge crossings in current tour.
632    /// For 2D Euclidean TSP, optimal tour has 0 crossings.
633    #[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            // Check against all non-adjacent edges
648            for j in (i + 2)..n {
649                let j_next = (j + 1) % n;
650
651                // Skip if edges share a vertex
652                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    /// Check if tour is at a 2-opt local optimum.
669    #[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    /// Construct initial tour using randomized greedy (GRASP).
691    fn construct_randomized_greedy(&mut self) {
692        let n = self.n;
693        let rcl_size_param = self.rcl_size;
694
695        // Take RNG out temporarily
696        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        // Start from random city
704        let start = gen_range_usize(&mut rng, n);
705        tour.push(start);
706        visited[start] = true;
707
708        // Build tour using RCL
709        while tour.len() < n {
710            let Some(&current) = tour.last() else {
711                break;
712            };
713
714            // Find k nearest unvisited cities (RCL)
715            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            // Take top k candidates (or all remaining if fewer)
723            let rcl_size = rcl_size_param.min(candidates.len());
724            let rcl = &candidates[..rcl_size];
725
726            // Select randomly from RCL
727            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        // Put RNG back
735        self.rng = Some(rng);
736
737        self.tour = tour;
738        self.tour_length = self.compute_tour_length(&self.tour);
739    }
740
741    /// Construct initial tour using pure nearest neighbor.
742    fn construct_nearest_neighbor(&mut self) {
743        let n = self.n;
744
745        // Take RNG out temporarily
746        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        // Start from random city
754        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(&current) = tour.last() else {
760                break;
761            };
762
763            // Find nearest unvisited city
764            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        // Put RNG back
784        self.rng = Some(rng);
785
786        self.tour = tour;
787        self.tour_length = self.compute_tour_length(&self.tour);
788    }
789
790    /// Construct initial tour randomly (for falsification).
791    fn construct_random(&mut self) {
792        let n = self.n;
793
794        // Take RNG out temporarily
795        let Some(mut rng) = self.rng.take() else {
796            return;
797        };
798
799        // Fisher-Yates shuffle
800        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        // Put RNG back
807        self.rng = Some(rng);
808
809        self.tour = tour;
810        self.tour_length = self.compute_tour_length(&self.tour);
811    }
812
813    /// Construct initial tour based on selected method.
814    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    /// Compute 2-opt improvement for swapping edges (i, i+1) and (j, j+1).
823    /// Returns positive value if swap improves tour.
824    #[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        // Current edges: (tour[i], tour[i+1]) and (tour[j], tour[j+1])
835        // New edges: (tour[i], tour[j]) and (tour[i+1], tour[j+1])
836        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    /// Apply 2-opt swap: reverse segment between i+1 and j.
846    fn apply_two_opt(&mut self, i: usize, j: usize) {
847        // Reverse the segment from i+1 to j
848        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    /// Perform one pass of 2-opt local search.
859    /// Returns true if an improvement was made.
860    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        // Find best improving move
869        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                // Skip adjacent edges that share a node
876                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    /// Run 2-opt to local optimum.
899    pub fn two_opt_to_local_optimum(&mut self) {
900        while self.two_opt_pass() {}
901    }
902
903    /// Run one GRASP iteration: construct + 2-opt.
904    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        // Track restart
913        self.restarts += 1;
914        self.restart_history.push(self.tour_length);
915
916        // Log construction + 2-opt as single GRASP iteration
917        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        // Update best (0.0 means no tour yet computed)
940        // MUDA: Track stagnation to eliminate restart waste
941        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; // Reset on improvement
945        } else {
946            self.stagnation_count += 1;
947            // MUDA: Mark converged when stagnation threshold reached
948            if self.stagnation_count >= self.stagnation_threshold {
949                self.converged = true;
950            }
951        }
952    }
953
954    /// Run multiple GRASP iterations.
955    pub fn run_grasp(&mut self, iterations: usize) {
956        for _ in 0..iterations {
957            // MUDA: Skip iterations after convergence (eliminate waste)
958            if self.converged {
959                break;
960            }
961            self.grasp_iteration();
962        }
963    }
964
965    /// Check if the algorithm has converged (MUDA: stagnation detection).
966    #[must_use]
967    pub const fn is_converged(&self) -> bool {
968        self.converged
969    }
970
971    /// Set stagnation threshold (MUDA: configurable waste elimination).
972    pub fn set_stagnation_threshold(&mut self, threshold: u64) {
973        self.stagnation_threshold = threshold;
974    }
975
976    /// Get optimality gap: (`tour_length` - `lower_bound`) / `lower_bound`.
977    #[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    /// Get expected tour length from BHH formula.
987    #[must_use]
988    pub fn bhh_expected_length(&self) -> f64 {
989        // E[L] ≈ 0.7124 * sqrt(n * A)
990        0.7124 * (self.n as f64 * self.area).sqrt()
991    }
992
993    /// Compute variance of restart tour lengths.
994    #[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    /// Compute coefficient of variation of restart tour lengths.
1013    #[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    // =========================================================================
1029    // Audit Logging (EDD-16, EDD-17, EDD-18)
1030    // =========================================================================
1031
1032    /// Create a state snapshot for audit logging.
1033    #[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    /// Get RNG state hash (for audit logging).
1047    fn rng_state_hash(&self) -> [u8; 32] {
1048        self.rng.as_ref().map_or([0u8; 32], |rng| {
1049            // Hash the RNG's internal state
1050            let state_bytes = rng.state_bytes();
1051            *blake3::hash(&state_bytes).as_bytes()
1052        })
1053    }
1054
1055    /// Log a step to the audit trail.
1056    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), // Arbitrary time step
1076            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    /// Enable or disable audit logging.
1096    pub fn set_audit_enabled(&mut self, enabled: bool) {
1097        self.audit_enabled = enabled;
1098    }
1099}
1100
1101// =============================================================================
1102// SimulationAuditLog Implementation (EDD-16 MANDATORY)
1103// =============================================================================
1104
1105impl 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        // One step = one GRASP iteration
1137        self.grasp_iteration();
1138    }
1139
1140    fn verify_equation(&self) -> bool {
1141        // Verify optimality gap < 20% (1-tree bound typically gives 10-20% gap)
1142        let gap_ok = self.optimality_gap() < 0.20;
1143
1144        // Verify low variance across restarts (CV < 5%)
1145        let cv_ok = self.restart_cv() < 0.05 || self.restart_history.len() < 5;
1146
1147        // Verify no edge crossings in best tour (ONLY for Euclidean TSP)
1148        // Lin & Kernighan (1973): Non-Euclidean instances may have optimal "crossing" tours
1149        let no_crossings = !self.is_euclidean || self.best_tour.is_empty() || {
1150            // Temporarily check crossings on best tour
1151            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        // Check crossings on best tour
1164        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        // JIDOKA: Crossings only matter for EUCLIDEAN instances
1176        // Lin & Kernighan (1973): Only Euclidean TSP optimal tours have ZERO crossings
1177        // Non-Euclidean (driving distances) may have "crossings" that are actually optimal
1178        let crossings_passed = !self.is_euclidean || crossings == 0;
1179
1180        FalsificationStatus {
1181            verified: gap_passed && cv_passed && crossings_passed,
1182            criteria: vec![
1183                // JIDOKA: Crossings first - but only for Euclidean instances
1184                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                // JIDOKA: Crossings are highest priority - stop the line! (Euclidean only)
1228                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// =============================================================================
1261// WASM Bindings
1262// =============================================================================
1263
1264#[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        // =====================================================================
1277        // Construction
1278        // =====================================================================
1279
1280        #[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        /// Create demo from YAML configuration string.
1288        ///
1289        /// This is the primary YAML-first entry point for web applications.
1290        /// Load `bay_area_tsp.yaml` or user-modified YAML.
1291        ///
1292        /// # Example (JavaScript)
1293        /// ```javascript
1294        /// const yaml = `
1295        /// meta:
1296        ///   id: "MY-TSP"
1297        /// cities:
1298        ///   - id: 0
1299        ///     name: "A"
1300        ///     alias: "A"
1301        ///     coords: { lat: 0.0, lon: 0.0 }
1302        ///   - id: 1
1303        ///     name: "B"
1304        ///     alias: "B"
1305        ///     coords: { lat: 1.0, lon: 1.0 }
1306        /// matrix:
1307        ///   - [0, 10]
1308        ///   - [10, 0]
1309        /// `;
1310        /// const demo = WasmTspGrasp.fromYaml(yaml);
1311        /// ```
1312        #[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        /// Create demo with cities from JavaScript array.
1319        /// Expected format: [[x1, y1], [x2, y2], ...]
1320        #[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        // =====================================================================
1346        // Simulation Control
1347        // =====================================================================
1348
1349        pub fn step(&mut self) {
1350            self.inner.step(0.0);
1351        }
1352
1353        /// Run a single GRASP iteration (construct + 2-opt).
1354        #[wasm_bindgen(js_name = graspIteration)]
1355        pub fn grasp_iteration(&mut self) {
1356            self.inner.grasp_iteration();
1357        }
1358
1359        /// Run multiple GRASP iterations.
1360        #[wasm_bindgen(js_name = runGrasp)]
1361        pub fn run_grasp(&mut self, iterations: usize) {
1362            self.inner.run_grasp(iterations);
1363        }
1364
1365        /// Construct initial tour only (no 2-opt).
1366        #[wasm_bindgen(js_name = constructTour)]
1367        pub fn construct_tour(&mut self) {
1368            self.inner.construct_tour();
1369        }
1370
1371        /// Run a single 2-opt pass. Returns true if improvement was made.
1372        #[wasm_bindgen(js_name = twoOptPass)]
1373        pub fn two_opt_pass(&mut self) -> bool {
1374            self.inner.two_opt_pass()
1375        }
1376
1377        /// Run 2-opt to local optimum.
1378        #[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        // =====================================================================
1388        // Configuration
1389        // =====================================================================
1390
1391        /// Set construction method: 0=RandomizedGreedy, 1=NearestNeighbor, 2=Random.
1392        #[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, // 0 and any invalid value
1398            };
1399        }
1400
1401        /// Set RCL size for randomized greedy construction.
1402        #[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        // =====================================================================
1408        // State Queries - Primitives
1409        // =====================================================================
1410
1411        #[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        /// Get the unit label for distances (e.g., "miles", "km").
1452        #[wasm_bindgen(js_name = getUnits)]
1453        pub fn get_units(&self) -> String {
1454            self.inner.units.clone()
1455        }
1456
1457        /// Get known optimal tour length (if available from YAML).
1458        #[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        // =====================================================================
1488        // State Queries - JavaScript Objects
1489        // =====================================================================
1490
1491        /// Get cities as JavaScript array: [[x1, y1], [x2, y2], ...]
1492        #[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        /// Get current tour as JavaScript array of indices.
1499        #[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        /// Get best tour as JavaScript array of indices.
1505        #[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        /// Get restart history as JavaScript array.
1511        #[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        /// Get full state as JavaScript object for serialization.
1517        #[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        // =====================================================================
1523        // EDD Verification
1524        // =====================================================================
1525
1526        pub fn verify(&self) -> bool {
1527            self.inner.verify_equation()
1528        }
1529
1530        /// Get falsification status as JavaScript object.
1531        #[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        /// Get demo name.
1538        #[wasm_bindgen(js_name = getName)]
1539        pub fn get_name(&self) -> String {
1540            self.inner.name().to_string()
1541        }
1542
1543        /// Get EMC reference.
1544        #[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;