1use u_nesting_core::timing::Timer;
23
24use crate::config::{CutDirectionPreference, CuttingConfig};
25use crate::contour::{ContourType, CutContour};
26use crate::cost::point_distance;
27use crate::result::CutDirection;
28
29#[derive(Debug, Clone)]
31pub struct PierceCandidate {
32 pub contour_id: usize,
34 pub candidate_index: usize,
36 pub point: (f64, f64),
38 pub vertex_index: usize,
40 pub direction: CutDirection,
42 pub end_point: (f64, f64),
44}
45
46#[derive(Debug, Clone)]
48pub struct GtspCluster {
49 pub contour_id: usize,
51 pub candidates: Vec<PierceCandidate>,
53}
54
55#[derive(Debug, Clone)]
57pub struct GtspInstance {
58 pub clusters: Vec<GtspCluster>,
60 pub distances: Vec<Vec<f64>>,
65 pub home_distances: Vec<f64>,
67 pub cluster_offsets: Vec<usize>,
69 pub total_candidates: usize,
71}
72
73impl GtspInstance {
74 pub fn global_to_local(&self, global_idx: usize) -> (usize, usize) {
76 for (c, offset) in self.cluster_offsets.iter().enumerate() {
77 let size = self.clusters[c].candidates.len();
78 if global_idx >= *offset && global_idx < offset + size {
79 return (c, global_idx - offset);
80 }
81 }
82 (self.clusters.len() - 1, 0)
84 }
85
86 pub fn local_to_global(&self, cluster_idx: usize, candidate_idx: usize) -> usize {
88 self.cluster_offsets[cluster_idx] + candidate_idx
89 }
90
91 pub fn candidate(&self, global_idx: usize) -> &PierceCandidate {
93 let (c, l) = self.global_to_local(global_idx);
94 &self.clusters[c].candidates[l]
95 }
96}
97
98pub fn discretize_contours(contours: &[CutContour], config: &CuttingConfig) -> Vec<GtspCluster> {
108 let n_candidates = config.pierce_candidates.max(1);
109
110 contours
111 .iter()
112 .map(|contour| {
113 let direction = determine_direction(contour.contour_type, config);
114 let candidates = if n_candidates == 1 {
115 vec![PierceCandidate {
117 contour_id: contour.id,
118 candidate_index: 0,
119 point: contour.vertices[0],
120 vertex_index: 0,
121 direction,
122 end_point: contour.vertices[0],
123 }]
124 } else {
125 generate_equidistant_candidates(contour, n_candidates, direction)
126 };
127
128 GtspCluster {
129 contour_id: contour.id,
130 candidates,
131 }
132 })
133 .collect()
134}
135
136pub fn build_gtsp_instance(clusters: Vec<GtspCluster>, home: (f64, f64)) -> GtspInstance {
147 let mut cluster_offsets = Vec::with_capacity(clusters.len());
149 let mut offset = 0;
150 for cluster in &clusters {
151 cluster_offsets.push(offset);
152 offset += cluster.candidates.len();
153 }
154 let total = offset;
155
156 let all_candidates: Vec<&PierceCandidate> =
158 clusters.iter().flat_map(|c| c.candidates.iter()).collect();
159
160 let mut distances = vec![vec![f64::MAX; total]; total];
162 for (i, ci) in all_candidates.iter().enumerate() {
163 for (j, cj) in all_candidates.iter().enumerate() {
164 if ci.contour_id == cj.contour_id {
165 continue; }
167 distances[i][j] = point_distance(ci.end_point, cj.point);
168 }
169 }
170
171 let home_distances: Vec<f64> = all_candidates
173 .iter()
174 .map(|c| point_distance(home, c.point))
175 .collect();
176
177 GtspInstance {
178 clusters,
179 distances,
180 home_distances,
181 cluster_offsets,
182 total_candidates: total,
183 }
184}
185
186pub fn evaluate_solution(instance: &GtspInstance, solution: &[usize]) -> f64 {
190 if solution.is_empty() {
191 return 0.0;
192 }
193
194 let mut total = instance.home_distances[solution[0]];
195
196 for i in 1..solution.len() {
197 total += instance.distances[solution[i - 1]][solution[i]];
198 }
199
200 total
201}
202
203pub fn solve_nn(instance: &GtspInstance) -> Vec<usize> {
208 let n_clusters = instance.clusters.len();
209 if n_clusters == 0 {
210 return Vec::new();
211 }
212
213 let mut visited_clusters = vec![false; n_clusters];
214 let mut solution = Vec::with_capacity(n_clusters);
215
216 let mut best_idx = 0;
218 let mut best_dist = f64::MAX;
219 for (g, dist) in instance.home_distances.iter().enumerate() {
220 if *dist < best_dist {
221 best_dist = *dist;
222 best_idx = g;
223 }
224 }
225
226 let (cluster, _) = instance.global_to_local(best_idx);
227 visited_clusters[cluster] = true;
228 solution.push(best_idx);
229
230 for _ in 1..n_clusters {
232 let last = *solution.last().expect("solution not empty");
233 let mut next_best = 0;
234 let mut next_dist = f64::MAX;
235
236 for (g, &dist) in instance.distances[last].iter().enumerate() {
237 let (c, _) = instance.global_to_local(g);
238 if visited_clusters[c] {
239 continue;
240 }
241 if dist < next_dist {
242 next_dist = dist;
243 next_best = g;
244 }
245 }
246
247 let (c, _) = instance.global_to_local(next_best);
248 visited_clusters[c] = true;
249 solution.push(next_best);
250 }
251
252 solution
253}
254
255pub fn solve_constrained(
264 instance: &GtspInstance,
265 dag: &crate::hierarchy::CuttingDag,
266 config: &CuttingConfig,
267) -> Vec<usize> {
268 let n_clusters = instance.clusters.len();
269 if n_clusters == 0 {
270 return Vec::new();
271 }
272
273 let mut solution = nn_constrained(instance, dag);
275
276 if config.max_2opt_iterations > 0 && solution.len() >= 3 {
278 improve_2opt_constrained(&mut solution, instance, dag, config);
279 }
280
281 solution
282}
283
284fn nn_constrained(instance: &GtspInstance, dag: &crate::hierarchy::CuttingDag) -> Vec<usize> {
286 let n_clusters = instance.clusters.len();
287 let mut visited_clusters = vec![false; n_clusters];
288 let mut solution = Vec::with_capacity(n_clusters);
289 let mut visited_contours: std::collections::HashSet<usize> =
290 std::collections::HashSet::with_capacity(n_clusters);
291
292 for _ in 0..n_clusters {
293 let mut best_idx = None;
294 let mut best_dist = f64::MAX;
295
296 for (ci, cluster) in instance.clusters.iter().enumerate() {
297 if visited_clusters[ci] {
298 continue;
299 }
300
301 let predecessors = dag.predecessors(cluster.contour_id);
303 let ready = predecessors
304 .iter()
305 .all(|pred_id| visited_contours.contains(pred_id));
306 if !ready {
307 continue;
308 }
309
310 for cand in &cluster.candidates {
312 let global = instance.local_to_global(ci, cand.candidate_index);
313 let dist = if solution.is_empty() {
314 instance.home_distances[global]
315 } else {
316 let last: usize = *solution.last().expect("solution not empty");
317 let row: &Vec<f64> = &instance.distances[last];
318 row[global]
319 };
320
321 if dist < best_dist {
322 best_dist = dist;
323 best_idx = Some((ci, global));
324 }
325 }
326 }
327
328 if let Some((ci, global)) = best_idx {
329 visited_clusters[ci] = true;
330 visited_contours.insert(instance.clusters[ci].contour_id);
331 solution.push(global);
332 }
333 }
334
335 solution
336}
337
338const TIME_CHECK_INTERVAL: u32 = 512;
341
342fn improve_2opt_constrained(
351 solution: &mut [usize],
352 instance: &GtspInstance,
353 dag: &crate::hierarchy::CuttingDag,
354 config: &CuttingConfig,
355) {
356 let n = solution.len();
357 let timer = Timer::now();
358 let time_limited = config.time_limit_ms > 0;
359 let mut since_check: u32 = 0;
360 let mut improved = true;
361 let mut iterations = 0;
362 let mut current_cost = evaluate_solution(instance, solution);
363
364 while improved && iterations < config.max_2opt_iterations {
365 improved = false;
366 iterations += 1;
367
368 for pos in 0..n {
370 let current_global = solution[pos];
371 let (ci, _) = instance.global_to_local(current_global);
372 let cluster = &instance.clusters[ci];
373
374 for cand in &cluster.candidates {
375 let alt_global = instance.local_to_global(ci, cand.candidate_index);
376 if alt_global == current_global {
377 continue;
378 }
379
380 since_check += 1;
383 if time_limited && since_check >= TIME_CHECK_INTERVAL {
384 since_check = 0;
385 if timer.elapsed_ms() >= config.time_limit_ms {
386 return;
387 }
388 }
389
390 solution[pos] = alt_global;
391 let new_cost = evaluate_solution(instance, solution);
392
393 if new_cost < current_cost - 1e-10 {
394 current_cost = new_cost;
395 improved = true;
396 } else {
397 solution[pos] = current_global; }
399 }
400 }
401
402 for i in 0..n.saturating_sub(1) {
404 for j in (i + 2)..n {
405 since_check += 1;
408 if time_limited && since_check >= TIME_CHECK_INTERVAL {
409 since_check = 0;
410 if timer.elapsed_ms() >= config.time_limit_ms {
411 return;
412 }
413 }
414
415 solution[i + 1..=j].reverse();
416
417 let cluster_order: Vec<usize> = solution
419 .iter()
420 .map(|&g| instance.clusters[instance.global_to_local(g).0].contour_id)
421 .collect();
422
423 if dag.is_valid_sequence(&cluster_order) {
424 let new_cost = evaluate_solution(instance, solution);
425 if new_cost < current_cost - 1e-10 {
426 current_cost = new_cost;
427 improved = true;
428 } else {
429 solution[i + 1..=j].reverse(); }
431 } else {
432 solution[i + 1..=j].reverse(); }
434 }
435 }
436 }
437}
438
439fn determine_direction(contour_type: ContourType, config: &CuttingConfig) -> CutDirection {
441 let pref = match contour_type {
442 ContourType::Exterior => config.exterior_direction,
443 ContourType::Interior => config.interior_direction,
444 };
445
446 match pref {
447 CutDirectionPreference::Ccw => CutDirection::Ccw,
448 CutDirectionPreference::Cw => CutDirection::Cw,
449 CutDirectionPreference::Auto => match contour_type {
450 ContourType::Exterior => CutDirection::Ccw,
451 ContourType::Interior => CutDirection::Cw,
452 },
453 }
454}
455
456fn generate_equidistant_candidates(
458 contour: &CutContour,
459 n: usize,
460 direction: CutDirection,
461) -> Vec<PierceCandidate> {
462 let vertices = &contour.vertices;
463 let nv = vertices.len();
464 if nv == 0 {
465 return Vec::new();
466 }
467
468 let mut edge_lengths = Vec::with_capacity(nv);
470 let mut cumulative = Vec::with_capacity(nv + 1);
471 cumulative.push(0.0);
472
473 for i in 0..nv {
474 let j = (i + 1) % nv;
475 let len = point_distance(vertices[i], vertices[j]);
476 edge_lengths.push(len);
477 cumulative.push(cumulative[i] + len);
478 }
479
480 let perimeter = *cumulative.last().expect("at least one vertex");
481 if perimeter < 1e-12 {
482 return vec![PierceCandidate {
483 contour_id: contour.id,
484 candidate_index: 0,
485 point: vertices[0],
486 vertex_index: 0,
487 direction,
488 end_point: vertices[0],
489 }];
490 }
491
492 let spacing = perimeter / n as f64;
493 let mut candidates = Vec::with_capacity(n);
494
495 for k in 0..n {
496 let target_dist = k as f64 * spacing;
497
498 let (point, vertex_idx) =
500 point_at_distance(vertices, &cumulative, &edge_lengths, target_dist);
501
502 candidates.push(PierceCandidate {
503 contour_id: contour.id,
504 candidate_index: k,
505 point,
506 vertex_index: vertex_idx,
507 direction,
508 end_point: point, });
510 }
511
512 candidates
513}
514
515fn point_at_distance(
517 vertices: &[(f64, f64)],
518 cumulative: &[f64],
519 edge_lengths: &[f64],
520 distance: f64,
521) -> ((f64, f64), usize) {
522 let nv = vertices.len();
523 let perimeter = cumulative[nv];
524 let dist = distance % perimeter;
525
526 for i in 0..nv {
527 if dist >= cumulative[i] && dist <= cumulative[i + 1] + 1e-12 {
528 let edge_len = edge_lengths[i];
529 if edge_len < 1e-12 {
530 return (vertices[i], i);
531 }
532 let t = (dist - cumulative[i]) / edge_len;
533 let j = (i + 1) % nv;
534 let px = vertices[i].0 + t * (vertices[j].0 - vertices[i].0);
535 let py = vertices[i].1 + t * (vertices[j].1 - vertices[i].1);
536 return ((px, py), i);
537 }
538 }
539
540 (vertices[nv - 1], nv - 1)
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547
548 fn make_rect(id: usize, x: f64, y: f64, w: f64, h: f64) -> CutContour {
549 CutContour {
550 id,
551 geometry_id: format!("part{}", id),
552 instance: 0,
553 contour_type: ContourType::Exterior,
554 vertices: vec![(x, y), (x + w, y), (x + w, y + h), (x, y + h)],
555 perimeter: 2.0 * (w + h),
556 centroid: (x + w / 2.0, y + h / 2.0),
557 }
558 }
559
560 #[test]
561 fn test_discretize_single_candidate() {
562 let contours = vec![make_rect(0, 0.0, 0.0, 10.0, 10.0)];
563 let config = CuttingConfig::new().with_pierce_candidates(1);
564 let clusters = discretize_contours(&contours, &config);
565
566 assert_eq!(clusters.len(), 1);
567 assert_eq!(clusters[0].candidates.len(), 1);
568 assert_eq!(clusters[0].candidates[0].point, (0.0, 0.0));
569 }
570
571 #[test]
572 fn test_discretize_four_candidates_on_square() {
573 let contours = vec![make_rect(0, 0.0, 0.0, 10.0, 10.0)];
574 let config = CuttingConfig::new().with_pierce_candidates(4);
575 let clusters = discretize_contours(&contours, &config);
576
577 assert_eq!(clusters.len(), 1);
578 let cands = &clusters[0].candidates;
579 assert_eq!(cands.len(), 4);
580
581 assert!((cands[0].point.0 - 0.0).abs() < 1e-10);
585 assert!((cands[0].point.1 - 0.0).abs() < 1e-10);
586 assert!((cands[1].point.0 - 10.0).abs() < 1e-10);
587 assert!((cands[1].point.1 - 0.0).abs() < 1e-10);
588 assert!((cands[2].point.0 - 10.0).abs() < 1e-10);
589 assert!((cands[2].point.1 - 10.0).abs() < 1e-10);
590 assert!((cands[3].point.0 - 0.0).abs() < 1e-10);
591 assert!((cands[3].point.1 - 10.0).abs() < 1e-10);
592 }
593
594 #[test]
595 fn test_discretize_eight_candidates_midpoints() {
596 let contours = vec![make_rect(0, 0.0, 0.0, 10.0, 10.0)];
597 let config = CuttingConfig::new().with_pierce_candidates(8);
598 let clusters = discretize_contours(&contours, &config);
599
600 let cands = &clusters[0].candidates;
601 assert_eq!(cands.len(), 8);
602
603 assert!((cands[1].point.0 - 5.0).abs() < 1e-10);
606 assert!((cands[1].point.1 - 0.0).abs() < 1e-10);
607 assert!((cands[3].point.0 - 10.0).abs() < 1e-10);
608 assert!((cands[3].point.1 - 5.0).abs() < 1e-10);
609 }
610
611 #[test]
612 fn test_build_gtsp_instance_distances() {
613 let contours = vec![
614 make_rect(0, 0.0, 0.0, 10.0, 10.0),
615 make_rect(1, 20.0, 0.0, 10.0, 10.0),
616 ];
617 let config = CuttingConfig::new().with_pierce_candidates(1);
618 let clusters = discretize_contours(&contours, &config);
619 let instance = build_gtsp_instance(clusters, (0.0, 0.0));
620
621 assert_eq!(instance.total_candidates, 2);
622 assert_eq!(instance.clusters.len(), 2);
623
624 assert_eq!(instance.distances[0][0], f64::MAX);
626 assert_eq!(instance.distances[1][1], f64::MAX);
627
628 assert!((instance.distances[0][1] - 20.0).abs() < 1e-10);
630 assert!((instance.distances[1][0] - 20.0).abs() < 1e-10);
632
633 assert!((instance.home_distances[0] - 0.0).abs() < 1e-10);
635 assert!((instance.home_distances[1] - 20.0).abs() < 1e-10);
636 }
637
638 #[test]
639 fn test_global_to_local() {
640 let contours = vec![
641 make_rect(0, 0.0, 0.0, 10.0, 10.0),
642 make_rect(1, 20.0, 0.0, 10.0, 10.0),
643 ];
644 let config = CuttingConfig::new().with_pierce_candidates(4);
645 let clusters = discretize_contours(&contours, &config);
646 let instance = build_gtsp_instance(clusters, (0.0, 0.0));
647
648 assert_eq!(instance.total_candidates, 8);
649 assert_eq!(instance.global_to_local(0), (0, 0));
650 assert_eq!(instance.global_to_local(3), (0, 3));
651 assert_eq!(instance.global_to_local(4), (1, 0));
652 assert_eq!(instance.global_to_local(7), (1, 3));
653 }
654
655 #[test]
656 fn test_evaluate_solution() {
657 let contours = vec![
658 make_rect(0, 0.0, 0.0, 10.0, 10.0),
659 make_rect(1, 30.0, 0.0, 10.0, 10.0),
660 ];
661 let config = CuttingConfig::new().with_pierce_candidates(1);
662 let clusters = discretize_contours(&contours, &config);
663 let instance = build_gtsp_instance(clusters, (0.0, 0.0));
664
665 let cost = evaluate_solution(&instance, &[0, 1]);
667 assert!((cost - 30.0).abs() < 1e-10);
670 }
671
672 #[test]
673 fn test_solve_nn() {
674 let contours = vec![
675 make_rect(0, 50.0, 0.0, 10.0, 10.0),
676 make_rect(1, 10.0, 0.0, 10.0, 10.0),
677 make_rect(2, 30.0, 0.0, 10.0, 10.0),
678 ];
679 let config = CuttingConfig::new().with_pierce_candidates(1);
680 let clusters = discretize_contours(&contours, &config);
681 let instance = build_gtsp_instance(clusters, (0.0, 0.0));
682
683 let solution = solve_nn(&instance);
684 assert_eq!(solution.len(), 3);
685
686 let cluster_order: Vec<usize> = solution
688 .iter()
689 .map(|&g| instance.global_to_local(g).0)
690 .collect();
691 assert_eq!(cluster_order, vec![1, 2, 0]);
692 }
693
694 #[test]
695 fn test_solve_nn_empty() {
696 let instance = build_gtsp_instance(Vec::new(), (0.0, 0.0));
697 let solution = solve_nn(&instance);
698 assert!(solution.is_empty());
699 }
700
701 #[test]
702 fn test_nn_picks_best_candidate() {
703 let contours = vec![
709 make_rect(0, 0.0, 0.0, 10.0, 10.0),
710 make_rect(1, 12.0, 0.0, 10.0, 10.0),
711 ];
712 let config = CuttingConfig::new().with_pierce_candidates(4);
713 let clusters = discretize_contours(&contours, &config);
714 let instance = build_gtsp_instance(clusters, (0.0, 0.0));
715
716 let solution = solve_nn(&instance);
717 assert_eq!(solution.len(), 2);
718
719 let (c0, _) = instance.global_to_local(solution[0]);
721 assert_eq!(c0, 0);
722
723 let (c1, l1) = instance.global_to_local(solution[1]);
725 assert_eq!(c1, 1);
726 let picked = &instance.clusters[1].candidates[l1];
728 assert!((picked.point.0 - 12.0).abs() < 1e-10);
729 }
730
731 #[test]
732 fn test_direction_assignment() {
733 let mut contours = vec![make_rect(0, 0.0, 0.0, 10.0, 10.0)];
734 contours[0].contour_type = ContourType::Interior;
735
736 let config = CuttingConfig::default();
737 let clusters = discretize_contours(&contours, &config);
738
739 assert_eq!(clusters[0].candidates[0].direction, CutDirection::Cw);
741 }
742
743 #[test]
744 fn test_multi_candidate_improves_over_single() {
745 let contours = vec![
748 make_rect(0, 0.0, 0.0, 10.0, 10.0),
749 make_rect(1, 15.0, 5.0, 10.0, 10.0),
750 make_rect(2, 30.0, 0.0, 10.0, 10.0),
751 ];
752
753 let config1 = CuttingConfig::new().with_pierce_candidates(1);
755 let clusters1 = discretize_contours(&contours, &config1);
756 let inst1 = build_gtsp_instance(clusters1, (0.0, 0.0));
757 let sol1 = solve_nn(&inst1);
758 let cost1 = evaluate_solution(&inst1, &sol1);
759
760 let config8 = CuttingConfig::new().with_pierce_candidates(8);
762 let clusters8 = discretize_contours(&contours, &config8);
763 let inst8 = build_gtsp_instance(clusters8, (0.0, 0.0));
764 let sol8 = solve_nn(&inst8);
765 let cost8 = evaluate_solution(&inst8, &sol8);
766
767 assert!(
769 cost8 <= cost1 + 1e-6,
770 "Multi-candidate cost {} should be <= single-candidate cost {}",
771 cost8,
772 cost1
773 );
774 }
775
776 #[test]
777 fn test_solve_constrained_respects_precedence() {
778 use crate::hierarchy::CuttingDag;
779
780 let contours = vec![
782 CutContour {
783 id: 0,
784 geometry_id: "part1".to_string(),
785 instance: 0,
786 contour_type: ContourType::Exterior,
787 vertices: vec![(0.0, 0.0), (20.0, 0.0), (20.0, 20.0), (0.0, 20.0)],
788 perimeter: 80.0,
789 centroid: (10.0, 10.0),
790 },
791 CutContour {
792 id: 1,
793 geometry_id: "part1".to_string(),
794 instance: 0,
795 contour_type: ContourType::Interior,
796 vertices: vec![(5.0, 5.0), (15.0, 5.0), (15.0, 15.0), (5.0, 15.0)],
797 perimeter: 40.0,
798 centroid: (10.0, 10.0),
799 },
800 ];
801
802 let dag = CuttingDag::build(&contours);
803 let config = CuttingConfig::new().with_pierce_candidates(4);
804 let clusters = discretize_contours(&contours, &config);
805 let instance = build_gtsp_instance(clusters, (0.0, 0.0));
806
807 let solution = solve_constrained(&instance, &dag, &config);
808 assert_eq!(solution.len(), 2);
809
810 let cluster_order: Vec<usize> = solution
812 .iter()
813 .map(|&g| instance.clusters[instance.global_to_local(g).0].contour_id)
814 .collect();
815
816 let pos_interior = cluster_order.iter().position(|&id| id == 1).unwrap();
817 let pos_exterior = cluster_order.iter().position(|&id| id == 0).unwrap();
818 assert!(pos_interior < pos_exterior);
819 }
820
821 #[test]
822 fn test_solve_constrained_with_multiple_parts() {
823 use crate::hierarchy::CuttingDag;
824
825 let contours = vec![
826 make_rect(0, 0.0, 0.0, 10.0, 10.0),
827 make_rect(1, 15.0, 0.0, 10.0, 10.0),
828 make_rect(2, 30.0, 0.0, 10.0, 10.0),
829 ];
830
831 let dag = CuttingDag::build(&contours);
832 let config = CuttingConfig::new().with_pierce_candidates(4);
833 let clusters = discretize_contours(&contours, &config);
834 let instance = build_gtsp_instance(clusters, (0.0, 0.0));
835
836 let solution = solve_constrained(&instance, &dag, &config);
837 assert_eq!(solution.len(), 3);
838
839 let cluster_order: Vec<usize> = solution
841 .iter()
842 .map(|&g| instance.global_to_local(g).0)
843 .collect();
844 assert_eq!(cluster_order, vec![0, 1, 2]);
845 }
846
847 #[test]
848 fn test_2opt_improves_solution() {
849 use crate::hierarchy::CuttingDag;
850
851 let contours = vec![
856 make_rect(0, 0.0, 0.0, 10.0, 10.0),
857 make_rect(1, 20.0, 0.0, 10.0, 10.0),
858 make_rect(2, 40.0, 0.0, 10.0, 10.0),
859 ];
860
861 let dag = CuttingDag::build(&contours);
862 let config = CuttingConfig::new().with_pierce_candidates(4);
863 let clusters = discretize_contours(&contours, &config);
864 let instance = build_gtsp_instance(clusters, (0.0, 0.0));
865
866 let solution = solve_constrained(&instance, &dag, &config);
867 let cost = evaluate_solution(&instance, &solution);
868
869 assert!(
872 cost < 100.0,
873 "Solution cost {} should be < worst case 100",
874 cost
875 );
876 }
877
878 #[test]
879 fn test_constrained_empty() {
880 use crate::hierarchy::CuttingDag;
881
882 let contours: Vec<CutContour> = Vec::new();
883 let dag = CuttingDag::build(&contours);
884 let instance = build_gtsp_instance(Vec::new(), (0.0, 0.0));
885 let config = CuttingConfig::default();
886
887 let solution = solve_constrained(&instance, &dag, &config);
888 assert!(solution.is_empty());
889 }
890}