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}
72
73#[non_exhaustive]
75#[derive(Debug, Clone, PartialEq)]
76pub enum SolveStatus {
77 Optimal,
79 LocallyOptimal,
85 Infeasible,
87 Unbounded,
89 MaxIterations,
91 SuboptimalSolution,
93 Timeout,
95 NumericalError,
97 NonConvex(String),
99 NonconvexLocal,
105 NonconvexGlobal,
110 NotSupported(String),
115}
116
117impl fmt::Display for SolveStatus {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 match self {
120 SolveStatus::Optimal => write!(f, "Optimal"),
121 SolveStatus::LocallyOptimal => write!(f, "LocallyOptimal"),
122 SolveStatus::Infeasible => write!(f, "Infeasible"),
123 SolveStatus::Unbounded => write!(f, "Unbounded"),
124 SolveStatus::MaxIterations => write!(f, "MaxIterations"),
125 SolveStatus::SuboptimalSolution => write!(f, "SuboptimalSolution"),
126 SolveStatus::Timeout => write!(f, "Timeout"),
127 SolveStatus::NumericalError => write!(f, "NumericalError"),
128 SolveStatus::NonConvex(msg) => write!(f, "NonConvex({})", msg),
129 SolveStatus::NonconvexLocal => write!(f, "NonconvexLocal"),
130 SolveStatus::NonconvexGlobal => write!(f, "NonconvexGlobal"),
131 SolveStatus::NotSupported(msg) => write!(f, "NotSupported({})", msg),
132 }
133 }
134}
135
136#[derive(Debug, Clone)]
142pub struct SolverResult {
143 pub status: SolveStatus,
145 pub objective: f64,
147 pub solution: Vec<f64>,
149 pub dual_solution: Vec<f64>,
151 pub reduced_costs: Vec<f64>,
154 pub slack: Vec<f64>,
156 pub warm_start_basis: Option<WarmStartBasis>,
158 pub bound_duals: Vec<f64>,
168 pub iterations: usize,
170 pub final_residuals: Option<(f64, f64, f64)>,
172 pub duality_gap_rel: Option<f64>,
176 pub timing_breakdown: Option<TimingBreakdown>,
179 pub postsolve_dfeas: Option<f64>,
183 pub stats: SolveStats,
185 pub bound_gap_cert: Option<BoundGapCertificate>,
190 pub opt_cert: Option<OptimalCertificate>,
195}
196
197#[derive(Debug, Clone, Copy, Default, PartialEq)]
199pub struct TimingBreakdown {
200 pub presolve_us: u64,
203 pub solve_us: u64,
205 pub postsolve_us: u64,
207
208 pub ipm_factorize_us: u64,
211 pub ipm_solve_us: u64,
213 pub ipm_reg_retries: u32,
215 pub ipm_used_iterative: bool,
217
218 pub postsolve_map_us: u64,
221 pub postsolve_lsq_us: u64,
223 pub postsolve_recovery_us: u64,
225 pub postsolve_refine_us: u64,
227 pub postsolve_krylov_ir_us: u64,
229}
230
231impl Default for SolverResult {
232 fn default() -> Self {
233 SolverResult {
234 status: SolveStatus::NumericalError,
235 objective: 0.0,
236 solution: vec![],
237 dual_solution: vec![],
238 reduced_costs: vec![],
239 slack: vec![],
240 warm_start_basis: None,
241 bound_duals: vec![],
242 iterations: 0,
243 final_residuals: None,
244 duality_gap_rel: None,
245 timing_breakdown: None,
246 postsolve_dfeas: None,
247 stats: SolveStats::default(),
248 bound_gap_cert: None,
249 opt_cert: None,
250 }
251 }
252}
253
254impl fmt::Display for SolverResult {
255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 write!(f, "Status: {}, Objective: {}", self.status, self.objective)
257 }
258}
259
260#[derive(Debug, Clone)]
268pub struct LpProblem {
269 pub c: Vec<f64>,
271 pub a: CscMatrix,
273 pub b: Vec<f64>,
275 pub num_vars: usize,
277 pub num_constraints: usize,
279 pub constraint_types: Vec<ConstraintType>,
281 pub bounds: Vec<(f64, f64)>,
283 pub name: Option<String>,
285 pub obj_offset: f64,
287}
288
289impl LpProblem {
290 pub fn new(c: Vec<f64>, a: CscMatrix, b: Vec<f64>) -> Result<Self, SolverError> {
304 let num_vars = c.len();
305 let num_constraints = b.len();
306
307 let constraint_types = vec![ConstraintType::Le; num_constraints];
309 let bounds = vec![(0.0, f64::INFINITY); num_vars];
310 let name = None;
311
312 Self::new_general(c, a, b, constraint_types, bounds, name)
313 }
314
315 pub fn new_general(
329 c: Vec<f64>,
330 a: CscMatrix,
331 b: Vec<f64>,
332 constraint_types: Vec<ConstraintType>,
333 bounds: Vec<(f64, f64)>,
334 name: Option<String>,
335 ) -> Result<Self, SolverError> {
336 if c.len() != a.ncols {
338 return Err(SolverError::DimensionMismatch {
339 field: "c",
340 expected: a.ncols,
341 got: c.len(),
342 });
343 }
344 if b.len() != a.nrows {
345 return Err(SolverError::DimensionMismatch {
346 field: "b",
347 expected: a.nrows,
348 got: b.len(),
349 });
350 }
351 if constraint_types.len() != b.len() {
352 return Err(SolverError::DimensionMismatch {
353 field: "constraint_types",
354 expected: b.len(),
355 got: constraint_types.len(),
356 });
357 }
358 if bounds.len() != c.len() {
359 return Err(SolverError::DimensionMismatch {
360 field: "bounds",
361 expected: c.len(),
362 got: bounds.len(),
363 });
364 }
365 for (i, &v) in c.iter().enumerate() {
366 if !v.is_finite() {
367 return Err(SolverError::NonFiniteCoefficient {
368 field: "c",
369 index: i,
370 });
371 }
372 }
373 for (i, &v) in b.iter().enumerate() {
374 if !v.is_finite() {
375 return Err(SolverError::NonFiniteCoefficient {
376 field: "b",
377 index: i,
378 });
379 }
380 }
381 for (i, &v) in a.values.iter().enumerate() {
382 if !v.is_finite() {
383 return Err(SolverError::NonFiniteCoefficient {
384 field: "A",
385 index: i,
386 });
387 }
388 }
389 for (i, &(lb, ub)) in bounds.iter().enumerate() {
390 if !is_valid_bound_pair(lb, ub) {
391 return Err(SolverError::InvalidBounds { index: i, lb, ub });
392 }
393 }
394
395 Ok(LpProblem {
396 num_vars: c.len(),
397 num_constraints: b.len(),
398 c,
399 a,
400 b,
401 constraint_types,
402 bounds,
403 name,
404 obj_offset: 0.0,
405 })
406 }
407}
408
409impl fmt::Display for LpProblem {
410 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
411 write!(
412 f,
413 "LP: min c^T x, {} vars, {} constraints",
414 self.num_vars, self.num_constraints
415 )
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422 use crate::error::SolverError;
423
424 #[test]
425 fn test_lp_problem_new_valid() {
426 let c = vec![1.0, 2.0];
428 let a = CscMatrix::new(2, 2);
429 let b = vec![5.0, 6.0];
430
431 let lp = LpProblem::new(c, a, b).unwrap();
432 assert_eq!(lp.num_vars, 2);
433 assert_eq!(lp.num_constraints, 2);
434 }
435
436 #[test]
437 fn test_lp_problem_new_invalid_c_dimension() {
438 let c = vec![1.0, 2.0, 3.0];
440 let a = CscMatrix::new(2, 2);
441 let b = vec![5.0, 6.0];
442
443 let result = LpProblem::new(c, a, b);
444 assert!(result.is_err());
445 assert!(matches!(
446 result.unwrap_err(),
447 SolverError::DimensionMismatch { field: "c", .. }
448 ));
449 }
450
451 #[test]
452 fn test_lp_problem_new_invalid_b_dimension() {
453 let c = vec![1.0, 2.0];
455 let a = CscMatrix::new(2, 2);
456 let b = vec![5.0, 6.0, 7.0];
457
458 let result = LpProblem::new(c, a, b);
459 assert!(result.is_err());
460 assert!(matches!(
461 result.unwrap_err(),
462 SolverError::DimensionMismatch { field: "b", .. }
463 ));
464 }
465
466 #[test]
467 fn test_lp_problem_display() {
468 let c = vec![1.0, 2.0];
469 let a = CscMatrix::new(2, 2);
470 let b = vec![5.0, 6.0];
471 let lp = LpProblem::new(c, a, b).unwrap();
472
473 let display = format!("{}", lp);
474 assert_eq!(display, "LP: min c^T x, 2 vars, 2 constraints");
475 }
476
477 #[test]
478 fn test_solve_status_display() {
479 assert_eq!(format!("{}", SolveStatus::Optimal), "Optimal");
480 assert_eq!(format!("{}", SolveStatus::Infeasible), "Infeasible");
481 assert_eq!(format!("{}", SolveStatus::Unbounded), "Unbounded");
482 }
483
484 #[test]
485 fn test_solver_result_display() {
486 let result = SolverResult {
487 status: SolveStatus::Optimal,
488 objective: 42.5,
489 solution: vec![1.0, 2.0],
490 dual_solution: vec![],
491 reduced_costs: vec![],
492 slack: vec![],
493 warm_start_basis: None,
494 ..Default::default()
495 };
496 let display = format!("{}", result);
497 assert_eq!(display, "Status: Optimal, Objective: 42.5");
498 }
499
500 #[test]
501 fn solver_result_default_is_not_success() {
502 let result = SolverResult::default();
503 assert_eq!(result.status, SolveStatus::NumericalError);
504 assert!(result.solution.is_empty());
505 }
506
507 fn make_lp(
508 c: Vec<f64>,
509 b: Vec<f64>,
510 a_vals: Vec<f64>,
511 bounds: Vec<(f64, f64)>,
512 ) -> Result<LpProblem, SolverError> {
513 let n = c.len();
514 let m = b.len();
515 let a = if a_vals.is_empty() {
516 CscMatrix::new(m, n)
517 } else {
518 let rows = vec![0usize; n];
519 let cols: Vec<usize> = (0..n).collect();
520 CscMatrix::from_triplets(&rows, &cols, &a_vals, m, n).unwrap()
521 };
522 let ct = vec![ConstraintType::Le; m];
523 LpProblem::new_general(c, a, b, ct, bounds, None)
524 }
525
526 #[test]
527 fn lp_valid_accepted() {
528 let res = make_lp(
529 vec![1.0, 2.0],
530 vec![5.0],
531 vec![1.0, 1.0],
532 vec![(0.0, f64::INFINITY), (0.0, 10.0)],
533 );
534 assert!(res.is_ok());
535 }
536
537 #[test]
538 fn lp_nan_in_c_rejected() {
539 let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
540 for bad in bad_vals {
541 let res = make_lp(
542 vec![bad, 1.0],
543 vec![5.0],
544 vec![1.0, 1.0],
545 vec![(0.0, f64::INFINITY); 2],
546 );
547 assert!(
548 matches!(
549 res,
550 Err(SolverError::NonFiniteCoefficient { field: "c", .. })
551 ),
552 "expected NonFiniteCoefficient for c={bad}"
553 );
554 }
555 }
556
557 #[test]
558 fn lp_nan_in_b_rejected() {
559 let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
560 for bad in bad_vals {
561 let res = make_lp(
562 vec![1.0, 2.0],
563 vec![bad],
564 vec![1.0, 1.0],
565 vec![(0.0, f64::INFINITY); 2],
566 );
567 assert!(
568 matches!(
569 res,
570 Err(SolverError::NonFiniteCoefficient { field: "b", .. })
571 ),
572 "expected NonFiniteCoefficient for b={bad}"
573 );
574 }
575 }
576
577 #[test]
578 fn lp_nan_in_a_rejected() {
579 let n = 2;
580 let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
581 for bad in bad_vals {
582 let mut a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, n).unwrap();
584 a.values[0] = bad;
585 let res = LpProblem::new_general(
586 vec![1.0, 2.0],
587 a,
588 vec![5.0],
589 vec![ConstraintType::Le],
590 vec![(0.0, f64::INFINITY); n],
591 None,
592 );
593 assert!(
594 matches!(
595 res,
596 Err(SolverError::NonFiniteCoefficient { field: "A", .. })
597 ),
598 "expected NonFiniteCoefficient for A val={bad}"
599 );
600 }
601 }
602
603 #[test]
604 fn lp_nan_in_bounds_rejected() {
605 let cases: Vec<(f64, f64)> = vec![(f64::NAN, 1.0), (0.0, f64::NAN), (f64::NAN, f64::NAN)];
606 for (lb, ub) in cases {
607 let res = make_lp(
608 vec![1.0, 2.0],
609 vec![5.0],
610 vec![1.0, 1.0],
611 vec![(lb, ub), (0.0, f64::INFINITY)],
612 );
613 assert!(
614 matches!(res, Err(SolverError::InvalidBounds { index: 0, .. })),
615 "expected InvalidBounds for ({lb},{ub})"
616 );
617 }
618 }
619
620 #[test]
621 fn lp_lb_gt_ub_rejected() {
622 let cases: Vec<(f64, f64)> = vec![
623 (5.0, 1.0),
624 (1.0, 0.0),
625 (f64::INFINITY, f64::NEG_INFINITY),
626 (0.1, 0.0),
627 ];
628 for (lb, ub) in cases {
629 let res = make_lp(
630 vec![1.0, 2.0],
631 vec![5.0],
632 vec![1.0, 1.0],
633 vec![(lb, ub), (0.0, f64::INFINITY)],
634 );
635 assert!(
636 matches!(res, Err(SolverError::InvalidBounds { .. })),
637 "expected InvalidBounds for lb={lb} ub={ub}"
638 );
639 }
640 }
641
642 #[test]
643 fn lp_inf_bounds_accepted() {
644 let res = make_lp(
645 vec![1.0, 2.0],
646 vec![5.0],
647 vec![1.0, 1.0],
648 vec![(f64::NEG_INFINITY, f64::INFINITY), (0.0, f64::INFINITY)],
649 );
650 assert!(res.is_ok(), "±inf bounds should be valid");
651 }
652
653 #[test]
656 fn lp_empty_interval_same_inf_rejected() {
657 let cases: Vec<(f64, f64)> = vec![
658 (f64::INFINITY, f64::INFINITY),
659 (f64::NEG_INFINITY, f64::NEG_INFINITY),
660 ];
661 for (lb, ub) in cases {
662 let res = make_lp(
663 vec![1.0, 2.0],
664 vec![5.0],
665 vec![1.0, 1.0],
666 vec![(lb, ub), (0.0, f64::INFINITY)],
667 );
668 assert!(
669 matches!(res, Err(SolverError::InvalidBounds { index: 0, .. })),
670 "expected InvalidBounds for ({lb},{ub})"
671 );
672 }
673 }
674
675 #[test]
681 fn lp_valid_bound_variants_accepted() {
682 let valid_cases: Vec<Vec<(f64, f64)>> = vec![
683 vec![(f64::NEG_INFINITY, 5.0), (0.0, f64::INFINITY)],
684 vec![(0.0, f64::INFINITY), (f64::NEG_INFINITY, f64::INFINITY)],
685 vec![(3.0, 3.0), (0.0, 10.0)],
686 vec![(0.0, 0.0), (0.0, 0.0)],
687 ];
688 for bounds in valid_cases {
689 let res = make_lp(
690 vec![1.0, 2.0],
691 vec![5.0],
692 vec![1.0, 1.0],
693 bounds.clone(),
694 );
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}