use num_traits::Float;
use crate::neural::{
rbf_network::{RbfCenter, RbfNetwork},
NeuralError,
};
#[derive(Debug, Clone, Copy)]
pub struct NeuralPidConfig<S: Float + Copy> {
pub base_kp: S,
pub base_ki: S,
pub base_kd: S,
pub lr: S,
pub adjustment_limit: S,
pub integral_limit: S,
pub output_limit: S,
}
impl<S: Float + Copy> NeuralPidConfig<S> {
pub fn new(base_kp: S, base_ki: S, base_kd: S, lr: S, adjustment_limit: S) -> Self {
let large = S::from(1e6).unwrap_or(S::one());
Self {
base_kp,
base_ki,
base_kd,
lr,
adjustment_limit,
integral_limit: large,
output_limit: large,
}
}
pub fn with_integral_limit(mut self, limit: S) -> Self {
self.integral_limit = limit;
self
}
pub fn with_output_limit(mut self, limit: S) -> Self {
self.output_limit = limit;
self
}
}
#[derive(Clone)]
pub struct NeuralPid<S: Float + Copy, const H: usize> {
pub config: NeuralPidConfig<S>,
rbf_kp: RbfNetwork<S, 3, H>,
rbf_ki: RbfNetwork<S, 3, H>,
rbf_kd: RbfNetwork<S, 3, H>,
integral: S,
prev_error: S,
prev_output: S,
first_step: bool,
}
impl<S: Float + Copy, const H: usize> NeuralPid<S, H> {
pub fn new(config: NeuralPidConfig<S>, centers: [RbfCenter<S, 3>; H]) -> Self {
Self {
config,
rbf_kp: RbfNetwork::new(centers),
rbf_ki: RbfNetwork::new(centers),
rbf_kd: RbfNetwork::new(centers),
integral: S::zero(),
prev_error: S::zero(),
prev_output: S::zero(),
first_step: true,
}
}
pub fn update(&mut self, setpoint: S, measurement: S, dt: S) -> Result<S, NeuralError> {
if dt <= S::zero() {
return Err(NeuralError::InvalidDimension);
}
let error = setpoint - measurement;
let error_dot = if self.first_step {
S::zero()
} else {
(error - self.prev_error) / dt
};
self.integral = self.integral + error * dt;
let limit = self.config.integral_limit;
if self.integral > limit {
self.integral = limit;
} else if self.integral < -limit {
self.integral = -limit;
}
let state = [error, error_dot, self.integral];
let delta_kp = self.rbf_kp.forward(&state);
let delta_ki = self.rbf_ki.forward(&state);
let delta_kd = self.rbf_kd.forward(&state);
let adj_lim = self.config.adjustment_limit;
let kp = self.config.base_kp + clamp(delta_kp, -adj_lim, adj_lim);
let ki = self.config.base_ki + clamp(delta_ki, -adj_lim, adj_lim);
let kd = self.config.base_kd + clamp(delta_kd, -adj_lim, adj_lim);
let raw = kp * error + ki * self.integral + kd * error_dot;
let out_lim = self.config.output_limit;
let output = clamp(raw, -out_lim, out_lim);
if !self.first_step {
let sign_err = if self.prev_error > S::zero() {
S::one()
} else if self.prev_error < S::zero() {
-S::one()
} else {
S::zero()
};
let target_adj = adj_lim * sign_err;
let prev_state = [self.prev_error, S::zero(), self.integral];
self.rbf_kp
.train_step(&prev_state, target_adj, self.config.lr)?;
self.rbf_ki
.train_step(&prev_state, S::zero(), self.config.lr)?;
self.rbf_kd
.train_step(&prev_state, S::zero(), self.config.lr)?;
}
self.prev_error = error;
self.prev_output = output;
self.first_step = false;
Ok(output)
}
pub fn reset(&mut self) {
self.integral = S::zero();
self.prev_error = S::zero();
self.prev_output = S::zero();
self.first_step = true;
}
pub fn reset_weights(&mut self) {
self.rbf_kp.reset_weights();
self.rbf_ki.reset_weights();
self.rbf_kd.reset_weights();
}
pub fn integral(&self) -> S {
self.integral
}
pub fn last_kp(&self) -> S {
let state = [self.prev_error, S::zero(), self.integral];
let delta = self.rbf_kp.forward(&state);
let adj_lim = self.config.adjustment_limit;
self.config.base_kp + clamp(delta, -adj_lim, adj_lim)
}
}
#[inline]
fn clamp<S: Float + Copy>(v: S, lo: S, hi: S) -> S {
if v < lo {
lo
} else if v > hi {
hi
} else {
v
}
}
pub fn make_rbf_centers<S: Float + Copy, const H: usize>(
e_min: S,
e_max: S,
sigma: S,
) -> Result<[RbfCenter<S, 3>; H], NeuralError> {
if H == 0 {
return Err(NeuralError::InvalidDimension);
}
let n = S::from(H - 1).unwrap_or(S::one()).max(S::one());
let centers = core::array::from_fn(|k| {
let t = S::from(k).unwrap_or(S::zero()) / n;
let e = e_min + t * (e_max - e_min);
RbfCenter::new([e, S::zero(), S::zero()], sigma).unwrap_or_else(|_| RbfCenter {
center: [S::zero(); 3],
sigma: S::one(),
})
});
Ok(centers)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_pid_6() -> NeuralPid<f64, 6> {
let cfg = NeuralPidConfig::new(1.0, 0.1, 0.05, 0.01, 0.5)
.with_integral_limit(10.0)
.with_output_limit(100.0);
let centers = make_rbf_centers::<f64, 6>(-5.0, 5.0, 2.0).expect("centers");
NeuralPid::new(cfg, centers)
}
#[test]
fn output_is_finite() {
let mut pid = make_pid_6();
let u = pid.update(1.0, 0.0, 0.01).expect("update");
assert!(u.is_finite(), "control output must be finite, got {u}");
}
#[test]
fn output_respects_output_limit() {
let cfg = NeuralPidConfig::new(1000.0, 0.0, 0.0, 0.0, 0.0).with_output_limit(5.0);
let centers = make_rbf_centers::<f64, 4>(-10.0, 10.0, 3.0).expect("c");
let mut pid = NeuralPid::new(cfg, centers);
let u = pid.update(100.0, 0.0, 0.01).expect("update");
assert!(u.abs() <= 5.0 + 1e-9, "output {u} exceeds limit 5.0");
}
#[test]
fn zero_error_gives_small_output() {
let mut pid = make_pid_6();
let u = pid.update(0.0, 0.0, 0.01).expect("update");
assert!(u.abs() < 1.0, "zero-error output should be small, got {u}");
}
#[test]
fn gain_adjustment_direction() {
let cfg = NeuralPidConfig::new(1.0, 0.0, 0.0, 0.5, 0.5);
let centers = make_rbf_centers::<f64, 4>(-5.0, 5.0, 2.0).expect("c");
let mut pid = NeuralPid::new(cfg, centers);
for _ in 0..50 {
pid.update(1.0, 0.0, 0.01).expect("update");
}
let kp = pid.last_kp();
assert!(
kp.is_finite(),
"Kp should be finite after training, got {kp}"
);
}
#[test]
fn reset_clears_integral() {
let mut pid = make_pid_6();
for _ in 0..20 {
pid.update(1.0, 0.5, 0.01).expect("update");
}
assert!(pid.integral().abs() > 0.0, "integral should be non-zero");
pid.reset();
assert_eq!(pid.integral(), 0.0, "integral should be zero after reset");
}
#[test]
fn invalid_dt_returns_error() {
let mut pid = make_pid_6();
let result = pid.update(1.0, 0.0, 0.0);
assert!(result.is_err(), "dt=0 should return an error");
let result2 = pid.update(1.0, 0.0, -0.01);
assert!(result2.is_err(), "negative dt should return an error");
}
#[test]
fn make_rbf_centers_invalid_h() {
let result = make_rbf_centers::<f64, 0>(-1.0, 1.0, 0.5);
assert!(result.is_err(), "H=0 should be invalid");
}
#[test]
fn neural_pid_multiple_steps_stays_finite() {
let mut pid = make_pid_6();
let mut measurement = 0.0_f64;
let dt = 0.01_f64;
for _ in 0..200 {
let u = pid.update(1.0, measurement, dt).expect("update");
assert!(u.is_finite(), "output became non-finite: {u}");
measurement += dt * u * 0.1;
if measurement > 10.0 {
measurement = 10.0;
}
}
}
}