1use crate::regime::WorkloadPhase;
14use crate::residual::ResidualSign;
15
16#[derive(Debug, Clone, Copy)]
23pub struct AdmissibilityEnvelope {
24 pub residual_lower: f64,
26 pub residual_upper: f64,
28 pub drift_limit: f64,
30 pub slew_limit: f64,
32 pub phase: WorkloadPhase,
34 pub boundary_fraction: f64,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum EnvelopePosition {
43 Interior,
45 BoundaryZone,
47 Exterior,
49}
50
51impl AdmissibilityEnvelope {
52 pub fn new(
56 residual_lower: f64,
57 residual_upper: f64,
58 drift_limit: f64,
59 slew_limit: f64,
60 phase: WorkloadPhase,
61 boundary_fraction: f64,
62 ) -> Self {
63 Self {
64 residual_lower,
65 residual_upper,
66 drift_limit,
67 slew_limit,
68 phase,
69 boundary_fraction: boundary_fraction.clamp(0.5, 0.99),
70 }
71 }
72
73 pub fn symmetric(
75 half_width: f64,
76 drift_limit: f64,
77 slew_limit: f64,
78 phase: WorkloadPhase,
79 ) -> Self {
80 Self::new(-half_width, half_width, drift_limit, slew_limit, phase, 0.8)
81 }
82
83 pub fn classify(&self, sign: &ResidualSign) -> EnvelopePosition {
88 let r_pos = self.classify_scalar(sign.residual, self.residual_lower, self.residual_upper);
89 let d_pos = self.classify_symmetric(sign.drift, self.drift_limit);
90 let s_pos = self.classify_symmetric(sign.slew, self.slew_limit);
91
92 worst_position(worst_position(r_pos, d_pos), s_pos)
94 }
95
96 fn classify_scalar(&self, value: f64, lower: f64, upper: f64) -> EnvelopePosition {
98 let range = upper - lower;
99 if range <= 0.0 {
100 return EnvelopePosition::Exterior;
101 }
102 let boundary_lower = lower + range * (1.0 - self.boundary_fraction) / 2.0;
103 let boundary_upper = upper - range * (1.0 - self.boundary_fraction) / 2.0;
104
105 if value < lower || value > upper {
106 EnvelopePosition::Exterior
107 } else if value < boundary_lower || value > boundary_upper {
108 EnvelopePosition::BoundaryZone
109 } else {
110 EnvelopePosition::Interior
111 }
112 }
113
114 fn classify_symmetric(&self, value: f64, limit: f64) -> EnvelopePosition {
116 if limit <= 0.0 {
117 return if value.abs() > 0.0 {
118 EnvelopePosition::Exterior
119 } else {
120 EnvelopePosition::Interior
121 };
122 }
123 let abs_val = value.abs();
124 let boundary = limit * self.boundary_fraction;
125
126 if abs_val > limit {
127 EnvelopePosition::Exterior
128 } else if abs_val > boundary {
129 EnvelopePosition::BoundaryZone
130 } else {
131 EnvelopePosition::Interior
132 }
133 }
134
135 pub fn residual_width(&self) -> f64 {
137 self.residual_upper - self.residual_lower
138 }
139
140 pub fn fractional_position(&self, residual: f64) -> f64 {
143 let center = (self.residual_upper + self.residual_lower) / 2.0;
144 let half_width = self.residual_width() / 2.0;
145 if half_width <= 0.0 {
146 return f64::INFINITY;
147 }
148 (residual - center).abs() / half_width
149 }
150}
151
152fn worst_position(a: EnvelopePosition, b: EnvelopePosition) -> EnvelopePosition {
154 match (a, b) {
155 (EnvelopePosition::Exterior, _) | (_, EnvelopePosition::Exterior) => {
156 EnvelopePosition::Exterior
157 }
158 (EnvelopePosition::BoundaryZone, _) | (_, EnvelopePosition::BoundaryZone) => {
159 EnvelopePosition::BoundaryZone
160 }
161 (EnvelopePosition::Interior, EnvelopePosition::Interior) => EnvelopePosition::Interior,
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use crate::residual::ResidualSource;
169
170 fn sign(r: f64, d: f64, s: f64) -> ResidualSign {
171 ResidualSign {
172 residual: r,
173 drift: d,
174 slew: s,
175 timestamp_ns: 0,
176 source: ResidualSource::Latency,
177 }
178 }
179
180 #[test]
181 fn test_interior_classification() {
182 let env = AdmissibilityEnvelope::symmetric(10.0, 1.0, 0.5, WorkloadPhase::SteadyState);
183 assert_eq!(
184 env.classify(&sign(0.0, 0.0, 0.0)),
185 EnvelopePosition::Interior
186 );
187 assert_eq!(
188 env.classify(&sign(5.0, 0.3, 0.1)),
189 EnvelopePosition::Interior
190 );
191 }
192
193 #[test]
194 fn test_boundary_classification() {
195 let env = AdmissibilityEnvelope::symmetric(10.0, 1.0, 0.5, WorkloadPhase::SteadyState);
196 assert_eq!(
198 env.classify(&sign(9.0, 0.0, 0.0)),
199 EnvelopePosition::BoundaryZone
200 );
201 }
202
203 #[test]
204 fn test_exterior_classification() {
205 let env = AdmissibilityEnvelope::symmetric(10.0, 1.0, 0.5, WorkloadPhase::SteadyState);
206 assert_eq!(
207 env.classify(&sign(11.0, 0.0, 0.0)),
208 EnvelopePosition::Exterior
209 );
210 assert_eq!(
211 env.classify(&sign(0.0, 1.5, 0.0)),
212 EnvelopePosition::Exterior
213 );
214 }
215
216 #[test]
217 fn test_drift_triggers_boundary() {
218 let env = AdmissibilityEnvelope::symmetric(10.0, 1.0, 0.5, WorkloadPhase::SteadyState);
219 assert_eq!(
221 env.classify(&sign(0.0, 0.9, 0.0)),
222 EnvelopePosition::BoundaryZone
223 );
224 }
225}