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#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn iss_floor_matches_formula() {
200 let v = ultimate_floor(2.0, 1.0, 1.0, 0.5).unwrap();
202 assert!((v - 1.0).abs() < 1e-12);
203 }
204
205 #[test]
206 fn iss_floor_requires_alpha_gt_beta() {
207 assert!(ultimate_floor(1.0, 1.0, 1.0, 0.5).is_err());
208 assert!(ultimate_floor(1.0, 2.0, 1.0, 0.5).is_err());
209 }
210
211 #[test]
212 fn iss_floor_requires_positive_mu() {
213 assert!(ultimate_floor(2.0, 1.0, 1.0, 0.0).is_err());
214 }
215
216 #[test]
217 fn step_size_bound_is_min_of_two_terms() {
218 let bound = step_size_upper_bound(2.0, 1.0, 1.0, 10.0).unwrap();
220 assert!((bound - 0.05).abs() < 1e-12);
221 assert!(validate_step_size(0.04, 2.0, 1.0, 1.0, 10.0).unwrap());
222 assert!(!validate_step_size(0.06, 2.0, 1.0, 1.0, 10.0).unwrap());
223 }
224
225 #[test]
226 fn claim_resolves_floor() {
227 let mut claim = StabilityClaim::not_claimed("toy");
228 claim.alpha = Some(2.0);
229 claim.beta = Some(1.0);
230 claim.delta = Some(1.0);
231 claim.mu = Some(0.5);
232 assert!(claim.claims_floor());
233 let f = claim.resolve_floor().unwrap().unwrap();
234 assert!((f - 1.0).abs() < 1e-12);
235 assert_eq!(claim.ultimate_floor, Some(f));
236 }
237
238 #[test]
239 fn not_claimed_has_no_floor() {
240 let mut claim = StabilityClaim::not_claimed("coding");
241 assert!(!claim.claims_floor());
242 assert_eq!(claim.resolve_floor().unwrap(), None);
243 }
244}