use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegionContractV1 {
pub region_id: String,
pub region_digest_id: String,
pub node_ids: Vec<String>,
pub hyperedge_ids: Vec<String>,
pub constraint_ids: Vec<String>,
pub bounded_default_unit_of_work: bool,
pub max_iterations: u32,
pub convergence_tolerance: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResidualEnvelopeV1 {
pub region_id: String,
pub expected: HashMap<String, f64>,
pub observed: HashMap<String, f64>,
pub residuals: HashMap<String, f64>,
pub max_residual: f64,
pub within_tolerance: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyndromeEnvelopeV1 {
pub region_id: String,
pub violated_constraint_ids: Vec<String>,
pub violation_descriptions: Vec<String>,
pub recoverable: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegionConvergenceReportV1 {
pub region_id: String,
pub iterations: u32,
pub convergence_state: ConvergenceState,
pub residual_history: Vec<f64>,
pub stop_reason: ConvergenceStopReason,
pub elapsed_ms: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConvergenceState {
Converged,
Oscillating,
Diverging,
MaxIterationsReached,
SyndromeDetected,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConvergenceStopReason {
Converged,
MaxIterations,
OscillationDetected,
DivergenceDetected,
Syndrome,
BudgetExhausted,
}
impl RegionConvergenceReportV1 {
pub fn is_converged(&self) -> bool {
matches!(self.convergence_state, ConvergenceState::Converged)
}
pub fn is_problematic(&self) -> bool {
matches!(
self.convergence_state,
ConvergenceState::Oscillating
| ConvergenceState::Diverging
| ConvergenceState::SyndromeDetected
)
}
}
pub fn classify_convergence(
residual_history: &[f64],
tolerance: f64,
max_iterations: u32,
) -> (ConvergenceState, ConvergenceStopReason) {
if residual_history.is_empty() {
return (
ConvergenceState::MaxIterationsReached,
ConvergenceStopReason::MaxIterations,
);
}
let last = *residual_history.last().unwrap();
if last < tolerance {
return (
ConvergenceState::Converged,
ConvergenceStopReason::Converged,
);
}
if residual_history.len() >= max_iterations as usize {
if residual_history.len() >= 4 {
let n = residual_history.len();
let a = residual_history[n - 4];
let b = residual_history[n - 3];
let c = residual_history[n - 2];
let d = residual_history[n - 1];
if (a > b && b < c && c > d) || (a < b && b > c && c < d) {
return (
ConvergenceState::Oscillating,
ConvergenceStopReason::OscillationDetected,
);
}
}
if residual_history.len() >= 3 {
let n = residual_history.len();
if residual_history[n - 3] < residual_history[n - 2]
&& residual_history[n - 2] < residual_history[n - 1]
{
return (
ConvergenceState::Diverging,
ConvergenceStopReason::DivergenceDetected,
);
}
}
return (
ConvergenceState::MaxIterationsReached,
ConvergenceStopReason::MaxIterations,
);
}
(
ConvergenceState::MaxIterationsReached,
ConvergenceStopReason::MaxIterations,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converged_state_is_converged() {
let report = RegionConvergenceReportV1 {
region_id: "r1".into(),
iterations: 5,
convergence_state: ConvergenceState::Converged,
residual_history: vec![1.0, 0.5, 0.1, 0.01, 0.001],
stop_reason: ConvergenceStopReason::Converged,
elapsed_ms: 42,
};
assert!(report.is_converged());
assert!(!report.is_problematic());
}
#[test]
fn oscillating_state_is_problematic() {
let report = RegionConvergenceReportV1 {
region_id: "r1".into(),
iterations: 10,
convergence_state: ConvergenceState::Oscillating,
residual_history: vec![0.5, 0.1, 0.5, 0.1, 0.5],
stop_reason: ConvergenceStopReason::OscillationDetected,
elapsed_ms: 100,
};
assert!(!report.is_converged());
assert!(report.is_problematic());
}
#[test]
fn classify_convergence_detects_converged() {
let history = vec![1.0, 0.5, 0.1, 0.001];
let (state, reason) = classify_convergence(&history, 0.01, 100);
assert_eq!(state, ConvergenceState::Converged);
assert_eq!(reason, ConvergenceStopReason::Converged);
}
#[test]
fn classify_convergence_detects_oscillation() {
let history = vec![0.5, 0.1, 0.5, 0.1, 0.5];
let (state, reason) = classify_convergence(&history, 0.01, 4);
assert_eq!(state, ConvergenceState::Oscillating);
assert_eq!(reason, ConvergenceStopReason::OscillationDetected);
}
#[test]
fn classify_convergence_detects_divergence() {
let history = vec![0.1, 0.2, 0.5, 0.8];
let (state, reason) = classify_convergence(&history, 0.01, 3);
assert_eq!(state, ConvergenceState::Diverging);
assert_eq!(reason, ConvergenceStopReason::DivergenceDetected);
}
#[test]
fn classify_convergence_detects_max_iterations() {
let history = vec![0.5, 0.4, 0.3, 0.25, 0.2];
let (state, reason) = classify_convergence(&history, 0.01, 4);
assert_eq!(state, ConvergenceState::MaxIterationsReached);
assert_eq!(reason, ConvergenceStopReason::MaxIterations);
}
#[test]
fn region_contract_serializes() {
let contract = RegionContractV1 {
region_id: "region-01".into(),
region_digest_id: "digest-01".into(),
node_ids: vec!["node-a".into(), "node-b".into()],
hyperedge_ids: vec!["edge-01".into()],
constraint_ids: vec!["constraint:edge-01".into()],
bounded_default_unit_of_work: true,
max_iterations: 100,
convergence_tolerance: 0.01,
};
let json = serde_json::to_string(&contract).unwrap();
let deserialized: RegionContractV1 = serde_json::from_str(&json).unwrap();
assert_eq!(contract.region_id, deserialized.region_id);
assert_eq!(contract.max_iterations, deserialized.max_iterations);
}
#[test]
fn residual_envelope_computes_within_tolerance() {
let mut expected = HashMap::new();
expected.insert("a".into(), 1.0);
expected.insert("b".into(), 2.0);
let mut observed = HashMap::new();
observed.insert("a".into(), 1.01);
observed.insert("b".into(), 2.02);
let mut residuals = HashMap::new();
residuals.insert("a".into(), 0.01);
residuals.insert("b".into(), 0.02);
let envelope = ResidualEnvelopeV1 {
region_id: "r1".into(),
expected,
observed,
residuals,
max_residual: 0.02,
within_tolerance: true,
};
assert!(envelope.within_tolerance);
}
#[test]
fn syndrome_envelope_marks_recoverable() {
let syndrome = SyndromeEnvelopeV1 {
region_id: "r1".into(),
violated_constraint_ids: vec!["c1".into()],
violation_descriptions: vec!["constraint c1 violated".into()],
recoverable: true,
};
assert!(syndrome.recoverable);
}
}