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#[doc(hidden)]
35pub mod diag {
36    pub use crate::presolve::scaling::{
37        lp_scale_profile_enabled, lp_scale_profile_snapshot, reset_lp_scale_profile,
38        LpScaleProfileSnapshot,
39    };
40
41    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
42    pub struct SimplexFallbackSnapshot {
43        pub ub_violation_out_of_scope: u64,
44        pub phase1_bound_violation: u64,
45        pub crash_infeasible: u64,
46    }
47
48    pub fn reset_simplex_fallback_profile() {
49        crate::simplex::dual_advanced::reset_fallback_profile();
50    }
51
52    pub fn simplex_fallback_profile_snapshot() -> SimplexFallbackSnapshot {
53        let s = crate::simplex::dual_advanced::fallback_profile_snapshot();
54        SimplexFallbackSnapshot {
55            ub_violation_out_of_scope: s.ub_violation_out_of_scope,
56            phase1_bound_violation: s.phase1_bound_violation,
57            crash_infeasible: s.crash_infeasible,
58        }
59    }
60}
61
62#[cfg(test)]
63pub(crate) mod test_kkt;
64
65// --- re-export: ユーザーが最も使う型を最短パスで ---
66pub use lp::solve_lp_with;
67pub use mip::{
68    solve_milp, solve_milp_with_stats, solve_miqp, solve_miqp_with_stats, MilpProblem,
69    MipProblemError, MipStats, MiqpProblem,
70};
71pub use problem::certificate::{BoundGapCertificate, NotProven, OptimalCertificate};
72pub use problem::{SolveRoute, SolveStats, SolveStatus, SolverResult};
73pub use qp::certificate::prove_optimal;
74pub use qp::{solve_qp, solve_qp_global, solve_qp_with, QpProblem, QpWarmStart};
75pub use sparse::CscMatrix;
76
77/// Solve an LP with default options. Includes `problem.obj_offset` in the returned objective.
78///
79/// Delegates to [`solve_lp_with`].
80pub fn solve(problem: &crate::problem::LpProblem) -> crate::problem::SolverResult {
81    lp::solve_lp_with(problem, &SolverOptions::default())
82}
83
84pub use lp::solve_lp_with as solve_with;
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use crate::problem::{ConstraintType, SolveStatus};
90    use crate::sparse::CscMatrix;
91
92    fn make_offset_lp(obj_offset: f64) -> crate::problem::LpProblem {
93        // min x  s.t. x <= 5,  x >= 0;  optimal x* = 0, c^T x* = 0
94        let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
95        let mut lp = crate::problem::LpProblem::new_general(
96            vec![1.0],
97            a,
98            vec![5.0],
99            vec![ConstraintType::Le],
100            vec![(0.0, f64::INFINITY)],
101            None,
102        )
103        .unwrap();
104        lp.obj_offset = obj_offset;
105        lp
106    }
107
108    /// `solve` and `solve_with` must include `obj_offset` in the returned objective.
109    ///
110    /// Sentinel: removing `result.objective += problem.obj_offset` from
111    /// `lp::solve_lp_with` causes `result.objective == 0.0` instead of 5.0 → FAIL.
112    #[test]
113    fn test_legacy_lp_exports_apply_obj_offset() {
114        let lp = make_offset_lp(5.0);
115
116        let r1 = solve(&lp);
117        assert_eq!(r1.status, SolveStatus::Optimal);
118        assert!(
119            (r1.objective - 5.0).abs() < 1e-9,
120            "solve: expected 5.0 (c^Tx=0 + offset 5), got {}",
121            r1.objective
122        );
123
124        let r2 = solve_with(&lp, &SolverOptions::default());
125        assert_eq!(r2.status, SolveStatus::Optimal);
126        assert!(
127            (r2.objective - 5.0).abs() < 1e-9,
128            "solve_with: expected 5.0 (c^Tx=0 + offset 5), got {}",
129            r2.objective
130        );
131    }
132}
133
134/// Internal BFRT (Bound-Flipping Ratio Test) primitives for integration tests.
135/// Deferred for removal until typed pipeline restructures the simplex tree.
136#[doc(hidden)]
137pub mod bound_flip {
138    pub use crate::simplex::dual_advanced::bound_flip::{
139        bfrt_flip_invocations, bfrt_select_entering, reset_bfrt_flip_invocations, BfrtResult,
140        ColBound,
141    };
142}
143
144/// RAII guard that disables a production sentinel for the duration of its lifetime.
145///
146/// On construction: calls `enable` to disable the sentinel.
147/// On drop: calls `restore` to re-enable the sentinel.
148/// Panic-safe: `restore` runs even if the guarded closure panics.
149#[cfg(test)]
150pub(crate) struct ScopedDisable<D: Fn()> {
151    restore: D,
152}
153
154#[cfg(test)]
155impl<D: Fn()> ScopedDisable<D> {
156    pub(crate) fn new<E: Fn()>(enable: E, restore: D) -> Self {
157        enable();
158        ScopedDisable { restore }
159    }
160}
161
162#[cfg(test)]
163impl<D: Fn()> Drop for ScopedDisable<D> {
164    fn drop(&mut self) {
165        (self.restore)();
166    }
167}
168
169/// Apply the LP KKT optimality guard to a solver result.
170///
171/// Exposed for integration-test sentinel load-bearing proofs. Runs full
172/// KKT+dual_sign verification via `prove_optimal_lp`; demotes false-Optimal
173/// to `SuboptimalSolution`. Non-Optimal results pass through unchanged.
174#[doc(hidden)]
175pub fn apply_lp_primal_guard(
176    result: crate::problem::SolverResult,
177    problem: &crate::problem::LpProblem,
178) -> crate::problem::SolverResult {
179    crate::qp::certificate::guard_lp_optimal(result, problem)
180}