1#[cfg(feature = "serde")]
24use serde::{Deserialize, Serialize};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
29pub enum SolutionStatus {
30 Optimal,
32 Feasible,
34 Infeasible,
36 Timeout,
38 Error,
40 #[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#[derive(Debug, Clone)]
60#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
61pub struct ExactConfig {
62 pub time_limit_ms: u64,
64
65 pub max_items: usize,
67
68 pub grid_step: f64,
70}
71
72impl Default for ExactConfig {
73 fn default() -> Self {
74 Self {
75 time_limit_ms: 60000, max_items: 15, grid_step: 1.0, }
79 }
80}
81
82impl ExactConfig {
83 pub fn new() -> Self {
85 Self::default()
86 }
87
88 pub fn with_time_limit_ms(mut self, ms: u64) -> Self {
90 self.time_limit_ms = ms;
91 self
92 }
93
94 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 pub fn with_max_items(mut self, max: usize) -> Self {
109 self.max_items = max;
110 self
111 }
112
113 pub fn with_grid_step(mut self, step: f64) -> Self {
115 self.grid_step = step;
116 self
117 }
118
119 pub fn is_within_limit(&self, num_items: usize) -> bool {
121 num_items <= self.max_items
122 }
123}
124
125#[derive(Debug, Clone, Default)]
127#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
128pub struct ExactResult {
129 pub status: SolutionStatus,
131
132 pub objective_value: f64,
134
135 pub best_bound: f64,
137
138 pub gap: f64,
140
141 pub nodes_explored: u64,
143
144 pub iterations: u64,
146
147 pub is_optimal: bool,
149
150 pub message: String,
152}
153
154impl ExactResult {
155 pub fn new() -> Self {
157 Self::default()
158 }
159
160 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 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 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 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 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 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}