use crate::inference::layer_transport::{ChartTopology, FittedTransport};
use crate::inference::transport_class::CircleTransportReport;
use ndarray::Array1;
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 {
fn validate(&self, stage: usize) -> Result<(), String> {
for (label, v) in [
("domain_radius", self.domain_radius),
("defect", self.defect),
("lipschitz", self.lipschitz),
] {
if !v.is_finite() {
return Err(format!(
"contract stage {stage} ({}): {label} = {v} is not finite",
self.name
));
}
if v < 0.0 {
return Err(format!(
"contract stage {stage} ({}): {label} = {v} is negative",
self.name
));
}
}
Ok(())
}
pub fn from_transport(
t: &FittedTransport,
name: impl Into<String>,
grid: usize,
) -> Result<Contract, String> {
let (lo, hi) = match t.topology_from {
ChartTopology::Circle => (0.0, TAU),
ChartTopology::Interval { lo, hi } => (lo, hi),
};
let g = grid.max(2);
let pts: Vec<f64> = (0..g)
.map(|k| lo + (hi - lo) * (k as f64) / ((g - 1) as f64))
.collect();
let deriv = t.derivative(Array1::from_vec(pts).view())?;
let lipschitz = deriv.iter().fold(0.0_f64, |m, &v| m.max(v.abs()));
Ok(Contract {
name: name.into(),
domain_radius: hi - lo,
defect: t.residual_rms,
lipschitz,
})
}
}
#[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,
}
}
pub fn compose_with_trace(
chain: &[Contract],
entry_radii: &[f64],
) -> Result<ComposedContract, String> {
if chain.len() != entry_radii.len() {
return Err(format!(
"compose_with_trace: {} contracts but {} entry radii",
chain.len(),
entry_radii.len()
));
}
for (j, c) in chain.iter().enumerate() {
c.validate(j)?;
}
for (j, &r) in entry_radii.iter().enumerate() {
if !r.is_finite() || r < 0.0 {
return Err(format!(
"compose_with_trace: entry_radii[{j}] = {r} invalid"
));
}
}
let composed = compose_contracts(chain);
let mut violations: Vec<String> = Vec::new();
let mut accumulated = 0.0_f64;
for (j, stage) in chain.iter().enumerate() {
let required = entry_radii[j] + accumulated;
if required > stage.domain_radius {
let overflow = required - stage.domain_radius;
violations.push(format!(
"stage {j} ({}): entry radius {} + accumulated error {} = {} exceeds \
domain_radius {} by {}",
stage.name, entry_radii[j], accumulated, required, stage.domain_radius, overflow
));
}
accumulated = stage.defect + stage.lipschitz * accumulated;
}
if !violations.is_empty() {
return Err(format!(
"compose_with_trace: {} domain violation(s) (bound total_defect = {} is unchanged): {}",
violations.len(),
composed.total_defect,
violations.join("; ")
));
}
Ok(ComposedContract {
domain_ok: true,
..composed
})
}
#[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,
}
}
pub fn holonomy_from_transports(loop_edges: &[CircleTransportReport]) -> HolonomyReport {
let edges: Vec<(i8, f64)> = loop_edges.iter().map(|r| (r.winding, r.phase)).collect();
let defects: Vec<f64> = loop_edges.iter().map(|r| r.defect).collect();
loop_holonomy(&edges, &defects)
}
#[cfg(test)]
mod tests {
use super::*;
fn lcg(seed: &mut u64) -> f64 {
*seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((*seed >> 11) as f64) / ((1u64 << 53) as f64)
}
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 trace_reports_domain_violation_at_right_stage() {
let chain = [
c("a", 100.0, 1.0, 10.0),
c("b", 100.0, 1.0, 10.0),
c("c", 5.0, 1.0, 1.0),
];
let entry = [0.0, 0.0, 0.0];
let res = compose_with_trace(&chain, &entry);
let err = res.expect_err("expected a domain violation");
assert!(err.contains("stage 2"), "wrong stage reported: {err}");
assert!(err.contains("by 6"), "wrong overflow reported: {err}");
assert!(err.contains("total_defect"));
}
#[test]
fn trace_feasible_chain_is_ok() {
let chain = [
c("a", 100.0, 0.1, 1.0),
c("b", 100.0, 0.1, 1.0),
c("c", 100.0, 0.1, 1.0),
];
let entry = [1.0, 1.0, 1.0];
let out = compose_with_trace(&chain, &entry).expect("feasible");
assert!(out.domain_ok);
assert!((out.total_defect - 0.3).abs() < 1e-12);
}
#[test]
fn trace_length_mismatch_errors() {
let chain = [c("a", 1.0, 0.1, 1.0)];
assert!(compose_with_trace(&chain, &[1.0, 2.0]).is_err());
}
#[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);
}
#[test]
fn contract_from_near_isometric_transport() {
use crate::inference::layer_transport::{ChartTopology, fit_transport_map};
let mut s = 11u64;
let n = 256usize;
let (mut from, mut to) = (Vec::with_capacity(n), Vec::with_capacity(n));
for _ in 0..n {
let th = lcg(&mut s) * TAU;
from.push(th);
to.push((th + 0.6).rem_euclid(TAU));
}
let fit = fit_transport_map(
Array1::from_vec(from).view(),
Array1::from_vec(to).view(),
ChartTopology::Circle,
ChartTopology::Circle,
)
.expect("transport fit");
let c = Contract::from_transport(&fit, "L_a->L_b", 128).expect("contract");
assert!((c.domain_radius - TAU).abs() < 1e-9);
assert!(
(c.lipschitz - 1.0).abs() < 0.05,
"lipschitz = {}",
c.lipschitz
);
assert!(c.defect < 0.05, "defect = {}", c.defect);
let composed = compose_contracts(&[c.clone(), c]);
assert!(composed.total_defect.is_finite());
}
#[test]
fn adapter_matches_plain_interface() {
let reports = [
CircleTransportReport {
layer_from: 0,
layer_to: 1,
n_samples: 128,
winding: -1,
phase: 0.3,
defect: 1e-6,
resultant_shift: 0.0,
resultant_reflect: 1.0,
class: crate::inference::transport_class::CircleTransportClass::Reflect,
},
CircleTransportReport {
layer_from: 1,
layer_to: 2,
n_samples: 128,
winding: -1,
phase: 0.9,
defect: 1e-6,
resultant_shift: 0.0,
resultant_reflect: 1.0,
class: crate::inference::transport_class::CircleTransportClass::Reflect,
},
];
let via_adapter = holonomy_from_transports(&reports);
let via_plain = loop_holonomy(&[(-1, 0.3), (-1, 0.9)], &[1e-6, 1e-6]);
assert_eq!(via_adapter.net_sign, via_plain.net_sign);
assert!((via_adapter.net_angle - via_plain.net_angle).abs() < 1e-15);
assert_eq!(via_adapter.is_trivial, via_plain.is_trivial);
}
}