1use crate::geometry::GeometryId;
4use crate::placement::{Placement, PlacementStats};
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone)]
11#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12pub struct SolveResult<S> {
13 pub placements: Vec<Placement<S>>,
15
16 pub boundaries_used: usize,
18
19 pub utilization: f64,
22
23 pub unplaced: Vec<GeometryId>,
25
26 pub computation_time_ms: u64,
28
29 pub generations: Option<u32>,
31
32 pub iterations: Option<u64>,
34
35 pub best_fitness: Option<f64>,
37
38 pub fitness_history: Option<Vec<f64>>,
40
41 pub strategy: Option<String>,
43
44 pub cancelled: bool,
46
47 pub target_reached: bool,
49}
50
51impl<S> SolveResult<S> {
52 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 pub fn all_placed(&self) -> bool {
72 self.unplaced.is_empty()
73 }
74
75 pub fn placed_count(&self) -> usize {
77 self.placements.len()
78 }
79
80 pub fn unplaced_count(&self) -> usize {
82 self.unplaced.len()
83 }
84
85 pub fn is_successful(&self) -> bool {
87 !self.placements.is_empty()
88 }
89
90 pub fn completed_normally(&self) -> bool {
92 !self.cancelled
93 }
94
95 pub fn with_strategy(mut self, strategy: impl Into<String>) -> Self {
97 self.strategy = Some(strategy.into());
98 self
99 }
100
101 pub fn with_generations(mut self, generations: u32) -> Self {
103 self.generations = Some(generations);
104 self
105 }
106
107 pub fn with_best_fitness(mut self, fitness: f64) -> Self {
109 self.best_fitness = Some(fitness);
110 self
111 }
112
113 pub fn with_fitness_history(mut self, history: Vec<f64>) -> Self {
115 self.fitness_history = Some(history);
116 self
117 }
118
119 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 pub fn placement_stats(&self) -> PlacementStats {
128 PlacementStats::from_placements(&self.placements)
129 }
130
131 pub fn utilization_percent(&self) -> String {
133 format!("{:.1}%", self.utilization * 100.0)
134 }
135
136 pub fn merge(&mut self, other: SolveResult<S>, boundary_offset: usize) {
138 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 }
152}
153
154impl<S> Default for SolveResult<S> {
155 fn default() -> Self {
156 Self::new()
157 }
158}
159
160#[derive(Debug, Clone)]
162#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
163pub struct SolveSummary {
164 pub total_requested: usize,
166 pub total_placed: usize,
168 pub utilization_percent: f64,
170 pub bins_used: usize,
172 pub time_ms: u64,
174 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 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}