Skip to main content

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    /// Parses a strategy name (case-insensitive, whitespace-trimmed).
39    ///
40    /// Returns `None` for unrecognized names so callers surface an explicit
41    /// error instead of silently falling back to a default strategy (which
42    /// hides typos from consumers). This is the single source of truth for
43    /// strategy-name parsing across all bindings (C FFI, Python, WASM).
44    pub fn parse(name: &str) -> Option<Self> {
45        match name.trim().to_lowercase().as_str() {
46            "blf" | "bottomleftfill" => Some(Self::BottomLeftFill),
47            "nfp" | "nfpguided" => Some(Self::NfpGuided),
48            "ga" | "genetic" | "geneticalgorithm" => Some(Self::GeneticAlgorithm),
49            "brkga" => Some(Self::Brkga),
50            "sa" | "simulatedannealing" => Some(Self::SimulatedAnnealing),
51            "ep" | "extremepoint" => Some(Self::ExtremePoint),
52            "gdrr" => Some(Self::Gdrr),
53            "alns" => Some(Self::Alns),
54            "milpexact" | "exact" => Some(Self::MilpExact),
55            "hybridexact" | "hybrid" => Some(Self::HybridExact),
56            _ => None,
57        }
58    }
59}
60
61/// Common configuration for solvers.
62#[derive(Debug, Clone)]
63#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
64pub struct Config {
65    /// Optimization strategy.
66    pub strategy: Strategy,
67
68    /// Minimum spacing between geometries.
69    pub spacing: f64,
70
71    /// Margin from boundary edges.
72    pub margin: f64,
73
74    /// Maximum computation time in milliseconds (0 = unlimited).
75    pub time_limit_ms: u64,
76
77    /// Target utilization (0.0 - 1.0). Solver stops if reached.
78    pub target_utilization: Option<f64>,
79
80    /// Number of threads to use (0 = auto).
81    pub threads: usize,
82
83    // GA-specific parameters
84    /// Population size for GA.
85    pub population_size: usize,
86
87    /// Number of generations for GA.
88    pub max_generations: u32,
89
90    /// Crossover rate for GA (0.0 - 1.0).
91    pub crossover_rate: f64,
92
93    /// Mutation rate for GA (0.0 - 1.0).
94    pub mutation_rate: f64,
95
96    /// Elite count for GA.
97    pub elite_count: usize,
98
99    /// Optional RNG seed for reproducible runs of the stochastic strategies
100    /// (GA, BRKGA, SA). `None` seeds from system entropy (non-deterministic).
101    pub seed: Option<u64>,
102}
103
104impl Default for Config {
105    fn default() -> Self {
106        Self {
107            strategy: Strategy::default(),
108            spacing: 0.0,
109            margin: 0.0,
110            time_limit_ms: 30000,
111            target_utilization: None,
112            threads: 0,
113            population_size: 100,
114            max_generations: 500,
115            crossover_rate: 0.85,
116            mutation_rate: 0.05,
117            elite_count: 5,
118            seed: None,
119        }
120    }
121}
122
123impl Config {
124    /// Creates a new configuration with default values.
125    pub fn new() -> Self {
126        Self::default()
127    }
128
129    /// Sets the optimization strategy.
130    pub fn with_strategy(mut self, strategy: Strategy) -> Self {
131        self.strategy = strategy;
132        self
133    }
134
135    /// Sets the spacing between geometries.
136    pub fn with_spacing(mut self, spacing: f64) -> Self {
137        self.spacing = spacing;
138        self
139    }
140
141    /// Sets the margin from boundary edges.
142    pub fn with_margin(mut self, margin: f64) -> Self {
143        self.margin = margin;
144        self
145    }
146
147    /// Sets the time limit in milliseconds.
148    pub fn with_time_limit(mut self, ms: u64) -> Self {
149        self.time_limit_ms = ms;
150        self
151    }
152
153    /// Sets the target utilization.
154    pub fn with_target_utilization(mut self, util: f64) -> Self {
155        self.target_utilization = Some(util.clamp(0.0, 1.0));
156        self
157    }
158
159    /// Sets the RNG seed for reproducible stochastic runs (GA, BRKGA, SA).
160    pub fn with_seed(mut self, seed: u64) -> Self {
161        self.seed = Some(seed);
162        self
163    }
164}
165
166/// Progress callback for long-running operations.
167pub type ProgressCallback = Box<dyn Fn(ProgressInfo) + Send + Sync>;
168
169/// Progress information during solving.
170#[derive(Debug, Clone, Default)]
171pub struct ProgressInfo {
172    /// Current iteration/generation number.
173    pub iteration: u32,
174    /// Total expected iterations (0 if unknown).
175    pub total_iterations: u32,
176    /// Current best utilization (0.0 to 1.0).
177    pub utilization: f64,
178    /// Current best fitness value.
179    pub best_fitness: f64,
180    /// Number of items placed.
181    pub items_placed: usize,
182    /// Total number of items.
183    pub total_items: usize,
184    /// Elapsed time in milliseconds.
185    pub elapsed_ms: u64,
186    /// Current phase/stage description.
187    pub phase: String,
188    /// Whether the solver is still running.
189    pub running: bool,
190}
191
192impl ProgressInfo {
193    /// Creates a new progress info with default values.
194    pub fn new() -> Self {
195        Self {
196            running: true,
197            ..Default::default()
198        }
199    }
200
201    /// Sets the iteration info.
202    pub fn with_iteration(mut self, current: u32, total: u32) -> Self {
203        self.iteration = current;
204        self.total_iterations = total;
205        self
206    }
207
208    /// Sets the utilization.
209    pub fn with_utilization(mut self, utilization: f64) -> Self {
210        self.utilization = utilization;
211        self
212    }
213
214    /// Sets the best fitness.
215    pub fn with_fitness(mut self, fitness: f64) -> Self {
216        self.best_fitness = fitness;
217        self
218    }
219
220    /// Sets the items placed info.
221    pub fn with_items(mut self, placed: usize, total: usize) -> Self {
222        self.items_placed = placed;
223        self.total_items = total;
224        self
225    }
226
227    /// Sets the elapsed time.
228    pub fn with_elapsed(mut self, elapsed_ms: u64) -> Self {
229        self.elapsed_ms = elapsed_ms;
230        self
231    }
232
233    /// Sets the phase description.
234    pub fn with_phase(mut self, phase: impl Into<String>) -> Self {
235        self.phase = phase.into();
236        self
237    }
238
239    /// Marks the solver as finished.
240    pub fn finished(mut self) -> Self {
241        self.running = false;
242        self
243    }
244
245    /// Calculates the progress percentage (0.0 to 1.0).
246    pub fn progress_percent(&self) -> f64 {
247        if self.total_iterations > 0 {
248            self.iteration as f64 / self.total_iterations as f64
249        } else {
250            0.0
251        }
252    }
253}
254
255/// Trait for nesting/packing solvers.
256pub trait Solver {
257    /// The geometry type this solver handles.
258    type Geometry: Geometry;
259    /// The boundary type this solver handles.
260    type Boundary: Boundary;
261    /// The scalar type for coordinates.
262    type Scalar;
263
264    /// Solves the nesting/packing problem.
265    fn solve(
266        &self,
267        geometries: &[Self::Geometry],
268        boundary: &Self::Boundary,
269    ) -> Result<SolveResult<Self::Scalar>>;
270
271    /// Solves with a progress callback.
272    fn solve_with_progress(
273        &self,
274        geometries: &[Self::Geometry],
275        boundary: &Self::Boundary,
276        callback: ProgressCallback,
277    ) -> Result<SolveResult<Self::Scalar>>;
278
279    /// Cancels an ongoing solve operation.
280    fn cancel(&self);
281}