use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use crate::generators::polynomial::{horner_ascending, least_squares_fit};
use sha2::{Digest, Sha256};
pub const LAW_ID_DOMAIN: &[u8] = b"alice-zip/law-id/v1";
pub const SIGNAL_LAW_KIND: &[u8] = b"signal-law/polynomial/v1";
pub use alice_det_math::SEMANTICS_ID;
fn update_length_prefixed(h: &mut Sha256, bytes: &[u8]) {
h.update(u64_len(bytes.len()).to_be_bytes());
h.update(bytes);
}
#[allow(
clippy::cast_possible_truncation,
reason = "usize is at most 64 bits on every supported target"
)]
const fn u64_len(n: usize) -> u64 {
n as u64
}
#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use crate::math::FloatExt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum LawError {
TooFewPoints,
DegenerateDomain,
NonFinite,
RankDeficient,
OutOfRange,
}
impl core::fmt::Display for LawError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let s = match self {
Self::TooFewPoints => "fewer points than parameters",
Self::DegenerateDomain => "evidence covers a single x",
Self::NonFinite => "evidence contains a non-finite value",
Self::RankDeficient => "evidence does not determine the parameters",
Self::OutOfRange => "x is outside the valid range",
};
f.write_str(s)
}
}
#[cfg(feature = "std")]
impl std::error::Error for LawError {}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ValidRange {
pub lo: f64,
pub hi: f64,
}
impl ValidRange {
#[must_use]
pub fn contains(&self, x: f64) -> bool {
x >= self.lo && x <= self.hi
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ResidualStats {
pub n: usize,
pub rms: f64,
pub max_abs: f64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Provenance {
pub source: String,
pub method: String,
}
impl Provenance {
#[must_use]
pub fn new(source: &str, method: &str) -> Self {
Self {
source: String::from(source),
method: String::from(method),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct OracleCase {
pub x: f64,
pub expected: f64,
pub tolerance: f64,
pub source: String,
}
impl OracleCase {
#[must_use]
pub fn new(x: f64, expected: f64, tolerance: f64, source: &str) -> Self {
Self {
x,
expected,
tolerance,
source: String::from(source),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct OracleOutcome {
pub value: Result<f64, LawError>,
pub error: Option<f64>,
pub passed: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct IngestPolicy {
pub abs_tolerance: f64,
pub break_factor: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Verdict {
NoEvidence,
OutOfRange {
outside: usize,
},
Supports {
rms: f64,
},
ParameterUpdate {
previous_rms: f64,
updated: Box<SignalLaw>,
},
ResidualGrew {
rms: f64,
},
Breaks {
rms: f64,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct SignalLawParts {
pub coefficients: Vec<f64>,
pub domain: ValidRange,
pub evidence: Vec<(f64, f64)>,
pub residual: ResidualStats,
pub provenance: Provenance,
pub oracles: Vec<OracleCase>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SignalLaw {
coeffs: Vec<f64>,
domain: ValidRange,
evidence: Vec<(f64, f64)>,
residual: ResidualStats,
provenance: Provenance,
oracles: Vec<OracleCase>,
}
impl SignalLaw {
pub fn fit_polynomial(
points: &[(f64, f64)],
degree: usize,
provenance: Provenance,
) -> Result<Self, LawError> {
if points.len() < degree + 1 || points.is_empty() {
return Err(LawError::TooFewPoints);
}
if points.iter().any(|(x, y)| !x.is_finite() || !y.is_finite()) {
return Err(LawError::NonFinite);
}
let lo = points.iter().fold(f64::INFINITY, |a, p| a.min(p.0));
let hi = points.iter().fold(f64::NEG_INFINITY, |a, p| a.max(p.0));
if hi <= lo {
return Err(LawError::DegenerateDomain);
}
let span = hi - lo;
let us: Vec<f64> = points.iter().map(|p| (p.0 - lo) / span).collect();
let ys: Vec<f64> = points.iter().map(|p| p.1).collect();
let coeffs = least_squares_fit(&us, &ys, degree).ok_or(LawError::RankDeficient)?;
let mut law = Self {
coeffs,
domain: ValidRange { lo, hi },
evidence: points.to_vec(),
residual: ResidualStats {
n: 0,
rms: 0.0,
max_abs: 0.0,
},
provenance,
oracles: Vec::new(),
};
law.residual = law.residual_over(points);
Ok(law)
}
#[must_use]
pub fn with_oracle(mut self, case: OracleCase) -> Self {
self.oracles.push(case);
self
}
#[must_use]
pub fn coefficients(&self) -> &[f64] {
&self.coeffs
}
#[must_use]
pub fn to_parts(&self) -> SignalLawParts {
SignalLawParts {
coefficients: self.coeffs.clone(),
domain: self.domain,
evidence: self.evidence.clone(),
residual: self.residual,
provenance: self.provenance.clone(),
oracles: self.oracles.clone(),
}
}
pub fn from_parts(parts: SignalLawParts) -> Result<Self, LawError> {
let SignalLawParts {
coefficients,
domain,
evidence,
provenance,
oracles,
..
} = parts;
if coefficients.is_empty() || evidence.is_empty() || evidence.len() < coefficients.len() {
return Err(LawError::TooFewPoints);
}
if coefficients.iter().any(|c| !c.is_finite())
|| !domain.lo.is_finite()
|| !domain.hi.is_finite()
|| evidence
.iter()
.any(|(x, y)| !x.is_finite() || !y.is_finite())
{
return Err(LawError::NonFinite);
}
if domain.hi <= domain.lo {
return Err(LawError::DegenerateDomain);
}
if evidence.iter().any(|(x, _)| !domain.contains(*x)) {
return Err(LawError::OutOfRange);
}
let mut law = Self {
coeffs: coefficients,
domain,
evidence,
residual: ResidualStats {
n: 0,
rms: 0.0,
max_abs: 0.0,
},
provenance,
oracles,
};
law.residual = law.residual_over(&law.evidence);
Ok(law)
}
#[must_use]
pub fn degree(&self) -> usize {
self.coeffs.len() - 1
}
#[must_use]
pub fn domain(&self) -> ValidRange {
self.domain
}
#[must_use]
pub fn residual(&self) -> ResidualStats {
self.residual
}
#[must_use]
pub fn provenance(&self) -> &Provenance {
&self.provenance
}
#[must_use]
pub fn evidence(&self) -> &[(f64, f64)] {
&self.evidence
}
#[must_use]
pub fn oracles(&self) -> &[OracleCase] {
&self.oracles
}
pub fn evaluate(&self, x: f64) -> Result<f64, LawError> {
if !self.domain.contains(x) {
return Err(LawError::OutOfRange);
}
Ok(self.eval_unchecked(x))
}
fn eval_unchecked(&self, x: f64) -> f64 {
let u = (x - self.domain.lo) / (self.domain.hi - self.domain.lo);
horner_ascending(&self.coeffs, u)
}
#[must_use]
pub fn law_id(&self, semantics_id: &[u8; 32]) -> [u8; 32] {
let mut h = Sha256::new();
update_length_prefixed(&mut h, LAW_ID_DOMAIN);
update_length_prefixed(&mut h, SIGNAL_LAW_KIND);
h.update(semantics_id);
h.update(self.domain.lo.to_bits().to_be_bytes());
h.update(self.domain.hi.to_bits().to_be_bytes());
h.update(u64_len(self.coeffs.len()).to_be_bytes());
for c in &self.coeffs {
h.update(c.to_bits().to_be_bytes());
}
h.finalize().into()
}
fn residual_over(&self, points: &[(f64, f64)]) -> ResidualStats {
let mut sum_sq = 0.0_f64;
let mut max_abs = 0.0_f64;
for &(x, y) in points {
let d = y - self.eval_unchecked(x);
sum_sq += d * d;
max_abs = max_abs.max(d.abs());
}
#[allow(clippy::cast_precision_loss)]
let rms = if points.is_empty() {
0.0
} else {
(sum_sq / points.len() as f64).sqrt()
};
ResidualStats {
n: points.len(),
rms,
max_abs,
}
}
#[must_use]
pub fn check_oracles(&self) -> Vec<OracleOutcome> {
self.oracles
.iter()
.map(|case| {
let value = self.evaluate(case.x);
let error = value.as_ref().ok().map(|v| (v - case.expected).abs());
OracleOutcome {
value,
error,
passed: error.is_some_and(|e| e <= case.tolerance),
}
})
.collect()
}
#[must_use]
pub fn ingest(&self, points: &[(f64, f64)], policy: &IngestPolicy) -> Verdict {
if points.is_empty() {
return Verdict::NoEvidence;
}
let outside = points
.iter()
.filter(|(x, y)| !self.domain.contains(*x) || !y.is_finite())
.count();
if outside > 0 {
return Verdict::OutOfRange { outside };
}
let band = policy.abs_tolerance.max(self.residual.rms);
let rms = self.residual_over(points).rms;
if rms <= band {
return Verdict::Supports { rms };
}
let mut combined = self.evidence.clone();
combined.extend_from_slice(points);
if let Ok(refit) = Self::fit_polynomial(&combined, self.degree(), self.provenance.clone()) {
if refit.residual.rms <= band {
let updated = Self {
oracles: self.oracles.clone(),
..refit
};
return Verdict::ParameterUpdate {
previous_rms: rms,
updated: Box::new(updated),
};
}
}
if rms <= band * policy.break_factor {
Verdict::ResidualGrew { rms }
} else {
Verdict::Breaks { rms }
}
}
}
#[cfg(test)]
mod law_id_encoding {
use super::{update_length_prefixed, Digest, Sha256};
#[test]
fn length_prefix_prevents_field_confusion() {
let prefixed = |a: &[u8], b: &[u8]| {
let mut h = Sha256::new();
update_length_prefixed(&mut h, a);
update_length_prefixed(&mut h, b);
h.finalize()
};
let plain = |a: &[u8], b: &[u8]| {
let mut h = Sha256::new();
h.update(a);
h.update(b);
h.finalize()
};
assert_eq!(
plain(b"ab", b"c"),
plain(b"a", b"bc"),
"premise: without a prefix these two field splits are one byte string"
);
assert_ne!(
prefixed(b"ab", b"c"),
prefixed(b"a", b"bc"),
"the length prefix must separate adjacent fields"
);
}
#[test]
fn length_prefix_is_eight_big_endian_bytes() {
let mut h = Sha256::new();
update_length_prefixed(&mut h, b"xy");
let via_helper = h.finalize();
let mut h = Sha256::new();
h.update(2_u64.to_be_bytes());
h.update(b"xy");
assert_eq!(via_helper, h.finalize());
}
}