Skip to main content

cobre_solver/
lib.rs

1//! # cobre-solver
2//!
3//! LP/MIP solver abstraction for the [Cobre](https://github.com/cobre-rs/cobre) power systems ecosystem.
4//!
5//! This crate defines a backend-agnostic interface for mathematical programming
6//! solvers, with a default [HiGHS](https://highs.dev) backend:
7//!
8//! - **Solver trait**: unified API for LP and MIP problem construction, solving,
9//!   and dual/basis extraction.
10//! - **`HiGHS` backend** (`highs` feature, on by default): production-grade
11//!   open-source solver, well-suited for iterative LP solving in power system
12//!   optimization; exposed as `HighsSolver` and `HighsProfile`.
13//! - **`CLP` backend** (`clp` feature, off by default): optional CLP/`CoinUtils`
14//!   backend exposed as `ClpSolver` and `ClpProfile`; conformance-validated
15//!   as a drop-in implementing the same [`SolverInterface`]. Requires the Clp
16//!   and `CoinUtils` submodules to be initialized (`git submodule update --init
17//!   --recursive`) before the first build with `--features clp`.
18//! - **Basis management**: warm-starting support for iterative algorithms
19//!   that solve sequences of related LPs.
20//!
21//! ## Status
22//!
23//! This crate is in early development. The API **will** change.
24//!
25//! See the [repository](https://github.com/cobre-rs/cobre) for the current status.
26
27// Relax strict production lints for test builds. These lints (unwrap_used,
28// expect_used, etc.) guard library code but are normal in tests.
29#![cfg_attr(
30    test,
31    allow(
32        clippy::unwrap_used,
33        clippy::expect_used,
34        clippy::float_cmp,
35        clippy::panic,
36        clippy::too_many_lines
37    )
38)]
39
40// Exactly one LP backend must be selected at compile time. Enabling both
41// `highs` and `clp` is rejected with an actionable diagnostic naming the exact
42// CLP build command; selecting neither is rejected to avoid downstream
43// missing-symbol errors.
44#[cfg(all(feature = "highs", feature = "clp"))]
45compile_error!(
46    "enable exactly one LP backend: `highs` OR `clp`. \
47     To use CLP, build with `--no-default-features --features clp`."
48);
49
50#[cfg(not(any(feature = "highs", feature = "clp")))]
51compile_error!("no LP backend selected: enable exactly one of `highs` or `clp`.");
52
53pub mod ffi;
54
55#[cfg(feature = "clp")]
56pub use ffi::clp as clp_ffi;
57
58pub mod trait_def;
59pub use trait_def::SolverInterface;
60
61pub mod types;
62pub use types::{
63    Basis, LpSolution, RowBatch, SolutionView, SolverError, SolverStatistics, StageTemplate,
64};
65
66pub mod profile;
67pub use profile::{DEFAULT_PROFILE_HEURISTIC_SENTINEL, DEFAULT_PROFILE_IPM_UNBOUNDED_SENTINEL};
68
69pub mod baking;
70pub use baking::{BakingScratch, bake_rows_into_template};
71
72pub mod backends;
73pub use backends::profiled::ProfiledSolver;
74
75#[cfg(feature = "highs")]
76pub use backends::highs::{HighsProfile, HighsSolver, highs_version};
77
78#[cfg(feature = "clp")]
79pub use backends::clp::{ClpAlgorithm, ClpProfile, ClpSolver, clp_version};
80
81// Module-path shims preserving the pre-relocation public paths
82// `cobre_solver::highs` / `cobre_solver::clp` for downstream code that imports a
83// backend by module rather than through the curated re-exports above. Mirrors
84// the `clp_ffi` shim for the `ffi::clp` module.
85#[cfg(feature = "clp")]
86pub use backends::clp;
87#[cfg(feature = "highs")]
88pub use backends::highs;
89
90// Active backend selection (compile-time type alias).
91
92/// The compile-time-selected active LP solver backend.
93///
94/// Downstream crates reference this alias instead of a concrete backend type,
95/// so a single binary is bound to exactly one solver with zero runtime
96/// dispatch. Exactly one LP backend is enabled at a time — enabling both
97/// `highs` and `clp` is rejected at compile time (see the `compile_error!` in
98/// this module). This resolves to `ClpSolver` under `--features clp` and to
99/// `HighsSolver` under the default `highs` feature. When neither feature is
100/// enabled, this alias is not defined.
101#[cfg(feature = "clp")]
102pub type ActiveSolver = ClpSolver;
103
104/// The compile-time-selected active LP solver backend.
105///
106/// Downstream crates reference this alias instead of a concrete backend type,
107/// so a single binary is bound to exactly one solver with zero runtime
108/// dispatch. Exactly one LP backend is enabled at a time — enabling both
109/// `highs` and `clp` is rejected at compile time (see the `compile_error!` in
110/// this module). This resolves to `ClpSolver` under `--features clp` and to
111/// `HighsSolver` under the default `highs` feature. When neither feature is
112/// enabled, this alias is not defined.
113#[cfg(all(feature = "highs", not(feature = "clp")))]
114pub type ActiveSolver = HighsSolver;
115
116/// The solver profile type of the compile-time-selected active backend.
117///
118/// Resolved under the same mutually-exclusive backend contract as
119/// [`ActiveSolver`] (enabling both backends is a compile error): this is
120/// `ClpProfile` under `--features clp`, otherwise `HighsProfile` under the
121/// default `highs` feature. When neither feature is enabled, this alias is not
122/// defined. It is the same type as `<ActiveSolver as SolverInterface>::Profile`.
123#[cfg(feature = "clp")]
124pub type ActiveProfile = ClpProfile;
125
126/// The solver profile type of the compile-time-selected active backend.
127///
128/// Resolved under the same mutually-exclusive backend contract as
129/// [`ActiveSolver`] (enabling both backends is a compile error): this is
130/// `ClpProfile` under `--features clp`, otherwise `HighsProfile` under the
131/// default `highs` feature. When neither feature is enabled, this alias is not
132/// defined. It is the same type as `<ActiveSolver as SolverInterface>::Profile`.
133#[cfg(all(feature = "highs", not(feature = "clp")))]
134pub type ActiveProfile = HighsProfile;
135
136/// Returns the version string of the compile-time-selected active backend.
137///
138/// A single backend-agnostic entry point for metadata wiring. Resolved under
139/// the same mutually-exclusive backend contract as [`ActiveSolver`]: returns
140/// `clp_version`'s value when CLP is active, `highs_version`'s value when
141/// `HiGHS` is active.
142#[cfg(feature = "clp")]
143#[must_use]
144pub fn active_solver_version() -> String {
145    clp_version()
146}
147
148/// Returns the version string of the compile-time-selected active backend.
149///
150/// A single backend-agnostic entry point for metadata wiring. Resolved under
151/// the same mutually-exclusive backend contract as [`ActiveSolver`]: returns
152/// `clp_version`'s value when CLP is active, `highs_version`'s value when
153/// `HiGHS` is active.
154#[cfg(all(feature = "highs", not(feature = "clp")))]
155#[must_use]
156pub fn active_solver_version() -> String {
157    highs_version()
158}
159
160/// Returns the display name of the compile-time-selected active backend.
161///
162/// This is the same string the active backend's [`SolverInterface::name`]
163/// returns (`"CLP"` or `"HiGHS"`). Resolved under the same mutually-exclusive
164/// backend contract as [`ActiveSolver`].
165#[cfg(feature = "clp")]
166#[must_use]
167pub fn active_solver_name() -> &'static str {
168    "CLP"
169}
170
171/// Returns the display name of the compile-time-selected active backend.
172///
173/// This is the same string the active backend's [`SolverInterface::name`]
174/// returns (`"CLP"` or `"HiGHS"`). Resolved under the same mutually-exclusive
175/// backend contract as [`ActiveSolver`].
176#[cfg(all(feature = "highs", not(feature = "clp")))]
177#[must_use]
178pub fn active_solver_name() -> &'static str {
179    "HiGHS"
180}
181
182/// Returns the canonical lowercase backend id used in persisted output metadata
183/// (`OutputContext.solver`).
184///
185/// Unlike [`active_solver_name`] (the mixed-case display name `"CLP"`/`"HiGHS"`),
186/// this is the stable lowercase id recorded in output manifests. Resolved under
187/// the same mutually-exclusive backend contract as [`ActiveSolver`].
188#[cfg(feature = "clp")]
189#[must_use]
190pub fn active_solver_metadata_id() -> &'static str {
191    "clp"
192}
193
194/// Returns the canonical lowercase backend id used in persisted output metadata
195/// (`OutputContext.solver`).
196///
197/// Unlike [`active_solver_name`] (the mixed-case display name `"CLP"`/`"HiGHS"`),
198/// this is the stable lowercase id recorded in output manifests. Resolved under
199/// the same mutually-exclusive backend contract as [`ActiveSolver`].
200#[cfg(all(feature = "highs", not(feature = "clp")))]
201#[must_use]
202pub fn active_solver_metadata_id() -> &'static str {
203    "highs"
204}
205
206#[cfg(all(feature = "test-support", feature = "highs"))]
207pub mod test_support {
208    //! Test-only utilities for configuring solver options from integration tests.
209    //!
210    //! Do **not** enable this feature in production builds. The re-exported functions
211    //! call into the `HiGHS` C API directly and bypass all safe-wrapper validation,
212    //! so the module is HiGHS-only (gated on both `test-support` and `highs`).
213
214    pub use crate::ffi::{
215        cobre_highs_get_double_option, cobre_highs_get_int_option, cobre_highs_set_double_option,
216        cobre_highs_set_int_option, cobre_highs_set_string_option,
217    };
218}
219
220#[cfg(all(test, any(feature = "highs", feature = "clp")))]
221mod active_alias_tests {
222    use crate::{
223        ActiveProfile, ActiveSolver, SolverInterface, active_solver_metadata_id,
224        active_solver_name, active_solver_version,
225    };
226
227    /// `ActiveProfile` must be exactly the profile of the active solver.
228    #[allow(dead_code)]
229    const fn _assert_profile_identity(
230        p: ActiveProfile,
231    ) -> <ActiveSolver as SolverInterface>::Profile {
232        p
233    }
234
235    #[test]
236    fn active_aliases_resolve_per_feature() {
237        let name = active_solver_name();
238        let version = active_solver_version();
239        assert!(!version.is_empty(), "active version must be non-empty");
240        assert!(
241            version.contains('.'),
242            "active version `{version}` should contain a `.`"
243        );
244
245        // Exactly one backend is active (both-enabled is a compile error): the
246        // active backend is CLP under `--features clp`, else HiGHS by default.
247        #[cfg(feature = "clp")]
248        assert_eq!(name, "CLP");
249        #[cfg(all(feature = "highs", not(feature = "clp")))]
250        assert_eq!(name, "HiGHS");
251    }
252
253    #[test]
254    fn active_solver_name_matches_instance_name() {
255        let solver = ActiveSolver::new().expect("active solver must construct");
256        assert_eq!(solver.name(), active_solver_name());
257    }
258
259    /// HiGHS-only build (`--features highs`, no `clp`): the active backend
260    /// resolves to `HiGHS`. The both-on configuration cannot compile (a
261    /// `compile_error!` in `lib.rs` rejects it), so no runnable test is needed
262    /// for that contract.
263    #[cfg(all(feature = "highs", not(feature = "clp")))]
264    #[test]
265    fn active_backend_is_highs_when_only_highs_enabled() {
266        assert_eq!(active_solver_name(), "HiGHS");
267        assert_eq!(active_solver_metadata_id(), "highs");
268    }
269
270    /// CLP build (`--no-default-features --features clp`): the active backend
271    /// resolves to CLP.
272    #[cfg(feature = "clp")]
273    #[test]
274    fn active_backend_is_clp_when_clp_enabled() {
275        assert_eq!(active_solver_name(), "CLP");
276        assert_eq!(active_solver_metadata_id(), "clp");
277    }
278}