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 { default: &'static ComplexityClass, worst: &'static ComplexityClass },
87}
88
89impl ComplexityClass {
90 /// Rank suitable for ordering. Lower = cheaper. `Adaptive` ranks by its
91 /// `worst` bound so callers comparing against a budget see the safe
92 /// upper bound.
93 pub const fn rank(&self) -> u16 {
94 match self {
95 ComplexityClass::Logarithmic => 100,
96 ComplexityClass::PolyLogarithmic => 200,
97 ComplexityClass::SubLinear => 300,
98 ComplexityClass::Linear => 400,
99 ComplexityClass::QuasiLinear => 500,
100 ComplexityClass::SubQuadratic => 600,
101 // Polynomial: 700 + degree, so Polynomial(2) = 702, Polynomial(3) = 703, …
102 ComplexityClass::Polynomial(degree) => 700u16 + *degree as u16,
103 ComplexityClass::SuperPolynomial => 800,
104 ComplexityClass::SubExponential => 900,
105 ComplexityClass::Exponential => 1000,
106 ComplexityClass::Factorial => 1100,
107 ComplexityClass::DoubleExponential => 1200,
108 ComplexityClass::Adaptive { worst, .. } => worst.rank(),
109 }
110 }
111
112 /// Short human-readable label suitable for log lines and MCP tool schemas.
113 pub const fn short_label(&self) -> &'static str {
114 match self {
115 ComplexityClass::Logarithmic => "O(log n)",
116 ComplexityClass::PolyLogarithmic => "O((log n)^k)",
117 ComplexityClass::SubLinear => "O(n^c), c<1",
118 ComplexityClass::Linear => "O(n)",
119 ComplexityClass::QuasiLinear => "O(n log n)",
120 ComplexityClass::SubQuadratic => "O(n^{2-ε})",
121 ComplexityClass::Polynomial(2) => "O(n^2)",
122 ComplexityClass::Polynomial(3) => "O(n^3)",
123 ComplexityClass::Polynomial(_) => "O(n^k)",
124 ComplexityClass::SuperPolynomial => "superpoly",
125 ComplexityClass::SubExponential => "subexp",
126 ComplexityClass::Exponential => "O(2^n)",
127 ComplexityClass::Factorial => "O(n!)",
128 ComplexityClass::DoubleExponential => "O(2^{2^n})",
129 ComplexityClass::Adaptive { .. } => "adaptive",
130 }
131 }
132
133 /// True if this class is acceptable for a real-time / edge / always-on hot
134 /// path. Currently: anything strictly cheaper than `Linear`. Linear itself
135 /// is conditional (acceptable for one-pass streaming, not for per-query
136 /// work) — callers needing nuance should match directly.
137 pub const fn is_edge_safe(&self) -> bool {
138 self.rank() < ComplexityClass::Linear.rank()
139 }
140}
141
142impl PartialOrd for ComplexityClass {
143 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
144 Some(self.cmp(other))
145 }
146}
147
148impl Ord for ComplexityClass {
149 fn cmp(&self, other: &Self) -> Ordering {
150 self.rank().cmp(&other.rank())
151 }
152}
153
154/// Compile-time complexity-class declaration. Every public solver / sampler /
155/// analyser in this crate implements this trait, exposing its worst-case
156/// class as a `const` so callers can `match` on it at compile time.
157///
158/// ```rust,no_run
159/// # use sublinear_solver::complexity::{Complexity, ComplexityClass};
160/// fn pick_solver<S: Complexity>() {
161/// match S::CLASS {
162/// ComplexityClass::Logarithmic | ComplexityClass::SubLinear => {
163/// // use it — edge-safe
164/// }
165/// _ => {
166/// // fall back to something cheaper or refuse
167/// }
168/// }
169/// }
170/// ```
171pub trait Complexity {
172 /// Worst-case complexity class on a single-query call. For iterative
173 /// solvers this is the per-iter cost; the iteration count is bounded by
174 /// other configuration (max_iterations, tolerance, ef_construction).
175 const CLASS: ComplexityClass;
176
177 /// Optional human-readable detail for documentation / MCP tool schemas.
178 /// Defaults to the short label of `CLASS`. Override when there's a
179 /// non-obvious constant or k-bound.
180 const DETAIL: &'static str = "";
181}
182
183/// Object-safe runtime introspection. Defaulting `impl<T: Complexity>` so any
184/// type with a static `Complexity` impl gets `ComplexityIntrospect` for free,
185/// and a `dyn ComplexityIntrospect` query works on solver trait objects.
186///
187/// ```rust,no_run
188/// # use sublinear_solver::complexity::{ComplexityIntrospect, ComplexityClass};
189/// fn within_budget(s: &dyn ComplexityIntrospect, max: ComplexityClass) -> bool {
190/// s.complexity_class() <= max
191/// }
192/// ```
193pub trait ComplexityIntrospect {
194 fn complexity_class(&self) -> ComplexityClass;
195 fn complexity_detail(&self) -> &'static str {
196 ""
197 }
198}
199
200impl<T: Complexity> ComplexityIntrospect for T {
201 fn complexity_class(&self) -> ComplexityClass {
202 T::CLASS
203 }
204 fn complexity_detail(&self) -> &'static str {
205 T::DETAIL
206 }
207}
208
209// ─────────────────────────────────────────────────────────────────────────
210// Complexity impls for the headline solvers / samplers / embeddings.
211//
212// Each impl is one line and forces the implementer to think about which
213// class their algorithm actually inhabits. CI regression-guards in
214// `.github/workflows/ci.yml` will eventually diff these between PRs.
215// ─────────────────────────────────────────────────────────────────────────
216
217impl Complexity for crate::solver::neumann::NeumannSolver {
218 const CLASS: ComplexityClass = ComplexityClass::Linear;
219 const DETAIL: &'static str =
220 "O(k · nnz(A)) per iter; k bounded by series_tolerance + max_terms (default 200).";
221}
222
223impl Complexity for crate::optimized_solver::OptimizedConjugateGradientSolver {
224 const CLASS: ComplexityClass = ComplexityClass::Linear;
225 const DETAIL: &'static str =
226 "O(k · nnz(A)) per iter; k ≈ √κ(A) on SPD inputs.";
227}
228
229impl Complexity for crate::sublinear::sublinear_neumann::SublinearNeumannSolver {
230 // Adaptive: O(log n) on the per-entry sublinear-guaranteed path,
231 // degrades to O(n) on the base-case path when n ≤ base_case_threshold.
232 const CLASS: ComplexityClass = ComplexityClass::Adaptive {
233 default: &ComplexityClass::Logarithmic,
234 worst: &ComplexityClass::Linear,
235 };
236 const DETAIL: &'static str =
237 "O(log n) per single-entry query on diagonally-dominant systems via JL + recursive Neumann; \
238 O(n) base case at n ≤ base_case_threshold.";
239}
240
241impl Complexity for crate::sublinear::johnson_lindenstrauss::JLEmbedding {
242 const CLASS: ComplexityClass = ComplexityClass::Linear;
243 const DETAIL: &'static str = "O(d · k) per project_vector; k = target_dim, capped at original_dim - 1.";
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn ordering_is_by_asymptotic_growth() {
252 assert!(ComplexityClass::Logarithmic < ComplexityClass::PolyLogarithmic);
253 assert!(ComplexityClass::PolyLogarithmic < ComplexityClass::SubLinear);
254 assert!(ComplexityClass::SubLinear < ComplexityClass::Linear);
255 assert!(ComplexityClass::Linear < ComplexityClass::QuasiLinear);
256 assert!(ComplexityClass::QuasiLinear < ComplexityClass::SubQuadratic);
257 assert!(ComplexityClass::SubQuadratic < ComplexityClass::Polynomial(2));
258 assert!(ComplexityClass::Polynomial(2) < ComplexityClass::Polynomial(3));
259 assert!(ComplexityClass::Polynomial(3) < ComplexityClass::SuperPolynomial);
260 assert!(ComplexityClass::SuperPolynomial < ComplexityClass::SubExponential);
261 assert!(ComplexityClass::SubExponential < ComplexityClass::Exponential);
262 assert!(ComplexityClass::Exponential < ComplexityClass::Factorial);
263 assert!(ComplexityClass::Factorial < ComplexityClass::DoubleExponential);
264 }
265
266 #[test]
267 fn edge_safe_predicate_matches_adr() {
268 assert!(ComplexityClass::Logarithmic.is_edge_safe());
269 assert!(ComplexityClass::PolyLogarithmic.is_edge_safe());
270 assert!(ComplexityClass::SubLinear.is_edge_safe());
271 // Linear and above are conditional / not safe by default.
272 assert!(!ComplexityClass::Linear.is_edge_safe());
273 assert!(!ComplexityClass::QuasiLinear.is_edge_safe());
274 assert!(!ComplexityClass::Exponential.is_edge_safe());
275 }
276
277 #[test]
278 fn adaptive_ranks_by_worst_case() {
279 static QUASILIN: ComplexityClass = ComplexityClass::QuasiLinear;
280 static POLY3: ComplexityClass = ComplexityClass::Polynomial(3);
281 let adaptive = ComplexityClass::Adaptive {
282 default: &QUASILIN,
283 worst: &POLY3,
284 };
285 // adaptive is treated as expensive-as-worst-case for budget purposes.
286 assert!(adaptive > ComplexityClass::QuasiLinear);
287 assert_eq!(adaptive.rank(), POLY3.rank());
288 }
289
290 #[test]
291 fn short_label_format() {
292 assert_eq!(ComplexityClass::Logarithmic.short_label(), "O(log n)");
293 assert_eq!(ComplexityClass::Polynomial(2).short_label(), "O(n^2)");
294 assert_eq!(ComplexityClass::Polynomial(3).short_label(), "O(n^3)");
295 assert_eq!(ComplexityClass::Polynomial(4).short_label(), "O(n^k)");
296 assert_eq!(ComplexityClass::Exponential.short_label(), "O(2^n)");
297 }
298
299 /// Compile-time / runtime parity: a value that has `Complexity` also has
300 /// `ComplexityIntrospect`, and the two report the same class.
301 #[test]
302 fn introspect_blanket_impl_matches_const() {
303 struct Dummy;
304 impl Complexity for Dummy {
305 const CLASS: ComplexityClass = ComplexityClass::SubLinear;
306 const DETAIL: &'static str = "test dummy";
307 }
308 let d = Dummy;
309 assert_eq!(d.complexity_class(), ComplexityClass::SubLinear);
310 assert_eq!(d.complexity_detail(), "test dummy");
311 // and the const path agrees:
312 assert_eq!(<Dummy as Complexity>::CLASS, ComplexityClass::SubLinear);
313 }
314}