1use core::ops::{Add, Div, Mul, Neg, Sub};
16
17#[derive(Clone, Copy, Debug)]
20pub struct Dual {
21 pub re: f64,
22 pub eps: f64,
23}
24
25impl Dual {
26 pub fn var(x: f64) -> Self {
28 Dual { re: x, eps: 1.0 }
29 }
30 pub fn constant(c: f64) -> Self {
32 Dual { re: c, eps: 0.0 }
33 }
34 fn map(self, v: f64, d: f64) -> Self {
35 Dual { re: v, eps: d * self.eps }
36 }
37 pub fn sin(self) -> Self {
39 self.map(self.re.sin(), self.re.cos())
40 }
41 pub fn cos(self) -> Self {
43 self.map(self.re.cos(), -self.re.sin())
44 }
45 pub fn exp(self) -> Self {
47 let e = self.re.exp();
48 self.map(e, e)
49 }
50 pub fn ln(self) -> Self {
52 self.map(self.re.ln(), 1.0 / self.re)
53 }
54 pub fn tanh(self) -> Self {
56 let t = self.re.tanh();
57 self.map(t, 1.0 - t * t)
58 }
59 pub fn powf(self, n: f64) -> Self {
61 self.map(self.re.powf(n), n * self.re.powf(n - 1.0))
62 }
63}
64
65impl Add for Dual {
66 type Output = Dual;
67 fn add(self, o: Dual) -> Dual {
68 Dual { re: self.re + o.re, eps: self.eps + o.eps }
69 }
70}
71impl Sub for Dual {
72 type Output = Dual;
73 fn sub(self, o: Dual) -> Dual {
74 Dual { re: self.re - o.re, eps: self.eps - o.eps }
75 }
76}
77impl Mul for Dual {
78 type Output = Dual;
79 fn mul(self, o: Dual) -> Dual {
80 Dual { re: self.re * o.re, eps: self.re * o.eps + self.eps * o.re }
81 }
82}
83impl Div for Dual {
84 type Output = Dual;
85 fn div(self, o: Dual) -> Dual {
86 Dual { re: self.re / o.re, eps: (self.eps * o.re - self.re * o.eps) / (o.re * o.re) }
87 }
88}
89impl Neg for Dual {
90 type Output = Dual;
91 fn neg(self) -> Dual {
92 Dual { re: -self.re, eps: -self.eps }
93 }
94}
95
96#[derive(Clone, Copy, Debug)]
100pub struct HyperDual {
101 pub re: f64,
102 pub e1: f64,
103 pub e2: f64,
104 pub e12: f64,
105}
106
107impl HyperDual {
108 pub fn constant(c: f64) -> Self {
110 HyperDual { re: c, e1: 0.0, e2: 0.0, e12: 0.0 }
111 }
112 pub fn var2(x: f64) -> Self {
115 HyperDual { re: x, e1: 1.0, e2: 1.0, e12: 0.0 }
116 }
117 pub fn var_e1(x: f64) -> Self {
119 HyperDual { re: x, e1: 1.0, e2: 0.0, e12: 0.0 }
120 }
121 pub fn var_e2(x: f64) -> Self {
123 HyperDual { re: x, e1: 0.0, e2: 1.0, e12: 0.0 }
124 }
125 fn chain(self, h: f64, dh: f64, ddh: f64) -> Self {
127 HyperDual {
128 re: h,
129 e1: dh * self.e1,
130 e2: dh * self.e2,
131 e12: dh * self.e12 + ddh * self.e1 * self.e2,
132 }
133 }
134 pub fn sin(self) -> Self {
136 self.chain(self.re.sin(), self.re.cos(), -self.re.sin())
137 }
138 pub fn cos(self) -> Self {
140 self.chain(self.re.cos(), -self.re.sin(), -self.re.cos())
141 }
142 pub fn exp(self) -> Self {
144 let e = self.re.exp();
145 self.chain(e, e, e)
146 }
147 pub fn tanh(self) -> Self {
149 let t = self.re.tanh();
150 let d = 1.0 - t * t;
151 self.chain(t, d, -2.0 * t * d)
152 }
153 pub fn powf(self, n: f64) -> Self {
155 self.chain(self.re.powf(n), n * self.re.powf(n - 1.0), n * (n - 1.0) * self.re.powf(n - 2.0))
156 }
157}
158
159impl Add for HyperDual {
160 type Output = HyperDual;
161 fn add(self, o: HyperDual) -> HyperDual {
162 HyperDual { re: self.re + o.re, e1: self.e1 + o.e1, e2: self.e2 + o.e2, e12: self.e12 + o.e12 }
163 }
164}
165impl Sub for HyperDual {
166 type Output = HyperDual;
167 fn sub(self, o: HyperDual) -> HyperDual {
168 HyperDual { re: self.re - o.re, e1: self.e1 - o.e1, e2: self.e2 - o.e2, e12: self.e12 - o.e12 }
169 }
170}
171impl Mul for HyperDual {
172 type Output = HyperDual;
173 fn mul(self, o: HyperDual) -> HyperDual {
174 HyperDual {
175 re: self.re * o.re,
176 e1: self.re * o.e1 + self.e1 * o.re,
177 e2: self.re * o.e2 + self.e2 * o.re,
178 e12: self.re * o.e12 + self.e1 * o.e2 + self.e2 * o.e1 + self.e12 * o.re,
179 }
180 }
181}
182impl Neg for HyperDual {
183 type Output = HyperDual;
184 fn neg(self) -> HyperDual {
185 HyperDual { re: -self.re, e1: -self.e1, e2: -self.e2, e12: -self.e12 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn dual_first_derivative_matches_finite_difference() {
195 let f = |x: f64| x.sin() * x.exp();
197 let x0 = 0.8;
198 let d = (Dual::var(x0).sin()) * (Dual::var(x0).exp());
199 let fd = (f(x0 + 1e-6) - f(x0 - 1e-6)) / 2e-6;
200 assert!((d.re - f(x0)).abs() < 1e-12, "value");
201 assert!((d.eps - fd).abs() < 1e-6, "dual f'={} vs fd={fd}", d.eps);
202 }
203
204 #[test]
205 fn hyperdual_second_derivative_matches_finite_difference() {
206 let f = |x: f64| (x * x).tanh();
208 let x0 = 0.9;
209 let x = HyperDual::var2(x0);
210 let y = (x * x).tanh();
211 let h = 1e-4;
212 let fdd = (f(x0 + h) - 2.0 * f(x0) + f(x0 - h)) / (h * h);
213 assert!((y.re - f(x0)).abs() < 1e-12, "value");
214 assert!((y.e1 - 2.0 * x0 * (1.0 - f(x0) * f(x0))).abs() < 1e-9, "first derivative in e1");
215 assert!((y.e12 - fdd).abs() < 1e-4, "hyperdual f''={} vs fd={fdd}", y.e12);
216 }
217
218 #[test]
219 fn hyperdual_mixed_partial_is_exact() {
220 let (x0, y0) = (0.5, 1.7);
222 let x = HyperDual::var_e1(x0);
223 let y = HyperDual::var_e2(y0);
224 let out = x.sin() * (y * y);
225 let expect = 2.0 * y0 * x0.cos();
226 assert!((out.e12 - expect).abs() < 1e-12, "mixed partial {} vs {expect}", out.e12);
227 assert!((out.e1 - x0.cos() * y0 * y0).abs() < 1e-12, "∂/∂x");
229 assert!((out.e2 - x0.sin() * 2.0 * y0).abs() < 1e-12, "∂/∂y");
230 }
231}
232
233
234impl ferromotion_core::gendyn::Real for Dual {
242 fn from_f64(v: f64) -> Self {
243 Dual::constant(v)
244 }
245 fn sin(self) -> Self {
246 Dual::sin(self)
247 }
248 fn cos(self) -> Self {
249 Dual::cos(self)
250 }
251 fn sqrt(self) -> Self {
252 self.map(self.re.sqrt(), 0.5 / self.re.sqrt())
253 }
254 fn tanh(self) -> Self {
255 Dual::tanh(self)
256 }
257}
258
259#[cfg(test)]
260mod gendyn_tests {
261 use super::*;
262 use ferromotion_core::gendyn::GenModel;
263 use ferromotion_core::{Iso, Joint, LinkInertia, Robot};
264 use nalgebra::{Matrix3, Translation3, UnitQuaternion, Vector3};
265
266 fn test_robot() -> (Robot, Vec<LinkInertia>) {
267 let mk = |xyz: [f64; 3], rpy: [f64; 3]| {
268 Iso::from_parts(
269 Translation3::new(xyz[0], xyz[1], xyz[2]),
270 UnitQuaternion::from_euler_angles(rpy[0], rpy[1], rpy[2]),
271 )
272 };
273 let joints = vec![
274 Joint::revolute(mk([0.0, 0.0, 0.3], [0.0, 0.0, 0.4]), Vector3::z()),
275 Joint::revolute(mk([0.1, 0.0, 0.2], [0.3, 0.0, 0.0]), Vector3::y()),
276 Joint::prismatic(mk([0.0, 0.05, 0.25], [0.0, 0.2, 0.0]), Vector3::x()),
277 Joint::revolute(mk([0.2, 0.0, 0.1], [0.0, 0.0, -0.3]), Vector3::y()),
278 ];
279 let inertia: Vec<LinkInertia> = (0..4)
280 .map(|i| {
281 let f = i as f64;
282 LinkInertia {
283 mass: 1.5 + 0.3 * f,
284 com: Vector3::new(0.02 * f, -0.01, 0.05 + 0.01 * f),
285 inertia: Matrix3::new(
286 0.02 + 0.005 * f, 0.001, 0.002,
287 0.001, 0.03 + 0.002 * f, 0.0015,
288 0.002, 0.0015, 0.025,
289 ),
290 }
291 })
292 .collect();
293 (Robot { joints, ee_offset: Iso::identity() }, inertia)
294 }
295
296 #[test]
299 fn dual_rnea_matches_analytical_id_derivatives() {
300 let (robot, inertia) = test_robot();
301 let g = Vector3::new(0.0, 0.0, -9.81);
302 let q = [0.3, -0.7, 0.12, 1.1];
303 let qd = [0.5, -0.2, 0.3, -0.8];
304 let qdd = [1.2, 0.4, -0.9, 0.3];
305 let (dq_ref, dqd_ref) = ferromotion_core::id_derivatives(&robot, &inertia, &q, &qd, &qdd, g);
306 let m = GenModel::<Dual>::from_robot(&robot, &inertia, [0.0, 0.0, -9.81]);
307 let n = 4;
308 for j in 0..n {
309 let mk = |v: &[f64], active: Option<usize>| -> Vec<Dual> {
310 v.iter()
311 .enumerate()
312 .map(|(i, &x)| if Some(i) == active { Dual::var(x) } else { Dual::constant(x) })
313 .collect()
314 };
315 let tau = m.rnea(&mk(&q, Some(j)), &mk(&qd, None), &mk(&qdd, None));
317 for i in 0..n {
318 let (got, want) = (tau[i].eps, dq_ref[(i, j)]);
319 assert!((got - want).abs() < 1e-8 * want.abs().max(1.0), "dtau/dq ({i},{j}): {got} vs {want}");
320 }
321 let tau = m.rnea(&mk(&q, None), &mk(&qd, Some(j)), &mk(&qdd, None));
323 for i in 0..n {
324 let (got, want) = (tau[i].eps, dqd_ref[(i, j)]);
325 assert!((got - want).abs() < 1e-8 * want.abs().max(1.0), "dtau/dqd ({i},{j}): {got} vs {want}");
326 }
327 }
328 }
329
330 #[test]
333 fn dual_parameter_gradients_match_finite_differences() {
334 let (robot, inertia) = test_robot();
335 let g = Vector3::new(0.0, 0.0, -9.81);
336 let q = [0.3, -0.7, 0.12, 1.1];
337 let qd = [0.5, -0.2, 0.3, -0.8];
338 let qdd = [1.2, 0.4, -0.9, 0.3];
339 let eps = 1e-6;
340 for &(link, kind) in &[(0usize, 0usize), (2, 0), (1, 1), (3, 2)] {
342 let mut m = GenModel::<Dual>::from_robot(&robot, &inertia, [0.0, 0.0, -9.81]);
344 match kind {
345 0 => m.links[link].mass.eps = 1.0,
346 1 => m.links[link].com.0[1].eps = 1.0,
347 _ => m.links[link].inertia.0[0][0].eps = 1.0,
348 }
349 let mkc = |v: &[f64]| -> Vec<Dual> { v.iter().map(|&x| Dual::constant(x)).collect() };
350 let tau = m.rnea(&mkc(&q), &mkc(&qd), &mkc(&qdd));
351 let perturb = |s: f64| -> Vec<f64> {
353 let mut li = inertia.clone();
354 match kind {
355 0 => li[link].mass += s,
356 1 => li[link].com[1] += s,
357 _ => li[link].inertia[(0, 0)] += s,
358 }
359 ferromotion_core::inverse_dynamics(&robot, &li, &q, &qd, &qdd, g)
360 };
361 let (tp, tm) = (perturb(eps), perturb(-eps));
362 for i in 0..4 {
363 let want = (tp[i] - tm[i]) / (2.0 * eps);
364 let got = tau[i].eps;
365 assert!(
366 (got - want).abs() < 1e-5 * want.abs().max(1.0),
367 "dtau/dparam link={link} kind={kind} row {i}: {got} vs {want}"
368 );
369 }
370 }
371 }
372}