1use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct RegionContractV1 {
14 pub region_id: String,
16 pub region_digest_id: String,
18 pub node_ids: Vec<String>,
20 pub hyperedge_ids: Vec<String>,
22 pub constraint_ids: Vec<String>,
24 pub bounded_default_unit_of_work: bool,
26 pub max_iterations: u32,
28 pub convergence_tolerance: f64,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ResidualEnvelopeV1 {
36 pub region_id: String,
38 pub expected: HashMap<String, f64>,
40 pub observed: HashMap<String, f64>,
42 pub residuals: HashMap<String, f64>,
44 pub max_residual: f64,
46 pub within_tolerance: bool,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct SyndromeEnvelopeV1 {
54 pub region_id: String,
56 pub violated_constraint_ids: Vec<String>,
58 pub violation_descriptions: Vec<String>,
60 pub recoverable: bool,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct RegionConvergenceReportV1 {
68 pub region_id: String,
70 pub iterations: u32,
72 pub convergence_state: ConvergenceState,
74 pub residual_history: Vec<f64>,
76 pub stop_reason: ConvergenceStopReason,
78 pub elapsed_ms: u64,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
84pub enum ConvergenceState {
85 Converged,
87 Oscillating,
89 Diverging,
91 MaxIterationsReached,
93 SyndromeDetected,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
99pub enum ConvergenceStopReason {
100 Converged,
102 MaxIterations,
104 OscillationDetected,
106 DivergenceDetected,
108 Syndrome,
110 BudgetExhausted,
112}
113
114impl RegionConvergenceReportV1 {
115 pub fn is_converged(&self) -> bool {
117 matches!(self.convergence_state, ConvergenceState::Converged)
118 }
119
120 pub fn is_problematic(&self) -> bool {
122 matches!(
123 self.convergence_state,
124 ConvergenceState::Oscillating
125 | ConvergenceState::Diverging
126 | ConvergenceState::SyndromeDetected
127 )
128 }
129}
130
131pub fn classify_convergence(
133 residual_history: &[f64],
134 tolerance: f64,
135 max_iterations: u32,
136) -> (ConvergenceState, ConvergenceStopReason) {
137 if residual_history.is_empty() {
138 return (
139 ConvergenceState::MaxIterationsReached,
140 ConvergenceStopReason::MaxIterations,
141 );
142 }
143
144 let last = *residual_history.last().unwrap();
145
146 if last < tolerance {
147 return (
148 ConvergenceState::Converged,
149 ConvergenceStopReason::Converged,
150 );
151 }
152
153 if residual_history.len() >= max_iterations as usize {
154 if residual_history.len() >= 4 {
156 let n = residual_history.len();
157 let a = residual_history[n - 4];
158 let b = residual_history[n - 3];
159 let c = residual_history[n - 2];
160 let d = residual_history[n - 1];
161 if (a > b && b < c && c > d) || (a < b && b > c && c < d) {
162 return (
163 ConvergenceState::Oscillating,
164 ConvergenceStopReason::OscillationDetected,
165 );
166 }
167 }
168
169 if residual_history.len() >= 3 {
171 let n = residual_history.len();
172 if residual_history[n - 3] < residual_history[n - 2]
173 && residual_history[n - 2] < residual_history[n - 1]
174 {
175 return (
176 ConvergenceState::Diverging,
177 ConvergenceStopReason::DivergenceDetected,
178 );
179 }
180 }
181
182 return (
183 ConvergenceState::MaxIterationsReached,
184 ConvergenceStopReason::MaxIterations,
185 );
186 }
187
188 (
189 ConvergenceState::MaxIterationsReached,
190 ConvergenceStopReason::MaxIterations,
191 )
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn converged_state_is_converged() {
200 let report = RegionConvergenceReportV1 {
201 region_id: "r1".into(),
202 iterations: 5,
203 convergence_state: ConvergenceState::Converged,
204 residual_history: vec![1.0, 0.5, 0.1, 0.01, 0.001],
205 stop_reason: ConvergenceStopReason::Converged,
206 elapsed_ms: 42,
207 };
208 assert!(report.is_converged());
209 assert!(!report.is_problematic());
210 }
211
212 #[test]
213 fn oscillating_state_is_problematic() {
214 let report = RegionConvergenceReportV1 {
215 region_id: "r1".into(),
216 iterations: 10,
217 convergence_state: ConvergenceState::Oscillating,
218 residual_history: vec![0.5, 0.1, 0.5, 0.1, 0.5],
219 stop_reason: ConvergenceStopReason::OscillationDetected,
220 elapsed_ms: 100,
221 };
222 assert!(!report.is_converged());
223 assert!(report.is_problematic());
224 }
225
226 #[test]
227 fn classify_convergence_detects_converged() {
228 let history = vec![1.0, 0.5, 0.1, 0.001];
229 let (state, reason) = classify_convergence(&history, 0.01, 100);
230 assert_eq!(state, ConvergenceState::Converged);
231 assert_eq!(reason, ConvergenceStopReason::Converged);
232 }
233
234 #[test]
235 fn classify_convergence_detects_oscillation() {
236 let history = vec![0.5, 0.1, 0.5, 0.1, 0.5];
237 let (state, reason) = classify_convergence(&history, 0.01, 4);
238 assert_eq!(state, ConvergenceState::Oscillating);
239 assert_eq!(reason, ConvergenceStopReason::OscillationDetected);
240 }
241
242 #[test]
243 fn classify_convergence_detects_divergence() {
244 let history = vec![0.1, 0.2, 0.5, 0.8];
245 let (state, reason) = classify_convergence(&history, 0.01, 3);
246 assert_eq!(state, ConvergenceState::Diverging);
247 assert_eq!(reason, ConvergenceStopReason::DivergenceDetected);
248 }
249
250 #[test]
251 fn classify_convergence_detects_max_iterations() {
252 let history = vec![0.5, 0.4, 0.3, 0.25, 0.2];
253 let (state, reason) = classify_convergence(&history, 0.01, 4);
254 assert_eq!(state, ConvergenceState::MaxIterationsReached);
255 assert_eq!(reason, ConvergenceStopReason::MaxIterations);
256 }
257
258 #[test]
259 fn region_contract_serializes() {
260 let contract = RegionContractV1 {
261 region_id: "region-01".into(),
262 region_digest_id: "digest-01".into(),
263 node_ids: vec!["node-a".into(), "node-b".into()],
264 hyperedge_ids: vec!["edge-01".into()],
265 constraint_ids: vec!["constraint:edge-01".into()],
266 bounded_default_unit_of_work: true,
267 max_iterations: 100,
268 convergence_tolerance: 0.01,
269 };
270 let json = serde_json::to_string(&contract).unwrap();
271 let deserialized: RegionContractV1 = serde_json::from_str(&json).unwrap();
272 assert_eq!(contract.region_id, deserialized.region_id);
273 assert_eq!(contract.max_iterations, deserialized.max_iterations);
274 }
275
276 #[test]
277 fn residual_envelope_computes_within_tolerance() {
278 let mut expected = HashMap::new();
279 expected.insert("a".into(), 1.0);
280 expected.insert("b".into(), 2.0);
281 let mut observed = HashMap::new();
282 observed.insert("a".into(), 1.01);
283 observed.insert("b".into(), 2.02);
284
285 let mut residuals = HashMap::new();
286 residuals.insert("a".into(), 0.01);
287 residuals.insert("b".into(), 0.02);
288
289 let envelope = ResidualEnvelopeV1 {
290 region_id: "r1".into(),
291 expected,
292 observed,
293 residuals,
294 max_residual: 0.02,
295 within_tolerance: true,
296 };
297 assert!(envelope.within_tolerance);
298 }
299
300 #[test]
301 fn syndrome_envelope_marks_recoverable() {
302 let syndrome = SyndromeEnvelopeV1 {
303 region_id: "r1".into(),
304 violated_constraint_ids: vec!["c1".into()],
305 violation_descriptions: vec!["constraint c1 violated".into()],
306 recoverable: true,
307 };
308 assert!(syndrome.recoverable);
309 }
310}