use crate::core::scalar::ControlScalar;
use crate::fuzzy::inference::SugenoEngine;
use crate::fuzzy::membership::MembershipFn;
use crate::fuzzy::FuzzyError;
#[derive(Debug, Clone, Copy)]
pub struct FuzzyPidConfig<S: ControlScalar> {
pub kp_base: S,
pub ki_base: S,
pub kd_base: S,
pub kp_delta_max: S,
pub ki_delta_max: S,
pub kd_delta_max: S,
pub out_max: Option<S>,
}
impl<S: ControlScalar> FuzzyPidConfig<S> {
pub fn new(kp_base: S, ki_base: S, kd_base: S) -> Self {
Self {
kp_base,
ki_base,
kd_base,
kp_delta_max: S::ZERO,
ki_delta_max: S::ZERO,
kd_delta_max: S::ZERO,
out_max: None,
}
}
pub fn with_deltas(mut self, kp_delta_max: S, ki_delta_max: S, kd_delta_max: S) -> Self {
self.kp_delta_max = kp_delta_max;
self.ki_delta_max = ki_delta_max;
self.kd_delta_max = kd_delta_max;
self
}
pub fn with_out_max(mut self, out_max: S) -> Self {
self.out_max = Some(out_max);
self
}
}
pub struct FuzzyPid<S: ControlScalar, const R: usize> {
config: FuzzyPidConfig<S>,
kp_engine: SugenoEngine<S, R>,
ki_engine: SugenoEngine<S, R>,
kd_engine: SugenoEngine<S, R>,
integral: S,
prev_error: Option<S>,
}
impl<S: ControlScalar, const R: usize> FuzzyPid<S, R> {
pub fn new(
config: FuzzyPidConfig<S>,
kp_engine: SugenoEngine<S, R>,
ki_engine: SugenoEngine<S, R>,
kd_engine: SugenoEngine<S, R>,
) -> Self {
Self {
config,
kp_engine,
ki_engine,
kd_engine,
integral: S::ZERO,
prev_error: None,
}
}
pub fn reset(&mut self) {
self.integral = S::ZERO;
self.prev_error = None;
}
pub fn update(
&mut self,
setpoint: S,
measurement: S,
dt: S,
input_mfs: &[&[&dyn MembershipFn<S>]],
) -> Result<S, FuzzyError> {
if dt <= S::ZERO {
return Err(FuzzyError::InvalidParameter(
"FuzzyPid: dt must be positive",
));
}
let error = setpoint - measurement;
let error_rate = match self.prev_error {
Some(pe) => (error - pe) / dt,
None => S::ZERO,
};
let crisp_inputs = [error, error_rate];
let delta_kp = self
.kp_engine
.infer(&crisp_inputs, input_mfs)
.unwrap_or(S::ZERO);
let delta_ki = self
.ki_engine
.infer(&crisp_inputs, input_mfs)
.unwrap_or(S::ZERO);
let delta_kd = self
.kd_engine
.infer(&crisp_inputs, input_mfs)
.unwrap_or(S::ZERO);
let kp = (self.config.kp_base + delta_kp * self.config.kp_delta_max)
.clamp_val(S::ZERO, S::from_f64(f64::MAX));
let ki = (self.config.ki_base + delta_ki * self.config.ki_delta_max)
.clamp_val(S::ZERO, S::from_f64(f64::MAX));
let kd = (self.config.kd_base + delta_kd * self.config.kd_delta_max)
.clamp_val(S::ZERO, S::from_f64(f64::MAX));
self.integral += ki * error * dt;
let derivative = kd * error_rate;
let output_raw = kp * error + self.integral + derivative;
let output = match self.config.out_max {
Some(max_val) => output_raw.clamp_val(-max_val, max_val),
None => output_raw,
};
self.prev_error = Some(error);
Ok(output)
}
pub fn integral(&self) -> S {
self.integral
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fuzzy::inference::SugenoRule;
use crate::fuzzy::membership::Trapezoidal;
use crate::fuzzy::rule_base::{Antecedent, TNorm};
fn build_constant_sugeno_engine(output: f64) -> SugenoEngine<f64, 4> {
let universal_mf = Trapezoidal::new(-1e6_f64, -1e5, 1e5, 1e6).unwrap();
let _ = universal_mf;
let mut engine: SugenoEngine<f64, 4> = SugenoEngine::new(TNorm::Min);
let mut ant = Antecedent::new();
ant.add(0, 0).unwrap(); engine
.add_rule(SugenoRule::new(ant, 0.0, 0.0, output))
.unwrap();
engine
}
#[test]
fn fuzzy_pid_proportional_only_no_adjustment() {
let config = FuzzyPidConfig::new(2.0_f64, 0.0, 0.0);
let kp_eng = build_constant_sugeno_engine(0.0);
let ki_eng = build_constant_sugeno_engine(0.0);
let kd_eng = build_constant_sugeno_engine(0.0);
let mut controller = FuzzyPid::new(config, kp_eng, ki_eng, kd_eng);
let universal_mf = Trapezoidal::new(-1e6_f64, -1e5, 1e5, 1e6).unwrap();
let err_mfs: &[&dyn MembershipFn<f64>] = &[&universal_mf];
let rate_mfs: &[&dyn MembershipFn<f64>] = &[&universal_mf];
let input_mfs: &[&[&dyn MembershipFn<f64>]] = &[err_mfs, rate_mfs];
let output = controller.update(10.0, 4.0, 0.01, input_mfs).unwrap();
assert!((output - 12.0).abs() < 1e-9, "Expected 12.0, got {output}");
}
#[test]
fn fuzzy_pid_integrates_over_time() {
let config = FuzzyPidConfig::new(0.0_f64, 1.0, 0.0);
let kp_eng = build_constant_sugeno_engine(0.0);
let ki_eng = build_constant_sugeno_engine(0.0);
let kd_eng = build_constant_sugeno_engine(0.0);
let mut controller = FuzzyPid::new(config, kp_eng, ki_eng, kd_eng);
let universal_mf = Trapezoidal::new(-1e6_f64, -1e5, 1e5, 1e6).unwrap();
let err_mfs: &[&dyn MembershipFn<f64>] = &[&universal_mf];
let rate_mfs: &[&dyn MembershipFn<f64>] = &[&universal_mf];
let input_mfs: &[&[&dyn MembershipFn<f64>]] = &[err_mfs, rate_mfs];
for _ in 0..10 {
controller.update(5.0, 0.0, 0.1, input_mfs).unwrap();
}
assert!(
(controller.integral() - 5.0).abs() < 1e-9,
"Integral should be 5.0, got {}",
controller.integral()
);
}
#[test]
fn fuzzy_pid_reset_clears_state() {
let config = FuzzyPidConfig::new(1.0_f64, 1.0, 0.0);
let kp_eng = build_constant_sugeno_engine(0.0);
let ki_eng = build_constant_sugeno_engine(0.0);
let kd_eng = build_constant_sugeno_engine(0.0);
let mut controller = FuzzyPid::new(config, kp_eng, ki_eng, kd_eng);
let universal_mf = Trapezoidal::new(-1e6_f64, -1e5, 1e5, 1e6).unwrap();
let err_mfs: &[&dyn MembershipFn<f64>] = &[&universal_mf];
let rate_mfs: &[&dyn MembershipFn<f64>] = &[&universal_mf];
let input_mfs: &[&[&dyn MembershipFn<f64>]] = &[err_mfs, rate_mfs];
controller.update(10.0, 0.0, 0.1, input_mfs).unwrap();
assert!(controller.integral() != 0.0);
controller.reset();
assert_eq!(controller.integral(), 0.0);
}
#[test]
fn fuzzy_pid_invalid_dt_returns_error() {
let config = FuzzyPidConfig::new(1.0_f64, 0.0, 0.0);
let kp_eng = build_constant_sugeno_engine(0.0);
let ki_eng = build_constant_sugeno_engine(0.0);
let kd_eng = build_constant_sugeno_engine(0.0);
let mut controller = FuzzyPid::new(config, kp_eng, ki_eng, kd_eng);
let universal_mf = Trapezoidal::new(-1e6_f64, -1e5, 1e5, 1e6).unwrap();
let err_mfs: &[&dyn MembershipFn<f64>] = &[&universal_mf];
let rate_mfs: &[&dyn MembershipFn<f64>] = &[&universal_mf];
let input_mfs: &[&[&dyn MembershipFn<f64>]] = &[err_mfs, rate_mfs];
let result = controller.update(10.0, 0.0, 0.0, input_mfs);
assert!(
matches!(result, Err(FuzzyError::InvalidParameter(_))),
"Expected InvalidParameter for dt=0"
);
}
#[test]
fn fuzzy_pid_output_clamped() {
let config = FuzzyPidConfig::new(100.0_f64, 0.0, 0.0).with_out_max(5.0);
let kp_eng = build_constant_sugeno_engine(0.0);
let ki_eng = build_constant_sugeno_engine(0.0);
let kd_eng = build_constant_sugeno_engine(0.0);
let mut controller = FuzzyPid::new(config, kp_eng, ki_eng, kd_eng);
let universal_mf = Trapezoidal::new(-1e6_f64, -1e5, 1e5, 1e6).unwrap();
let err_mfs: &[&dyn MembershipFn<f64>] = &[&universal_mf];
let rate_mfs: &[&dyn MembershipFn<f64>] = &[&universal_mf];
let input_mfs: &[&[&dyn MembershipFn<f64>]] = &[err_mfs, rate_mfs];
let output = controller.update(10.0, 0.0, 0.01, input_mfs).unwrap();
assert_eq!(output, 5.0, "Expected clamped output 5.0, got {output}");
}
}