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}