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 joint_axes_positions(
263        &self,
264        q: &SRobotQ<N, F>,
265    ) -> Result<([AVec3<F>; N], [AVec3<F>; N], AVec3<F>), Self::Error> {
266        let frames = self.fk(q)?;
267        let mut axes = [AVec3::<F>::Z; N];
268        let mut positions = [AVec3::<F>::ZERO; N];
269
270        for i in 1..N {
271            axes[i] = frames[i - 1].matrix3().z_axis();
272            positions[i] = frames[i - 1].translation();
273        }
274
275        Ok((axes, positions, frames[N - 1].translation()))
276    }
277}
278
279impl From<DHJoint<f32>> for DHJoint<f64> {
280    #[inline]
281    fn from(j: DHJoint<f32>) -> Self {
282        DHJoint {
283            a: j.a as f64,
284            alpha: j.alpha as f64,
285            d: j.d as f64,
286            theta_offset: j.theta_offset as f64,
287        }
288    }
289}
290
291impl From<DHJoint<f64>> for DHJoint<f32> {
292    #[inline]
293    fn from(j: DHJoint<f64>) -> Self {
294        DHJoint {
295            a: j.a as f32,
296            alpha: j.alpha as f32,
297            d: j.d as f32,
298            theta_offset: j.theta_offset as f32,
299        }
300    }
301}
302
303#[inline]
304fn cast_arr<const N: usize, A: Copy, B: Copy>(src: [A; N], cast: impl Fn(A) -> B) -> [B; N] {
305    std::array::from_fn(|i| cast(src[i]))
306}
307
308impl<const N: usize> From<DHChain<N, f32>> for DHChain<N, f64> {
309    #[inline]
310    fn from(c: DHChain<N, f32>) -> Self {
311        DHChain::<N, f64> {
312            a: cast_arr(c.a, |x| x as f64),
313            d: cast_arr(c.d, |x| x as f64),
314            sin_alpha: cast_arr(c.sin_alpha, |x| x as f64),
315            cos_alpha: cast_arr(c.cos_alpha, |x| x as f64),
316            theta_offset: cast_arr(c.theta_offset, |x| x as f64),
317        }
318    }
319}
320
321impl<const N: usize> From<DHChain<N, f64>> for DHChain<N, f32> {
322    #[inline]
323    fn from(c: DHChain<N, f64>) -> Self {
324        DHChain::<N, f32> {
325            a: cast_arr(c.a, |x| x as f32),
326            d: cast_arr(c.d, |x| x as f32),
327            sin_alpha: cast_arr(c.sin_alpha, |x| x as f32),
328            cos_alpha: cast_arr(c.cos_alpha, |x| x as f32),
329            theta_offset: cast_arr(c.theta_offset, |x| x as f32),
330        }
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337    use glam_traits_ext::TAffine3;
338
339    fn planar_2dof<F: FKScalar>() -> DHChain<2, F> {
340        let zero = F::zero();
341        let one = F::one();
342        DHChain::<2, F>::from_joints([
343            DHJoint { a: one, alpha: zero, d: zero, theta_offset: zero },
344            DHJoint { a: one, alpha: zero, d: zero, theta_offset: zero },
345        ])
346    }
347
348    #[test]
349    fn f32_and_f64_agree_at_zero() {
350        let f32_chain = planar_2dof::<f32>();
351        let f64_chain = planar_2dof::<f64>();
352
353        let q32 = SRobotQ::<2, f32>::zeros();
354        let q64 = SRobotQ::<2, f64>::zeros();
355
356        let end32 = f32_chain.fk_end(&q32).unwrap();
357        let end64 = f64_chain.fk_end(&q64).unwrap();
358
359        let t32 = end32.translation();
360        let t64 = end64.translation();
361        assert!((t32.x() as f64 - t64.x()).abs() < 1e-5);
362        assert!((t32.y() as f64 - t64.y()).abs() < 1e-5);
363        assert!((t32.z() as f64 - t64.z()).abs() < 1e-5);
364    }
365
366    #[test]
367    fn const_f64_constructor_matches_runtime_f64() {
368        const CHAIN_CONST: DHChain<2, f64> = DHChain::<2, f64>::new_f64([
369            DHJoint { a: 1.0, alpha: 0.0, d: 0.0, theta_offset: 0.0 },
370            DHJoint { a: 1.0, alpha: 0.0, d: 0.0, theta_offset: 0.0 },
371        ]);
372        let chain_runtime = planar_2dof::<f64>();
373        let q = SRobotQ::<2, f64>::from_array([0.5, -0.3]);
374        let end_const = CHAIN_CONST.fk_end(&q).unwrap().translation();
375        let end_runtime = chain_runtime.fk_end(&q).unwrap().translation();
376        assert!((end_const.x() - end_runtime.x()).abs() < 1e-12);
377        assert!((end_const.y() - end_runtime.y()).abs() < 1e-12);
378    }
379
380    #[test]
381    fn cast_f32_to_f64_and_back_preserves_fk() {
382        let f32_chain = planar_2dof::<f32>();
383        let f64_chain: DHChain<2, f64> = f32_chain.clone().into();
384        let f32_again: DHChain<2, f32> = f64_chain.clone().into();
385
386        let q32 = SRobotQ::<2, f32>::from_array([0.5, -0.3]);
387        let q64 = SRobotQ::<2, f64>::from_array([0.5, -0.3]);
388
389        let end32 = f32_chain.fk_end(&q32).unwrap().translation();
390        let end64 = f64_chain.fk_end(&q64).unwrap().translation();
391        let end32b = f32_again.fk_end(&q32).unwrap().translation();
392
393        assert!((end32.x() as f64 - end64.x()).abs() < 1e-4);
394        assert!((end32.y() as f64 - end64.y()).abs() < 1e-4);
395        assert_eq!(end32, end32b);
396    }
397}