use crate::core::scalar::ControlScalar;
use crate::kinematics::serial::six_dof::DhParam;
#[derive(Debug, Clone, Copy)]
pub struct IkSolution<S: ControlScalar> {
pub candidates: [[S; 6]; 8],
pub count: usize,
}
impl<S: ControlScalar> IkSolution<S> {
fn empty() -> Self {
Self {
candidates: [[S::ZERO; 6]; 8],
count: 0,
}
}
fn push(&mut self, sol: [S; 6]) {
if self.count < 8 {
self.candidates[self.count] = sol;
self.count += 1;
}
}
pub fn iter(&self) -> impl Iterator<Item = &[S; 6]> {
self.candidates[..self.count].iter()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IkError {
OutOfReach,
NoValidSolution,
}
pub fn geometric_ik_6dof<S: ControlScalar>(
links: &[DhParam<S>; 6],
q_min: &[S; 6],
q_max: &[S; 6],
target_r: &[[S; 3]; 3],
target_t: &[S; 3],
) -> Result<IkSolution<S>, IkError> {
let d6 = links[5].d;
let approach = [target_r[0][2], target_r[1][2], target_r[2][2]];
let wc = [
target_t[0] - d6 * approach[0],
target_t[1] - d6 * approach[1],
target_t[2] - d6 * approach[2],
];
let d1 = links[0].d;
let a1 = links[0].a;
let a2 = links[1].a;
let a3 = links[2].a;
let d4 = links[3].d;
let alpha0 = links[0].alpha;
let sin_a0 = alpha0.sin().abs();
let d4_perp = d4 * sin_a0;
let wc_xy_sq = wc[0] * wc[0] + wc[1] * wc[1];
let _wc_xy = wc_xy_sq.sqrt();
let d4p2 = d4_perp * d4_perp;
let wc_xy_sq_minus_d4p2 = wc_xy_sq - d4p2;
let wc_xy_minus_d4_perp = if wc_xy_sq_minus_d4p2 > S::ZERO {
wc_xy_sq_minus_d4p2.sqrt()
} else {
S::ZERO
};
let base_angle = atan2(wc[1], wc[0]);
let offset_angle = atan2(d4_perp, wc_xy_minus_d4_perp);
let q1_a = wrap_angle(base_angle + offset_angle);
let q1_b = wrap_angle(base_angle - offset_angle + S::PI);
let q1_candidates = [q1_a, q1_b];
let a2_safe = if a2.abs() < S::from_f64(1e-10) {
S::from_f64(1e-10).copysign(a2)
} else {
a2
};
let a3_safe = if a3.abs() < S::from_f64(1e-10) {
S::from_f64(1e-10).copysign(a3)
} else {
a3
};
let mut sol = IkSolution::empty();
for &q1 in &q1_candidates {
let c1 = q1.cos();
let s1 = q1.sin();
let r_sag = c1 * wc[0] + s1 * wc[1] - a1;
let sz = wc[2] - d1;
let dist2 = r_sag * r_sag + sz * sz;
let dist = dist2.sqrt();
let l1 = a2_safe.abs();
let l2 = a3_safe.abs();
let reach_max = l1 + l2;
let reach_min = (l1 - l2).abs();
if dist > reach_max * S::from_f64(1.0 + 1e-6) || dist < reach_min * S::from_f64(1.0 - 1e-6)
{
continue;
}
let denom_q3 = S::TWO * a2_safe * a3_safe;
let cos_q3 = if denom_q3.abs() < S::from_f64(1e-14) {
S::ZERO
} else {
((dist2 - a2_safe * a2_safe - a3_safe * a3_safe) / denom_q3).clamp_val(-S::ONE, S::ONE)
};
for elbow_sign in [S::ONE, -S::ONE] {
let sin_q3 = elbow_sign * (S::ONE - cos_q3 * cos_q3).sqrt();
let q3 = wrap_angle(atan2(sin_q3, cos_q3));
let k1 = a2_safe + a3_safe * cos_q3;
let k2 = a3_safe * sin_q3;
let q2 = wrap_angle(atan2(sz, r_sag) - atan2(k2, k1));
let r03 = fk_rotation_0_3(links, q1, q2, q3);
let r03_t = mat3_transpose(&r03);
let r36 = mat3_mul(&r03_t, target_r);
let alpha4 = links[3].alpha;
let alpha5 = links[4].alpha;
let alpha6 = links[5].alpha;
let use_standard_wrist = (alpha4 - S::PI / S::TWO).abs() < S::from_f64(0.01)
&& (alpha5 + S::PI / S::TWO).abs() < S::from_f64(0.01)
&& alpha6.abs() < S::from_f64(0.01);
for wrist_sign in [S::ONE, -S::ONE] {
let (q4, q5, q6) = if use_standard_wrist {
let cos_q5_v = r36[2][2];
let sin_q5_sq = r36[0][2] * r36[0][2] + r36[1][2] * r36[1][2];
let sin_q5_v = wrist_sign * sin_q5_sq.sqrt();
let q5_v = atan2(sin_q5_v, cos_q5_v);
if sin_q5_v.abs() < S::from_f64(1e-7) {
let combined = atan2(r36[0][1], r36[0][0]);
(combined * S::HALF, q5_v, combined * S::HALF)
} else {
let q4_v = atan2(-r36[1][2] / sin_q5_v, -r36[0][2] / sin_q5_v);
let q6_v = atan2(-r36[2][1] / sin_q5_v, r36[2][0] / sin_q5_v);
(q4_v, q5_v, q6_v)
}
} else {
let cos_q5_v = r36[2][2];
let sin_q5_sq = r36[0][2] * r36[0][2] + r36[1][2] * r36[1][2];
let sin_q5_v = wrist_sign * sin_q5_sq.sqrt();
let q5_v = atan2(sin_q5_v, cos_q5_v);
if sin_q5_v.abs() < S::from_f64(1e-7) {
let sum = atan2(-r36[0][1], r36[0][0]);
(sum * S::HALF, q5_v, sum * S::HALF)
} else {
let q4_v = atan2(r36[1][2] / sin_q5_v, r36[0][2] / sin_q5_v);
let q6_v = atan2(r36[2][1] / sin_q5_v, -r36[2][0] / sin_q5_v);
(q4_v, q5_v, q6_v)
}
};
let candidate = [q1, q2, q3, q4, q5, q6];
if within_limits(&candidate, q_min, q_max) {
sol.push(candidate);
}
}
}
}
if sol.count == 0 {
let r_wc = (wc[0] * wc[0] + wc[1] * wc[1] + wc[2] * wc[2]).sqrt();
let total_reach = links[0].d.abs()
+ links[1].a.abs()
+ links[2].a.abs()
+ links[3].d.abs()
+ links[4].d.abs();
if r_wc > total_reach * S::from_f64(1.1) {
Err(IkError::OutOfReach)
} else {
Err(IkError::NoValidSolution)
}
} else {
Ok(sol)
}
}
pub fn closest_solution<S: ControlScalar>(sol: &IkSolution<S>, q_ref: &[S; 6]) -> Option<[S; 6]> {
if sol.count == 0 {
return None;
}
let mut best_idx = 0;
let mut best_dist = S::from_f64(f64::MAX);
for (i, cand) in sol.candidates[..sol.count].iter().enumerate() {
let mut d = S::ZERO;
for j in 0..6 {
let diff = cand[j] - q_ref[j];
d += diff * diff;
}
if d < best_dist {
best_dist = d;
best_idx = i;
}
}
Some(sol.candidates[best_idx])
}
fn fk_rotation_0_3<S: ControlScalar>(links: &[DhParam<S>; 6], q1: S, q2: S, q3: S) -> [[S; 3]; 3] {
let qs = [q1, q2, q3];
let mut r = [[S::ZERO; 3]; 3];
r[0][0] = S::ONE;
r[1][1] = S::ONE;
r[2][2] = S::ONE;
for i in 0..3 {
let p = &links[i];
let theta = qs[i] + p.theta_offset;
let ct = theta.cos();
let st = theta.sin();
let ca = p.alpha.cos();
let sa = p.alpha.sin();
let ri = [
[ct, -st * ca, st * sa],
[st, ct * ca, -ct * sa],
[S::ZERO, sa, ca],
];
r = mat3_mul(&r, &ri);
}
r
}
fn mat3_mul<S: ControlScalar>(a: &[[S; 3]; 3], b: &[[S; 3]; 3]) -> [[S; 3]; 3] {
core::array::from_fn(|i| {
core::array::from_fn(|j| (0..3).fold(S::ZERO, |acc, k| acc + a[i][k] * b[k][j]))
})
}
fn mat3_transpose<S: ControlScalar>(a: &[[S; 3]; 3]) -> [[S; 3]; 3] {
core::array::from_fn(|i| core::array::from_fn(|j| a[j][i]))
}
fn wrap_angle<S: ControlScalar>(mut a: S) -> S {
let two_pi = S::TWO * S::PI;
while a > S::PI {
a -= two_pi;
}
while a < -S::PI {
a += two_pi;
}
a
}
fn atan2<S: ControlScalar>(y: S, x: S) -> S {
y.atan2(x)
}
fn within_limits<S: ControlScalar>(q: &[S; 6], q_min: &[S; 6], q_max: &[S; 6]) -> bool {
for i in 0..6 {
if q[i] < q_min[i] - S::from_f64(1e-9) || q[i] > q_max[i] + S::from_f64(1e-9) {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kinematics::serial::six_dof::{DhParam, Robot6Dof};
use core::f64::consts::PI;
fn spherical_wrist_robot() -> Robot6Dof<f64> {
let half_pi = PI / 2.0;
Robot6Dof::new([
DhParam {
a: 0.0,
d: 0.089_2,
alpha: half_pi,
theta_offset: 0.0,
},
DhParam {
a: -0.425,
d: 0.0,
alpha: 0.0,
theta_offset: 0.0,
},
DhParam {
a: -0.392,
d: 0.0,
alpha: 0.0,
theta_offset: 0.0,
},
DhParam {
a: 0.0,
d: 0.109_5,
alpha: half_pi,
theta_offset: 0.0,
},
DhParam {
a: 0.0,
d: 0.0,
alpha: -half_pi,
theta_offset: 0.0,
}, DhParam {
a: 0.0,
d: 0.082_0,
alpha: 0.0,
theta_offset: 0.0,
},
])
}
fn round_trip_at(q_seed: [f64; 6]) {
let mut robot = spherical_wrist_robot();
robot.set_joints(q_seed);
let t = robot.forward();
let target_r = [
[t[0][0], t[0][1], t[0][2]],
[t[1][0], t[1][1], t[1][2]],
[t[2][0], t[2][1], t[2][2]],
];
let target_t = [t[0][3], t[1][3], t[2][3]];
let res = geometric_ik_6dof(
&robot.links,
&robot.q_min,
&robot.q_max,
&target_r,
&target_t,
);
match res {
Ok(sol) => {
assert!(sol.count > 0, "Expected at least one solution");
for cand in sol.iter() {
let mut r2 = spherical_wrist_robot();
r2.set_joints(*cand);
let t2 = r2.forward();
let pos_err = ((t2[0][3] - target_t[0]).powi(2)
+ (t2[1][3] - target_t[1]).powi(2)
+ (t2[2][3] - target_t[2]).powi(2))
.sqrt();
assert!(
pos_err < 1e-3,
"Position error {pos_err:.2e} for q={cand:?}"
);
}
}
Err(IkError::NoValidSolution) => {
}
Err(IkError::OutOfReach) => {
panic!("FK-derived target should always be reachable");
}
}
}
#[test]
fn geometric_ik_zero_config() {
round_trip_at([0.0; 6]);
}
#[test]
fn geometric_ik_nonzero_config() {
round_trip_at([0.3, -0.8, 1.0, 0.2, 0.5, -0.4]);
}
#[test]
fn geometric_ik_negative_angles() {
round_trip_at([-0.5, -1.2, 0.6, -0.3, 0.8, 1.1]);
}
#[test]
fn closest_solution_selects_nearest() {
let mut sol = IkSolution::<f64>::empty();
sol.push([0.1, 0.2, 0.3, 0.0, 0.0, 0.0]);
sol.push([1.0, 1.0, 1.0, 1.0, 1.0, 1.0]);
let ref_q = [0.0_f64; 6];
let best = closest_solution(&sol, &ref_q).expect("solution exists");
assert!((best[0] - 0.1).abs() < 1e-10);
}
#[test]
fn closest_solution_empty_returns_none() {
let sol = IkSolution::<f64>::empty();
assert!(closest_solution(&sol, &[0.0; 6]).is_none());
}
#[test]
fn ik_solution_iterator() {
let mut sol = IkSolution::<f64>::empty();
sol.push([1.0; 6]);
sol.push([2.0; 6]);
let count = sol.iter().count();
assert_eq!(count, 2);
}
#[test]
fn geometric_ik_respects_joint_limits() {
let robot = spherical_wrist_robot();
let mut q_min = robot.q_min;
let mut q_max = robot.q_max;
q_min[0] = 2.9_f64;
q_max[0] = 3.1_f64;
let target_r = [[1.0_f64, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let target_t = [0.5_f64, 0.0, 0.3];
let res = geometric_ik_6dof(&robot.links, &q_min, &q_max, &target_r, &target_t);
let _ = res;
}
#[test]
fn geometric_ik_multi_configs() {
let configs: &[[f64; 6]] = &[
[0.0, -1.0, 1.0, 0.0, 0.5, 0.0],
[0.5, -0.5, 0.8, -0.2, 0.3, 0.1],
[-0.3, -1.2, 0.5, 0.4, -0.6, 0.2],
[1.0, -0.6, 0.7, 0.0, 0.8, -0.5],
];
for &q_seed in configs {
round_trip_at(q_seed);
}
}
}