use crate::tolerances::*;
use std::sync::{
atomic::AtomicBool,
Arc,
};
use std::time::Instant;
#[derive(Debug, Clone, PartialEq)]
pub struct OptionsError {
pub field: &'static str,
pub reason: &'static str,
}
impl std::fmt::Display for OptionsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "invalid option `{}`: {}", self.field, self.reason)
}
}
impl std::error::Error for OptionsError {}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DualPricing {
#[default]
MostInfeasible,
SteepestEdge,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SimplexMethod {
#[default]
Auto,
Primal,
Dual,
DualAdvanced,
}
#[derive(Debug, Clone)]
pub struct WarmStartBasis {
pub basis: Vec<usize>,
pub x_b: Vec<f64>,
}
#[derive(Debug, Clone)]
pub struct QpWarmStart {
pub x: Vec<f64>,
pub y: Vec<f64>,
pub mu: f64,
}
#[derive(Debug, Clone)]
pub struct LpWarmStart {
pub basis: Vec<usize>,
pub x_orig: Option<Vec<f64>>,
pub y_orig: Option<Vec<f64>>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StartStrategy {
RandomBox,
LatinHypercube,
}
#[derive(Debug, Clone)]
pub struct MultiStartConfig {
pub n_starts: usize,
pub seed: u64,
pub strategy: StartStrategy,
}
pub const DEFAULT_MULTISTART_SEED: u64 = 0x_00C0_FFEE_DEAD_BEEF;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BranchingStrategy {
MaxViolation,
}
pub const DEFAULT_GLOBAL_GAP_TOL: f64 = 1e-3;
pub const DEFAULT_GLOBAL_MAX_DEPTH: usize = 20;
pub const DEFAULT_GLOBAL_MAX_NODES: usize = 10_000;
#[derive(Debug, Clone)]
pub struct GlobalOptimizationConfig {
pub gap_tol: f64,
pub max_depth: usize,
pub max_nodes: usize,
pub branching: BranchingStrategy,
pub use_alpha_bb: bool,
pub use_mccormick: bool,
}
impl Default for GlobalOptimizationConfig {
fn default() -> Self {
Self {
gap_tol: DEFAULT_GLOBAL_GAP_TOL,
max_depth: DEFAULT_GLOBAL_MAX_DEPTH,
max_nodes: DEFAULT_GLOBAL_MAX_NODES,
branching: BranchingStrategy::MaxViolation,
use_alpha_bb: true,
use_mccormick: false,
}
}
}
impl Default for MultiStartConfig {
fn default() -> Self {
Self {
n_starts: 1,
seed: DEFAULT_MULTISTART_SEED,
strategy: StartStrategy::RandomBox,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MipBranching {
MostFractional,
}
pub const DEFAULT_MIP_GAP_TOL: f64 = 1e-6;
pub const DEFAULT_INTEGER_FEAS_TOL: f64 = 1e-6;
pub const DEFAULT_MIP_MAX_NODES: usize = 1_000_000;
pub const DEFAULT_MIP_MAX_DEPTH: usize = 1_000;
#[derive(Debug, Clone)]
pub struct MipConfig {
pub gap_tol: f64,
pub integer_feas_tol: f64,
pub max_nodes: usize,
pub max_depth: usize,
pub branching: MipBranching,
}
impl Default for MipConfig {
fn default() -> Self {
Self {
gap_tol: DEFAULT_MIP_GAP_TOL,
integer_feas_tol: DEFAULT_INTEGER_FEAS_TOL,
max_nodes: DEFAULT_MIP_MAX_NODES,
max_depth: DEFAULT_MIP_MAX_DEPTH,
branching: MipBranching::MostFractional,
}
}
}
pub const TOLERANCE_HIGH_EPS: f64 = 1e-8;
pub const TOLERANCE_MEDIUM_EPS: f64 = 1e-6;
pub const TOLERANCE_FAST_EPS: f64 = 1e-4;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Tolerance {
High,
Medium,
Fast,
Custom(f64),
}
pub const DEFAULT_IPM_EPS: f64 = 1e-6;
pub const DEFAULT_IPM_DELTA_MIN: f64 = 1e-8;
pub const DEFAULT_IPM_DELTA_INIT: f64 = 1e-6;
pub const DEFAULT_IPM_MAX_CORRECTORS: usize = 3;
#[derive(Debug, Clone)]
pub struct IpmOptions {
pub max_iter: usize,
pub eps: f64,
pub delta_min: f64,
pub delta_p_init: f64,
pub delta_d_init: f64,
pub max_correctors: usize,
pub dd_ldl: bool,
pub minres_ir: Option<usize>,
pub kkt_memory_budget_bytes: Option<usize>,
}
impl Default for IpmOptions {
fn default() -> Self {
Self {
max_iter: usize::MAX,
eps: DEFAULT_IPM_EPS,
delta_min: DEFAULT_IPM_DELTA_MIN,
delta_p_init: DEFAULT_IPM_DELTA_INIT,
delta_d_init: DEFAULT_IPM_DELTA_INIT,
max_correctors: DEFAULT_IPM_MAX_CORRECTORS,
dd_ldl: false,
minres_ir: None,
kkt_memory_budget_bytes: None,
}
}
}
impl IpmOptions {
pub fn validate(&self) -> Result<(), OptionsError> {
if !self.eps.is_finite() || self.eps <= 0.0 {
return Err(OptionsError { field: "ipm.eps", reason: "must be finite and > 0" });
}
if !self.delta_min.is_finite() || self.delta_min <= 0.0 {
return Err(OptionsError { field: "ipm.delta_min", reason: "must be finite and > 0" });
}
if !self.delta_p_init.is_finite() || self.delta_p_init <= 0.0 {
return Err(OptionsError { field: "ipm.delta_p_init", reason: "must be finite and > 0" });
}
if !self.delta_d_init.is_finite() || self.delta_d_init <= 0.0 {
return Err(OptionsError { field: "ipm.delta_d_init", reason: "must be finite and > 0" });
}
if self.max_correctors == 0 {
return Err(OptionsError { field: "ipm.max_correctors", reason: "must be >= 1" });
}
if let Some(ir) = self.minres_ir {
if ir > 10 {
return Err(OptionsError { field: "ipm.minres_ir", reason: "must be <= 10" });
}
}
Ok(())
}
pub fn with_eps(mut self, eps: f64) -> Result<Self, OptionsError> {
if !eps.is_finite() || eps <= 0.0 {
return Err(OptionsError { field: "ipm.eps", reason: "must be finite and > 0" });
}
self.eps = eps;
Ok(self)
}
pub fn with_max_correctors(mut self, n: usize) -> Result<Self, OptionsError> {
if n == 0 {
return Err(OptionsError { field: "ipm.max_correctors", reason: "must be >= 1" });
}
self.max_correctors = n;
Ok(self)
}
pub(crate) fn effective_minres_ir(&self) -> usize {
self.minres_ir.unwrap_or(0)
}
pub(crate) fn effective_kkt_memory_budget_bytes(&self) -> usize {
use crate::linalg::kkt_solver::DEFAULT_MEMORY_BUDGET_BYTES;
self.kkt_memory_budget_bytes.unwrap_or(DEFAULT_MEMORY_BUDGET_BYTES)
}
pub(crate) fn effective_max_l_nnz(&self) -> usize {
use crate::linalg::kkt_solver::BYTES_PER_L_ENTRY;
self.effective_kkt_memory_budget_bytes() / BYTES_PER_L_ENTRY
}
}
pub const DEFAULT_CLAMP_TOL: f64 = 1e-14;
#[derive(Debug, Clone)]
pub struct SolverOptions {
pub primal_tol: f64,
pub max_etas: usize,
pub clamp_tol: f64,
pub simplex_method: SimplexMethod,
pub dual_tol: f64,
pub dual_pricing: DualPricing,
pub enable_bound_flipping: bool,
pub warm_start: Option<WarmStartBasis>,
pub warm_start_qp: Option<QpWarmStart>,
pub warm_start_lp: Option<LpWarmStart>,
pub recover_warm_start_basis: bool,
pub use_lp_crash_basis: bool,
pub presolve: bool,
pub presolve_max_pass: usize,
pub presolve_skip_large_coeff: bool,
pub presolve_phase2: bool,
pub timeout_secs: Option<f64>,
pub(crate) cancel_flag: Option<Arc<AtomicBool>>,
pub(crate) deadline: Option<Instant>,
pub use_ruiz_scaling: bool,
pub tolerance: Option<Tolerance>,
pub ipm: IpmOptions,
pub multistart: Option<MultiStartConfig>,
pub global_optimization: Option<GlobalOptimizationConfig>,
pub threads: usize,
pub known_optimal_obj: Option<f64>,
}
const MAX_ETAS_DIVISOR: usize = 50;
const MAX_ETAS_FLOOR: usize = 20;
pub(crate) const DEFAULT_PRESOLVE_MAX_PASS: usize = 10;
pub fn default_max_etas(m: usize) -> usize {
(m / MAX_ETAS_DIVISOR).max(MAX_ETAS_FLOOR)
}
pub const MAX_PHASE1_RETRIES: usize = 8;
impl Default for SolverOptions {
fn default() -> Self {
Self {
primal_tol: PIVOT_TOL,
max_etas: 0,
clamp_tol: DEFAULT_CLAMP_TOL,
simplex_method: SimplexMethod::Auto,
dual_tol: PIVOT_TOL,
dual_pricing: DualPricing::default(),
enable_bound_flipping: false,
warm_start: None,
warm_start_qp: None,
warm_start_lp: None,
recover_warm_start_basis: false,
use_lp_crash_basis: true,
presolve: true,
presolve_max_pass: DEFAULT_PRESOLVE_MAX_PASS,
presolve_skip_large_coeff: false,
presolve_phase2: true,
timeout_secs: None,
cancel_flag: None,
deadline: None,
use_ruiz_scaling: true,
tolerance: None,
ipm: IpmOptions::default(),
multistart: None,
global_optimization: None,
threads: 1,
known_optimal_obj: None,
}
}
}
impl SolverOptions {
pub fn ipm_eps(&self) -> f64 {
match self.tolerance {
Some(Tolerance::High) => TOLERANCE_HIGH_EPS,
Some(Tolerance::Medium) => TOLERANCE_MEDIUM_EPS,
Some(Tolerance::Fast) => TOLERANCE_FAST_EPS,
Some(Tolerance::Custom(v)) => v,
None => self.ipm.eps,
}
}
pub fn validate(&self) -> Result<(), OptionsError> {
if !self.primal_tol.is_finite() || self.primal_tol <= 0.0 {
return Err(OptionsError { field: "primal_tol", reason: "must be finite and > 0" });
}
if !self.dual_tol.is_finite() || self.dual_tol <= 0.0 {
return Err(OptionsError { field: "dual_tol", reason: "must be finite and > 0" });
}
if !self.clamp_tol.is_finite() || self.clamp_tol < 0.0 {
return Err(OptionsError { field: "clamp_tol", reason: "must be finite and >= 0" });
}
if self.threads == 0 {
return Err(OptionsError { field: "threads", reason: "must be >= 1" });
}
if let Some(t) = self.timeout_secs {
if !t.is_finite() || t < 0.0 {
return Err(OptionsError { field: "timeout_secs", reason: "must be finite and >= 0" });
}
}
if let Some(Tolerance::Custom(v)) = self.tolerance {
if !v.is_finite() || v <= 0.0 {
return Err(OptionsError {
field: "tolerance.Custom",
reason: "must be finite and > 0",
});
}
}
self.ipm.validate()?;
Ok(())
}
pub fn with_timeout(mut self, secs: f64) -> Result<Self, OptionsError> {
if !secs.is_finite() || secs < 0.0 {
return Err(OptionsError { field: "timeout_secs", reason: "must be finite and >= 0" });
}
self.timeout_secs = Some(secs);
Ok(self)
}
pub fn with_threads(mut self, n: usize) -> Result<Self, OptionsError> {
if n == 0 {
return Err(OptionsError { field: "threads", reason: "must be >= 1" });
}
self.threads = n;
Ok(self)
}
pub fn with_tolerance(mut self, tol: Tolerance) -> Result<Self, OptionsError> {
if let Tolerance::Custom(v) = tol {
if !v.is_finite() || v <= 0.0 {
return Err(OptionsError {
field: "tolerance.Custom",
reason: "must be finite and > 0",
});
}
}
self.tolerance = Some(tol);
Ok(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tolerance_translation() {
let cases: &[(Option<Tolerance>, f64)] = &[
(Some(Tolerance::High), TOLERANCE_HIGH_EPS),
(Some(Tolerance::Medium), TOLERANCE_MEDIUM_EPS),
(Some(Tolerance::Fast), TOLERANCE_FAST_EPS),
(Some(Tolerance::Custom(1e-5)), 1e-5),
(None, DEFAULT_IPM_EPS), ];
for (tol, expected) in cases {
let opts = SolverOptions { tolerance: *tol, ..Default::default() };
assert_eq!(opts.ipm_eps(), *expected, "tolerance = {:?}", tol);
}
}
#[test]
#[allow(clippy::assertions_on_constants)]
fn test_tolerance_fast_is_looser_than_medium() {
const { assert!(TOLERANCE_FAST_EPS > TOLERANCE_MEDIUM_EPS) }
const { assert!(TOLERANCE_MEDIUM_EPS > TOLERANCE_HIGH_EPS) }
}
#[test]
fn test_ipm_validate_defaults_ok() {
assert!(IpmOptions::default().validate().is_ok());
}
#[test]
fn test_ipm_validate_eps() {
for bad in [0.0_f64, -1e-6, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let o = IpmOptions { eps: bad, ..Default::default() };
assert!(o.validate().is_err(), "eps={bad} should be invalid");
}
let o = IpmOptions { eps: f64::MIN_POSITIVE, ..Default::default() };
assert!(o.validate().is_ok());
}
#[test]
fn test_ipm_validate_delta_min() {
for bad in [0.0_f64, -1.0, f64::NAN, f64::INFINITY] {
let o = IpmOptions { delta_min: bad, ..Default::default() };
assert!(o.validate().is_err(), "delta_min={bad} should be invalid");
}
}
#[test]
fn test_ipm_validate_delta_p_init() {
for bad in [0.0_f64, -1.0, f64::NAN, f64::INFINITY] {
let o = IpmOptions { delta_p_init: bad, ..Default::default() };
assert!(o.validate().is_err(), "delta_p_init={bad} should be invalid");
}
}
#[test]
fn test_ipm_validate_delta_d_init() {
for bad in [0.0_f64, -1.0, f64::NAN, f64::INFINITY] {
let o = IpmOptions { delta_d_init: bad, ..Default::default() };
assert!(o.validate().is_err(), "delta_d_init={bad} should be invalid");
}
}
#[test]
fn test_ipm_validate_max_correctors() {
let o = IpmOptions { max_correctors: 0, ..Default::default() };
assert!(o.validate().is_err(), "max_correctors=0 should be invalid");
let o = IpmOptions { max_correctors: 1, ..Default::default() };
assert!(o.validate().is_ok());
}
#[test]
fn test_ipm_builder_with_eps() {
assert!(IpmOptions::default().with_eps(1e-4).is_ok());
assert!(IpmOptions::default().with_eps(f64::MIN_POSITIVE).is_ok());
for bad in [0.0_f64, -1.0, f64::NAN, f64::INFINITY] {
assert!(IpmOptions::default().with_eps(bad).is_err(), "with_eps({bad}) should err");
}
}
#[test]
fn test_ipm_builder_with_max_correctors() {
assert!(IpmOptions::default().with_max_correctors(1).is_ok());
assert!(IpmOptions::default().with_max_correctors(10).is_ok());
assert!(IpmOptions::default().with_max_correctors(0).is_err());
}
#[test]
fn test_solver_validate_defaults_ok() {
assert!(SolverOptions::default().validate().is_ok());
}
#[test]
fn test_solver_validate_primal_tol() {
for bad in [0.0_f64, -1e-8, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let o = SolverOptions { primal_tol: bad, ..Default::default() };
assert!(o.validate().is_err(), "primal_tol={bad}");
}
let o = SolverOptions { primal_tol: f64::MIN_POSITIVE, ..Default::default() };
assert!(o.validate().is_ok());
}
#[test]
fn test_solver_validate_dual_tol() {
for bad in [0.0_f64, -1e-8, f64::NAN, f64::INFINITY] {
let o = SolverOptions { dual_tol: bad, ..Default::default() };
assert!(o.validate().is_err(), "dual_tol={bad}");
}
}
#[test]
fn test_solver_validate_clamp_tol() {
let o = SolverOptions { clamp_tol: 0.0, ..Default::default() };
assert!(o.validate().is_ok(), "clamp_tol=0 should be ok");
for bad in [-1.0_f64, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let o = SolverOptions { clamp_tol: bad, ..Default::default() };
assert!(o.validate().is_err(), "clamp_tol={bad}");
}
}
#[test]
fn test_solver_validate_threads() {
let o = SolverOptions { threads: 0, ..Default::default() };
assert!(o.validate().is_err(), "threads=0");
for ok in [1_usize, 2, 8, usize::MAX] {
let o = SolverOptions { threads: ok, ..Default::default() };
assert!(o.validate().is_ok(), "threads={ok}");
}
}
#[test]
fn test_solver_validate_timeout_secs() {
assert!(SolverOptions { timeout_secs: None, ..Default::default() }.validate().is_ok());
for ok in [0.0_f64, 0.001, 1.0, 1000.0] {
let o = SolverOptions { timeout_secs: Some(ok), ..Default::default() };
assert!(o.validate().is_ok(), "timeout_secs=Some({ok}) must be valid");
}
for bad in [-1.0_f64, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let o = SolverOptions { timeout_secs: Some(bad), ..Default::default() };
assert!(o.validate().is_err(), "timeout_secs=Some({bad})");
}
}
#[test]
fn test_solver_validate_tolerance_custom() {
for tol in [Tolerance::High, Tolerance::Medium, Tolerance::Fast] {
let o = SolverOptions { tolerance: Some(tol), ..Default::default() };
assert!(o.validate().is_ok(), "tolerance={tol:?}");
}
let o = SolverOptions { tolerance: Some(Tolerance::Custom(1e-5)), ..Default::default() };
assert!(o.validate().is_ok());
for bad in [0.0_f64, -1e-4, f64::NAN, f64::INFINITY] {
let o = SolverOptions { tolerance: Some(Tolerance::Custom(bad)), ..Default::default() };
assert!(o.validate().is_err(), "Tolerance::Custom({bad})");
}
}
#[test]
fn test_solver_validate_propagates_ipm() {
let o = SolverOptions {
ipm: IpmOptions { eps: 0.0, ..Default::default() },
..Default::default()
};
assert!(o.validate().is_err(), "ipm.eps=0 must propagate");
let o = SolverOptions {
ipm: IpmOptions { max_correctors: 0, ..Default::default() },
..Default::default()
};
assert!(o.validate().is_err(), "ipm.max_correctors=0 must propagate");
}
#[test]
fn test_solver_builder_with_timeout() {
assert!(SolverOptions::default().with_timeout(10.0).is_ok());
assert!(SolverOptions::default().with_timeout(0.001).is_ok());
assert!(SolverOptions::default().with_timeout(0.0).is_ok(), "0.0 = immediately-expired deadline");
for bad in [-1.0_f64, f64::NAN, f64::INFINITY] {
assert!(SolverOptions::default().with_timeout(bad).is_err(), "with_timeout({bad})");
}
let o = SolverOptions::default().with_timeout(5.0).unwrap();
assert_eq!(o.timeout_secs, Some(5.0));
}
#[test]
fn test_solver_builder_with_threads() {
assert!(SolverOptions::default().with_threads(1).is_ok());
assert!(SolverOptions::default().with_threads(8).is_ok());
assert!(SolverOptions::default().with_threads(0).is_err());
let o = SolverOptions::default().with_threads(4).unwrap();
assert_eq!(o.threads, 4);
}
#[test]
fn test_solver_builder_with_tolerance() {
assert!(SolverOptions::default().with_tolerance(Tolerance::High).is_ok());
assert!(SolverOptions::default().with_tolerance(Tolerance::Medium).is_ok());
assert!(SolverOptions::default().with_tolerance(Tolerance::Fast).is_ok());
assert!(SolverOptions::default().with_tolerance(Tolerance::Custom(1e-5)).is_ok());
for bad in [0.0_f64, -1e-4, f64::NAN, f64::INFINITY] {
assert!(
SolverOptions::default().with_tolerance(Tolerance::Custom(bad)).is_err(),
"with_tolerance(Custom({bad}))"
);
}
let o = SolverOptions::default().with_tolerance(Tolerance::Fast).unwrap();
assert_eq!(o.tolerance, Some(Tolerance::Fast));
}
#[test]
fn test_options_error_display() {
let e = OptionsError { field: "ipm.eps", reason: "must be finite and > 0" };
let s = e.to_string();
assert!(s.contains("ipm.eps"), "display: {s}");
assert!(s.contains("finite"), "display: {s}");
}
#[test]
fn test_ipm_new_fields_default() {
let o = IpmOptions::default();
assert!(!o.dd_ldl, "dd_ldl default false");
assert!(o.minres_ir.is_none(), "minres_ir default None");
assert!(o.kkt_memory_budget_bytes.is_none(), "kkt_memory_budget_bytes default None");
}
#[test]
fn test_ipm_effective_minres_ir_default_and_override() {
let o = IpmOptions::default();
assert_eq!(o.effective_minres_ir(), 0, "default IR = 0");
let o2 = IpmOptions { minres_ir: Some(3), ..Default::default() };
assert_eq!(o2.effective_minres_ir(), 3);
}
#[test]
#[allow(clippy::assertions_on_constants, clippy::absurd_extreme_comparisons)]
fn test_ipm_validate_minres_ir() {
use crate::linalg::kkt_solver::MINRES_INEXACT_NEWTON_IR_STEPS;
assert!(IpmOptions::default().validate().is_ok());
for ok in [0_usize, 1, 5, 10] {
let o = IpmOptions { minres_ir: Some(ok), ..Default::default() };
assert!(o.validate().is_ok(), "minres_ir={ok} should be valid");
}
for bad in [11_usize, 100, usize::MAX] {
let o = IpmOptions { minres_ir: Some(bad), ..Default::default() };
assert!(o.validate().is_err(), "minres_ir={bad} should be invalid");
}
let _ = MINRES_INEXACT_NEWTON_IR_STEPS;
}
#[test]
fn test_ipm_effective_max_l_nnz_default_and_override() {
use crate::linalg::kkt_solver::{BYTES_PER_L_ENTRY, DEFAULT_MEMORY_BUDGET_BYTES};
let o = IpmOptions::default();
assert_eq!(o.effective_kkt_memory_budget_bytes(), DEFAULT_MEMORY_BUDGET_BYTES);
assert_eq!(o.effective_max_l_nnz(), DEFAULT_MEMORY_BUDGET_BYTES / BYTES_PER_L_ENTRY);
let o2 = IpmOptions { kkt_memory_budget_bytes: Some(1600), ..Default::default() };
assert_eq!(o2.effective_max_l_nnz(), 1600 / BYTES_PER_L_ENTRY);
}
#[test]
fn test_solver_presolve_fields_default() {
let o = SolverOptions::default();
assert_eq!(o.presolve_max_pass, DEFAULT_PRESOLVE_MAX_PASS, "default max pass");
assert!(!o.presolve_skip_large_coeff, "default skip_large_coeff = false");
assert!(o.presolve_phase2, "default phase2 = true");
}
#[test]
fn test_presolve_max_pass_controls_iteration_count() {
use crate::problem::SolveStatus;
use crate::qp::{solve_qp_with, QpProblem};
use crate::sparse::CscMatrix;
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let a = CscMatrix::new(0, 1);
let prob = QpProblem::new(q, vec![0.0], a, vec![], vec![(0.0_f64, 1.0_f64)], vec![]).unwrap();
let opts0 = SolverOptions { presolve_max_pass: 0, ..Default::default() };
let opts10 = SolverOptions { presolve_max_pass: 10, ..Default::default() };
let r0 = solve_qp_with(&prob, &opts0);
let r10 = solve_qp_with(&prob, &opts10);
assert_eq!(r0.status, SolveStatus::Optimal, "presolve_max_pass=0 should still solve trivial QP");
assert_eq!(r10.status, SolveStatus::Optimal, "presolve_max_pass=10 should solve trivial QP");
}
#[test]
fn test_presolve_phase2_false_skips_phase2() {
let o = SolverOptions { presolve_phase2: false, ..Default::default() };
assert!(!o.presolve_phase2);
let o2 = SolverOptions { presolve_phase2: true, ..Default::default() };
assert!(o2.presolve_phase2);
}
#[test]
fn test_presolve_skip_large_coeff_field() {
let no_skip = SolverOptions { presolve_skip_large_coeff: false, use_ruiz_scaling: false, ..Default::default() };
assert!(!no_skip.presolve_skip_large_coeff && !no_skip.use_ruiz_scaling);
let skip_via_field = SolverOptions { presolve_skip_large_coeff: true, use_ruiz_scaling: false, ..Default::default() };
assert!(skip_via_field.presolve_skip_large_coeff);
let skip_via_ruiz = SolverOptions { presolve_skip_large_coeff: false, use_ruiz_scaling: true, ..Default::default() };
assert!(skip_via_ruiz.presolve_skip_large_coeff || skip_via_ruiz.use_ruiz_scaling);
}
}