use crate::physics::basic::{
AngularAcceleration, AngularMomentum, AngularVelocity, Inertia, Torque, Vector3,
};
use crate::spatial_geometry::cos_matrix::CosMatrix;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct InertiaTensor {
data: [[Inertia; 3]; 3],
}
impl Default for InertiaTensor {
fn default() -> Self {
Self::from_isotropic(Inertia::default())
}
}
impl InertiaTensor {
pub fn new(
i_xx: Inertia,
i_yy: Inertia,
i_zz: Inertia,
i_xy: Inertia,
i_xz: Inertia,
i_yz: Inertia,
) -> Self {
Self {
data: [
[i_xx, i_xy, i_xz],
[i_xy, i_yy, i_yz],
[i_xz, i_yz, i_zz],
],
}
}
pub fn from_isotropic(j: Inertia) -> Self {
Self::from_diagonal([j, j, j])
}
pub fn from_diagonal(diag: [Inertia; 3]) -> Self {
Self {
data: [
[diag[0], Inertia::default(), Inertia::default()],
[Inertia::default(), diag[1], Inertia::default()],
[Inertia::default(), Inertia::default(), diag[2]],
],
}
}
pub fn diag(&self) -> [Inertia; 3] {
[self.data[0][0], self.data[1][1], self.data[2][2]]
}
pub fn products(&self) -> [Inertia; 3] {
[self.data[0][1], self.data[0][2], self.data[1][2]]
}
pub fn get(&self, row: usize, col: usize) -> Option<Inertia> {
if row >= 3 || col >= 3 {
return None;
}
Some(self.data[row][col])
}
pub fn about_axis(
&self,
n: Vector3<crate::physics::basic::Coef>,
) -> Result<Inertia, crate::dense::error::OperationError> {
let a = [n.x.get_value(), n.y.get_value(), n.z.get_value()];
let norm_sq = a[0] * a[0] + a[1] * a[1] + a[2] * a[2];
if norm_sq == 0.0 {
return Err(crate::dense::error::OperationError::new(
"about_axis: axis vector is zero, no direction to project onto",
));
}
let mut sum = 0.0;
for i in 0..3 {
for j in 0..3 {
sum += a[i] * self.data[i][j].as_kg_m2() * a[j];
}
}
Ok(Inertia::from_kg_m2(sum / norm_sq))
}
pub fn isotropic_equivalent(&self) -> Inertia {
let trace = self.data[0][0].as_kg_m2()
+ self.data[1][1].as_kg_m2()
+ self.data[2][2].as_kg_m2();
Inertia::from_kg_m2(trace / 3.0)
}
pub fn rotate(&self, r: &CosMatrix) -> Self {
let m = r.to_array();
let mut rj = [[0.0_f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
let mut sum = 0.0;
for k in 0..3 {
sum += m[i][k] * self.data[k][j].as_kg_m2();
}
rj[i][j] = sum;
}
}
let mut result = [[Inertia::default(); 3]; 3];
for i in 0..3 {
for j in 0..3 {
let mut sum = 0.0;
for k in 0..3 {
sum += rj[i][k] * m[j][k];
}
result[i][j] = Inertia::from_kg_m2(sum);
}
}
Self { data: result }
}
}
impl std::ops::Mul<Vector3<AngularVelocity>> for InertiaTensor {
type Output = Vector3<AngularMomentum>;
fn mul(self, rhs: Vector3<AngularVelocity>) -> Self::Output {
let w = [
rhs.x.as_rad_per_second(),
rhs.y.as_rad_per_second(),
rhs.z.as_rad_per_second(),
];
let mut out = [0.0_f64; 3];
for i in 0..3 {
for j in 0..3 {
out[i] += self.data[i][j].as_kg_m2() * w[j];
}
}
Vector3::new(
AngularMomentum::from_nms(out[0]),
AngularMomentum::from_nms(out[1]),
AngularMomentum::from_nms(out[2]),
)
}
}
impl std::ops::Mul<Vector3<AngularVelocity>> for &InertiaTensor {
type Output = Vector3<AngularMomentum>;
fn mul(self, rhs: Vector3<AngularVelocity>) -> Self::Output {
(*self) * rhs
}
}
impl<'a> std::ops::Mul<&'a Vector3<AngularVelocity>> for InertiaTensor {
type Output = Vector3<AngularMomentum>;
fn mul(self, rhs: &'a Vector3<AngularVelocity>) -> Self::Output {
self * *rhs
}
}
impl<'a, 'b> std::ops::Mul<&'b Vector3<AngularVelocity>> for &'a InertiaTensor {
type Output = Vector3<AngularMomentum>;
fn mul(self, rhs: &'b Vector3<AngularVelocity>) -> Self::Output {
(*self) * *rhs
}
}
impl std::ops::Mul<Vector3<AngularAcceleration>> for InertiaTensor {
type Output = Vector3<Torque>;
fn mul(self, rhs: Vector3<AngularAcceleration>) -> Self::Output {
let a = [
rhs.x.as_rad_per_second2(),
rhs.y.as_rad_per_second2(),
rhs.z.as_rad_per_second2(),
];
let mut out = [0.0_f64; 3];
for i in 0..3 {
for j in 0..3 {
out[i] += self.data[i][j].as_kg_m2() * a[j];
}
}
Vector3::new(
Torque::from_nm(out[0]),
Torque::from_nm(out[1]),
Torque::from_nm(out[2]),
)
}
}
impl std::ops::Mul<Vector3<AngularAcceleration>> for &InertiaTensor {
type Output = Vector3<Torque>;
fn mul(self, rhs: Vector3<AngularAcceleration>) -> Self::Output {
(*self) * rhs
}
}
impl<'a> std::ops::Mul<&'a Vector3<AngularAcceleration>> for InertiaTensor {
type Output = Vector3<Torque>;
fn mul(self, rhs: &'a Vector3<AngularAcceleration>) -> Self::Output {
self * *rhs
}
}
impl<'a, 'b> std::ops::Mul<&'b Vector3<AngularAcceleration>> for &'a InertiaTensor {
type Output = Vector3<Torque>;
fn mul(self, rhs: &'b Vector3<AngularAcceleration>) -> Self::Output {
(*self) * *rhs
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn unit_inertia_tensor() -> InertiaTensor {
InertiaTensor::new(
Inertia::from_kg_m2(2.0),
Inertia::from_kg_m2(3.0),
Inertia::from_kg_m2(4.0),
Inertia::from_kg_m2(-0.5),
Inertia::from_kg_m2(0.25),
Inertia::from_kg_m2(-0.75),
)
}
#[test]
fn test_new_symmetric() {
let j = unit_inertia_tensor();
assert_relative_eq!(j.get(0, 0).unwrap().as_kg_m2(), 2.0, epsilon = 1e-12);
assert_relative_eq!(j.get(1, 1).unwrap().as_kg_m2(), 3.0, epsilon = 1e-12);
assert_relative_eq!(j.get(2, 2).unwrap().as_kg_m2(), 4.0, epsilon = 1e-12);
assert_eq!(j.get(0, 1), j.get(1, 0));
assert_eq!(j.get(0, 2), j.get(2, 0));
assert_eq!(j.get(1, 2), j.get(2, 1));
assert_relative_eq!(j.get(0, 1).unwrap().as_kg_m2(), -0.5, epsilon = 1e-12);
assert_relative_eq!(j.get(0, 2).unwrap().as_kg_m2(), 0.25, epsilon = 1e-12);
assert_relative_eq!(j.get(1, 2).unwrap().as_kg_m2(), -0.75, epsilon = 1e-12);
assert!(j.get(3, 0).is_none());
assert!(j.get(0, 3).is_none());
assert_relative_eq!(j.diag()[0].as_kg_m2(), 2.0, epsilon = 1e-12);
assert_relative_eq!(j.products()[0].as_kg_m2(), -0.5, epsilon = 1e-12);
assert_relative_eq!(j.products()[2].as_kg_m2(), -0.75, epsilon = 1e-12);
}
#[test]
fn test_isotropic() {
let j = InertiaTensor::from_isotropic(Inertia::from_kg_m2(5.0));
assert_relative_eq!(j.diag()[0].as_kg_m2(), 5.0, epsilon = 1e-12);
assert_relative_eq!(j.diag()[2].as_kg_m2(), 5.0, epsilon = 1e-12);
for p in j.products() {
assert_relative_eq!(p.as_kg_m2(), 0.0, epsilon = 1e-12);
}
assert_relative_eq!(j.isotropic_equivalent().as_kg_m2(), 5.0, epsilon = 1e-12);
assert_relative_eq!(InertiaTensor::default().isotropic_equivalent().as_kg_m2(), 0.0);
}
#[test]
fn test_from_diagonal() {
let j = InertiaTensor::from_diagonal([
Inertia::from_kg_m2(1.0),
Inertia::from_kg_m2(2.0),
Inertia::from_kg_m2(3.0),
]);
assert_relative_eq!(j.get(0, 0).unwrap().as_kg_m2(), 1.0, epsilon = 1e-12);
assert_relative_eq!(j.get(1, 1).unwrap().as_kg_m2(), 2.0, epsilon = 1e-12);
assert_relative_eq!(j.get(2, 2).unwrap().as_kg_m2(), 3.0, epsilon = 1e-12);
assert_relative_eq!(j.get(0, 1).unwrap().as_kg_m2(), 0.0, epsilon = 1e-12);
assert_relative_eq!(j.isotropic_equivalent().as_kg_m2(), 2.0, epsilon = 1e-12);
}
#[test]
fn test_mul_omega_isotropic_keeps_direction() {
let j = InertiaTensor::from_isotropic(Inertia::from_kg_m2(2.0));
let omega = Vector3::new(
AngularVelocity::from_rad_per_second(1.0),
AngularVelocity::from_rad_per_second(2.0),
AngularVelocity::from_rad_per_second(3.0),
);
let l = j * omega;
assert_relative_eq!(l.x.as_nms(), 2.0, epsilon = 1e-12);
assert_relative_eq!(l.y.as_nms(), 4.0, epsilon = 1e-12);
assert_relative_eq!(l.z.as_nms(), 6.0, epsilon = 1e-12);
}
#[test]
fn test_mul_omega_anisotropic_deviates() {
let j = InertiaTensor::new(
Inertia::from_kg_m2(2.0),
Inertia::from_kg_m2(3.0),
Inertia::from_kg_m2(4.0),
Inertia::from_kg_m2(0.0),
Inertia::from_kg_m2(1.0), Inertia::from_kg_m2(0.0),
);
let omega = Vector3::new(
AngularVelocity::from_rad_per_second(0.0),
AngularVelocity::from_rad_per_second(0.0),
AngularVelocity::from_rad_per_second(2.0),
);
let l = j * omega;
assert_relative_eq!(l.x.as_nms(), 2.0, epsilon = 1e-12);
assert_relative_eq!(l.y.as_nms(), 0.0, epsilon = 1e-12);
assert_relative_eq!(l.z.as_nms(), 8.0, epsilon = 1e-12);
let cross_z = l.x.as_nms() * omega.y.as_rad_per_second()
- l.y.as_nms() * omega.x.as_rad_per_second();
assert_relative_eq!(cross_z, 0.0, epsilon = 1e-12);
assert!(l.x.as_nms().abs() > 1e-9);
}
#[test]
fn test_mul_alpha_gives_torque() {
let j = InertiaTensor::from_diagonal([
Inertia::from_kg_m2(2.0),
Inertia::from_kg_m2(4.0),
Inertia::from_kg_m2(6.0),
]);
let alpha = Vector3::new(
AngularAcceleration::from_rad_per_second2(1.0),
AngularAcceleration::from_rad_per_second2(2.0),
AngularAcceleration::from_rad_per_second2(3.0),
);
let m = j * alpha;
assert_relative_eq!(m.x.as_nm(), 2.0, epsilon = 1e-12);
assert_relative_eq!(m.y.as_nm(), 8.0, epsilon = 1e-12);
assert_relative_eq!(m.z.as_nm(), 18.0, epsilon = 1e-12);
}
#[test]
fn test_ref_variants() {
let j = InertiaTensor::from_isotropic(Inertia::from_kg_m2(2.0));
let omega = Vector3::new(
AngularVelocity::from_rad_per_second(1.0),
AngularVelocity::from_rad_per_second(1.0),
AngularVelocity::from_rad_per_second(1.0),
);
assert_eq!((j * omega).x.as_nms(), 2.0);
assert_eq!((&j * omega).x.as_nms(), 2.0);
assert_eq!((j * &omega).x.as_nms(), 2.0);
assert_eq!((&j * &omega).x.as_nms(), 2.0);
let alpha = Vector3::new(
AngularAcceleration::from_rad_per_second2(1.0),
AngularAcceleration::from_rad_per_second2(1.0),
AngularAcceleration::from_rad_per_second2(1.0),
);
assert_eq!((j * alpha).x.as_nm(), 2.0);
assert_eq!((&j * alpha).x.as_nm(), 2.0);
assert_eq!((j * &alpha).x.as_nm(), 2.0);
assert_eq!((&j * &alpha).x.as_nm(), 2.0);
}
#[test]
fn test_about_axis() {
let j = unit_inertia_tensor();
let ex = Vector3::new(
crate::physics::basic::Coef::new(1.0),
crate::physics::basic::Coef::new(0.0),
crate::physics::basic::Coef::new(0.0),
);
assert_relative_eq!(j.about_axis(ex).unwrap().as_kg_m2(), 2.0, epsilon = 1e-12);
let ey = Vector3::new(
crate::physics::basic::Coef::new(0.0),
crate::physics::basic::Coef::new(1.0),
crate::physics::basic::Coef::new(0.0),
);
assert_relative_eq!(j.about_axis(ey).unwrap().as_kg_m2(), 3.0, epsilon = 1e-12);
let ez = Vector3::new(
crate::physics::basic::Coef::new(0.0),
crate::physics::basic::Coef::new(0.0),
crate::physics::basic::Coef::new(1.0),
);
assert_relative_eq!(j.about_axis(ez).unwrap().as_kg_m2(), 4.0, epsilon = 1e-12);
let s = std::f64::consts::FRAC_1_SQRT_2;
let n45 = Vector3::new(
crate::physics::basic::Coef::new(s),
crate::physics::basic::Coef::new(s),
crate::physics::basic::Coef::new(0.0),
);
assert_relative_eq!(j.about_axis(n45).unwrap().as_kg_m2(), 2.0, epsilon = 1e-12);
}
#[test]
fn test_about_axis_independent_of_magnitude() {
let j = unit_inertia_tensor();
let coef = crate::physics::basic::Coef::new;
for k in [1.0, 2.0, 10.0, 1e-3] {
let n = Vector3::new(coef(0.6 * k), coef(0.8 * k), coef(0.0));
let v = j.about_axis(n).unwrap().as_kg_m2();
let n_unit = Vector3::new(coef(0.6), coef(0.8), coef(0.0));
let v_unit = j.about_axis(n_unit).unwrap().as_kg_m2();
assert_relative_eq!(v, v_unit, epsilon = 1e-9);
}
}
#[test]
fn test_about_axis_zero_vector_errors() {
let j = unit_inertia_tensor();
let zero = Vector3::new(
crate::physics::basic::Coef::new(0.0),
crate::physics::basic::Coef::new(0.0),
crate::physics::basic::Coef::new(0.0),
);
let r = j.about_axis(zero);
assert!(r.is_err());
assert!(r.unwrap_err().message.contains("zero"));
}
#[test]
fn test_about_axis_differs_from_diagonal() {
let j = InertiaTensor::new(
Inertia::from_kg_m2(2.0),
Inertia::from_kg_m2(2.0),
Inertia::from_kg_m2(3.0),
Inertia::from_kg_m2(-1.0),
Inertia::default(),
Inertia::default(),
);
let coef = crate::physics::basic::Coef::new;
let ez = Vector3::new(coef(0.0), coef(0.0), coef(1.0));
assert_relative_eq!(j.about_axis(ez).unwrap().as_kg_m2(), 3.0, epsilon = 1e-12);
let s = std::f64::consts::FRAC_1_SQRT_2;
let n45 = Vector3::new(coef(s), coef(s), coef(0.0));
assert_relative_eq!(j.about_axis(n45).unwrap().as_kg_m2(), 1.0, epsilon = 1e-12);
assert!(
(j.about_axis(ez).unwrap().as_kg_m2() - j.about_axis(n45).unwrap().as_kg_m2()).abs()
> 1.0
);
}
#[test]
fn test_about_axis_matches_diagonal_when_isotropic() {
let j = InertiaTensor::from_isotropic(Inertia::from_kg_m2(7.0));
let s = std::f64::consts::FRAC_1_SQRT_2;
let axes = [
Vector3::new(
crate::physics::basic::Coef::new(1.0),
crate::physics::basic::Coef::new(0.0),
crate::physics::basic::Coef::new(0.0),
),
Vector3::new(
crate::physics::basic::Coef::new(s),
crate::physics::basic::Coef::new(s),
crate::physics::basic::Coef::new(0.0),
),
Vector3::new(
crate::physics::basic::Coef::new(1.0 / 3.0_f64.sqrt()),
crate::physics::basic::Coef::new(1.0 / 3.0_f64.sqrt()),
crate::physics::basic::Coef::new(1.0 / 3.0_f64.sqrt()),
),
];
for n in axes {
assert_relative_eq!(j.about_axis(n).unwrap().as_kg_m2(), 7.0, epsilon = 1e-9);
}
}
#[test]
fn test_rotate_identity() {
let j = unit_inertia_tensor();
let r = CosMatrix::unit();
let j2 = j.rotate(&r);
for i in 0..3 {
for k in 0..3 {
assert_relative_eq!(
j2.get(i, k).unwrap().as_kg_m2(),
j.get(i, k).unwrap().as_kg_m2(),
epsilon = 1e-12
);
}
}
}
#[test]
fn test_rotate_preserves_trace_and_symmetry() {
let j = InertiaTensor::from_diagonal([
Inertia::from_kg_m2(1.0),
Inertia::from_kg_m2(2.0),
Inertia::from_kg_m2(3.0),
]);
let r = CosMatrix::new([[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
let j2 = j.rotate(&r);
assert_relative_eq!(j2.get(0, 0).unwrap().as_kg_m2(), 2.0, epsilon = 1e-12);
assert_relative_eq!(j2.get(1, 1).unwrap().as_kg_m2(), 1.0, epsilon = 1e-12);
assert_relative_eq!(j2.get(2, 2).unwrap().as_kg_m2(), 3.0, epsilon = 1e-12);
assert_relative_eq!(
j2.isotropic_equivalent().as_kg_m2(),
j.isotropic_equivalent().as_kg_m2(),
epsilon = 1e-12
);
assert_eq!(j2.get(0, 1), j2.get(1, 0));
assert_eq!(j2.get(0, 2), j2.get(2, 0));
assert_eq!(j2.get(1, 2), j2.get(2, 1));
}
#[test]
fn test_rotate_double_is_identity_asymmetry() {
let j = unit_inertia_tensor();
let r = CosMatrix::new([[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
let rt = r.transfer();
let back = j.rotate(&r).rotate(&rt);
for i in 0..3 {
for k in 0..3 {
assert_relative_eq!(
back.get(i, k).unwrap().as_kg_m2(),
j.get(i, k).unwrap().as_kg_m2(),
epsilon = 1e-9
);
}
}
}
}