1mod tsplib;
9
10pub use tsplib::TsplibParser;
11
12use crate::error::{TspError, TspResult};
13use std::path::Path;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum EdgeWeightType {
18 Euc2d,
20 Euc3d,
22 Ceil2d,
24 Geo,
26 Explicit,
28 Att,
30}
31
32#[derive(Debug, Clone)]
34pub struct TspInstance {
35 pub name: String,
37 pub dimension: usize,
39 pub comment: Option<String>,
41 pub edge_weight_type: EdgeWeightType,
43 pub coords: Option<Vec<(f64, f64)>>,
45 pub distances: Vec<Vec<f64>>,
47 pub best_known: Option<f64>,
49}
50
51impl TspInstance {
52 pub fn from_coords(name: &str, coords: Vec<(f64, f64)>) -> TspResult<Self> {
54 if coords.is_empty() {
55 return Err(TspError::InvalidInstance {
56 message: "Instance must have at least one city".into(),
57 });
58 }
59
60 let dimension = coords.len();
61 let distances = Self::compute_distance_matrix(&coords);
62
63 Ok(Self {
64 name: name.to_string(),
65 dimension,
66 comment: None,
67 edge_weight_type: EdgeWeightType::Euc2d,
68 coords: Some(coords),
69 distances,
70 best_known: None,
71 })
72 }
73
74 pub fn from_matrix(name: &str, distances: Vec<Vec<f64>>) -> TspResult<Self> {
76 if distances.is_empty() {
77 return Err(TspError::InvalidInstance {
78 message: "Distance matrix cannot be empty".into(),
79 });
80 }
81
82 let dimension = distances.len();
83
84 for (i, row) in distances.iter().enumerate() {
86 if row.len() != dimension {
87 return Err(TspError::InvalidInstance {
88 message: format!(
89 "Distance matrix row {i} has {} elements, expected {dimension}",
90 row.len()
91 ),
92 });
93 }
94 }
95
96 Ok(Self {
97 name: name.to_string(),
98 dimension,
99 comment: None,
100 edge_weight_type: EdgeWeightType::Explicit,
101 coords: None,
102 distances,
103 best_known: None,
104 })
105 }
106
107 pub fn load(path: &Path) -> TspResult<Self> {
109 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
110
111 match extension.to_lowercase().as_str() {
112 "tsp" => TsplibParser::parse_file(path),
113 "csv" => Self::load_csv(path),
114 _ => Err(TspError::ParseError {
115 file: path.to_path_buf(),
116 line: None,
117 cause: format!("Unknown file extension: {extension}"),
118 }),
119 }
120 }
121
122 fn load_csv(path: &Path) -> TspResult<Self> {
124 let content = std::fs::read_to_string(path)?;
125 let mut coords = Vec::new();
126
127 for (line_num, line) in content.lines().enumerate() {
128 let line = line.trim();
129 if line.is_empty() || line.starts_with('#') {
130 continue;
131 }
132
133 let parts: Vec<&str> = line.split(',').collect();
134 if parts.len() < 3 {
135 return Err(TspError::ParseError {
136 file: path.to_path_buf(),
137 line: Some(line_num + 1),
138 cause: "Expected at least 3 columns: id,x,y".into(),
139 });
140 }
141
142 let x: f64 = parts[1].trim().parse().map_err(|_| TspError::ParseError {
143 file: path.to_path_buf(),
144 line: Some(line_num + 1),
145 cause: format!("Invalid x coordinate: {}", parts[1]),
146 })?;
147
148 let y: f64 = parts[2].trim().parse().map_err(|_| TspError::ParseError {
149 file: path.to_path_buf(),
150 line: Some(line_num + 1),
151 cause: format!("Invalid y coordinate: {}", parts[2]),
152 })?;
153
154 coords.push((x, y));
155 }
156
157 let name = path
158 .file_stem()
159 .and_then(|s| s.to_str())
160 .unwrap_or("unnamed")
161 .to_string();
162
163 Self::from_coords(&name, coords)
164 }
165
166 fn compute_distance_matrix(coords: &[(f64, f64)]) -> Vec<Vec<f64>> {
168 let n = coords.len();
169 let mut matrix = vec![vec![0.0; n]; n];
170
171 for i in 0..n {
172 for j in i + 1..n {
173 let dx = coords[i].0 - coords[j].0;
174 let dy = coords[i].1 - coords[j].1;
175 let dist = (dx * dx + dy * dy).sqrt();
176 matrix[i][j] = dist;
177 matrix[j][i] = dist;
178 }
179 }
180
181 matrix
182 }
183
184 #[inline]
186 pub fn num_cities(&self) -> usize {
187 self.dimension
188 }
189
190 #[inline]
192 pub fn distance(&self, i: usize, j: usize) -> f64 {
193 self.distances[i][j]
194 }
195
196 pub fn tour_length(&self, tour: &[usize]) -> f64 {
198 if tour.is_empty() {
199 return 0.0;
200 }
201
202 let mut length = 0.0;
203 for window in tour.windows(2) {
204 length += self.distance(window[0], window[1]);
205 }
206 length += self.distance(tour[tour.len() - 1], tour[0]);
208 length
209 }
210
211 pub fn validate_tour(&self, tour: &[usize]) -> TspResult<()> {
213 if tour.len() != self.dimension {
214 return Err(TspError::InvalidInstance {
215 message: format!(
216 "Tour has {} cities, expected {}",
217 tour.len(),
218 self.dimension
219 ),
220 });
221 }
222
223 let mut visited = vec![false; self.dimension];
224 for &city in tour {
225 if city >= self.dimension {
226 return Err(TspError::InvalidInstance {
227 message: format!("City {city} out of range [0, {})", self.dimension),
228 });
229 }
230 if visited[city] {
231 return Err(TspError::InvalidInstance {
232 message: format!("City {city} visited multiple times"),
233 });
234 }
235 visited[city] = true;
236 }
237
238 Ok(())
239 }
240
241 pub fn nearest_neighbor_rank(&self, i: usize, j: usize) -> usize {
243 let mut neighbors: Vec<(usize, f64)> = (0..self.dimension)
244 .filter(|&k| k != i)
245 .map(|k| (k, self.distance(i, k)))
246 .collect();
247 neighbors.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
248
249 neighbors
250 .iter()
251 .position(|&(k, _)| k == j)
252 .map_or(self.dimension, |p| p + 1)
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_from_coords_creates_instance() {
262 let coords = vec![(0.0, 0.0), (3.0, 0.0), (3.0, 4.0)];
263 let instance = TspInstance::from_coords("test", coords).expect("should create instance");
264
265 assert_eq!(instance.name, "test");
266 assert_eq!(instance.dimension, 3);
267 assert_eq!(instance.num_cities(), 3);
268 }
269
270 #[test]
271 fn test_from_coords_computes_distances() {
272 let coords = vec![(0.0, 0.0), (3.0, 0.0), (3.0, 4.0)];
274 let instance = TspInstance::from_coords("test", coords).expect("should create instance");
275
276 assert!((instance.distance(0, 1) - 3.0).abs() < 1e-10);
277 assert!((instance.distance(1, 2) - 4.0).abs() < 1e-10);
278 assert!((instance.distance(0, 2) - 5.0).abs() < 1e-10);
279 }
280
281 #[test]
282 fn test_from_coords_empty_fails() {
283 let result = TspInstance::from_coords("test", vec![]);
284 assert!(result.is_err());
285 }
286
287 #[test]
288 fn test_from_matrix_creates_instance() {
289 let matrix = vec![
290 vec![0.0, 10.0, 15.0],
291 vec![10.0, 0.0, 20.0],
292 vec![15.0, 20.0, 0.0],
293 ];
294 let instance = TspInstance::from_matrix("test", matrix).expect("should create instance");
295
296 assert_eq!(instance.dimension, 3);
297 assert!((instance.distance(0, 1) - 10.0).abs() < 1e-10);
298 }
299
300 #[test]
301 fn test_from_matrix_non_square_fails() {
302 let matrix = vec![vec![0.0, 10.0], vec![10.0, 0.0, 5.0]];
303 let result = TspInstance::from_matrix("test", matrix);
304 assert!(result.is_err());
305 }
306
307 #[test]
308 fn test_tour_length_calculation() {
309 let coords = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
310 let instance = TspInstance::from_coords("square", coords).expect("should create");
311
312 let tour = vec![0, 1, 2, 3];
314 let length = instance.tour_length(&tour);
315 assert!((length - 4.0).abs() < 1e-10);
316 }
317
318 #[test]
319 fn test_validate_tour_correct() {
320 let coords = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)];
321 let instance = TspInstance::from_coords("test", coords).expect("should create");
322
323 let tour = vec![0, 1, 2];
324 assert!(instance.validate_tour(&tour).is_ok());
325 }
326
327 #[test]
328 fn test_validate_tour_wrong_length() {
329 let coords = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)];
330 let instance = TspInstance::from_coords("test", coords).expect("should create");
331
332 let tour = vec![0, 1];
333 assert!(instance.validate_tour(&tour).is_err());
334 }
335
336 #[test]
337 fn test_validate_tour_duplicate_city() {
338 let coords = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)];
339 let instance = TspInstance::from_coords("test", coords).expect("should create");
340
341 let tour = vec![0, 1, 1];
342 assert!(instance.validate_tour(&tour).is_err());
343 }
344
345 #[test]
346 fn test_validate_tour_out_of_range() {
347 let coords = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)];
348 let instance = TspInstance::from_coords("test", coords).expect("should create");
349
350 let tour = vec![0, 1, 5];
351 assert!(instance.validate_tour(&tour).is_err());
352 }
353
354 #[test]
355 fn test_nearest_neighbor_rank() {
356 let coords = vec![(0.0, 0.0), (1.0, 0.0), (3.0, 0.0)];
359 let instance = TspInstance::from_coords("test", coords).expect("should create");
360
361 assert_eq!(instance.nearest_neighbor_rank(0, 1), 1); assert_eq!(instance.nearest_neighbor_rank(0, 2), 2); }
364
365 #[test]
366 fn test_empty_tour_length() {
367 let coords = vec![(0.0, 0.0), (1.0, 0.0)];
368 let instance = TspInstance::from_coords("test", coords).expect("should create");
369
370 assert!((instance.tour_length(&[]) - 0.0).abs() < 1e-10);
371 }
372}