1use std::hash::{DefaultHasher, Hash, Hasher};
17use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
18
19use serde::{Deserialize, Serialize};
20
21use crate::types::{Algorithm, CsrMatrix, SolverResult};
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct SolverAuditEntry {
38 pub request_id: String,
40
41 pub algorithm: Algorithm,
43
44 pub input_hash: [u8; 8],
47
48 pub output_hash: [u8; 8],
51
52 pub iterations: usize,
54
55 pub wall_time_us: u64,
57
58 pub converged: bool,
60
61 pub residual: f64,
63
64 pub timestamp_ns: u128,
66
67 pub matrix_rows: usize,
69
70 pub matrix_nnz: usize,
72}
73
74pub fn hash_input(matrix: &CsrMatrix<f32>, rhs: &[f32]) -> [u8; 8] {
83 let mut h = DefaultHasher::new();
84
85 matrix.rows.hash(&mut h);
87 matrix.cols.hash(&mut h);
88 matrix.row_ptr.hash(&mut h);
89 matrix.col_indices.hash(&mut h);
90
91 for &v in &matrix.values {
93 v.to_bits().hash(&mut h);
94 }
95
96 for &v in rhs {
98 v.to_bits().hash(&mut h);
99 }
100
101 h.finish().to_le_bytes()
102}
103
104pub fn hash_output(solution: &[f32]) -> [u8; 8] {
106 let mut h = DefaultHasher::new();
107 for &v in solution {
108 v.to_bits().hash(&mut h);
109 }
110 h.finish().to_le_bytes()
111}
112
113pub struct AuditBuilder {
131 request_id: String,
132 input_hash: [u8; 8],
133 matrix_rows: usize,
134 matrix_nnz: usize,
135 start: Instant,
136 timestamp_ns: u128,
137}
138
139impl AuditBuilder {
140 pub fn start(request_id: impl Into<String>, matrix: &CsrMatrix<f32>, rhs: &[f32]) -> Self {
145 let timestamp_ns = SystemTime::now()
146 .duration_since(UNIX_EPOCH)
147 .unwrap_or(Duration::ZERO)
148 .as_nanos();
149
150 Self {
151 request_id: request_id.into(),
152 input_hash: hash_input(matrix, rhs),
153 matrix_rows: matrix.rows,
154 matrix_nnz: matrix.values.len(),
155 start: Instant::now(),
156 timestamp_ns,
157 }
158 }
159
160 pub fn finish(self, result: &SolverResult, tolerance: f64) -> SolverAuditEntry {
165 let elapsed = self.start.elapsed();
166
167 SolverAuditEntry {
168 request_id: self.request_id,
169 algorithm: result.algorithm,
170 input_hash: self.input_hash,
171 output_hash: hash_output(&result.solution),
172 iterations: result.iterations,
173 wall_time_us: elapsed.as_micros() as u64,
174 converged: result.residual_norm <= tolerance,
175 residual: result.residual_norm,
176 timestamp_ns: self.timestamp_ns,
177 matrix_rows: self.matrix_rows,
178 matrix_nnz: self.matrix_nnz,
179 }
180 }
181}
182
183#[cfg(test)]
188mod tests {
189 use super::*;
190 use crate::types::{Algorithm, ConvergenceInfo, SolverResult};
191 use std::time::Duration;
192
193 fn sample_matrix() -> CsrMatrix<f32> {
194 CsrMatrix::<f32>::from_coo(
195 2,
196 2,
197 vec![(0, 0, 2.0), (0, 1, -0.5), (1, 0, -0.5), (1, 1, 2.0)],
198 )
199 }
200
201 fn sample_result() -> SolverResult {
202 SolverResult {
203 solution: vec![0.5, 0.5],
204 iterations: 10,
205 residual_norm: 1e-9,
206 wall_time: Duration::from_millis(2),
207 convergence_history: vec![ConvergenceInfo {
208 iteration: 9,
209 residual_norm: 1e-9,
210 }],
211 algorithm: Algorithm::Neumann,
212 }
213 }
214
215 #[test]
216 fn hash_input_deterministic() {
217 let m = sample_matrix();
218 let rhs = vec![1.0f32, 1.0];
219 let h1 = hash_input(&m, &rhs);
220 let h2 = hash_input(&m, &rhs);
221 assert_eq!(h1, h2, "same input must produce same hash");
222 }
223
224 #[test]
225 fn hash_input_changes_with_values() {
226 let m1 = sample_matrix();
227 let mut m2 = sample_matrix();
228 m2.values[0] = 3.0;
229 let rhs = vec![1.0f32, 1.0];
230 assert_ne!(
231 hash_input(&m1, &rhs),
232 hash_input(&m2, &rhs),
233 "different values must produce different hashes",
234 );
235 }
236
237 #[test]
238 fn hash_input_changes_with_rhs() {
239 let m = sample_matrix();
240 let rhs1 = vec![1.0f32, 1.0];
241 let rhs2 = vec![1.0f32, 2.0];
242 assert_ne!(
243 hash_input(&m, &rhs1),
244 hash_input(&m, &rhs2),
245 "different rhs must produce different hashes",
246 );
247 }
248
249 #[test]
250 fn hash_output_deterministic() {
251 let sol = vec![0.5f32, 0.5];
252 assert_eq!(hash_output(&sol), hash_output(&sol));
253 }
254
255 #[test]
256 fn hash_output_changes() {
257 let sol1 = vec![0.5f32, 0.5];
258 let sol2 = vec![0.5f32, 0.6];
259 assert_ne!(hash_output(&sol1), hash_output(&sol2));
260 }
261
262 #[test]
263 fn audit_builder_produces_entry() {
264 let m = sample_matrix();
265 let rhs = vec![1.0f32, 1.0];
266 let builder = AuditBuilder::start("test-req-1", &m, &rhs);
267
268 let result = sample_result();
269 let entry = builder.finish(&result, 1e-6);
270
271 assert_eq!(entry.request_id, "test-req-1");
272 assert_eq!(entry.algorithm, Algorithm::Neumann);
273 assert_eq!(entry.iterations, 10);
274 assert!(entry.converged, "residual 1e-9 < tolerance 1e-6");
275 assert_eq!(entry.matrix_rows, 2);
276 assert_eq!(entry.matrix_nnz, 4);
277 assert!(entry.timestamp_ns > 0);
278 }
279
280 #[test]
281 fn audit_builder_not_converged() {
282 let m = sample_matrix();
283 let rhs = vec![1.0f32, 1.0];
284 let builder = AuditBuilder::start("test-req-2", &m, &rhs);
285
286 let mut result = sample_result();
287 result.residual_norm = 0.1; let entry = builder.finish(&result, 1e-6);
289
290 assert!(!entry.converged);
291 }
292
293 #[test]
294 fn audit_entry_is_serializable() {
295 let m = sample_matrix();
299 let rhs = vec![1.0f32, 1.0];
300 let builder = AuditBuilder::start("ser-test", &m, &rhs);
301 let result = sample_result();
302 let entry = builder.finish(&result, 1e-6);
303
304 let debug = format!("{:?}", entry);
306 assert!(debug.contains("ser-test"), "debug: {debug}");
307 assert!(debug.contains("Neumann"), "debug: {debug}");
308
309 let cloned = entry.clone();
311 assert_eq!(cloned.request_id, entry.request_id);
312 assert_eq!(cloned.input_hash, entry.input_hash);
313 assert_eq!(cloned.output_hash, entry.output_hash);
314 assert_eq!(cloned.iterations, entry.iterations);
315 }
316}