Skip to main content

deke_types/fk/
hp.rs

1use const_soft_float::soft_f32::SoftF32;
2use const_soft_float::soft_f64::SoftF64;
3use glam_traits_ext::{FloatAffine, FloatMat, FloatVec, TAffine3, TMat3, TVec3};
4
5#[cfg(debug_assertions)]
6use crate::DekeError;
7use crate::SRobotQ;
8
9use super::{
10    AAffine3, AMat3, AVec3, FKChain, FKScalar, check_finite, const_sin_cos, const_sin_cos_f64,
11};
12
13#[derive(Debug, Clone, Copy)]
14pub struct HPJoint<F: FKScalar = f32> {
15    pub a: F,
16    pub alpha: F,
17    pub beta: F,
18    pub d: F,
19    pub theta_offset: F,
20}
21
22/// Precomputed Hayati-Paul chain with SoA layout.
23///
24/// Convention: `T_i = Rz(θ) · Rx(α) · Ry(β) · Tx(a) · Tz(d)`
25///
26/// HP adds a `β` rotation about Y, which makes it numerically stable for
27/// nearly-parallel consecutive joint axes where standard DH is singular.
28#[derive(Debug, Clone)]
29pub struct HPChain<const N: usize, F: FKScalar = f32> {
30    a: [F; N],
31    d: [F; N],
32    sin_alpha: [F; N],
33    cos_alpha: [F; N],
34    sin_beta: [F; N],
35    cos_beta: [F; N],
36    theta_offset: [F; N],
37}
38
39impl<const N: usize> HPChain<N, f32> {
40    pub const fn new(joints: [HPJoint<f32>; N]) -> Self {
41        let mut a = [0.0; N];
42        let mut d = [0.0; N];
43        let mut sin_alpha = [0.0; N];
44        let mut cos_alpha = [0.0; N];
45        let mut sin_beta = [0.0; N];
46        let mut cos_beta = [0.0; N];
47        let mut theta_offset = [0.0; N];
48
49        let mut i = 0;
50        while i < N {
51            a[i] = joints[i].a;
52            d[i] = joints[i].d;
53            let (sa, ca) = const_sin_cos(joints[i].alpha);
54            sin_alpha[i] = sa;
55            cos_alpha[i] = ca;
56            let (sb, cb) = const_sin_cos(joints[i].beta);
57            sin_beta[i] = sb;
58            cos_beta[i] = cb;
59            theta_offset[i] = joints[i].theta_offset;
60            i += 1;
61        }
62
63        Self {
64            a,
65            d,
66            sin_alpha,
67            cos_alpha,
68            sin_beta,
69            cos_beta,
70            theta_offset,
71        }
72    }
73
74    /// Construct from the row-major `HP_H` and `HP_P` const arrays emitted by
75    /// the workcell macro.
76    ///
77    /// `h`: `[[f32; N]; 3]` — rows are (x, y, z) components across joints.
78    /// `p`: `[[f32; N]; 3]` — rows are (x, y, z) components across points.
79    ///
80    /// Each `h[_][i]` is joint `i`'s axis in the base frame at zero config.
81    /// `p[_][0]` is the base-to-joint-0 offset; `p[_][i]` for `1..N` is the
82    /// offset from joint `i-1`'s origin to joint `i`'s origin. The tool
83    /// offset from joint `N-1` to the flange is not part of this input
84    /// because `HPChain` has no end-effector slot — wrap the result in a
85    /// [`TransformedFK`](crate::TransformedFK) if a tool offset is required.
86    ///
87    /// `theta_offset` is set to zero for every joint: at zero config each
88    /// local x-axis is pinned to `Rx(α) · Ry(β) · [1, 0, 0]` expressed in the
89    /// parent frame.
90    pub const fn from_hp(h: &[[f32; N]; 3], p: &[[f32; N]; 3]) -> Self {
91
92        let mut a = [0.0f32; N];
93        let mut d = [0.0f32; N];
94        let mut sin_alpha = [0.0f32; N];
95        let mut cos_alpha = [0.0f32; N];
96        let mut sin_beta = [0.0f32; N];
97        let mut cos_beta = [0.0f32; N];
98
99        let mut c0 = [1.0f32, 0.0, 0.0];
100        let mut c1 = [0.0f32, 1.0, 0.0];
101        let mut c2 = [0.0f32, 0.0, 1.0];
102
103        const EPS: f32 = 1e-12;
104
105        let mut i = 0;
106        while i < N {
107            let hx = h[0][i];
108            let hy = h[1][i];
109            let hz = h[2][i];
110            let px = p[0][i];
111            let py = p[1][i];
112            let pz = p[2][i];
113
114            let vx = c0[0] * hx + c0[1] * hy + c0[2] * hz;
115            let vy = c1[0] * hx + c1[1] * hy + c1[2] * hz;
116            let vz = c2[0] * hx + c2[1] * hy + c2[2] * hz;
117            let ux = c0[0] * px + c0[1] * py + c0[2] * pz;
118            let uy = c1[0] * px + c1[1] * py + c1[2] * pz;
119            let uz = c2[0] * px + c2[1] * py + c2[2] * pz;
120
121            let sb = vx;
122            let cb = SoftF32::from_f32(vy * vy + vz * vz).sqrt().to_f32();
123
124            let (sa, ca) = if cb > EPS {
125                (-vy / cb, vz / cb)
126            } else {
127                (0.0, 1.0)
128            };
129
130            let big_a = ux;
131            let big_b = sa * uy - ca * uz;
132            let ai = cb * big_a + sb * big_b;
133            let di = sb * big_a - cb * big_b;
134
135            a[i] = ai;
136            d[i] = di;
137            sin_alpha[i] = sa;
138            cos_alpha[i] = ca;
139            sin_beta[i] = sb;
140            cos_beta[i] = cb;
141
142            let sasb = sa * sb;
143            let casb = ca * sb;
144            let sacb = sa * cb;
145            let cacb = ca * cb;
146            let new_c0 = [
147                cb * c0[0] + sasb * c1[0] - casb * c2[0],
148                cb * c0[1] + sasb * c1[1] - casb * c2[1],
149                cb * c0[2] + sasb * c1[2] - casb * c2[2],
150            ];
151            let new_c1 = [
152                ca * c1[0] + sa * c2[0],
153                ca * c1[1] + sa * c2[1],
154                ca * c1[2] + sa * c2[2],
155            ];
156            let new_c2 = [
157                sb * c0[0] - sacb * c1[0] + cacb * c2[0],
158                sb * c0[1] - sacb * c1[1] + cacb * c2[1],
159                sb * c0[2] - sacb * c1[2] + cacb * c2[2],
160            ];
161            c0 = new_c0;
162            c1 = new_c1;
163            c2 = new_c2;
164
165            i += 1;
166        }
167
168        Self {
169            a,
170            d,
171            sin_alpha,
172            cos_alpha,
173            sin_beta,
174            cos_beta,
175            theta_offset: [0.0f32; N],
176        }
177    }
178}
179
180impl<const N: usize> HPChain<N, f64> {
181    /// `const`-evaluable f64 constructor — analogue of [`HPChain::<N, f32>::new`].
182    pub const fn new_f64(joints: [HPJoint<f64>; N]) -> Self {
183        let mut a = [0.0; N];
184        let mut d = [0.0; N];
185        let mut sin_alpha = [0.0; N];
186        let mut cos_alpha = [0.0; N];
187        let mut sin_beta = [0.0; N];
188        let mut cos_beta = [0.0; N];
189        let mut theta_offset = [0.0; N];
190
191        let mut i = 0;
192        while i < N {
193            a[i] = joints[i].a;
194            d[i] = joints[i].d;
195            let (sa, ca) = const_sin_cos_f64(joints[i].alpha);
196            sin_alpha[i] = sa;
197            cos_alpha[i] = ca;
198            let (sb, cb) = const_sin_cos_f64(joints[i].beta);
199            sin_beta[i] = sb;
200            cos_beta[i] = cb;
201            theta_offset[i] = joints[i].theta_offset;
202            i += 1;
203        }
204
205        Self {
206            a,
207            d,
208            sin_alpha,
209            cos_alpha,
210            sin_beta,
211            cos_beta,
212            theta_offset,
213        }
214    }
215
216    /// `const`-evaluable f64 analogue of [`HPChain::from_hp`].
217    pub const fn from_hp_f64(h: &[[f64; N]; 3], p: &[[f64; N]; 3]) -> Self {
218        let mut a = [0.0f64; N];
219        let mut d = [0.0f64; N];
220        let mut sin_alpha = [0.0f64; N];
221        let mut cos_alpha = [0.0f64; N];
222        let mut sin_beta = [0.0f64; N];
223        let mut cos_beta = [0.0f64; N];
224
225        let mut c0 = [1.0f64, 0.0, 0.0];
226        let mut c1 = [0.0f64, 1.0, 0.0];
227        let mut c2 = [0.0f64, 0.0, 1.0];
228
229        const EPS: f64 = 1e-15;
230
231        let mut i = 0;
232        while i < N {
233            let hx = h[0][i];
234            let hy = h[1][i];
235            let hz = h[2][i];
236            let px = p[0][i];
237            let py = p[1][i];
238            let pz = p[2][i];
239
240            let vx = c0[0] * hx + c0[1] * hy + c0[2] * hz;
241            let vy = c1[0] * hx + c1[1] * hy + c1[2] * hz;
242            let vz = c2[0] * hx + c2[1] * hy + c2[2] * hz;
243            let ux = c0[0] * px + c0[1] * py + c0[2] * pz;
244            let uy = c1[0] * px + c1[1] * py + c1[2] * pz;
245            let uz = c2[0] * px + c2[1] * py + c2[2] * pz;
246
247            let sb = vx;
248            let cb = SoftF64::from_f64(vy * vy + vz * vz).sqrt().to_f64();
249
250            let (sa, ca) = if cb > EPS {
251                (-vy / cb, vz / cb)
252            } else {
253                (0.0, 1.0)
254            };
255
256            let big_a = ux;
257            let big_b = sa * uy - ca * uz;
258            let ai = cb * big_a + sb * big_b;
259            let di = sb * big_a - cb * big_b;
260
261            a[i] = ai;
262            d[i] = di;
263            sin_alpha[i] = sa;
264            cos_alpha[i] = ca;
265            sin_beta[i] = sb;
266            cos_beta[i] = cb;
267
268            let sasb = sa * sb;
269            let casb = ca * sb;
270            let sacb = sa * cb;
271            let cacb = ca * cb;
272            let new_c0 = [
273                cb * c0[0] + sasb * c1[0] - casb * c2[0],
274                cb * c0[1] + sasb * c1[1] - casb * c2[1],
275                cb * c0[2] + sasb * c1[2] - casb * c2[2],
276            ];
277            let new_c1 = [
278                ca * c1[0] + sa * c2[0],
279                ca * c1[1] + sa * c2[1],
280                ca * c1[2] + sa * c2[2],
281            ];
282            let new_c2 = [
283                sb * c0[0] - sacb * c1[0] + cacb * c2[0],
284                sb * c0[1] - sacb * c1[1] + cacb * c2[1],
285                sb * c0[2] - sacb * c1[2] + cacb * c2[2],
286            ];
287            c0 = new_c0;
288            c1 = new_c1;
289            c2 = new_c2;
290
291            i += 1;
292        }
293
294        Self {
295            a,
296            d,
297            sin_alpha,
298            cos_alpha,
299            sin_beta,
300            cos_beta,
301            theta_offset: [0.0f64; N],
302        }
303    }
304}
305
306impl<const N: usize, F: FKScalar> HPChain<N, F> {
307    /// Generic runtime constructor. For `f32` the const-evaluable
308    /// [`HPChain::new`] is preferred; this exists so `HPChain<N, f64>` (and
309    /// any other future scalar) is usable.
310    pub fn from_joints(joints: [HPJoint<F>; N]) -> Self {
311        let zero = F::zero();
312        let mut a = [zero; N];
313        let mut d = [zero; N];
314        let mut sin_alpha = [zero; N];
315        let mut cos_alpha = [zero; N];
316        let mut sin_beta = [zero; N];
317        let mut cos_beta = [zero; N];
318        let mut theta_offset = [zero; N];
319
320        for i in 0..N {
321            a[i] = joints[i].a;
322            d[i] = joints[i].d;
323            let (sa, ca) = joints[i].alpha.sin_cos();
324            sin_alpha[i] = sa;
325            cos_alpha[i] = ca;
326            let (sb, cb) = joints[i].beta.sin_cos();
327            sin_beta[i] = sb;
328            cos_beta[i] = cb;
329            theta_offset[i] = joints[i].theta_offset;
330        }
331
332        Self {
333            a,
334            d,
335            sin_alpha,
336            cos_alpha,
337            sin_beta,
338            cos_beta,
339            theta_offset,
340        }
341    }
342
343    /// Build the local rotation columns and translation for joint `i`.
344    ///
345    /// R = Rz(θ) · Rx(α) · Ry(β), then t = R · [a, 0, d].
346    ///
347    /// Rx(α)·Ry(β) columns:
348    ///   col0 = [ cβ,       sα·sβ,     -cα·sβ     ]
349    ///   col1 = [ 0,        cα,          sα        ]
350    ///   col2 = [ sβ,      -sα·cβ,      cα·cβ     ]
351    ///
352    /// Then Rz(θ) rotates each column: [cθ·x - sθ·y, sθ·x + cθ·y, z]
353    ///
354    /// Translation = a·col0 + d·col2  (since R·[a,0,d] = a·col0 + d·col2).
355    #[inline(always)]
356    fn local_frame(&self, i: usize, st: F, ct: F) -> (AVec3<F>, AVec3<F>, AVec3<F>, AVec3<F>) {
357        let sa = self.sin_alpha[i];
358        let ca = self.cos_alpha[i];
359        let sb = self.sin_beta[i];
360        let cb = self.cos_beta[i];
361        let ai = self.a[i];
362        let di = self.d[i];
363
364        let sa_sb = sa * sb;
365        let sa_cb = sa * cb;
366        let ca_sb = ca * sb;
367        let ca_cb = ca * cb;
368
369        let c0 = AVec3::<F>::new(ct * cb - st * sa_sb, st * cb + ct * sa_sb, -ca_sb);
370        let c1 = AVec3::<F>::new(-st * ca, ct * ca, sa);
371        let c2 = AVec3::<F>::new(ct * sb + st * sa_cb, st * sb - ct * sa_cb, ca_cb);
372        let t = c0 * ai + c2 * di;
373
374        (c0, c1, c2, t)
375    }
376}
377
378/// Accumulate a local rotation + translation into the running transform.
379#[inline(always)]
380fn accumulate<F: FKScalar>(
381    acc_m: &mut AMat3<F>,
382    acc_t: &mut AVec3<F>,
383    local_c0: AVec3<F>,
384    local_c1: AVec3<F>,
385    local_c2: AVec3<F>,
386    local_t: AVec3<F>,
387) {
388    let new_c0 = *acc_m * local_c0;
389    let new_c1 = *acc_m * local_c1;
390    let new_c2 = *acc_m * local_c2;
391    *acc_t = *acc_m * local_t + *acc_t;
392    *acc_m = AMat3::<F>::from_cols(new_c0, new_c1, new_c2);
393}
394
395impl<const N: usize, F: FKScalar> FKChain<N, F> for HPChain<N, F> {
396    #[cfg(debug_assertions)]
397    type Error = DekeError;
398    #[cfg(not(debug_assertions))]
399    type Error = std::convert::Infallible;
400
401    fn fk(&self, q: &SRobotQ<N, F>) -> Result<[AAffine3<F>; N], Self::Error> {
402        check_finite::<N, F>(q)?;
403        let mut out = [AAffine3::<F>::IDENTITY; N];
404        let mut acc_m = AMat3::<F>::IDENTITY;
405        let mut acc_t = AVec3::<F>::ZERO;
406
407        let mut i = 0;
408        while i < N {
409            let (st, ct) = (q.0[i] + self.theta_offset[i]).sin_cos();
410            let (c0, c1, c2, t) = self.local_frame(i, st, ct);
411            accumulate::<F>(&mut acc_m, &mut acc_t, c0, c1, c2, t);
412
413            out[i] = AAffine3::<F>::from_mat3_translation(acc_m, acc_t);
414            i += 1;
415        }
416        Ok(out)
417    }
418
419    fn fk_end(&self, q: &SRobotQ<N, F>) -> Result<AAffine3<F>, Self::Error> {
420        check_finite::<N, F>(q)?;
421        let mut acc_m = AMat3::<F>::IDENTITY;
422        let mut acc_t = AVec3::<F>::ZERO;
423
424        let mut i = 0;
425        while i < N {
426            let (st, ct) = (q.0[i] + self.theta_offset[i]).sin_cos();
427            let (c0, c1, c2, t) = self.local_frame(i, st, ct);
428            accumulate::<F>(&mut acc_m, &mut acc_t, c0, c1, c2, t);
429            i += 1;
430        }
431
432        Ok(AAffine3::<F>::from_mat3_translation(acc_m, acc_t))
433    }
434
435    fn all_fk(
436        &self,
437        q: &SRobotQ<N, F>,
438    ) -> Result<(AAffine3<F>, [AAffine3<F>; N], AAffine3<F>), Self::Error> {
439        let frames = self.fk(q)?;
440        // HP has no tool/suffix offset; the last accumulated frame is the
441        // EE frame.
442        let end = if N > 0 {
443            frames[N - 1]
444        } else {
445            AAffine3::<F>::IDENTITY
446        };
447        Ok((self.base_tf(), frames, end))
448    }
449
450    fn joint_axes_positions(
451        &self,
452        q: &SRobotQ<N, F>,
453    ) -> Result<([AVec3<F>; N], [AVec3<F>; N], AVec3<F>), Self::Error> {
454        let frames = self.fk(q)?;
455        let mut axes = [AVec3::<F>::Z; N];
456        let mut positions = [AVec3::<F>::ZERO; N];
457
458        for i in 1..N {
459            axes[i] = frames[i - 1].matrix3().z_axis();
460            positions[i] = frames[i - 1].translation();
461        }
462
463        Ok((axes, positions, frames[N - 1].translation()))
464    }
465}
466
467impl From<HPJoint<f32>> for HPJoint<f64> {
468    #[inline]
469    fn from(j: HPJoint<f32>) -> Self {
470        HPJoint {
471            a: j.a as f64,
472            alpha: j.alpha as f64,
473            beta: j.beta as f64,
474            d: j.d as f64,
475            theta_offset: j.theta_offset as f64,
476        }
477    }
478}
479
480impl From<HPJoint<f64>> for HPJoint<f32> {
481    #[inline]
482    fn from(j: HPJoint<f64>) -> Self {
483        HPJoint {
484            a: j.a as f32,
485            alpha: j.alpha as f32,
486            beta: j.beta as f32,
487            d: j.d as f32,
488            theta_offset: j.theta_offset as f32,
489        }
490    }
491}
492
493#[inline]
494fn cast_arr<const N: usize, A: Copy, B: Copy>(src: [A; N], cast: impl Fn(A) -> B) -> [B; N] {
495    std::array::from_fn(|i| cast(src[i]))
496}
497
498impl<const N: usize> From<HPChain<N, f32>> for HPChain<N, f64> {
499    #[inline]
500    fn from(c: HPChain<N, f32>) -> Self {
501        HPChain::<N, f64> {
502            a: cast_arr(c.a, |x| x as f64),
503            d: cast_arr(c.d, |x| x as f64),
504            sin_alpha: cast_arr(c.sin_alpha, |x| x as f64),
505            cos_alpha: cast_arr(c.cos_alpha, |x| x as f64),
506            sin_beta: cast_arr(c.sin_beta, |x| x as f64),
507            cos_beta: cast_arr(c.cos_beta, |x| x as f64),
508            theta_offset: cast_arr(c.theta_offset, |x| x as f64),
509        }
510    }
511}
512
513impl<const N: usize> From<HPChain<N, f64>> for HPChain<N, f32> {
514    #[inline]
515    fn from(c: HPChain<N, f64>) -> Self {
516        HPChain::<N, f32> {
517            a: cast_arr(c.a, |x| x as f32),
518            d: cast_arr(c.d, |x| x as f32),
519            sin_alpha: cast_arr(c.sin_alpha, |x| x as f32),
520            cos_alpha: cast_arr(c.cos_alpha, |x| x as f32),
521            sin_beta: cast_arr(c.sin_beta, |x| x as f32),
522            cos_beta: cast_arr(c.cos_beta, |x| x as f32),
523            theta_offset: cast_arr(c.theta_offset, |x| x as f32),
524        }
525    }
526}