1use crate::autodiff::{Tape, Var};
19
20pub struct Delan {
22 sizes: Vec<usize>, params: Vec<f64>,
24 m: Vec<f64>,
25 v: Vec<f64>,
26 t: u64,
27}
28
29fn splitmix64(state: &mut u64) -> u64 {
30 *state = state.wrapping_add(0x9E3779B97F4A7C15);
31 let mut z = *state;
32 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
33 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
34 z ^ (z >> 31)
35}
36
37type J<'t> = (Var<'t>, Var<'t>, Var<'t>);
39
40fn j_add<'t>(a: J<'t>, b: J<'t>) -> J<'t> {
41 (a.0 + b.0, a.1 + b.1, a.2 + b.2)
42}
43fn j_scale<'t>(w: Var<'t>, a: J<'t>) -> J<'t> {
44 (w * a.0, w * a.1, w * a.2)
45}
46fn j_mul<'t>(a: J<'t>, b: J<'t>) -> J<'t> {
47 (a.0 * b.0, a.0 * b.1 + a.1 * b.0, a.0 * b.2 + a.2 * b.0)
48}
49fn j_tanh<'t>(g: J<'t>) -> J<'t> {
50 let f0 = g.0.tanh();
51 let fp = f0 * f0 * (-1.0) + 1.0;
52 (f0, fp * g.1, fp * g.2)
53}
54fn j_softplus<'t>(g: J<'t>) -> J<'t> {
55 let val = (g.0.exp() + 1.0).ln();
57 let s = g.0.sigmoid();
58 (val, s * g.1, s * g.2)
59}
60fn dqk<'t>(ent: J<'t>, k: usize) -> Var<'t> {
62 if k == 0 { ent.1 } else { ent.2 }
63}
64
65impl Delan {
66 pub fn new(hidden: usize, seed: u64) -> Self {
68 let sizes = vec![2, hidden, hidden, 4];
69 let mut state = seed ^ 0x2468_ACE0_1357_9BDF;
70 let mut params = Vec::new();
71 for l in 0..sizes.len() - 1 {
72 let (ind, outd) = (sizes[l], sizes[l + 1]);
73 let r = (6.0 / (ind + outd) as f64).sqrt();
74 for _ in 0..ind * outd {
75 let u = (splitmix64(&mut state) as f64 / u64::MAX as f64) * 2.0 - 1.0;
76 params.push(u * r);
77 }
78 params.extend(std::iter::repeat_n(0.0, outd));
79 }
80 let n = params.len();
81 Delan { sizes, params, m: vec![0.0; n], v: vec![0.0; n], t: 0 }
82 }
83
84 fn forward_jet<'t>(&self, tape: &'t Tape, pv: &[Var<'t>], q0: f64, q1: f64, zero: Var<'t>, one: Var<'t>) -> Vec<J<'t>> {
86 let mut a: Vec<J<'t>> = vec![(tape.constant(q0), one, zero), (tape.constant(q1), zero, one)];
87 let mut off = 0;
88 let layers = self.sizes.len() - 1;
89 for l in 0..layers {
90 let (ind, outd) = (self.sizes[l], self.sizes[l + 1]);
91 let mut z: Vec<J<'t>> = Vec::with_capacity(outd);
92 for o in 0..outd {
93 let mut s: J<'t> = (pv[off + ind * outd + o], zero, zero);
94 for (i, &ai) in a.iter().enumerate() {
95 s = j_add(s, j_scale(pv[off + o * ind + i], ai));
96 }
97 z.push(if l + 1 < layers { j_tanh(s) } else { s });
98 }
99 off += ind * outd + outd;
100 a = z;
101 }
102 a
103 }
104
105 fn assemble<'t>(raw: &[J<'t>]) -> (J<'t>, J<'t>, J<'t>, J<'t>) {
107 let l11 = j_softplus(raw[0]); let l21 = raw[1];
109 let l22 = j_softplus(raw[2]);
110 let m00 = j_mul(l11, l11);
112 let m01 = j_mul(l11, l21);
113 let m11 = j_add(j_mul(l21, l21), j_mul(l22, l22));
114 let vpot = raw[3];
115 (m00, m01, m11, vpot)
116 }
117
118 pub fn train_step(&mut self, q: &[[f64; 2]], qd: &[[f64; 2]], qdd: &[[f64; 2]], tau: &[[f64; 2]], lr: f64) -> f64 {
120 let tape = Tape::new();
121 let pv: Vec<Var> = self.params.iter().map(|&x| tape.var(x)).collect();
122 let zero = tape.constant(0.0);
123 let one = tape.constant(1.0);
124 let mut loss = tape.constant(0.0);
125
126 for s in 0..q.len() {
127 let raw = self.forward_jet(&tape, &pv, q[s][0], q[s][1], zero, one);
128 let (m00, m01, m11, vpot) = Self::assemble(&raw);
129 let mm = [[m00, m01], [m01, m11]]; let qdv = [qd[s][0], qd[s][1]];
132 for k in 0..2 {
134 let mut cq = tape.constant(0.0);
135 for i in 0..2 {
136 for j in 0..2 {
137 let term = dqk(mm[k][i], j) + dqk(mm[k][j], i) - dqk(mm[i][j], k);
138 cq = cq + term * (0.5 * qdv[i] * qdv[j]);
139 }
140 }
141 let g = if k == 0 { vpot.1 } else { vpot.2 };
143 let tau_pred = mm[k][0].0 * qdd[s][0] + mm[k][1].0 * qdd[s][1] + cq + g;
145 let e = tau_pred - tau[s][k];
146 loss = loss + e * e;
147 }
148 }
149 loss = loss * (1.0 / (q.len() * 2) as f64);
150 let grad_g = loss.backward();
151 let grad: Vec<f64> = pv.iter().map(|&x| grad_g.wrt(x)).collect();
152
153 self.t += 1;
154 let (b1, b2, eps) = (0.9_f64, 0.999_f64, 1e-8);
155 let bc1 = 1.0 - b1.powi(self.t as i32);
156 let bc2 = 1.0 - b2.powi(self.t as i32);
157 for (i, &gi) in grad.iter().enumerate() {
158 self.m[i] = b1 * self.m[i] + (1.0 - b1) * gi;
159 self.v[i] = b2 * self.v[i] + (1.0 - b2) * gi * gi;
160 self.params[i] -= lr * (self.m[i] / bc1) / ((self.v[i] / bc2).sqrt() + eps);
161 }
162 loss.value()
163 }
164
165 pub fn train(&mut self, q: &[[f64; 2]], qd: &[[f64; 2]], qdd: &[[f64; 2]], tau: &[[f64; 2]], epochs: usize, lr: f64) -> f64 {
167 let mut l = f64::INFINITY;
168 for _ in 0..epochs {
169 l = self.train_step(q, qd, qdd, tau, lr);
170 }
171 l
172 }
173
174 fn forward_plain(&self, q0: f64, q1: f64) -> Vec<(f64, f64, f64)> {
176 let mut a: Vec<(f64, f64, f64)> = vec![(q0, 1.0, 0.0), (q1, 0.0, 1.0)];
177 let mut off = 0;
178 let layers = self.sizes.len() - 1;
179 for l in 0..layers {
180 let (ind, outd) = (self.sizes[l], self.sizes[l + 1]);
181 let mut z = Vec::with_capacity(outd);
182 for o in 0..outd {
183 let (mut v0, mut v1, mut v2) = (self.params[off + ind * outd + o], 0.0, 0.0);
184 for (i, ai) in a.iter().enumerate() {
185 let w = self.params[off + o * ind + i];
186 v0 += w * ai.0;
187 v1 += w * ai.1;
188 v2 += w * ai.2;
189 }
190 if l + 1 < layers {
191 let t = v0.tanh();
192 let fp = 1.0 - t * t;
193 z.push((t, fp * v1, fp * v2));
194 } else {
195 z.push((v0, v1, v2));
196 }
197 }
198 off += ind * outd + outd;
199 a = z;
200 }
201 a
202 }
203
204 pub fn mass(&self, q: &[f64]) -> [f64; 3] {
206 let raw = self.forward_plain(q[0], q[1]);
207 let l11 = softplus(raw[0].0);
208 let l21 = raw[1].0;
209 let l22 = softplus(raw[2].0);
210 [l11 * l11, l11 * l21, l21 * l21 + l22 * l22]
211 }
212
213 pub fn predict_tau(&self, q: &[f64], qd: &[f64], qdd: &[f64]) -> [f64; 2] {
215 let raw = self.forward_plain(q[0], q[1]);
216 let l11 = softplus(raw[0].0);
217 let sp0 = sigmoid(raw[0].0);
218 let l21 = raw[1].0;
219 let l22 = softplus(raw[2].0);
220 let sp2 = sigmoid(raw[2].0);
221 let m = [l11 * l11, l11 * l21, l21 * l21 + l22 * l22]; let dl11 = [sp0 * raw[0].1, sp0 * raw[0].2];
225 let dl21 = [raw[1].1, raw[1].2];
226 let dl22 = [sp2 * raw[2].1, sp2 * raw[2].2];
227 let dm00 = [2.0 * l11 * dl11[0], 2.0 * l11 * dl11[1]];
229 let dm01 = [dl11[0] * l21 + l11 * dl21[0], dl11[1] * l21 + l11 * dl21[1]];
230 let dm11 = [2.0 * l21 * dl21[0] + 2.0 * l22 * dl22[0], 2.0 * l21 * dl21[1] + 2.0 * l22 * dl22[1]];
231 let mm = [[m[0], m[1]], [m[1], m[2]]];
232 let dmm = [[dm00, dm01], [dm01, dm11]];
233 let g = [raw[3].1, raw[3].2]; let mut tau = [0.0f64; 2];
235 for k in 0..2 {
236 let mut cq = 0.0;
237 for i in 0..2 {
238 for j in 0..2 {
239 cq += 0.5 * (dmm[k][i][j] + dmm[k][j][i] - dmm[i][j][k]) * qd[i] * qd[j];
240 }
241 }
242 tau[k] = mm[k][0] * qdd[0] + mm[k][1] * qdd[1] + cq + g[k];
243 }
244 tau
245 }
246}
247
248fn softplus(x: f64) -> f64 {
249 (x.exp() + 1.0).ln()
250}
251fn sigmoid(x: f64) -> f64 {
252 1.0 / (1.0 + (-x).exp())
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use ferromotion_core::{from_urdf_full, inverse_dynamics, mass_matrix, LinkInertia, Robot};
259 use nalgebra::Vector3;
260
261 const URDF: &str = r#"<robot name="dpend">
262 <link name="base"/>
263 <link name="l1"><inertial><origin xyz="0.5 0 0" rpy="0 0 0"/><mass value="1.0"/>
264 <inertia ixx="0.01" ixy="0" ixz="0" iyy="0.083" iyz="0" izz="0.083"/></inertial></link>
265 <link name="l2"><inertial><origin xyz="0.5 0 0" rpy="0 0 0"/><mass value="1.0"/>
266 <inertia ixx="0.01" ixy="0" ixz="0" iyy="0.083" iyz="0" izz="0.083"/></inertial></link>
267 <link name="tool"/>
268 <joint name="j1" type="revolute"><parent link="base"/><child link="l1"/><origin xyz="0 0 0" rpy="0 0 0"/>
269 <axis xyz="0 1 0"/><limit lower="-6.3" upper="6.3" effort="100" velocity="100"/></joint>
270 <joint name="j2" type="revolute"><parent link="l1"/><child link="l2"/><origin xyz="1 0 0" rpy="0 0 0"/>
271 <axis xyz="0 1 0"/><limit lower="-6.3" upper="6.3" effort="100" velocity="100"/></joint>
272 <joint name="jt" type="fixed"><parent link="l2"/><child link="tool"/><origin xyz="1 0 0" rpy="0 0 0"/></joint>
273 </robot>"#;
274
275 fn gen_data(robot: &Robot, inertia: &[LinkInertia], n: usize) -> (Vec<[f64; 2]>, Vec<[f64; 2]>, Vec<[f64; 2]>, Vec<[f64; 2]>) {
276 let mut s = 987u64;
277 let mut rnd = |lo: f64, hi: f64| {
278 let u = splitmix64(&mut s) as f64 / u64::MAX as f64;
279 lo + u * (hi - lo)
280 };
281 let g = Vector3::new(0.0, 0.0, -9.81);
282 let (mut q, mut qd, mut qdd, mut tau) = (vec![], vec![], vec![], vec![]);
283 for _ in 0..n {
284 let qi = [rnd(-2.0, 2.0), rnd(-2.0, 2.0)];
285 let qdi = [rnd(-1.5, 1.5), rnd(-1.5, 1.5)];
286 let qddi = [rnd(-2.0, 2.0), rnd(-2.0, 2.0)];
287 let t = inverse_dynamics(robot, inertia, &qi, &qdi, &qddi, g);
288 q.push(qi);
289 qd.push(qdi);
290 qdd.push(qddi);
291 tau.push([t[0], t[1]]);
292 }
293 (q, qd, qdd, tau)
294 }
295
296 fn rmse(delan: &Delan, tq: &[[f64; 2]], tqd: &[[f64; 2]], tqdd: &[[f64; 2]], ttau: &[[f64; 2]]) -> f64 {
297 let mut err = 0.0;
298 for i in 0..tq.len() {
299 let p = delan.predict_tau(&tq[i], &tqd[i], &tqdd[i]);
300 err += (p[0] - ttau[i][0]).powi(2) + (p[1] - ttau[i][1]).powi(2);
301 }
302 (err / (tq.len() * 2) as f64).sqrt()
303 }
304
305 #[test]
306 fn delan_learns_structured_dynamics_and_keeps_mass_matrix_spd() {
307 let (robot, inertia) = from_urdf_full(URDF, "base", "tool").unwrap();
311 let (q, qd, qdd, tau) = gen_data(&robot, &inertia, 80);
312 let (tq, tqd, tqdd, ttau) = gen_data(&robot, &inertia, 30);
313 let mut delan = Delan::new(20, 5);
314 let before = rmse(&delan, &tq, &tqd, &tqdd, &ttau);
315 delan.train(&q, &qd, &qdd, &tau, 1500, 4e-3);
316 let after = rmse(&delan, &tq, &tqd, &tqdd, &ttau);
317 assert!(after < before * 0.3, "structured dynamics should train: rmse {before} → {after}");
318
319 for &qc in &[[0.0, 0.0], [0.3, 0.7], [-1.2, 0.9], [2.0, -1.5]] {
321 let m = delan.mass(&qc);
322 let det = m[0] * m[2] - m[1] * m[1];
323 assert!(m[0] > 0.0 && m[2] > 0.0 && det > 0.0, "M must be SPD at {qc:?}: {m:?}");
324 }
325 }
326
327 #[test]
328 #[ignore = "slow (~30s): recovers M(q) to within ~0.05 of ferromotion's true mass matrix; run explicitly"]
329 fn delan_recovers_the_true_mass_matrix() {
330 let (robot, inertia) = from_urdf_full(URDF, "base", "tool").unwrap();
333 let (q, qd, qdd, tau) = gen_data(&robot, &inertia, 200);
334 let mut delan = Delan::new(24, 5);
335 delan.train(&q, &qd, &qdd, &tau, 8000, 3e-3);
336 let qc = [0.3, 0.7];
337 let m_true = mass_matrix(&robot, &inertia, &qc);
338 let m_hat = delan.mass(&qc);
339 assert!((m_hat[0] - m_true[(0, 0)]).abs() < 0.15, "M00: {} vs {}", m_hat[0], m_true[(0, 0)]);
340 assert!((m_hat[1] - m_true[(0, 1)]).abs() < 0.15, "M01: {} vs {}", m_hat[1], m_true[(0, 1)]);
341 assert!((m_hat[2] - m_true[(1, 1)]).abs() < 0.15, "M11: {} vs {}", m_hat[2], m_true[(1, 1)]);
342 }
343}