u_nesting_core/
result.rs

1//! Solve result representation.
2
3use crate::geometry::GeometryId;
4use crate::placement::{Placement, PlacementStats};
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9/// Result of a nesting or packing solve operation.
10#[derive(Debug, Clone)]
11#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12pub struct SolveResult<S> {
13    /// List of placements for all successfully placed geometry instances.
14    pub placements: Vec<Placement<S>>,
15
16    /// Number of boundaries (bins) used.
17    pub boundaries_used: usize,
18
19    /// Utilization ratio (0.0 - 1.0).
20    /// Calculated as: total_geometry_measure / total_boundary_measure
21    pub utilization: f64,
22
23    /// IDs of geometries that could not be placed.
24    pub unplaced: Vec<GeometryId>,
25
26    /// Computation time in milliseconds.
27    pub computation_time_ms: u64,
28
29    /// Number of generations (for GA-based solvers).
30    pub generations: Option<u32>,
31
32    /// Number of iterations (for SA-based solvers).
33    pub iterations: Option<u64>,
34
35    /// Best fitness value achieved (for GA/SA-based solvers).
36    pub best_fitness: Option<f64>,
37
38    /// Fitness history over generations (for analysis).
39    pub fitness_history: Option<Vec<f64>>,
40
41    /// Strategy used for solving.
42    pub strategy: Option<String>,
43
44    /// Whether the solve was cancelled early.
45    pub cancelled: bool,
46
47    /// Whether the target utilization was reached.
48    pub target_reached: bool,
49}
50
51impl<S> SolveResult<S> {
52    /// Creates a new empty result.
53    pub fn new() -> Self {
54        Self {
55            placements: Vec::new(),
56            boundaries_used: 0,
57            utilization: 0.0,
58            unplaced: Vec::new(),
59            computation_time_ms: 0,
60            generations: None,
61            iterations: None,
62            best_fitness: None,
63            fitness_history: None,
64            strategy: None,
65            cancelled: false,
66            target_reached: false,
67        }
68    }
69
70    /// Returns true if all geometries were placed.
71    pub fn all_placed(&self) -> bool {
72        self.unplaced.is_empty()
73    }
74
75    /// Returns the number of placed geometry instances.
76    pub fn placed_count(&self) -> usize {
77        self.placements.len()
78    }
79
80    /// Returns the number of unplaced geometry types.
81    pub fn unplaced_count(&self) -> usize {
82        self.unplaced.len()
83    }
84
85    /// Returns true if the solve was successful (at least one placement).
86    pub fn is_successful(&self) -> bool {
87        !self.placements.is_empty()
88    }
89
90    /// Returns true if the solve completed within the time limit.
91    pub fn completed_normally(&self) -> bool {
92        !self.cancelled
93    }
94
95    /// Sets the strategy name.
96    pub fn with_strategy(mut self, strategy: impl Into<String>) -> Self {
97        self.strategy = Some(strategy.into());
98        self
99    }
100
101    /// Sets the generations count.
102    pub fn with_generations(mut self, generations: u32) -> Self {
103        self.generations = Some(generations);
104        self
105    }
106
107    /// Sets the best fitness.
108    pub fn with_best_fitness(mut self, fitness: f64) -> Self {
109        self.best_fitness = Some(fitness);
110        self
111    }
112
113    /// Sets the fitness history.
114    pub fn with_fitness_history(mut self, history: Vec<f64>) -> Self {
115        self.fitness_history = Some(history);
116        self
117    }
118
119    /// Removes duplicate entries from the unplaced list.
120    /// This is useful when multiple instances of the same geometry failed to place.
121    pub fn deduplicate_unplaced(&mut self) {
122        let mut seen = std::collections::HashSet::new();
123        self.unplaced.retain(|id| seen.insert(id.clone()));
124    }
125
126    /// Computes placement statistics.
127    pub fn placement_stats(&self) -> PlacementStats {
128        PlacementStats::from_placements(&self.placements)
129    }
130
131    /// Returns utilization as a percentage string.
132    pub fn utilization_percent(&self) -> String {
133        format!("{:.1}%", self.utilization * 100.0)
134    }
135
136    /// Merges placements from another result (for multi-bin scenarios).
137    pub fn merge(&mut self, other: SolveResult<S>, boundary_offset: usize) {
138        // Offset boundary indices
139        for mut placement in other.placements {
140            placement.boundary_index += boundary_offset;
141            self.placements.push(placement);
142        }
143
144        self.boundaries_used = self
145            .boundaries_used
146            .max(other.boundaries_used + boundary_offset);
147        self.unplaced.extend(other.unplaced);
148        self.computation_time_ms += other.computation_time_ms;
149
150        // Recalculate utilization would require knowing the total measures
151    }
152}
153
154impl<S> Default for SolveResult<S> {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160/// Summary statistics for a solve result.
161#[derive(Debug, Clone)]
162#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
163pub struct SolveSummary {
164    /// Total geometries requested.
165    pub total_requested: usize,
166    /// Total geometries placed.
167    pub total_placed: usize,
168    /// Utilization percentage.
169    pub utilization_percent: f64,
170    /// Number of bins/boundaries used.
171    pub bins_used: usize,
172    /// Computation time in milliseconds.
173    pub time_ms: u64,
174    /// Strategy used.
175    pub strategy: String,
176}
177
178impl<S> From<&SolveResult<S>> for SolveSummary {
179    fn from(result: &SolveResult<S>) -> Self {
180        Self {
181            total_requested: result.placements.len() + result.unplaced.len(),
182            total_placed: result.placements.len(),
183            utilization_percent: result.utilization * 100.0,
184            bins_used: result.boundaries_used,
185            time_ms: result.computation_time_ms,
186            strategy: result
187                .strategy
188                .clone()
189                .unwrap_or_else(|| "unknown".to_string()),
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn test_result_new() {
200        let result: SolveResult<f64> = SolveResult::new();
201        assert!(result.placements.is_empty());
202        assert_eq!(result.utilization, 0.0);
203        assert!(result.all_placed());
204    }
205
206    #[test]
207    fn test_result_with_placements() {
208        let mut result: SolveResult<f64> = SolveResult::new();
209        result
210            .placements
211            .push(Placement::new_2d("test".to_string(), 0, 0.0, 0.0, 0.0));
212        result.utilization = 0.85;
213
214        assert_eq!(result.placed_count(), 1);
215        assert!(result.is_successful());
216        assert_eq!(result.utilization_percent(), "85.0%");
217    }
218
219    #[test]
220    fn test_result_with_unplaced() {
221        let mut result: SolveResult<f64> = SolveResult::new();
222        result.unplaced.push("G1".to_string());
223        result.unplaced.push("G2".to_string());
224
225        assert!(!result.all_placed());
226        assert_eq!(result.unplaced_count(), 2);
227    }
228
229    #[test]
230    fn test_solve_summary() {
231        let mut result: SolveResult<f64> = SolveResult::new();
232        result
233            .placements
234            .push(Placement::new_2d("test".to_string(), 0, 0.0, 0.0, 0.0));
235        result.utilization = 0.75;
236        result.boundaries_used = 1;
237        result.computation_time_ms = 100;
238        result.strategy = Some("GA".to_string());
239
240        let summary = SolveSummary::from(&result);
241        assert_eq!(summary.total_placed, 1);
242        assert_eq!(summary.utilization_percent, 75.0);
243        assert_eq!(summary.strategy, "GA");
244    }
245
246    #[test]
247    fn test_deduplicate_unplaced() {
248        let mut result: SolveResult<f64> = SolveResult::new();
249        // Simulate multiple instances of same geometry failing to place
250        result.unplaced.push("G1".to_string());
251        result.unplaced.push("G1".to_string());
252        result.unplaced.push("G2".to_string());
253        result.unplaced.push("G1".to_string());
254        result.unplaced.push("G2".to_string());
255
256        assert_eq!(result.unplaced.len(), 5);
257
258        result.deduplicate_unplaced();
259
260        assert_eq!(result.unplaced.len(), 2);
261        assert!(result.unplaced.contains(&"G1".to_string()));
262        assert!(result.unplaced.contains(&"G2".to_string()));
263    }
264}