1use crate::autodiff::Tape;
15use nalgebra::{DMatrix, DVector};
16
17pub struct ModelBasedControl {
19 true_mass: f64,
21 true_damping: f64,
22 true_disturbance: f64,
23 setpoint: f64,
24 dt: f64,
25 steps: usize,
26 learned: [f64; 4],
28 identified: bool,
29 gains: [f64; 3],
31 m: [f64; 3],
32 v: [f64; 3],
33 t: u64,
34}
35
36fn splitmix64(state: &mut u64) -> u64 {
37 *state = state.wrapping_add(0x9E3779B97F4A7C15);
38 let mut z = *state;
39 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
40 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
41 z ^ (z >> 31)
42}
43
44impl ModelBasedControl {
45 pub fn new(mass: f64, damping: f64, disturbance: f64, setpoint: f64) -> Self {
47 ModelBasedControl {
48 true_mass: mass,
49 true_damping: damping,
50 true_disturbance: disturbance,
51 setpoint,
52 dt: 0.05,
53 steps: 90,
54 learned: [0.0; 4],
55 identified: false,
56 gains: [1.0, 0.0, 0.0],
57 m: [0.0; 3],
58 v: [0.0; 3],
59 t: 0,
60 }
61 }
62
63 fn true_accel(&self, x: f64, v: f64, u: f64) -> f64 {
64 let _ = x; (u - self.true_damping * v - self.true_disturbance) / self.true_mass
66 }
67
68 pub fn identify(&mut self) {
70 let mut s = 20240101u64;
71 let mut rnd = |lo: f64, hi: f64| lo + (splitmix64(&mut s) as f64 / u64::MAX as f64) * (hi - lo);
72 let n = 200;
73 let mut a = DMatrix::zeros(n, 4);
74 let mut y = DVector::zeros(n);
75 for i in 0..n {
76 let (x, v, u) = (rnd(-2.0, 2.0), rnd(-2.0, 2.0), rnd(-5.0, 5.0));
77 a[(i, 0)] = x;
78 a[(i, 1)] = v;
79 a[(i, 2)] = u;
80 a[(i, 3)] = 1.0;
81 y[i] = self.true_accel(x, v, u);
82 }
83 let sol = a.svd(true, true).solve(&y, 1e-12).unwrap_or_else(|_| DVector::zeros(4));
84 self.learned = [sol[0], sol[1], sol[2], sol[3]];
85 self.identified = true;
86 }
87
88 pub fn learned_coeffs(&self) -> [f64; 4] {
90 self.learned
91 }
92 pub fn true_coeffs(&self) -> [f64; 4] {
94 [0.0, -self.true_damping / self.true_mass, 1.0 / self.true_mass, -self.true_disturbance / self.true_mass]
95 }
96 pub fn gains(&self) -> [f64; 3] {
97 self.gains
98 }
99
100 fn simulate(&self, use_true: bool) -> Vec<f64> {
102 let (mut x, mut v, mut integ) = (0.0, 0.0, 0.0);
103 let mut out = vec![x];
104 for _ in 0..self.steps {
105 let e = self.setpoint - x;
106 integ += e * self.dt;
107 let u = self.gains[0] * e + self.gains[1] * integ + self.gains[2] * (-v);
108 let acc = if use_true {
109 self.true_accel(x, v, u)
110 } else {
111 self.learned[0] * x + self.learned[1] * v + self.learned[2] * u + self.learned[3]
112 };
113 v += acc * self.dt;
114 x += v * self.dt;
115 out.push(x);
116 }
117 out
118 }
119
120 pub fn deploy(&self) -> Vec<f64> {
122 self.simulate(true)
123 }
124 pub fn imagine(&self) -> Vec<f64> {
126 self.simulate(false)
127 }
128 pub fn deploy_error(&self) -> f64 {
130 (self.deploy().last().copied().unwrap_or(0.0) - self.setpoint).abs()
131 }
132
133 pub fn train_step(&mut self, lr: f64) -> f64 {
135 let tape = Tape::new();
136 let kp = tape.var(self.gains[0]);
137 let ki = tape.var(self.gains[1]);
138 let kd = tape.var(self.gains[2]);
139 let mut x = tape.constant(0.0);
140 let mut v = tape.constant(0.0);
141 let mut integ = tape.constant(0.0);
142 let mut cost = tape.constant(0.0);
143 let (sp, dt) = (self.setpoint, self.dt);
144 let [a, b, c, d] = self.learned;
145 for step in 0..self.steps {
146 let e = tape.constant(sp) - x;
147 integ = integ + e * dt;
148 let u = kp * e + ki * integ + kd * (v * -1.0);
149 let acc = x * a + v * b + u * c + tape.constant(d);
151 v = v + acc * dt;
152 x = x + v * dt;
153 let w = 0.5 + step as f64 / self.steps as f64;
154 cost = cost + e * e * (w * dt) + u * u * (1e-4 * dt);
155 }
156 let g = cost.backward();
157 let grad = [g.wrt(kp), g.wrt(ki), g.wrt(kd)];
158 self.t += 1;
159 let (b1, b2, eps) = (0.9_f64, 0.999_f64, 1e-8);
160 let (bc1, bc2) = (1.0 - b1.powi(self.t as i32), 1.0 - b2.powi(self.t as i32));
161 for (i, &gi) in grad.iter().enumerate() {
162 self.m[i] = b1 * self.m[i] + (1.0 - b1) * gi;
163 self.v[i] = b2 * self.v[i] + (1.0 - b2) * gi * gi;
164 self.gains[i] -= lr * (self.m[i] / bc1) / ((self.v[i] / bc2).sqrt() + eps);
165 }
166 cost.value()
167 }
168
169 pub fn train(&mut self, epochs: usize, lr: f64) -> f64 {
171 let mut c = f64::INFINITY;
172 for _ in 0..epochs {
173 c = self.train_step(lr);
174 }
175 c
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn learn_a_model_then_control_the_real_plant_through_it() {
185 let mut mbc = ModelBasedControl::new(1.0, 0.6, 2.0, 1.0);
187 mbc.identify();
188 let (lc, tc) = (mbc.learned_coeffs(), mbc.true_coeffs());
190 for i in 0..4 {
191 assert!((lc[i] - tc[i]).abs() < 1e-6, "coeff {i}: learned {} vs true {}", lc[i], tc[i]);
192 }
193 let before = mbc.deploy_error();
195 assert!(before > 0.3, "proportional-only should miss on the true plant: {before}");
196 mbc.train(800, 0.05);
197 let after = mbc.deploy_error();
198 assert!(after < 0.06, "controller trained in imagination should settle the real plant: {after}");
199 assert!(mbc.gains()[1] > 0.1, "it should discover integral action: Ki={}", mbc.gains()[1]);
200 }
201}