1use serde::{Deserialize, Serialize};
23
24use crate::error::{Result, SdkError};
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29pub struct StabilityClaim {
30 pub claim_id: String,
31 pub analysis_domain: String,
33 pub alpha: Option<f64>,
34 pub beta: Option<f64>,
35 pub delta: Option<f64>,
36 pub mu: Option<f64>,
38 pub smoothness_l: Option<f64>,
39 pub eta: Option<f64>,
40 pub ultimate_floor: Option<f64>,
42 pub evidence_refs: Vec<String>,
43}
44
45impl StabilityClaim {
46 pub fn not_claimed(analysis_domain: impl Into<String>) -> Self {
47 Self {
48 claim_id: uuid::Uuid::new_v4().to_string(),
49 analysis_domain: analysis_domain.into(),
50 alpha: None,
51 beta: None,
52 delta: None,
53 mu: None,
54 smoothness_l: None,
55 eta: None,
56 ultimate_floor: None,
57 evidence_refs: Vec::new(),
58 }
59 }
60
61 pub fn claims_floor(&self) -> bool {
63 self.alpha.is_some() && self.beta.is_some() && self.delta.is_some() && self.mu.is_some()
64 }
65
66 pub fn resolve_floor(&mut self) -> Result<Option<f64>> {
69 let floor = match (self.alpha, self.beta, self.delta, self.mu) {
70 (Some(a), Some(b), Some(d), Some(m)) => Some(ultimate_floor(a, b, d, m)?),
71 _ => None,
72 };
73 self.ultimate_floor = floor;
74 Ok(floor)
75 }
76}
77
78pub fn ultimate_floor(alpha: f64, beta: f64, delta: f64, mu: f64) -> Result<f64> {
82 for (name, v) in [
83 ("alpha", alpha),
84 ("beta", beta),
85 ("delta", delta),
86 ("mu", mu),
87 ] {
88 if !v.is_finite() {
89 return Err(SdkError::InvalidStability(format!("{name} is not finite")));
90 }
91 }
92 if alpha <= beta {
93 return Err(SdkError::InvalidStability(format!(
94 "ISS floor requires alpha > beta (got alpha={alpha}, beta={beta})"
95 )));
96 }
97 if mu <= 0.0 {
98 return Err(SdkError::InvalidStability(format!(
99 "ISS floor requires mu > 0 (got mu={mu})"
100 )));
101 }
102 let gap = alpha - beta;
103 Ok((delta * delta) / (2.0 * gap * gap * mu))
104}
105
106pub fn step_size_upper_bound(alpha: f64, beta: f64, smoothness_l: f64, mu: f64) -> Result<f64> {
112 if alpha <= beta {
113 return Err(SdkError::InvalidStability(
114 "step-size bound requires alpha > beta".into(),
115 ));
116 }
117 if smoothness_l <= 0.0 || mu <= 0.0 {
118 return Err(SdkError::InvalidStability(
119 "step-size bound requires L > 0 and mu > 0".into(),
120 ));
121 }
122 let gap = alpha - beta;
123 let sum = alpha + beta;
124 let smoothness_term = (2.0 * gap) / (smoothness_l * sum * sum);
125 let curvature_term = 1.0 / (2.0 * mu * gap);
126 Ok(smoothness_term.min(curvature_term))
127}
128
129pub fn c_eta(alpha: f64, beta: f64, smoothness_l: f64, eta: f64) -> f64 {
131 let sum = alpha + beta;
132 (alpha - beta) - (smoothness_l * eta * sum * sum) / 2.0
133}
134
135pub fn geometric_factor(eta: f64, c: f64, mu: f64) -> f64 {
138 1.0 - 2.0 * eta * c * mu
139}
140
141pub fn validate_step_size(
143 eta: f64,
144 alpha: f64,
145 beta: f64,
146 smoothness_l: f64,
147 mu: f64,
148) -> Result<bool> {
149 if eta <= 0.0 {
150 return Ok(false);
151 }
152 let bound = step_size_upper_bound(alpha, beta, smoothness_l, mu)?;
153 Ok(eta < bound)
154}
155
156#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158pub struct StabilityParameters {
159 pub energy_tolerance: f64,
160 pub rho_gate: f64,
162 pub predicted_descent: Option<f64>,
164 pub mu: Option<f64>,
166 pub alpha: Option<f64>,
167 pub beta: Option<f64>,
168 pub delta: Option<f64>,
169 pub smoothness_l: Option<f64>,
170 pub eta: Option<f64>,
171 pub ultimate_floor: Option<f64>,
173}
174
175impl StabilityParameters {
176 pub fn measured(rho_gate: f64, energy_tolerance: f64) -> Self {
179 Self {
180 energy_tolerance,
181 rho_gate,
182 predicted_descent: None,
183 mu: None,
184 alpha: None,
185 beta: None,
186 delta: None,
187 smoothness_l: None,
188 eta: None,
189 ultimate_floor: None,
190 }
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
202#[serde(rename_all = "snake_case")]
203pub enum BoundMode {
204 Deterministic,
205 InExpectation,
206 WithProbability(f64),
207}
208
209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
211pub struct CertifiedBound {
212 pub value: f64,
213 pub evidence_refs: Vec<String>,
215}
216
217#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
222pub struct RealizabilityClaim {
223 pub coordinate_schema: String,
224 pub projection_id: String,
225 pub encoder_id: String,
226 pub norm_id: String,
227 pub delta_r_bound: CertifiedBound,
229 pub delta_r_mode: BoundMode,
230 pub analysis_domain: String,
231 pub invariant_domain_evidence: Vec<String>,
232}
233
234pub trait ProxyGeometry: Send + Sync {
239 fn proposed_proxy(&self, candidate_id: &str) -> Result<Vec<f64>>;
241 fn encode_realized(&self, candidate_id: &str) -> Result<Vec<f64>>;
243 fn residual_norm(&self, proposed: &[f64], realized: &[f64]) -> Result<f64>;
245 fn stability_claim(&self) -> StabilityClaim;
246 fn realizability_claim(&self) -> RealizabilityClaim;
247}
248
249#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
251#[serde(rename_all = "snake_case")]
252pub enum AnalyticPathVerdict {
253 Admitted,
255 Refused { missing: String },
258}
259
260pub fn admit_analytic_path(
268 stability: &StabilityClaim,
269 realizability: &RealizabilityClaim,
270) -> AnalyticPathVerdict {
271 let constants = [
272 ("alpha", stability.alpha),
273 ("beta", stability.beta),
274 ("mu", stability.mu),
275 ("smoothness_l", stability.smoothness_l),
276 ("eta", stability.eta),
277 ];
278 for (name, value) in constants {
279 match value {
280 Some(v) if v.is_finite() => {}
281 _ => {
282 return AnalyticPathVerdict::Refused {
283 missing: format!("StabilityClaim.{name} is not a validated finite constant"),
284 }
285 }
286 }
287 }
288 if stability.analysis_domain != realizability.analysis_domain {
289 return AnalyticPathVerdict::Refused {
290 missing: "stability and realizability claims cover different analysis domains".into(),
291 };
292 }
293 if !realizability.delta_r_bound.value.is_finite() || realizability.delta_r_bound.value < 0.0 {
294 return AnalyticPathVerdict::Refused {
295 missing: "RealizabilityClaim.delta_r_bound is not a finite non-negative bound".into(),
296 };
297 }
298 if realizability.delta_r_bound.evidence_refs.is_empty() {
299 return AnalyticPathVerdict::Refused {
300 missing: "delta_r bound carries no validation evidence".into(),
301 };
302 }
303 let (alpha, beta) = (stability.alpha.unwrap(), stability.beta.unwrap());
304 let (l, eta, mu) = (
305 stability.smoothness_l.unwrap(),
306 stability.eta.unwrap(),
307 stability.mu.unwrap(),
308 );
309 match validate_step_size(eta, alpha, beta, l, mu) {
310 Ok(true) => AnalyticPathVerdict::Admitted,
311 Ok(false) => AnalyticPathVerdict::Refused {
312 missing: format!(
313 "Theorem 12.1 step-size precondition failed: eta = {eta} exceeds the \
314 admissible bound"
315 ),
316 },
317 Err(e) => AnalyticPathVerdict::Refused {
318 missing: format!("Theorem 12.1 step-size precondition failed: {e}"),
319 },
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn iss_floor_matches_formula() {
329 let v = ultimate_floor(2.0, 1.0, 1.0, 0.5).unwrap();
331 assert!((v - 1.0).abs() < 1e-12);
332 }
333
334 #[test]
335 fn iss_floor_requires_alpha_gt_beta() {
336 assert!(ultimate_floor(1.0, 1.0, 1.0, 0.5).is_err());
337 assert!(ultimate_floor(1.0, 2.0, 1.0, 0.5).is_err());
338 }
339
340 #[test]
341 fn iss_floor_requires_positive_mu() {
342 assert!(ultimate_floor(2.0, 1.0, 1.0, 0.0).is_err());
343 }
344
345 #[test]
346 fn step_size_bound_is_min_of_two_terms() {
347 let bound = step_size_upper_bound(2.0, 1.0, 1.0, 10.0).unwrap();
349 assert!((bound - 0.05).abs() < 1e-12);
350 assert!(validate_step_size(0.04, 2.0, 1.0, 1.0, 10.0).unwrap());
351 assert!(!validate_step_size(0.06, 2.0, 1.0, 1.0, 10.0).unwrap());
352 }
353
354 #[test]
355 fn claim_resolves_floor() {
356 let mut claim = StabilityClaim::not_claimed("toy");
357 claim.alpha = Some(2.0);
358 claim.beta = Some(1.0);
359 claim.delta = Some(1.0);
360 claim.mu = Some(0.5);
361 assert!(claim.claims_floor());
362 let f = claim.resolve_floor().unwrap().unwrap();
363 assert!((f - 1.0).abs() < 1e-12);
364 assert_eq!(claim.ultimate_floor, Some(f));
365 }
366
367 #[test]
368 fn not_claimed_has_no_floor() {
369 let mut claim = StabilityClaim::not_claimed("coding");
370 assert!(!claim.claims_floor());
371 assert_eq!(claim.resolve_floor().unwrap(), None);
372 }
373
374 fn full_claims() -> (StabilityClaim, RealizabilityClaim) {
375 let stability = StabilityClaim {
376 claim_id: "c".into(),
377 analysis_domain: "unit-disk".into(),
378 alpha: Some(1.0),
379 beta: Some(0.2),
380 delta: None,
381 mu: Some(0.5),
382 smoothness_l: Some(1.0),
383 eta: Some(0.5),
384 ultimate_floor: None,
385 evidence_refs: vec!["ev1".into()],
386 };
387 let realizability = RealizabilityClaim {
388 coordinate_schema: "r1".into(),
389 projection_id: "round".into(),
390 encoder_id: "identity".into(),
391 norm_id: "l2".into(),
392 delta_r_bound: CertifiedBound {
393 value: 0.125,
394 evidence_refs: vec!["ev2".into()],
395 },
396 delta_r_mode: BoundMode::Deterministic,
397 analysis_domain: "unit-disk".into(),
398 invariant_domain_evidence: vec!["ev3".into()],
399 };
400 (stability, realizability)
401 }
402
403 #[test]
404 fn analytic_path_admits_only_with_every_hypothesis() {
405 let (stability, realizability) = full_claims();
406 assert_eq!(
407 admit_analytic_path(&stability, &realizability),
408 AnalyticPathVerdict::Admitted
409 );
410 }
411
412 #[test]
413 fn an_inadmissible_step_size_refuses_the_analytic_path() {
414 let (mut stability, realizability) = full_claims();
415 stability.eta = Some(100.0);
417 assert!(matches!(
418 admit_analytic_path(&stability, &realizability),
419 AnalyticPathVerdict::Refused { missing } if missing.contains("step-size")
420 ));
421 }
422
423 #[test]
424 fn a_not_claimed_constant_refuses_the_analytic_path() {
425 let (mut stability, realizability) = full_claims();
426 stability.mu = None;
427 assert!(matches!(
428 admit_analytic_path(&stability, &realizability),
429 AnalyticPathVerdict::Refused { missing } if missing.contains("mu")
430 ));
431 }
432
433 #[test]
434 fn mismatched_analysis_domains_refuse_the_analytic_path() {
435 let (stability, mut realizability) = full_claims();
436 realizability.analysis_domain = "other".into();
437 assert!(matches!(
438 admit_analytic_path(&stability, &realizability),
439 AnalyticPathVerdict::Refused { .. }
440 ));
441 }
442
443 #[test]
444 fn an_unevidenced_delta_r_bound_refuses_the_analytic_path() {
445 let (stability, mut realizability) = full_claims();
446 realizability.delta_r_bound.evidence_refs.clear();
447 assert!(matches!(
448 admit_analytic_path(&stability, &realizability),
449 AnalyticPathVerdict::Refused { missing } if missing.contains("evidence")
450 ));
451 }
452}