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