use crate::autodiff::{Tape, Var};
pub struct Delan {
sizes: Vec<usize>, params: Vec<f64>,
m: Vec<f64>,
v: Vec<f64>,
t: u64,
}
fn splitmix64(state: &mut u64) -> u64 {
*state = state.wrapping_add(0x9E3779B97F4A7C15);
let mut z = *state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
z ^ (z >> 31)
}
type J<'t> = (Var<'t>, Var<'t>, Var<'t>);
fn j_add<'t>(a: J<'t>, b: J<'t>) -> J<'t> {
(a.0 + b.0, a.1 + b.1, a.2 + b.2)
}
fn j_scale<'t>(w: Var<'t>, a: J<'t>) -> J<'t> {
(w * a.0, w * a.1, w * a.2)
}
fn j_mul<'t>(a: J<'t>, b: J<'t>) -> J<'t> {
(a.0 * b.0, a.0 * b.1 + a.1 * b.0, a.0 * b.2 + a.2 * b.0)
}
fn j_tanh<'t>(g: J<'t>) -> J<'t> {
let f0 = g.0.tanh();
let fp = f0 * f0 * (-1.0) + 1.0;
(f0, fp * g.1, fp * g.2)
}
fn j_softplus<'t>(g: J<'t>) -> J<'t> {
let val = (g.0.exp() + 1.0).ln();
let s = g.0.sigmoid();
(val, s * g.1, s * g.2)
}
fn dqk<'t>(ent: J<'t>, k: usize) -> Var<'t> {
if k == 0 { ent.1 } else { ent.2 }
}
impl Delan {
pub fn new(hidden: usize, seed: u64) -> Self {
let sizes = vec![2, hidden, hidden, 4];
let mut state = seed ^ 0x2468_ACE0_1357_9BDF;
let mut params = Vec::new();
for l in 0..sizes.len() - 1 {
let (ind, outd) = (sizes[l], sizes[l + 1]);
let r = (6.0 / (ind + outd) as f64).sqrt();
for _ in 0..ind * outd {
let u = (splitmix64(&mut state) as f64 / u64::MAX as f64) * 2.0 - 1.0;
params.push(u * r);
}
params.extend(std::iter::repeat_n(0.0, outd));
}
let n = params.len();
Delan { sizes, params, m: vec![0.0; n], v: vec![0.0; n], t: 0 }
}
fn forward_jet<'t>(&self, tape: &'t Tape, pv: &[Var<'t>], q0: f64, q1: f64, zero: Var<'t>, one: Var<'t>) -> Vec<J<'t>> {
let mut a: Vec<J<'t>> = vec![(tape.constant(q0), one, zero), (tape.constant(q1), zero, one)];
let mut off = 0;
let layers = self.sizes.len() - 1;
for l in 0..layers {
let (ind, outd) = (self.sizes[l], self.sizes[l + 1]);
let mut z: Vec<J<'t>> = Vec::with_capacity(outd);
for o in 0..outd {
let mut s: J<'t> = (pv[off + ind * outd + o], zero, zero);
for (i, &ai) in a.iter().enumerate() {
s = j_add(s, j_scale(pv[off + o * ind + i], ai));
}
z.push(if l + 1 < layers { j_tanh(s) } else { s });
}
off += ind * outd + outd;
a = z;
}
a
}
fn assemble<'t>(raw: &[J<'t>]) -> (J<'t>, J<'t>, J<'t>, J<'t>) {
let l11 = j_softplus(raw[0]); let l21 = raw[1];
let l22 = j_softplus(raw[2]);
let m00 = j_mul(l11, l11);
let m01 = j_mul(l11, l21);
let m11 = j_add(j_mul(l21, l21), j_mul(l22, l22));
let vpot = raw[3];
(m00, m01, m11, vpot)
}
pub fn train_step(&mut self, q: &[[f64; 2]], qd: &[[f64; 2]], qdd: &[[f64; 2]], tau: &[[f64; 2]], lr: f64) -> f64 {
let tape = Tape::new();
let pv: Vec<Var> = self.params.iter().map(|&x| tape.var(x)).collect();
let zero = tape.constant(0.0);
let one = tape.constant(1.0);
let mut loss = tape.constant(0.0);
for s in 0..q.len() {
let raw = self.forward_jet(&tape, &pv, q[s][0], q[s][1], zero, one);
let (m00, m01, m11, vpot) = Self::assemble(&raw);
let mm = [[m00, m01], [m01, m11]]; let qdv = [qd[s][0], qd[s][1]];
for k in 0..2 {
let mut cq = tape.constant(0.0);
for i in 0..2 {
for j in 0..2 {
let term = dqk(mm[k][i], j) + dqk(mm[k][j], i) - dqk(mm[i][j], k);
cq = cq + term * (0.5 * qdv[i] * qdv[j]);
}
}
let g = if k == 0 { vpot.1 } else { vpot.2 };
let tau_pred = mm[k][0].0 * qdd[s][0] + mm[k][1].0 * qdd[s][1] + cq + g;
let e = tau_pred - tau[s][k];
loss = loss + e * e;
}
}
loss = loss * (1.0 / (q.len() * 2) as f64);
let grad_g = loss.backward();
let grad: Vec<f64> = pv.iter().map(|&x| grad_g.wrt(x)).collect();
self.t += 1;
let (b1, b2, eps) = (0.9_f64, 0.999_f64, 1e-8);
let bc1 = 1.0 - b1.powi(self.t as i32);
let bc2 = 1.0 - b2.powi(self.t as i32);
for (i, &gi) in grad.iter().enumerate() {
self.m[i] = b1 * self.m[i] + (1.0 - b1) * gi;
self.v[i] = b2 * self.v[i] + (1.0 - b2) * gi * gi;
self.params[i] -= lr * (self.m[i] / bc1) / ((self.v[i] / bc2).sqrt() + eps);
}
loss.value()
}
pub fn train(&mut self, q: &[[f64; 2]], qd: &[[f64; 2]], qdd: &[[f64; 2]], tau: &[[f64; 2]], epochs: usize, lr: f64) -> f64 {
let mut l = f64::INFINITY;
for _ in 0..epochs {
l = self.train_step(q, qd, qdd, tau, lr);
}
l
}
fn forward_plain(&self, q0: f64, q1: f64) -> Vec<(f64, f64, f64)> {
let mut a: Vec<(f64, f64, f64)> = vec![(q0, 1.0, 0.0), (q1, 0.0, 1.0)];
let mut off = 0;
let layers = self.sizes.len() - 1;
for l in 0..layers {
let (ind, outd) = (self.sizes[l], self.sizes[l + 1]);
let mut z = Vec::with_capacity(outd);
for o in 0..outd {
let (mut v0, mut v1, mut v2) = (self.params[off + ind * outd + o], 0.0, 0.0);
for (i, ai) in a.iter().enumerate() {
let w = self.params[off + o * ind + i];
v0 += w * ai.0;
v1 += w * ai.1;
v2 += w * ai.2;
}
if l + 1 < layers {
let t = v0.tanh();
let fp = 1.0 - t * t;
z.push((t, fp * v1, fp * v2));
} else {
z.push((v0, v1, v2));
}
}
off += ind * outd + outd;
a = z;
}
a
}
pub fn mass(&self, q: &[f64]) -> [f64; 3] {
let raw = self.forward_plain(q[0], q[1]);
let l11 = softplus(raw[0].0);
let l21 = raw[1].0;
let l22 = softplus(raw[2].0);
[l11 * l11, l11 * l21, l21 * l21 + l22 * l22]
}
pub fn predict_tau(&self, q: &[f64], qd: &[f64], qdd: &[f64]) -> [f64; 2] {
let raw = self.forward_plain(q[0], q[1]);
let l11 = softplus(raw[0].0);
let sp0 = sigmoid(raw[0].0);
let l21 = raw[1].0;
let l22 = softplus(raw[2].0);
let sp2 = sigmoid(raw[2].0);
let m = [l11 * l11, l11 * l21, l21 * l21 + l22 * l22]; let dl11 = [sp0 * raw[0].1, sp0 * raw[0].2];
let dl21 = [raw[1].1, raw[1].2];
let dl22 = [sp2 * raw[2].1, sp2 * raw[2].2];
let dm00 = [2.0 * l11 * dl11[0], 2.0 * l11 * dl11[1]];
let dm01 = [dl11[0] * l21 + l11 * dl21[0], dl11[1] * l21 + l11 * dl21[1]];
let dm11 = [2.0 * l21 * dl21[0] + 2.0 * l22 * dl22[0], 2.0 * l21 * dl21[1] + 2.0 * l22 * dl22[1]];
let mm = [[m[0], m[1]], [m[1], m[2]]];
let dmm = [[dm00, dm01], [dm01, dm11]];
let g = [raw[3].1, raw[3].2]; let mut tau = [0.0f64; 2];
for k in 0..2 {
let mut cq = 0.0;
for i in 0..2 {
for j in 0..2 {
cq += 0.5 * (dmm[k][i][j] + dmm[k][j][i] - dmm[i][j][k]) * qd[i] * qd[j];
}
}
tau[k] = mm[k][0] * qdd[0] + mm[k][1] * qdd[1] + cq + g[k];
}
tau
}
}
fn softplus(x: f64) -> f64 {
(x.exp() + 1.0).ln()
}
fn sigmoid(x: f64) -> f64 {
1.0 / (1.0 + (-x).exp())
}
#[cfg(test)]
mod tests {
use super::*;
use ferromotion_core::{from_urdf_full, inverse_dynamics, mass_matrix, LinkInertia, Robot};
use nalgebra::Vector3;
const URDF: &str = r#"<robot name="dpend">
<link name="base"/>
<link name="l1"><inertial><origin xyz="0.5 0 0" rpy="0 0 0"/><mass value="1.0"/>
<inertia ixx="0.01" ixy="0" ixz="0" iyy="0.083" iyz="0" izz="0.083"/></inertial></link>
<link name="l2"><inertial><origin xyz="0.5 0 0" rpy="0 0 0"/><mass value="1.0"/>
<inertia ixx="0.01" ixy="0" ixz="0" iyy="0.083" iyz="0" izz="0.083"/></inertial></link>
<link name="tool"/>
<joint name="j1" type="revolute"><parent link="base"/><child link="l1"/><origin xyz="0 0 0" rpy="0 0 0"/>
<axis xyz="0 1 0"/><limit lower="-6.3" upper="6.3" effort="100" velocity="100"/></joint>
<joint name="j2" type="revolute"><parent link="l1"/><child link="l2"/><origin xyz="1 0 0" rpy="0 0 0"/>
<axis xyz="0 1 0"/><limit lower="-6.3" upper="6.3" effort="100" velocity="100"/></joint>
<joint name="jt" type="fixed"><parent link="l2"/><child link="tool"/><origin xyz="1 0 0" rpy="0 0 0"/></joint>
</robot>"#;
fn gen_data(robot: &Robot, inertia: &[LinkInertia], n: usize) -> (Vec<[f64; 2]>, Vec<[f64; 2]>, Vec<[f64; 2]>, Vec<[f64; 2]>) {
let mut s = 987u64;
let mut rnd = |lo: f64, hi: f64| {
let u = splitmix64(&mut s) as f64 / u64::MAX as f64;
lo + u * (hi - lo)
};
let g = Vector3::new(0.0, 0.0, -9.81);
let (mut q, mut qd, mut qdd, mut tau) = (vec![], vec![], vec![], vec![]);
for _ in 0..n {
let qi = [rnd(-2.0, 2.0), rnd(-2.0, 2.0)];
let qdi = [rnd(-1.5, 1.5), rnd(-1.5, 1.5)];
let qddi = [rnd(-2.0, 2.0), rnd(-2.0, 2.0)];
let t = inverse_dynamics(robot, inertia, &qi, &qdi, &qddi, g);
q.push(qi);
qd.push(qdi);
qdd.push(qddi);
tau.push([t[0], t[1]]);
}
(q, qd, qdd, tau)
}
fn rmse(delan: &Delan, tq: &[[f64; 2]], tqd: &[[f64; 2]], tqdd: &[[f64; 2]], ttau: &[[f64; 2]]) -> f64 {
let mut err = 0.0;
for i in 0..tq.len() {
let p = delan.predict_tau(&tq[i], &tqd[i], &tqdd[i]);
err += (p[0] - ttau[i][0]).powi(2) + (p[1] - ttau[i][1]).powi(2);
}
(err / (tq.len() * 2) as f64).sqrt()
}
#[test]
fn delan_learns_structured_dynamics_and_keeps_mass_matrix_spd() {
let (robot, inertia) = from_urdf_full(URDF, "base", "tool").unwrap();
let (q, qd, qdd, tau) = gen_data(&robot, &inertia, 80);
let (tq, tqd, tqdd, ttau) = gen_data(&robot, &inertia, 30);
let mut delan = Delan::new(20, 5);
let before = rmse(&delan, &tq, &tqd, &tqdd, &ttau);
delan.train(&q, &qd, &qdd, &tau, 1500, 4e-3);
let after = rmse(&delan, &tq, &tqd, &tqdd, &ttau);
assert!(after < before * 0.3, "structured dynamics should train: rmse {before} → {after}");
for &qc in &[[0.0, 0.0], [0.3, 0.7], [-1.2, 0.9], [2.0, -1.5]] {
let m = delan.mass(&qc);
let det = m[0] * m[2] - m[1] * m[1];
assert!(m[0] > 0.0 && m[2] > 0.0 && det > 0.0, "M must be SPD at {qc:?}: {m:?}");
}
}
#[test]
#[ignore = "slow (~30s): recovers M(q) to within ~0.05 of ferromotion's true mass matrix; run explicitly"]
fn delan_recovers_the_true_mass_matrix() {
let (robot, inertia) = from_urdf_full(URDF, "base", "tool").unwrap();
let (q, qd, qdd, tau) = gen_data(&robot, &inertia, 200);
let mut delan = Delan::new(24, 5);
delan.train(&q, &qd, &qdd, &tau, 8000, 3e-3);
let qc = [0.3, 0.7];
let m_true = mass_matrix(&robot, &inertia, &qc);
let m_hat = delan.mass(&qc);
assert!((m_hat[0] - m_true[(0, 0)]).abs() < 0.15, "M00: {} vs {}", m_hat[0], m_true[(0, 0)]);
assert!((m_hat[1] - m_true[(0, 1)]).abs() < 0.15, "M01: {} vs {}", m_hat[1], m_true[(0, 1)]);
assert!((m_hat[2] - m_true[(1, 1)]).abs() < 0.15, "M11: {} vs {}", m_hat[2], m_true[(1, 1)]);
}
}