use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Coords {
pub lat: f64,
pub lon: f64,
}
impl Default for Coords {
fn default() -> Self {
Self { lat: 0.0, lon: 0.0 }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TspCity {
pub id: usize,
pub name: String,
pub alias: String,
pub coords: Coords,
}
impl TspCity {
#[must_use]
pub fn new(
id: usize,
name: impl Into<String>,
alias: impl Into<String>,
lat: f64,
lon: f64,
) -> Self {
Self {
id,
name: name.into(),
alias: alias.into(),
coords: Coords { lat, lon },
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TspMeta {
pub id: String,
#[serde(default = "default_version")]
pub version: String,
pub description: String,
#[serde(default)]
pub source: String,
#[serde(default = "default_units")]
pub units: String,
#[serde(default)]
pub optimal_known: Option<u32>,
}
fn default_version() -> String {
"1.0.0".to_string()
}
fn default_units() -> String {
"miles".to_string()
}
impl Default for TspMeta {
fn default() -> Self {
Self {
id: "TSP-UNNAMED".to_string(),
version: default_version(),
description: "Unnamed TSP instance".to_string(),
source: String::new(),
units: default_units(),
optimal_known: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TspParams {
#[serde(default = "default_rcl_size")]
pub rcl_size: usize,
#[serde(default = "default_restarts")]
pub restarts: usize,
#[serde(default = "default_two_opt")]
pub two_opt: bool,
#[serde(default = "default_seed")]
pub seed: u64,
}
fn default_rcl_size() -> usize {
3
}
fn default_restarts() -> usize {
10
}
fn default_two_opt() -> bool {
true
}
fn default_seed() -> u64 {
42
}
impl Default for TspParams {
fn default() -> Self {
Self {
rcl_size: default_rcl_size(),
restarts: default_restarts(),
two_opt: default_two_opt(),
seed: default_seed(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TspAlgorithmConfig {
#[serde(default = "default_method")]
pub method: String,
#[serde(default)]
pub params: TspParams,
}
fn default_method() -> String {
"grasp".to_string()
}
impl Default for TspAlgorithmConfig {
fn default() -> Self {
Self {
method: default_method(),
params: TspParams::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TspInstanceYaml {
pub meta: TspMeta,
pub cities: Vec<TspCity>,
pub matrix: Vec<Vec<u32>>,
#[serde(default)]
pub algorithm: TspAlgorithmConfig,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TspInstanceError {
ParseError(String),
MatrixDimensionMismatch { expected: usize, got_rows: usize },
MatrixRowMismatch {
row: usize,
expected: usize,
got: usize,
},
TriangleInequalityViolation { i: usize, j: usize, k: usize },
AsymmetricMatrix {
i: usize,
j: usize,
forward: u32,
backward: u32,
},
InvalidCityId { id: usize, max: usize },
IoError(String),
}
impl std::fmt::Display for TspInstanceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ParseError(msg) => write!(f, "YAML parse error: {msg}"),
Self::MatrixDimensionMismatch { expected, got_rows } => {
write!(f, "Matrix dimension mismatch: expected {expected}x{expected}, got {got_rows} rows")
}
Self::MatrixRowMismatch { row, expected, got } => {
write!(f, "Matrix row {row} has {got} columns, expected {expected}")
}
Self::TriangleInequalityViolation { i, j, k } => {
write!(
f,
"Triangle inequality violated: d({i},{k}) > d({i},{j}) + d({j},{k})"
)
}
Self::AsymmetricMatrix {
i,
j,
forward,
backward,
} => {
write!(
f,
"Asymmetric matrix: d({i},{j})={forward} != d({j},{i})={backward}"
)
}
Self::InvalidCityId { id, max } => {
write!(f, "Invalid city ID {id}, max is {max}")
}
Self::IoError(msg) => write!(f, "IO error: {msg}"),
}
}
}
impl std::error::Error for TspInstanceError {}
impl TspInstanceYaml {
pub fn from_yaml(yaml: &str) -> Result<Self, TspInstanceError> {
serde_yaml::from_str(yaml).map_err(|e| TspInstanceError::ParseError(e.to_string()))
}
pub fn from_yaml_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, TspInstanceError> {
let content =
std::fs::read_to_string(path).map_err(|e| TspInstanceError::IoError(e.to_string()))?;
Self::from_yaml(&content)
}
pub fn to_yaml(&self) -> Result<String, TspInstanceError> {
serde_yaml::to_string(self).map_err(|e| TspInstanceError::ParseError(e.to_string()))
}
#[must_use]
pub fn city_count(&self) -> usize {
self.cities.len()
}
#[must_use]
pub fn distance(&self, from: usize, to: usize) -> u32 {
self.matrix[from][to]
}
pub fn validate(&self) -> Result<(), TspInstanceError> {
let n = self.cities.len();
if self.matrix.len() != n {
return Err(TspInstanceError::MatrixDimensionMismatch {
expected: n,
got_rows: self.matrix.len(),
});
}
for (i, row) in self.matrix.iter().enumerate() {
if row.len() != n {
return Err(TspInstanceError::MatrixRowMismatch {
row: i,
expected: n,
got: row.len(),
});
}
}
for city in &self.cities {
if city.id >= n {
return Err(TspInstanceError::InvalidCityId {
id: city.id,
max: n - 1,
});
}
}
Ok(())
}
pub fn check_symmetry(&self) -> Result<(), TspInstanceError> {
let n = self.cities.len();
for i in 0..n {
for j in (i + 1)..n {
let forward = self.matrix[i][j];
let backward = self.matrix[j][i];
if forward != backward {
return Err(TspInstanceError::AsymmetricMatrix {
i,
j,
forward,
backward,
});
}
}
}
Ok(())
}
pub fn check_triangle_inequality(&self) -> Result<(), TspInstanceError> {
let n = self.cities.len();
for i in 0..n {
for j in 0..n {
for k in 0..n {
if i != j && j != k && i != k {
let direct = self.matrix[i][k];
let via_j = self.matrix[i][j].saturating_add(self.matrix[j][k]);
if direct > via_j {
return Err(TspInstanceError::TriangleInequalityViolation { i, j, k });
}
}
}
}
}
Ok(())
}
#[must_use]
pub fn tour_length(&self, tour: &[usize]) -> u32 {
if tour.is_empty() {
return 0;
}
let mut total = 0u32;
for i in 0..tour.len() {
let from = tour[i];
let to = tour[(i + 1) % tour.len()];
total = total.saturating_add(self.matrix[from][to]);
}
total
}
#[must_use]
pub fn tour_length_verified(&self, tour: &[usize]) -> (u32, Vec<String>) {
let mut total = 0u32;
let mut steps = Vec::new();
for i in 0..tour.len() {
let from = tour[i];
let to = tour[(i + 1) % tour.len()];
let dist = self.matrix[from][to];
total = total.saturating_add(dist);
let from_name = self.cities.get(from).map_or("?", |c| &c.name);
let to_name = self.cities.get(to).map_or("?", |c| &c.name);
steps.push(format!(
"Step {}: {} → {} = {} {} (total: {})",
i + 1,
from_name,
to_name,
dist,
self.meta.units,
total
));
}
(total, steps)
}
}
#[cfg(feature = "wasm")]
mod wasm {
use super::TspInstanceYaml;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct TspWasmInstance {
inner: TspInstanceYaml,
}
#[allow(clippy::must_use_candidate)]
#[wasm_bindgen]
impl TspWasmInstance {
#[wasm_bindgen(js_name = fromYaml)]
pub fn from_yaml(yaml: &str) -> Result<Self, String> {
let inner = TspInstanceYaml::from_yaml(yaml).map_err(|e| e.to_string())?;
Ok(Self { inner })
}
#[wasm_bindgen(js_name = toYaml)]
pub fn to_yaml(&self) -> Result<String, String> {
self.inner.to_yaml().map_err(|e| e.to_string())
}
#[wasm_bindgen]
pub fn validate(&self) -> Result<(), String> {
self.inner.validate().map_err(|e| e.to_string())
}
#[wasm_bindgen(js_name = checkSymmetry)]
pub fn check_symmetry(&self) -> Result<(), String> {
self.inner.check_symmetry().map_err(|e| e.to_string())
}
#[wasm_bindgen(js_name = checkTriangleInequality)]
pub fn check_triangle_inequality(&self) -> Result<(), String> {
self.inner
.check_triangle_inequality()
.map_err(|e| e.to_string())
}
#[wasm_bindgen(js_name = cityCount)]
pub fn city_count(&self) -> usize {
self.inner.city_count()
}
#[wasm_bindgen]
pub fn distance(&self, from: usize, to: usize) -> u32 {
self.inner.distance(from, to)
}
#[wasm_bindgen(js_name = tourLength)]
pub fn tour_length(&self, tour: &[usize]) -> u32 {
self.inner.tour_length(tour)
}
#[wasm_bindgen(js_name = getId)]
pub fn get_id(&self) -> String {
self.inner.meta.id.clone()
}
#[wasm_bindgen(js_name = getDescription)]
pub fn get_description(&self) -> String {
self.inner.meta.description.clone()
}
#[wasm_bindgen(js_name = getUnits)]
pub fn get_units(&self) -> String {
self.inner.meta.units.clone()
}
#[wasm_bindgen(js_name = getOptimalKnown)]
pub fn get_optimal_known(&self) -> Option<u32> {
self.inner.meta.optimal_known
}
#[wasm_bindgen(js_name = getAlgorithmMethod)]
pub fn get_algorithm_method(&self) -> String {
self.inner.algorithm.method.clone()
}
#[wasm_bindgen(js_name = getSeed)]
pub fn get_seed(&self) -> u64 {
self.inner.algorithm.params.seed
}
#[wasm_bindgen(js_name = getRclSize)]
pub fn get_rcl_size(&self) -> usize {
self.inner.algorithm.params.rcl_size
}
#[wasm_bindgen(js_name = getRestarts)]
pub fn get_restarts(&self) -> usize {
self.inner.algorithm.params.restarts
}
#[wasm_bindgen(js_name = getTwoOptEnabled)]
pub fn get_two_opt_enabled(&self) -> bool {
self.inner.algorithm.params.two_opt
}
#[wasm_bindgen(js_name = getCityNamesJson)]
pub fn get_city_names_json(&self) -> String {
let names: Vec<&str> = self.inner.cities.iter().map(|c| c.name.as_str()).collect();
serde_json::to_string(&names).unwrap_or_else(|_| "[]".to_string())
}
#[wasm_bindgen(js_name = getCityAliasesJson)]
pub fn get_city_aliases_json(&self) -> String {
let aliases: Vec<&str> = self.inner.cities.iter().map(|c| c.alias.as_str()).collect();
serde_json::to_string(&aliases).unwrap_or_else(|_| "[]".to_string())
}
#[wasm_bindgen(js_name = getMatrixJson)]
pub fn get_matrix_json(&self) -> String {
serde_json::to_string(&self.inner.matrix).unwrap_or_else(|_| "[]".to_string())
}
#[wasm_bindgen(js_name = getCityCoordsJson)]
pub fn get_city_coords_json(&self) -> String {
let coords: Vec<_> = self.inner.cities.iter().map(|c| &c.coords).collect();
serde_json::to_string(&coords).unwrap_or_else(|_| "[]".to_string())
}
#[wasm_bindgen(js_name = tourLengthVerifiedJson)]
pub fn tour_length_verified_json(&self, tour: &[usize]) -> String {
let (length, steps) = self.inner.tour_length_verified(tour);
serde_json::json!({
"length": length,
"steps": steps
})
.to_string()
}
#[wasm_bindgen(js_name = toJson)]
pub fn to_json(&self) -> String {
serde_json::to_string(&self.inner).unwrap_or_else(|_| "{}".to_string())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const MINIMAL_YAML: &str = r#"
meta:
id: "TEST-001"
description: "Minimal test instance"
cities:
- id: 0
name: "City A"
alias: "A"
coords: { lat: 0.0, lon: 0.0 }
- id: 1
name: "City B"
alias: "B"
coords: { lat: 1.0, lon: 1.0 }
matrix:
- [0, 10]
- [10, 0]
"#;
const BAY_AREA_YAML: &str = r#"
meta:
id: "TSP-BAY-006"
version: "1.0.0"
description: "6-city Bay Area ground truth instance"
source: "Google Maps (Dec 2024)"
units: "miles"
optimal_known: 115
cities:
- id: 0
name: "San Francisco"
alias: "SF"
coords: { lat: 37.7749, lon: -122.4194 }
- id: 1
name: "Oakland"
alias: "OAK"
coords: { lat: 37.8044, lon: -122.2712 }
- id: 2
name: "San Jose"
alias: "SJ"
coords: { lat: 37.3382, lon: -121.8863 }
- id: 3
name: "Palo Alto"
alias: "PA"
coords: { lat: 37.4419, lon: -122.1430 }
- id: 4
name: "Berkeley"
alias: "BRK"
coords: { lat: 37.8716, lon: -122.2727 }
- id: 5
name: "Fremont"
alias: "FRE"
coords: { lat: 37.5485, lon: -121.9886 }
matrix:
- [ 0, 12, 48, 35, 14, 42]
- [12, 0, 42, 30, 4, 30]
- [48, 42, 0, 15, 46, 17]
- [35, 30, 15, 0, 32, 18]
- [14, 4, 46, 32, 0, 32]
- [42, 30, 17, 18, 32, 0]
algorithm:
method: "grasp"
params:
rcl_size: 3
restarts: 10
two_opt: true
seed: 42
"#;
#[test]
fn test_deserialize_valid_yaml() {
let instance = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Should parse valid YAML");
assert_eq!(instance.meta.id, "TSP-BAY-006");
assert_eq!(instance.cities.len(), 6);
assert_eq!(instance.matrix.len(), 6);
assert_eq!(instance.algorithm.method, "grasp");
}
#[test]
fn test_deserialize_minimal_yaml() {
let instance = TspInstanceYaml::from_yaml(MINIMAL_YAML).expect("Should parse minimal YAML");
assert_eq!(instance.meta.id, "TEST-001");
assert_eq!(instance.cities.len(), 2);
assert_eq!(instance.matrix.len(), 2);
assert_eq!(instance.algorithm.method, "grasp");
assert_eq!(instance.algorithm.params.rcl_size, 3);
}
#[test]
fn test_deserialize_invalid_yaml() {
let invalid = "this is not valid yaml: [[[";
let result = TspInstanceYaml::from_yaml(invalid);
assert!(result.is_err());
if let Err(TspInstanceError::ParseError(msg)) = result {
assert!(!msg.is_empty());
} else {
panic!("Expected ParseError");
}
}
#[test]
fn test_deserialize_missing_required_fields() {
let missing_cities = r#"
meta:
id: "TEST"
description: "No cities"
matrix: []
"#;
let result = TspInstanceYaml::from_yaml(missing_cities);
assert!(result.is_err());
}
#[test]
fn test_serialize_roundtrip() {
let original = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Parse");
let yaml = original.to_yaml().expect("Serialize");
let restored = TspInstanceYaml::from_yaml(&yaml).expect("Reparse");
assert_eq!(original.meta.id, restored.meta.id);
assert_eq!(original.cities.len(), restored.cities.len());
assert_eq!(original.matrix, restored.matrix);
}
#[test]
fn test_default_algorithm_params() {
let params = TspParams::default();
assert_eq!(params.rcl_size, 3);
assert_eq!(params.restarts, 10);
assert!(params.two_opt);
assert_eq!(params.seed, 42);
}
#[test]
fn test_default_meta() {
let meta = TspMeta::default();
assert_eq!(meta.id, "TSP-UNNAMED");
assert_eq!(meta.version, "1.0.0");
assert_eq!(meta.units, "miles");
assert!(meta.optimal_known.is_none());
}
#[test]
fn test_default_algorithm_config() {
let config = TspAlgorithmConfig::default();
assert_eq!(config.method, "grasp");
assert_eq!(config.params.rcl_size, 3);
}
#[test]
fn test_coords_default() {
let coords = Coords::default();
assert_eq!(coords.lat, 0.0);
assert_eq!(coords.lon, 0.0);
}
#[test]
fn test_tsp_city_new() {
let city = TspCity::new(0, "San Francisco", "SF", 37.7749, -122.4194);
assert_eq!(city.id, 0);
assert_eq!(city.name, "San Francisco");
assert_eq!(city.alias, "SF");
assert!((city.coords.lat - 37.7749).abs() < 0.0001);
}
#[test]
fn test_validate_valid_instance() {
let instance = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Parse");
assert!(instance.validate().is_ok());
}
#[test]
fn test_validate_matrix_dimension_mismatch() {
let yaml = r#"
meta:
id: "TEST"
description: "Bad matrix"
cities:
- id: 0
name: "A"
alias: "A"
coords: { lat: 0.0, lon: 0.0 }
- id: 1
name: "B"
alias: "B"
coords: { lat: 1.0, lon: 1.0 }
matrix:
- [0, 10, 20]
- [10, 0, 30]
- [20, 30, 0]
"#;
let instance = TspInstanceYaml::from_yaml(yaml).expect("Parse");
let result = instance.validate();
assert!(matches!(
result,
Err(TspInstanceError::MatrixDimensionMismatch { .. })
));
}
#[test]
fn test_validate_matrix_row_mismatch() {
let yaml = r#"
meta:
id: "TEST"
description: "Bad row"
cities:
- id: 0
name: "A"
alias: "A"
coords: { lat: 0.0, lon: 0.0 }
- id: 1
name: "B"
alias: "B"
coords: { lat: 1.0, lon: 1.0 }
matrix:
- [0, 10]
- [10]
"#;
let instance = TspInstanceYaml::from_yaml(yaml).expect("Parse");
let result = instance.validate();
assert!(matches!(
result,
Err(TspInstanceError::MatrixRowMismatch { .. })
));
}
#[test]
fn test_validate_invalid_city_id() {
let yaml = r#"
meta:
id: "TEST"
description: "Bad city ID"
cities:
- id: 5
name: "A"
alias: "A"
coords: { lat: 0.0, lon: 0.0 }
- id: 1
name: "B"
alias: "B"
coords: { lat: 1.0, lon: 1.0 }
matrix:
- [0, 10]
- [10, 0]
"#;
let instance = TspInstanceYaml::from_yaml(yaml).expect("Parse");
let result = instance.validate();
assert!(matches!(
result,
Err(TspInstanceError::InvalidCityId { .. })
));
}
#[test]
fn test_check_symmetry_valid() {
let instance = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Parse");
assert!(instance.check_symmetry().is_ok());
}
#[test]
fn test_check_symmetry_invalid() {
let yaml = r#"
meta:
id: "TEST"
description: "Asymmetric"
cities:
- id: 0
name: "A"
alias: "A"
coords: { lat: 0.0, lon: 0.0 }
- id: 1
name: "B"
alias: "B"
coords: { lat: 1.0, lon: 1.0 }
matrix:
- [0, 10]
- [20, 0]
"#;
let instance = TspInstanceYaml::from_yaml(yaml).expect("Parse");
let result = instance.check_symmetry();
assert!(matches!(
result,
Err(TspInstanceError::AsymmetricMatrix { .. })
));
}
#[test]
fn test_check_triangle_inequality_valid() {
let instance = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Parse");
assert!(instance.check_triangle_inequality().is_ok());
}
#[test]
fn test_check_triangle_inequality_violation() {
let yaml = r#"
meta:
id: "TEST"
description: "Triangle violation"
cities:
- id: 0
name: "A"
alias: "A"
coords: { lat: 0.0, lon: 0.0 }
- id: 1
name: "B"
alias: "B"
coords: { lat: 1.0, lon: 1.0 }
- id: 2
name: "C"
alias: "C"
coords: { lat: 2.0, lon: 2.0 }
matrix:
- [0, 10, 100]
- [10, 0, 10]
- [100, 10, 0]
"#;
let instance = TspInstanceYaml::from_yaml(yaml).expect("Parse");
let result = instance.check_triangle_inequality();
assert!(matches!(
result,
Err(TspInstanceError::TriangleInequalityViolation { .. })
));
}
#[test]
fn test_tour_length_bay_area_optimal() {
let instance = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Parse");
let tour = vec![0, 1, 4, 5, 2, 3];
let length = instance.tour_length(&tour);
assert_eq!(length, 115);
}
#[test]
fn test_tour_length_empty() {
let instance = TspInstanceYaml::from_yaml(MINIMAL_YAML).expect("Parse");
let length = instance.tour_length(&[]);
assert_eq!(length, 0);
}
#[test]
fn test_tour_length_single_city() {
let instance = TspInstanceYaml::from_yaml(MINIMAL_YAML).expect("Parse");
let length = instance.tour_length(&[0]);
assert_eq!(length, 0); }
#[test]
fn test_tour_length_verified() {
let instance = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Parse");
let tour = vec![0, 1, 4, 5, 2, 3];
let (length, steps) = instance.tour_length_verified(&tour);
assert_eq!(length, 115);
assert_eq!(steps.len(), 6);
assert!(steps[0].contains("San Francisco"));
assert!(steps[0].contains("Oakland"));
assert!(steps[0].contains("12"));
}
#[test]
fn test_city_count() {
let instance = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Parse");
assert_eq!(instance.city_count(), 6);
}
#[test]
fn test_distance() {
let instance = TspInstanceYaml::from_yaml(BAY_AREA_YAML).expect("Parse");
assert_eq!(instance.distance(0, 1), 12); assert_eq!(instance.distance(1, 4), 4); }
#[test]
fn test_error_display_parse() {
let err = TspInstanceError::ParseError("test error".to_string());
assert!(err.to_string().contains("YAML parse error"));
}
#[test]
fn test_error_display_matrix_dimension() {
let err = TspInstanceError::MatrixDimensionMismatch {
expected: 6,
got_rows: 4,
};
let msg = err.to_string();
assert!(msg.contains("6x6"));
assert!(msg.contains("4 rows"));
}
#[test]
fn test_error_display_matrix_row() {
let err = TspInstanceError::MatrixRowMismatch {
row: 2,
expected: 6,
got: 4,
};
let msg = err.to_string();
assert!(msg.contains("row 2"));
assert!(msg.contains("4 columns"));
}
#[test]
fn test_error_display_triangle() {
let err = TspInstanceError::TriangleInequalityViolation { i: 0, j: 1, k: 2 };
assert!(err.to_string().contains("Triangle inequality"));
}
#[test]
fn test_error_display_asymmetric() {
let err = TspInstanceError::AsymmetricMatrix {
i: 0,
j: 1,
forward: 10,
backward: 20,
};
let msg = err.to_string();
assert!(msg.contains("Asymmetric"));
assert!(msg.contains("10"));
assert!(msg.contains("20"));
}
#[test]
fn test_error_display_invalid_city() {
let err = TspInstanceError::InvalidCityId { id: 10, max: 5 };
assert!(err.to_string().contains("Invalid city ID"));
}
#[test]
fn test_error_display_io() {
let err = TspInstanceError::IoError("file not found".to_string());
assert!(err.to_string().contains("IO error"));
}
#[test]
fn test_from_yaml_file_not_found() {
let result = TspInstanceYaml::from_yaml_file("/nonexistent/path/file.yaml");
assert!(matches!(result, Err(TspInstanceError::IoError(_))));
}
#[test]
fn test_from_yaml_file_success() {
let result = TspInstanceYaml::from_yaml_file("examples/experiments/bay_area_tsp.yaml");
assert!(result.is_ok());
let instance = result.unwrap();
assert_eq!(instance.meta.id, "TSP-CA-020");
assert_eq!(instance.city_count(), 20);
}
#[test]
fn test_coords_clone_and_copy() {
let coords = Coords {
lat: 37.0,
lon: -122.0,
};
let cloned = coords.clone();
let copied = coords;
assert_eq!(coords, cloned);
assert_eq!(coords, copied);
}
#[test]
fn test_coords_partial_eq() {
let c1 = Coords {
lat: 37.0,
lon: -122.0,
};
let c2 = Coords {
lat: 37.0,
lon: -122.0,
};
let c3 = Coords {
lat: 38.0,
lon: -122.0,
};
assert_eq!(c1, c2);
assert_ne!(c1, c3);
}
#[test]
fn test_tsp_city_clone() {
let city = TspCity::new(0, "SF", "SF", 37.0, -122.0);
let cloned = city.clone();
assert_eq!(city, cloned);
}
#[test]
fn test_tsp_meta_clone() {
let meta = TspMeta::default();
let cloned = meta.clone();
assert_eq!(meta, cloned);
}
#[test]
fn test_tsp_params_clone() {
let params = TspParams::default();
let cloned = params.clone();
assert_eq!(params, cloned);
}
#[test]
fn test_tsp_algorithm_config_clone() {
let config = TspAlgorithmConfig::default();
let cloned = config.clone();
assert_eq!(config, cloned);
}
#[test]
fn test_tsp_instance_yaml_clone() {
let instance = TspInstanceYaml::from_yaml(MINIMAL_YAML).expect("Parse");
let cloned = instance.clone();
assert_eq!(instance, cloned);
}
#[test]
fn test_error_is_error_trait() {
let err: Box<dyn std::error::Error> =
Box::new(TspInstanceError::ParseError("test".to_string()));
assert!(!err.to_string().is_empty());
}
#[test]
fn test_coords_debug() {
let coords = Coords {
lat: 37.0,
lon: -122.0,
};
let debug = format!("{:?}", coords);
assert!(debug.contains("Coords"));
assert!(debug.contains("37"));
}
#[test]
fn test_tsp_instance_error_debug() {
let err = TspInstanceError::ParseError("test".to_string());
let debug = format!("{:?}", err);
assert!(debug.contains("ParseError"));
}
}
#[cfg(all(test, feature = "wasm"))]
mod wasm_tests {
use super::wasm::TspWasmInstance;
const BAY_AREA_YAML: &str = include_str!("../../examples/experiments/bay_area_tsp.yaml");
const MINIMAL_YAML: &str = r#"
meta:
id: "TEST-001"
description: "Minimal test instance"
cities:
- id: 0
name: "A"
alias: "A"
coords: { lat: 0.0, lon: 0.0 }
- id: 1
name: "B"
alias: "B"
coords: { lat: 1.0, lon: 1.0 }
matrix:
- [0, 10]
- [10, 0]
"#;
#[test]
fn test_wasm_from_yaml() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML);
assert!(instance.is_ok());
}
#[test]
fn test_wasm_from_yaml_invalid() {
let result = TspWasmInstance::from_yaml("invalid yaml: [[[");
assert!(result.is_err());
}
#[test]
fn test_wasm_to_yaml() {
let instance = TspWasmInstance::from_yaml(MINIMAL_YAML).expect("parse");
let yaml = instance.to_yaml();
assert!(yaml.is_ok());
assert!(yaml.unwrap().contains("TEST-001"));
}
#[test]
fn test_wasm_validate() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
assert!(instance.validate().is_ok());
}
#[test]
fn test_wasm_check_symmetry() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
assert!(instance.check_symmetry().is_ok());
}
#[test]
fn test_wasm_check_triangle_inequality() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
let result = instance.check_triangle_inequality();
assert!(result.is_ok() || result.is_err());
}
#[test]
fn test_wasm_city_count() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
assert_eq!(instance.city_count(), 20); }
#[test]
fn test_wasm_distance() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
assert_eq!(instance.distance(0, 1), 12); }
#[test]
fn test_wasm_tour_length() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
let tour = [0, 1, 4, 5, 2, 3]; assert_eq!(instance.tour_length(&tour), 115);
}
#[test]
fn test_wasm_get_id() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
assert_eq!(instance.get_id(), "TSP-CA-020"); }
#[test]
fn test_wasm_get_description() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
assert!(instance.get_description().contains("Bay Area"));
}
#[test]
fn test_wasm_get_units() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
assert_eq!(instance.get_units(), "miles");
}
#[test]
fn test_wasm_get_optimal_known() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
assert_eq!(instance.get_optimal_known(), None); }
#[test]
fn test_wasm_get_algorithm_method() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
assert_eq!(instance.get_algorithm_method(), "grasp");
}
#[test]
fn test_wasm_get_seed() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
assert_eq!(instance.get_seed(), 42);
}
#[test]
fn test_wasm_get_rcl_size() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
assert_eq!(instance.get_rcl_size(), 3);
}
#[test]
fn test_wasm_get_restarts() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
assert_eq!(instance.get_restarts(), 100); }
#[test]
fn test_wasm_get_two_opt_enabled() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
assert!(instance.get_two_opt_enabled());
}
#[test]
fn test_wasm_get_city_names_json() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
let json = instance.get_city_names_json();
assert!(json.contains("San Francisco"));
assert!(json.contains("Oakland"));
}
#[test]
fn test_wasm_get_city_aliases_json() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
let json = instance.get_city_aliases_json();
assert!(json.contains("SF"));
assert!(json.contains("OAK"));
}
#[test]
fn test_wasm_get_matrix_json() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
let json = instance.get_matrix_json();
assert!(json.contains("[0,12")); }
#[test]
fn test_wasm_get_city_coords_json() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
let json = instance.get_city_coords_json();
assert!(json.contains("37.7749")); }
#[test]
fn test_wasm_tour_length_verified_json() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
let tour = [0, 1, 4, 5, 2, 3];
let json = instance.tour_length_verified_json(&tour);
assert!(json.contains("\"length\":115"));
assert!(json.contains("steps"));
assert!(json.contains("San Francisco"));
}
#[test]
fn test_wasm_to_json() {
let instance = TspWasmInstance::from_yaml(BAY_AREA_YAML).expect("parse");
let json = instance.to_json();
assert!(json.contains("TSP-CA-020")); assert!(json.contains("cities"));
assert!(json.contains("matrix"));
}
#[test]
fn test_wasm_check_symmetry_fails() {
let asymmetric_yaml = r#"
meta:
id: "ASYM"
description: "Asymmetric"
cities:
- id: 0
name: "A"
alias: "A"
coords: { lat: 0.0, lon: 0.0 }
- id: 1
name: "B"
alias: "B"
coords: { lat: 1.0, lon: 1.0 }
matrix:
- [0, 10]
- [99, 0]
"#;
let instance = TspWasmInstance::from_yaml(asymmetric_yaml).expect("parse");
assert!(instance.check_symmetry().is_err());
}
#[test]
fn test_wasm_validate_fails() {
let bad_yaml = r#"
meta:
id: "BAD"
description: "Bad matrix"
cities:
- id: 0
name: "A"
alias: "A"
coords: { lat: 0.0, lon: 0.0 }
matrix:
- [0, 10, 20]
- [10, 0, 30]
- [20, 30, 0]
"#;
let instance = TspWasmInstance::from_yaml(bad_yaml).expect("parse");
assert!(instance.validate().is_err());
}
}