Skip to main content

u_nesting_d2/
placement_utils.rs

1//! Shared utility functions for 2D nesting placement algorithms.
2//!
3//! This module consolidates common geometry operations used across
4//! multiple nesting strategy implementations (GA, SA, BRKGA, ALNS, GDRR).
5
6use crate::nfp::Nfp;
7
8/// Instance information for decoding placement orders.
9///
10/// Maps a flat instance index to the corresponding geometry and its
11/// repetition number (when `quantity > 1`).
12#[derive(Debug, Clone)]
13pub struct InstanceInfo {
14    /// Index into the geometries array.
15    pub geometry_idx: usize,
16    /// Instance number within this geometry's quantity.
17    pub instance_num: usize,
18}
19
20/// Computes the centroid (arithmetic mean of vertices) of a polygon.
21///
22/// # Returns
23///
24/// `(0.0, 0.0)` for an empty polygon, otherwise the mean of all vertices.
25pub fn polygon_centroid(polygon: &[(f64, f64)]) -> (f64, f64) {
26    if polygon.is_empty() {
27        return (0.0, 0.0);
28    }
29
30    let sum: (f64, f64) = polygon
31        .iter()
32        .fold((0.0, 0.0), |acc, &(x, y)| (acc.0 + x, acc.1 + y));
33    let n = polygon.len() as f64;
34    (sum.0 / n, sum.1 / n)
35}
36
37/// Expands an NFP outward by the given spacing amount.
38///
39/// Each vertex of each polygon is moved away from its polygon's centroid
40/// by `spacing` units. This approximates a Minkowski sum with a circle
41/// of radius `spacing`.
42///
43/// Returns the original NFP unchanged if `spacing <= 0.0`.
44pub fn expand_nfp(nfp: &Nfp, spacing: f64) -> Nfp {
45    if spacing <= 0.0 {
46        return nfp.clone();
47    }
48
49    let expanded_polygons: Vec<Vec<(f64, f64)>> = nfp
50        .polygons
51        .iter()
52        .map(|polygon| {
53            let (cx, cy) = polygon_centroid(polygon);
54            polygon
55                .iter()
56                .map(|&(x, y)| {
57                    let dx = x - cx;
58                    let dy = y - cy;
59                    let dist = (dx * dx + dy * dy).sqrt();
60                    if dist > 1e-10 {
61                        let scale = (dist + spacing) / dist;
62                        (cx + dx * scale, cy + dy * scale)
63                    } else {
64                        (x, y)
65                    }
66                })
67                .collect()
68        })
69        .collect();
70
71    Nfp::from_polygons(expanded_polygons)
72}
73
74/// Shrinks an IFP (Inner-Fit Polygon) inward by the given spacing amount.
75///
76/// Each vertex of each polygon is moved toward its polygon's centroid
77/// by `spacing` units. Polygons that collapse to fewer than 3 vertices
78/// are discarded.
79///
80/// Returns the original IFP unchanged if `spacing <= 0.0`.
81pub fn shrink_ifp(ifp: &Nfp, spacing: f64) -> Nfp {
82    if spacing <= 0.0 {
83        return ifp.clone();
84    }
85
86    let shrunk_polygons: Vec<Vec<(f64, f64)>> = ifp
87        .polygons
88        .iter()
89        .filter_map(|polygon| {
90            let (cx, cy) = polygon_centroid(polygon);
91            let shrunk: Vec<(f64, f64)> = polygon
92                .iter()
93                .map(|&(x, y)| {
94                    let dx = x - cx;
95                    let dy = y - cy;
96                    let dist = (dx * dx + dy * dy).sqrt();
97                    if dist > spacing + 1e-10 {
98                        let scale = (dist - spacing) / dist;
99                        (cx + dx * scale, cy + dy * scale)
100                    } else {
101                        (cx, cy)
102                    }
103                })
104                .collect();
105
106            if shrunk.len() >= 3 {
107                Some(shrunk)
108            } else {
109                None
110            }
111        })
112        .collect();
113
114    Nfp::from_polygons(shrunk_polygons)
115}
116
117/// Computes the nesting fitness score from placement results.
118///
119/// The fitness function prioritizes placement count (weight 100) over
120/// utilization (weight 10), ensuring all-placed solutions always rank
121/// higher than partial solutions.
122///
123/// # Arguments
124///
125/// * `placed_count` - Number of successfully placed instances
126/// * `total_count` - Total number of instances to place
127/// * `utilization` - Fraction of boundary area used (0.0..1.0)
128///
129/// # Returns
130///
131/// Fitness score in range [0, 110] where higher is better.
132pub fn nesting_fitness(placed_count: usize, total_count: usize, utilization: f64) -> f64 {
133    let placement_ratio = placed_count as f64 / total_count.max(1) as f64;
134    placement_ratio * 100.0 + utilization * 10.0
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_polygon_centroid_empty() {
143        assert_eq!(polygon_centroid(&[]), (0.0, 0.0));
144    }
145
146    #[test]
147    fn test_polygon_centroid_square() {
148        let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
149        let (cx, cy) = polygon_centroid(&square);
150        assert!((cx - 5.0).abs() < 1e-10);
151        assert!((cy - 5.0).abs() < 1e-10);
152    }
153
154    #[test]
155    fn test_expand_nfp_zero_spacing() {
156        let nfp = Nfp::from_polygons(vec![vec![
157            (0.0, 0.0),
158            (10.0, 0.0),
159            (10.0, 10.0),
160            (0.0, 10.0),
161        ]]);
162        let expanded = expand_nfp(&nfp, 0.0);
163        assert_eq!(expanded.polygons.len(), nfp.polygons.len());
164        assert_eq!(expanded.polygons[0], nfp.polygons[0]);
165    }
166
167    #[test]
168    fn test_expand_nfp_positive_spacing() {
169        let nfp = Nfp::from_polygons(vec![vec![
170            (0.0, 0.0),
171            (10.0, 0.0),
172            (10.0, 10.0),
173            (0.0, 10.0),
174        ]]);
175        let expanded = expand_nfp(&nfp, 1.0);
176        // All vertices should move outward from centroid (5,5)
177        for &(x, y) in &expanded.polygons[0] {
178            let dx = x - 5.0;
179            let dy = y - 5.0;
180            let dist = (dx * dx + dy * dy).sqrt();
181            // Original distance was ~7.07, expanded should be ~8.07
182            assert!(dist > 7.0);
183        }
184    }
185
186    #[test]
187    fn test_shrink_ifp_zero_spacing() {
188        let ifp = Nfp::from_polygons(vec![vec![
189            (0.0, 0.0),
190            (10.0, 0.0),
191            (10.0, 10.0),
192            (0.0, 10.0),
193        ]]);
194        let shrunk = shrink_ifp(&ifp, 0.0);
195        assert_eq!(shrunk.polygons.len(), ifp.polygons.len());
196        assert_eq!(shrunk.polygons[0], ifp.polygons[0]);
197    }
198
199    #[test]
200    fn test_shrink_ifp_positive_spacing() {
201        let ifp = Nfp::from_polygons(vec![vec![
202            (0.0, 0.0),
203            (10.0, 0.0),
204            (10.0, 10.0),
205            (0.0, 10.0),
206        ]]);
207        let shrunk = shrink_ifp(&ifp, 1.0);
208        // All vertices should move inward toward centroid (5,5)
209        for &(x, y) in &shrunk.polygons[0] {
210            let dx = x - 5.0;
211            let dy = y - 5.0;
212            let dist = (dx * dx + dy * dy).sqrt();
213            // Original distance was ~7.07, shrunk should be ~6.07
214            assert!(dist < 7.0);
215        }
216    }
217
218    #[test]
219    fn test_shrink_ifp_collapse() {
220        // Very small polygon that collapses with spacing
221        let ifp = Nfp::from_polygons(vec![vec![(0.0, 0.0), (1.0, 0.0), (0.5, 0.5)]]);
222        let shrunk = shrink_ifp(&ifp, 10.0);
223        // Should collapse to empty (all vertices become centroid, < 3 unique)
224        // The shrunk polygon may still have 3 points (all at centroid)
225        // but the logic preserves polygons with >= 3 vertices
226        assert!(shrunk.polygons.is_empty() || shrunk.polygons[0].len() >= 3);
227    }
228
229    #[test]
230    fn test_nesting_fitness_all_placed() {
231        let f = nesting_fitness(10, 10, 0.85);
232        assert!((f - 108.5).abs() < 1e-10);
233    }
234
235    #[test]
236    fn test_nesting_fitness_partial() {
237        let f = nesting_fitness(5, 10, 0.40);
238        // 0.5 * 100 + 0.4 * 10 = 54.0
239        assert!((f - 54.0).abs() < 1e-10);
240    }
241
242    #[test]
243    fn test_nesting_fitness_none_placed() {
244        let f = nesting_fitness(0, 10, 0.0);
245        assert!((f - 0.0).abs() < 1e-10);
246    }
247
248    #[test]
249    fn test_nesting_fitness_empty_total() {
250        let f = nesting_fitness(0, 0, 0.0);
251        assert!((f - 0.0).abs() < 1e-10);
252    }
253}