1use crate::tolerances::*;
12use std::sync::{
13 atomic::AtomicBool,
14 Arc,
15};
16
17use std::time::Instant;
18
19#[derive(Debug, Clone, PartialEq)]
26pub struct OptionsError {
27 pub field: &'static str,
29 pub reason: &'static str,
31}
32
33impl std::fmt::Display for OptionsError {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 write!(f, "invalid option `{}`: {}", self.field, self.reason)
36 }
37}
38
39impl std::error::Error for OptionsError {}
40
41#[non_exhaustive]
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
56pub enum DualPricing {
57 #[default]
58 MostInfeasible,
59 SteepestEdge,
60}
61
62#[non_exhaustive]
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
65pub enum SimplexMethod {
66 #[default]
68 Auto,
69 Primal,
71 Dual,
73 DualAdvanced,
75}
76
77#[derive(Debug, Clone)]
82pub struct WarmStartBasis {
83 pub basis: Vec<usize>,
85 pub x_b: Vec<f64>,
88}
89
90#[derive(Debug, Clone)]
104pub struct QpWarmStart {
105 pub x: Vec<f64>,
106 pub y: Vec<f64>,
107 pub mu: f64,
108}
109
110#[derive(Debug, Clone)]
121pub struct LpWarmStart {
122 pub basis: Vec<usize>,
123 pub x_orig: Option<Vec<f64>>,
124 pub y_orig: Option<Vec<f64>>,
125}
126
127#[non_exhaustive]
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum StartStrategy {
134 RandomBox,
136 LatinHypercube,
139}
140
141#[derive(Debug, Clone)]
156pub struct MultiStartConfig {
157 pub n_starts: usize,
159 pub seed: u64,
161 pub strategy: StartStrategy,
163}
164
165pub const DEFAULT_MULTISTART_SEED: u64 = 0x_00C0_FFEE_DEAD_BEEF;
168
169#[non_exhaustive]
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum BranchingStrategy {
176 MaxViolation,
177}
178
179pub const DEFAULT_GLOBAL_GAP_TOL: f64 = 1e-3;
186pub const DEFAULT_GLOBAL_MAX_DEPTH: usize = 20;
187pub const DEFAULT_GLOBAL_MAX_NODES: usize = 10_000;
188
189#[derive(Debug, Clone)]
199pub struct GlobalOptimizationConfig {
200 pub gap_tol: f64,
201 pub max_depth: usize,
202 pub max_nodes: usize,
203 pub branching: BranchingStrategy,
204 pub use_alpha_bb: bool,
205 pub use_mccormick: bool,
206}
207
208impl Default for GlobalOptimizationConfig {
209 fn default() -> Self {
210 Self {
211 gap_tol: DEFAULT_GLOBAL_GAP_TOL,
212 max_depth: DEFAULT_GLOBAL_MAX_DEPTH,
213 max_nodes: DEFAULT_GLOBAL_MAX_NODES,
214 branching: BranchingStrategy::MaxViolation,
215 use_alpha_bb: true,
216 use_mccormick: false,
217 }
218 }
219}
220
221impl Default for MultiStartConfig {
222 fn default() -> Self {
223 Self {
224 n_starts: 1,
225 seed: DEFAULT_MULTISTART_SEED,
226 strategy: StartStrategy::RandomBox,
227 }
228 }
229}
230
231#[non_exhaustive]
236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237pub enum MipBranching {
238 MostFractional,
239}
240
241pub const DEFAULT_MIP_GAP_TOL: f64 = 1e-6;
249pub const DEFAULT_INTEGER_FEAS_TOL: f64 = 1e-6;
250pub const DEFAULT_MIP_MAX_NODES: usize = 1_000_000;
251pub const DEFAULT_MIP_MAX_DEPTH: usize = 1_000;
252
253#[derive(Debug, Clone)]
262pub struct MipConfig {
263 pub gap_tol: f64,
264 pub integer_feas_tol: f64,
265 pub max_nodes: usize,
266 pub max_depth: usize,
267 pub branching: MipBranching,
268}
269
270impl Default for MipConfig {
271 fn default() -> Self {
272 Self {
273 gap_tol: DEFAULT_MIP_GAP_TOL,
274 integer_feas_tol: DEFAULT_INTEGER_FEAS_TOL,
275 max_nodes: DEFAULT_MIP_MAX_NODES,
276 max_depth: DEFAULT_MIP_MAX_DEPTH,
277 branching: MipBranching::MostFractional,
278 }
279 }
280}
281
282pub const TOLERANCE_HIGH_EPS: f64 = 1e-8;
286pub const TOLERANCE_MEDIUM_EPS: f64 = 1e-6;
288pub const TOLERANCE_FAST_EPS: f64 = 1e-4;
290
291#[non_exhaustive]
310#[derive(Debug, Clone, Copy, PartialEq)]
311pub enum Tolerance {
312 High,
314 Medium,
316 Fast,
318 Custom(f64),
320}
321
322pub const DEFAULT_IPM_EPS: f64 = 1e-6;
326pub const DEFAULT_IPM_DELTA_MIN: f64 = 1e-8;
328pub const DEFAULT_IPM_DELTA_INIT: f64 = 1e-6;
331pub const DEFAULT_IPM_MAX_CORRECTORS: usize = 3;
333
334#[derive(Debug, Clone)]
339pub struct IpmOptions {
340 pub max_iter: usize,
342 pub eps: f64,
344 pub delta_min: f64,
346 pub delta_p_init: f64,
348 pub delta_d_init: f64,
350 pub max_correctors: usize,
352 pub dd_ldl: bool,
355 pub minres_ir: Option<usize>,
359 pub kkt_memory_budget_bytes: Option<usize>,
363}
364
365impl Default for IpmOptions {
366 fn default() -> Self {
367 Self {
368 max_iter: usize::MAX,
369 eps: DEFAULT_IPM_EPS,
370 delta_min: DEFAULT_IPM_DELTA_MIN,
371 delta_p_init: DEFAULT_IPM_DELTA_INIT,
372 delta_d_init: DEFAULT_IPM_DELTA_INIT,
373 max_correctors: DEFAULT_IPM_MAX_CORRECTORS,
374 dd_ldl: false,
375 minres_ir: None,
376 kkt_memory_budget_bytes: None,
377 }
378 }
379}
380
381impl IpmOptions {
382 pub fn validate(&self) -> Result<(), OptionsError> {
387 if !self.eps.is_finite() || self.eps <= 0.0 {
388 return Err(OptionsError { field: "ipm.eps", reason: "must be finite and > 0" });
389 }
390 if !self.delta_min.is_finite() || self.delta_min <= 0.0 {
391 return Err(OptionsError { field: "ipm.delta_min", reason: "must be finite and > 0" });
392 }
393 if !self.delta_p_init.is_finite() || self.delta_p_init <= 0.0 {
394 return Err(OptionsError { field: "ipm.delta_p_init", reason: "must be finite and > 0" });
395 }
396 if !self.delta_d_init.is_finite() || self.delta_d_init <= 0.0 {
397 return Err(OptionsError { field: "ipm.delta_d_init", reason: "must be finite and > 0" });
398 }
399 if self.max_correctors == 0 {
400 return Err(OptionsError { field: "ipm.max_correctors", reason: "must be >= 1" });
401 }
402 if let Some(ir) = self.minres_ir {
403 if ir > 10 {
404 return Err(OptionsError { field: "ipm.minres_ir", reason: "must be <= 10" });
405 }
406 }
407 Ok(())
408 }
409
410 pub fn with_eps(mut self, eps: f64) -> Result<Self, OptionsError> {
412 if !eps.is_finite() || eps <= 0.0 {
413 return Err(OptionsError { field: "ipm.eps", reason: "must be finite and > 0" });
414 }
415 self.eps = eps;
416 Ok(self)
417 }
418
419 pub fn with_max_correctors(mut self, n: usize) -> Result<Self, OptionsError> {
421 if n == 0 {
422 return Err(OptionsError { field: "ipm.max_correctors", reason: "must be >= 1" });
423 }
424 self.max_correctors = n;
425 Ok(self)
426 }
427
428 pub(crate) fn effective_minres_ir(&self) -> usize {
430 self.minres_ir.unwrap_or(0)
431 }
432
433 pub(crate) fn effective_kkt_memory_budget_bytes(&self) -> usize {
435 use crate::linalg::kkt_solver::DEFAULT_MEMORY_BUDGET_BYTES;
436 self.kkt_memory_budget_bytes.unwrap_or(DEFAULT_MEMORY_BUDGET_BYTES)
437 }
438
439 pub(crate) fn effective_max_l_nnz(&self) -> usize {
441 use crate::linalg::kkt_solver::BYTES_PER_L_ENTRY;
442 self.effective_kkt_memory_budget_bytes() / BYTES_PER_L_ENTRY
443 }
444}
445
446pub const DEFAULT_CLAMP_TOL: f64 = 1e-14;
450
451#[derive(Debug, Clone)]
465pub struct SolverOptions {
466 pub primal_tol: f64,
469 pub max_etas: usize,
471 pub clamp_tol: f64,
473 pub simplex_method: SimplexMethod,
475 pub dual_tol: f64,
477 pub dual_pricing: DualPricing,
479 pub enable_bound_flipping: bool,
482 pub warm_start: Option<WarmStartBasis>,
484 pub warm_start_qp: Option<QpWarmStart>,
486 pub warm_start_lp: Option<LpWarmStart>,
488 pub recover_warm_start_basis: bool,
497 pub use_lp_crash_basis: bool,
501 pub presolve: bool,
503 pub presolve_max_pass: usize,
505 pub presolve_skip_large_coeff: bool,
508 pub presolve_phase2: bool,
510 pub timeout_secs: Option<f64>,
512 pub(crate) cancel_flag: Option<Arc<AtomicBool>>,
514 pub(crate) deadline: Option<Instant>,
516
517 pub use_ruiz_scaling: bool,
520
521 pub tolerance: Option<Tolerance>,
526
527 pub ipm: IpmOptions,
530
531 pub multistart: Option<MultiStartConfig>,
533
534 pub global_optimization: Option<GlobalOptimizationConfig>,
537
538 pub threads: usize,
547
548 pub known_optimal_obj: Option<f64>,
554}
555
556const MAX_ETAS_DIVISOR: usize = 50;
558const MAX_ETAS_FLOOR: usize = 20;
560
561pub(crate) const DEFAULT_PRESOLVE_MAX_PASS: usize = 10;
563
564pub fn default_max_etas(m: usize) -> usize {
568 (m / MAX_ETAS_DIVISOR).max(MAX_ETAS_FLOOR)
569}
570
571pub const MAX_PHASE1_RETRIES: usize = 8;
574
575impl Default for SolverOptions {
576 fn default() -> Self {
577 Self {
578 primal_tol: PIVOT_TOL,
579 max_etas: 0,
580 clamp_tol: DEFAULT_CLAMP_TOL,
581 simplex_method: SimplexMethod::Auto,
582 dual_tol: PIVOT_TOL,
583 dual_pricing: DualPricing::default(),
584 enable_bound_flipping: false,
585 warm_start: None,
586 warm_start_qp: None,
587 warm_start_lp: None,
588 recover_warm_start_basis: false,
589 use_lp_crash_basis: true,
590 presolve: true,
591 presolve_max_pass: DEFAULT_PRESOLVE_MAX_PASS,
592 presolve_skip_large_coeff: false,
593 presolve_phase2: true,
594 timeout_secs: None,
595 cancel_flag: None,
596 deadline: None,
597 use_ruiz_scaling: true,
598 tolerance: None,
599 ipm: IpmOptions::default(),
600 multistart: None,
601 global_optimization: None,
602 threads: 1,
603 known_optimal_obj: None,
604 }
605 }
606}
607
608impl SolverOptions {
609 pub fn ipm_eps(&self) -> f64 {
611 match self.tolerance {
612 Some(Tolerance::High) => TOLERANCE_HIGH_EPS,
613 Some(Tolerance::Medium) => TOLERANCE_MEDIUM_EPS,
614 Some(Tolerance::Fast) => TOLERANCE_FAST_EPS,
615 Some(Tolerance::Custom(v)) => v,
616 None => self.ipm.eps,
617 }
618 }
619
620 pub fn validate(&self) -> Result<(), OptionsError> {
637 if !self.primal_tol.is_finite() || self.primal_tol <= 0.0 {
638 return Err(OptionsError { field: "primal_tol", reason: "must be finite and > 0" });
639 }
640 if !self.dual_tol.is_finite() || self.dual_tol <= 0.0 {
641 return Err(OptionsError { field: "dual_tol", reason: "must be finite and > 0" });
642 }
643 if !self.clamp_tol.is_finite() || self.clamp_tol < 0.0 {
644 return Err(OptionsError { field: "clamp_tol", reason: "must be finite and >= 0" });
645 }
646 if self.threads == 0 {
647 return Err(OptionsError { field: "threads", reason: "must be >= 1" });
648 }
649 if let Some(t) = self.timeout_secs {
650 if !t.is_finite() || t < 0.0 {
651 return Err(OptionsError { field: "timeout_secs", reason: "must be finite and >= 0" });
652 }
653 }
654 if let Some(Tolerance::Custom(v)) = self.tolerance {
655 if !v.is_finite() || v <= 0.0 {
656 return Err(OptionsError {
657 field: "tolerance.Custom",
658 reason: "must be finite and > 0",
659 });
660 }
661 }
662 self.ipm.validate()?;
663 Ok(())
664 }
665
666 pub fn with_timeout(mut self, secs: f64) -> Result<Self, OptionsError> {
668 if !secs.is_finite() || secs < 0.0 {
669 return Err(OptionsError { field: "timeout_secs", reason: "must be finite and >= 0" });
670 }
671 self.timeout_secs = Some(secs);
672 Ok(self)
673 }
674
675 pub fn with_threads(mut self, n: usize) -> Result<Self, OptionsError> {
677 if n == 0 {
678 return Err(OptionsError { field: "threads", reason: "must be >= 1" });
679 }
680 self.threads = n;
681 Ok(self)
682 }
683
684 pub fn with_tolerance(mut self, tol: Tolerance) -> Result<Self, OptionsError> {
689 if let Tolerance::Custom(v) = tol {
690 if !v.is_finite() || v <= 0.0 {
691 return Err(OptionsError {
692 field: "tolerance.Custom",
693 reason: "must be finite and > 0",
694 });
695 }
696 }
697 self.tolerance = Some(tol);
698 Ok(self)
699 }
700}
701
702#[cfg(test)]
703mod tests {
704 use super::*;
705
706 #[test]
709 fn test_tolerance_translation() {
710 let cases: &[(Option<Tolerance>, f64)] = &[
712 (Some(Tolerance::High), TOLERANCE_HIGH_EPS),
713 (Some(Tolerance::Medium), TOLERANCE_MEDIUM_EPS),
714 (Some(Tolerance::Fast), TOLERANCE_FAST_EPS),
715 (Some(Tolerance::Custom(1e-5)), 1e-5),
716 (None, DEFAULT_IPM_EPS), ];
718 for (tol, expected) in cases {
719 let opts = SolverOptions { tolerance: *tol, ..Default::default() };
720 assert_eq!(opts.ipm_eps(), *expected, "tolerance = {:?}", tol);
721 }
722 }
723
724 #[test]
725 #[allow(clippy::assertions_on_constants)]
726 fn test_tolerance_fast_is_looser_than_medium() {
727 const { assert!(TOLERANCE_FAST_EPS > TOLERANCE_MEDIUM_EPS) }
729 const { assert!(TOLERANCE_MEDIUM_EPS > TOLERANCE_HIGH_EPS) }
730 }
731
732 #[test]
735 fn test_ipm_validate_defaults_ok() {
736 assert!(IpmOptions::default().validate().is_ok());
737 }
738
739 #[test]
740 fn test_ipm_validate_eps() {
741 for bad in [0.0_f64, -1e-6, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
742 let o = IpmOptions { eps: bad, ..Default::default() };
743 assert!(o.validate().is_err(), "eps={bad} should be invalid");
744 }
745 let o = IpmOptions { eps: f64::MIN_POSITIVE, ..Default::default() };
747 assert!(o.validate().is_ok());
748 }
749
750 #[test]
751 fn test_ipm_validate_delta_min() {
752 for bad in [0.0_f64, -1.0, f64::NAN, f64::INFINITY] {
753 let o = IpmOptions { delta_min: bad, ..Default::default() };
754 assert!(o.validate().is_err(), "delta_min={bad} should be invalid");
755 }
756 }
757
758 #[test]
759 fn test_ipm_validate_delta_p_init() {
760 for bad in [0.0_f64, -1.0, f64::NAN, f64::INFINITY] {
761 let o = IpmOptions { delta_p_init: bad, ..Default::default() };
762 assert!(o.validate().is_err(), "delta_p_init={bad} should be invalid");
763 }
764 }
765
766 #[test]
767 fn test_ipm_validate_delta_d_init() {
768 for bad in [0.0_f64, -1.0, f64::NAN, f64::INFINITY] {
769 let o = IpmOptions { delta_d_init: bad, ..Default::default() };
770 assert!(o.validate().is_err(), "delta_d_init={bad} should be invalid");
771 }
772 }
773
774 #[test]
775 fn test_ipm_validate_max_correctors() {
776 let o = IpmOptions { max_correctors: 0, ..Default::default() };
777 assert!(o.validate().is_err(), "max_correctors=0 should be invalid");
778 let o = IpmOptions { max_correctors: 1, ..Default::default() };
779 assert!(o.validate().is_ok());
780 }
781
782 #[test]
785 fn test_ipm_builder_with_eps() {
786 assert!(IpmOptions::default().with_eps(1e-4).is_ok());
787 assert!(IpmOptions::default().with_eps(f64::MIN_POSITIVE).is_ok());
788 for bad in [0.0_f64, -1.0, f64::NAN, f64::INFINITY] {
789 assert!(IpmOptions::default().with_eps(bad).is_err(), "with_eps({bad}) should err");
790 }
791 }
792
793 #[test]
794 fn test_ipm_builder_with_max_correctors() {
795 assert!(IpmOptions::default().with_max_correctors(1).is_ok());
796 assert!(IpmOptions::default().with_max_correctors(10).is_ok());
797 assert!(IpmOptions::default().with_max_correctors(0).is_err());
798 }
799
800 #[test]
803 fn test_solver_validate_defaults_ok() {
804 assert!(SolverOptions::default().validate().is_ok());
805 }
806
807 #[test]
808 fn test_solver_validate_primal_tol() {
809 for bad in [0.0_f64, -1e-8, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
810 let o = SolverOptions { primal_tol: bad, ..Default::default() };
811 assert!(o.validate().is_err(), "primal_tol={bad}");
812 }
813 let o = SolverOptions { primal_tol: f64::MIN_POSITIVE, ..Default::default() };
814 assert!(o.validate().is_ok());
815 }
816
817 #[test]
818 fn test_solver_validate_dual_tol() {
819 for bad in [0.0_f64, -1e-8, f64::NAN, f64::INFINITY] {
820 let o = SolverOptions { dual_tol: bad, ..Default::default() };
821 assert!(o.validate().is_err(), "dual_tol={bad}");
822 }
823 }
824
825 #[test]
826 fn test_solver_validate_clamp_tol() {
827 let o = SolverOptions { clamp_tol: 0.0, ..Default::default() };
829 assert!(o.validate().is_ok(), "clamp_tol=0 should be ok");
830 for bad in [-1.0_f64, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
831 let o = SolverOptions { clamp_tol: bad, ..Default::default() };
832 assert!(o.validate().is_err(), "clamp_tol={bad}");
833 }
834 }
835
836 #[test]
837 fn test_solver_validate_threads() {
838 let o = SolverOptions { threads: 0, ..Default::default() };
839 assert!(o.validate().is_err(), "threads=0");
840 for ok in [1_usize, 2, 8, usize::MAX] {
841 let o = SolverOptions { threads: ok, ..Default::default() };
842 assert!(o.validate().is_ok(), "threads={ok}");
843 }
844 }
845
846 #[test]
847 fn test_solver_validate_timeout_secs() {
848 assert!(SolverOptions { timeout_secs: None, ..Default::default() }.validate().is_ok());
850 for ok in [0.0_f64, 0.001, 1.0, 1000.0] {
852 let o = SolverOptions { timeout_secs: Some(ok), ..Default::default() };
853 assert!(o.validate().is_ok(), "timeout_secs=Some({ok}) must be valid");
854 }
855 for bad in [-1.0_f64, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
857 let o = SolverOptions { timeout_secs: Some(bad), ..Default::default() };
858 assert!(o.validate().is_err(), "timeout_secs=Some({bad})");
859 }
860 }
861
862 #[test]
863 fn test_solver_validate_tolerance_custom() {
864 for tol in [Tolerance::High, Tolerance::Medium, Tolerance::Fast] {
866 let o = SolverOptions { tolerance: Some(tol), ..Default::default() };
867 assert!(o.validate().is_ok(), "tolerance={tol:?}");
868 }
869 let o = SolverOptions { tolerance: Some(Tolerance::Custom(1e-5)), ..Default::default() };
871 assert!(o.validate().is_ok());
872 for bad in [0.0_f64, -1e-4, f64::NAN, f64::INFINITY] {
874 let o = SolverOptions { tolerance: Some(Tolerance::Custom(bad)), ..Default::default() };
875 assert!(o.validate().is_err(), "Tolerance::Custom({bad})");
876 }
877 }
878
879 #[test]
880 fn test_solver_validate_propagates_ipm() {
881 let o = SolverOptions {
883 ipm: IpmOptions { eps: 0.0, ..Default::default() },
884 ..Default::default()
885 };
886 assert!(o.validate().is_err(), "ipm.eps=0 must propagate");
887
888 let o = SolverOptions {
889 ipm: IpmOptions { max_correctors: 0, ..Default::default() },
890 ..Default::default()
891 };
892 assert!(o.validate().is_err(), "ipm.max_correctors=0 must propagate");
893 }
894
895 #[test]
898 fn test_solver_builder_with_timeout() {
899 assert!(SolverOptions::default().with_timeout(10.0).is_ok());
900 assert!(SolverOptions::default().with_timeout(0.001).is_ok());
901 assert!(SolverOptions::default().with_timeout(0.0).is_ok(), "0.0 = immediately-expired deadline");
902 for bad in [-1.0_f64, f64::NAN, f64::INFINITY] {
903 assert!(SolverOptions::default().with_timeout(bad).is_err(), "with_timeout({bad})");
904 }
905 let o = SolverOptions::default().with_timeout(5.0).unwrap();
907 assert_eq!(o.timeout_secs, Some(5.0));
908 }
909
910 #[test]
911 fn test_solver_builder_with_threads() {
912 assert!(SolverOptions::default().with_threads(1).is_ok());
913 assert!(SolverOptions::default().with_threads(8).is_ok());
914 assert!(SolverOptions::default().with_threads(0).is_err());
915 let o = SolverOptions::default().with_threads(4).unwrap();
916 assert_eq!(o.threads, 4);
917 }
918
919 #[test]
920 fn test_solver_builder_with_tolerance() {
921 assert!(SolverOptions::default().with_tolerance(Tolerance::High).is_ok());
922 assert!(SolverOptions::default().with_tolerance(Tolerance::Medium).is_ok());
923 assert!(SolverOptions::default().with_tolerance(Tolerance::Fast).is_ok());
924 assert!(SolverOptions::default().with_tolerance(Tolerance::Custom(1e-5)).is_ok());
925 for bad in [0.0_f64, -1e-4, f64::NAN, f64::INFINITY] {
926 assert!(
927 SolverOptions::default().with_tolerance(Tolerance::Custom(bad)).is_err(),
928 "with_tolerance(Custom({bad}))"
929 );
930 }
931 let o = SolverOptions::default().with_tolerance(Tolerance::Fast).unwrap();
932 assert_eq!(o.tolerance, Some(Tolerance::Fast));
933 }
934
935 #[test]
938 fn test_options_error_display() {
939 let e = OptionsError { field: "ipm.eps", reason: "must be finite and > 0" };
940 let s = e.to_string();
941 assert!(s.contains("ipm.eps"), "display: {s}");
942 assert!(s.contains("finite"), "display: {s}");
943 }
944
945 #[test]
948 fn test_ipm_new_fields_default() {
949 let o = IpmOptions::default();
950 assert!(!o.dd_ldl, "dd_ldl default false");
951 assert!(o.minres_ir.is_none(), "minres_ir default None");
952 assert!(o.kkt_memory_budget_bytes.is_none(), "kkt_memory_budget_bytes default None");
953 }
954
955 #[test]
956 fn test_ipm_effective_minres_ir_default_and_override() {
957 let o = IpmOptions::default();
958 assert_eq!(o.effective_minres_ir(), 0, "default IR = 0");
959 let o2 = IpmOptions { minres_ir: Some(3), ..Default::default() };
960 assert_eq!(o2.effective_minres_ir(), 3);
961 }
962
963 #[test]
964 #[allow(clippy::assertions_on_constants, clippy::absurd_extreme_comparisons)]
965 fn test_ipm_validate_minres_ir() {
966 use crate::linalg::kkt_solver::MINRES_INEXACT_NEWTON_IR_STEPS;
967 assert!(IpmOptions::default().validate().is_ok());
969 for ok in [0_usize, 1, 5, 10] {
970 let o = IpmOptions { minres_ir: Some(ok), ..Default::default() };
971 assert!(o.validate().is_ok(), "minres_ir={ok} should be valid");
972 }
973 for bad in [11_usize, 100, usize::MAX] {
975 let o = IpmOptions { minres_ir: Some(bad), ..Default::default() };
976 assert!(o.validate().is_err(), "minres_ir={bad} should be invalid");
977 }
978 let _ = MINRES_INEXACT_NEWTON_IR_STEPS;
980 }
981
982 #[test]
983 fn test_ipm_effective_max_l_nnz_default_and_override() {
984 use crate::linalg::kkt_solver::{BYTES_PER_L_ENTRY, DEFAULT_MEMORY_BUDGET_BYTES};
985 let o = IpmOptions::default();
986 assert_eq!(o.effective_kkt_memory_budget_bytes(), DEFAULT_MEMORY_BUDGET_BYTES);
987 assert_eq!(o.effective_max_l_nnz(), DEFAULT_MEMORY_BUDGET_BYTES / BYTES_PER_L_ENTRY);
988 let o2 = IpmOptions { kkt_memory_budget_bytes: Some(1600), ..Default::default() };
989 assert_eq!(o2.effective_max_l_nnz(), 1600 / BYTES_PER_L_ENTRY);
990 }
991
992 #[test]
995 fn test_solver_presolve_fields_default() {
996 let o = SolverOptions::default();
997 assert_eq!(o.presolve_max_pass, DEFAULT_PRESOLVE_MAX_PASS, "default max pass");
998 assert!(!o.presolve_skip_large_coeff, "default skip_large_coeff = false");
999 assert!(o.presolve_phase2, "default phase2 = true");
1000 }
1001
1002 #[test]
1003 fn test_presolve_max_pass_controls_iteration_count() {
1004 use crate::problem::SolveStatus;
1005 use crate::qp::{solve_qp_with, QpProblem};
1006 use crate::sparse::CscMatrix;
1007
1008 let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
1010 let a = CscMatrix::new(0, 1);
1011 let prob = QpProblem::new(q, vec![0.0], a, vec![], vec![(0.0_f64, 1.0_f64)], vec![]).unwrap();
1012
1013 let opts0 = SolverOptions { presolve_max_pass: 0, ..Default::default() };
1015 let opts10 = SolverOptions { presolve_max_pass: 10, ..Default::default() };
1016 let r0 = solve_qp_with(&prob, &opts0);
1017 let r10 = solve_qp_with(&prob, &opts10);
1018 assert_eq!(r0.status, SolveStatus::Optimal, "presolve_max_pass=0 should still solve trivial QP");
1019 assert_eq!(r10.status, SolveStatus::Optimal, "presolve_max_pass=10 should solve trivial QP");
1020 }
1021
1022 #[test]
1023 fn test_presolve_phase2_false_skips_phase2() {
1024 let o = SolverOptions { presolve_phase2: false, ..Default::default() };
1027 assert!(!o.presolve_phase2);
1028 let o2 = SolverOptions { presolve_phase2: true, ..Default::default() };
1029 assert!(o2.presolve_phase2);
1030 }
1031
1032 #[test]
1033 fn test_presolve_skip_large_coeff_field() {
1034 let no_skip = SolverOptions { presolve_skip_large_coeff: false, use_ruiz_scaling: false, ..Default::default() };
1036 assert!(!no_skip.presolve_skip_large_coeff && !no_skip.use_ruiz_scaling);
1037 let skip_via_field = SolverOptions { presolve_skip_large_coeff: true, use_ruiz_scaling: false, ..Default::default() };
1038 assert!(skip_via_field.presolve_skip_large_coeff);
1039 let skip_via_ruiz = SolverOptions { presolve_skip_large_coeff: false, use_ruiz_scaling: true, ..Default::default() };
1040 assert!(skip_via_ruiz.presolve_skip_large_coeff || skip_via_ruiz.use_ruiz_scaling);
1042 }
1043
1044}