Skip to main content

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}