#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(feature = "std")]
use std::string::String;
#[cfg(feature = "std")]
use std::string::ToString;
#[cfg(feature = "std")]
use std::vec::Vec;
#[cfg(feature = "std")]
use std::format;
#[cfg(not(feature = "std"))]
use alloc::format;
#[cfg(not(feature = "std"))]
use alloc::string::String;
#[cfg(not(feature = "std"))]
use alloc::string::ToString;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
pub const DEFAULT_TOLERANCE: f64 = 1e-9;
const LN_2: f64 = 0.6931471805599453;
#[derive(Debug, Clone)]
pub struct ConservationReport {
pub delta: f64,
pub tolerance: f64,
pub passed: bool,
pub timestamp_ms: u64,
pub label: Option<String>,
}
impl ConservationReport {
pub fn new(parts: &[f64], total: f64, tolerance: f64, timestamp_ms: u64) -> Self {
let sum: f64 = parts.iter().copied().sum();
let delta = sum - total;
let passed = delta.abs() <= tolerance;
Self {
delta,
tolerance,
passed,
timestamp_ms,
label: None,
}
}
pub fn with_label(mut self, label: &str) -> Self {
self.label = Some(label.to_string());
self
}
pub fn to_json(&self) -> String {
let label_json = match &self.label {
Some(l) => format!(",\"label\":\"{}\"", l),
None => String::new(),
};
format!(
"{{\"delta\":{},{},\"tolerance\":{},{},\"ts\":{}}}",
fmt_f64(self.delta),
if self.passed { "\"passed\":true" } else { "\"passed\":false" },
fmt_f64(self.tolerance),
label_json,
self.timestamp_ms,
)
}
}
#[derive(Debug, Clone)]
pub struct SummaryReport {
pub checks: Vec<ConservationReport>,
pub total: usize,
pub passed: usize,
pub failed: usize,
pub timestamp_ms: u64,
}
impl SummaryReport {
pub fn to_json(&self) -> String {
let items: Vec<String> = self.checks.iter().map(|c| c.to_json()).collect();
format!(
"{{\"total\":{},\"passed\":{},\"failed\":{},\"ts\":{},\"checks\":[{}]}}",
self.total,
self.passed,
self.failed,
self.timestamp_ms,
items.join(","),
)
}
}
pub fn verify_conservation(parts: &[f64], total: f64) -> ConservationReport {
verify_conservation_with_tolerance(parts, total, DEFAULT_TOLERANCE)
}
pub fn verify_conservation_with_tolerance(
parts: &[f64],
total: f64,
tolerance: f64,
) -> ConservationReport {
let ts = raw_timestamp_ms();
ConservationReport::new(parts, total, tolerance, ts)
}
pub fn shannon_entropy(probs: &[f64]) -> f64 {
let mut h = 0.0f64;
for &p in probs {
if p > 0.0 {
h -= p * (ln_f64(p) / LN_2);
}
}
h
}
pub fn kl_divergence(p: &[f64], q: &[f64]) -> f64 {
assert_eq!(p.len(), q.len(), "p and q must have equal length");
let mut d = 0.0f64;
for (&pi, &qi) in p.iter().zip(q.iter()) {
if pi > 0.0 {
assert!(qi > 0.0, "q_i must be > 0 where p_i > 0");
d += pi * (ln_f64(pi / qi) / LN_2);
}
}
d
}
pub fn verify_determinant(m: &[f64; 4], expected_det: f64) -> bool {
let det = m[0] * m[3] - m[1] * m[2];
(det - expected_det).abs() <= DEFAULT_TOLERANCE
}
pub struct EdgeVerifier {
checks: Vec<ConservationReport>,
now_fn: fn() -> u64,
}
impl EdgeVerifier {
pub fn new() -> Self {
Self {
checks: Vec::new(),
now_fn: raw_timestamp_ms,
}
}
pub fn with_clock(now_fn: fn() -> u64) -> Self {
Self {
checks: Vec::new(),
now_fn,
}
}
pub fn verify(&mut self, parts: &[f64], total: f64, tolerance: f64, label: &str) -> bool {
let ts = (self.now_fn)();
let report = ConservationReport::new(parts, total, tolerance, ts).with_label(label);
let passed = report.passed;
self.checks.push(report);
passed
}
pub fn verify_det(&mut self, m: &[f64; 4], expected: f64, label: &str) -> bool {
let ts = (self.now_fn)();
let det = m[0] * m[3] - m[1] * m[2];
let passed = (det - expected).abs() <= DEFAULT_TOLERANCE;
let delta = det - expected;
self.checks.push(ConservationReport {
delta,
tolerance: DEFAULT_TOLERANCE,
passed,
timestamp_ms: ts,
label: Some(label.to_string()),
});
passed
}
pub fn summary(self) -> SummaryReport {
let ts = (self.now_fn)();
let total = self.checks.len();
let passed = self.checks.iter().filter(|c| c.passed).count();
SummaryReport {
checks: self.checks,
total,
passed,
failed: total - passed,
timestamp_ms: ts,
}
}
}
fn raw_timestamp_ms() -> u64 {
#[cfg(feature = "std")]
{
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
COUNTER.fetch_add(1, Ordering::Relaxed)
}
#[cfg(not(feature = "std"))]
{
static mut COUNTER: u64 = 0;
unsafe { COUNTER += 1; COUNTER }
}
}
#[cfg(feature = "std")]
fn ln_f64(x: f64) -> f64 {
x.ln()
}
#[cfg(not(feature = "std"))]
fn ln_f64(x: f64) -> f64 {
libm::log(x)
}
pub(crate) fn fmt_f64(v: f64) -> String {
if v.is_nan() {
return "null".to_string();
}
if v.is_infinite() {
return if v.is_sign_positive() { "1e308".to_string() } else { "-1e308".to_string() };
}
let s = format!("{}", v);
if s.contains('.') {
let trimmed = s.trim_end_matches('0');
if trimmed.ends_with('.') {
format!("{}0", trimmed)
} else {
trimmed.to_string()
}
} else {
s
}
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;