Skip to main content

otspot_core/
lib.rs

1// Numerical solver code uses index loops over multiple arrays (a[i], b[i], c[i])
2// where iterator-based rewrites hurt readability or introduce borrow conflicts.
3// Solver and IPM functions legitimately accept many parameters; struct-wrapping
4// would be over-engineering for hot-path internals.
5#![allow(clippy::needless_range_loop, clippy::too_many_arguments)]
6#![deny(clippy::print_stdout, clippy::print_stderr)]
7
8//! otspot — LP / QP / MILP / MIQP ソルバー。
9//!
10//! LP は改訂単体法、QP は内点法 (IPM / IP-PMM) を核とし、実行不可能・非有界判定と
11//! 完全な主双対情報出力に対応する。
12
13pub mod error;
14pub use error::MpsError;
15pub use error::SolverError;
16pub(crate) mod basis;
17pub mod options;
18#[doc(hidden)]
19pub mod presolve;
20pub mod problem;
21pub(crate) mod simplex;
22pub mod sparse;
23pub mod tolerances;
24pub use options::{
25    BranchingStrategy, DualPricing, GlobalOptimizationConfig, LpWarmStart, MipBranching, MipConfig,
26    SolverOptions, Tolerance, WarmStartBasis,
27};
28#[doc(hidden)]
29pub mod linalg;
30pub mod lp;
31pub mod mip;
32pub mod qp;
33
34#[cfg(test)]
35pub(crate) mod test_kkt;
36
37// --- re-export: ユーザーが最も使う型を最短パスで ---
38pub use lp::solve_lp_with;
39pub use mip::{
40    solve_milp, solve_milp_with_stats, solve_miqp, solve_miqp_with_stats, MilpProblem,
41    MipProblemError, MipStats, MiqpProblem,
42};
43pub use problem::certificate::{BoundGapCertificate, NotProven, OptimalCertificate};
44pub use problem::{SolveRoute, SolveStats, SolveStatus, SolverResult};
45pub use qp::certificate::prove_optimal;
46pub use qp::{solve_qp, solve_qp_global, solve_qp_with, QpProblem, QpWarmStart};
47pub use sparse::CscMatrix;
48
49/// Solve an LP with default options. Includes `problem.obj_offset` in the returned objective.
50///
51/// Delegates to [`solve_lp_with`].
52pub fn solve(problem: &crate::problem::LpProblem) -> crate::problem::SolverResult {
53    lp::solve_lp_with(problem, &SolverOptions::default())
54}
55
56pub use lp::solve_lp_with as solve_with;
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::problem::{ConstraintType, SolveStatus};
62    use crate::sparse::CscMatrix;
63
64    fn make_offset_lp(obj_offset: f64) -> crate::problem::LpProblem {
65        // min x  s.t. x <= 5,  x >= 0;  optimal x* = 0, c^T x* = 0
66        let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
67        let mut lp = crate::problem::LpProblem::new_general(
68            vec![1.0],
69            a,
70            vec![5.0],
71            vec![ConstraintType::Le],
72            vec![(0.0, f64::INFINITY)],
73            None,
74        )
75        .unwrap();
76        lp.obj_offset = obj_offset;
77        lp
78    }
79
80    /// `solve` and `solve_with` must include `obj_offset` in the returned objective.
81    ///
82    /// Sentinel: removing `result.objective += problem.obj_offset` from
83    /// `lp::solve_lp_with` causes `result.objective == 0.0` instead of 5.0 → FAIL.
84    #[test]
85    fn test_legacy_lp_exports_apply_obj_offset() {
86        let lp = make_offset_lp(5.0);
87
88        let r1 = solve(&lp);
89        assert_eq!(r1.status, SolveStatus::Optimal);
90        assert!(
91            (r1.objective - 5.0).abs() < 1e-9,
92            "solve: expected 5.0 (c^Tx=0 + offset 5), got {}",
93            r1.objective
94        );
95
96        let r2 = solve_with(&lp, &SolverOptions::default());
97        assert_eq!(r2.status, SolveStatus::Optimal);
98        assert!(
99            (r2.objective - 5.0).abs() < 1e-9,
100            "solve_with: expected 5.0 (c^Tx=0 + offset 5), got {}",
101            r2.objective
102        );
103    }
104}
105
106/// Internal BFRT (Bound-Flipping Ratio Test) primitives for integration tests.
107/// Deferred for removal until typed pipeline restructures the simplex tree.
108#[doc(hidden)]
109pub mod bound_flip {
110    pub use crate::simplex::dual_advanced::bound_flip::{
111        bfrt_flip_invocations, bfrt_select_entering, reset_bfrt_flip_invocations, BfrtResult,
112        ColBound,
113    };
114}
115
116/// RAII guard that disables a production sentinel for the duration of its lifetime.
117///
118/// On construction: calls `enable` to disable the sentinel.
119/// On drop: calls `restore` to re-enable the sentinel.
120/// Panic-safe: `restore` runs even if the guarded closure panics.
121#[cfg(test)]
122pub(crate) struct ScopedDisable<D: Fn()> {
123    restore: D,
124}
125
126#[cfg(test)]
127impl<D: Fn()> ScopedDisable<D> {
128    pub(crate) fn new<E: Fn()>(enable: E, restore: D) -> Self {
129        enable();
130        ScopedDisable { restore }
131    }
132}
133
134#[cfg(test)]
135impl<D: Fn()> Drop for ScopedDisable<D> {
136    fn drop(&mut self) {
137        (self.restore)();
138    }
139}
140
141/// Apply the LP KKT optimality guard to a solver result.
142///
143/// Exposed for integration-test sentinel load-bearing proofs. Runs full
144/// KKT+dual_sign verification via `prove_optimal_lp`; demotes false-Optimal
145/// to `SuboptimalSolution`. Non-Optimal results pass through unchanged.
146#[doc(hidden)]
147pub fn apply_lp_primal_guard(
148    result: crate::problem::SolverResult,
149    problem: &crate::problem::LpProblem,
150) -> crate::problem::SolverResult {
151    crate::qp::certificate::guard_lp_optimal(result, problem)
152}