1use crate::complexity::{Complexity, ComplexityClass};
38use crate::error::{Result, SolverError};
39use crate::matrix::Matrix;
40use crate::types::Precision;
41use alloc::collections::BTreeMap;
42
43#[derive(Debug, Clone, Copy, Default)]
45pub struct VerifySparseSolutionOp;
46
47impl Complexity for VerifySparseSolutionOp {
48 const CLASS: ComplexityClass = ComplexityClass::SubLinear;
49 const DETAIL: &'static str = "Residual audit restricted to caller-supplied closure entries. \
50 O(|entries| · avg_row_nnz) — same class as the SubLinear orchestrator \
51 whose output it verifies. Independent of n for sparse DD matrices.";
52}
53
54#[derive(Debug, Clone, PartialEq)]
56pub struct WitnessReport {
57 pub max_residual: Precision,
59 pub threshold: Precision,
62 pub ok: bool,
64 pub worst_row: Option<usize>,
67}
68
69pub fn verify_sparse_solution(
102 matrix: &dyn Matrix,
103 prev_solution: &[Precision],
104 b: &[Precision],
105 entries: &[(usize, Precision)],
106 tolerance: Precision,
107) -> Result<WitnessReport> {
108 let n = matrix.rows();
109 if prev_solution.len() != n {
110 return Err(SolverError::DimensionMismatch {
111 expected: n,
112 actual: prev_solution.len(),
113 operation: alloc::string::String::from("verify_sparse_solution::prev_solution"),
114 });
115 }
116 if b.len() != n {
117 return Err(SolverError::DimensionMismatch {
118 expected: n,
119 actual: b.len(),
120 operation: alloc::string::String::from("verify_sparse_solution::b"),
121 });
122 }
123
124 let mut overlay: BTreeMap<usize, Precision> = BTreeMap::new();
126 for &(i, val) in entries {
127 if i < n {
128 overlay.insert(i, val);
129 }
130 }
131 let x_at = |j: usize| -> Precision {
133 match overlay.get(&j) {
134 Some(&v) => v,
135 None => {
136 if j < prev_solution.len() {
137 prev_solution[j]
138 } else {
139 0.0
140 }
141 }
142 }
143 };
144
145 let b_inf = b
147 .iter()
148 .map(|v| v.abs())
149 .fold(0.0_f64, |a, x| if a > x { a } else { x });
150 let threshold = tolerance * b_inf.max(1.0);
151
152 let mut max_residual: Precision = 0.0;
154 let mut worst_row: Option<usize> = None;
155 for &(i, _) in entries {
156 if i >= n {
157 continue;
158 }
159 let mut ax_i: Precision = 0.0;
160 for (col_idx, a_ij) in matrix.row_iter(i) {
161 let j = col_idx as usize;
162 ax_i += a_ij * x_at(j);
163 }
164 let r = (b[i] - ax_i).abs();
165 if r > max_residual {
166 max_residual = r;
167 worst_row = Some(i);
168 }
169 }
170
171 Ok(WitnessReport {
172 max_residual,
173 threshold,
174 ok: max_residual <= threshold,
175 worst_row,
176 })
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::matrix::SparseMatrix;
183 use crate::solver::{neumann::NeumannSolver, SolverAlgorithm, SolverOptions};
184 use crate::{solve_on_change_sublinear, SparseDelta};
185
186 fn build_ring(n: usize) -> SparseMatrix {
188 let mut t = Vec::new();
189 for i in 0..n {
190 t.push((i, i, 5.0_f64));
191 t.push((i, (i + 1) % n, 1.0));
192 t.push((i, (i + 2) % n, 1.0));
193 t.push((i, (i + n - 1) % n, -1.0));
194 t.push((i, (i + n - 2) % n, -1.0));
195 }
196 SparseMatrix::from_triplets(t, n, n).unwrap()
197 }
198
199 fn build_strong_ring(n: usize) -> SparseMatrix {
203 let mut t = Vec::new();
204 for i in 0..n {
205 t.push((i, i, 10.0_f64));
206 t.push((i, (i + 1) % n, 0.5));
207 t.push((i, (i + n - 1) % n, -0.5));
208 }
209 SparseMatrix::from_triplets(t, n, n).unwrap()
210 }
211
212 #[test]
213 fn op_complexity_class_is_sublinear() {
214 assert_eq!(
215 <VerifySparseSolutionOp as Complexity>::CLASS,
216 ComplexityClass::SubLinear
217 );
218 }
219
220 #[test]
221 fn op_compile_time_bound() {
222 const _: () = assert!(matches!(
223 <VerifySparseSolutionOp as Complexity>::CLASS,
224 ComplexityClass::SubLinear
225 ));
226 }
227
228 #[test]
229 fn witness_passes_on_genuine_sublinear_output() {
230 let n = 32;
233 let m = build_strong_ring(n);
234 let b_prev: Vec<f64> = (0..n).map(|i| i as f64 + 1.0).collect();
235
236 let solver = NeumannSolver::new(64, 1e-12);
237 let opts = SolverOptions::default();
238 let prev_solution = solver.solve(&m, &b_prev, &opts).unwrap().solution;
239
240 let delta = SparseDelta::new(vec![10], vec![0.5]).unwrap();
241 let mut b_new = b_prev.clone();
242 delta.apply_to(&mut b_new).unwrap();
243
244 let entries = solve_on_change_sublinear(
245 &m,
246 &prev_solution,
247 &b_new,
248 &delta,
249 8,
250 32,
251 1e-12,
252 )
253 .unwrap();
254
255 let report = verify_sparse_solution(&m, &prev_solution, &b_new, &entries, 1e-3).unwrap();
259 assert!(
260 report.ok,
261 "witness should pass on a genuine SubLinear output; max_residual={}, threshold={}, worst_row={:?}",
262 report.max_residual, report.threshold, report.worst_row
263 );
264 }
265
266 #[test]
267 fn witness_fails_on_perturbed_output() {
268 let n = 16;
270 let m = build_ring(n);
271 let b_prev: Vec<f64> = (0..n).map(|i| i as f64 + 1.0).collect();
272
273 let solver = NeumannSolver::new(64, 1e-12);
274 let opts = SolverOptions::default();
275 let prev = solver.solve(&m, &b_prev, &opts).unwrap().solution;
276
277 let delta = SparseDelta::new(vec![3], vec![0.2]).unwrap();
278 let mut b_new = b_prev.clone();
279 delta.apply_to(&mut b_new).unwrap();
280
281 let mut entries =
282 solve_on_change_sublinear(&m, &prev, &b_new, &delta, 6, 32, 1e-10).unwrap();
283 if let Some(first) = entries.first_mut() {
285 first.1 += 100.0;
286 }
287
288 let report = verify_sparse_solution(&m, &prev, &b_new, &entries, 1e-6).unwrap();
289 assert!(!report.ok, "witness should fail on a corrupted entry");
290 assert!(report.worst_row.is_some());
291 }
292
293 #[test]
294 fn witness_empty_entries_passes_trivially() {
295 let m = build_ring(4);
296 let prev = vec![0.0; 4];
297 let b = vec![1.0; 4];
298 let report = verify_sparse_solution(&m, &prev, &b, &[], 1e-6).unwrap();
299 assert!(report.ok);
300 assert_eq!(report.max_residual, 0.0);
301 assert_eq!(report.worst_row, None);
302 }
303
304 #[test]
305 fn witness_dimension_mismatch_errors() {
306 let m = build_ring(4);
307 let bad_prev = vec![0.0; 3];
308 let b = vec![1.0; 4];
309 let err = verify_sparse_solution(&m, &bad_prev, &b, &[], 1e-6).unwrap_err();
310 assert!(matches!(err, SolverError::DimensionMismatch { .. }));
311
312 let prev = vec![0.0; 4];
313 let bad_b = vec![1.0; 5];
314 let err = verify_sparse_solution(&m, &prev, &bad_b, &[], 1e-6).unwrap_err();
315 assert!(matches!(err, SolverError::DimensionMismatch { .. }));
316 }
317
318 #[test]
319 fn witness_out_of_bound_entries_silently_ignored() {
320 let m = build_ring(4);
323 let prev = vec![0.0; 4];
324 let b = vec![1.0; 4];
325 let entries = vec![(99usize, 1.0), (100usize, 2.0)];
326 let report = verify_sparse_solution(&m, &prev, &b, &entries, 1e-6).unwrap();
327 assert!(report.ok);
329 assert_eq!(report.max_residual, 0.0);
330 }
331}