use_system_response/
lib.rs1#![forbid(unsafe_code)]
2#[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}