use astrodyn_frames::{RefFrameRot, RefFrameState, RefFrameTrans};
use astrodyn_math::JeodQuat;
use glam::DVec3;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct JointKinematicsSpec {
pub axis_in_parent: DVec3,
pub rate_rad_per_s: f64,
pub initial_angle_rad: f64,
}
pub const AXIS_NORM_TOL: f64 = 1.0e-6;
pub fn evaluate(spec: &JointKinematicsSpec, elapsed_seconds: f64) -> (JeodQuat, DVec3) {
let axis_norm_sq = spec.axis_in_parent.length_squared();
assert!(
(axis_norm_sq - 1.0).abs() < AXIS_NORM_TOL,
"JointKinematicsSpec.axis_in_parent must be a unit vector \
(found length² = {axis_norm_sq}, axis = {:?}). Normalize the \
axis once at vehicle-setup time, e.g. \
`axis_in_parent: DVec3::Z` or `axis_in_parent: raw.normalize()`.",
spec.axis_in_parent,
);
assert!(
elapsed_seconds.is_finite(),
"joint kinematics elapsed_seconds must be finite, got {elapsed_seconds}. \
The simulation time has gone non-finite — fix the time-update path."
);
assert!(
spec.rate_rad_per_s.is_finite(),
"JointKinematicsSpec.rate_rad_per_s must be finite, got {}. \
Replace the spec with a finite rate or remove the joint.",
spec.rate_rad_per_s,
);
assert!(
spec.initial_angle_rad.is_finite(),
"JointKinematicsSpec.initial_angle_rad must be finite, got {}. \
Replace the spec with a finite angle.",
spec.initial_angle_rad,
);
let angle = spec.initial_angle_rad + spec.rate_rad_per_s * elapsed_seconds;
let q_parent_this = JeodQuat::left_quat_from_eigen_rotation(angle, spec.axis_in_parent);
let ang_vel_this = spec.axis_in_parent * spec.rate_rad_per_s;
(q_parent_this, ang_vel_this)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SinusoidalJointKinematicsSpec {
pub axis_in_parent: DVec3,
pub amplitude_rad: f64,
pub omega_rad_per_s: f64,
pub phase_rad: f64,
pub offset_rad: f64,
}
pub fn evaluate_sinusoidal(
spec: &SinusoidalJointKinematicsSpec,
elapsed_seconds: f64,
) -> (JeodQuat, DVec3) {
let axis_norm_sq = spec.axis_in_parent.length_squared();
assert!(
(axis_norm_sq - 1.0).abs() < AXIS_NORM_TOL,
"SinusoidalJointKinematicsSpec.axis_in_parent must be a unit vector \
(found length² = {axis_norm_sq}, axis = {:?}). Normalize the axis once \
at vehicle-setup time, e.g. `axis_in_parent: DVec3::Z` or \
`axis_in_parent: raw.normalize()`.",
spec.axis_in_parent,
);
assert!(
elapsed_seconds.is_finite(),
"joint kinematics elapsed_seconds must be finite, got {elapsed_seconds}. \
The simulation time has gone non-finite — fix the time-update path."
);
assert!(
spec.amplitude_rad.is_finite(),
"SinusoidalJointKinematicsSpec.amplitude_rad must be finite, got {}. \
Replace the spec with a finite amplitude or remove the joint.",
spec.amplitude_rad,
);
assert!(
spec.omega_rad_per_s.is_finite(),
"SinusoidalJointKinematicsSpec.omega_rad_per_s must be finite, got {}. \
Replace the spec with a finite frequency or remove the joint.",
spec.omega_rad_per_s,
);
assert!(
spec.phase_rad.is_finite(),
"SinusoidalJointKinematicsSpec.phase_rad must be finite, got {}. \
Replace the spec with a finite phase.",
spec.phase_rad,
);
assert!(
spec.offset_rad.is_finite(),
"SinusoidalJointKinematicsSpec.offset_rad must be finite, got {}. \
Replace the spec with a finite offset.",
spec.offset_rad,
);
let phase = spec.omega_rad_per_s * elapsed_seconds + spec.phase_rad;
let angle = spec.offset_rad + spec.amplitude_rad * phase.sin();
let rate = spec.amplitude_rad * spec.omega_rad_per_s * phase.cos();
let q_parent_this = JeodQuat::left_quat_from_eigen_rotation(angle, spec.axis_in_parent);
let ang_vel_this = spec.axis_in_parent * rate;
(q_parent_this, ang_vel_this)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ClosureJointKinematicsSpec {
pub axis_in_parent: DVec3,
pub fixed_angle_rad: f64,
}
pub fn evaluate_closure(
spec: &ClosureJointKinematicsSpec,
elapsed_seconds: f64,
) -> (JeodQuat, DVec3) {
let axis_norm_sq = spec.axis_in_parent.length_squared();
assert!(
(axis_norm_sq - 1.0).abs() < AXIS_NORM_TOL,
"ClosureJointKinematicsSpec.axis_in_parent must be a unit vector \
(found length² = {axis_norm_sq}, axis = {:?}). Normalize the axis once \
at vehicle-setup time.",
spec.axis_in_parent,
);
assert!(
elapsed_seconds.is_finite(),
"joint kinematics elapsed_seconds must be finite, got {elapsed_seconds}. \
The simulation time has gone non-finite — fix the time-update path."
);
assert!(
spec.fixed_angle_rad.is_finite(),
"ClosureJointKinematicsSpec.fixed_angle_rad must be finite, got {}. \
Replace the spec with a finite angle.",
spec.fixed_angle_rad,
);
let q_parent_this =
JeodQuat::left_quat_from_eigen_rotation(spec.fixed_angle_rad, spec.axis_in_parent);
(q_parent_this, DVec3::ZERO)
}
pub const MAX_MULTI_DOF_AXES: usize = 6;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SingleDofKinematics {
ConstantRate(JointKinematicsSpec),
Sinusoidal(SinusoidalJointKinematicsSpec),
Closure(ClosureJointKinematicsSpec),
}
impl SingleDofKinematics {
fn evaluate(&self, elapsed_seconds: f64) -> (JeodQuat, DVec3) {
match self {
SingleDofKinematics::ConstantRate(spec) => evaluate(spec, elapsed_seconds),
SingleDofKinematics::Sinusoidal(spec) => evaluate_sinusoidal(spec, elapsed_seconds),
SingleDofKinematics::Closure(spec) => evaluate_closure(spec, elapsed_seconds),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MultiDofJointKinematicsSpec {
pub axes: [Option<SingleDofKinematics>; MAX_MULTI_DOF_AXES],
}
impl MultiDofJointKinematicsSpec {
pub fn from_slice(dofs: &[SingleDofKinematics]) -> Self {
assert!(
dofs.len() <= MAX_MULTI_DOF_AXES,
"MultiDofJointKinematicsSpec supports at most {} DOFs, got {}. \
Split the chain into multiple chained joint frame entities, \
one per DOF, so the frame-tree walk produces the same composed \
state.",
MAX_MULTI_DOF_AXES,
dofs.len(),
);
let mut axes = [None; MAX_MULTI_DOF_AXES];
for (i, dof) in dofs.iter().enumerate() {
axes[i] = Some(*dof);
}
Self { axes }
}
pub fn count(&self) -> usize {
self.axes.iter().take_while(|a| a.is_some()).count()
}
}
pub fn evaluate_multi_dof(
spec: &MultiDofJointKinematicsSpec,
elapsed_seconds: f64,
) -> (JeodQuat, DVec3) {
assert!(
elapsed_seconds.is_finite(),
"joint kinematics elapsed_seconds must be finite, got {elapsed_seconds}. \
The simulation time has gone non-finite — fix the time-update path."
);
let mut seen_gap = false;
for (i, slot) in spec.axes.iter().enumerate() {
match slot {
Some(_) => {
assert!(
!seen_gap,
"MultiDofJointKinematicsSpec.axes must be a contiguous \
prefix of `Some(_)` followed by `None`, but axes[{i}] is \
populated after a hole. Pack the populated stages into \
axes[0..count].",
);
}
None => {
seen_gap = true;
}
}
}
let mut acc = RefFrameState {
trans: RefFrameTrans {
position: DVec3::ZERO,
velocity: DVec3::ZERO,
},
rot: RefFrameRot {
q_parent_this: JeodQuat::identity(),
t_parent_this: JeodQuat::identity().left_quat_to_transformation(),
ang_vel_this: DVec3::ZERO,
},
};
for slot in spec.axes.iter() {
let Some(dof) = slot else { break };
let (q_stage, av_stage) = dof.evaluate(elapsed_seconds);
let stage_state = RefFrameState {
trans: RefFrameTrans {
position: DVec3::ZERO,
velocity: DVec3::ZERO,
},
rot: RefFrameRot {
q_parent_this: q_stage,
t_parent_this: q_stage.left_quat_to_transformation(),
ang_vel_this: av_stage,
},
};
acc = acc.incr_right(&stage_state);
}
(acc.rot.q_parent_this, acc.rot.ang_vel_this)
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum JointKinematicsModel {
ConstantRate(JointKinematicsSpec),
Sinusoidal(SinusoidalJointKinematicsSpec),
Closure(ClosureJointKinematicsSpec),
MultiDof(MultiDofJointKinematicsSpec),
}
pub fn evaluate_model(model: &JointKinematicsModel, elapsed_seconds: f64) -> (JeodQuat, DVec3) {
match model {
JointKinematicsModel::ConstantRate(spec) => evaluate(spec, elapsed_seconds),
JointKinematicsModel::Sinusoidal(spec) => evaluate_sinusoidal(spec, elapsed_seconds),
JointKinematicsModel::Closure(spec) => evaluate_closure(spec, elapsed_seconds),
JointKinematicsModel::MultiDof(spec) => evaluate_multi_dof(spec, elapsed_seconds),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn evaluate_zero_rate_zero_initial_angle_is_identity() {
let spec = JointKinematicsSpec {
axis_in_parent: DVec3::Z,
rate_rad_per_s: 0.0,
initial_angle_rad: 0.0,
};
let (q, omega) = evaluate(&spec, 12_345.678_9);
assert_eq!(q, JeodQuat::identity());
assert_eq!(omega, DVec3::ZERO);
}
#[test]
fn evaluate_constant_rate_quarter_turn_about_z() {
let rate = 10.0_f64.to_radians();
let spec = JointKinematicsSpec {
axis_in_parent: DVec3::Z,
rate_rad_per_s: rate,
initial_angle_rad: 0.0,
};
let t_quarter = (PI / 2.0) / rate;
let (q, _omega) = evaluate(&spec, t_quarter);
let expected = JeodQuat::left_quat_from_eigen_rotation(PI / 2.0, DVec3::Z);
assert_eq!(q, expected);
}
#[test]
fn evaluate_angular_velocity_tracks_rate_and_axis() {
let axis = DVec3::Y;
for rate in &[-3.5, 0.0, 1.25] {
let spec = JointKinematicsSpec {
axis_in_parent: axis,
rate_rad_per_s: *rate,
initial_angle_rad: 0.0,
};
let (_q, omega) = evaluate(&spec, 0.0);
assert_eq!(omega, axis * *rate);
let (_q2, omega2) = evaluate(&spec, 7.5);
assert_eq!(omega2, axis * *rate);
}
}
#[test]
fn evaluate_respects_initial_angle() {
let theta0 = 0.4;
let spec = JointKinematicsSpec {
axis_in_parent: DVec3::X,
rate_rad_per_s: 0.0,
initial_angle_rad: theta0,
};
let (q, _omega) = evaluate(&spec, 0.0);
let expected = JeodQuat::left_quat_from_eigen_rotation(theta0, DVec3::X);
assert_eq!(q, expected);
}
#[test]
fn evaluate_left_quat_sign_convention() {
let spec = JointKinematicsSpec {
axis_in_parent: DVec3::Z,
rate_rad_per_s: 1.0,
initial_angle_rad: 0.0,
};
let (q, _omega) = evaluate(&spec, 1.0); let half = 0.5_f64;
let scalar = half.cos();
let zv = -half.sin();
let dq = q.to_glam();
assert!((dq.w - scalar).abs() < 1.0e-15);
assert!((dq.z - zv).abs() < 1.0e-15);
}
#[test]
#[should_panic(expected = "must be a unit vector")]
fn evaluate_panics_on_non_unit_axis() {
let spec = JointKinematicsSpec {
axis_in_parent: DVec3::new(0.0, 0.0, 2.0),
rate_rad_per_s: 1.0,
initial_angle_rad: 0.0,
};
let _ = evaluate(&spec, 0.0);
}
#[test]
#[should_panic(expected = "elapsed_seconds must be finite")]
fn evaluate_panics_on_nan_elapsed() {
let spec = JointKinematicsSpec {
axis_in_parent: DVec3::Z,
rate_rad_per_s: 1.0,
initial_angle_rad: 0.0,
};
let _ = evaluate(&spec, f64::NAN);
}
#[test]
#[should_panic(expected = "rate_rad_per_s must be finite")]
fn evaluate_panics_on_nan_rate() {
let spec = JointKinematicsSpec {
axis_in_parent: DVec3::Z,
rate_rad_per_s: f64::NAN,
initial_angle_rad: 0.0,
};
let _ = evaluate(&spec, 0.0);
}
#[test]
#[should_panic(expected = "initial_angle_rad must be finite")]
fn evaluate_panics_on_nan_initial_angle() {
let spec = JointKinematicsSpec {
axis_in_parent: DVec3::Z,
rate_rad_per_s: 0.0,
initial_angle_rad: f64::INFINITY,
};
let _ = evaluate(&spec, 0.0);
}
#[test]
fn evaluate_axis_is_rotation_eigenvector() {
let axis = DVec3::new(1.0, 1.0, 1.0).normalize();
let spec = JointKinematicsSpec {
axis_in_parent: axis,
rate_rad_per_s: 0.7,
initial_angle_rad: 0.2,
};
let (q, _omega) = evaluate(&spec, 3.0);
let mat = q.left_quat_to_transformation();
let rotated = mat * axis;
for i in 0..3 {
assert!(
(rotated[i] - axis[i]).abs() < 1.0e-12,
"axis component {i}: rotated={} axis={}",
rotated[i],
axis[i],
);
}
}
#[test]
fn sinusoidal_evaluates_at_t_zero_to_closed_form() {
let spec = SinusoidalJointKinematicsSpec {
axis_in_parent: DVec3::Z,
amplitude_rad: 0.4,
omega_rad_per_s: 1.5,
phase_rad: PI / 3.0,
offset_rad: 0.1,
};
let (q, omega) = evaluate_sinusoidal(&spec, 0.0);
let expected_angle = 0.1 + 0.4 * (PI / 3.0).sin();
let expected_rate = 0.4 * 1.5 * (PI / 3.0).cos();
let expected_q = JeodQuat::left_quat_from_eigen_rotation(expected_angle, DVec3::Z);
for i in 0..4 {
assert!(
(q.data[i] - expected_q.data[i]).abs() < 1.0e-15,
"q[{i}]: got {}, expected {}",
q.data[i],
expected_q.data[i]
);
}
assert!((omega.x - 0.0).abs() < 1.0e-15);
assert!((omega.y - 0.0).abs() < 1.0e-15);
assert!((omega.z - expected_rate).abs() < 1.0e-15);
}
#[test]
fn sinusoidal_quarter_period_peak_angle_zero_rate() {
let amplitude = 0.7_f64;
let omega = 2.0_f64;
let spec = SinusoidalJointKinematicsSpec {
axis_in_parent: DVec3::Y,
amplitude_rad: amplitude,
omega_rad_per_s: omega,
phase_rad: 0.0,
offset_rad: 0.0,
};
let t_peak = PI / (2.0 * omega);
let (q, ang_vel) = evaluate_sinusoidal(&spec, t_peak);
let expected_q = JeodQuat::left_quat_from_eigen_rotation(amplitude, DVec3::Y);
for i in 0..4 {
assert!(
(q.data[i] - expected_q.data[i]).abs() < 1.0e-12,
"q[{i}]: got {}, expected {}",
q.data[i],
expected_q.data[i]
);
}
for i in 0..3 {
assert!(
ang_vel[i].abs() < 1.0e-12,
"ang_vel component {i}: got {}, expected 0",
ang_vel[i]
);
}
}
#[test]
fn sinusoidal_zero_amplitude_is_constant_offset() {
let spec = SinusoidalJointKinematicsSpec {
axis_in_parent: DVec3::X,
amplitude_rad: 0.0,
omega_rad_per_s: 1.0,
phase_rad: 1.5,
offset_rad: 0.3,
};
let expected_q = JeodQuat::left_quat_from_eigen_rotation(0.3, DVec3::X);
for &t in &[0.0_f64, 1.0, 7.5, 100.0] {
let (q, ang_vel) = evaluate_sinusoidal(&spec, t);
for i in 0..4 {
assert!(
(q.data[i] - expected_q.data[i]).abs() < 1.0e-15,
"t={t} q[{i}]: got {}, expected {}",
q.data[i],
expected_q.data[i]
);
}
assert_eq!(ang_vel, DVec3::ZERO, "t={t}: ang_vel must be zero");
}
}
#[test]
#[should_panic(expected = "must be a unit vector")]
fn sinusoidal_panics_on_non_unit_axis() {
let spec = SinusoidalJointKinematicsSpec {
axis_in_parent: DVec3::new(0.0, 0.0, 2.0),
amplitude_rad: 0.1,
omega_rad_per_s: 1.0,
phase_rad: 0.0,
offset_rad: 0.0,
};
let _ = evaluate_sinusoidal(&spec, 0.0);
}
#[test]
#[should_panic(expected = "amplitude_rad must be finite")]
fn sinusoidal_panics_on_nan_amplitude() {
let spec = SinusoidalJointKinematicsSpec {
axis_in_parent: DVec3::Z,
amplitude_rad: f64::NAN,
omega_rad_per_s: 1.0,
phase_rad: 0.0,
offset_rad: 0.0,
};
let _ = evaluate_sinusoidal(&spec, 0.0);
}
#[test]
#[should_panic(expected = "omega_rad_per_s must be finite")]
fn sinusoidal_panics_on_inf_omega() {
let spec = SinusoidalJointKinematicsSpec {
axis_in_parent: DVec3::Z,
amplitude_rad: 0.1,
omega_rad_per_s: f64::INFINITY,
phase_rad: 0.0,
offset_rad: 0.0,
};
let _ = evaluate_sinusoidal(&spec, 0.0);
}
#[test]
fn closure_transform_is_time_invariant() {
let spec = ClosureJointKinematicsSpec {
axis_in_parent: DVec3::new(1.0, 1.0, 1.0).normalize(),
fixed_angle_rad: 0.42,
};
let (q0, av0) = evaluate_closure(&spec, 0.0);
for &t in &[1.0_f64, 5.5, 100.0, 1.0e6] {
let (q, av) = evaluate_closure(&spec, t);
for i in 0..4 {
assert_eq!(
q.data[i].to_bits(),
q0.data[i].to_bits(),
"t={t} q[{i}]: not bit-identical"
);
}
assert_eq!(av, av0);
assert_eq!(av, DVec3::ZERO);
}
}
#[test]
fn closure_produces_expected_quaternion() {
let spec = ClosureJointKinematicsSpec {
axis_in_parent: DVec3::Z,
fixed_angle_rad: PI / 4.0,
};
let (q, av) = evaluate_closure(&spec, 0.0);
let expected = JeodQuat::left_quat_from_eigen_rotation(PI / 4.0, DVec3::Z);
assert_eq!(q, expected);
assert_eq!(av, DVec3::ZERO);
}
#[test]
#[should_panic(expected = "must be a unit vector")]
fn closure_panics_on_non_unit_axis() {
let spec = ClosureJointKinematicsSpec {
axis_in_parent: DVec3::new(0.0, 0.0, 2.0),
fixed_angle_rad: 0.5,
};
let _ = evaluate_closure(&spec, 0.0);
}
#[test]
#[should_panic(expected = "fixed_angle_rad must be finite")]
fn closure_panics_on_nan_angle() {
let spec = ClosureJointKinematicsSpec {
axis_in_parent: DVec3::Z,
fixed_angle_rad: f64::NAN,
};
let _ = evaluate_closure(&spec, 0.0);
}
#[test]
fn multi_dof_empty_chain_is_identity() {
let spec = MultiDofJointKinematicsSpec::from_slice(&[]);
assert_eq!(spec.count(), 0);
let (q, av) = evaluate_multi_dof(&spec, 5.0);
assert_eq!(q, JeodQuat::identity());
assert_eq!(av, DVec3::ZERO);
}
#[test]
fn multi_dof_single_stage_matches_constant_rate_kernel() {
let inner = JointKinematicsSpec {
axis_in_parent: DVec3::Z,
rate_rad_per_s: 0.3,
initial_angle_rad: 0.1,
};
let spec =
MultiDofJointKinematicsSpec::from_slice(&[SingleDofKinematics::ConstantRate(inner)]);
for &t in &[0.0_f64, 1.0, 10.0] {
let (q_chain, av_chain) = evaluate_multi_dof(&spec, t);
let (q_single, av_single) = evaluate(&inner, t);
for i in 0..4 {
assert!(
(q_chain.data[i] - q_single.data[i]).abs() < 1.0e-15,
"t={t} q[{i}]: chain {} vs single {}",
q_chain.data[i],
q_single.data[i]
);
}
for i in 0..3 {
assert!(
(av_chain[i] - av_single[i]).abs() < 1.0e-15,
"t={t} av[{i}]: chain {} vs single {}",
av_chain[i],
av_single[i]
);
}
}
}
#[test]
fn multi_dof_two_stage_matches_manual_incr_right() {
let stage0 = JointKinematicsSpec {
axis_in_parent: DVec3::Z,
rate_rad_per_s: 0.5,
initial_angle_rad: 0.2,
};
let stage1 = SinusoidalJointKinematicsSpec {
axis_in_parent: DVec3::Y,
amplitude_rad: 0.3,
omega_rad_per_s: 1.2,
phase_rad: 0.4,
offset_rad: 0.05,
};
let spec = MultiDofJointKinematicsSpec::from_slice(&[
SingleDofKinematics::ConstantRate(stage0),
SingleDofKinematics::Sinusoidal(stage1),
]);
let t = 2.5_f64;
let (q_chain, av_chain) = evaluate_multi_dof(&spec, t);
let (q0, av0) = evaluate(&stage0, t);
let s0 = RefFrameState {
trans: RefFrameTrans {
position: DVec3::ZERO,
velocity: DVec3::ZERO,
},
rot: RefFrameRot {
q_parent_this: q0,
t_parent_this: q0.left_quat_to_transformation(),
ang_vel_this: av0,
},
};
let (q1, av1) = evaluate_sinusoidal(&stage1, t);
let s1 = RefFrameState {
trans: RefFrameTrans {
position: DVec3::ZERO,
velocity: DVec3::ZERO,
},
rot: RefFrameRot {
q_parent_this: q1,
t_parent_this: q1.left_quat_to_transformation(),
ang_vel_this: av1,
},
};
let identity = RefFrameState {
trans: RefFrameTrans {
position: DVec3::ZERO,
velocity: DVec3::ZERO,
},
rot: RefFrameRot {
q_parent_this: JeodQuat::identity(),
t_parent_this: JeodQuat::identity().left_quat_to_transformation(),
ang_vel_this: DVec3::ZERO,
},
};
let expected = identity.incr_right(&s0).incr_right(&s1);
for i in 0..4 {
assert!(
(q_chain.data[i] - expected.rot.q_parent_this.data[i]).abs() < 1.0e-15,
"q[{i}]: chain {} vs manual {}",
q_chain.data[i],
expected.rot.q_parent_this.data[i]
);
}
for i in 0..3 {
assert!(
(av_chain[i] - expected.rot.ang_vel_this[i]).abs() < 1.0e-15,
"av[{i}]: chain {} vs manual {}",
av_chain[i],
expected.rot.ang_vel_this[i]
);
}
}
#[test]
fn multi_dof_same_axis_stages_sum_to_aggregate() {
let axis = DVec3::Z;
let stage_a = JointKinematicsSpec {
axis_in_parent: axis,
rate_rad_per_s: 0.4,
initial_angle_rad: 0.1,
};
let stage_b = JointKinematicsSpec {
axis_in_parent: axis,
rate_rad_per_s: 0.7,
initial_angle_rad: 0.2,
};
let chain = MultiDofJointKinematicsSpec::from_slice(&[
SingleDofKinematics::ConstantRate(stage_a),
SingleDofKinematics::ConstantRate(stage_b),
]);
let aggregate = JointKinematicsSpec {
axis_in_parent: axis,
rate_rad_per_s: 0.4 + 0.7,
initial_angle_rad: 0.1 + 0.2,
};
let t = 3.5_f64;
let (q_chain, av_chain) = evaluate_multi_dof(&chain, t);
let (q_agg, av_agg) = evaluate(&aggregate, t);
for i in 0..4 {
assert!(
(q_chain.data[i] - q_agg.data[i]).abs() < 1.0e-12,
"q[{i}]: chain {} vs aggregate {}",
q_chain.data[i],
q_agg.data[i]
);
}
for i in 0..3 {
assert!(
(av_chain[i] - av_agg[i]).abs() < 1.0e-15,
"av[{i}]: chain {} vs aggregate {}",
av_chain[i],
av_agg[i]
);
}
}
#[test]
fn multi_dof_all_closure_is_time_invariant() {
let spec = MultiDofJointKinematicsSpec::from_slice(&[
SingleDofKinematics::Closure(ClosureJointKinematicsSpec {
axis_in_parent: DVec3::Z,
fixed_angle_rad: 0.5,
}),
SingleDofKinematics::Closure(ClosureJointKinematicsSpec {
axis_in_parent: DVec3::Y,
fixed_angle_rad: -0.3,
}),
]);
let (q0, av0) = evaluate_multi_dof(&spec, 0.0);
for &t in &[1.0_f64, 100.0, 1.0e6] {
let (q, av) = evaluate_multi_dof(&spec, t);
for i in 0..4 {
assert_eq!(
q.data[i].to_bits(),
q0.data[i].to_bits(),
"t={t} q[{i}] not bit-identical"
);
}
assert_eq!(av, av0);
assert_eq!(av, DVec3::ZERO);
}
}
#[test]
#[should_panic(expected = "elapsed_seconds must be finite")]
fn multi_dof_empty_chain_nan_time_panics() {
let spec = MultiDofJointKinematicsSpec {
axes: [None; MAX_MULTI_DOF_AXES],
};
let _ = evaluate_multi_dof(&spec, f64::NAN);
}
#[test]
#[should_panic(expected = "contiguous prefix")]
fn multi_dof_hole_in_middle_panics() {
let mut axes = [None; MAX_MULTI_DOF_AXES];
axes[0] = Some(SingleDofKinematics::ConstantRate(JointKinematicsSpec {
axis_in_parent: DVec3::Z,
rate_rad_per_s: 0.1,
initial_angle_rad: 0.0,
}));
axes[2] = Some(SingleDofKinematics::ConstantRate(JointKinematicsSpec {
axis_in_parent: DVec3::Y,
rate_rad_per_s: 0.2,
initial_angle_rad: 0.0,
}));
let spec = MultiDofJointKinematicsSpec { axes };
let _ = evaluate_multi_dof(&spec, 0.0);
}
#[test]
#[should_panic(expected = "supports at most")]
fn multi_dof_from_slice_too_many_panics() {
let dof = SingleDofKinematics::ConstantRate(JointKinematicsSpec {
axis_in_parent: DVec3::Z,
rate_rad_per_s: 0.1,
initial_angle_rad: 0.0,
});
let too_many = vec![dof; MAX_MULTI_DOF_AXES + 1];
let _ = MultiDofJointKinematicsSpec::from_slice(&too_many);
}
#[test]
fn evaluate_model_dispatches_to_each_variant() {
let t = 1.7_f64;
let const_spec = JointKinematicsSpec {
axis_in_parent: DVec3::Z,
rate_rad_per_s: 0.5,
initial_angle_rad: 0.1,
};
let (q_direct, av_direct) = evaluate(&const_spec, t);
let (q_model, av_model) =
evaluate_model(&JointKinematicsModel::ConstantRate(const_spec), t);
assert_eq!(q_model, q_direct);
assert_eq!(av_model, av_direct);
let sin_spec = SinusoidalJointKinematicsSpec {
axis_in_parent: DVec3::Y,
amplitude_rad: 0.4,
omega_rad_per_s: 2.0,
phase_rad: 0.3,
offset_rad: 0.0,
};
let (q_direct, av_direct) = evaluate_sinusoidal(&sin_spec, t);
let (q_model, av_model) = evaluate_model(&JointKinematicsModel::Sinusoidal(sin_spec), t);
assert_eq!(q_model, q_direct);
assert_eq!(av_model, av_direct);
let close_spec = ClosureJointKinematicsSpec {
axis_in_parent: DVec3::X,
fixed_angle_rad: 0.7,
};
let (q_direct, av_direct) = evaluate_closure(&close_spec, t);
let (q_model, av_model) = evaluate_model(&JointKinematicsModel::Closure(close_spec), t);
assert_eq!(q_model, q_direct);
assert_eq!(av_model, av_direct);
let multi_spec = MultiDofJointKinematicsSpec::from_slice(&[
SingleDofKinematics::ConstantRate(const_spec),
SingleDofKinematics::Sinusoidal(sin_spec),
]);
let (q_direct, av_direct) = evaluate_multi_dof(&multi_spec, t);
let (q_model, av_model) = evaluate_model(&JointKinematicsModel::MultiDof(multi_spec), t);
assert_eq!(q_model, q_direct);
assert_eq!(av_model, av_direct);
}
}