use std::f64::consts::{PI, TAU};
#[derive(Debug, Clone)]
pub struct Contract {
pub name: String,
pub domain_radius: f64,
pub defect: f64,
pub lipschitz: f64,
}
impl Contract {
}
#[derive(Debug, Clone)]
pub struct ComposedContract {
pub total_defect: f64,
pub per_stage_contribution: Vec<f64>,
pub domain_ok: bool,
}
pub fn compose_contracts(chain: &[Contract]) -> ComposedContract {
let n = chain.len();
let mut suffix = vec![1.0_f64; n + 1];
for j in (0..n).rev() {
suffix[j] = suffix[j + 1] * chain[j].lipschitz;
}
let mut per_stage_contribution = vec![0.0_f64; n];
let mut total_defect = 0.0_f64;
for j in 0..n {
let c = chain[j].defect * suffix[j + 1];
per_stage_contribution[j] = c;
total_defect += c;
}
let mut domain_ok = true;
let mut accumulated = 0.0_f64;
for stage in chain.iter() {
if accumulated > stage.domain_radius {
domain_ok = false;
}
accumulated = stage.defect + stage.lipschitz * accumulated;
}
ComposedContract {
total_defect,
per_stage_contribution,
domain_ok,
}
}
#[derive(Debug, Clone)]
pub struct HolonomyReport {
pub loop_len: usize,
pub net_sign: i8,
pub net_angle: f64,
pub is_trivial: bool,
pub angle_tolerance: f64,
}
fn wrap_pi(x: f64) -> f64 {
let w = (x + PI).rem_euclid(TAU) - PI;
if w <= -PI { w + TAU } else { w }
}
pub fn invert_o2_edge(edge: (i8, f64)) -> (i8, f64) {
let s = if edge.0 >= 0 { 1i8 } else { -1i8 };
(s, -(s as f64) * edge.1)
}
pub fn loop_holonomy(edges: &[(i8, f64)], defects: &[f64]) -> HolonomyReport {
let mut acc_sign = 1i8;
let mut acc_angle = 0.0_f64;
for &(sign, angle) in edges.iter() {
let s = if sign >= 0 { 1i8 } else { -1i8 };
acc_angle = (s as f64) * acc_angle + angle;
acc_sign *= s;
}
let net_angle = wrap_pi(acc_angle);
let angle_tolerance = defects
.iter()
.copied()
.filter(|v| v.is_finite() && *v >= 0.0)
.sum::<f64>();
let is_trivial = acc_sign == 1 && net_angle.abs() <= angle_tolerance;
HolonomyReport {
loop_len: edges.len(),
net_sign: acc_sign,
net_angle,
is_trivial,
angle_tolerance,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn c(name: &str, domain_radius: f64, defect: f64, lipschitz: f64) -> Contract {
Contract {
name: name.to_string(),
domain_radius,
defect,
lipschitz,
}
}
#[test]
fn composition_matches_closed_form_sum() {
let chain = [
c("a", 10.0, 0.1, 2.0),
c("b", 10.0, 0.2, 3.0),
c("c", 10.0, 0.4, 5.0),
];
let out = compose_contracts(&chain);
assert!((out.total_defect - 2.9).abs() < 1e-12);
assert!((out.per_stage_contribution[0] - 1.5).abs() < 1e-12);
assert!((out.per_stage_contribution[1] - 1.0).abs() < 1e-12);
assert!((out.per_stage_contribution[2] - 0.4).abs() < 1e-12);
let s: f64 = out.per_stage_contribution.iter().sum();
assert!((s - out.total_defect).abs() < 1e-12);
assert!(out.domain_ok);
}
#[test]
fn lipschitz_gt_one_amplifies_early_defects_more() {
let chain = [
c("a", 100.0, 0.3, 2.0),
c("b", 100.0, 0.3, 2.0),
c("c", 100.0, 0.3, 2.0),
c("d", 100.0, 0.3, 2.0),
];
let out = compose_contracts(&chain);
for w in out.per_stage_contribution.windows(2) {
assert!(w[0] > w[1], "contribution not strictly decreasing: {w:?}");
}
assert!((out.total_defect - 4.5).abs() < 1e-12);
}
#[test]
fn empty_chain_is_identity() {
let out = compose_contracts(&[]);
assert_eq!(out.total_defect, 0.0);
assert!(out.per_stage_contribution.is_empty());
assert!(out.domain_ok);
}
#[test]
fn rotations_summing_to_zero_are_trivial() {
let edges = [(1i8, 2.0), (1, 2.0), (1, TAU - 4.0)];
let defects = [1e-6, 1e-6, 1e-6];
let r = loop_holonomy(&edges, &defects);
assert_eq!(r.net_sign, 1);
assert!(r.net_angle.abs() < 1e-9, "net_angle = {}", r.net_angle);
assert!(r.is_trivial);
}
#[test]
fn small_net_rotation_with_tiny_defects_is_nontrivial() {
let edges = [(1i8, PI / 7.0)];
let defects = [1e-4];
let r = loop_holonomy(&edges, &defects);
assert_eq!(r.net_sign, 1);
assert!((r.net_angle - PI / 7.0).abs() < 1e-12);
assert!(!r.is_trivial);
}
#[test]
fn two_reflections_compose_to_a_rotation() {
let edges = [(-1i8, 0.3), (-1, 0.9)];
let defects = [1e-6, 1e-6];
let r = loop_holonomy(&edges, &defects);
assert_eq!(r.net_sign, 1);
assert!((r.net_angle - 0.6).abs() < 1e-12);
}
#[test]
fn single_reflection_stays_a_reflection() {
let edges = [(1i8, 0.2), (-1, 0.4)];
let defects = [1e-6, 1e-6];
let r = loop_holonomy(&edges, &defects);
assert_eq!(r.net_sign, -1);
assert!(!r.is_trivial);
}
#[test]
fn tolerance_above_net_angle_cannot_exclude_identity() {
let edges = [(1i8, PI / 7.0)];
let defects = [PI / 7.0 + 0.01];
let r = loop_holonomy(&edges, &defects);
assert!(r.angle_tolerance > (PI / 7.0));
assert!(r.is_trivial);
}
#[test]
fn empty_loop_is_trivial_identity() {
let r = loop_holonomy(&[], &[]);
assert_eq!(r.loop_len, 0);
assert_eq!(r.net_sign, 1);
assert_eq!(r.net_angle, 0.0);
assert!(r.is_trivial);
}
#[test]
fn invert_o2_edge_round_trips_to_identity() {
use crate::inference::contracts::invert_o2_edge;
let e = (1i8, 0.7);
let inv = invert_o2_edge(e);
let r = loop_holonomy(&[e, inv], &[0.0, 0.0]);
assert_eq!(r.net_sign, 1);
assert!(r.net_angle.abs() < 1e-12);
assert!(r.is_trivial);
let f = (-1i8, 1.1);
let finv = invert_o2_edge(f);
assert_eq!(finv.0, -1);
let r2 = loop_holonomy(&[f, finv], &[0.0, 0.0]);
assert_eq!(r2.net_sign, 1);
assert!(r2.net_angle.abs() < 1e-12);
}
}