Skip to main content

u_nesting_core/
exact.rs

1//! Exact solver configuration and result types.
2//!
3//! This module provides types for MILP-based exact solving of small nesting instances.
4//! Exact solvers guarantee optimal solutions but are computationally expensive,
5//! making them suitable only for small instances (typically ≤15-20 pieces).
6//!
7//! # Features
8//!
9//! - `ExactConfig`: Configuration for exact solvers (time limits, gap tolerance)
10//! - `ExactResult`: Extended result with optimality proof information
11//! - `SolutionStatus`: Optimal, Feasible, Infeasible, or Timeout
12//!
13//! # Example
14//!
15//! ```ignore
16//! use u_nesting_core::exact::{ExactConfig, SolutionStatus};
17//!
18//! let config = ExactConfig::default()
19//!     .with_time_limit_ms(60000)  // 1 minute
20//!     .with_max_items(10);
21//! ```
22
23#[cfg(feature = "serde")]
24use serde::{Deserialize, Serialize};
25
26/// Solution status from exact solver.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
29pub enum SolutionStatus {
30    /// Proven optimal solution found.
31    Optimal,
32    /// Feasible solution found, but optimality not proven.
33    Feasible,
34    /// Problem is infeasible (no valid placement exists).
35    Infeasible,
36    /// Time limit reached without finding any feasible solution.
37    Timeout,
38    /// Solver encountered an error.
39    Error,
40    /// Solution status unknown or not applicable.
41    #[default]
42    Unknown,
43}
44
45impl std::fmt::Display for SolutionStatus {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            Self::Optimal => write!(f, "Optimal"),
49            Self::Feasible => write!(f, "Feasible"),
50            Self::Infeasible => write!(f, "Infeasible"),
51            Self::Timeout => write!(f, "Timeout"),
52            Self::Error => write!(f, "Error"),
53            Self::Unknown => write!(f, "Unknown"),
54        }
55    }
56}
57
58/// Configuration for exact (MILP-based) solvers.
59#[derive(Debug, Clone)]
60#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
61pub struct ExactConfig {
62    /// Maximum computation time in milliseconds.
63    pub time_limit_ms: u64,
64
65    /// Maximum number of items for exact solving (fallback to heuristic if exceeded).
66    pub max_items: usize,
67
68    /// Grid discretization step for position variables.
69    pub grid_step: f64,
70}
71
72impl Default for ExactConfig {
73    fn default() -> Self {
74        Self {
75            time_limit_ms: 60000, // 1 minute default
76            max_items: 15,        // Small instances only
77            grid_step: 1.0,       // 1 unit grid
78        }
79    }
80}
81
82impl ExactConfig {
83    /// Create a new configuration with default values.
84    pub fn new() -> Self {
85        Self::default()
86    }
87
88    /// Set time limit in milliseconds.
89    pub fn with_time_limit_ms(mut self, ms: u64) -> Self {
90        self.time_limit_ms = ms;
91        self
92    }
93
94    /// Checks every field against its range; the builders store what they are
95    /// given.
96    ///
97    /// # Errors
98    /// `ConfigError` for a `grid_step` that is not finite and positive, or a
99    /// `max_items` below 1.
100    pub fn validate(&self) -> crate::error::Result<()> {
101        use crate::error::{check_above, check_at_least};
102        check_above("grid_step", self.grid_step, 0.0)?;
103        check_at_least("max_items", self.max_items as f64, 1.0)?;
104        Ok(())
105    }
106
107    /// Set maximum number of items for exact solving.
108    pub fn with_max_items(mut self, max: usize) -> Self {
109        self.max_items = max;
110        self
111    }
112
113    /// Set grid discretization step.
114    pub fn with_grid_step(mut self, step: f64) -> Self {
115        self.grid_step = step;
116        self
117    }
118
119    /// Check if the number of items is within the exact solving limit.
120    pub fn is_within_limit(&self, num_items: usize) -> bool {
121        num_items <= self.max_items
122    }
123}
124
125/// Extended result information from exact solver.
126#[derive(Debug, Clone, Default)]
127#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
128pub struct ExactResult {
129    /// Solution status.
130    pub status: SolutionStatus,
131
132    /// Best objective value found (lower is better for minimization).
133    pub objective_value: f64,
134
135    /// Best bound on optimal value (for optimality gap calculation).
136    pub best_bound: f64,
137
138    /// Optimality gap: (objective - bound) / objective.
139    pub gap: f64,
140
141    /// Number of branch-and-bound nodes explored.
142    pub nodes_explored: u64,
143
144    /// Number of simplex iterations.
145    pub iterations: u64,
146
147    /// Whether the solution is proven optimal.
148    pub is_optimal: bool,
149
150    /// Solver-specific status message.
151    pub message: String,
152}
153
154impl ExactResult {
155    /// Create a new result with default values.
156    pub fn new() -> Self {
157        Self::default()
158    }
159
160    /// Create a result indicating optimal solution.
161    pub fn optimal(objective: f64) -> Self {
162        Self {
163            status: SolutionStatus::Optimal,
164            objective_value: objective,
165            best_bound: objective,
166            gap: 0.0,
167            is_optimal: true,
168            message: "Optimal solution found".to_string(),
169            ..Default::default()
170        }
171    }
172
173    /// Create a result indicating feasible (but not proven optimal) solution.
174    pub fn feasible(objective: f64, bound: f64) -> Self {
175        let gap = if objective.abs() > 1e-10 {
176            (objective - bound).abs() / objective.abs()
177        } else {
178            0.0
179        };
180        Self {
181            status: SolutionStatus::Feasible,
182            objective_value: objective,
183            best_bound: bound,
184            gap,
185            is_optimal: false,
186            message: format!("Feasible solution found (gap: {:.2}%)", gap * 100.0),
187            ..Default::default()
188        }
189    }
190
191    /// Create a result indicating infeasibility.
192    pub fn infeasible() -> Self {
193        Self {
194            status: SolutionStatus::Infeasible,
195            objective_value: f64::INFINITY,
196            best_bound: f64::INFINITY,
197            is_optimal: false,
198            message: "Problem is infeasible".to_string(),
199            ..Default::default()
200        }
201    }
202
203    /// Create a result indicating timeout.
204    pub fn timeout(best_objective: Option<f64>, best_bound: f64) -> Self {
205        match best_objective {
206            Some(obj) => {
207                let gap = if obj.abs() > 1e-10 {
208                    (obj - best_bound).abs() / obj.abs()
209                } else {
210                    0.0
211                };
212                Self {
213                    status: SolutionStatus::Timeout,
214                    objective_value: obj,
215                    best_bound,
216                    gap,
217                    is_optimal: false,
218                    message: format!("Time limit reached (gap: {:.2}%)", gap * 100.0),
219                    ..Default::default()
220                }
221            }
222            None => Self {
223                status: SolutionStatus::Timeout,
224                objective_value: f64::INFINITY,
225                best_bound,
226                is_optimal: false,
227                message: "Time limit reached without feasible solution".to_string(),
228                ..Default::default()
229            },
230        }
231    }
232
233    /// Create a result indicating an error.
234    pub fn error(message: impl Into<String>) -> Self {
235        Self {
236            status: SolutionStatus::Error,
237            message: message.into(),
238            ..Default::default()
239        }
240    }
241
242    /// Set solver statistics.
243    pub fn with_stats(mut self, nodes: u64, iterations: u64) -> Self {
244        self.nodes_explored = nodes;
245        self.iterations = iterations;
246        self
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn test_exact_config_default() {
256        let config = ExactConfig::default();
257        assert_eq!(config.time_limit_ms, 60000);
258        assert_eq!(config.max_items, 15);
259    }
260
261    #[test]
262    fn test_exact_config_builder() {
263        let config = ExactConfig::new()
264            .with_time_limit_ms(30000)
265            .with_max_items(10)
266            .with_grid_step(0.5);
267
268        assert_eq!(config.time_limit_ms, 30000);
269        assert_eq!(config.max_items, 10);
270        assert_eq!(config.grid_step, 0.5);
271    }
272
273    #[test]
274    fn test_is_within_limit() {
275        let config = ExactConfig::default().with_max_items(10);
276        assert!(config.is_within_limit(5));
277        assert!(config.is_within_limit(10));
278        assert!(!config.is_within_limit(11));
279    }
280
281    #[test]
282    fn test_solution_status_display() {
283        assert_eq!(format!("{}", SolutionStatus::Optimal), "Optimal");
284        assert_eq!(format!("{}", SolutionStatus::Feasible), "Feasible");
285        assert_eq!(format!("{}", SolutionStatus::Infeasible), "Infeasible");
286        assert_eq!(format!("{}", SolutionStatus::Timeout), "Timeout");
287    }
288
289    #[test]
290    fn test_exact_result_optimal() {
291        let result = ExactResult::optimal(100.0);
292        assert_eq!(result.status, SolutionStatus::Optimal);
293        assert_eq!(result.objective_value, 100.0);
294        assert_eq!(result.gap, 0.0);
295        assert!(result.is_optimal);
296    }
297
298    #[test]
299    fn test_exact_result_feasible() {
300        let result = ExactResult::feasible(100.0, 95.0);
301        assert_eq!(result.status, SolutionStatus::Feasible);
302        assert_eq!(result.objective_value, 100.0);
303        assert_eq!(result.best_bound, 95.0);
304        assert!((result.gap - 0.05).abs() < 1e-10);
305        assert!(!result.is_optimal);
306    }
307
308    #[test]
309    fn test_exact_result_timeout() {
310        let result = ExactResult::timeout(Some(100.0), 90.0);
311        assert_eq!(result.status, SolutionStatus::Timeout);
312        assert!((result.gap - 0.10).abs() < 1e-10);
313
314        let result_no_solution = ExactResult::timeout(None, 0.0);
315        assert_eq!(result_no_solution.status, SolutionStatus::Timeout);
316        assert_eq!(result_no_solution.objective_value, f64::INFINITY);
317    }
318
319    #[test]
320    fn test_exact_result_with_stats() {
321        let result = ExactResult::optimal(100.0).with_stats(1000, 50000);
322        assert_eq!(result.nodes_explored, 1000);
323        assert_eq!(result.iterations, 50000);
324    }
325}