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(
6    clippy::needless_range_loop,
7    clippy::too_many_arguments,
8)]
9#![deny(clippy::print_stdout, clippy::print_stderr)]
10
11//! # otspot — 数理最適化ソルバー
12//!
13//! 線形計画法(LP)・二次計画法(QP)と混合整数問題(MILP / MIQP)を解く Rust ソルバークレート。
14//! LP は改訂単体法(Revised Simplex)、QP は内点法(IPM / IP-PMM)を核とし、
15//! 実行不可能・非有界の判定と完全な主双対情報の出力に対応する。
16//!
17//! ## 主要モジュール
18//!
19//! | モジュール | 役割 |
20//! |-----------|------|
21//! | [`sparse`] | CSC 形式の疎行列・疎ベクトル演算 |
22//! | [`problem`] | 問題定義(`LpProblem` / `QpProblem`、`SolveStatus`、`SolverResult`) |
23//! | [`lp`] | LP 求解エントリポイント(`solve_lp_with`) |
24//! | [`qp`] | 内点法ソルバー(QP、IPM / IP-PMM) |
25//! | [`mip`] | 混合整数ソルバー(MILP / MIQP、branch-and-bound) |
26//! | [`options`] | `SolverOptions`、`Tolerance` |
27//!
28//! ## 使用例
29//!
30//! MPS ファイルから LP 問題を読み込んで解く (via the `otspot` facade):
31//!
32//! ```rust,ignore
33//! use std::path::Path;
34//! use otspot::io::mps;
35//!
36//! let prob = mps::parse_mps_file(Path::new("problem.mps")).expect("MPS読み込み失敗");
37//! let result = otspot_core::solve(&prob);
38//! println!("最適値: {:?}", result);
39//! ```
40
41pub mod error;
42pub use error::SolverError;
43pub use error::MpsError;
44#[doc(hidden)]
45pub mod presolve;
46pub mod sparse;
47pub mod problem;
48pub(crate) mod simplex;
49// Internal parsers compiled only under cfg(test), used by otspot-core's own
50// integration-style tests (e.g. qp::ipm_solver diagnostics). These are
51// source-duplicates of otspot-io's canonical, published parsers and are not
52// part of the production library. Full removal is tracked separately (the
53// ipm_solver tests depend on crate-internal IPM diagnostics).
54#[cfg(test)]
55#[allow(dead_code)]
56pub(crate) mod io;
57pub(crate) mod basis;
58pub mod tolerances;
59pub mod options;
60pub use options::{
61    BranchingStrategy, DualPricing, GlobalOptimizationConfig, LpWarmStart, MipBranching, MipConfig,
62    SolverOptions, Tolerance, WarmStartBasis,
63};
64pub mod qp;
65pub mod mip;
66pub mod lp;
67#[doc(hidden)]
68pub mod linalg;
69
70#[cfg(test)]
71pub(crate) mod test_kkt;
72
73/// Thread-local peak-allocation tracker for memory sentinel tests.
74///
75/// Wraps the system allocator and records per-thread net live bytes.
76/// `TrackingAlloc` is wired as the `#[global_allocator]` in test builds so
77/// that any future sentinel test can read allocation deltas via `update`.
78#[cfg(test)]
79pub(crate) mod peak_alloc {
80    use std::alloc::{GlobalAlloc, Layout, System};
81    use std::cell::Cell;
82
83    thread_local! {
84        static CURRENT: Cell<isize> = const { Cell::new(0) };
85    }
86
87    #[inline]
88    fn update(delta: isize) {
89        CURRENT.with(|c| c.set(c.get() + delta));
90    }
91
92    pub struct TrackingAlloc;
93
94    unsafe impl GlobalAlloc for TrackingAlloc {
95        unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
96            let ptr = System.alloc(layout);
97            if !ptr.is_null() {
98                update(layout.size() as isize);
99            }
100            ptr
101        }
102        unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
103            let ptr = System.alloc_zeroed(layout);
104            if !ptr.is_null() {
105                update(layout.size() as isize);
106            }
107            ptr
108        }
109        unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
110            System.dealloc(ptr, layout);
111            update(-(layout.size() as isize));
112        }
113        unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
114            let new_ptr = System.realloc(ptr, layout, new_size);
115            if !new_ptr.is_null() {
116                update(new_size as isize - layout.size() as isize);
117            }
118            new_ptr
119        }
120    }
121}
122
123#[cfg(test)]
124#[global_allocator]
125static TEST_ALLOC: peak_alloc::TrackingAlloc = peak_alloc::TrackingAlloc;
126
127// --- re-export: ユーザーが最も使う型を最短パスで ---
128pub use sparse::CscMatrix;
129pub use problem::{SolveRoute, SolveStats, SolveStatus, SolverResult};
130pub use problem::certificate::{BoundGapCertificate, NotProven, OptimalCertificate};
131pub use qp::certificate::prove_optimal;
132pub use qp::{solve_qp, solve_qp_global, solve_qp_with, QpProblem, QpWarmStart};
133pub use mip::{
134    solve_milp, solve_milp_with_stats, solve_miqp, solve_miqp_with_stats, MilpProblem,
135    MipProblemError, MipStats, MiqpProblem,
136};
137pub use lp::solve_lp_with;
138pub use simplex::{solve, solve_with};
139
140/// Internal BFRT (Bound-Flipping Ratio Test) primitives for integration tests.
141/// Deferred for removal until typed pipeline restructures the simplex tree.
142#[doc(hidden)]
143pub mod bound_flip {
144    pub use crate::simplex::dual_advanced::bound_flip::{
145        bfrt_flip_invocations, bfrt_select_entering, reset_bfrt_flip_invocations,
146        BfrtResult, ColBound,
147    };
148}
149
150/// RAII guard that disables a production sentinel for the duration of its lifetime.
151///
152/// On construction: calls `enable` to disable the sentinel.
153/// On drop: calls `restore` to re-enable the sentinel.
154/// Panic-safe: `restore` runs even if the guarded closure panics.
155#[cfg(test)]
156pub(crate) struct ScopedDisable<D: Fn()> {
157    restore: D,
158}
159
160#[cfg(test)]
161impl<D: Fn()> ScopedDisable<D> {
162    pub(crate) fn new<E: Fn()>(enable: E, restore: D) -> Self {
163        enable();
164        ScopedDisable { restore }
165    }
166}
167
168#[cfg(test)]
169impl<D: Fn()> Drop for ScopedDisable<D> {
170    fn drop(&mut self) {
171        (self.restore)();
172    }
173}
174
175/// Apply the LP KKT optimality guard to a solver result.
176///
177/// Exposed for integration-test sentinel load-bearing proofs. Runs full
178/// KKT+dual_sign verification via `prove_optimal_lp`; demotes false-Optimal
179/// to `SuboptimalSolution`. Non-Optimal results pass through unchanged.
180#[doc(hidden)]
181pub fn apply_lp_primal_guard(
182    result: crate::problem::SolverResult,
183    problem: &crate::problem::LpProblem,
184) -> crate::problem::SolverResult {
185    crate::qp::certificate::guard_lp_optimal(result, problem)
186}