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}