#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use multicalc::discretization::{q_discrete_white_noise, van_loan, zoh};
use multicalc::error::LinalgError;
use multicalc::linear_algebra::{Matrix, Matrix2D, Matrix3D, Matrix4D};
use multicalc::scalar::Dual;
use proptest::prelude::*;
fn matrix3(entries: [[f64; 3]; 3]) -> Matrix3D {
Matrix::new(entries)
}
#[test]
fn expm_zero_is_identity() {
let exponential = Matrix4D::<f64>::zeros().expm().unwrap();
for row in 0..4 {
for column in 0..4 {
let expected = if row == column { 1.0 } else { 0.0 };
assert!((exponential[(row, column)] - expected).abs() < 1e-12);
}
}
}
#[test]
fn expm_diagonal_is_elementwise_exp() {
let diagonal = matrix3([[0.5, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 2.0]]);
let exponential = diagonal.expm().unwrap();
for (index, entry) in [0.5_f64, -1.0, 2.0].into_iter().enumerate() {
assert!((exponential[(index, index)] - entry.exp()).abs() < 1e-10);
}
}
#[test]
fn expm_rejects_non_finite_input() {
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let matrix = Matrix::<1, 1>::new([[value]]);
assert_eq!(matrix.expm().err(), Some(LinalgError::NonFinite));
}
}
#[test]
fn expm_derivative_finite_and_correct() {
let generator = matrix3([[0.1, 0.4, -0.2], [0.0, -0.3, 0.5], [0.2, 0.1, 0.05]]);
let autodiff =
Matrix3D::<Dual<f64>>::from_fn(|row, column| Dual::new(0.0, generator[(row, column)]))
.expm()
.unwrap();
for row in 0..3 {
for column in 0..3 {
let cell = autodiff[(row, column)];
assert!(cell.deriv.is_finite());
assert!((cell.deriv - generator[(row, column)]).abs() < 1e-9);
}
}
}
#[test]
fn zoh_double_integrator() {
let state_matrix = Matrix2D::new([[0.0, 1.0], [0.0, 0.0]]);
let input_matrix = Matrix::<2, 1>::new([[0.0], [1.0]]);
let timestep = 0.1;
let (discrete_state, discrete_input) =
zoh::<2, 1, 3, f64>(state_matrix, input_matrix, timestep).unwrap();
let expected_state = [[1.0, timestep], [0.0, 1.0]];
for (row, entries) in expected_state.iter().enumerate() {
for (column, &expected) in entries.iter().enumerate() {
assert!((discrete_state[(row, column)] - expected).abs() < 1e-9);
}
}
assert!((discrete_input[(0, 0)] - timestep * timestep / 2.0).abs() < 1e-9);
assert!((discrete_input[(1, 0)] - timestep).abs() < 1e-9);
}
#[test]
fn van_loan_qd_symmetric_and_f_matches_expm() {
let state_matrix = matrix3([[-0.5, 0.2, 0.0], [0.1, -0.3, 0.4], [0.0, 0.2, -0.6]]);
let continuous_noise = matrix3([[0.2, 0.0, 0.0], [0.0, 0.3, 0.0], [0.0, 0.0, 0.1]]);
let timestep = 0.05;
let (discrete_state, discrete_noise) =
van_loan::<3, 6, f64>(state_matrix, continuous_noise, timestep).unwrap();
let expected_from_expm = state_matrix.scale(timestep).expm().unwrap();
for row in 0..3 {
for column in 0..3 {
assert!(
(discrete_state[(row, column)] - expected_from_expm[(row, column)]).abs() < 1e-9
);
assert!((discrete_noise[(row, column)] - discrete_noise[(column, row)]).abs() < 1e-10);
}
}
}
#[test]
fn zoh_rejects_negative_timestep() {
let state_matrix = Matrix::<1, 1>::new([[1.0]]);
let input_matrix = Matrix::<1, 1>::new([[1.0]]);
assert_eq!(
zoh::<1, 1, 2, f64>(state_matrix, input_matrix, -0.1).err(),
Some(LinalgError::InvalidTimestep)
);
}
#[test]
fn van_loan_rejects_negative_timestep() {
let state_matrix = Matrix::<1, 1>::new([[1.0]]);
let continuous_noise = Matrix::<1, 1>::new([[1.0]]);
assert_eq!(
van_loan::<1, 2, f64>(state_matrix, continuous_noise, -0.1).err(),
Some(LinalgError::InvalidTimestep)
);
}
#[test]
fn discretization_rejects_non_finite_timesteps() {
let state_matrix = Matrix::<1, 1>::new([[1.0]]);
let input_or_noise_matrix = Matrix::<1, 1>::new([[1.0]]);
for timestep in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert_eq!(
zoh::<1, 1, 2, f64>(state_matrix, input_or_noise_matrix, timestep).err(),
Some(LinalgError::InvalidTimestep)
);
assert_eq!(
van_loan::<1, 2, f64>(state_matrix, input_or_noise_matrix, timestep).err(),
Some(LinalgError::InvalidTimestep)
);
}
}
#[test]
fn zero_timestep_is_valid() {
let state_matrix = Matrix::<1, 1>::new([[1.0]]);
let input_or_noise_matrix = Matrix::<1, 1>::new([[1.0]]);
let (zoh_state, zoh_input) =
zoh::<1, 1, 2, f64>(state_matrix, input_or_noise_matrix, 0.0).unwrap();
assert_eq!(zoh_state, Matrix::identity());
assert_eq!(zoh_input, Matrix::zeros());
let (van_loan_state, van_loan_noise) =
van_loan::<1, 2, f64>(state_matrix, input_or_noise_matrix, 0.0).unwrap();
assert_eq!(van_loan_state, Matrix::identity());
assert_eq!(van_loan_noise, Matrix::zeros());
}
#[test]
fn qdwn_matches_closed_form() {
let timestep = 0.1;
let variance = 2.0;
let noise = q_discrete_white_noise::<2, f64>(timestep, variance);
assert!((noise[(0, 0)] - variance * timestep.powi(4) / 4.0).abs() < 1e-15);
assert!((noise[(0, 1)] - variance * timestep.powi(3) / 2.0).abs() < 1e-15);
assert!((noise[(1, 0)] - variance * timestep.powi(3) / 2.0).abs() < 1e-15);
assert!((noise[(1, 1)] - variance * timestep * timestep).abs() < 1e-15);
}
proptest! {
#[test]
fn expm_times_neg_expm_is_identity(
v in prop::collection::vec(-0.6f64..0.6, 9)
) {
let matrix = Matrix3D::from_fn(|row, column| v[row * 3 + column]);
let product = matrix.expm().unwrap() * matrix.scale(-1.0).expm().unwrap();
for row in 0..3 {
for column in 0..3 {
let expected = if row == column { 1.0 } else { 0.0 };
prop_assert!((product[(row, column)] - expected).abs() < 1e-9);
}
}
}
}