1use serde::{Deserialize, Serialize};
38
39#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
41pub struct Coords {
42 pub lat: f64,
44 pub lon: f64,
46}
47
48impl Default for Coords {
49 fn default() -> Self {
50 Self { lat: 0.0, lon: 0.0 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub struct TspCity {
57 pub id: usize,
59 pub name: String,
61 pub alias: String,
63 pub coords: Coords,
65}
66
67impl TspCity {
68 #[must_use]
70 pub fn new(
71 id: usize,
72 name: impl Into<String>,
73 alias: impl Into<String>,
74 lat: f64,
75 lon: f64,
76 ) -> Self {
77 Self {
78 id,
79 name: name.into(),
80 alias: alias.into(),
81 coords: Coords { lat, lon },
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88pub struct TspMeta {
89 pub id: String,
91 #[serde(default = "default_version")]
93 pub version: String,
94 pub description: String,
96 #[serde(default)]
98 pub source: String,
99 #[serde(default = "default_units")]
101 pub units: String,
102 #[serde(default)]
104 pub optimal_known: Option<u32>,
105}
106
107fn default_version() -> String {
108 "1.0.0".to_string()
109}
110
111fn default_units() -> String {
112 "miles".to_string()
113}
114
115impl Default for TspMeta {
116 fn default() -> Self {
117 Self {
118 id: "TSP-UNNAMED".to_string(),
119 version: default_version(),
120 description: "Unnamed TSP instance".to_string(),
121 source: String::new(),
122 units: default_units(),
123 optimal_known: None,
124 }
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub struct TspParams {
131 #[serde(default = "default_rcl_size")]
133 pub rcl_size: usize,
134 #[serde(default = "default_restarts")]
136 pub restarts: usize,
137 #[serde(default = "default_two_opt")]
139 pub two_opt: bool,
140 #[serde(default = "default_seed")]
142 pub seed: u64,
143}
144
145fn default_rcl_size() -> usize {
146 3
147}
148
149fn default_restarts() -> usize {
150 10
151}
152
153fn default_two_opt() -> bool {
154 true
155}
156
157fn default_seed() -> u64 {
158 42
159}
160
161impl Default for TspParams {
162 fn default() -> Self {
163 Self {
164 rcl_size: default_rcl_size(),
165 restarts: default_restarts(),
166 two_opt: default_two_opt(),
167 seed: default_seed(),
168 }
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174pub struct TspAlgorithmConfig {
175 #[serde(default = "default_method")]
177 pub method: String,
178 #[serde(default)]
180 pub params: TspParams,
181}
182
183fn default_method() -> String {
184 "grasp".to_string()
185}
186
187impl Default for TspAlgorithmConfig {
188 fn default() -> Self {
189 Self {
190 method: default_method(),
191 params: TspParams::default(),
192 }
193 }
194}
195
196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
198pub struct TspInstanceYaml {
199 pub meta: TspMeta,
201 pub cities: Vec<TspCity>,
203 pub matrix: Vec<Vec<u32>>,
205 #[serde(default)]
207 pub algorithm: TspAlgorithmConfig,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
212pub enum TspInstanceError {
213 ParseError(String),
215 MatrixDimensionMismatch { expected: usize, got_rows: usize },
217 MatrixRowMismatch {
219 row: usize,
220 expected: usize,
221 got: usize,
222 },
223 TriangleInequalityViolation { i: usize, j: usize, k: usize },
225 AsymmetricMatrix {
227 i: usize,
228 j: usize,
229 forward: u32,
230 backward: u32,
231 },
232 InvalidCityId { id: usize, max: usize },
234 IoError(String),
236}
237
238impl std::fmt::Display for TspInstanceError {
239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240 match self {
241 Self::ParseError(msg) => write!(f, "YAML parse error: {msg}"),
242 Self::MatrixDimensionMismatch { expected, got_rows } => {
243 write!(f, "Matrix dimension mismatch: expected {expected}x{expected}, got {got_rows} rows")
244 }
245 Self::MatrixRowMismatch { row, expected, got } => {
246 write!(f, "Matrix row {row} has {got} columns, expected {expected}")
247 }
248 Self::TriangleInequalityViolation { i, j, k } => {
249 write!(
250 f,
251 "Triangle inequality violated: d({i},{k}) > d({i},{j}) + d({j},{k})"
252 )
253 }
254 Self::AsymmetricMatrix {
255 i,
256 j,
257 forward,
258 backward,
259 } => {
260 write!(
261 f,
262 "Asymmetric matrix: d({i},{j})={forward} != d({j},{i})={backward}"
263 )
264 }
265 Self::InvalidCityId { id, max } => {
266 write!(f, "Invalid city ID {id}, max is {max}")
267 }
268 Self::IoError(msg) => write!(f, "IO error: {msg}"),
269 }
270 }
271}
272
273impl std::error::Error for TspInstanceError {}
274
275impl TspInstanceYaml {
276 pub fn from_yaml(yaml: &str) -> Result<Self, TspInstanceError> {
282 serde_yaml::from_str(yaml).map_err(|e| TspInstanceError::ParseError(e.to_string()))
283 }
284
285 pub fn from_yaml_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, TspInstanceError> {
291 let content =
292 std::fs::read_to_string(path).map_err(|e| TspInstanceError::IoError(e.to_string()))?;
293 Self::from_yaml(&content)
294 }
295
296 pub fn to_yaml(&self) -> Result<String, TspInstanceError> {
302 serde_yaml::to_string(self).map_err(|e| TspInstanceError::ParseError(e.to_string()))
303 }
304
305 #[must_use]
307 pub fn city_count(&self) -> usize {
308 self.cities.len()
309 }
310
311 #[must_use]
317 pub fn distance(&self, from: usize, to: usize) -> u32 {
318 self.matrix[from][to]
319 }
320
321 pub fn validate(&self) -> Result<(), TspInstanceError> {
327 let n = self.cities.len();
328
329 if self.matrix.len() != n {
331 return Err(TspInstanceError::MatrixDimensionMismatch {
332 expected: n,
333 got_rows: self.matrix.len(),
334 });
335 }
336
337 for (i, row) in self.matrix.iter().enumerate() {
339 if row.len() != n {
340 return Err(TspInstanceError::MatrixRowMismatch {
341 row: i,
342 expected: n,
343 got: row.len(),
344 });
345 }
346 }
347
348 for city in &self.cities {
350 if city.id >= n {
351 return Err(TspInstanceError::InvalidCityId {
352 id: city.id,
353 max: n - 1,
354 });
355 }
356 }
357
358 Ok(())
359 }
360
361 pub fn check_symmetry(&self) -> Result<(), TspInstanceError> {
367 let n = self.cities.len();
368 for i in 0..n {
369 for j in (i + 1)..n {
370 let forward = self.matrix[i][j];
371 let backward = self.matrix[j][i];
372 if forward != backward {
373 return Err(TspInstanceError::AsymmetricMatrix {
374 i,
375 j,
376 forward,
377 backward,
378 });
379 }
380 }
381 }
382 Ok(())
383 }
384
385 pub fn check_triangle_inequality(&self) -> Result<(), TspInstanceError> {
391 let n = self.cities.len();
392 for i in 0..n {
393 for j in 0..n {
394 for k in 0..n {
395 if i != j && j != k && i != k {
396 let direct = self.matrix[i][k];
397 let via_j = self.matrix[i][j].saturating_add(self.matrix[j][k]);
398 if direct > via_j {
399 return Err(TspInstanceError::TriangleInequalityViolation { i, j, k });
400 }
401 }
402 }
403 }
404 }
405 Ok(())
406 }
407
408 #[must_use]
418 pub fn tour_length(&self, tour: &[usize]) -> u32 {
419 if tour.is_empty() {
420 return 0;
421 }
422 let mut total = 0u32;
423 for i in 0..tour.len() {
424 let from = tour[i];
425 let to = tour[(i + 1) % tour.len()];
426 total = total.saturating_add(self.matrix[from][to]);
427 }
428 total
429 }
430
431 #[must_use]
437 pub fn tour_length_verified(&self, tour: &[usize]) -> (u32, Vec<String>) {
438 let mut total = 0u32;
439 let mut steps = Vec::new();
440
441 for i in 0..tour.len() {
442 let from = tour[i];
443 let to = tour[(i + 1) % tour.len()];
444 let dist = self.matrix[from][to];
445 total = total.saturating_add(dist);
446
447 let from_name = self.cities.get(from).map_or("?", |c| &c.name);
448 let to_name = self.cities.get(to).map_or("?", |c| &c.name);
449 steps.push(format!(
450 "Step {}: {} → {} = {} {} (total: {})",
451 i + 1,
452 from_name,
453 to_name,
454 dist,
455 self.meta.units,
456 total
457 ));
458 }
459
460 (total, steps)
461 }
462}
463
464#[cfg(feature = "wasm")]
469mod wasm {
470 use super::TspInstanceYaml;
471 use wasm_bindgen::prelude::*;
472
473 #[wasm_bindgen]
500 pub struct TspWasmInstance {
501 inner: TspInstanceYaml,
502 }
503
504 #[allow(clippy::must_use_candidate)]
506 #[wasm_bindgen]
507 impl TspWasmInstance {
508 #[wasm_bindgen(js_name = fromYaml)]
514 pub fn from_yaml(yaml: &str) -> Result<Self, String> {
515 let inner = TspInstanceYaml::from_yaml(yaml).map_err(|e| e.to_string())?;
516 Ok(Self { inner })
517 }
518
519 #[wasm_bindgen(js_name = toYaml)]
521 pub fn to_yaml(&self) -> Result<String, String> {
522 self.inner.to_yaml().map_err(|e| e.to_string())
523 }
524
525 #[wasm_bindgen]
529 pub fn validate(&self) -> Result<(), String> {
530 self.inner.validate().map_err(|e| e.to_string())
531 }
532
533 #[wasm_bindgen(js_name = checkSymmetry)]
535 pub fn check_symmetry(&self) -> Result<(), String> {
536 self.inner.check_symmetry().map_err(|e| e.to_string())
537 }
538
539 #[wasm_bindgen(js_name = checkTriangleInequality)]
541 pub fn check_triangle_inequality(&self) -> Result<(), String> {
542 self.inner
543 .check_triangle_inequality()
544 .map_err(|e| e.to_string())
545 }
546
547 #[wasm_bindgen(js_name = cityCount)]
549 pub fn city_count(&self) -> usize {
550 self.inner.city_count()
551 }
552
553 #[wasm_bindgen]
555 pub fn distance(&self, from: usize, to: usize) -> u32 {
556 self.inner.distance(from, to)
557 }
558
559 #[wasm_bindgen(js_name = tourLength)]
563 pub fn tour_length(&self, tour: &[usize]) -> u32 {
564 self.inner.tour_length(tour)
565 }
566
567 #[wasm_bindgen(js_name = getId)]
569 pub fn get_id(&self) -> String {
570 self.inner.meta.id.clone()
571 }
572
573 #[wasm_bindgen(js_name = getDescription)]
575 pub fn get_description(&self) -> String {
576 self.inner.meta.description.clone()
577 }
578
579 #[wasm_bindgen(js_name = getUnits)]
581 pub fn get_units(&self) -> String {
582 self.inner.meta.units.clone()
583 }
584
585 #[wasm_bindgen(js_name = getOptimalKnown)]
587 pub fn get_optimal_known(&self) -> Option<u32> {
588 self.inner.meta.optimal_known
589 }
590
591 #[wasm_bindgen(js_name = getAlgorithmMethod)]
593 pub fn get_algorithm_method(&self) -> String {
594 self.inner.algorithm.method.clone()
595 }
596
597 #[wasm_bindgen(js_name = getSeed)]
599 pub fn get_seed(&self) -> u64 {
600 self.inner.algorithm.params.seed
601 }
602
603 #[wasm_bindgen(js_name = getRclSize)]
605 pub fn get_rcl_size(&self) -> usize {
606 self.inner.algorithm.params.rcl_size
607 }
608
609 #[wasm_bindgen(js_name = getRestarts)]
611 pub fn get_restarts(&self) -> usize {
612 self.inner.algorithm.params.restarts
613 }
614
615 #[wasm_bindgen(js_name = getTwoOptEnabled)]
617 pub fn get_two_opt_enabled(&self) -> bool {
618 self.inner.algorithm.params.two_opt
619 }
620
621 #[wasm_bindgen(js_name = getCityNamesJson)]
623 pub fn get_city_names_json(&self) -> String {
624 let names: Vec<&str> = self.inner.cities.iter().map(|c| c.name.as_str()).collect();
625 serde_json::to_string(&names).unwrap_or_else(|_| "[]".to_string())
626 }
627
628 #[wasm_bindgen(js_name = getCityAliasesJson)]
630 pub fn get_city_aliases_json(&self) -> String {
631 let aliases: Vec<&str> = self.inner.cities.iter().map(|c| c.alias.as_str()).collect();
632 serde_json::to_string(&aliases).unwrap_or_else(|_| "[]".to_string())
633 }
634
635 #[wasm_bindgen(js_name = getMatrixJson)]
637 pub fn get_matrix_json(&self) -> String {
638 serde_json::to_string(&self.inner.matrix).unwrap_or_else(|_| "[]".to_string())
639 }
640
641 #[wasm_bindgen(js_name = getCityCoordsJson)]
643 pub fn get_city_coords_json(&self) -> String {
644 let coords: Vec<_> = self.inner.cities.iter().map(|c| &c.coords).collect();
645 serde_json::to_string(&coords).unwrap_or_else(|_| "[]".to_string())
646 }
647
648 #[wasm_bindgen(js_name = tourLengthVerifiedJson)]
652 pub fn tour_length_verified_json(&self, tour: &[usize]) -> String {
653 let (length, steps) = self.inner.tour_length_verified(tour);
654 serde_json::json!({
655 "length": length,
656 "steps": steps
657 })
658 .to_string()
659 }
660
661 #[wasm_bindgen(js_name = toJson)]
663 pub fn to_json(&self) -> String {
664 serde_json::to_string(&self.inner).unwrap_or_else(|_| "{}".to_string())
665 }
666 }
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672
673 const MINIMAL_YAML: &str = r#"
674meta:
675 id: "TEST-001"
676 description: "Minimal test instance"
677cities:
678 - id: 0
679 name: "City A"
680 alias: "A"
681 coords: { lat: 0.0, lon: 0.0 }
682 - id: 1
683 name: "City B"
684 alias: "B"
685 coords: { lat: 1.0, lon: 1.0 }
686matrix:
687 - [0, 10]
688 - [10, 0]
689"#;
690
691 const BAY_AREA_YAML: &str = r#"
692meta:
693 id: "TSP-BAY-006"
694 version: "1.0.0"
695 description: "6-city Bay Area ground truth instance"
696 source: "Google Maps (Dec 2024)"
697 units: "miles"
698 optimal_known: 115
699cities:
700 - id: 0
701 name: "San Francisco"
702 alias: "SF"
703 coords: { lat: 37.7749, lon: -122.4194 }
704 - id: 1
705 name: "Oakland"
706 alias: "OAK"
707 coords: { lat: 37.8044, lon: -122.2712 }
708 - id: 2
709 name: "San Jose"
710 alias: "SJ"
711 coords: { lat: 37.3382, lon: -121.8863 }
712 - id: 3
713 name: "Palo Alto"
714 alias: "PA"
715 coords: { lat: 37.4419, lon: -122.1430 }
716 - id: 4
717 name: "Berkeley"
718 alias: "BRK"
719 coords: { lat: 37.8716, lon: -122.2727 }
720 - id: 5
721 name: "Fremont"
722 alias: "FRE"
723 coords: { lat: 37.5485, lon: -121.9886 }
724matrix:
725 - [ 0, 12, 48, 35, 14, 42]
726 - [12, 0, 42, 30, 4, 30]
727 - [48, 42, 0, 15, 46, 17]
728 - [35, 30, 15, 0, 32, 18]
729 - [14, 4, 46, 32, 0, 32]
730 - [42, 30, 17, 18, 32, 0]
731algorithm:
732 method: "grasp"
733 params:
734 rcl_size: 3
735 restarts: 10
736 two_opt: true
737 seed: 42
738"#;
739
740 #[test]
745 fn test_deserialize_valid_yaml() {
746 let instance = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Should parse valid YAML");
747 assert_eq!(instance.meta.id, "TSP-BAY-006");
748 assert_eq!(instance.cities.len(), 6);
749 assert_eq!(instance.matrix.len(), 6);
750 assert_eq!(instance.algorithm.method, "grasp");
751 }
752
753 #[test]
754 fn test_deserialize_minimal_yaml() {
755 let instance = TspInstanceYaml::from_yaml(MINIMAL_YAML).expect("Should parse minimal YAML");
756 assert_eq!(instance.meta.id, "TEST-001");
757 assert_eq!(instance.cities.len(), 2);
758 assert_eq!(instance.matrix.len(), 2);
759 assert_eq!(instance.algorithm.method, "grasp");
761 assert_eq!(instance.algorithm.params.rcl_size, 3);
762 }
763
764 #[test]
765 fn test_deserialize_invalid_yaml() {
766 let invalid = "this is not valid yaml: [[[";
767 let result = TspInstanceYaml::from_yaml(invalid);
768 assert!(result.is_err());
769 if let Err(TspInstanceError::ParseError(msg)) = result {
770 assert!(!msg.is_empty());
771 } else {
772 panic!("Expected ParseError");
773 }
774 }
775
776 #[test]
777 fn test_deserialize_missing_required_fields() {
778 let missing_cities = r#"
779meta:
780 id: "TEST"
781 description: "No cities"
782matrix: []
783"#;
784 let result = TspInstanceYaml::from_yaml(missing_cities);
785 assert!(result.is_err());
786 }
787
788 #[test]
789 fn test_serialize_roundtrip() {
790 let original = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Parse");
791 let yaml = original.to_yaml().expect("Serialize");
792 let restored = TspInstanceYaml::from_yaml(&yaml).expect("Reparse");
793 assert_eq!(original.meta.id, restored.meta.id);
794 assert_eq!(original.cities.len(), restored.cities.len());
795 assert_eq!(original.matrix, restored.matrix);
796 }
797
798 #[test]
799 fn test_default_algorithm_params() {
800 let params = TspParams::default();
801 assert_eq!(params.rcl_size, 3);
802 assert_eq!(params.restarts, 10);
803 assert!(params.two_opt);
804 assert_eq!(params.seed, 42);
805 }
806
807 #[test]
808 fn test_default_meta() {
809 let meta = TspMeta::default();
810 assert_eq!(meta.id, "TSP-UNNAMED");
811 assert_eq!(meta.version, "1.0.0");
812 assert_eq!(meta.units, "miles");
813 assert!(meta.optimal_known.is_none());
814 }
815
816 #[test]
817 fn test_default_algorithm_config() {
818 let config = TspAlgorithmConfig::default();
819 assert_eq!(config.method, "grasp");
820 assert_eq!(config.params.rcl_size, 3);
821 }
822
823 #[test]
824 fn test_coords_default() {
825 let coords = Coords::default();
826 assert_eq!(coords.lat, 0.0);
827 assert_eq!(coords.lon, 0.0);
828 }
829
830 #[test]
831 fn test_tsp_city_new() {
832 let city = TspCity::new(0, "San Francisco", "SF", 37.7749, -122.4194);
833 assert_eq!(city.id, 0);
834 assert_eq!(city.name, "San Francisco");
835 assert_eq!(city.alias, "SF");
836 assert!((city.coords.lat - 37.7749).abs() < 0.0001);
837 }
838
839 #[test]
844 fn test_validate_valid_instance() {
845 let instance = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Parse");
846 assert!(instance.validate().is_ok());
847 }
848
849 #[test]
850 fn test_validate_matrix_dimension_mismatch() {
851 let yaml = r#"
852meta:
853 id: "TEST"
854 description: "Bad matrix"
855cities:
856 - id: 0
857 name: "A"
858 alias: "A"
859 coords: { lat: 0.0, lon: 0.0 }
860 - id: 1
861 name: "B"
862 alias: "B"
863 coords: { lat: 1.0, lon: 1.0 }
864matrix:
865 - [0, 10, 20]
866 - [10, 0, 30]
867 - [20, 30, 0]
868"#;
869 let instance = TspInstanceYaml::from_yaml(yaml).expect("Parse");
870 let result = instance.validate();
871 assert!(matches!(
872 result,
873 Err(TspInstanceError::MatrixDimensionMismatch { .. })
874 ));
875 }
876
877 #[test]
878 fn test_validate_matrix_row_mismatch() {
879 let yaml = r#"
880meta:
881 id: "TEST"
882 description: "Bad row"
883cities:
884 - id: 0
885 name: "A"
886 alias: "A"
887 coords: { lat: 0.0, lon: 0.0 }
888 - id: 1
889 name: "B"
890 alias: "B"
891 coords: { lat: 1.0, lon: 1.0 }
892matrix:
893 - [0, 10]
894 - [10]
895"#;
896 let instance = TspInstanceYaml::from_yaml(yaml).expect("Parse");
897 let result = instance.validate();
898 assert!(matches!(
899 result,
900 Err(TspInstanceError::MatrixRowMismatch { .. })
901 ));
902 }
903
904 #[test]
905 fn test_validate_invalid_city_id() {
906 let yaml = r#"
907meta:
908 id: "TEST"
909 description: "Bad city ID"
910cities:
911 - id: 5
912 name: "A"
913 alias: "A"
914 coords: { lat: 0.0, lon: 0.0 }
915 - id: 1
916 name: "B"
917 alias: "B"
918 coords: { lat: 1.0, lon: 1.0 }
919matrix:
920 - [0, 10]
921 - [10, 0]
922"#;
923 let instance = TspInstanceYaml::from_yaml(yaml).expect("Parse");
924 let result = instance.validate();
925 assert!(matches!(
926 result,
927 Err(TspInstanceError::InvalidCityId { .. })
928 ));
929 }
930
931 #[test]
932 fn test_check_symmetry_valid() {
933 let instance = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Parse");
934 assert!(instance.check_symmetry().is_ok());
935 }
936
937 #[test]
938 fn test_check_symmetry_invalid() {
939 let yaml = r#"
940meta:
941 id: "TEST"
942 description: "Asymmetric"
943cities:
944 - id: 0
945 name: "A"
946 alias: "A"
947 coords: { lat: 0.0, lon: 0.0 }
948 - id: 1
949 name: "B"
950 alias: "B"
951 coords: { lat: 1.0, lon: 1.0 }
952matrix:
953 - [0, 10]
954 - [20, 0]
955"#;
956 let instance = TspInstanceYaml::from_yaml(yaml).expect("Parse");
957 let result = instance.check_symmetry();
958 assert!(matches!(
959 result,
960 Err(TspInstanceError::AsymmetricMatrix { .. })
961 ));
962 }
963
964 #[test]
965 fn test_check_triangle_inequality_valid() {
966 let instance = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Parse");
967 assert!(instance.check_triangle_inequality().is_ok());
968 }
969
970 #[test]
971 fn test_check_triangle_inequality_violation() {
972 let yaml = r#"
974meta:
975 id: "TEST"
976 description: "Triangle violation"
977cities:
978 - id: 0
979 name: "A"
980 alias: "A"
981 coords: { lat: 0.0, lon: 0.0 }
982 - id: 1
983 name: "B"
984 alias: "B"
985 coords: { lat: 1.0, lon: 1.0 }
986 - id: 2
987 name: "C"
988 alias: "C"
989 coords: { lat: 2.0, lon: 2.0 }
990matrix:
991 - [0, 10, 100]
992 - [10, 0, 10]
993 - [100, 10, 0]
994"#;
995 let instance = TspInstanceYaml::from_yaml(yaml).expect("Parse");
996 let result = instance.check_triangle_inequality();
997 assert!(matches!(
998 result,
999 Err(TspInstanceError::TriangleInequalityViolation { .. })
1000 ));
1001 }
1002
1003 #[test]
1008 fn test_tour_length_bay_area_optimal() {
1009 let instance = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Parse");
1010 let tour = vec![0, 1, 4, 5, 2, 3];
1012 let length = instance.tour_length(&tour);
1013 assert_eq!(length, 115);
1015 }
1016
1017 #[test]
1018 fn test_tour_length_empty() {
1019 let instance = TspInstanceYaml::from_yaml(MINIMAL_YAML).expect("Parse");
1020 let length = instance.tour_length(&[]);
1021 assert_eq!(length, 0);
1022 }
1023
1024 #[test]
1025 fn test_tour_length_single_city() {
1026 let instance = TspInstanceYaml::from_yaml(MINIMAL_YAML).expect("Parse");
1027 let length = instance.tour_length(&[0]);
1028 assert_eq!(length, 0); }
1030
1031 #[test]
1032 fn test_tour_length_verified() {
1033 let instance = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Parse");
1034 let tour = vec![0, 1, 4, 5, 2, 3];
1035 let (length, steps) = instance.tour_length_verified(&tour);
1036 assert_eq!(length, 115);
1037 assert_eq!(steps.len(), 6);
1038 assert!(steps[0].contains("San Francisco"));
1039 assert!(steps[0].contains("Oakland"));
1040 assert!(steps[0].contains("12"));
1041 }
1042
1043 #[test]
1048 fn test_city_count() {
1049 let instance = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Parse");
1050 assert_eq!(instance.city_count(), 6);
1051 }
1052
1053 #[test]
1054 fn test_distance() {
1055 let instance = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Parse");
1056 assert_eq!(instance.distance(0, 1), 12); assert_eq!(instance.distance(1, 4), 4); }
1059
1060 #[test]
1065 fn test_error_display_parse() {
1066 let err = TspInstanceError::ParseError("test error".to_string());
1067 assert!(err.to_string().contains("YAML parse error"));
1068 }
1069
1070 #[test]
1071 fn test_error_display_matrix_dimension() {
1072 let err = TspInstanceError::MatrixDimensionMismatch {
1073 expected: 6,
1074 got_rows: 4,
1075 };
1076 let msg = err.to_string();
1077 assert!(msg.contains("6x6"));
1078 assert!(msg.contains("4 rows"));
1079 }
1080
1081 #[test]
1082 fn test_error_display_matrix_row() {
1083 let err = TspInstanceError::MatrixRowMismatch {
1084 row: 2,
1085 expected: 6,
1086 got: 4,
1087 };
1088 let msg = err.to_string();
1089 assert!(msg.contains("row 2"));
1090 assert!(msg.contains("4 columns"));
1091 }
1092
1093 #[test]
1094 fn test_error_display_triangle() {
1095 let err = TspInstanceError::TriangleInequalityViolation { i: 0, j: 1, k: 2 };
1096 assert!(err.to_string().contains("Triangle inequality"));
1097 }
1098
1099 #[test]
1100 fn test_error_display_asymmetric() {
1101 let err = TspInstanceError::AsymmetricMatrix {
1102 i: 0,
1103 j: 1,
1104 forward: 10,
1105 backward: 20,
1106 };
1107 let msg = err.to_string();
1108 assert!(msg.contains("Asymmetric"));
1109 assert!(msg.contains("10"));
1110 assert!(msg.contains("20"));
1111 }
1112
1113 #[test]
1114 fn test_error_display_invalid_city() {
1115 let err = TspInstanceError::InvalidCityId { id: 10, max: 5 };
1116 assert!(err.to_string().contains("Invalid city ID"));
1117 }
1118
1119 #[test]
1120 fn test_error_display_io() {
1121 let err = TspInstanceError::IoError("file not found".to_string());
1122 assert!(err.to_string().contains("IO error"));
1123 }
1124
1125 #[test]
1130 fn test_from_yaml_file_not_found() {
1131 let result = TspInstanceYaml::from_yaml_file("/nonexistent/path/file.yaml");
1132 assert!(matches!(result, Err(TspInstanceError::IoError(_))));
1133 }
1134
1135 #[test]
1136 fn test_from_yaml_file_success() {
1137 let result = TspInstanceYaml::from_yaml_file("examples/experiments/bay_area_tsp.yaml");
1139 assert!(result.is_ok());
1140 let instance = result.unwrap();
1141 assert_eq!(instance.meta.id, "TSP-CA-020");
1142 assert_eq!(instance.city_count(), 20);
1143 }
1144
1145 #[test]
1150 fn test_coords_clone_and_copy() {
1151 let coords = Coords {
1152 lat: 37.0,
1153 lon: -122.0,
1154 };
1155 let cloned = coords.clone();
1156 let copied = coords;
1157 assert_eq!(coords, cloned);
1158 assert_eq!(coords, copied);
1159 }
1160
1161 #[test]
1162 fn test_coords_partial_eq() {
1163 let c1 = Coords {
1164 lat: 37.0,
1165 lon: -122.0,
1166 };
1167 let c2 = Coords {
1168 lat: 37.0,
1169 lon: -122.0,
1170 };
1171 let c3 = Coords {
1172 lat: 38.0,
1173 lon: -122.0,
1174 };
1175 assert_eq!(c1, c2);
1176 assert_ne!(c1, c3);
1177 }
1178
1179 #[test]
1180 fn test_tsp_city_clone() {
1181 let city = TspCity::new(0, "SF", "SF", 37.0, -122.0);
1182 let cloned = city.clone();
1183 assert_eq!(city, cloned);
1184 }
1185
1186 #[test]
1187 fn test_tsp_meta_clone() {
1188 let meta = TspMeta::default();
1189 let cloned = meta.clone();
1190 assert_eq!(meta, cloned);
1191 }
1192
1193 #[test]
1194 fn test_tsp_params_clone() {
1195 let params = TspParams::default();
1196 let cloned = params.clone();
1197 assert_eq!(params, cloned);
1198 }
1199
1200 #[test]
1201 fn test_tsp_algorithm_config_clone() {
1202 let config = TspAlgorithmConfig::default();
1203 let cloned = config.clone();
1204 assert_eq!(config, cloned);
1205 }
1206
1207 #[test]
1208 fn test_tsp_instance_yaml_clone() {
1209 let instance = TspInstanceYaml::from_yaml(MINIMAL_YAML).expect("Parse");
1210 let cloned = instance.clone();
1211 assert_eq!(instance, cloned);
1212 }
1213
1214 #[test]
1215 fn test_error_is_error_trait() {
1216 let err: Box<dyn std::error::Error> =
1217 Box::new(TspInstanceError::ParseError("test".to_string()));
1218 assert!(!err.to_string().is_empty());
1219 }
1220
1221 #[test]
1226 fn test_coords_debug() {
1227 let coords = Coords {
1228 lat: 37.0,
1229 lon: -122.0,
1230 };
1231 let debug = format!("{:?}", coords);
1232 assert!(debug.contains("Coords"));
1233 assert!(debug.contains("37"));
1234 }
1235
1236 #[test]
1237 fn test_tsp_instance_error_debug() {
1238 let err = TspInstanceError::ParseError("test".to_string());
1239 let debug = format!("{:?}", err);
1240 assert!(debug.contains("ParseError"));
1241 }
1242}
1243
1244#[cfg(all(test, feature = "wasm"))]
1249mod wasm_tests {
1250 use super::wasm::TspWasmInstance;
1251
1252 const BAY_AREA_YAML: &str = include_str!("../../examples/experiments/bay_area_tsp.yaml");
1253
1254 const MINIMAL_YAML: &str = r#"
1255meta:
1256 id: "TEST-001"
1257 description: "Minimal test instance"
1258cities:
1259 - id: 0
1260 name: "A"
1261 alias: "A"
1262 coords: { lat: 0.0, lon: 0.0 }
1263 - id: 1
1264 name: "B"
1265 alias: "B"
1266 coords: { lat: 1.0, lon: 1.0 }
1267matrix:
1268 - [0, 10]
1269 - [10, 0]
1270"#;
1271
1272 #[test]
1273 fn test_wasm_from_yaml() {
1274 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML);
1275 assert!(instance.is_ok());
1276 }
1277
1278 #[test]
1279 fn test_wasm_from_yaml_invalid() {
1280 let result = TspWasmInstance::from_yaml("invalid yaml: [[[");
1281 assert!(result.is_err());
1282 }
1283
1284 #[test]
1285 fn test_wasm_to_yaml() {
1286 let instance = TspWasmInstance::from_yaml(MINIMAL_YAML).expect("parse");
1287 let yaml = instance.to_yaml();
1288 assert!(yaml.is_ok());
1289 assert!(yaml.unwrap().contains("TEST-001"));
1290 }
1291
1292 #[test]
1293 fn test_wasm_validate() {
1294 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1295 assert!(instance.validate().is_ok());
1296 }
1297
1298 #[test]
1299 fn test_wasm_check_symmetry() {
1300 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1301 assert!(instance.check_symmetry().is_ok());
1302 }
1303
1304 #[test]
1305 fn test_wasm_check_triangle_inequality() {
1306 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1307 let result = instance.check_triangle_inequality();
1310 assert!(result.is_ok() || result.is_err());
1312 }
1313
1314 #[test]
1315 fn test_wasm_city_count() {
1316 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1317 assert_eq!(instance.city_count(), 20); }
1319
1320 #[test]
1321 fn test_wasm_distance() {
1322 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1323 assert_eq!(instance.distance(0, 1), 12); }
1325
1326 #[test]
1327 fn test_wasm_tour_length() {
1328 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1329 let tour = [0, 1, 4, 5, 2, 3]; assert_eq!(instance.tour_length(&tour), 115);
1331 }
1332
1333 #[test]
1334 fn test_wasm_get_id() {
1335 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1336 assert_eq!(instance.get_id(), "TSP-CA-020"); }
1338
1339 #[test]
1340 fn test_wasm_get_description() {
1341 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1342 assert!(instance.get_description().contains("Bay Area"));
1343 }
1344
1345 #[test]
1346 fn test_wasm_get_units() {
1347 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1348 assert_eq!(instance.get_units(), "miles");
1349 }
1350
1351 #[test]
1352 fn test_wasm_get_optimal_known() {
1353 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1354 assert_eq!(instance.get_optimal_known(), None); }
1356
1357 #[test]
1358 fn test_wasm_get_algorithm_method() {
1359 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1360 assert_eq!(instance.get_algorithm_method(), "grasp");
1361 }
1362
1363 #[test]
1364 fn test_wasm_get_seed() {
1365 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1366 assert_eq!(instance.get_seed(), 42);
1367 }
1368
1369 #[test]
1370 fn test_wasm_get_rcl_size() {
1371 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1372 assert_eq!(instance.get_rcl_size(), 3);
1373 }
1374
1375 #[test]
1376 fn test_wasm_get_restarts() {
1377 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1378 assert_eq!(instance.get_restarts(), 100); }
1380
1381 #[test]
1382 fn test_wasm_get_two_opt_enabled() {
1383 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1384 assert!(instance.get_two_opt_enabled());
1385 }
1386
1387 #[test]
1388 fn test_wasm_get_city_names_json() {
1389 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1390 let json = instance.get_city_names_json();
1391 assert!(json.contains("San Francisco"));
1392 assert!(json.contains("Oakland"));
1393 }
1394
1395 #[test]
1396 fn test_wasm_get_city_aliases_json() {
1397 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1398 let json = instance.get_city_aliases_json();
1399 assert!(json.contains("SF"));
1400 assert!(json.contains("OAK"));
1401 }
1402
1403 #[test]
1404 fn test_wasm_get_matrix_json() {
1405 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1406 let json = instance.get_matrix_json();
1407 assert!(json.contains("[0,12")); }
1409
1410 #[test]
1411 fn test_wasm_get_city_coords_json() {
1412 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1413 let json = instance.get_city_coords_json();
1414 assert!(json.contains("37.7749")); }
1416
1417 #[test]
1418 fn test_wasm_tour_length_verified_json() {
1419 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1420 let tour = [0, 1, 4, 5, 2, 3];
1421 let json = instance.tour_length_verified_json(&tour);
1422 assert!(json.contains("\"length\":115"));
1423 assert!(json.contains("steps"));
1424 assert!(json.contains("San Francisco"));
1425 }
1426
1427 #[test]
1428 fn test_wasm_to_json() {
1429 let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
1430 let json = instance.to_json();
1431 assert!(json.contains("TSP-CA-020")); assert!(json.contains("cities"));
1433 assert!(json.contains("matrix"));
1434 }
1435
1436 #[test]
1437 fn test_wasm_check_symmetry_fails() {
1438 let asymmetric_yaml = r#"
1439meta:
1440 id: "ASYM"
1441 description: "Asymmetric"
1442cities:
1443 - id: 0
1444 name: "A"
1445 alias: "A"
1446 coords: { lat: 0.0, lon: 0.0 }
1447 - id: 1
1448 name: "B"
1449 alias: "B"
1450 coords: { lat: 1.0, lon: 1.0 }
1451matrix:
1452 - [0, 10]
1453 - [99, 0]
1454"#;
1455 let instance = TspWasmInstance::from_yaml(asymmetric_yaml).expect("parse");
1456 assert!(instance.check_symmetry().is_err());
1457 }
1458
1459 #[test]
1460 fn test_wasm_validate_fails() {
1461 let bad_yaml = r#"
1462meta:
1463 id: "BAD"
1464 description: "Bad matrix"
1465cities:
1466 - id: 0
1467 name: "A"
1468 alias: "A"
1469 coords: { lat: 0.0, lon: 0.0 }
1470matrix:
1471 - [0, 10, 20]
1472 - [10, 0, 30]
1473 - [20, 30, 0]
1474"#;
1475 let instance = TspWasmInstance::from_yaml(bad_yaml).expect("parse");
1476 assert!(instance.validate().is_err());
1477 }
1478}