use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct RoboticArmParams<S: ControlScalar> {
pub link1_length: S,
pub link2_length: S,
pub link1_com: S,
pub link2_com: S,
pub link1_mass: S,
pub link2_mass: S,
pub link1_inertia: S,
pub link2_inertia: S,
pub gravity: S,
}
impl<S: ControlScalar> RoboticArmParams<S> {
#[allow(clippy::too_many_arguments)]
pub fn new(
link1_length: S,
link2_length: S,
link1_com: S,
link2_com: S,
link1_mass: S,
link2_mass: S,
link1_inertia: S,
link2_inertia: S,
gravity: S,
) -> Result<Self, RoboticArmError> {
if link1_length <= S::ZERO {
return Err(RoboticArmError::InvalidParameter(
"link1_length must be positive",
));
}
if link2_length <= S::ZERO {
return Err(RoboticArmError::InvalidParameter(
"link2_length must be positive",
));
}
if link1_com <= S::ZERO || link1_com > link1_length {
return Err(RoboticArmError::InvalidParameter(
"link1_com must be in (0, link1_length]",
));
}
if link2_com <= S::ZERO || link2_com > link2_length {
return Err(RoboticArmError::InvalidParameter(
"link2_com must be in (0, link2_length]",
));
}
if link1_mass <= S::ZERO {
return Err(RoboticArmError::InvalidParameter(
"link1_mass must be positive",
));
}
if link2_mass <= S::ZERO {
return Err(RoboticArmError::InvalidParameter(
"link2_mass must be positive",
));
}
if link1_inertia <= S::ZERO {
return Err(RoboticArmError::InvalidParameter(
"link1_inertia must be positive",
));
}
if link2_inertia <= S::ZERO {
return Err(RoboticArmError::InvalidParameter(
"link2_inertia must be positive",
));
}
if gravity < S::ZERO {
return Err(RoboticArmError::InvalidParameter(
"gravity must be non-negative",
));
}
Ok(Self {
link1_length,
link2_length,
link1_com,
link2_com,
link1_mass,
link2_mass,
link1_inertia,
link2_inertia,
gravity,
})
}
pub fn standard() -> Self {
let l = S::ONE;
let lc = S::HALF;
let m = S::ONE;
let inertia = S::from_f64(1.0 / 3.0);
Self {
link1_length: l,
link2_length: l,
link1_com: lc,
link2_com: lc,
link1_mass: m,
link2_mass: m,
link1_inertia: inertia,
link2_inertia: inertia,
gravity: S::from_f64(9.81),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RoboticArmState<S: ControlScalar> {
pub q1: S,
pub q1_dot: S,
pub q2: S,
pub q2_dot: S,
}
impl<S: ControlScalar> RoboticArmState<S> {
pub fn to_array(&self) -> [S; 4] {
[self.q1, self.q1_dot, self.q2, self.q2_dot]
}
pub fn from_array(a: &[S; 4]) -> Self {
Self {
q1: a[0],
q1_dot: a[1],
q2: a[2],
q2_dot: a[3],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RoboticArmError {
InvalidParameter(&'static str),
SingularMassMatrix,
}
impl core::fmt::Display for RoboticArmError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::InvalidParameter(msg) => write!(f, "Invalid parameter: {}", msg),
Self::SingularMassMatrix => write!(f, "Singular mass matrix in robotic arm"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct RoboticArmPlant<S: ControlScalar> {
params: RoboticArmParams<S>,
state: RoboticArmState<S>,
}
impl<S: ControlScalar> RoboticArmPlant<S> {
pub fn new(params: RoboticArmParams<S>) -> Self {
Self {
params,
state: RoboticArmState::default(),
}
}
pub fn state(&self) -> &RoboticArmState<S> {
&self.state
}
pub fn set_state(&mut self, state: RoboticArmState<S>) {
self.state = state;
}
pub fn reset(&mut self) {
self.state = RoboticArmState::default();
}
pub fn params(&self) -> &RoboticArmParams<S> {
&self.params
}
fn mass_matrix(&self, q2: S) -> [[S; 2]; 2] {
let p = &self.params;
let alpha = p.link1_inertia
+ p.link2_inertia
+ p.link1_mass * p.link1_com * p.link1_com
+ p.link2_mass * (p.link1_length * p.link1_length + p.link2_com * p.link2_com);
let beta = p.link2_mass * p.link1_length * p.link2_com;
let delta = p.link2_inertia + p.link2_mass * p.link2_com * p.link2_com;
let cos_q2 = q2.cos();
let m11 = alpha + S::TWO * beta * cos_q2;
let m12 = delta + beta * cos_q2;
let m21 = m12;
let m22 = delta;
[[m11, m12], [m21, m22]]
}
fn invert_2x2(m: &[[S; 2]; 2]) -> Option<[[S; 2]; 2]> {
let det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
if det.abs() < S::EPSILON * S::from_f64(1e6) {
return None;
}
let inv_det = S::ONE / det;
Some([
[m[1][1] * inv_det, -m[0][1] * inv_det],
[-m[1][0] * inv_det, m[0][0] * inv_det],
])
}
fn derivatives(&self, s: &[S; 4], tau: &[S; 2]) -> Result<[S; 4], RoboticArmError> {
let q1_dot = s[1];
let q2 = s[2];
let q2_dot = s[3];
let p = &self.params;
let beta = p.link2_mass * p.link1_length * p.link2_com;
let sin_q2 = q2.sin();
let h = -beta * sin_q2;
let c11 = h * q2_dot;
let c12 = h * (q1_dot + q2_dot);
let c21 = -h * q1_dot;
let c22 = S::ZERO;
let q1 = s[0];
let g = p.gravity;
let cos_q1 = q1.cos();
let cos_q1_q2 = (q1 + q2).cos();
let g1 = (p.link1_mass * p.link1_com + p.link2_mass * p.link1_length) * g * cos_q1
+ p.link2_mass * p.link2_com * g * cos_q1_q2;
let g2 = p.link2_mass * p.link2_com * g * cos_q1_q2;
let coriolis_1 = c11 * q1_dot + c12 * q2_dot;
let coriolis_2 = c21 * q1_dot + c22 * q2_dot;
let tau_eff = [tau[0] - coriolis_1 - g1, tau[1] - coriolis_2 - g2];
let m = self.mass_matrix(q2);
let m_inv = Self::invert_2x2(&m).ok_or(RoboticArmError::SingularMassMatrix)?;
let q1_ddot = m_inv[0][0] * tau_eff[0] + m_inv[0][1] * tau_eff[1];
let q2_ddot = m_inv[1][0] * tau_eff[0] + m_inv[1][1] * tau_eff[1];
Ok([q1_dot, q1_ddot, q2_dot, q2_ddot])
}
pub fn step(&mut self, tau: &[S; 2], dt: S) -> Result<(), RoboticArmError> {
let s = self.state.to_array();
let half = S::HALF;
let two = S::TWO;
let sixth = S::ONE / S::from_f64(6.0);
let k1 = self.derivatives(&s, tau)?;
let s2: [S; 4] = core::array::from_fn(|i| s[i] + half * dt * k1[i]);
let k2 = self.derivatives(&s2, tau)?;
let s3: [S; 4] = core::array::from_fn(|i| s[i] + half * dt * k2[i]);
let k3 = self.derivatives(&s3, tau)?;
let s4: [S; 4] = core::array::from_fn(|i| s[i] + dt * k3[i]);
let k4 = self.derivatives(&s4, tau)?;
let new_s: [S; 4] = core::array::from_fn(|i| {
s[i] + sixth * dt * (k1[i] + two * k2[i] + two * k3[i] + k4[i])
});
self.state = RoboticArmState::from_array(&new_s);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn static_equilibrium_horizontal() {
let params = RoboticArmParams::standard();
let plant = RoboticArmPlant::new(params);
let p = ¶ms;
let g = p.gravity;
let cos_q1 = 1.0_f64; let cos_q1q2 = 1.0_f64;
let g1 = (p.link1_mass * p.link1_com + p.link2_mass * p.link1_length) * g * cos_q1
+ p.link2_mass * p.link2_com * g * cos_q1q2;
let g2 = p.link2_mass * p.link2_com * g * cos_q1q2;
let s = [0.0_f64, 0.0, 0.0, 0.0];
let tau = [g1, g2];
let deriv = plant.derivatives(&s, &tau).expect("should not fail");
for (i, &d) in deriv.iter().enumerate() {
assert!(
d.abs() < 1e-10,
"derivative[{}] = {} should be zero at static equilibrium",
i,
d
);
}
}
#[test]
fn positive_tau1_increases_q1_dot() {
let params = RoboticArmParams::new(1.0, 1.0, 0.5, 0.5, 1.0, 1.0, 1.0 / 3.0, 1.0 / 3.0, 0.0)
.expect("params ok");
let mut plant = RoboticArmPlant::new(params);
let tau = [1.0_f64, 0.0];
let dt = 0.001_f64;
for _ in 0..200 {
plant.step(&tau, dt).expect("step ok");
}
assert!(
plant.state().q1_dot > 0.0,
"positive τ1 should increase q̇1: got {}",
plant.state().q1_dot
);
}
#[test]
fn mass_matrix_positive_definite() {
let params = RoboticArmParams::standard();
let plant = RoboticArmPlant::new(params);
let q2_values = [0.0_f64, 0.3, 0.9, 1.5, -0.5, -1.2];
for q2 in q2_values {
let m = plant.mass_matrix(q2);
let det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
assert!(det > 0.0, "mass matrix not PD at q2={}: det={}", q2, det);
assert!(
m[0][0] > 0.0,
"M[0][0] must be positive at q2={}: got {}",
q2,
m[0][0]
);
}
}
#[test]
fn invalid_params_rejected() {
assert!(
RoboticArmParams::<f64>::new(-1.0, 1.0, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0, 9.81).is_err()
);
assert!(
RoboticArmParams::<f64>::new(1.0, 1.0, 2.0, 0.5, 1.0, 1.0, 1.0, 1.0, 9.81).is_err()
);
}
}