1pub mod certificate;
7use certificate::BoundGapCertificate;
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 pfeas: Option<f64>,
165 pub dfeas: Option<f64>,
167 pub gap: Option<f64>,
169 pub duality_gap_rel: Option<f64>,
173 pub timing_breakdown: Option<TimingBreakdown>,
176 pub postsolve_dfeas: Option<f64>,
180 pub stats: SolveStats,
182 pub bound_gap_cert: Option<BoundGapCertificate>,
187}
188
189#[derive(Debug, Clone, Copy, Default, PartialEq)]
191pub struct TimingBreakdown {
192 pub presolve_us: u64,
195 pub solve_us: u64,
197 pub postsolve_us: u64,
199
200 pub ipm_factorize_us: u64,
203 pub ipm_solve_us: u64,
205 pub ipm_reg_retries: u32,
207 pub ipm_used_iterative: bool,
209
210 pub postsolve_map_us: u64,
213 pub postsolve_lsq_us: u64,
215 pub postsolve_recovery_us: u64,
217 pub postsolve_refine_us: u64,
219 pub postsolve_krylov_ir_us: u64,
221}
222
223impl Default for SolverResult {
224 fn default() -> Self {
225 SolverResult {
226 status: SolveStatus::NumericalError,
227 objective: 0.0,
228 solution: vec![],
229 dual_solution: vec![],
230 reduced_costs: vec![],
231 slack: vec![],
232 warm_start_basis: None,
233 bound_duals: vec![],
234 iterations: 0,
235 final_residuals: None,
236 pfeas: None,
237 dfeas: None,
238 gap: None,
239 duality_gap_rel: None,
240 timing_breakdown: None,
241 postsolve_dfeas: None,
242 stats: SolveStats::default(),
243 bound_gap_cert: None,
244 }
245 }
246}
247
248impl fmt::Display for SolverResult {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 write!(f, "Status: {}, Objective: {}", self.status, self.objective)
251 }
252}
253
254#[derive(Debug, Clone)]
259pub struct LpProblem {
260 pub c: Vec<f64>,
262 pub a: CscMatrix,
264 pub b: Vec<f64>,
266 pub num_vars: usize,
268 pub num_constraints: usize,
270 pub constraint_types: Vec<ConstraintType>,
272 pub bounds: Vec<(f64, f64)>,
274 pub name: Option<String>,
276}
277
278impl LpProblem {
279 pub fn new(c: Vec<f64>, a: CscMatrix, b: Vec<f64>) -> Result<Self, SolverError> {
293 let num_vars = c.len();
294 let num_constraints = b.len();
295
296 let constraint_types = vec![ConstraintType::Le; num_constraints];
298 let bounds = vec![(0.0, f64::INFINITY); num_vars];
299 let name = None;
300
301 Self::new_general(c, a, b, constraint_types, bounds, name)
302 }
303
304 pub fn new_general(
318 c: Vec<f64>,
319 a: CscMatrix,
320 b: Vec<f64>,
321 constraint_types: Vec<ConstraintType>,
322 bounds: Vec<(f64, f64)>,
323 name: Option<String>,
324 ) -> Result<Self, SolverError> {
325 if c.len() != a.ncols {
327 return Err(SolverError::DimensionMismatch {
328 field: "c",
329 expected: a.ncols,
330 got: c.len(),
331 });
332 }
333 if b.len() != a.nrows {
334 return Err(SolverError::DimensionMismatch {
335 field: "b",
336 expected: a.nrows,
337 got: b.len(),
338 });
339 }
340 if constraint_types.len() != b.len() {
341 return Err(SolverError::DimensionMismatch {
342 field: "constraint_types",
343 expected: b.len(),
344 got: constraint_types.len(),
345 });
346 }
347 if bounds.len() != c.len() {
348 return Err(SolverError::DimensionMismatch {
349 field: "bounds",
350 expected: c.len(),
351 got: bounds.len(),
352 });
353 }
354 for (i, &v) in c.iter().enumerate() {
355 if !v.is_finite() {
356 return Err(SolverError::NonFiniteCoefficient { field: "c", index: i });
357 }
358 }
359 for (i, &v) in b.iter().enumerate() {
360 if !v.is_finite() {
361 return Err(SolverError::NonFiniteCoefficient { field: "b", index: i });
362 }
363 }
364 for (i, &v) in a.values.iter().enumerate() {
365 if !v.is_finite() {
366 return Err(SolverError::NonFiniteCoefficient { field: "A", index: i });
367 }
368 }
369 for (i, &(lb, ub)) in bounds.iter().enumerate() {
370 if lb.is_nan() || ub.is_nan() || lb > ub {
371 return Err(SolverError::InvalidBounds { index: i, lb, ub });
372 }
373 }
374
375 Ok(LpProblem {
376 num_vars: c.len(),
377 num_constraints: b.len(),
378 c,
379 a,
380 b,
381 constraint_types,
382 bounds,
383 name,
384 })
385 }
386}
387
388impl fmt::Display for LpProblem {
389 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390 write!(
391 f,
392 "LP: min c^T x, {} vars, {} constraints",
393 self.num_vars, self.num_constraints
394 )
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use crate::error::SolverError;
402
403 #[test]
404 fn test_lp_problem_new_valid() {
405 let c = vec![1.0, 2.0];
407 let a = CscMatrix::new(2, 2);
408 let b = vec![5.0, 6.0];
409
410 let lp = LpProblem::new(c, a, b).unwrap();
411 assert_eq!(lp.num_vars, 2);
412 assert_eq!(lp.num_constraints, 2);
413 }
414
415 #[test]
416 fn test_lp_problem_new_invalid_c_dimension() {
417 let c = vec![1.0, 2.0, 3.0];
419 let a = CscMatrix::new(2, 2);
420 let b = vec![5.0, 6.0];
421
422 let result = LpProblem::new(c, a, b);
423 assert!(result.is_err());
424 assert!(matches!(
425 result.unwrap_err(),
426 SolverError::DimensionMismatch { field: "c", .. }
427 ));
428 }
429
430 #[test]
431 fn test_lp_problem_new_invalid_b_dimension() {
432 let c = vec![1.0, 2.0];
434 let a = CscMatrix::new(2, 2);
435 let b = vec![5.0, 6.0, 7.0];
436
437 let result = LpProblem::new(c, a, b);
438 assert!(result.is_err());
439 assert!(matches!(
440 result.unwrap_err(),
441 SolverError::DimensionMismatch { field: "b", .. }
442 ));
443 }
444
445 #[test]
446 fn test_lp_problem_display() {
447 let c = vec![1.0, 2.0];
448 let a = CscMatrix::new(2, 2);
449 let b = vec![5.0, 6.0];
450 let lp = LpProblem::new(c, a, b).unwrap();
451
452 let display = format!("{}", lp);
453 assert_eq!(display, "LP: min c^T x, 2 vars, 2 constraints");
454 }
455
456 #[test]
457 fn test_solve_status_display() {
458 assert_eq!(format!("{}", SolveStatus::Optimal), "Optimal");
459 assert_eq!(format!("{}", SolveStatus::Infeasible), "Infeasible");
460 assert_eq!(format!("{}", SolveStatus::Unbounded), "Unbounded");
461 }
462
463 #[test]
464 fn test_solver_result_display() {
465 let result = SolverResult {
466 status: SolveStatus::Optimal,
467 objective: 42.5,
468 solution: vec![1.0, 2.0],
469 dual_solution: vec![],
470 reduced_costs: vec![],
471 slack: vec![],
472 warm_start_basis: None,
473 ..Default::default()
474 };
475 let display = format!("{}", result);
476 assert_eq!(display, "Status: Optimal, Objective: 42.5");
477 }
478
479 #[test]
480 fn solver_result_default_is_not_success() {
481 let result = SolverResult::default();
482 assert_eq!(result.status, SolveStatus::NumericalError);
483 assert!(result.solution.is_empty());
484 }
485
486 fn make_lp(c: Vec<f64>, b: Vec<f64>, a_vals: Vec<f64>, bounds: Vec<(f64, f64)>)
487 -> Result<LpProblem, SolverError>
488 {
489 let n = c.len();
490 let m = b.len();
491 let a = if a_vals.is_empty() {
492 CscMatrix::new(m, n)
493 } else {
494 let rows = vec![0usize; n];
495 let cols: Vec<usize> = (0..n).collect();
496 CscMatrix::from_triplets(&rows, &cols, &a_vals, m, n).unwrap()
497 };
498 let ct = vec![ConstraintType::Le; m];
499 LpProblem::new_general(c, a, b, ct, bounds, None)
500 }
501
502 #[test]
503 fn lp_valid_accepted() {
504 let res = make_lp(
505 vec![1.0, 2.0], vec![5.0], vec![1.0, 1.0],
506 vec![(0.0, f64::INFINITY), (0.0, 10.0)],
507 );
508 assert!(res.is_ok());
509 }
510
511 #[test]
512 fn lp_nan_in_c_rejected() {
513 let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
514 for bad in bad_vals {
515 let res = make_lp(vec![bad, 1.0], vec![5.0], vec![1.0, 1.0],
516 vec![(0.0, f64::INFINITY); 2]);
517 assert!(
518 matches!(res, Err(SolverError::NonFiniteCoefficient { field: "c", .. })),
519 "expected NonFiniteCoefficient for c={bad}"
520 );
521 }
522 }
523
524 #[test]
525 fn lp_nan_in_b_rejected() {
526 let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
527 for bad in bad_vals {
528 let res = make_lp(vec![1.0, 2.0], vec![bad], vec![1.0, 1.0],
529 vec![(0.0, f64::INFINITY); 2]);
530 assert!(
531 matches!(res, Err(SolverError::NonFiniteCoefficient { field: "b", .. })),
532 "expected NonFiniteCoefficient for b={bad}"
533 );
534 }
535 }
536
537 #[test]
538 fn lp_nan_in_a_rejected() {
539 let n = 2;
540 let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
541 for bad in bad_vals {
542 let mut a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, n).unwrap();
544 a.values[0] = bad;
545 let res = LpProblem::new_general(
546 vec![1.0, 2.0], a, vec![5.0],
547 vec![ConstraintType::Le], vec![(0.0, f64::INFINITY); n], None,
548 );
549 assert!(
550 matches!(res, Err(SolverError::NonFiniteCoefficient { field: "A", .. })),
551 "expected NonFiniteCoefficient for A val={bad}"
552 );
553 }
554 }
555
556 #[test]
557 fn lp_nan_in_bounds_rejected() {
558 let cases: Vec<(f64, f64)> = vec![
559 (f64::NAN, 1.0),
560 (0.0, f64::NAN),
561 (f64::NAN, f64::NAN),
562 ];
563 for (lb, ub) in cases {
564 let res = make_lp(
565 vec![1.0, 2.0], vec![5.0], vec![1.0, 1.0],
566 vec![(lb, ub), (0.0, f64::INFINITY)],
567 );
568 assert!(
569 matches!(res, Err(SolverError::InvalidBounds { index: 0, .. })),
570 "expected InvalidBounds for ({lb},{ub})"
571 );
572 }
573 }
574
575 #[test]
576 fn lp_lb_gt_ub_rejected() {
577 let cases: Vec<(f64, f64)> = vec![
578 (5.0, 1.0),
579 (1.0, 0.0),
580 (f64::INFINITY, f64::NEG_INFINITY),
581 (0.1, 0.0),
582 ];
583 for (lb, ub) in cases {
584 let res = make_lp(
585 vec![1.0, 2.0], vec![5.0], vec![1.0, 1.0],
586 vec![(lb, ub), (0.0, f64::INFINITY)],
587 );
588 assert!(
589 matches!(res, Err(SolverError::InvalidBounds { .. })),
590 "expected InvalidBounds for lb={lb} ub={ub}"
591 );
592 }
593 }
594
595 #[test]
596 fn lp_inf_bounds_accepted() {
597 let res = make_lp(
598 vec![1.0, 2.0], vec![5.0], vec![1.0, 1.0],
599 vec![(f64::NEG_INFINITY, f64::INFINITY), (0.0, f64::INFINITY)],
600 );
601 assert!(res.is_ok(), "±inf bounds should be valid");
602 }
603}