1pub mod certificate;
7use certificate::{BoundGapCertificate, OptimalCertificate};
8
9use crate::error::SolverError;
10use crate::options::WarmStartBasis;
11use crate::sparse::CscMatrix;
12use std::fmt;
13
14pub(crate) fn is_valid_bound_pair(lb: f64, ub: f64) -> bool {
20 !lb.is_nan() && !ub.is_nan() && lb <= ub && lb < f64::INFINITY && ub > f64::NEG_INFINITY
21}
22
23#[non_exhaustive]
25#[derive(Debug, Clone, Copy, PartialEq)]
26pub enum ConstraintType {
27 Le,
29 Ge,
31 Eq,
33}
34
35#[non_exhaustive]
37#[derive(Debug, Clone, Copy, PartialEq, Default)]
38pub enum SolveRoute {
39 #[default]
41 Unknown,
42 LpDirect,
44 LpForwardedFromQp,
46 QpIpm,
48}
49
50#[derive(Debug, Clone, Default)]
55pub struct SolveStats {
56 pub route: SolveRoute,
58 pub deadline_triggered: bool,
63 pub postsolve_krylov_ir_skipped: bool,
67 pub lp_ipm_path: bool,
71 pub bounded_eq_ub_path: bool,
76}
77
78#[non_exhaustive]
80#[derive(Debug, Clone, PartialEq)]
81pub enum SolveStatus {
82 Optimal,
84 LocallyOptimal,
90 Infeasible,
92 Unbounded,
94 MaxIterations,
96 SuboptimalSolution,
98 Timeout,
100 NumericalError,
102 NonConvex(String),
104 NonconvexLocal,
110 NonconvexGlobal,
115 NotSupported(String),
120}
121
122impl fmt::Display for SolveStatus {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 match self {
125 SolveStatus::Optimal => write!(f, "Optimal"),
126 SolveStatus::LocallyOptimal => write!(f, "LocallyOptimal"),
127 SolveStatus::Infeasible => write!(f, "Infeasible"),
128 SolveStatus::Unbounded => write!(f, "Unbounded"),
129 SolveStatus::MaxIterations => write!(f, "MaxIterations"),
130 SolveStatus::SuboptimalSolution => write!(f, "SuboptimalSolution"),
131 SolveStatus::Timeout => write!(f, "Timeout"),
132 SolveStatus::NumericalError => write!(f, "NumericalError"),
133 SolveStatus::NonConvex(msg) => write!(f, "NonConvex({})", msg),
134 SolveStatus::NonconvexLocal => write!(f, "NonconvexLocal"),
135 SolveStatus::NonconvexGlobal => write!(f, "NonconvexGlobal"),
136 SolveStatus::NotSupported(msg) => write!(f, "NotSupported({})", msg),
137 }
138 }
139}
140
141#[derive(Debug, Clone)]
147pub struct SolverResult {
148 pub status: SolveStatus,
150 pub objective: f64,
152 pub solution: Vec<f64>,
154 pub dual_solution: Vec<f64>,
156 pub reduced_costs: Vec<f64>,
159 pub slack: Vec<f64>,
161 pub warm_start_basis: Option<WarmStartBasis>,
163 pub bound_duals: Vec<f64>,
173 pub iterations: usize,
175 pub final_residuals: Option<(f64, f64, f64)>,
177 pub duality_gap_rel: Option<f64>,
181 pub timing_breakdown: Option<TimingBreakdown>,
184 pub postsolve_dfeas: Option<f64>,
188 pub stats: SolveStats,
190 pub bound_gap_cert: Option<BoundGapCertificate>,
195 pub opt_cert: Option<OptimalCertificate>,
200}
201
202#[derive(Debug, Clone, Copy, Default, PartialEq)]
204pub struct TimingBreakdown {
205 pub presolve_us: u64,
208 pub solve_us: u64,
210 pub postsolve_us: u64,
212
213 pub ipm_factorize_us: u64,
216 pub ipm_solve_us: u64,
218 pub ipm_reg_retries: u32,
220 pub ipm_used_iterative: bool,
222
223 pub postsolve_map_us: u64,
226 pub postsolve_lsq_us: u64,
228 pub postsolve_recovery_us: u64,
230 pub postsolve_refine_us: u64,
232 pub postsolve_krylov_ir_us: u64,
234}
235
236impl Default for SolverResult {
237 fn default() -> Self {
238 SolverResult {
239 status: SolveStatus::NumericalError,
240 objective: 0.0,
241 solution: vec![],
242 dual_solution: vec![],
243 reduced_costs: vec![],
244 slack: vec![],
245 warm_start_basis: None,
246 bound_duals: vec![],
247 iterations: 0,
248 final_residuals: None,
249 duality_gap_rel: None,
250 timing_breakdown: None,
251 postsolve_dfeas: None,
252 stats: SolveStats::default(),
253 bound_gap_cert: None,
254 opt_cert: None,
255 }
256 }
257}
258
259impl fmt::Display for SolverResult {
260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261 write!(f, "Status: {}, Objective: {}", self.status, self.objective)
262 }
263}
264
265#[derive(Debug, Clone)]
273pub struct LpProblem {
274 pub c: Vec<f64>,
276 pub a: CscMatrix,
278 pub b: Vec<f64>,
280 pub num_vars: usize,
282 pub num_constraints: usize,
284 pub constraint_types: Vec<ConstraintType>,
286 pub bounds: Vec<(f64, f64)>,
288 pub name: Option<String>,
290 pub obj_offset: f64,
292}
293
294impl LpProblem {
295 pub fn new(c: Vec<f64>, a: CscMatrix, b: Vec<f64>) -> Result<Self, SolverError> {
309 let num_vars = c.len();
310 let num_constraints = b.len();
311
312 let constraint_types = vec![ConstraintType::Le; num_constraints];
314 let bounds = vec![(0.0, f64::INFINITY); num_vars];
315 let name = None;
316
317 Self::new_general(c, a, b, constraint_types, bounds, name)
318 }
319
320 pub fn new_general(
334 c: Vec<f64>,
335 a: CscMatrix,
336 b: Vec<f64>,
337 constraint_types: Vec<ConstraintType>,
338 bounds: Vec<(f64, f64)>,
339 name: Option<String>,
340 ) -> Result<Self, SolverError> {
341 if c.len() != a.ncols {
343 return Err(SolverError::DimensionMismatch {
344 field: "c",
345 expected: a.ncols,
346 got: c.len(),
347 });
348 }
349 if b.len() != a.nrows {
350 return Err(SolverError::DimensionMismatch {
351 field: "b",
352 expected: a.nrows,
353 got: b.len(),
354 });
355 }
356 if constraint_types.len() != b.len() {
357 return Err(SolverError::DimensionMismatch {
358 field: "constraint_types",
359 expected: b.len(),
360 got: constraint_types.len(),
361 });
362 }
363 if bounds.len() != c.len() {
364 return Err(SolverError::DimensionMismatch {
365 field: "bounds",
366 expected: c.len(),
367 got: bounds.len(),
368 });
369 }
370 for (i, &v) in c.iter().enumerate() {
371 if !v.is_finite() {
372 return Err(SolverError::NonFiniteCoefficient {
373 field: "c",
374 index: i,
375 });
376 }
377 }
378 for (i, &v) in b.iter().enumerate() {
379 if !v.is_finite() {
380 return Err(SolverError::NonFiniteCoefficient {
381 field: "b",
382 index: i,
383 });
384 }
385 }
386 for (i, &v) in a.values.iter().enumerate() {
387 if !v.is_finite() {
388 return Err(SolverError::NonFiniteCoefficient {
389 field: "A",
390 index: i,
391 });
392 }
393 }
394 for (i, &(lb, ub)) in bounds.iter().enumerate() {
395 if !is_valid_bound_pair(lb, ub) {
396 return Err(SolverError::InvalidBounds { index: i, lb, ub });
397 }
398 }
399
400 Ok(LpProblem {
401 num_vars: c.len(),
402 num_constraints: b.len(),
403 c,
404 a,
405 b,
406 constraint_types,
407 bounds,
408 name,
409 obj_offset: 0.0,
410 })
411 }
412}
413
414impl fmt::Display for LpProblem {
415 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
416 write!(
417 f,
418 "LP: min c^T x, {} vars, {} constraints",
419 self.num_vars, self.num_constraints
420 )
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427 use crate::error::SolverError;
428
429 #[test]
430 fn test_lp_problem_new_valid() {
431 let c = vec![1.0, 2.0];
433 let a = CscMatrix::new(2, 2);
434 let b = vec![5.0, 6.0];
435
436 let lp = LpProblem::new(c, a, b).unwrap();
437 assert_eq!(lp.num_vars, 2);
438 assert_eq!(lp.num_constraints, 2);
439 }
440
441 #[test]
442 fn test_lp_problem_new_invalid_c_dimension() {
443 let c = vec![1.0, 2.0, 3.0];
445 let a = CscMatrix::new(2, 2);
446 let b = vec![5.0, 6.0];
447
448 let result = LpProblem::new(c, a, b);
449 assert!(result.is_err());
450 assert!(matches!(
451 result.unwrap_err(),
452 SolverError::DimensionMismatch { field: "c", .. }
453 ));
454 }
455
456 #[test]
457 fn test_lp_problem_new_invalid_b_dimension() {
458 let c = vec![1.0, 2.0];
460 let a = CscMatrix::new(2, 2);
461 let b = vec![5.0, 6.0, 7.0];
462
463 let result = LpProblem::new(c, a, b);
464 assert!(result.is_err());
465 assert!(matches!(
466 result.unwrap_err(),
467 SolverError::DimensionMismatch { field: "b", .. }
468 ));
469 }
470
471 #[test]
472 fn test_lp_problem_display() {
473 let c = vec![1.0, 2.0];
474 let a = CscMatrix::new(2, 2);
475 let b = vec![5.0, 6.0];
476 let lp = LpProblem::new(c, a, b).unwrap();
477
478 let display = format!("{}", lp);
479 assert_eq!(display, "LP: min c^T x, 2 vars, 2 constraints");
480 }
481
482 #[test]
483 fn test_solve_status_display() {
484 assert_eq!(format!("{}", SolveStatus::Optimal), "Optimal");
485 assert_eq!(format!("{}", SolveStatus::Infeasible), "Infeasible");
486 assert_eq!(format!("{}", SolveStatus::Unbounded), "Unbounded");
487 }
488
489 #[test]
490 fn test_solver_result_display() {
491 let result = SolverResult {
492 status: SolveStatus::Optimal,
493 objective: 42.5,
494 solution: vec![1.0, 2.0],
495 dual_solution: vec![],
496 reduced_costs: vec![],
497 slack: vec![],
498 warm_start_basis: None,
499 ..Default::default()
500 };
501 let display = format!("{}", result);
502 assert_eq!(display, "Status: Optimal, Objective: 42.5");
503 }
504
505 #[test]
506 fn solver_result_default_is_not_success() {
507 let result = SolverResult::default();
508 assert_eq!(result.status, SolveStatus::NumericalError);
509 assert!(result.solution.is_empty());
510 }
511
512 fn make_lp(
513 c: Vec<f64>,
514 b: Vec<f64>,
515 a_vals: Vec<f64>,
516 bounds: Vec<(f64, f64)>,
517 ) -> Result<LpProblem, SolverError> {
518 let n = c.len();
519 let m = b.len();
520 let a = if a_vals.is_empty() {
521 CscMatrix::new(m, n)
522 } else {
523 let rows = vec![0usize; n];
524 let cols: Vec<usize> = (0..n).collect();
525 CscMatrix::from_triplets(&rows, &cols, &a_vals, m, n).unwrap()
526 };
527 let ct = vec![ConstraintType::Le; m];
528 LpProblem::new_general(c, a, b, ct, bounds, None)
529 }
530
531 #[test]
532 fn lp_valid_accepted() {
533 let res = make_lp(
534 vec![1.0, 2.0],
535 vec![5.0],
536 vec![1.0, 1.0],
537 vec![(0.0, f64::INFINITY), (0.0, 10.0)],
538 );
539 assert!(res.is_ok());
540 }
541
542 #[test]
543 fn lp_nan_in_c_rejected() {
544 let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
545 for bad in bad_vals {
546 let res = make_lp(
547 vec![bad, 1.0],
548 vec![5.0],
549 vec![1.0, 1.0],
550 vec![(0.0, f64::INFINITY); 2],
551 );
552 assert!(
553 matches!(
554 res,
555 Err(SolverError::NonFiniteCoefficient { field: "c", .. })
556 ),
557 "expected NonFiniteCoefficient for c={bad}"
558 );
559 }
560 }
561
562 #[test]
563 fn lp_nan_in_b_rejected() {
564 let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
565 for bad in bad_vals {
566 let res = make_lp(
567 vec![1.0, 2.0],
568 vec![bad],
569 vec![1.0, 1.0],
570 vec![(0.0, f64::INFINITY); 2],
571 );
572 assert!(
573 matches!(
574 res,
575 Err(SolverError::NonFiniteCoefficient { field: "b", .. })
576 ),
577 "expected NonFiniteCoefficient for b={bad}"
578 );
579 }
580 }
581
582 #[test]
583 fn lp_nan_in_a_rejected() {
584 let n = 2;
585 let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
586 for bad in bad_vals {
587 let mut a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, n).unwrap();
589 a.values[0] = bad;
590 let res = LpProblem::new_general(
591 vec![1.0, 2.0],
592 a,
593 vec![5.0],
594 vec![ConstraintType::Le],
595 vec![(0.0, f64::INFINITY); n],
596 None,
597 );
598 assert!(
599 matches!(
600 res,
601 Err(SolverError::NonFiniteCoefficient { field: "A", .. })
602 ),
603 "expected NonFiniteCoefficient for A val={bad}"
604 );
605 }
606 }
607
608 #[test]
609 fn lp_nan_in_bounds_rejected() {
610 let cases: Vec<(f64, f64)> = vec![(f64::NAN, 1.0), (0.0, f64::NAN), (f64::NAN, f64::NAN)];
611 for (lb, ub) in cases {
612 let res = make_lp(
613 vec![1.0, 2.0],
614 vec![5.0],
615 vec![1.0, 1.0],
616 vec![(lb, ub), (0.0, f64::INFINITY)],
617 );
618 assert!(
619 matches!(res, Err(SolverError::InvalidBounds { index: 0, .. })),
620 "expected InvalidBounds for ({lb},{ub})"
621 );
622 }
623 }
624
625 #[test]
626 fn lp_lb_gt_ub_rejected() {
627 let cases: Vec<(f64, f64)> = vec![
628 (5.0, 1.0),
629 (1.0, 0.0),
630 (f64::INFINITY, f64::NEG_INFINITY),
631 (0.1, 0.0),
632 ];
633 for (lb, ub) in cases {
634 let res = make_lp(
635 vec![1.0, 2.0],
636 vec![5.0],
637 vec![1.0, 1.0],
638 vec![(lb, ub), (0.0, f64::INFINITY)],
639 );
640 assert!(
641 matches!(res, Err(SolverError::InvalidBounds { .. })),
642 "expected InvalidBounds for lb={lb} ub={ub}"
643 );
644 }
645 }
646
647 #[test]
648 fn lp_inf_bounds_accepted() {
649 let res = make_lp(
650 vec![1.0, 2.0],
651 vec![5.0],
652 vec![1.0, 1.0],
653 vec![(f64::NEG_INFINITY, f64::INFINITY), (0.0, f64::INFINITY)],
654 );
655 assert!(res.is_ok(), "±inf bounds should be valid");
656 }
657
658 #[test]
661 fn lp_empty_interval_same_inf_rejected() {
662 let cases: Vec<(f64, f64)> = vec![
663 (f64::INFINITY, f64::INFINITY),
664 (f64::NEG_INFINITY, f64::NEG_INFINITY),
665 ];
666 for (lb, ub) in cases {
667 let res = make_lp(
668 vec![1.0, 2.0],
669 vec![5.0],
670 vec![1.0, 1.0],
671 vec![(lb, ub), (0.0, f64::INFINITY)],
672 );
673 assert!(
674 matches!(res, Err(SolverError::InvalidBounds { index: 0, .. })),
675 "expected InvalidBounds for ({lb},{ub})"
676 );
677 }
678 }
679
680 #[test]
686 fn lp_valid_bound_variants_accepted() {
687 let valid_cases: Vec<Vec<(f64, f64)>> = vec![
688 vec![(f64::NEG_INFINITY, 5.0), (0.0, f64::INFINITY)],
689 vec![(0.0, f64::INFINITY), (f64::NEG_INFINITY, f64::INFINITY)],
690 vec![(3.0, 3.0), (0.0, 10.0)],
691 vec![(0.0, 0.0), (0.0, 0.0)],
692 ];
693 for bounds in valid_cases {
694 let res = make_lp(vec![1.0, 2.0], vec![5.0], vec![1.0, 1.0], bounds.clone());
695 assert!(res.is_ok(), "expected Ok for bounds={bounds:?}");
696 }
697 }
698
699 #[test]
701 fn lp_lone_inf_bound_rejected() {
702 let cases: Vec<(f64, f64)> = vec![
703 (f64::INFINITY, 5.0),
704 (f64::INFINITY, f64::INFINITY),
705 (0.0, f64::NEG_INFINITY),
706 (f64::NEG_INFINITY, f64::NEG_INFINITY),
707 ];
708 for (lb, ub) in cases {
709 let res = make_lp(
710 vec![1.0, 2.0],
711 vec![5.0],
712 vec![1.0, 1.0],
713 vec![(lb, ub), (0.0, f64::INFINITY)],
714 );
715 assert!(
716 matches!(res, Err(SolverError::InvalidBounds { index: 0, .. })),
717 "expected InvalidBounds for ({lb},{ub})"
718 );
719 }
720 }
721}