1pub mod certificate;
7use certificate::{BoundGapCertificate, OptimalCertificate};
8
9use crate::error::SolverError;
10use crate::options::WarmStartBasis;
11use crate::sparse::CscMatrix;
12use std::fmt;
13
14#[non_exhaustive]
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub enum ConstraintType {
18 Le,
20 Ge,
22 Eq,
24}
25
26#[non_exhaustive]
28#[derive(Debug, Clone, Copy, PartialEq, Default)]
29pub enum SolveRoute {
30 #[default]
32 Unknown,
33 LpDirect,
35 LpForwardedFromQp,
37 QpIpm,
39}
40
41#[derive(Debug, Clone, Default)]
46pub struct SolveStats {
47 pub route: SolveRoute,
49 pub deadline_triggered: bool,
54 pub postsolve_krylov_ir_skipped: bool,
58 pub lp_ipm_path: bool,
62}
63
64#[non_exhaustive]
66#[derive(Debug, Clone, PartialEq)]
67pub enum SolveStatus {
68 Optimal,
70 LocallyOptimal,
76 Infeasible,
78 Unbounded,
80 MaxIterations,
82 SuboptimalSolution,
84 Timeout,
86 NumericalError,
88 NonConvex(String),
90 NonconvexLocal,
96 NonconvexGlobal,
101 NotSupported(String),
106}
107
108impl fmt::Display for SolveStatus {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 match self {
111 SolveStatus::Optimal => write!(f, "Optimal"),
112 SolveStatus::LocallyOptimal => write!(f, "LocallyOptimal"),
113 SolveStatus::Infeasible => write!(f, "Infeasible"),
114 SolveStatus::Unbounded => write!(f, "Unbounded"),
115 SolveStatus::MaxIterations => write!(f, "MaxIterations"),
116 SolveStatus::SuboptimalSolution => write!(f, "SuboptimalSolution"),
117 SolveStatus::Timeout => write!(f, "Timeout"),
118 SolveStatus::NumericalError => write!(f, "NumericalError"),
119 SolveStatus::NonConvex(msg) => write!(f, "NonConvex({})", msg),
120 SolveStatus::NonconvexLocal => write!(f, "NonconvexLocal"),
121 SolveStatus::NonconvexGlobal => write!(f, "NonconvexGlobal"),
122 SolveStatus::NotSupported(msg) => write!(f, "NotSupported({})", msg),
123 }
124 }
125}
126
127#[derive(Debug, Clone)]
133pub struct SolverResult {
134 pub status: SolveStatus,
136 pub objective: f64,
138 pub solution: Vec<f64>,
140 pub dual_solution: Vec<f64>,
142 pub reduced_costs: Vec<f64>,
145 pub slack: Vec<f64>,
147 pub warm_start_basis: Option<WarmStartBasis>,
149 pub bound_duals: Vec<f64>,
159 pub iterations: usize,
161 pub final_residuals: Option<(f64, f64, f64)>,
163 pub duality_gap_rel: Option<f64>,
167 pub timing_breakdown: Option<TimingBreakdown>,
170 pub postsolve_dfeas: Option<f64>,
174 pub stats: SolveStats,
176 pub bound_gap_cert: Option<BoundGapCertificate>,
181 pub opt_cert: Option<OptimalCertificate>,
186}
187
188#[derive(Debug, Clone, Copy, Default, PartialEq)]
190pub struct TimingBreakdown {
191 pub presolve_us: u64,
194 pub solve_us: u64,
196 pub postsolve_us: u64,
198
199 pub ipm_factorize_us: u64,
202 pub ipm_solve_us: u64,
204 pub ipm_reg_retries: u32,
206 pub ipm_used_iterative: bool,
208
209 pub postsolve_map_us: u64,
212 pub postsolve_lsq_us: u64,
214 pub postsolve_recovery_us: u64,
216 pub postsolve_refine_us: u64,
218 pub postsolve_krylov_ir_us: u64,
220}
221
222impl Default for SolverResult {
223 fn default() -> Self {
224 SolverResult {
225 status: SolveStatus::NumericalError,
226 objective: 0.0,
227 solution: vec![],
228 dual_solution: vec![],
229 reduced_costs: vec![],
230 slack: vec![],
231 warm_start_basis: None,
232 bound_duals: vec![],
233 iterations: 0,
234 final_residuals: None,
235 duality_gap_rel: None,
236 timing_breakdown: None,
237 postsolve_dfeas: None,
238 stats: SolveStats::default(),
239 bound_gap_cert: None,
240 opt_cert: None,
241 }
242 }
243}
244
245impl fmt::Display for SolverResult {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 write!(f, "Status: {}, Objective: {}", self.status, self.objective)
248 }
249}
250
251#[derive(Debug, Clone)]
256pub struct LpProblem {
257 pub c: Vec<f64>,
259 pub a: CscMatrix,
261 pub b: Vec<f64>,
263 pub num_vars: usize,
265 pub num_constraints: usize,
267 pub constraint_types: Vec<ConstraintType>,
269 pub bounds: Vec<(f64, f64)>,
271 pub name: Option<String>,
273}
274
275impl LpProblem {
276 pub fn new(c: Vec<f64>, a: CscMatrix, b: Vec<f64>) -> Result<Self, SolverError> {
290 let num_vars = c.len();
291 let num_constraints = b.len();
292
293 let constraint_types = vec![ConstraintType::Le; num_constraints];
295 let bounds = vec![(0.0, f64::INFINITY); num_vars];
296 let name = None;
297
298 Self::new_general(c, a, b, constraint_types, bounds, name)
299 }
300
301 pub fn new_general(
315 c: Vec<f64>,
316 a: CscMatrix,
317 b: Vec<f64>,
318 constraint_types: Vec<ConstraintType>,
319 bounds: Vec<(f64, f64)>,
320 name: Option<String>,
321 ) -> Result<Self, SolverError> {
322 if c.len() != a.ncols {
324 return Err(SolverError::DimensionMismatch {
325 field: "c",
326 expected: a.ncols,
327 got: c.len(),
328 });
329 }
330 if b.len() != a.nrows {
331 return Err(SolverError::DimensionMismatch {
332 field: "b",
333 expected: a.nrows,
334 got: b.len(),
335 });
336 }
337 if constraint_types.len() != b.len() {
338 return Err(SolverError::DimensionMismatch {
339 field: "constraint_types",
340 expected: b.len(),
341 got: constraint_types.len(),
342 });
343 }
344 if bounds.len() != c.len() {
345 return Err(SolverError::DimensionMismatch {
346 field: "bounds",
347 expected: c.len(),
348 got: bounds.len(),
349 });
350 }
351 for (i, &v) in c.iter().enumerate() {
352 if !v.is_finite() {
353 return Err(SolverError::NonFiniteCoefficient { field: "c", index: i });
354 }
355 }
356 for (i, &v) in b.iter().enumerate() {
357 if !v.is_finite() {
358 return Err(SolverError::NonFiniteCoefficient { field: "b", index: i });
359 }
360 }
361 for (i, &v) in a.values.iter().enumerate() {
362 if !v.is_finite() {
363 return Err(SolverError::NonFiniteCoefficient { field: "A", index: i });
364 }
365 }
366 for (i, &(lb, ub)) in bounds.iter().enumerate() {
367 if lb.is_nan() || ub.is_nan() || lb > ub {
368 return Err(SolverError::InvalidBounds { index: i, lb, ub });
369 }
370 }
371
372 Ok(LpProblem {
373 num_vars: c.len(),
374 num_constraints: b.len(),
375 c,
376 a,
377 b,
378 constraint_types,
379 bounds,
380 name,
381 })
382 }
383}
384
385impl fmt::Display for LpProblem {
386 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
387 write!(
388 f,
389 "LP: min c^T x, {} vars, {} constraints",
390 self.num_vars, self.num_constraints
391 )
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398 use crate::error::SolverError;
399
400 #[test]
401 fn test_lp_problem_new_valid() {
402 let c = vec![1.0, 2.0];
404 let a = CscMatrix::new(2, 2);
405 let b = vec![5.0, 6.0];
406
407 let lp = LpProblem::new(c, a, b).unwrap();
408 assert_eq!(lp.num_vars, 2);
409 assert_eq!(lp.num_constraints, 2);
410 }
411
412 #[test]
413 fn test_lp_problem_new_invalid_c_dimension() {
414 let c = vec![1.0, 2.0, 3.0];
416 let a = CscMatrix::new(2, 2);
417 let b = vec![5.0, 6.0];
418
419 let result = LpProblem::new(c, a, b);
420 assert!(result.is_err());
421 assert!(matches!(
422 result.unwrap_err(),
423 SolverError::DimensionMismatch { field: "c", .. }
424 ));
425 }
426
427 #[test]
428 fn test_lp_problem_new_invalid_b_dimension() {
429 let c = vec![1.0, 2.0];
431 let a = CscMatrix::new(2, 2);
432 let b = vec![5.0, 6.0, 7.0];
433
434 let result = LpProblem::new(c, a, b);
435 assert!(result.is_err());
436 assert!(matches!(
437 result.unwrap_err(),
438 SolverError::DimensionMismatch { field: "b", .. }
439 ));
440 }
441
442 #[test]
443 fn test_lp_problem_display() {
444 let c = vec![1.0, 2.0];
445 let a = CscMatrix::new(2, 2);
446 let b = vec![5.0, 6.0];
447 let lp = LpProblem::new(c, a, b).unwrap();
448
449 let display = format!("{}", lp);
450 assert_eq!(display, "LP: min c^T x, 2 vars, 2 constraints");
451 }
452
453 #[test]
454 fn test_solve_status_display() {
455 assert_eq!(format!("{}", SolveStatus::Optimal), "Optimal");
456 assert_eq!(format!("{}", SolveStatus::Infeasible), "Infeasible");
457 assert_eq!(format!("{}", SolveStatus::Unbounded), "Unbounded");
458 }
459
460 #[test]
461 fn test_solver_result_display() {
462 let result = SolverResult {
463 status: SolveStatus::Optimal,
464 objective: 42.5,
465 solution: vec![1.0, 2.0],
466 dual_solution: vec![],
467 reduced_costs: vec![],
468 slack: vec![],
469 warm_start_basis: None,
470 ..Default::default()
471 };
472 let display = format!("{}", result);
473 assert_eq!(display, "Status: Optimal, Objective: 42.5");
474 }
475
476 #[test]
477 fn solver_result_default_is_not_success() {
478 let result = SolverResult::default();
479 assert_eq!(result.status, SolveStatus::NumericalError);
480 assert!(result.solution.is_empty());
481 }
482
483 fn make_lp(c: Vec<f64>, b: Vec<f64>, a_vals: Vec<f64>, bounds: Vec<(f64, f64)>)
484 -> Result<LpProblem, SolverError>
485 {
486 let n = c.len();
487 let m = b.len();
488 let a = if a_vals.is_empty() {
489 CscMatrix::new(m, n)
490 } else {
491 let rows = vec![0usize; n];
492 let cols: Vec<usize> = (0..n).collect();
493 CscMatrix::from_triplets(&rows, &cols, &a_vals, m, n).unwrap()
494 };
495 let ct = vec![ConstraintType::Le; m];
496 LpProblem::new_general(c, a, b, ct, bounds, None)
497 }
498
499 #[test]
500 fn lp_valid_accepted() {
501 let res = make_lp(
502 vec![1.0, 2.0], vec![5.0], vec![1.0, 1.0],
503 vec![(0.0, f64::INFINITY), (0.0, 10.0)],
504 );
505 assert!(res.is_ok());
506 }
507
508 #[test]
509 fn lp_nan_in_c_rejected() {
510 let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
511 for bad in bad_vals {
512 let res = make_lp(vec![bad, 1.0], vec![5.0], vec![1.0, 1.0],
513 vec![(0.0, f64::INFINITY); 2]);
514 assert!(
515 matches!(res, Err(SolverError::NonFiniteCoefficient { field: "c", .. })),
516 "expected NonFiniteCoefficient for c={bad}"
517 );
518 }
519 }
520
521 #[test]
522 fn lp_nan_in_b_rejected() {
523 let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
524 for bad in bad_vals {
525 let res = make_lp(vec![1.0, 2.0], vec![bad], vec![1.0, 1.0],
526 vec![(0.0, f64::INFINITY); 2]);
527 assert!(
528 matches!(res, Err(SolverError::NonFiniteCoefficient { field: "b", .. })),
529 "expected NonFiniteCoefficient for b={bad}"
530 );
531 }
532 }
533
534 #[test]
535 fn lp_nan_in_a_rejected() {
536 let n = 2;
537 let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
538 for bad in bad_vals {
539 let mut a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, n).unwrap();
541 a.values[0] = bad;
542 let res = LpProblem::new_general(
543 vec![1.0, 2.0], a, vec![5.0],
544 vec![ConstraintType::Le], vec![(0.0, f64::INFINITY); n], None,
545 );
546 assert!(
547 matches!(res, Err(SolverError::NonFiniteCoefficient { field: "A", .. })),
548 "expected NonFiniteCoefficient for A val={bad}"
549 );
550 }
551 }
552
553 #[test]
554 fn lp_nan_in_bounds_rejected() {
555 let cases: Vec<(f64, f64)> = vec![
556 (f64::NAN, 1.0),
557 (0.0, f64::NAN),
558 (f64::NAN, f64::NAN),
559 ];
560 for (lb, ub) in cases {
561 let res = make_lp(
562 vec![1.0, 2.0], vec![5.0], vec![1.0, 1.0],
563 vec![(lb, ub), (0.0, f64::INFINITY)],
564 );
565 assert!(
566 matches!(res, Err(SolverError::InvalidBounds { index: 0, .. })),
567 "expected InvalidBounds for ({lb},{ub})"
568 );
569 }
570 }
571
572 #[test]
573 fn lp_lb_gt_ub_rejected() {
574 let cases: Vec<(f64, f64)> = vec![
575 (5.0, 1.0),
576 (1.0, 0.0),
577 (f64::INFINITY, f64::NEG_INFINITY),
578 (0.1, 0.0),
579 ];
580 for (lb, ub) in cases {
581 let res = make_lp(
582 vec![1.0, 2.0], vec![5.0], vec![1.0, 1.0],
583 vec![(lb, ub), (0.0, f64::INFINITY)],
584 );
585 assert!(
586 matches!(res, Err(SolverError::InvalidBounds { .. })),
587 "expected InvalidBounds for lb={lb} ub={ub}"
588 );
589 }
590 }
591
592 #[test]
593 fn lp_inf_bounds_accepted() {
594 let res = make_lp(
595 vec![1.0, 2.0], vec![5.0], vec![1.0, 1.0],
596 vec![(f64::NEG_INFINITY, f64::INFINITY), (0.0, f64::INFINITY)],
597 );
598 assert!(res.is_ok(), "±inf bounds should be valid");
599 }
600}