1use crate::tolerances::*;
12use std::sync::{atomic::AtomicBool, Arc};
13
14use std::time::Instant;
15
16#[derive(Debug, Clone, PartialEq)]
23pub struct OptionsError {
24 pub field: &'static str,
26 pub reason: &'static str,
28}
29
30impl std::fmt::Display for OptionsError {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 write!(f, "invalid option `{}`: {}", self.field, self.reason)
33 }
34}
35
36impl std::error::Error for OptionsError {}
37
38#[non_exhaustive]
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
53pub enum DualPricing {
54 #[default]
55 MostInfeasible,
56 SteepestEdge,
57}
58
59#[non_exhaustive]
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
62pub enum SimplexMethod {
63 #[default]
65 Auto,
66 Primal,
68 Dual,
70 DualAdvanced,
72}
73
74#[derive(Debug, Clone)]
79pub struct WarmStartBasis {
80 pub basis: Vec<usize>,
82 pub x_b: Vec<f64>,
85}
86
87#[derive(Debug, Clone)]
101pub struct QpWarmStart {
102 pub x: Vec<f64>,
103 pub y: Vec<f64>,
104 pub mu: f64,
105}
106
107#[derive(Debug, Clone)]
118pub struct LpWarmStart {
119 pub basis: Vec<usize>,
120 pub x_orig: Option<Vec<f64>>,
121 pub y_orig: Option<Vec<f64>>,
122}
123
124#[non_exhaustive]
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum StartStrategy {
131 RandomBox,
133 LatinHypercube,
136}
137
138#[derive(Debug, Clone)]
153pub struct MultiStartConfig {
154 pub n_starts: usize,
156 pub seed: u64,
158 pub strategy: StartStrategy,
160}
161
162pub const DEFAULT_MULTISTART_SEED: u64 = 0x_00C0_FFEE_DEAD_BEEF;
165
166#[non_exhaustive]
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub enum BranchingStrategy {
173 MaxViolation,
174}
175
176pub const DEFAULT_GLOBAL_GAP_TOL: f64 = 1e-3;
183pub const DEFAULT_GLOBAL_MAX_DEPTH: usize = 20;
184pub const DEFAULT_GLOBAL_MAX_NODES: usize = 10_000;
185
186#[derive(Debug, Clone)]
196pub struct GlobalOptimizationConfig {
197 pub gap_tol: f64,
198 pub max_depth: usize,
199 pub max_nodes: usize,
200 pub branching: BranchingStrategy,
201 pub use_alpha_bb: bool,
202 pub use_mccormick: bool,
203}
204
205impl Default for GlobalOptimizationConfig {
206 fn default() -> Self {
207 Self {
208 gap_tol: DEFAULT_GLOBAL_GAP_TOL,
209 max_depth: DEFAULT_GLOBAL_MAX_DEPTH,
210 max_nodes: DEFAULT_GLOBAL_MAX_NODES,
211 branching: BranchingStrategy::MaxViolation,
212 use_alpha_bb: true,
213 use_mccormick: false,
214 }
215 }
216}
217
218impl Default for MultiStartConfig {
219 fn default() -> Self {
220 Self {
221 n_starts: 1,
222 seed: DEFAULT_MULTISTART_SEED,
223 strategy: StartStrategy::RandomBox,
224 }
225 }
226}
227
228#[non_exhaustive]
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub enum MipBranching {
235 MostFractional,
236}
237
238pub const DEFAULT_MIP_GAP_TOL: f64 = 1e-6;
246pub const DEFAULT_INTEGER_FEAS_TOL: f64 = 1e-6;
247pub const DEFAULT_MIP_MAX_NODES: usize = 1_000_000;
248pub const DEFAULT_MIP_MAX_DEPTH: usize = 1_000;
249
250#[derive(Debug, Clone)]
259pub struct MipConfig {
260 pub gap_tol: f64,
261 pub integer_feas_tol: f64,
262 pub max_nodes: usize,
263 pub max_depth: usize,
264 pub branching: MipBranching,
265}
266
267impl Default for MipConfig {
268 fn default() -> Self {
269 Self {
270 gap_tol: DEFAULT_MIP_GAP_TOL,
271 integer_feas_tol: DEFAULT_INTEGER_FEAS_TOL,
272 max_nodes: DEFAULT_MIP_MAX_NODES,
273 max_depth: DEFAULT_MIP_MAX_DEPTH,
274 branching: MipBranching::MostFractional,
275 }
276 }
277}
278
279pub const TOLERANCE_HIGH_EPS: f64 = 1e-8;
283pub const TOLERANCE_MEDIUM_EPS: f64 = 1e-6;
285pub const TOLERANCE_FAST_EPS: f64 = 1e-4;
287
288#[non_exhaustive]
294#[derive(Debug, Clone, Copy, PartialEq)]
295pub enum Tolerance {
296 High,
298 Medium,
300 Fast,
302 Custom(f64),
304}
305
306pub const DEFAULT_IPM_EPS: f64 = 1e-6;
310pub const DEFAULT_IPM_DELTA_MIN: f64 = 1e-8;
312pub const DEFAULT_IPM_DELTA_INIT: f64 = 1e-6;
315pub const DEFAULT_IPM_MAX_CORRECTORS: usize = 3;
317
318#[derive(Debug, Clone)]
323pub struct IpmOptions {
324 pub max_iter: usize,
329 pub eps: f64,
331 pub delta_min: f64,
333 pub delta_p_init: f64,
335 pub delta_d_init: f64,
337 pub max_correctors: usize,
339 pub dd_ldl: bool,
342 #[doc(hidden)]
346 pub minres_ir: Option<usize>,
347 #[doc(hidden)]
351 pub kkt_memory_budget_bytes: Option<usize>,
352}
353
354impl Default for IpmOptions {
355 fn default() -> Self {
356 Self {
357 max_iter: usize::MAX,
358 eps: DEFAULT_IPM_EPS,
359 delta_min: DEFAULT_IPM_DELTA_MIN,
360 delta_p_init: DEFAULT_IPM_DELTA_INIT,
361 delta_d_init: DEFAULT_IPM_DELTA_INIT,
362 max_correctors: DEFAULT_IPM_MAX_CORRECTORS,
363 dd_ldl: false,
364 minres_ir: None,
365 kkt_memory_budget_bytes: None,
366 }
367 }
368}
369
370impl IpmOptions {
371 pub fn validate(&self) -> Result<(), OptionsError> {
376 if !self.eps.is_finite() || self.eps <= 0.0 {
377 return Err(OptionsError {
378 field: "ipm.eps",
379 reason: "must be finite and > 0",
380 });
381 }
382 if !self.delta_min.is_finite() || self.delta_min <= 0.0 {
383 return Err(OptionsError {
384 field: "ipm.delta_min",
385 reason: "must be finite and > 0",
386 });
387 }
388 if !self.delta_p_init.is_finite() || self.delta_p_init <= 0.0 {
389 return Err(OptionsError {
390 field: "ipm.delta_p_init",
391 reason: "must be finite and > 0",
392 });
393 }
394 if !self.delta_d_init.is_finite() || self.delta_d_init <= 0.0 {
395 return Err(OptionsError {
396 field: "ipm.delta_d_init",
397 reason: "must be finite and > 0",
398 });
399 }
400 if self.max_correctors == 0 {
401 return Err(OptionsError {
402 field: "ipm.max_correctors",
403 reason: "must be >= 1",
404 });
405 }
406 if let Some(ir) = self.minres_ir {
407 if ir > 10 {
408 return Err(OptionsError {
409 field: "ipm.minres_ir",
410 reason: "must be <= 10",
411 });
412 }
413 }
414 Ok(())
415 }
416
417 pub fn with_eps(mut self, eps: f64) -> Result<Self, OptionsError> {
419 if !eps.is_finite() || eps <= 0.0 {
420 return Err(OptionsError {
421 field: "ipm.eps",
422 reason: "must be finite and > 0",
423 });
424 }
425 self.eps = eps;
426 Ok(self)
427 }
428
429 pub fn with_max_correctors(mut self, n: usize) -> Result<Self, OptionsError> {
431 if n == 0 {
432 return Err(OptionsError {
433 field: "ipm.max_correctors",
434 reason: "must be >= 1",
435 });
436 }
437 self.max_correctors = n;
438 Ok(self)
439 }
440
441 pub(crate) fn effective_minres_ir(&self) -> usize {
443 self.minres_ir.unwrap_or(0)
444 }
445
446 pub(crate) fn effective_kkt_memory_budget_bytes(&self) -> usize {
448 use crate::linalg::kkt_solver::DEFAULT_MEMORY_BUDGET_BYTES;
449 self.kkt_memory_budget_bytes
450 .unwrap_or(DEFAULT_MEMORY_BUDGET_BYTES)
451 }
452
453 pub(crate) fn effective_max_l_nnz(&self) -> usize {
455 use crate::linalg::kkt_solver::BYTES_PER_L_ENTRY;
456 self.effective_kkt_memory_budget_bytes() / BYTES_PER_L_ENTRY
457 }
458}
459
460pub const DEFAULT_CLAMP_TOL: f64 = 1e-14;
464
465#[derive(Debug, Clone)]
479pub struct SolverOptions {
480 pub primal_tol: f64,
483 pub max_etas: usize,
485 pub clamp_tol: f64,
487 pub simplex_method: SimplexMethod,
489 pub dual_tol: f64,
491 pub dual_pricing: DualPricing,
493 pub warm_start: Option<WarmStartBasis>,
495 pub warm_start_qp: Option<QpWarmStart>,
497 pub warm_start_lp: Option<LpWarmStart>,
499 pub recover_warm_start_basis: bool,
508 pub use_lp_crash_basis: bool,
511 pub presolve: bool,
513 pub presolve_max_pass: usize,
515 pub presolve_phase2: bool,
517 pub timeout_secs: Option<f64>,
519 pub(crate) cancel_flag: Option<Arc<AtomicBool>>,
521 pub(crate) deadline: Option<Instant>,
523
524 pub use_ruiz_scaling: bool,
527
528 pub tolerance: Option<Tolerance>,
533
534 pub ipm: IpmOptions,
537
538 pub multistart: Option<MultiStartConfig>,
540
541 pub global_optimization: Option<GlobalOptimizationConfig>,
544
545 pub threads: usize,
554
555 pub known_optimal_obj: Option<f64>,
561}
562
563const MAX_ETAS_DIVISOR: usize = 50;
565const MAX_ETAS_FLOOR: usize = 20;
567
568pub(crate) const DEFAULT_PRESOLVE_MAX_PASS: usize = 10;
570
571pub fn default_max_etas(m: usize) -> usize {
575 (m / MAX_ETAS_DIVISOR).max(MAX_ETAS_FLOOR)
576}
577
578pub(crate) const MAX_PHASE1_RETRIES: usize = 8;
581
582impl Default for SolverOptions {
583 fn default() -> Self {
584 Self {
585 primal_tol: PIVOT_TOL,
586 max_etas: 0,
587 clamp_tol: DEFAULT_CLAMP_TOL,
588 simplex_method: SimplexMethod::Auto,
589 dual_tol: PIVOT_TOL,
590 dual_pricing: DualPricing::default(),
591 warm_start: None,
592 warm_start_qp: None,
593 warm_start_lp: None,
594 recover_warm_start_basis: false,
595 use_lp_crash_basis: true,
596 presolve: true,
597 presolve_max_pass: DEFAULT_PRESOLVE_MAX_PASS,
598 presolve_phase2: true,
599 timeout_secs: None,
600 cancel_flag: None,
601 deadline: None,
602 use_ruiz_scaling: true,
603 tolerance: None,
604 ipm: IpmOptions::default(),
605 multistart: None,
606 global_optimization: None,
607 threads: 1,
608 known_optimal_obj: None,
609 }
610 }
611}
612
613impl SolverOptions {
614 pub fn ipm_eps(&self) -> f64 {
616 match self.tolerance {
617 Some(Tolerance::High) => TOLERANCE_HIGH_EPS,
618 Some(Tolerance::Medium) => TOLERANCE_MEDIUM_EPS,
619 Some(Tolerance::Fast) => TOLERANCE_FAST_EPS,
620 Some(Tolerance::Custom(v)) => v,
621 None => self.ipm.eps,
622 }
623 }
624
625 pub fn validate(&self) -> Result<(), OptionsError> {
642 if !self.primal_tol.is_finite() || self.primal_tol <= 0.0 {
643 return Err(OptionsError {
644 field: "primal_tol",
645 reason: "must be finite and > 0",
646 });
647 }
648 if !self.dual_tol.is_finite() || self.dual_tol <= 0.0 {
649 return Err(OptionsError {
650 field: "dual_tol",
651 reason: "must be finite and > 0",
652 });
653 }
654 if !self.clamp_tol.is_finite() || self.clamp_tol < 0.0 {
655 return Err(OptionsError {
656 field: "clamp_tol",
657 reason: "must be finite and >= 0",
658 });
659 }
660 if self.threads == 0 {
661 return Err(OptionsError {
662 field: "threads",
663 reason: "must be >= 1",
664 });
665 }
666 if let Some(t) = self.timeout_secs {
667 if !t.is_finite() || t < 0.0 {
668 return Err(OptionsError {
669 field: "timeout_secs",
670 reason: "must be finite and >= 0",
671 });
672 }
673 }
674 if let Some(Tolerance::Custom(v)) = self.tolerance {
675 if !v.is_finite() || v <= 0.0 {
676 return Err(OptionsError {
677 field: "tolerance.Custom",
678 reason: "must be finite and > 0",
679 });
680 }
681 }
682 self.ipm.validate()?;
683 Ok(())
684 }
685
686 pub fn with_timeout(mut self, secs: f64) -> Result<Self, OptionsError> {
688 if !secs.is_finite() || secs < 0.0 {
689 return Err(OptionsError {
690 field: "timeout_secs",
691 reason: "must be finite and >= 0",
692 });
693 }
694 self.timeout_secs = Some(secs);
695 Ok(self)
696 }
697
698 pub fn with_threads(mut self, n: usize) -> Result<Self, OptionsError> {
700 if n == 0 {
701 return Err(OptionsError {
702 field: "threads",
703 reason: "must be >= 1",
704 });
705 }
706 self.threads = n;
707 Ok(self)
708 }
709
710 pub fn with_tolerance(mut self, tol: Tolerance) -> Result<Self, OptionsError> {
715 if let Tolerance::Custom(v) = tol {
716 if !v.is_finite() || v <= 0.0 {
717 return Err(OptionsError {
718 field: "tolerance.Custom",
719 reason: "must be finite and > 0",
720 });
721 }
722 }
723 self.tolerance = Some(tol);
724 Ok(self)
725 }
726}
727
728#[cfg(test)]
729mod tests {
730 use super::*;
731
732 #[test]
735 fn test_tolerance_translation() {
736 let cases: &[(Option<Tolerance>, f64)] = &[
738 (Some(Tolerance::High), TOLERANCE_HIGH_EPS),
739 (Some(Tolerance::Medium), TOLERANCE_MEDIUM_EPS),
740 (Some(Tolerance::Fast), TOLERANCE_FAST_EPS),
741 (Some(Tolerance::Custom(1e-5)), 1e-5),
742 (None, DEFAULT_IPM_EPS), ];
744 for (tol, expected) in cases {
745 let opts = SolverOptions {
746 tolerance: *tol,
747 ..Default::default()
748 };
749 assert_eq!(opts.ipm_eps(), *expected, "tolerance = {:?}", tol);
750 }
751 }
752
753 #[test]
754 #[allow(clippy::assertions_on_constants)]
755 fn test_tolerance_fast_is_looser_than_medium() {
756 const { assert!(TOLERANCE_FAST_EPS > TOLERANCE_MEDIUM_EPS) }
758 const { assert!(TOLERANCE_MEDIUM_EPS > TOLERANCE_HIGH_EPS) }
759 }
760
761 #[test]
764 fn test_ipm_validate_defaults_ok() {
765 assert!(IpmOptions::default().validate().is_ok());
766 }
767
768 #[test]
769 fn test_ipm_validate_eps() {
770 for bad in [0.0_f64, -1e-6, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
771 let o = IpmOptions {
772 eps: bad,
773 ..Default::default()
774 };
775 assert!(o.validate().is_err(), "eps={bad} should be invalid");
776 }
777 let o = IpmOptions {
779 eps: f64::MIN_POSITIVE,
780 ..Default::default()
781 };
782 assert!(o.validate().is_ok());
783 }
784
785 #[test]
786 fn test_ipm_validate_delta_min() {
787 for bad in [0.0_f64, -1.0, f64::NAN, f64::INFINITY] {
788 let o = IpmOptions {
789 delta_min: bad,
790 ..Default::default()
791 };
792 assert!(o.validate().is_err(), "delta_min={bad} should be invalid");
793 }
794 }
795
796 #[test]
797 fn test_ipm_validate_delta_p_init() {
798 for bad in [0.0_f64, -1.0, f64::NAN, f64::INFINITY] {
799 let o = IpmOptions {
800 delta_p_init: bad,
801 ..Default::default()
802 };
803 assert!(
804 o.validate().is_err(),
805 "delta_p_init={bad} should be invalid"
806 );
807 }
808 }
809
810 #[test]
811 fn test_ipm_validate_delta_d_init() {
812 for bad in [0.0_f64, -1.0, f64::NAN, f64::INFINITY] {
813 let o = IpmOptions {
814 delta_d_init: bad,
815 ..Default::default()
816 };
817 assert!(
818 o.validate().is_err(),
819 "delta_d_init={bad} should be invalid"
820 );
821 }
822 }
823
824 #[test]
825 fn test_ipm_validate_max_correctors() {
826 let o = IpmOptions {
827 max_correctors: 0,
828 ..Default::default()
829 };
830 assert!(o.validate().is_err(), "max_correctors=0 should be invalid");
831 let o = IpmOptions {
832 max_correctors: 1,
833 ..Default::default()
834 };
835 assert!(o.validate().is_ok());
836 }
837
838 #[test]
841 fn test_ipm_builder_with_eps() {
842 assert!(IpmOptions::default().with_eps(1e-4).is_ok());
843 assert!(IpmOptions::default().with_eps(f64::MIN_POSITIVE).is_ok());
844 for bad in [0.0_f64, -1.0, f64::NAN, f64::INFINITY] {
845 assert!(
846 IpmOptions::default().with_eps(bad).is_err(),
847 "with_eps({bad}) should err"
848 );
849 }
850 }
851
852 #[test]
853 fn test_ipm_builder_with_max_correctors() {
854 assert!(IpmOptions::default().with_max_correctors(1).is_ok());
855 assert!(IpmOptions::default().with_max_correctors(10).is_ok());
856 assert!(IpmOptions::default().with_max_correctors(0).is_err());
857 }
858
859 #[test]
862 fn test_solver_validate_defaults_ok() {
863 assert!(SolverOptions::default().validate().is_ok());
864 }
865
866 #[test]
867 fn test_solver_validate_primal_tol() {
868 for bad in [0.0_f64, -1e-8, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
869 let o = SolverOptions {
870 primal_tol: bad,
871 ..Default::default()
872 };
873 assert!(o.validate().is_err(), "primal_tol={bad}");
874 }
875 let o = SolverOptions {
876 primal_tol: f64::MIN_POSITIVE,
877 ..Default::default()
878 };
879 assert!(o.validate().is_ok());
880 }
881
882 #[test]
883 fn test_solver_validate_dual_tol() {
884 for bad in [0.0_f64, -1e-8, f64::NAN, f64::INFINITY] {
885 let o = SolverOptions {
886 dual_tol: bad,
887 ..Default::default()
888 };
889 assert!(o.validate().is_err(), "dual_tol={bad}");
890 }
891 }
892
893 #[test]
894 fn test_solver_validate_clamp_tol() {
895 let o = SolverOptions {
897 clamp_tol: 0.0,
898 ..Default::default()
899 };
900 assert!(o.validate().is_ok(), "clamp_tol=0 should be ok");
901 for bad in [-1.0_f64, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
902 let o = SolverOptions {
903 clamp_tol: bad,
904 ..Default::default()
905 };
906 assert!(o.validate().is_err(), "clamp_tol={bad}");
907 }
908 }
909
910 #[test]
911 fn test_solver_validate_threads() {
912 let o = SolverOptions {
913 threads: 0,
914 ..Default::default()
915 };
916 assert!(o.validate().is_err(), "threads=0");
917 for ok in [1_usize, 2, 8, usize::MAX] {
918 let o = SolverOptions {
919 threads: ok,
920 ..Default::default()
921 };
922 assert!(o.validate().is_ok(), "threads={ok}");
923 }
924 }
925
926 #[test]
927 fn test_solver_validate_timeout_secs() {
928 assert!(SolverOptions {
930 timeout_secs: None,
931 ..Default::default()
932 }
933 .validate()
934 .is_ok());
935 for ok in [0.0_f64, 0.001, 1.0, 1000.0] {
937 let o = SolverOptions {
938 timeout_secs: Some(ok),
939 ..Default::default()
940 };
941 assert!(
942 o.validate().is_ok(),
943 "timeout_secs=Some({ok}) must be valid"
944 );
945 }
946 for bad in [-1.0_f64, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
948 let o = SolverOptions {
949 timeout_secs: Some(bad),
950 ..Default::default()
951 };
952 assert!(o.validate().is_err(), "timeout_secs=Some({bad})");
953 }
954 }
955
956 #[test]
957 fn test_solver_validate_tolerance_custom() {
958 for tol in [Tolerance::High, Tolerance::Medium, Tolerance::Fast] {
960 let o = SolverOptions {
961 tolerance: Some(tol),
962 ..Default::default()
963 };
964 assert!(o.validate().is_ok(), "tolerance={tol:?}");
965 }
966 let o = SolverOptions {
968 tolerance: Some(Tolerance::Custom(1e-5)),
969 ..Default::default()
970 };
971 assert!(o.validate().is_ok());
972 for bad in [0.0_f64, -1e-4, f64::NAN, f64::INFINITY] {
974 let o = SolverOptions {
975 tolerance: Some(Tolerance::Custom(bad)),
976 ..Default::default()
977 };
978 assert!(o.validate().is_err(), "Tolerance::Custom({bad})");
979 }
980 }
981
982 #[test]
983 fn test_solver_validate_propagates_ipm() {
984 let o = SolverOptions {
986 ipm: IpmOptions {
987 eps: 0.0,
988 ..Default::default()
989 },
990 ..Default::default()
991 };
992 assert!(o.validate().is_err(), "ipm.eps=0 must propagate");
993
994 let o = SolverOptions {
995 ipm: IpmOptions {
996 max_correctors: 0,
997 ..Default::default()
998 },
999 ..Default::default()
1000 };
1001 assert!(o.validate().is_err(), "ipm.max_correctors=0 must propagate");
1002 }
1003
1004 #[test]
1007 fn test_solver_builder_with_timeout() {
1008 assert!(SolverOptions::default().with_timeout(10.0).is_ok());
1009 assert!(SolverOptions::default().with_timeout(0.001).is_ok());
1010 assert!(
1011 SolverOptions::default().with_timeout(0.0).is_ok(),
1012 "0.0 = immediately-expired deadline"
1013 );
1014 for bad in [-1.0_f64, f64::NAN, f64::INFINITY] {
1015 assert!(
1016 SolverOptions::default().with_timeout(bad).is_err(),
1017 "with_timeout({bad})"
1018 );
1019 }
1020 let o = SolverOptions::default().with_timeout(5.0).unwrap();
1022 assert_eq!(o.timeout_secs, Some(5.0));
1023 }
1024
1025 #[test]
1026 fn test_solver_builder_with_threads() {
1027 assert!(SolverOptions::default().with_threads(1).is_ok());
1028 assert!(SolverOptions::default().with_threads(8).is_ok());
1029 assert!(SolverOptions::default().with_threads(0).is_err());
1030 let o = SolverOptions::default().with_threads(4).unwrap();
1031 assert_eq!(o.threads, 4);
1032 }
1033
1034 #[test]
1035 fn test_solver_builder_with_tolerance() {
1036 assert!(SolverOptions::default()
1037 .with_tolerance(Tolerance::High)
1038 .is_ok());
1039 assert!(SolverOptions::default()
1040 .with_tolerance(Tolerance::Medium)
1041 .is_ok());
1042 assert!(SolverOptions::default()
1043 .with_tolerance(Tolerance::Fast)
1044 .is_ok());
1045 assert!(SolverOptions::default()
1046 .with_tolerance(Tolerance::Custom(1e-5))
1047 .is_ok());
1048 for bad in [0.0_f64, -1e-4, f64::NAN, f64::INFINITY] {
1049 assert!(
1050 SolverOptions::default()
1051 .with_tolerance(Tolerance::Custom(bad))
1052 .is_err(),
1053 "with_tolerance(Custom({bad}))"
1054 );
1055 }
1056 let o = SolverOptions::default()
1057 .with_tolerance(Tolerance::Fast)
1058 .unwrap();
1059 assert_eq!(o.tolerance, Some(Tolerance::Fast));
1060 }
1061
1062 #[test]
1065 fn test_options_error_display() {
1066 let e = OptionsError {
1067 field: "ipm.eps",
1068 reason: "must be finite and > 0",
1069 };
1070 let s = e.to_string();
1071 assert!(s.contains("ipm.eps"), "display: {s}");
1072 assert!(s.contains("finite"), "display: {s}");
1073 }
1074
1075 #[test]
1078 fn test_ipm_new_fields_default() {
1079 let o = IpmOptions::default();
1080 assert!(!o.dd_ldl, "dd_ldl default false");
1081 assert!(o.minres_ir.is_none(), "minres_ir default None");
1082 assert!(
1083 o.kkt_memory_budget_bytes.is_none(),
1084 "kkt_memory_budget_bytes default None"
1085 );
1086 }
1087
1088 #[test]
1089 fn test_ipm_effective_minres_ir_default_and_override() {
1090 let o = IpmOptions::default();
1091 assert_eq!(o.effective_minres_ir(), 0, "default IR = 0");
1092 let o2 = IpmOptions {
1093 minres_ir: Some(3),
1094 ..Default::default()
1095 };
1096 assert_eq!(o2.effective_minres_ir(), 3);
1097 }
1098
1099 #[test]
1100 fn test_ipm_validate_minres_ir() {
1101 assert!(IpmOptions::default().validate().is_ok());
1103 for ok in [0_usize, 1, 5, 10] {
1104 let o = IpmOptions {
1105 minres_ir: Some(ok),
1106 ..Default::default()
1107 };
1108 assert!(o.validate().is_ok(), "minres_ir={ok} should be valid");
1109 }
1110 for bad in [11_usize, 100, usize::MAX] {
1112 let o = IpmOptions {
1113 minres_ir: Some(bad),
1114 ..Default::default()
1115 };
1116 assert!(o.validate().is_err(), "minres_ir={bad} should be invalid");
1117 }
1118 }
1120
1121 #[test]
1122 fn test_ipm_effective_max_l_nnz_default_and_override() {
1123 use crate::linalg::kkt_solver::{BYTES_PER_L_ENTRY, DEFAULT_MEMORY_BUDGET_BYTES};
1124 let o = IpmOptions::default();
1125 assert_eq!(
1126 o.effective_kkt_memory_budget_bytes(),
1127 DEFAULT_MEMORY_BUDGET_BYTES
1128 );
1129 assert_eq!(
1130 o.effective_max_l_nnz(),
1131 DEFAULT_MEMORY_BUDGET_BYTES / BYTES_PER_L_ENTRY
1132 );
1133 let o2 = IpmOptions {
1134 kkt_memory_budget_bytes: Some(1600),
1135 ..Default::default()
1136 };
1137 assert_eq!(o2.effective_max_l_nnz(), 1600 / BYTES_PER_L_ENTRY);
1138 }
1139
1140 #[test]
1143 fn test_solver_presolve_fields_default() {
1144 let o = SolverOptions::default();
1145 assert_eq!(
1146 o.presolve_max_pass, DEFAULT_PRESOLVE_MAX_PASS,
1147 "default max pass"
1148 );
1149 assert!(o.presolve_phase2, "default phase2 = true");
1150 }
1151
1152 #[test]
1153 fn test_presolve_max_pass_controls_iteration_count() {
1154 use crate::problem::SolveStatus;
1155 use crate::qp::{solve_qp_with, QpProblem};
1156 use crate::sparse::CscMatrix;
1157
1158 let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
1160 let a = CscMatrix::new(0, 1);
1161 let prob =
1162 QpProblem::new(q, vec![0.0], a, vec![], vec![(0.0_f64, 1.0_f64)], vec![]).unwrap();
1163
1164 let opts0 = SolverOptions {
1166 presolve_max_pass: 0,
1167 ..Default::default()
1168 };
1169 let opts10 = SolverOptions {
1170 presolve_max_pass: 10,
1171 ..Default::default()
1172 };
1173 let r0 = solve_qp_with(&prob, &opts0);
1174 let r10 = solve_qp_with(&prob, &opts10);
1175 assert_eq!(
1176 r0.status,
1177 SolveStatus::Optimal,
1178 "presolve_max_pass=0 should still solve trivial QP"
1179 );
1180 assert_eq!(
1181 r10.status,
1182 SolveStatus::Optimal,
1183 "presolve_max_pass=10 should solve trivial QP"
1184 );
1185 }
1186
1187 #[test]
1188 fn test_presolve_phase2_false_skips_phase2() {
1189 let o = SolverOptions {
1192 presolve_phase2: false,
1193 ..Default::default()
1194 };
1195 assert!(!o.presolve_phase2);
1196 let o2 = SolverOptions {
1197 presolve_phase2: true,
1198 ..Default::default()
1199 };
1200 assert!(o2.presolve_phase2);
1201 }
1202}