cobre_solver/profile.rs
1//! LP-solver configuration profiles.
2//!
3//! A [`SolveProfile`] is a set of LP-solver tuning values that callers swap in
4//! at phase boundaries via `ProfiledSolver::set_profile`. All fields are
5//! absolute — not relative to defaults. Two profiles can be compared with `==`
6//! to decide whether any FFI calls are needed.
7
8/// LP-solver configuration values applied at the start of each phase.
9///
10/// `SolveProfile` is a struct of tuning values that callers swap in via
11/// `ProfiledSolver::set_profile` at phase boundaries. The profile defines
12/// the configuration of the *default* solve attempt and the per-attempt
13/// iteration cap; the retry ladder layers additional behavior on top.
14///
15/// All fields are absolute, not relative to defaults. Callers construct
16/// profiles either via `Default::default()` (matching the historical
17/// hardcoded behavior) or as `const` values for compile-time profile
18/// libraries.
19///
20/// `SolveProfile` is `Copy` and `PartialEq` so the caller can compare the
21/// desired profile against the currently-applied profile and skip FFI calls
22/// when nothing has changed.
23#[derive(Debug, Clone, Copy, PartialEq)]
24pub struct SolveProfile {
25 /// Primal feasibility tolerance used by the default solve attempt.
26 ///
27 /// Smaller values are stricter. The retry ladder applies
28 /// `max(profile_value, level_default)` when it overrides this field, so a
29 /// strict profile is never silently relaxed by an early retry level.
30 pub primal_feasibility_tolerance: f64,
31
32 /// Dual feasibility tolerance used by the default solve attempt.
33 ///
34 /// Same composition rules as `primal_feasibility_tolerance`.
35 pub dual_feasibility_tolerance: f64,
36
37 /// Per-attempt simplex iteration cap, applied to the default attempt and
38 /// every retry level.
39 ///
40 /// A value of [`DEFAULT_PROFILE_HEURISTIC_SENTINEL`] (`0`) signals that
41 /// the solver should fall back to its historical heuristic
42 /// (`num_cols * 50 max 100_000` for `HighsSolver`). Any non-zero value is
43 /// applied verbatim.
44 ///
45 /// Tighter values cause attempts to bail to the next retry strategy faster,
46 /// bounding worst-case wall time per attempt at the cost of occasional
47 /// escalation. Correctness is preserved as long as some retry level
48 /// eventually solves the LP.
49 pub simplex_iteration_limit: u32,
50
51 /// Per-attempt IPM iteration cap.
52 ///
53 /// Applies to retry levels that invoke the interior-point solver. A value
54 /// of `0` is treated as "unbounded" (no cap). Any positive value is applied
55 /// verbatim.
56 pub ipm_iteration_limit: u32,
57}
58
59impl Default for SolveProfile {
60 /// Returns defaults that match the historical hardcoded behavior bit-for-bit,
61 /// so that callers that never touch profiles see no behavioral change.
62 ///
63 /// | Field | Value |
64 /// |--------------------------------|------------------------------------|
65 /// | `primal_feasibility_tolerance` | `1e-9` |
66 /// | `dual_feasibility_tolerance` | `1e-9` |
67 /// | `simplex_iteration_limit` | `0` (use heuristic — see sentinel) |
68 /// | `ipm_iteration_limit` | `10_000` |
69 ///
70 /// The sentinel value `0` for `simplex_iteration_limit` causes `HighsSolver`
71 /// to compute the historical per-call heuristic `num_cols * 50 max 100_000`
72 /// rather than applying a flat cap.
73 fn default() -> Self {
74 Self {
75 primal_feasibility_tolerance: 1e-9,
76 dual_feasibility_tolerance: 1e-9,
77 simplex_iteration_limit: DEFAULT_PROFILE_HEURISTIC_SENTINEL,
78 ipm_iteration_limit: 10_000,
79 }
80 }
81}
82
83/// Sentinel `u32` value indicating "use the historical heuristic
84/// `num_cols * 50 max 100_000`" for the simplex iteration limit.
85///
86/// When [`SolveProfile::simplex_iteration_limit`] equals this value, solver
87/// implementations MUST fall back to their per-call heuristic rather than
88/// applying a flat iteration cap. Any non-zero value is applied verbatim as
89/// the cap.
90///
91/// `0` is chosen as the sentinel because zero iterations is nonsensical for
92/// any LP solver; legitimate iteration caps are always positive.
93pub const DEFAULT_PROFILE_HEURISTIC_SENTINEL: u32 = 0;
94
95/// Sentinel `u32` value indicating "unbounded" for the IPM iteration limit.
96///
97/// When [`SolveProfile::ipm_iteration_limit`] equals this value, solver
98/// implementations MUST apply no cap (i.e., pass `i32::MAX` to the
99/// underlying solver). Any positive value is applied verbatim as the cap.
100///
101/// `0` is chosen as the sentinel because zero IPM iterations is nonsensical;
102/// legitimate iteration caps are always positive.
103pub const DEFAULT_PROFILE_IPM_UNBOUNDED_SENTINEL: u32 = 0;
104
105#[cfg(test)]
106mod tests {
107 use super::{DEFAULT_PROFILE_HEURISTIC_SENTINEL, SolveProfile};
108
109 #[test]
110 fn default_matches_historical_values() {
111 let p = SolveProfile::default();
112 assert_eq!(p.primal_feasibility_tolerance, 1e-9);
113 assert_eq!(p.dual_feasibility_tolerance, 1e-9);
114 assert_eq!(p.simplex_iteration_limit, 0);
115 assert_eq!(p.ipm_iteration_limit, 10_000);
116 }
117
118 #[test]
119 #[allow(clippy::clone_on_copy, clippy::no_effect_underscore_binding)]
120 fn derived_traits_behave() {
121 let p = SolveProfile::default();
122
123 // Clone yields an equal value.
124 let cloned = p.clone();
125 assert_eq!(cloned, p);
126
127 // Equality is reflexive.
128 assert_eq!(p, p);
129
130 // Copy semantics: binding p to q does not move p.
131 let q = p;
132 let _r = p; // This line would not compile if SolveProfile were not Copy.
133 assert_eq!(q, p);
134
135 // Debug output contains all four field names.
136 let debug = format!("{p:?}");
137 for field in [
138 "primal_feasibility_tolerance",
139 "dual_feasibility_tolerance",
140 "simplex_iteration_limit",
141 "ipm_iteration_limit",
142 ] {
143 assert!(
144 debug.contains(field),
145 "debug output missing '{field}': {debug}"
146 );
147 }
148 }
149
150 /// Verify that `SolveProfile` can be used in a `const` context.
151 const _CONST_PROFILE: SolveProfile = SolveProfile {
152 primal_feasibility_tolerance: 1e-9,
153 dual_feasibility_tolerance: 1e-9,
154 simplex_iteration_limit: DEFAULT_PROFILE_HEURISTIC_SENTINEL,
155 ipm_iteration_limit: 10_000,
156 };
157}