Skip to main content

sublinear_solver/
complexity.rs

1//! Complexity-class declarations for every public solver / sampler / analyser.
2//!
3//! See [ADR-001: Complexity as Architecture](../../docs/adr/ADR-001-complexity-as-architecture.md)
4//! for the strategic context. The short version: every public algorithm in this
5//! crate carries an explicit worst-case complexity class as a compile-time
6//! associated constant, so callers can refuse anything that exceeds their
7//! per-subsystem budget without reading the source.
8//!
9//! ```rust,no_run
10//! use sublinear_solver::complexity::{Complexity, ComplexityClass};
11//! use sublinear_solver::OptimizedConjugateGradientSolver;
12//!
13//! // Compile-time check: the optimised CG solver is linear-in-nonzeros per iter.
14//! const _: () = assert!(
15//!     matches!(<OptimizedConjugateGradientSolver as Complexity>::CLASS,
16//!              ComplexityClass::Linear)
17//! );
18//! ```
19//!
20//! Runtime introspection works the same way via the `ComplexityIntrospect`
21//! trait, which is object-safe so a `dyn ComplexityIntrospect` query works on
22//! any solver:
23//!
24//! ```rust,no_run
25//! # use sublinear_solver::complexity::{ComplexityIntrospect, ComplexityClass};
26//! fn budget_check(solver: &dyn ComplexityIntrospect, max: ComplexityClass) -> bool {
27//!     solver.complexity_class() <= max
28//! }
29//! ```
30
31use core::cmp::Ordering;
32
33/// The twelve-tier complexity taxonomy from the directive in
34/// [ADR-001](../../docs/adr/ADR-001-complexity-as-architecture.md).
35///
36/// Ordering is by asymptotic growth: `Logarithmic` is "easiest" / "cheapest",
37/// `DoubleExponential` is "hardest" / "most expensive". `PartialOrd` /
38/// `Ord` lift this into a usable budget comparison — `c <= max_budget`
39/// means *c is at most as expensive as the budget*, so a caller with a
40/// `SubLinear` budget will accept `Logarithmic` and `PolyLogarithmic` and
41/// reject anything stronger.
42///
43/// The single-parameter `Polynomial(degree)` is ranked by its degree;
44/// `Polynomial(2)` precedes `Polynomial(3)` etc. Adaptive solvers that
45/// degrade on hard inputs can use `Adaptive { default, worst }` so the
46/// caller sees both bounds.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48pub enum ComplexityClass {
49    /// `O(log n)` — binary search, HNSW layer traversal, sublinear-Neumann
50    /// single-entry query on a DD system.
51    Logarithmic,
52    /// `O((log n)^k)` — spectral sparsifiers, dynamic connectivity, live
53    /// graph repair. The "polylog" tier.
54    PolyLogarithmic,
55    /// `O(n^c), c < 1` — approximate nearest neighbour, sparse attention,
56    /// event-driven activation, anomaly detection.
57    SubLinear,
58    /// `O(n)` — one-pass streaming, ingest, WAL replay, sensor scan.
59    Linear,
60    /// `O(n log n)` — sorting, indexing, ANN build, graph compression.
61    /// The "practical sweet spot" for offline preprocessing.
62    QuasiLinear,
63    /// `O(n^{2-ε})` — sparsified mincut, sub-quadratic graph algorithms.
64    SubQuadratic,
65    /// `O(n^k)` — classical polynomial algorithms (matrix multiply for k=3,
66    /// dense linear solves for k=2.37+, etc.). Degree of the polynomial is
67    /// carried so callers can prefer lower-degree solvers.
68    Polynomial(u8),
69    /// Worse than any fixed polynomial but better than exponential.
70    /// Combinatorial enumeration with strong pruning lives here.
71    SuperPolynomial,
72    /// `2^{O(n^c)}, c < 1` — SAT solvers, advanced cryptography, optimisation.
73    SubExponential,
74    /// `O(2^n)` — brute-force search, exhaustive planning, unrestricted
75    /// combinatorics. Catastrophic at any non-trivial scale.
76    Exponential,
77    /// `O(n!)` — permutation search, exhaustive TSP. "Heat death of the
78    /// universe territory" at n ≥ 20.
79    Factorial,
80    /// `O(2^{2^n})` — formal systems, symbolic explosion. Theoretical only.
81    DoubleExponential,
82
83    /// Adaptive — a solver that runs at `default` on typical inputs but
84    /// can degrade to `worst` on adversarial inputs. Callers should budget
85    /// against `worst` to be safe.
86    Adaptive {
87        default: &'static ComplexityClass,
88        worst: &'static ComplexityClass,
89    },
90}
91
92impl ComplexityClass {
93    /// Rank suitable for ordering. Lower = cheaper. `Adaptive` ranks by its
94    /// `worst` bound so callers comparing against a budget see the safe
95    /// upper bound.
96    pub const fn rank(&self) -> u16 {
97        match self {
98            ComplexityClass::Logarithmic => 100,
99            ComplexityClass::PolyLogarithmic => 200,
100            ComplexityClass::SubLinear => 300,
101            ComplexityClass::Linear => 400,
102            ComplexityClass::QuasiLinear => 500,
103            ComplexityClass::SubQuadratic => 600,
104            // Polynomial: 700 + degree, so Polynomial(2) = 702, Polynomial(3) = 703, …
105            ComplexityClass::Polynomial(degree) => 700u16 + *degree as u16,
106            ComplexityClass::SuperPolynomial => 800,
107            ComplexityClass::SubExponential => 900,
108            ComplexityClass::Exponential => 1000,
109            ComplexityClass::Factorial => 1100,
110            ComplexityClass::DoubleExponential => 1200,
111            ComplexityClass::Adaptive { worst, .. } => worst.rank(),
112        }
113    }
114
115    /// Short human-readable label suitable for log lines and MCP tool schemas.
116    pub const fn short_label(&self) -> &'static str {
117        match self {
118            ComplexityClass::Logarithmic => "O(log n)",
119            ComplexityClass::PolyLogarithmic => "O((log n)^k)",
120            ComplexityClass::SubLinear => "O(n^c), c<1",
121            ComplexityClass::Linear => "O(n)",
122            ComplexityClass::QuasiLinear => "O(n log n)",
123            ComplexityClass::SubQuadratic => "O(n^{2-ε})",
124            ComplexityClass::Polynomial(2) => "O(n^2)",
125            ComplexityClass::Polynomial(3) => "O(n^3)",
126            ComplexityClass::Polynomial(_) => "O(n^k)",
127            ComplexityClass::SuperPolynomial => "superpoly",
128            ComplexityClass::SubExponential => "subexp",
129            ComplexityClass::Exponential => "O(2^n)",
130            ComplexityClass::Factorial => "O(n!)",
131            ComplexityClass::DoubleExponential => "O(2^{2^n})",
132            ComplexityClass::Adaptive { .. } => "adaptive",
133        }
134    }
135
136    /// True if this class is acceptable for a real-time / edge / always-on hot
137    /// path. Currently: anything strictly cheaper than `Linear`. Linear itself
138    /// is conditional (acceptable for one-pass streaming, not for per-query
139    /// work) — callers needing nuance should match directly.
140    pub const fn is_edge_safe(&self) -> bool {
141        self.rank() < ComplexityClass::Linear.rank()
142    }
143}
144
145impl PartialOrd for ComplexityClass {
146    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
147        Some(self.cmp(other))
148    }
149}
150
151impl Ord for ComplexityClass {
152    fn cmp(&self, other: &Self) -> Ordering {
153        self.rank().cmp(&other.rank())
154    }
155}
156
157/// Compile-time complexity-class declaration. Every public solver / sampler /
158/// analyser in this crate implements this trait, exposing its worst-case
159/// class as a `const` so callers can `match` on it at compile time.
160///
161/// ```rust,no_run
162/// # use sublinear_solver::complexity::{Complexity, ComplexityClass};
163/// fn pick_solver<S: Complexity>() {
164///     match S::CLASS {
165///         ComplexityClass::Logarithmic | ComplexityClass::SubLinear => {
166///             // use it — edge-safe
167///         }
168///         _ => {
169///             // fall back to something cheaper or refuse
170///         }
171///     }
172/// }
173/// ```
174pub trait Complexity {
175    /// Worst-case complexity class on a single-query call. For iterative
176    /// solvers this is the per-iter cost; the iteration count is bounded by
177    /// other configuration (max_iterations, tolerance, ef_construction).
178    const CLASS: ComplexityClass;
179
180    /// Optional human-readable detail for documentation / MCP tool schemas.
181    /// Defaults to the short label of `CLASS`. Override when there's a
182    /// non-obvious constant or k-bound.
183    const DETAIL: &'static str = "";
184}
185
186/// Object-safe runtime introspection. Defaulting `impl<T: Complexity>` so any
187/// type with a static `Complexity` impl gets `ComplexityIntrospect` for free,
188/// and a `dyn ComplexityIntrospect` query works on solver trait objects.
189///
190/// ```rust,no_run
191/// # use sublinear_solver::complexity::{ComplexityIntrospect, ComplexityClass};
192/// fn within_budget(s: &dyn ComplexityIntrospect, max: ComplexityClass) -> bool {
193///     s.complexity_class() <= max
194/// }
195/// ```
196pub trait ComplexityIntrospect {
197    fn complexity_class(&self) -> ComplexityClass;
198    fn complexity_detail(&self) -> &'static str {
199        ""
200    }
201}
202
203impl<T: Complexity> ComplexityIntrospect for T {
204    fn complexity_class(&self) -> ComplexityClass {
205        T::CLASS
206    }
207    fn complexity_detail(&self) -> &'static str {
208        T::DETAIL
209    }
210}
211
212// ─────────────────────────────────────────────────────────────────────────
213// Complexity impls for the headline solvers / samplers / embeddings.
214//
215// Each impl is one line and forces the implementer to think about which
216// class their algorithm actually inhabits. CI regression-guards in
217// `.github/workflows/ci.yml` will eventually diff these between PRs.
218// ─────────────────────────────────────────────────────────────────────────
219
220impl Complexity for crate::solver::neumann::NeumannSolver {
221    const CLASS: ComplexityClass = ComplexityClass::Linear;
222    const DETAIL: &'static str =
223        "O(k · nnz(A)) per iter; k bounded by series_tolerance + max_terms (default 200).";
224}
225
226impl Complexity for crate::optimized_solver::OptimizedConjugateGradientSolver {
227    const CLASS: ComplexityClass = ComplexityClass::Linear;
228    const DETAIL: &'static str = "O(k · nnz(A)) per iter; k ≈ √κ(A) on SPD inputs.";
229}
230
231impl Complexity for crate::sublinear::sublinear_neumann::SublinearNeumannSolver {
232    // Adaptive: O(log n) on the per-entry sublinear-guaranteed path,
233    // degrades to O(n) on the base-case path when n ≤ base_case_threshold.
234    const CLASS: ComplexityClass = ComplexityClass::Adaptive {
235        default: &ComplexityClass::Logarithmic,
236        worst: &ComplexityClass::Linear,
237    };
238    const DETAIL: &'static str =
239        "O(log n) per single-entry query on diagonally-dominant systems via JL + recursive Neumann; \
240         O(n) base case at n ≤ base_case_threshold.";
241}
242
243impl Complexity for crate::sublinear::johnson_lindenstrauss::JLEmbedding {
244    const CLASS: ComplexityClass = ComplexityClass::Linear;
245    const DETAIL: &'static str =
246        "O(d · k) per project_vector; k = target_dim, capped at original_dim - 1.";
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn ordering_is_by_asymptotic_growth() {
255        assert!(ComplexityClass::Logarithmic < ComplexityClass::PolyLogarithmic);
256        assert!(ComplexityClass::PolyLogarithmic < ComplexityClass::SubLinear);
257        assert!(ComplexityClass::SubLinear < ComplexityClass::Linear);
258        assert!(ComplexityClass::Linear < ComplexityClass::QuasiLinear);
259        assert!(ComplexityClass::QuasiLinear < ComplexityClass::SubQuadratic);
260        assert!(ComplexityClass::SubQuadratic < ComplexityClass::Polynomial(2));
261        assert!(ComplexityClass::Polynomial(2) < ComplexityClass::Polynomial(3));
262        assert!(ComplexityClass::Polynomial(3) < ComplexityClass::SuperPolynomial);
263        assert!(ComplexityClass::SuperPolynomial < ComplexityClass::SubExponential);
264        assert!(ComplexityClass::SubExponential < ComplexityClass::Exponential);
265        assert!(ComplexityClass::Exponential < ComplexityClass::Factorial);
266        assert!(ComplexityClass::Factorial < ComplexityClass::DoubleExponential);
267    }
268
269    #[test]
270    fn edge_safe_predicate_matches_adr() {
271        assert!(ComplexityClass::Logarithmic.is_edge_safe());
272        assert!(ComplexityClass::PolyLogarithmic.is_edge_safe());
273        assert!(ComplexityClass::SubLinear.is_edge_safe());
274        // Linear and above are conditional / not safe by default.
275        assert!(!ComplexityClass::Linear.is_edge_safe());
276        assert!(!ComplexityClass::QuasiLinear.is_edge_safe());
277        assert!(!ComplexityClass::Exponential.is_edge_safe());
278    }
279
280    #[test]
281    fn adaptive_ranks_by_worst_case() {
282        static QUASILIN: ComplexityClass = ComplexityClass::QuasiLinear;
283        static POLY3: ComplexityClass = ComplexityClass::Polynomial(3);
284        let adaptive = ComplexityClass::Adaptive {
285            default: &QUASILIN,
286            worst: &POLY3,
287        };
288        // adaptive is treated as expensive-as-worst-case for budget purposes.
289        assert!(adaptive > ComplexityClass::QuasiLinear);
290        assert_eq!(adaptive.rank(), POLY3.rank());
291    }
292
293    #[test]
294    fn short_label_format() {
295        assert_eq!(ComplexityClass::Logarithmic.short_label(), "O(log n)");
296        assert_eq!(ComplexityClass::Polynomial(2).short_label(), "O(n^2)");
297        assert_eq!(ComplexityClass::Polynomial(3).short_label(), "O(n^3)");
298        assert_eq!(ComplexityClass::Polynomial(4).short_label(), "O(n^k)");
299        assert_eq!(ComplexityClass::Exponential.short_label(), "O(2^n)");
300    }
301
302    /// Compile-time / runtime parity: a value that has `Complexity` also has
303    /// `ComplexityIntrospect`, and the two report the same class.
304    #[test]
305    fn introspect_blanket_impl_matches_const() {
306        struct Dummy;
307        impl Complexity for Dummy {
308            const CLASS: ComplexityClass = ComplexityClass::SubLinear;
309            const DETAIL: &'static str = "test dummy";
310        }
311        let d = Dummy;
312        assert_eq!(d.complexity_class(), ComplexityClass::SubLinear);
313        assert_eq!(d.complexity_detail(), "test dummy");
314        // and the const path agrees:
315        assert_eq!(<Dummy as Complexity>::CLASS, ComplexityClass::SubLinear);
316    }
317}