Skip to main content

use_system_response/
lib.rs

1#![forbid(unsafe_code)]
2//! Simple system response helpers.
3//!
4//! The crate starts with a first-order step response model and a helper for
5//! sampling the response at fixed time intervals.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_system_response::{first_order_response, sample_first_order_response};
11//!
12//! let initial = first_order_response(0.0, 1.0, 1.0, 0.0).unwrap();
13//! let samples = sample_first_order_response(0.0, 1.0, 1.0, 0.5, 2).unwrap();
14//!
15//! assert_eq!(initial, 0.0);
16//! assert_eq!(samples.len(), 3);
17//! assert!(samples[1].value > samples[0].value);
18//! ```
19
20#[derive(Debug, Clone, Copy, PartialEq)]
21pub struct StepResponsePoint {
22    pub time: f64,
23    pub value: f64,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum SystemResponseError {
28    InvalidInput,
29    InvalidTimeConstant,
30    InvalidTime,
31    InvalidTimestep,
32    NonFiniteResponse,
33}
34
35pub fn first_order_response(
36    initial: f64,
37    target: f64,
38    time_constant: f64,
39    time: f64,
40) -> Result<f64, SystemResponseError> {
41    if !initial.is_finite() || !target.is_finite() {
42        return Err(SystemResponseError::InvalidInput);
43    }
44
45    if !time_constant.is_finite() || time_constant <= 0.0 {
46        return Err(SystemResponseError::InvalidTimeConstant);
47    }
48
49    if !time.is_finite() || time < 0.0 {
50        return Err(SystemResponseError::InvalidTime);
51    }
52
53    let response = initial + (target - initial) * (1.0 - (-time / time_constant).exp());
54    if !response.is_finite() {
55        return Err(SystemResponseError::NonFiniteResponse);
56    }
57
58    Ok(response)
59}
60
61pub fn sample_first_order_response(
62    initial: f64,
63    target: f64,
64    time_constant: f64,
65    dt: f64,
66    steps: usize,
67) -> Result<Vec<StepResponsePoint>, SystemResponseError> {
68    if !dt.is_finite() || dt <= 0.0 {
69        return Err(SystemResponseError::InvalidTimestep);
70    }
71
72    let mut points = Vec::with_capacity(steps + 1);
73    for step in 0..=steps {
74        let time = step as f64 * dt;
75        let value = first_order_response(initial, target, time_constant, time)?;
76        points.push(StepResponsePoint { time, value });
77    }
78
79    Ok(points)
80}
81
82#[cfg(test)]
83mod tests {
84    use super::{SystemResponseError, first_order_response, sample_first_order_response};
85
86    #[test]
87    fn first_order_response_matches_expected_limits() {
88        assert_eq!(first_order_response(0.0, 1.0, 1.0, 0.0).unwrap(), 0.0);
89
90        let value = first_order_response(0.0, 1.0, 1.0, 1.0).unwrap();
91        assert!((value - (1.0 - (-1.0f64).exp())).abs() < 1e-12);
92    }
93
94    #[test]
95    fn samples_first_order_response_over_time() {
96        let samples = sample_first_order_response(0.0, 1.0, 1.0, 0.5, 3).unwrap();
97
98        assert_eq!(samples.len(), 4);
99        assert_eq!(samples[0].time, 0.0);
100        assert!(samples[1].value > samples[0].value);
101        assert!(samples[3].value < 1.0);
102    }
103
104    #[test]
105    fn rejects_invalid_inputs() {
106        assert_eq!(
107            first_order_response(f64::NAN, 1.0, 1.0, 0.0),
108            Err(SystemResponseError::InvalidInput)
109        );
110        assert_eq!(
111            first_order_response(0.0, 1.0, 0.0, 1.0),
112            Err(SystemResponseError::InvalidTimeConstant)
113        );
114        assert_eq!(
115            sample_first_order_response(0.0, 1.0, 1.0, 0.0, 3),
116            Err(SystemResponseError::InvalidTimestep)
117        );
118    }
119}