apsis-rs/crates/foundation/apsis-math/tests/mat3.rs

205 lines
6.3 KiB
Rust

//! Tests for three by three matrices.
use apsis_math::{Mat3, Quat, Vec3};
use core::f64::consts::FRAC_PI_2;
const EPS: f64 = 1e-12;
fn assert_mat_close(lhs: Mat3, rhs: Mat3) {
for row in 0..3 {
for column in 0..3 {
let (lhs, rhs) = (
lhs.get(row, column).expect("indices are in range"),
rhs.get(row, column).expect("indices are in range"),
);
assert!(
(lhs - rhs).abs() < EPS,
"at ({row}, {column}): {lhs} vs {rhs}"
);
}
}
}
#[test]
fn constructors_place_entries_in_the_right_slots() {
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
assert_eq!(m.get(0, 0), Some(1.0));
assert_eq!(m.get(0, 1), Some(2.0));
assert_eq!(m.get(0, 2), Some(3.0));
assert_eq!(m.get(1, 0), Some(4.0));
assert_eq!(m.get(2, 2), Some(9.0));
assert_eq!(m.get(3, 0), None);
assert_eq!(m.get(0, 3), None);
assert_eq!(m.row(1), Some(Vec3::new(4.0, 5.0, 6.0)));
assert_eq!(m.column(1), Some(Vec3::new(2.0, 5.0, 8.0)));
assert_eq!(m.row(3), None);
assert_eq!(m.column(3), None);
}
#[test]
fn from_columns_matches_from_rows() {
let c0 = Vec3::new(1.0, 4.0, 7.0);
let c1 = Vec3::new(2.0, 5.0, 8.0);
let c2 = Vec3::new(3.0, 6.0, 9.0);
let expected = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
assert_eq!(Mat3::from_columns(c0, c1, c2), expected);
}
#[test]
fn identity_is_the_multiplicative_unit() {
let m = Mat3::from_rotation_z(FRAC_PI_2);
assert_eq!(Mat3::IDENTITY * m, m);
assert_eq!(m * Mat3::IDENTITY, m);
assert_eq!(Mat3::IDENTITY.determinant(), 1.0);
assert_eq!(Mat3::default(), Mat3::ZERO);
}
#[test]
fn zero_annihilates() {
assert_eq!(Mat3::ZERO * Vec3::new(1.0, 2.0, 3.0), Vec3::ZERO);
assert_eq!(Mat3::ZERO * Mat3::IDENTITY, Mat3::ZERO);
assert_eq!(Mat3::ZERO.determinant(), 0.0);
}
#[test]
fn transpose_swaps_rows_and_columns() {
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
let t = m.transpose();
assert_eq!(t.row(0), m.column(0));
assert_eq!(t.row(1), m.column(1));
assert_eq!(t.row(2), m.column(2));
assert_eq!(t.transpose(), m);
}
#[test]
fn determinant_matches_hand_computation() {
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]);
assert!((m.determinant() + 3.0).abs() < EPS);
assert_eq!(
Mat3::from_diagonal(Vec3::new(2.0, 3.0, 4.0)).determinant(),
24.0
);
}
#[test]
fn trace_sums_the_diagonal() {
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
assert_eq!(m.trace(), 15.0);
}
#[test]
fn matrix_vector_product_uses_rows() {
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
let v = Vec3::new(1.0, 1.0, 1.0);
assert_eq!(m * v, Vec3::new(6.0, 15.0, 24.0));
}
#[test]
fn matrix_product_is_associative() {
let a = Mat3::from_rows([1.0, 2.0, 0.0], [0.0, 1.0, 3.0], [1.0, 0.0, 1.0]);
let b = Mat3::from_rows([0.0, 1.0, 2.0], [1.0, 0.0, 1.0], [2.0, 1.0, 0.0]);
let c = Mat3::from_rows([1.0, 1.0, 1.0], [2.0, 2.0, 2.0], [3.0, 3.0, 3.0]);
assert_eq!((a * b) * c, a * (b * c));
}
#[test]
fn matrix_product_preserves_the_determinant_product() {
let a = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]);
let b = Mat3::from_rows([2.0, 0.0, 1.0], [1.0, 3.0, 2.0], [0.0, 1.0, 4.0]);
let expected = a.determinant() * b.determinant();
assert!(((a * b).determinant() - expected).abs() < 1e-9);
}
#[test]
fn scalar_product_scales_every_entry() {
let m = Mat3::from_rows([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]);
let scaled = m * 2.0;
assert_eq!(scaled.get(0, 0), Some(2.0));
assert_eq!(scaled.get(2, 2), Some(18.0));
assert_eq!(2.0 * m, scaled);
}
#[test]
fn rotation_matrices_satisfy_the_rotation_predicate() {
let m = Mat3::from_rotation_z(FRAC_PI_2);
assert!(m.is_rotation(1e-12));
assert!((m.determinant() - 1.0).abs() < EPS);
assert_mat_close(m * m.transpose(), Mat3::IDENTITY);
}
#[test]
fn is_rotation_rejects_non_rotations() {
assert!(!Mat3::ZERO.is_rotation(1e-9));
assert!(!Mat3::from_diagonal(Vec3::splat(2.0)).is_rotation(1e-9));
// A reflection has determinant minus one.
assert!(!Mat3::from_diagonal(Vec3::new(1.0, 1.0, -1.0)).is_rotation(1e-9));
}
#[test]
fn axis_angle_matches_the_quaternion_rotation() {
let axis = Vec3::new(0.5, -2.0, 1.0);
let m = Mat3::from_axis_angle(axis, 0.9).expect("non-zero axis");
let q = Quat::from_axis_angle(axis, 0.9).expect("non-zero axis");
let v = Vec3::new(1.0, -2.0, 3.0);
assert!((m * v - q.rotate(v)).norm() < EPS);
assert_eq!(Mat3::from_axis_angle(Vec3::ZERO, 1.0), None);
}
#[test]
fn rotation_maps_axes_as_expected() {
let m = Mat3::from_rotation_z(FRAC_PI_2);
assert!((m * Vec3::X - Vec3::Y).norm() < EPS);
assert!((m * Vec3::Y + Vec3::X).norm() < EPS);
assert!((m * Vec3::Z - Vec3::Z).norm() < EPS);
}
#[test]
fn per_axis_constructors_agree_with_the_axis_angle_form() {
let angle = 0.6;
let about_x = Mat3::from_rotation_x(angle);
assert_eq!(
about_x,
Mat3::from_axis_angle(Vec3::X, angle).expect("unit axis")
);
assert_eq!(about_x * Vec3::X, Vec3::X);
let about_y = Mat3::from_rotation_y(angle);
assert_eq!(
about_y,
Mat3::from_axis_angle(Vec3::Y, angle).expect("unit axis")
);
assert_eq!(about_y * Vec3::Y, Vec3::Y);
let about_z = Mat3::from_rotation_z(angle);
assert_eq!(
about_z,
Mat3::from_axis_angle(Vec3::Z, angle).expect("unit axis")
);
assert_eq!(about_z * Vec3::Z, Vec3::Z);
assert!((about_z.determinant() - 1.0).abs() < EPS);
}
#[test]
fn finiteness_is_reported() {
assert!(Mat3::IDENTITY.is_finite());
let mut m = Mat3::IDENTITY;
m.m01 = f64::NAN;
assert!(!m.is_finite());
}
#[test]
fn display_uses_row_major_groups() {
let m = Mat3::from_diagonal(Vec3::new(1.0, 2.0, 3.0));
assert_eq!(m.to_string(), "[1, 0, 0; 0, 2, 0; 0, 0, 3]");
}
#[test]
fn from_quat_is_consistent_with_quaternion_rotation() {
let q = Quat::from_axis_angle(Vec3::new(1.0, 1.0, 0.0), 1.3).expect("non-zero axis");
let m = Mat3::from_quat(q);
let v = Vec3::new(2.0, -1.0, 0.5);
assert!((m * v - q.rotate(v)).norm() < EPS);
assert!(m.is_rotation(1e-12));
}