use crate::regime::WorkloadPhase;
use crate::residual::ResidualSign;
#[derive(Debug, Clone, Copy)]
pub struct AdmissibilityEnvelope {
pub residual_lower: f64,
pub residual_upper: f64,
pub drift_limit: f64,
pub slew_limit: f64,
pub phase: WorkloadPhase,
pub boundary_fraction: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnvelopePosition {
Interior,
BoundaryZone,
Exterior,
}
impl AdmissibilityEnvelope {
pub fn new(
residual_lower: f64,
residual_upper: f64,
drift_limit: f64,
slew_limit: f64,
phase: WorkloadPhase,
boundary_fraction: f64,
) -> Self {
Self {
residual_lower,
residual_upper,
drift_limit,
slew_limit,
phase,
boundary_fraction: boundary_fraction.clamp(0.5, 0.99),
}
}
pub fn symmetric(
half_width: f64,
drift_limit: f64,
slew_limit: f64,
phase: WorkloadPhase,
) -> Self {
Self::new(-half_width, half_width, drift_limit, slew_limit, phase, 0.8)
}
pub fn classify(&self, sign: &ResidualSign) -> EnvelopePosition {
let r_pos = self.classify_scalar(sign.residual, self.residual_lower, self.residual_upper);
let d_pos = self.classify_symmetric(sign.drift, self.drift_limit);
let s_pos = self.classify_symmetric(sign.slew, self.slew_limit);
worst_position(worst_position(r_pos, d_pos), s_pos)
}
fn classify_scalar(&self, value: f64, lower: f64, upper: f64) -> EnvelopePosition {
let range = upper - lower;
if range <= 0.0 {
return EnvelopePosition::Exterior;
}
let boundary_lower = lower + range * (1.0 - self.boundary_fraction) / 2.0;
let boundary_upper = upper - range * (1.0 - self.boundary_fraction) / 2.0;
if value < lower || value > upper {
EnvelopePosition::Exterior
} else if value < boundary_lower || value > boundary_upper {
EnvelopePosition::BoundaryZone
} else {
EnvelopePosition::Interior
}
}
fn classify_symmetric(&self, value: f64, limit: f64) -> EnvelopePosition {
if limit <= 0.0 {
return if value.abs() > 0.0 {
EnvelopePosition::Exterior
} else {
EnvelopePosition::Interior
};
}
let abs_val = value.abs();
let boundary = limit * self.boundary_fraction;
if abs_val > limit {
EnvelopePosition::Exterior
} else if abs_val > boundary {
EnvelopePosition::BoundaryZone
} else {
EnvelopePosition::Interior
}
}
pub fn residual_width(&self) -> f64 {
self.residual_upper - self.residual_lower
}
pub fn fractional_position(&self, residual: f64) -> f64 {
let center = (self.residual_upper + self.residual_lower) / 2.0;
let half_width = self.residual_width() / 2.0;
if half_width <= 0.0 {
return f64::INFINITY;
}
(residual - center).abs() / half_width
}
}
fn worst_position(a: EnvelopePosition, b: EnvelopePosition) -> EnvelopePosition {
match (a, b) {
(EnvelopePosition::Exterior, _) | (_, EnvelopePosition::Exterior) => {
EnvelopePosition::Exterior
}
(EnvelopePosition::BoundaryZone, _) | (_, EnvelopePosition::BoundaryZone) => {
EnvelopePosition::BoundaryZone
}
(EnvelopePosition::Interior, EnvelopePosition::Interior) => EnvelopePosition::Interior,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::residual::ResidualSource;
fn sign(r: f64, d: f64, s: f64) -> ResidualSign {
ResidualSign {
residual: r,
drift: d,
slew: s,
timestamp_ns: 0,
source: ResidualSource::Latency,
}
}
#[test]
fn test_interior_classification() {
let env = AdmissibilityEnvelope::symmetric(10.0, 1.0, 0.5, WorkloadPhase::SteadyState);
assert_eq!(
env.classify(&sign(0.0, 0.0, 0.0)),
EnvelopePosition::Interior
);
assert_eq!(
env.classify(&sign(5.0, 0.3, 0.1)),
EnvelopePosition::Interior
);
}
#[test]
fn test_boundary_classification() {
let env = AdmissibilityEnvelope::symmetric(10.0, 1.0, 0.5, WorkloadPhase::SteadyState);
assert_eq!(
env.classify(&sign(9.0, 0.0, 0.0)),
EnvelopePosition::BoundaryZone
);
}
#[test]
fn test_exterior_classification() {
let env = AdmissibilityEnvelope::symmetric(10.0, 1.0, 0.5, WorkloadPhase::SteadyState);
assert_eq!(
env.classify(&sign(11.0, 0.0, 0.0)),
EnvelopePosition::Exterior
);
assert_eq!(
env.classify(&sign(0.0, 1.5, 0.0)),
EnvelopePosition::Exterior
);
}
#[test]
fn test_drift_triggers_boundary() {
let env = AdmissibilityEnvelope::symmetric(10.0, 1.0, 0.5, WorkloadPhase::SteadyState);
assert_eq!(
env.classify(&sign(0.0, 0.9, 0.0)),
EnvelopePosition::BoundaryZone
);
}
}