1use crate::boundary::Boundary2D;
15use crate::clamp_placement_to_boundary;
16use crate::geometry::Geometry2D;
17use crate::nfp::{
18 compute_ifp_with_margin_and_mirror, compute_nfp_mirrored, find_bottom_left_placement,
19 verify_no_overlap_mirrored, Nfp, PlacedGeometry,
20};
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::sync::Arc;
23use u_nesting_core::geometry::{Boundary, Geometry};
24use u_nesting_core::sa::{
25 NeighborhoodOperator, PermutationSolution, SaConfig, SaProblem, SaRunner, SaSolution,
26};
27use u_nesting_core::solver::Config;
28use u_nesting_core::{Placement, SolveResult};
29
30use crate::placement_utils::{expand_nfp, nesting_fitness, shrink_ifp, InstanceInfo};
31
32pub struct SaNestingProblem {
34 geometries: Vec<Geometry2D>,
36 boundary: Boundary2D,
38 config: Config,
40 instances: Vec<InstanceInfo>,
42 rotation_angles: Vec<Vec<f64>>,
44 max_rotation_options: usize,
46 any_allow_flip: bool,
50 cancelled: Arc<AtomicBool>,
52}
53
54impl SaNestingProblem {
55 pub fn new(
57 geometries: Vec<Geometry2D>,
58 boundary: Boundary2D,
59 config: Config,
60 cancelled: Arc<AtomicBool>,
61 ) -> Self {
62 let mut instances = Vec::new();
64 let mut rotation_angles = Vec::new();
65 let mut max_rotation_options = 1;
66 let mut any_allow_flip = false;
67
68 for (geom_idx, geom) in geometries.iter().enumerate() {
69 let angles = geom.rotations();
71 let angles = if angles.is_empty() { vec![0.0] } else { angles };
72 max_rotation_options = max_rotation_options.max(angles.len());
73 rotation_angles.push(angles);
74 any_allow_flip = any_allow_flip || geom.allow_flip();
75
76 for instance_num in 0..geom.quantity() {
78 instances.push(InstanceInfo {
79 geometry_idx: geom_idx,
80 instance_num,
81 });
82 }
83 }
84
85 Self {
86 geometries,
87 boundary,
88 config,
89 instances,
90 rotation_angles,
91 max_rotation_options,
92 any_allow_flip,
93 cancelled,
94 }
95 }
96
97 pub fn num_instances(&self) -> usize {
99 self.instances.len()
100 }
101
102 pub fn decode(&self, solution: &PermutationSolution) -> (Vec<Placement<f64>>, f64, usize) {
104 let n = self.instances.len();
105 if n == 0 || solution.sequence.is_empty() {
106 return (Vec::new(), 0.0, 0);
107 }
108
109 let mut placements = Vec::new();
110 let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
111 let mut total_placed_area = 0.0;
112 let mut placed_count = 0;
113
114 let margin = self.config.margin;
115 let spacing = self.config.spacing;
116
117 let boundary_polygon = self.get_boundary_polygon_with_margin(margin);
119
120 let sample_step = self.compute_sample_step();
122
123 for (seq_idx, &instance_idx) in solution.sequence.iter().enumerate() {
125 if self.cancelled.load(Ordering::Relaxed) {
126 break;
127 }
128
129 if instance_idx >= self.instances.len() {
130 continue;
131 }
132
133 let info = &self.instances[instance_idx];
134 let geom = &self.geometries[info.geometry_idx];
135
136 let rotation_idx = solution.rotations.get(seq_idx).copied().unwrap_or(0);
138 let num_rotations = self
139 .rotation_angles
140 .get(info.geometry_idx)
141 .map(|a| a.len())
142 .unwrap_or(1);
143
144 let rotation_angle = self
145 .rotation_angles
146 .get(info.geometry_idx)
147 .and_then(|angles| angles.get(rotation_idx % num_rotations))
148 .copied()
149 .unwrap_or(0.0);
150
151 let mirror =
155 solution.mirrors.get(seq_idx).copied().unwrap_or(false) && geom.allow_flip();
156
157 let ifp = match compute_ifp_with_margin_and_mirror(
159 &boundary_polygon,
160 geom,
161 rotation_angle,
162 0.0,
163 mirror,
164 ) {
165 Ok(ifp) => ifp,
166 Err(_) => continue,
167 };
168
169 if ifp.is_empty() {
170 continue;
171 }
172
173 let mut nfps: Vec<Nfp> = Vec::new();
175 for placed in &placed_geometries {
176 let placed_exterior = placed.translated_exterior();
179 let placed_geom = Geometry2D::new(format!("_placed_{}", placed.geometry.id()))
180 .with_polygon(placed_exterior);
181
182 if let Ok(nfp) =
183 compute_nfp_mirrored(&placed_geom, geom, rotation_angle, false, mirror)
184 {
185 let expanded = expand_nfp(&nfp, spacing);
186 nfps.push(expanded);
187 }
188 }
189
190 let ifp_shrunk = shrink_ifp(&ifp, spacing);
192
193 let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
197 if let Some((x, y)) = find_bottom_left_placement(&ifp_shrunk, &nfp_refs, sample_step) {
198 let geom_aabb = geom.aabb_at_rotation_mirrored(rotation_angle, mirror);
202 let boundary_aabb = self.boundary.aabb();
203
204 if let Some((clamped_x, clamped_y)) =
205 clamp_placement_to_boundary(x, y, geom_aabb, boundary_aabb)
206 {
207 let was_clamped = (clamped_x - x).abs() > 1e-6 || (clamped_y - y).abs() > 1e-6;
210 if was_clamped {
211 if !verify_no_overlap_mirrored(
213 geom,
214 (clamped_x, clamped_y),
215 rotation_angle,
216 mirror,
217 &placed_geometries,
218 ) {
219 continue; }
221 }
222
223 let placement = Placement::new_2d(
224 geom.id().clone(),
225 info.instance_num,
226 clamped_x,
227 clamped_y,
228 rotation_angle,
229 )
230 .with_mirrored(mirror);
231
232 placements.push(placement);
233 placed_geometries.push(
234 PlacedGeometry::new(geom.clone(), (clamped_x, clamped_y), rotation_angle)
235 .with_mirrored(mirror),
236 );
237 total_placed_area += geom.measure();
238 placed_count += 1;
239 }
240 }
241 }
242
243 let utilization = total_placed_area / self.boundary.measure();
244 (placements, utilization, placed_count)
245 }
246
247 fn get_boundary_polygon_with_margin(&self, margin: f64) -> Vec<(f64, f64)> {
249 let (b_min, b_max) = self.boundary.aabb();
250 vec![
251 (b_min[0] + margin, b_min[1] + margin),
252 (b_max[0] - margin, b_min[1] + margin),
253 (b_max[0] - margin, b_max[1] - margin),
254 (b_min[0] + margin, b_max[1] - margin),
255 ]
256 }
257
258 fn compute_sample_step(&self) -> f64 {
260 if self.geometries.is_empty() {
261 return 1.0;
262 }
263
264 let mut min_dim = f64::INFINITY;
265 for geom in &self.geometries {
266 let (g_min, g_max) = geom.aabb();
267 let width = g_max[0] - g_min[0];
268 let height = g_max[1] - g_min[1];
269 min_dim = min_dim.min(width).min(height);
270 }
271
272 (min_dim / 4.0).clamp(0.5, 10.0)
273 }
274}
275
276impl SaProblem for SaNestingProblem {
277 type Solution = PermutationSolution;
278
279 fn initial_solution<R: rand::Rng>(&self, rng: &mut R) -> Self::Solution {
280 PermutationSolution::random(self.instances.len(), self.max_rotation_options, rng)
281 }
282
283 fn neighbor<R: rand::Rng>(
284 &self,
285 solution: &Self::Solution,
286 operator: NeighborhoodOperator,
287 rng: &mut R,
288 ) -> Self::Solution {
289 match operator {
290 NeighborhoodOperator::Swap => solution.apply_swap(rng),
291 NeighborhoodOperator::Relocate => solution.apply_relocate(rng),
292 NeighborhoodOperator::Inversion => solution.apply_inversion(rng),
293 NeighborhoodOperator::Rotation => solution.apply_rotation(rng),
294 NeighborhoodOperator::Chain => solution.apply_chain(rng),
295 NeighborhoodOperator::MirrorFlip => solution.apply_mirror_flip(rng),
296 }
297 }
298
299 fn evaluate(&self, solution: &mut Self::Solution) {
300 let (_, utilization, placed_count) = self.decode(solution);
301 let fitness = nesting_fitness(placed_count, self.instances.len(), utilization);
302 solution.set_objective(fitness);
303 }
304
305 fn available_operators(&self) -> Vec<NeighborhoodOperator> {
306 let mut ops = vec![
307 NeighborhoodOperator::Swap,
308 NeighborhoodOperator::Relocate,
309 NeighborhoodOperator::Inversion,
310 NeighborhoodOperator::Chain,
311 ];
312 if self.max_rotation_options > 1 {
313 ops.push(NeighborhoodOperator::Rotation);
314 }
315 if self.any_allow_flip {
316 ops.push(NeighborhoodOperator::MirrorFlip);
317 }
318 ops
319 }
320
321 fn on_temperature_change(
322 &self,
323 temperature: f64,
324 iteration: u64,
325 best: &Self::Solution,
326 _current: &Self::Solution,
327 ) {
328 log::debug!(
329 "SA Iteration {}: temp={:.4}, best_fitness={:.4}",
330 iteration,
331 temperature,
332 best.objective()
333 );
334 }
335}
336
337pub fn run_sa_nesting(
339 geometries: &[Geometry2D],
340 boundary: &Boundary2D,
341 config: &Config,
342 sa_config: SaConfig,
343 cancelled: Arc<AtomicBool>,
344) -> SolveResult<f64> {
345 let problem = SaNestingProblem::new(
346 geometries.to_vec(),
347 boundary.clone(),
348 config.clone(),
349 cancelled.clone(),
350 );
351
352 let runner = SaRunner::new(sa_config, problem);
353
354 #[cfg(not(target_arch = "wasm32"))]
356 {
357 let cancel_handle = runner.cancel_handle();
358 let cancelled_clone = cancelled.clone();
359 std::thread::spawn(move || {
360 while !cancelled_clone.load(Ordering::Relaxed) {
361 std::thread::sleep(std::time::Duration::from_millis(100));
362 }
363 cancel_handle.store(true, Ordering::Relaxed);
364 });
365 }
366
367 let sa_result = match config.seed {
370 Some(seed) => {
371 use rand::SeedableRng;
372 runner.run_with_rng(&mut rand::rngs::StdRng::seed_from_u64(seed))
373 }
374 None => runner.run(),
375 };
376
377 let problem = SaNestingProblem::new(
379 geometries.to_vec(),
380 boundary.clone(),
381 config.clone(),
382 Arc::new(AtomicBool::new(false)),
383 );
384
385 let (placements, utilization, _placed_count) = problem.decode(&sa_result.best);
386
387 let mut unplaced = Vec::new();
389 let mut placed_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
390 for p in &placements {
391 placed_ids.insert(p.geometry_id.clone());
392 }
393 for geom in geometries {
394 if !placed_ids.contains(geom.id()) {
395 unplaced.push(geom.id().clone());
396 }
397 }
398
399 let mut result = SolveResult::new();
400 result.placements = placements;
401 result.unplaced = unplaced;
402 result.boundaries_used = 1;
403 result.utilization = utilization;
404 result.computation_time_ms = sa_result.elapsed.as_millis() as u64;
405 result.iterations = Some(sa_result.iterations);
406 result.best_fitness = Some(sa_result.best.objective());
407 result.fitness_history = Some(sa_result.history);
408 result.strategy = Some("SimulatedAnnealing".to_string());
409 result.cancelled = cancelled.load(Ordering::Relaxed);
410 result.target_reached = sa_result.target_reached;
411
412 result
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
420 fn test_sa_nesting_basic() {
421 let geometries = vec![
422 Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
423 Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
424 ];
425
426 let boundary = Boundary2D::rectangle(100.0, 50.0);
427 let config = Config::default();
428 let sa_config = SaConfig::default()
429 .with_initial_temp(100.0)
430 .with_final_temp(0.1)
431 .with_cooling_rate(0.9)
432 .with_iterations_per_temp(20)
433 .with_max_iterations(500);
434
435 let result = run_sa_nesting(
436 &geometries,
437 &boundary,
438 &config,
439 sa_config,
440 Arc::new(AtomicBool::new(false)),
441 );
442
443 assert!(result.utilization > 0.0);
444 assert!(!result.placements.is_empty());
445 assert_eq!(result.strategy, Some("SimulatedAnnealing".to_string()));
446 }
447
448 #[test]
449 fn test_sa_nesting_all_placed() {
450 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
451
452 let boundary = Boundary2D::rectangle(100.0, 100.0);
453 let config = Config::default();
454 let sa_config = SaConfig::default()
455 .with_initial_temp(100.0)
456 .with_final_temp(0.1)
457 .with_max_iterations(1000);
458
459 let result = run_sa_nesting(
460 &geometries,
461 &boundary,
462 &config,
463 sa_config,
464 Arc::new(AtomicBool::new(false)),
465 );
466
467 assert_eq!(result.placements.len(), 4);
469 assert!(result.unplaced.is_empty());
470 }
471
472 #[test]
473 fn test_sa_nesting_with_rotation() {
474 let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
475 .with_quantity(3)
476 .with_rotations(vec![0.0, 90.0])];
477
478 let boundary = Boundary2D::rectangle(50.0, 50.0);
479 let config = Config::default();
480 let sa_config = SaConfig::default()
481 .with_initial_temp(100.0)
482 .with_final_temp(0.1)
483 .with_max_iterations(500);
484
485 let result = run_sa_nesting(
486 &geometries,
487 &boundary,
488 &config,
489 sa_config,
490 Arc::new(AtomicBool::new(false)),
491 );
492
493 assert!(result.utilization > 0.0);
494 assert!(!result.placements.is_empty());
495 }
496
497 #[test]
498 fn test_sa_problem_decode() {
499 let geometries = vec![Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2)];
500
501 let boundary = Boundary2D::rectangle(100.0, 50.0);
502 let config = Config::default();
503 let cancelled = Arc::new(AtomicBool::new(false));
504
505 let problem = SaNestingProblem::new(geometries, boundary, config, cancelled);
506
507 assert_eq!(problem.num_instances(), 2);
508
509 let mut rng = rand::rng();
511 let solution = PermutationSolution::random(problem.num_instances(), 1, &mut rng);
512 let (placements, utilization, placed_count) = problem.decode(&solution);
513
514 assert!(placed_count >= 1);
516 assert_eq!(placements.len(), placed_count);
517 if placed_count > 0 {
518 assert!(utilization > 0.0);
519 }
520 }
521
522 #[test]
523 fn test_permutation_solution_mirrors_gene_present() {
524 let mut rng = rand::rng();
525 let solution = PermutationSolution::random(10, 4, &mut rng);
526 assert_eq!(solution.mirrors.len(), 10);
527
528 let fixed = PermutationSolution::new(10, 4);
529 assert_eq!(fixed.mirrors, vec![false; 10]);
530 }
531
532 #[test]
533 fn test_apply_mirror_flip_flips_bit() {
534 let mut rng = rand::rng();
535 let mut single = PermutationSolution::new(1, 1);
536 assert!(!single.mirrors[0]);
537
538 single = single.apply_mirror_flip(&mut rng);
539 assert!(single.mirrors[0]);
540 single = single.apply_mirror_flip(&mut rng);
541 assert!(!single.mirrors[0]);
542 }
543
544 #[test]
545 fn test_available_operators_includes_mirror_flip_only_when_allowed() {
546 let boundary = Boundary2D::rectangle(65.0, 45.0);
547 let cancelled = Arc::new(AtomicBool::new(false));
548
549 let plain = vec![Geometry2D::rectangle("R", 10.0, 10.0).with_quantity(1)];
550 let problem = SaNestingProblem::new(
551 plain,
552 boundary.clone(),
553 Config::default(),
554 cancelled.clone(),
555 );
556 assert!(!problem
557 .available_operators()
558 .contains(&NeighborhoodOperator::MirrorFlip));
559
560 let flippable = vec![Geometry2D::rectangle("R", 10.0, 10.0)
561 .with_flip(true)
562 .with_quantity(1)];
563 let problem = SaNestingProblem::new(flippable, boundary, Config::default(), cancelled);
564 assert!(problem
565 .available_operators()
566 .contains(&NeighborhoodOperator::MirrorFlip));
567 }
568
569 fn chiral_l(id: &str) -> Geometry2D {
572 Geometry2D::l_shape(id, 30.0, 20.0, 20.0, 10.0)
573 }
574
575 fn polygons_overlap(a: &[(f64, f64)], b: &[(f64, f64)]) -> bool {
576 for i in 0..a.len() {
577 let (a1, a2) = (a[i], a[(i + 1) % a.len()]);
578 for j in 0..b.len() {
579 let (b1, b2) = (b[j], b[(j + 1) % b.len()]);
580 if crate::polygon_ops::segments_intersect(a1, a2, b1, b2) {
581 return true;
582 }
583 }
584 }
585 false
586 }
587
588 #[test]
595 fn test_sa_decode_mirror_no_overlap() {
596 let geometries = vec![chiral_l("L").with_flip(true).with_quantity(2)];
597 let boundary = Boundary2D::rectangle(65.0, 45.0);
598 let config = Config::default().with_spacing(1.0);
599 let problem = SaNestingProblem::new(
600 geometries.clone(),
601 boundary,
602 config,
603 Arc::new(AtomicBool::new(false)),
604 );
605
606 let mut solution = PermutationSolution::new(2, 1);
607 solution.mirrors = vec![false, true];
608
609 let (placements, utilization, placed_count) = problem.decode(&solution);
610
611 assert_eq!(
612 placed_count, 2,
613 "both instances should fit in this boundary"
614 );
615 assert_eq!(placements.len(), 2);
616 assert!(utilization > 0.0);
617 assert!(!placements[0].mirrored);
618 assert!(
619 placements[1].mirrored,
620 "instance 1's mirror gene was true and allow_flip is set — decode() must honor it"
621 );
622
623 let poly0 = PlacedGeometry::new(
624 geometries[0].clone(),
625 (placements[0].x(), placements[0].y()),
626 placements[0].angle(),
627 )
628 .with_mirrored(placements[0].mirrored)
629 .translated_exterior();
630 let poly1 = PlacedGeometry::new(
631 geometries[0].clone(),
632 (placements[1].x(), placements[1].y()),
633 placements[1].angle(),
634 )
635 .with_mirrored(placements[1].mirrored)
636 .translated_exterior();
637 assert!(
638 !polygons_overlap(&poly0, &poly1),
639 "unmirrored instance 0 and mirrored instance 1 must not overlap"
640 );
641 }
642
643 #[test]
644 fn test_sa_decode_mirror_ignored_without_allow_flip() {
645 let geometries = vec![chiral_l("L").with_quantity(1)];
646 let boundary = Boundary2D::rectangle(65.0, 45.0);
647 let problem = SaNestingProblem::new(
648 geometries,
649 boundary,
650 Config::default(),
651 Arc::new(AtomicBool::new(false)),
652 );
653
654 let mut solution = PermutationSolution::new(1, 1);
655 solution.mirrors = vec![true];
656
657 let (placements, _utilization, placed_count) = problem.decode(&solution);
658 assert_eq!(placed_count, 1);
659 assert!(
660 !placements[0].mirrored,
661 "allow_flip=false must suppress the mirror gene"
662 );
663 }
664}