Skip to main content

use_stability/
lib.rs

1#![forbid(unsafe_code)]
2//! Simple stability-oriented helpers.
3//!
4//! The crate provides scalar classification helpers and a settling-time helper
5//! over sampled response values.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_stability::{classify_gain, is_bounded, settling_time, Stability};
11//!
12//! assert_eq!(classify_gain(0.8, 1.0).unwrap(), Stability::Stable);
13//! assert!(is_bounded(&[0.5, -0.5, 0.25], 1.0).unwrap());
14//! assert_eq!(settling_time(&[1.4, 1.05, 1.01, 1.0], 1.0, 0.05, 0.5).unwrap(), Some(0.5));
15//! ```
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Stability {
19    Stable,
20    Unstable,
21    Marginal,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum StabilityError {
26    InvalidGain,
27    InvalidLimit,
28    InvalidValues,
29    InvalidBound,
30    InvalidTarget,
31    InvalidTolerance,
32    InvalidTimestep,
33}
34
35pub fn classify_gain(gain: f64, stable_limit: f64) -> Result<Stability, StabilityError> {
36    if !gain.is_finite() {
37        return Err(StabilityError::InvalidGain);
38    }
39
40    if !stable_limit.is_finite() || stable_limit <= 0.0 {
41        return Err(StabilityError::InvalidLimit);
42    }
43
44    let magnitude = gain.abs();
45    if magnitude < stable_limit {
46        Ok(Stability::Stable)
47    } else if magnitude > stable_limit {
48        Ok(Stability::Unstable)
49    } else {
50        Ok(Stability::Marginal)
51    }
52}
53
54pub fn is_bounded(values: &[f64], bound: f64) -> Result<bool, StabilityError> {
55    if !bound.is_finite() || bound <= 0.0 {
56        return Err(StabilityError::InvalidBound);
57    }
58
59    if values.iter().any(|value| !value.is_finite()) {
60        return Err(StabilityError::InvalidValues);
61    }
62
63    Ok(values.iter().all(|value| value.abs() <= bound))
64}
65
66pub fn settling_time(
67    values: &[f64],
68    target: f64,
69    tolerance: f64,
70    dt: f64,
71) -> Result<Option<f64>, StabilityError> {
72    if !target.is_finite() {
73        return Err(StabilityError::InvalidTarget);
74    }
75
76    if !tolerance.is_finite() || tolerance < 0.0 {
77        return Err(StabilityError::InvalidTolerance);
78    }
79
80    if !dt.is_finite() || dt <= 0.0 {
81        return Err(StabilityError::InvalidTimestep);
82    }
83
84    if values.iter().any(|value| !value.is_finite()) {
85        return Err(StabilityError::InvalidValues);
86    }
87
88    for start_index in 0..values.len() {
89        if values[start_index..]
90            .iter()
91            .all(|value| within_tolerance(*value, target, tolerance))
92        {
93            return Ok(Some(start_index as f64 * dt));
94        }
95    }
96
97    Ok(None)
98}
99
100fn within_tolerance(value: f64, target: f64, tolerance: f64) -> bool {
101    let scale = 1.0_f64
102        .max(value.abs())
103        .max(target.abs())
104        .max(tolerance.abs());
105
106    (value - target).abs() <= tolerance + f64::EPSILON * scale * 8.0
107}
108
109#[cfg(test)]
110mod tests {
111    use super::{Stability, StabilityError, classify_gain, is_bounded, settling_time};
112
113    #[test]
114    fn classifies_gain_magnitude() {
115        assert_eq!(classify_gain(0.8, 1.0).unwrap(), Stability::Stable);
116        assert_eq!(classify_gain(1.0, 1.0).unwrap(), Stability::Marginal);
117        assert_eq!(classify_gain(1.2, 1.0).unwrap(), Stability::Unstable);
118    }
119
120    #[test]
121    fn detects_bounded_series() {
122        assert!(is_bounded(&[0.5, -0.5, 0.25], 1.0).unwrap());
123        assert!(!is_bounded(&[0.5, 1.5], 1.0).unwrap());
124    }
125
126    #[test]
127    fn computes_settling_time_when_response_stays_within_tolerance() {
128        let time = settling_time(&[1.4, 1.05, 1.01, 1.0], 1.0, 0.05, 0.5).unwrap();
129        assert_eq!(time, Some(0.5));
130
131        let no_settle = settling_time(&[1.4, 1.2, 1.1], 1.0, 0.05, 0.5).unwrap();
132        assert_eq!(no_settle, None);
133    }
134
135    #[test]
136    fn rejects_invalid_inputs() {
137        assert_eq!(
138            classify_gain(f64::NAN, 1.0),
139            Err(StabilityError::InvalidGain)
140        );
141        assert_eq!(is_bounded(&[1.0], 0.0), Err(StabilityError::InvalidBound));
142        assert_eq!(
143            settling_time(&[1.0], 1.0, -0.1, 0.5),
144            Err(StabilityError::InvalidTolerance)
145        );
146        assert_eq!(
147            settling_time(&[1.0], 1.0, 0.1, 0.0),
148            Err(StabilityError::InvalidTimestep)
149        );
150    }
151}