1#![cfg_attr(not(feature = "std"), no_std)]
2
3#[cfg(not(feature = "std"))]
4extern crate alloc;
5
6#[cfg(feature = "std")]
7use std::string::String;
8#[cfg(feature = "std")]
9use std::string::ToString;
10#[cfg(feature = "std")]
11use std::vec::Vec;
12#[cfg(feature = "std")]
13use std::format;
14
15#[cfg(not(feature = "std"))]
16use alloc::format;
17#[cfg(not(feature = "std"))]
18use alloc::string::String;
19#[cfg(not(feature = "std"))]
20use alloc::string::ToString;
21#[cfg(not(feature = "std"))]
22use alloc::vec::Vec;
23
24pub const DEFAULT_TOLERANCE: f64 = 1e-9;
26
27const LN_2: f64 = 0.6931471805599453;
29
30#[derive(Debug, Clone)]
36pub struct ConservationReport {
37 pub delta: f64,
39 pub tolerance: f64,
41 pub passed: bool,
43 pub timestamp_ms: u64,
45 pub label: Option<String>,
47}
48
49impl ConservationReport {
50 pub fn new(parts: &[f64], total: f64, tolerance: f64, timestamp_ms: u64) -> Self {
52 let sum: f64 = parts.iter().copied().sum();
53 let delta = sum - total;
54 let passed = delta.abs() <= tolerance;
55 Self {
56 delta,
57 tolerance,
58 passed,
59 timestamp_ms,
60 label: None,
61 }
62 }
63
64 pub fn with_label(mut self, label: &str) -> Self {
66 self.label = Some(label.to_string());
67 self
68 }
69
70 pub fn to_json(&self) -> String {
72 let label_json = match &self.label {
73 Some(l) => format!(",\"label\":\"{}\"", l),
74 None => String::new(),
75 };
76 format!(
77 "{{\"delta\":{},{},\"tolerance\":{},{},\"ts\":{}}}",
78 fmt_f64(self.delta),
79 if self.passed { "\"passed\":true" } else { "\"passed\":false" },
80 fmt_f64(self.tolerance),
81 label_json,
82 self.timestamp_ms,
83 )
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct SummaryReport {
90 pub checks: Vec<ConservationReport>,
91 pub total: usize,
92 pub passed: usize,
93 pub failed: usize,
94 pub timestamp_ms: u64,
95}
96
97impl SummaryReport {
98 pub fn to_json(&self) -> String {
99 let items: Vec<String> = self.checks.iter().map(|c| c.to_json()).collect();
100 format!(
101 "{{\"total\":{},\"passed\":{},\"failed\":{},\"ts\":{},\"checks\":[{}]}}",
102 self.total,
103 self.passed,
104 self.failed,
105 self.timestamp_ms,
106 items.join(","),
107 )
108 }
109}
110
111pub fn verify_conservation(parts: &[f64], total: f64) -> ConservationReport {
117 verify_conservation_with_tolerance(parts, total, DEFAULT_TOLERANCE)
118}
119
120pub fn verify_conservation_with_tolerance(
122 parts: &[f64],
123 total: f64,
124 tolerance: f64,
125) -> ConservationReport {
126 let ts = raw_timestamp_ms();
127 ConservationReport::new(parts, total, tolerance, ts)
128}
129
130pub fn shannon_entropy(probs: &[f64]) -> f64 {
136 let mut h = 0.0f64;
137 for &p in probs {
138 if p > 0.0 {
139 h -= p * (ln_f64(p) / LN_2);
140 }
141 }
142 h
143}
144
145pub fn kl_divergence(p: &[f64], q: &[f64]) -> f64 {
151 assert_eq!(p.len(), q.len(), "p and q must have equal length");
152 let mut d = 0.0f64;
153 for (&pi, &qi) in p.iter().zip(q.iter()) {
154 if pi > 0.0 {
155 assert!(qi > 0.0, "q_i must be > 0 where p_i > 0");
156 d += pi * (ln_f64(pi / qi) / LN_2);
157 }
158 }
159 d
160}
161
162pub fn verify_determinant(m: &[f64; 4], expected_det: f64) -> bool {
166 let det = m[0] * m[3] - m[1] * m[2];
167 (det - expected_det).abs() <= DEFAULT_TOLERANCE
168}
169
170pub struct EdgeVerifier {
176 checks: Vec<ConservationReport>,
177 now_fn: fn() -> u64,
178}
179
180impl EdgeVerifier {
181 pub fn new() -> Self {
183 Self {
184 checks: Vec::new(),
185 now_fn: raw_timestamp_ms,
186 }
187 }
188
189 pub fn with_clock(now_fn: fn() -> u64) -> Self {
191 Self {
192 checks: Vec::new(),
193 now_fn,
194 }
195 }
196
197 pub fn verify(&mut self, parts: &[f64], total: f64, tolerance: f64, label: &str) -> bool {
199 let ts = (self.now_fn)();
200 let report = ConservationReport::new(parts, total, tolerance, ts).with_label(label);
201 let passed = report.passed;
202 self.checks.push(report);
203 passed
204 }
205
206 pub fn verify_det(&mut self, m: &[f64; 4], expected: f64, label: &str) -> bool {
208 let ts = (self.now_fn)();
209 let det = m[0] * m[3] - m[1] * m[2];
210 let passed = (det - expected).abs() <= DEFAULT_TOLERANCE;
211 let delta = det - expected;
212 self.checks.push(ConservationReport {
213 delta,
214 tolerance: DEFAULT_TOLERANCE,
215 passed,
216 timestamp_ms: ts,
217 label: Some(label.to_string()),
218 });
219 passed
220 }
221
222 pub fn summary(self) -> SummaryReport {
224 let ts = (self.now_fn)();
225 let total = self.checks.len();
226 let passed = self.checks.iter().filter(|c| c.passed).count();
227 SummaryReport {
228 checks: self.checks,
229 total,
230 passed,
231 failed: total - passed,
232 timestamp_ms: ts,
233 }
234 }
235}
236
237fn raw_timestamp_ms() -> u64 {
243 #[cfg(feature = "std")]
244 {
245 use std::sync::atomic::{AtomicU64, Ordering};
246 static COUNTER: AtomicU64 = AtomicU64::new(0);
247 COUNTER.fetch_add(1, Ordering::Relaxed)
248 }
249 #[cfg(not(feature = "std"))]
250 {
251 static mut COUNTER: u64 = 0;
252 unsafe { COUNTER += 1; COUNTER }
254 }
255}
256
257#[cfg(feature = "std")]
259fn ln_f64(x: f64) -> f64 {
260 x.ln()
261}
262
263#[cfg(not(feature = "std"))]
264fn ln_f64(x: f64) -> f64 {
265 libm::log(x)
266}
267
268pub(crate) fn fmt_f64(v: f64) -> String {
270 if v.is_nan() {
271 return "null".to_string();
272 }
273 if v.is_infinite() {
274 return if v.is_sign_positive() { "1e308".to_string() } else { "-1e308".to_string() };
275 }
276 let s = format!("{}", v);
277 if s.contains('.') {
278 let trimmed = s.trim_end_matches('0');
279 if trimmed.ends_with('.') {
280 format!("{}0", trimmed)
281 } else {
282 trimmed.to_string()
283 }
284 } else {
285 s
286 }
287}
288
289#[cfg(test)]
290#[path = "tests.rs"]
291mod tests;