Skip to main content

deke_types/fk/
dh.rs

1use glam_traits_ext::{FloatAffine, FloatVec, TAffine3, TMat3, TVec3};
2
3#[cfg(debug_assertions)]
4use crate::DekeError;
5use crate::SRobotQ;
6
7use super::{
8    AAffine3, AMat3, AVec3, FKChain, FKScalar, check_finite, const_sin_cos, const_sin_cos_f64,
9};
10
11#[derive(Debug, Clone, Copy)]
12pub struct DHJoint<F: FKScalar = f32> {
13    pub a: F,
14    pub alpha: F,
15    pub d: F,
16    pub theta_offset: F,
17}
18
19/// Precomputed standard-DH chain with SoA layout.
20///
21/// Convention: `T_i = Rz(θ) · Tz(d) · Tx(a) · Rx(α)`
22#[derive(Debug, Clone)]
23pub struct DHChain<const N: usize, F: FKScalar = f32> {
24    a: [F; N],
25    d: [F; N],
26    sin_alpha: [F; N],
27    cos_alpha: [F; N],
28    theta_offset: [F; N],
29}
30
31impl<const N: usize> DHChain<N, f32> {
32    pub const fn new(joints: [DHJoint<f32>; N]) -> Self {
33        let mut a = [0.0; N];
34        let mut d = [0.0; N];
35        let mut sin_alpha = [0.0; N];
36        let mut cos_alpha = [0.0; N];
37        let mut theta_offset = [0.0; N];
38
39        let mut i = 0;
40        while i < N {
41            a[i] = joints[i].a;
42            d[i] = joints[i].d;
43            let (sa, ca) = const_sin_cos(joints[i].alpha);
44            sin_alpha[i] = sa;
45            cos_alpha[i] = ca;
46            theta_offset[i] = joints[i].theta_offset;
47            i += 1;
48        }
49
50        Self {
51            a,
52            d,
53            sin_alpha,
54            cos_alpha,
55            theta_offset,
56        }
57    }
58
59    /// Construct from the row-major `DH_PARAMS` const array emitted by the
60    /// workcell macro.
61    ///
62    /// `params`: `[[f64; N]; 4]` — rows are `(a, alpha, d, theta_offset)`
63    /// across joints.
64    pub const fn from_dh(params: &[[f64; N]; 4]) -> Self {
65        let mut a = [0.0f32; N];
66        let mut d = [0.0f32; N];
67        let mut sin_alpha = [0.0f32; N];
68        let mut cos_alpha = [0.0f32; N];
69        let mut theta_offset = [0.0f32; N];
70
71        let mut i = 0;
72        while i < N {
73            a[i] = params[0][i] as f32;
74            let (sa, ca) = const_sin_cos(params[1][i] as f32);
75            sin_alpha[i] = sa;
76            cos_alpha[i] = ca;
77            d[i] = params[2][i] as f32;
78            theta_offset[i] = params[3][i] as f32;
79            i += 1;
80        }
81
82        Self {
83            a,
84            d,
85            sin_alpha,
86            cos_alpha,
87            theta_offset,
88        }
89    }
90}
91
92impl<const N: usize> DHChain<N, f64> {
93    /// `const`-evaluable f64 constructor — analogue of [`DHChain::<N, f32>::new`].
94    pub const fn new_f64(joints: [DHJoint<f64>; N]) -> Self {
95        let mut a = [0.0; N];
96        let mut d = [0.0; N];
97        let mut sin_alpha = [0.0; N];
98        let mut cos_alpha = [0.0; N];
99        let mut theta_offset = [0.0; N];
100
101        let mut i = 0;
102        while i < N {
103            a[i] = joints[i].a;
104            d[i] = joints[i].d;
105            let (sa, ca) = const_sin_cos_f64(joints[i].alpha);
106            sin_alpha[i] = sa;
107            cos_alpha[i] = ca;
108            theta_offset[i] = joints[i].theta_offset;
109            i += 1;
110        }
111
112        Self {
113            a,
114            d,
115            sin_alpha,
116            cos_alpha,
117            theta_offset,
118        }
119    }
120
121    /// Construct from the row-major `DH_PARAMS` const array, in `f64`.
122    /// `params`: `[[f64; N]; 4]` — rows are `(a, alpha, d, theta_offset)`.
123    pub const fn from_dh_f64(params: &[[f64; N]; 4]) -> Self {
124        let mut a = [0.0f64; N];
125        let mut d = [0.0f64; N];
126        let mut sin_alpha = [0.0f64; N];
127        let mut cos_alpha = [0.0f64; N];
128        let mut theta_offset = [0.0f64; N];
129
130        let mut i = 0;
131        while i < N {
132            a[i] = params[0][i];
133            let (sa, ca) = const_sin_cos_f64(params[1][i]);
134            sin_alpha[i] = sa;
135            cos_alpha[i] = ca;
136            d[i] = params[2][i];
137            theta_offset[i] = params[3][i];
138            i += 1;
139        }
140
141        Self {
142            a,
143            d,
144            sin_alpha,
145            cos_alpha,
146            theta_offset,
147        }
148    }
149}
150
151impl<const N: usize, F: FKScalar> DHChain<N, F> {
152    /// Generic runtime constructor. For `f32` the const-evaluable
153    /// [`DHChain::new`] is preferred; this exists so `DHChain<N, f64>` (and
154    /// any other future scalar) is usable.
155    pub fn from_joints(joints: [DHJoint<F>; N]) -> Self {
156        let zero = F::zero();
157        let mut a = [zero; N];
158        let mut d = [zero; N];
159        let mut sin_alpha = [zero; N];
160        let mut cos_alpha = [zero; N];
161        let mut theta_offset = [zero; N];
162
163        for i in 0..N {
164            a[i] = joints[i].a;
165            d[i] = joints[i].d;
166            let (sa, ca) = joints[i].alpha.sin_cos();
167            sin_alpha[i] = sa;
168            cos_alpha[i] = ca;
169            theta_offset[i] = joints[i].theta_offset;
170        }
171
172        Self {
173            a,
174            d,
175            sin_alpha,
176            cos_alpha,
177            theta_offset,
178        }
179    }
180}
181
182impl<const N: usize, F: FKScalar> FKChain<N, F> for DHChain<N, F> {
183    #[cfg(debug_assertions)]
184    type Error = DekeError;
185    #[cfg(not(debug_assertions))]
186    type Error = std::convert::Infallible;
187
188    /// DH forward kinematics exploiting the structure of `Rz(θ)·Rx(α)`.
189    ///
190    /// The per-joint accumulation decomposes into two 2D column rotations:
191    ///   1. Rotate `(c0, c1)` by θ  →  `(new_c0, perp)`
192    ///   2. Rotate `(perp, c2)` by α  →  `(new_c1, new_c2)`
193    /// Translation reuses `new_c0`:  `t += a·new_c0 + d·old_c2`
194    fn fk(&self, q: &SRobotQ<N, F>) -> Result<[AAffine3<F>; N], Self::Error> {
195        check_finite::<N, F>(q)?;
196        let mut out = [AAffine3::<F>::IDENTITY; N];
197        let mut c0 = AVec3::<F>::X;
198        let mut c1 = AVec3::<F>::Y;
199        let mut c2 = AVec3::<F>::Z;
200        let mut t = AVec3::<F>::ZERO;
201
202        let mut i = 0;
203        while i < N {
204            let (st, ct) = (q.0[i] + self.theta_offset[i]).sin_cos();
205            let sa = self.sin_alpha[i];
206            let ca = self.cos_alpha[i];
207
208            let new_c0 = c0 * ct + c1 * st;
209            let perp = c1 * ct - c0 * st;
210
211            let new_c1 = perp * ca + c2 * sa;
212            let new_c2 = c2 * ca - perp * sa;
213
214            t = new_c0 * self.a[i] + c2 * self.d[i] + t;
215
216            c0 = new_c0;
217            c1 = new_c1;
218            c2 = new_c2;
219
220            out[i] = AAffine3::<F>::from_mat3_translation(
221                AMat3::<F>::from_cols(c0, c1, c2),
222                t,
223            );
224            i += 1;
225        }
226        Ok(out)
227    }
228
229    fn fk_end(&self, q: &SRobotQ<N, F>) -> Result<AAffine3<F>, Self::Error> {
230        check_finite::<N, F>(q)?;
231        let mut c0 = AVec3::<F>::X;
232        let mut c1 = AVec3::<F>::Y;
233        let mut c2 = AVec3::<F>::Z;
234        let mut t = AVec3::<F>::ZERO;
235
236        let mut i = 0;
237        while i < N {
238            let (st, ct) = (q.0[i] + self.theta_offset[i]).sin_cos();
239            let sa = self.sin_alpha[i];
240            let ca = self.cos_alpha[i];
241
242            let new_c0 = c0 * ct + c1 * st;
243            let perp = c1 * ct - c0 * st;
244
245            let new_c1 = perp * ca + c2 * sa;
246            let new_c2 = c2 * ca - perp * sa;
247
248            t = new_c0 * self.a[i] + c2 * self.d[i] + t;
249
250            c0 = new_c0;
251            c1 = new_c1;
252            c2 = new_c2;
253            i += 1;
254        }
255
256        Ok(AAffine3::<F>::from_mat3_translation(
257            AMat3::<F>::from_cols(c0, c1, c2),
258            t,
259        ))
260    }
261
262    fn all_fk(
263        &self,
264        q: &SRobotQ<N, F>,
265    ) -> Result<(AAffine3<F>, [AAffine3<F>; N], AAffine3<F>), Self::Error> {
266        let frames = self.fk(q)?;
267        // DH has no tool/suffix offset, so the last accumulated frame *is*
268        // the EE frame. For N == 0 there is nothing to accumulate; the EE
269        // is identity.
270        let end = if N > 0 {
271            frames[N - 1]
272        } else {
273            AAffine3::<F>::IDENTITY
274        };
275        Ok((self.base_tf(), frames, end))
276    }
277
278    fn joint_axes_positions(
279        &self,
280        q: &SRobotQ<N, F>,
281    ) -> Result<([AVec3<F>; N], [AVec3<F>; N], AVec3<F>), Self::Error> {
282        let frames = self.fk(q)?;
283        let mut axes = [AVec3::<F>::Z; N];
284        let mut positions = [AVec3::<F>::ZERO; N];
285
286        for i in 1..N {
287            axes[i] = frames[i - 1].matrix3().z_axis();
288            positions[i] = frames[i - 1].translation();
289        }
290
291        Ok((axes, positions, frames[N - 1].translation()))
292    }
293}
294
295impl From<DHJoint<f32>> for DHJoint<f64> {
296    #[inline]
297    fn from(j: DHJoint<f32>) -> Self {
298        DHJoint {
299            a: j.a as f64,
300            alpha: j.alpha as f64,
301            d: j.d as f64,
302            theta_offset: j.theta_offset as f64,
303        }
304    }
305}
306
307impl From<DHJoint<f64>> for DHJoint<f32> {
308    #[inline]
309    fn from(j: DHJoint<f64>) -> Self {
310        DHJoint {
311            a: j.a as f32,
312            alpha: j.alpha as f32,
313            d: j.d as f32,
314            theta_offset: j.theta_offset as f32,
315        }
316    }
317}
318
319#[inline]
320fn cast_arr<const N: usize, A: Copy, B: Copy>(src: [A; N], cast: impl Fn(A) -> B) -> [B; N] {
321    std::array::from_fn(|i| cast(src[i]))
322}
323
324impl<const N: usize> From<DHChain<N, f32>> for DHChain<N, f64> {
325    #[inline]
326    fn from(c: DHChain<N, f32>) -> Self {
327        DHChain::<N, f64> {
328            a: cast_arr(c.a, |x| x as f64),
329            d: cast_arr(c.d, |x| x as f64),
330            sin_alpha: cast_arr(c.sin_alpha, |x| x as f64),
331            cos_alpha: cast_arr(c.cos_alpha, |x| x as f64),
332            theta_offset: cast_arr(c.theta_offset, |x| x as f64),
333        }
334    }
335}
336
337impl<const N: usize> From<DHChain<N, f64>> for DHChain<N, f32> {
338    #[inline]
339    fn from(c: DHChain<N, f64>) -> Self {
340        DHChain::<N, f32> {
341            a: cast_arr(c.a, |x| x as f32),
342            d: cast_arr(c.d, |x| x as f32),
343            sin_alpha: cast_arr(c.sin_alpha, |x| x as f32),
344            cos_alpha: cast_arr(c.cos_alpha, |x| x as f32),
345            theta_offset: cast_arr(c.theta_offset, |x| x as f32),
346        }
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353    use glam_traits_ext::TAffine3;
354
355    fn planar_2dof<F: FKScalar>() -> DHChain<2, F> {
356        let zero = F::zero();
357        let one = F::one();
358        DHChain::<2, F>::from_joints([
359            DHJoint { a: one, alpha: zero, d: zero, theta_offset: zero },
360            DHJoint { a: one, alpha: zero, d: zero, theta_offset: zero },
361        ])
362    }
363
364    #[test]
365    fn f32_and_f64_agree_at_zero() {
366        let f32_chain = planar_2dof::<f32>();
367        let f64_chain = planar_2dof::<f64>();
368
369        let q32 = SRobotQ::<2, f32>::zeros();
370        let q64 = SRobotQ::<2, f64>::zeros();
371
372        let end32 = f32_chain.fk_end(&q32).unwrap();
373        let end64 = f64_chain.fk_end(&q64).unwrap();
374
375        let t32 = end32.translation();
376        let t64 = end64.translation();
377        assert!((t32.x() as f64 - t64.x()).abs() < 1e-5);
378        assert!((t32.y() as f64 - t64.y()).abs() < 1e-5);
379        assert!((t32.z() as f64 - t64.z()).abs() < 1e-5);
380    }
381
382    #[test]
383    fn const_f64_constructor_matches_runtime_f64() {
384        const CHAIN_CONST: DHChain<2, f64> = DHChain::<2, f64>::new_f64([
385            DHJoint { a: 1.0, alpha: 0.0, d: 0.0, theta_offset: 0.0 },
386            DHJoint { a: 1.0, alpha: 0.0, d: 0.0, theta_offset: 0.0 },
387        ]);
388        let chain_runtime = planar_2dof::<f64>();
389        let q = SRobotQ::<2, f64>::from_array([0.5, -0.3]);
390        let end_const = CHAIN_CONST.fk_end(&q).unwrap().translation();
391        let end_runtime = chain_runtime.fk_end(&q).unwrap().translation();
392        assert!((end_const.x() - end_runtime.x()).abs() < 1e-12);
393        assert!((end_const.y() - end_runtime.y()).abs() < 1e-12);
394    }
395
396    #[test]
397    fn cast_f32_to_f64_and_back_preserves_fk() {
398        let f32_chain = planar_2dof::<f32>();
399        let f64_chain: DHChain<2, f64> = f32_chain.clone().into();
400        let f32_again: DHChain<2, f32> = f64_chain.clone().into();
401
402        let q32 = SRobotQ::<2, f32>::from_array([0.5, -0.3]);
403        let q64 = SRobotQ::<2, f64>::from_array([0.5, -0.3]);
404
405        let end32 = f32_chain.fk_end(&q32).unwrap().translation();
406        let end64 = f64_chain.fk_end(&q64).unwrap().translation();
407        let end32b = f32_again.fk_end(&q32).unwrap().translation();
408
409        assert!((end32.x() as f64 - end64.x()).abs() < 1e-4);
410        assert!((end32.y() as f64 - end64.y()).abs() < 1e-4);
411        assert_eq!(end32, end32b);
412    }
413}