use std::hash::{DefaultHasher, Hash, Hasher};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use crate::types::{Algorithm, CsrMatrix, SolverResult};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolverAuditEntry {
pub request_id: String,
pub algorithm: Algorithm,
pub input_hash: [u8; 8],
pub output_hash: [u8; 8],
pub iterations: usize,
pub wall_time_us: u64,
pub converged: bool,
pub residual: f64,
pub timestamp_ns: u128,
pub matrix_rows: usize,
pub matrix_nnz: usize,
}
pub fn hash_input(matrix: &CsrMatrix<f32>, rhs: &[f32]) -> [u8; 8] {
let mut h = DefaultHasher::new();
matrix.rows.hash(&mut h);
matrix.cols.hash(&mut h);
matrix.row_ptr.hash(&mut h);
matrix.col_indices.hash(&mut h);
for &v in &matrix.values {
v.to_bits().hash(&mut h);
}
for &v in rhs {
v.to_bits().hash(&mut h);
}
h.finish().to_le_bytes()
}
pub fn hash_output(solution: &[f32]) -> [u8; 8] {
let mut h = DefaultHasher::new();
for &v in solution {
v.to_bits().hash(&mut h);
}
h.finish().to_le_bytes()
}
pub struct AuditBuilder {
request_id: String,
input_hash: [u8; 8],
matrix_rows: usize,
matrix_nnz: usize,
start: Instant,
timestamp_ns: u128,
}
impl AuditBuilder {
pub fn start(request_id: impl Into<String>, matrix: &CsrMatrix<f32>, rhs: &[f32]) -> Self {
let timestamp_ns = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_nanos();
Self {
request_id: request_id.into(),
input_hash: hash_input(matrix, rhs),
matrix_rows: matrix.rows,
matrix_nnz: matrix.values.len(),
start: Instant::now(),
timestamp_ns,
}
}
pub fn finish(self, result: &SolverResult, tolerance: f64) -> SolverAuditEntry {
let elapsed = self.start.elapsed();
SolverAuditEntry {
request_id: self.request_id,
algorithm: result.algorithm,
input_hash: self.input_hash,
output_hash: hash_output(&result.solution),
iterations: result.iterations,
wall_time_us: elapsed.as_micros() as u64,
converged: result.residual_norm <= tolerance,
residual: result.residual_norm,
timestamp_ns: self.timestamp_ns,
matrix_rows: self.matrix_rows,
matrix_nnz: self.matrix_nnz,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Algorithm, ConvergenceInfo, SolverResult};
use std::time::Duration;
fn sample_matrix() -> CsrMatrix<f32> {
CsrMatrix::<f32>::from_coo(
2,
2,
vec![(0, 0, 2.0), (0, 1, -0.5), (1, 0, -0.5), (1, 1, 2.0)],
)
}
fn sample_result() -> SolverResult {
SolverResult {
solution: vec![0.5, 0.5],
iterations: 10,
residual_norm: 1e-9,
wall_time: Duration::from_millis(2),
convergence_history: vec![ConvergenceInfo {
iteration: 9,
residual_norm: 1e-9,
}],
algorithm: Algorithm::Neumann,
}
}
#[test]
fn hash_input_deterministic() {
let m = sample_matrix();
let rhs = vec![1.0f32, 1.0];
let h1 = hash_input(&m, &rhs);
let h2 = hash_input(&m, &rhs);
assert_eq!(h1, h2, "same input must produce same hash");
}
#[test]
fn hash_input_changes_with_values() {
let m1 = sample_matrix();
let mut m2 = sample_matrix();
m2.values[0] = 3.0;
let rhs = vec![1.0f32, 1.0];
assert_ne!(
hash_input(&m1, &rhs),
hash_input(&m2, &rhs),
"different values must produce different hashes",
);
}
#[test]
fn hash_input_changes_with_rhs() {
let m = sample_matrix();
let rhs1 = vec![1.0f32, 1.0];
let rhs2 = vec![1.0f32, 2.0];
assert_ne!(
hash_input(&m, &rhs1),
hash_input(&m, &rhs2),
"different rhs must produce different hashes",
);
}
#[test]
fn hash_output_deterministic() {
let sol = vec![0.5f32, 0.5];
assert_eq!(hash_output(&sol), hash_output(&sol));
}
#[test]
fn hash_output_changes() {
let sol1 = vec![0.5f32, 0.5];
let sol2 = vec![0.5f32, 0.6];
assert_ne!(hash_output(&sol1), hash_output(&sol2));
}
#[test]
fn audit_builder_produces_entry() {
let m = sample_matrix();
let rhs = vec![1.0f32, 1.0];
let builder = AuditBuilder::start("test-req-1", &m, &rhs);
let result = sample_result();
let entry = builder.finish(&result, 1e-6);
assert_eq!(entry.request_id, "test-req-1");
assert_eq!(entry.algorithm, Algorithm::Neumann);
assert_eq!(entry.iterations, 10);
assert!(entry.converged, "residual 1e-9 < tolerance 1e-6");
assert_eq!(entry.matrix_rows, 2);
assert_eq!(entry.matrix_nnz, 4);
assert!(entry.timestamp_ns > 0);
}
#[test]
fn audit_builder_not_converged() {
let m = sample_matrix();
let rhs = vec![1.0f32, 1.0];
let builder = AuditBuilder::start("test-req-2", &m, &rhs);
let mut result = sample_result();
result.residual_norm = 0.1; let entry = builder.finish(&result, 1e-6);
assert!(!entry.converged);
}
#[test]
fn audit_entry_is_serializable() {
let m = sample_matrix();
let rhs = vec![1.0f32, 1.0];
let builder = AuditBuilder::start("ser-test", &m, &rhs);
let result = sample_result();
let entry = builder.finish(&result, 1e-6);
let debug = format!("{:?}", entry);
assert!(debug.contains("ser-test"), "debug: {debug}");
assert!(debug.contains("Neumann"), "debug: {debug}");
let cloned = entry.clone();
assert_eq!(cloned.request_id, entry.request_id);
assert_eq!(cloned.input_hash, entry.input_hash);
assert_eq!(cloned.output_hash, entry.output_hash);
assert_eq!(cloned.iterations, entry.iterations);
}
}