u_nesting_core/solver.rs
1//! Solver traits and configuration.
2
3use crate::geometry::{Boundary, Geometry};
4use crate::result::SolveResult;
5use crate::Result;
6
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9
10/// Optimization strategy.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13pub enum Strategy {
14 /// Bottom-Left Fill (fast, lower quality).
15 #[default]
16 BottomLeftFill,
17 /// NFP-guided placement (balanced).
18 NfpGuided,
19 /// Genetic Algorithm (slower, higher quality).
20 GeneticAlgorithm,
21 /// Biased Random-Key Genetic Algorithm (balanced, robust).
22 Brkga,
23 /// Simulated Annealing.
24 SimulatedAnnealing,
25 /// Extreme Point heuristic (3D only).
26 ExtremePoint,
27 /// Goal-Driven Ruin and Recreate (GDRR).
28 Gdrr,
29 /// Adaptive Large Neighborhood Search (ALNS).
30 Alns,
31 /// MILP-based exact solver (small instances, optimal solution).
32 MilpExact,
33 /// Hybrid: Try exact first, fallback to heuristic on timeout.
34 HybridExact,
35}
36
37impl Strategy {
38 /// The short name of each strategy -- what a refusal of an unknown name
39 /// lists as expected. [`Strategy::parse`] reads these and the longer
40 /// forms in [`Strategy::SPELLINGS`].
41 /// Whether this strategy reads `setting` -- one of the settings only some
42 /// strategies use. A request that names one its strategy does not read is
43 /// refused rather than run without it (see `ConfigRequest::to_config`).
44 /// Settings every strategy reads are not listed and always read.
45 ///
46 /// - `crossover_rate`, `mutation_rate`: the genetic algorithm.
47 /// - `population_size`, `max_generations`: the two population searches.
48 /// - `seed`: the stochastic searches (and the hybrid's ALNS fallback).
49 /// - `target_utilization`: the searches that stop on a target (GA, BRKGA,
50 /// SA).
51 pub fn reads_setting(self, setting: &str) -> bool {
52 use Strategy::*;
53 match setting {
54 "crossover_rate" | "mutation_rate" => matches!(self, GeneticAlgorithm),
55 "population_size" | "max_generations" => matches!(self, GeneticAlgorithm | Brkga),
56 "seed" => matches!(
57 self,
58 GeneticAlgorithm | Brkga | SimulatedAnnealing | Gdrr | Alns | HybridExact
59 ),
60 "target_utilization" => matches!(self, GeneticAlgorithm | Brkga | SimulatedAnnealing),
61 _ => true,
62 }
63 }
64
65 /// The names of the strategies that read `setting` (see
66 /// [`Self::reads_setting`]).
67 pub fn readers_of(setting: &str) -> Vec<&'static str> {
68 Self::NAMES
69 .iter()
70 .copied()
71 .filter(|n| Self::parse(n).is_some_and(|s| s.reads_setting(setting)))
72 .collect()
73 }
74
75 pub const NAMES: [&'static str; 10] = [
76 "blf",
77 "nfp",
78 "ga",
79 "brkga",
80 "sa",
81 "ep",
82 "gdrr",
83 "alns",
84 "milpexact",
85 "hybridexact",
86 ];
87
88 /// Every spelling [`Strategy::parse`] reads, with the strategy it names:
89 /// the [`Strategy::NAMES`] and their longer forms.
90 pub const SPELLINGS: [(&'static str, Self); 18] = [
91 ("blf", Self::BottomLeftFill),
92 ("bottomleftfill", Self::BottomLeftFill),
93 ("nfp", Self::NfpGuided),
94 ("nfpguided", Self::NfpGuided),
95 ("ga", Self::GeneticAlgorithm),
96 ("genetic", Self::GeneticAlgorithm),
97 ("geneticalgorithm", Self::GeneticAlgorithm),
98 ("brkga", Self::Brkga),
99 ("sa", Self::SimulatedAnnealing),
100 ("simulatedannealing", Self::SimulatedAnnealing),
101 ("ep", Self::ExtremePoint),
102 ("extremepoint", Self::ExtremePoint),
103 ("gdrr", Self::Gdrr),
104 ("alns", Self::Alns),
105 ("milpexact", Self::MilpExact),
106 ("exact", Self::MilpExact),
107 ("hybridexact", Self::HybridExact),
108 ("hybrid", Self::HybridExact),
109 ];
110
111 /// Parses a strategy name (case-insensitive, whitespace-trimmed).
112 ///
113 /// Returns `None` for unrecognized names so callers surface an explicit
114 /// error instead of silently falling back to a default strategy (which
115 /// hides typos from consumers). This is the single source of truth for
116 /// strategy-name parsing across all bindings (C FFI, Python, WASM).
117 pub fn parse(name: &str) -> Option<Self> {
118 let name = name.trim().to_lowercase();
119 Self::SPELLINGS
120 .iter()
121 .find(|(spelling, _)| *spelling == name)
122 .map(|&(_, strategy)| strategy)
123 }
124}
125
126/// Common configuration for solvers.
127///
128/// It holds no thread count: a build with the `parallel` feature runs its
129/// parallel work on the rayon pool the solve is called from, so
130/// `rayon::ThreadPool::install` around the solve (or `RAYON_NUM_THREADS`)
131/// bounds it; without the feature a solve runs on the calling thread.
132#[derive(Debug, Clone)]
133#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
134pub struct Config {
135 /// Optimization strategy.
136 pub strategy: Strategy,
137
138 /// Minimum distance between any two placed geometries. It does not apply
139 /// between a geometry and the boundary — that is `margin`.
140 pub spacing: f64,
141
142 /// Minimum distance between any placed geometry and the boundary edge.
143 pub margin: f64,
144
145 /// Maximum computation time in milliseconds (0 = unlimited).
146 ///
147 /// The search strategies check it between individual placements, so a solve
148 /// returns within the limit plus the time of the greedy bottom-left pass it is
149 /// compared against. The layout returned is the best one found in that time.
150 pub time_limit_ms: u64,
151
152 /// Target utilization (0.0 - 1.0). Solver stops if reached.
153 pub target_utilization: Option<f64>,
154
155 // GA-specific parameters
156 /// Population size for the population-based strategies (GA, BRKGA).
157 ///
158 /// `None` runs each strategy with the size it is tuned for -- 2D GA and
159 /// BRKGA 30, 3D GA 100, 3D BRKGA 50; `Some(n)` runs it with `n`, at least
160 /// 2. The other strategies have no population and do not read it.
161 pub population_size: Option<usize>,
162
163 /// Generation budget for the population-based strategies (GA, BRKGA).
164 ///
165 /// `None` runs each strategy with the budget it is tuned for -- 2D GA and
166 /// BRKGA 50, 3D GA 500, 3D BRKGA 100; `Some(n)` allows `n` generations (0
167 /// evaluates the first population only). A run also ends on the time
168 /// limit, on cancellation and when the best layout stops improving.
169 pub max_generations: Option<u32>,
170
171 /// Crossover rate for GA (0.0 - 1.0).
172 pub crossover_rate: f64,
173
174 /// Mutation rate for GA (0.0 - 1.0).
175 pub mutation_rate: f64,
176
177 /// Elite count for GA.
178 pub elite_count: usize,
179
180 /// Optional RNG seed for reproducible runs of the stochastic strategies
181 /// (GA, BRKGA, SA, GDRR, ALNS -- 2D and 3D). `None` seeds from system
182 /// entropy (non-deterministic).
183 ///
184 /// A seeded run draws every random choice from the seed, so the same
185 /// request with the same seed returns the same layout **when the run ends
186 /// on its own budget** -- the generation or iteration count, convergence,
187 /// or the target utilization. A run cut short by the clock is not
188 /// reproduced: how far a search gets before `time_limit_ms` (or a
189 /// cancellation) stops it depends on the machine and its load. In 2D a
190 /// search keeps a time budget of its own even when `time_limit_ms` is 0
191 /// (15 s for GA and BRKGA, 10 s for SA, GDRR and ALNS), so a large 2D
192 /// request can reach it.
193 pub seed: Option<u64>,
194}
195
196impl Default for Config {
197 fn default() -> Self {
198 Self {
199 strategy: Strategy::default(),
200 spacing: 0.0,
201 margin: 0.0,
202 time_limit_ms: 30000,
203 target_utilization: None,
204 population_size: None,
205 max_generations: None,
206 crossover_rate: 0.85,
207 mutation_rate: 0.05,
208 elite_count: 5,
209 seed: None,
210 }
211 }
212}
213
214impl Config {
215 /// Creates a new configuration with default values.
216 pub fn new() -> Self {
217 Self::default()
218 }
219
220 /// Checks the fields a caller sets against their ranges -- the builders
221 /// store what they are given, and every solver calls this before it runs.
222 ///
223 /// # Errors
224 /// `ConfigError` naming the first field out of range: `spacing` or
225 /// `margin` negative or not finite, `crossover_rate`/`mutation_rate` or
226 /// `target_utilization` outside `[0, 1]`, a `population_size` set below 2.
227 pub fn validate(&self) -> Result<()> {
228 use crate::error::{check_at_least, check_range};
229 check_at_least("spacing", self.spacing, 0.0)?;
230 check_at_least("margin", self.margin, 0.0)?;
231 if let Some(size) = self.population_size {
232 Self::check_population_size(size as i128)?;
233 }
234 check_range("crossover_rate", self.crossover_rate, 0.0, 1.0)?;
235 check_range("mutation_rate", self.mutation_rate, 0.0, 1.0)?;
236 if let Some(util) = self.target_utilization {
237 check_range("target_utilization", util, 0.0, 1.0)?;
238 }
239 Ok(())
240 }
241
242 /// A population size: at least 2, the fewest a generation can cross. A
243 /// request's value and a configuration's own are judged here alike.
244 pub(crate) fn check_population_size(value: i128) -> Result<usize> {
245 crate::error::whole_at_least("population_size", value, 2)
246 }
247
248 /// Sets the optimization strategy.
249 pub fn with_strategy(mut self, strategy: Strategy) -> Self {
250 self.strategy = strategy;
251 self
252 }
253
254 /// Sets the spacing between geometries.
255 pub fn with_spacing(mut self, spacing: f64) -> Self {
256 self.spacing = spacing;
257 self
258 }
259
260 /// Sets the margin from boundary edges.
261 pub fn with_margin(mut self, margin: f64) -> Self {
262 self.margin = margin;
263 self
264 }
265
266 /// Sets the time limit in milliseconds.
267 pub fn with_time_limit(mut self, ms: u64) -> Self {
268 self.time_limit_ms = ms;
269 self
270 }
271
272 /// Sets the target utilization.
273 pub fn with_target_utilization(mut self, util: f64) -> Self {
274 self.target_utilization = Some(util);
275 self
276 }
277
278 /// Sets the RNG seed for reproducible stochastic runs (see [`Config::seed`]).
279 pub fn with_seed(mut self, seed: u64) -> Self {
280 self.seed = Some(seed);
281 self
282 }
283
284 /// Sets the population size of the population-based strategies, in place
285 /// of each strategy's own (see [`Config::population_size`]).
286 pub fn with_population_size(mut self, size: usize) -> Self {
287 self.population_size = Some(size);
288 self
289 }
290
291 /// Sets the generation budget of the population-based strategies, in
292 /// place of each strategy's own (see [`Config::max_generations`]).
293 pub fn with_max_generations(mut self, generations: u32) -> Self {
294 self.max_generations = Some(generations);
295 self
296 }
297}
298
299/// Progress callback for long-running operations.
300pub type ProgressCallback = Box<dyn Fn(ProgressInfo) + Send + Sync>;
301
302/// Progress information during solving.
303#[derive(Debug, Clone, Default)]
304pub struct ProgressInfo {
305 /// Current iteration/generation number.
306 pub iteration: u32,
307 /// Total expected iterations (0 if unknown).
308 pub total_iterations: u32,
309 /// Current best utilization (0.0 to 1.0).
310 pub utilization: f64,
311 /// Current best fitness value.
312 pub best_fitness: f64,
313 /// Number of items placed.
314 pub items_placed: usize,
315 /// Total number of items.
316 pub total_items: usize,
317 /// Elapsed time in milliseconds.
318 pub elapsed_ms: u64,
319 /// Current phase/stage description.
320 pub phase: String,
321 /// Whether the solver is still running.
322 pub running: bool,
323}
324
325impl ProgressInfo {
326 /// Creates a new progress info with default values.
327 pub fn new() -> Self {
328 Self {
329 running: true,
330 ..Default::default()
331 }
332 }
333
334 /// Sets the iteration info.
335 pub fn with_iteration(mut self, current: u32, total: u32) -> Self {
336 self.iteration = current;
337 self.total_iterations = total;
338 self
339 }
340
341 /// Sets the utilization.
342 pub fn with_utilization(mut self, utilization: f64) -> Self {
343 self.utilization = utilization;
344 self
345 }
346
347 /// Sets the best fitness.
348 pub fn with_fitness(mut self, fitness: f64) -> Self {
349 self.best_fitness = fitness;
350 self
351 }
352
353 /// Sets the items placed info.
354 pub fn with_items(mut self, placed: usize, total: usize) -> Self {
355 self.items_placed = placed;
356 self.total_items = total;
357 self
358 }
359
360 /// Sets the elapsed time.
361 pub fn with_elapsed(mut self, elapsed_ms: u64) -> Self {
362 self.elapsed_ms = elapsed_ms;
363 self
364 }
365
366 /// Sets the phase description.
367 pub fn with_phase(mut self, phase: impl Into<String>) -> Self {
368 self.phase = phase.into();
369 self
370 }
371
372 /// Marks the solver as finished.
373 pub fn finished(mut self) -> Self {
374 self.running = false;
375 self
376 }
377
378 /// Calculates the progress percentage (0.0 to 1.0).
379 pub fn progress_percent(&self) -> f64 {
380 if self.total_iterations > 0 {
381 self.iteration as f64 / self.total_iterations as f64
382 } else {
383 0.0
384 }
385 }
386}
387
388/// Trait for nesting/packing solvers.
389pub trait Solver {
390 /// The geometry type this solver handles.
391 type Geometry: Geometry;
392 /// The boundary type this solver handles.
393 type Boundary: Boundary;
394 /// The scalar type for coordinates.
395 type Scalar;
396
397 /// Solves the nesting/packing problem.
398 fn solve(
399 &self,
400 geometries: &[Self::Geometry],
401 boundary: &Self::Boundary,
402 ) -> Result<SolveResult<Self::Scalar>>;
403
404 /// Solves with a progress callback.
405 fn solve_with_progress(
406 &self,
407 geometries: &[Self::Geometry],
408 boundary: &Self::Boundary,
409 callback: ProgressCallback,
410 ) -> Result<SolveResult<Self::Scalar>>;
411
412 /// Cancels an ongoing solve operation.
413 fn cancel(&self);
414}
415
416#[cfg(test)]
417mod config_tests {
418 use super::Config;
419
420 #[test]
421 fn a_target_utilization_above_one_is_refused_not_clamped() {
422 let config = Config::default().with_target_utilization(1.5);
423 assert_eq!(config.target_utilization, Some(1.5), "stored as given");
424 let err = config.validate().expect_err("1.5 is not a utilization");
425 assert!(err.to_string().contains("target_utilization"), "{err}");
426 assert!(Config::default()
427 .with_target_utilization(0.9)
428 .validate()
429 .is_ok());
430 let nan_rate = Config {
431 mutation_rate: f64::NAN,
432 ..Config::default()
433 };
434 assert!(nan_rate.validate().is_err());
435 }
436
437 /// A budget left unset is the strategy's to choose; one that is set is
438 /// stored as given and judged by `validate`.
439 #[test]
440 fn a_population_is_stored_as_given_and_judged_by_validate() {
441 let unset = Config::default();
442 assert_eq!(unset.population_size, None);
443 assert_eq!(unset.max_generations, None);
444 assert!(unset.validate().is_ok());
445
446 let set = Config::default()
447 .with_population_size(100)
448 .with_max_generations(0);
449 assert_eq!(set.population_size, Some(100));
450 assert_eq!(set.max_generations, Some(0), "0 generations is a budget");
451 assert!(set.validate().is_ok());
452
453 let one = Config::default().with_population_size(1);
454 assert_eq!(one.population_size, Some(1), "stored as given");
455 let err = one.validate().expect_err("a population of 1 cannot cross");
456 assert_eq!(err.code(), "parameter_out_of_range");
457 assert_eq!(
458 err.to_string(),
459 "population_size must be a whole number >= 2, got 1"
460 );
461 }
462}