Skip to main content

use_control_error/
lib.rs

1#![forbid(unsafe_code)]
2//! Control error calculation primitives.
3//!
4//! The crate provides a narrow set of helpers over scalar `f64` control error
5//! values.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_control_error::{absolute_error, deadband, percent_error};
11//!
12//! assert_eq!(absolute_error(10.0, 9.0), 1.0);
13//! assert_eq!(deadband(0.05, 0.1).unwrap(), 0.0);
14//! assert_eq!(percent_error(10.0, 8.0), Some(20.0));
15//! ```
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ControlErrorError {
19    InvalidError,
20    InvalidThreshold,
21}
22
23pub fn error(setpoint: f64, measured: f64) -> f64 {
24    setpoint - measured
25}
26
27pub fn absolute_error(setpoint: f64, measured: f64) -> f64 {
28    error(setpoint, measured).abs()
29}
30
31pub fn relative_error(setpoint: f64, measured: f64) -> Option<f64> {
32    if !setpoint.is_finite() || !measured.is_finite() || setpoint == 0.0 {
33        return None;
34    }
35
36    Some(error(setpoint, measured) / setpoint)
37}
38
39pub fn percent_error(setpoint: f64, measured: f64) -> Option<f64> {
40    relative_error(setpoint, measured).map(|value| value * 100.0)
41}
42
43pub fn deadband(error: f64, threshold: f64) -> Result<f64, ControlErrorError> {
44    if !error.is_finite() {
45        return Err(ControlErrorError::InvalidError);
46    }
47
48    if !threshold.is_finite() || threshold < 0.0 {
49        return Err(ControlErrorError::InvalidThreshold);
50    }
51
52    if error.abs() <= threshold {
53        Ok(0.0)
54    } else {
55        Ok(error)
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::{
62        ControlErrorError, absolute_error, deadband, error, percent_error, relative_error,
63    };
64
65    #[test]
66    fn computes_error_forms() {
67        assert_eq!(error(10.0, 8.0), 2.0);
68        assert_eq!(absolute_error(10.0, 8.0), 2.0);
69        assert_eq!(relative_error(10.0, 8.0), Some(0.2));
70        assert_eq!(percent_error(10.0, 8.0), Some(20.0));
71    }
72
73    #[test]
74    fn applies_deadband() {
75        assert_eq!(deadband(0.05, 0.1).unwrap(), 0.0);
76        assert_eq!(deadband(-0.2, 0.1).unwrap(), -0.2);
77    }
78
79    #[test]
80    fn rejects_invalid_values() {
81        assert_eq!(relative_error(0.0, 1.0), None);
82        assert_eq!(percent_error(f64::NAN, 1.0), None);
83        assert_eq!(
84            deadband(f64::NAN, 0.1),
85            Err(ControlErrorError::InvalidError)
86        );
87        assert_eq!(
88            deadband(0.1, -0.1),
89            Err(ControlErrorError::InvalidThreshold)
90        );
91    }
92}