#![allow(clippy::needless_range_loop)]
use crate::core::matrix::{matmul, Matrix};
use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct WorkspaceAnalyzer<S: ControlScalar> {
pub link_lengths: [S; 3],
pub q_min: [S; 3],
pub q_max: [S; 3],
}
impl<S: ControlScalar> WorkspaceAnalyzer<S> {
pub fn new(link_lengths: [S; 3], q_min: [S; 3], q_max: [S; 3]) -> Self {
Self {
link_lengths,
q_min,
q_max,
}
}
pub fn is_reachable(&self, x: S, y: S, z: S) -> bool {
let r_sq = x * x + y * y + z * z;
let r = r_sq.sqrt();
let max_r = self.max_reach();
let min_r = self.min_reach();
r >= min_r && r <= max_r
}
pub fn dexterity<const N: usize, const M: usize>(&self, j: &Matrix<S, M, N>) -> S {
let jt = j.transpose();
let jjt = matmul(j, &jt);
let det = mat_det(&jjt);
if det < S::ZERO {
S::ZERO
} else {
det.sqrt()
}
}
pub fn condition_number<const N: usize>(&self, j: &Matrix<S, N, N>) -> S {
let jt = j.transpose();
let jtj = matmul(&jt, j);
let sigma_max = power_iteration_largest(&jtj, 30).sqrt();
let sigma_min = inverse_power_iteration_smallest(&jtj, 30).sqrt();
if sigma_min < S::EPSILON * S::from_f64(1e6) {
return S::from_f64(1e12); }
sigma_max / sigma_min
}
pub fn max_reach(&self) -> S {
self.link_lengths[0] + self.link_lengths[1] + self.link_lengths[2]
}
pub fn min_reach(&self) -> S {
let inner = self.link_lengths[0] - self.link_lengths[1] - self.link_lengths[2];
inner.abs()
}
}
fn mat_det<S: ControlScalar, const M: usize>(m: &Matrix<S, M, M>) -> S {
let mut a = m.data;
let mut det = S::ONE;
for col in 0..M {
let mut max_row = col;
let mut max_val = a[col][col].abs();
for row in (col + 1)..M {
if a[row][col].abs() > max_val {
max_val = a[row][col].abs();
max_row = row;
}
}
if max_val < S::EPSILON * S::from_f64(1e6) {
return S::ZERO;
}
if max_row != col {
a.swap(max_row, col);
det = det.neg(); }
let pivot = a[col][col];
det *= pivot;
let inv_pivot = S::ONE / pivot;
for row in (col + 1)..M {
let factor = a[row][col] * inv_pivot;
for c in col..M {
let tmp = a[col][c];
a[row][c] -= factor * tmp;
}
}
}
det
}
fn power_iteration_largest<S: ControlScalar, const N: usize>(
m: &Matrix<S, N, N>,
iters: usize,
) -> S {
if N == 0 {
return S::ZERO;
}
let mut v: [S; N] = core::array::from_fn(|_| S::ONE);
let mut lambda = S::ONE;
for _ in 0..iters {
let mut w: [S; N] = [S::ZERO; N];
for i in 0..N {
for j in 0..N {
w[i] += m.data[i][j] * v[j];
}
}
let mut norm_sq = S::ZERO;
for i in 0..N {
norm_sq += w[i] * w[i];
}
let norm = norm_sq.sqrt();
if norm < S::EPSILON {
return S::ZERO;
}
lambda = norm;
for i in 0..N {
v[i] = w[i] / norm;
}
}
lambda
}
fn inverse_power_iteration_smallest<S: ControlScalar, const N: usize>(
m: &Matrix<S, N, N>,
iters: usize,
) -> S {
if N == 0 {
return S::ZERO;
}
let m_inv = match m.inv() {
Some(inv) => inv,
None => return S::ZERO,
};
let lambda_inv_max = power_iteration_largest(&m_inv, iters);
if lambda_inv_max < S::EPSILON {
return S::ZERO;
}
S::ONE / lambda_inv_max
}
#[cfg(test)]
mod tests {
use super::*;
fn analyzer() -> WorkspaceAnalyzer<f64> {
WorkspaceAnalyzer::new(
[1.0, 1.0, 1.0],
[
-core::f64::consts::PI,
-core::f64::consts::PI,
-core::f64::consts::PI,
],
[
core::f64::consts::PI,
core::f64::consts::PI,
core::f64::consts::PI,
],
)
}
#[test]
fn max_reach_is_sum() {
let a = analyzer();
assert!((a.max_reach() - 3.0).abs() < 1e-12);
}
#[test]
fn min_reach_is_abs() {
let a = WorkspaceAnalyzer::new(
[2.0_f64, 1.0, 0.5],
[-core::f64::consts::PI; 3],
[core::f64::consts::PI; 3],
);
assert!((a.min_reach() - 0.5).abs() < 1e-12);
}
#[test]
fn reachable_point_origin_is_not_reachable_for_equal_links() {
let a = analyzer(); assert!(!a.is_reachable(0.0, 0.0, 0.0));
}
#[test]
fn reachable_point_at_max_reach() {
let a = analyzer(); assert!(a.is_reachable(3.0, 0.0, 0.0));
}
#[test]
fn dexterity_identity_jacobian() {
let a = analyzer();
let j = Matrix::<f64, 2, 2>::identity();
let w = a.dexterity::<2, 2>(&j);
assert!((w - 1.0).abs() < 1e-9, "w={w}");
}
#[test]
fn condition_number_identity_is_one() {
let a = analyzer();
let j = Matrix::<f64, 2, 2>::identity();
let cond = a.condition_number::<2>(&j);
assert!((cond - 1.0).abs() < 1e-6, "cond={cond}");
}
}
use crate::kinematics::forward::Transform3D;
use crate::kinematics::serial::six_dof::DhParam;
pub type DhParams<S> = DhParam<S>;
#[derive(Debug, Clone, Copy)]
pub struct DhConfig<S: ControlScalar, const N: usize> {
pub params: [DhParams<S>; N],
}
impl<S: ControlScalar, const N: usize> DhConfig<S, N> {
pub fn new(params: [DhParams<S>; N]) -> Self {
Self { params }
}
}
#[derive(Debug, Clone, Copy)]
pub struct WorkspaceBounds<S: ControlScalar> {
pub min: [S; 3],
pub max: [S; 3],
}
impl<S: ControlScalar> WorkspaceBounds<S> {
fn empty() -> Self {
let big = S::from_f64(f64::MAX / 2.0);
Self {
min: [big; 3],
max: [-big; 3],
}
}
fn include(&mut self, p: &[S; 3]) {
for i in 0..3 {
if p[i] < self.min[i] {
self.min[i] = p[i];
}
if p[i] > self.max[i] {
self.max[i] = p[i];
}
}
}
pub fn diagonal(&self) -> S {
let dx = self.max[0] - self.min[0];
let dy = self.max[1] - self.min[1];
let dz = self.max[2] - self.min[2];
(dx * dx + dy * dy + dz * dz).sqrt()
}
}
pub fn fk_from_dh<S: ControlScalar, const N: usize>(
config: &DhConfig<S, N>,
q: &[S; N],
) -> Transform3D<S> {
let mut result = Transform3D::identity();
for i in 0..N {
let p = &config.params[i];
let theta = q[i] + p.theta_offset;
let ct = theta.cos();
let st = theta.sin();
let ca = p.alpha.cos();
let sa = p.alpha.sin();
let r = [
[ct, -st * ca, st * sa],
[st, ct * ca, -ct * sa],
[S::ZERO, sa, ca],
];
let t = [p.a * ct, p.a * st, p.d];
let step = Transform3D { r, t };
result = result.compose(&step);
}
result
}
pub fn workspace_reachability<S: ControlScalar, const N: usize>(
config: &DhConfig<S, N>,
q_limits: &[(S, S); N],
samples: usize,
) -> Option<WorkspaceBounds<S>> {
if samples == 0 || N == 0 {
return None;
}
let mut grids: [[S; 32]; N] = [[S::ZERO; 32]; N];
let actual_samples = samples.min(32);
for i in 0..N {
let (lo, hi) = q_limits[i];
if actual_samples == 1 {
grids[i][0] = (lo + hi) * S::HALF;
} else {
let step = (hi - lo) / S::from_f64((actual_samples - 1) as f64);
for k in 0..actual_samples {
grids[i][k] = lo + step * S::from_f64(k as f64);
}
}
}
let mut bounds = WorkspaceBounds::empty();
let total: usize = actual_samples.pow(N as u32);
let mut found_any = false;
for idx in 0..total {
let mut q = [S::ZERO; N];
let mut remaining = idx;
for i in (0..N).rev() {
let digit = remaining % actual_samples;
remaining /= actual_samples;
q[i] = grids[i][digit];
}
let tf = fk_from_dh(config, &q);
bounds.include(&tf.t);
found_any = true;
}
if found_any {
Some(bounds)
} else {
None
}
}
#[cfg(test)]
mod dh_workspace_tests {
use super::*;
use crate::kinematics::serial::six_dof::robot6_ur5_like;
fn ur5_dh_config() -> DhConfig<f64, 6> {
let robot = robot6_ur5_like();
DhConfig::new(robot.links)
}
#[test]
fn fk_from_dh_zero_matches_robot6dof() {
use crate::kinematics::serial::six_dof::robot6_ur5_like;
let robot = robot6_ur5_like();
let t_robot = robot.forward();
let config = ur5_dh_config();
let q = [0.0_f64; 6];
let tf = fk_from_dh(&config, &q);
assert!(
(tf.t[0] - t_robot[0][3]).abs() < 1e-10,
"x: {} vs {}",
tf.t[0],
t_robot[0][3]
);
assert!(
(tf.t[1] - t_robot[1][3]).abs() < 1e-10,
"y: {} vs {}",
tf.t[1],
t_robot[1][3]
);
assert!(
(tf.t[2] - t_robot[2][3]).abs() < 1e-10,
"z: {} vs {}",
tf.t[2],
t_robot[2][3]
);
}
#[test]
fn workspace_reachability_samples_zero_returns_none() {
let config = ur5_dh_config();
let limits = [(-core::f64::consts::PI, core::f64::consts::PI); 6];
assert!(workspace_reachability(&config, &limits, 0).is_none());
}
#[test]
fn workspace_reachability_single_sample() {
let config = ur5_dh_config();
let limits = [(0.0_f64, 0.0_f64); 6];
let bounds = workspace_reachability(&config, &limits, 1)
.expect("single sample should produce bounds");
for i in 0..3 {
assert!(
(bounds.min[i] - bounds.max[i]).abs() < 1e-9,
"min[{i}]={} max[{i}]={}",
bounds.min[i],
bounds.max[i]
);
}
}
#[test]
fn workspace_reachability_nonzero_extent() {
let config = ur5_dh_config();
let limits = [(-core::f64::consts::PI, core::f64::consts::PI); 6];
let bounds = workspace_reachability(&config, &limits, 4).expect("should find workspace");
let diag = bounds.diagonal();
assert!(diag > 0.1, "workspace diagonal={diag:.4} should be nonzero");
}
#[test]
fn workspace_bounds_diagonal_correct() {
let mut b = WorkspaceBounds::<f64>::empty();
b.include(&[0.0, 0.0, 0.0]);
b.include(&[1.0, 1.0, 1.0]);
let d = b.diagonal();
assert!((d - 3.0_f64.sqrt()).abs() < 1e-10, "diag={d}");
}
#[test]
fn dh_config_new_roundtrip() {
let robot = robot6_ur5_like();
let config = DhConfig::new(robot.links);
assert!((config.params[0].d - robot.links[0].d).abs() < 1e-12);
assert!((config.params[1].a - robot.links[1].a).abs() < 1e-12);
}
}