#![allow(clippy::type_complexity)]
use nalgebra::{DMatrix, DVector, SMatrix, SVector};
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum DifferenceMethod {
Forward,
Central,
Backward,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PerturbationStrategy {
Adaptive {
scale_factor: f64,
min_value: f64,
},
Fixed(f64),
Percentage(f64),
}
pub trait SJacobianProvider<const S: usize, const P: usize>: Send + Sync {
fn compute(
&self,
t: f64,
state: &SVector<f64, S>,
params: Option<&SVector<f64, P>>,
) -> SMatrix<f64, S, S>;
}
pub trait DJacobianProvider: Send + Sync {
fn compute(&self, t: f64, state: &DVector<f64>, params: Option<&DVector<f64>>) -> DMatrix<f64>;
}
pub struct SAnalyticJacobian<const S: usize, const P: usize> {
jacobian_fn: Box<
dyn Fn(f64, &SVector<f64, S>, Option<&SVector<f64, P>>) -> SMatrix<f64, S, S> + Send + Sync,
>,
}
impl<const S: usize, const P: usize> SAnalyticJacobian<S, P> {
pub fn new(
jacobian_fn: Box<
dyn Fn(f64, &SVector<f64, S>, Option<&SVector<f64, P>>) -> SMatrix<f64, S, S>
+ Send
+ Sync,
>,
) -> Self {
Self { jacobian_fn }
}
}
impl<const S: usize, const P: usize> SJacobianProvider<S, P> for SAnalyticJacobian<S, P> {
fn compute(
&self,
t: f64,
state: &SVector<f64, S>,
params: Option<&SVector<f64, P>>,
) -> SMatrix<f64, S, S> {
(self.jacobian_fn)(t, state, params)
}
}
pub struct DAnalyticJacobian {
jacobian_fn:
Box<dyn Fn(f64, &DVector<f64>, Option<&DVector<f64>>) -> DMatrix<f64> + Send + Sync>,
}
impl DAnalyticJacobian {
pub fn new(
jacobian_fn: Box<
dyn Fn(f64, &DVector<f64>, Option<&DVector<f64>>) -> DMatrix<f64> + Send + Sync,
>,
) -> Self {
Self { jacobian_fn }
}
}
impl DJacobianProvider for DAnalyticJacobian {
fn compute(&self, t: f64, state: &DVector<f64>, params: Option<&DVector<f64>>) -> DMatrix<f64> {
(self.jacobian_fn)(t, state, params)
}
}
pub struct SNumericalJacobian<const S: usize, const P: usize> {
dynamics_fn: Box<
dyn Fn(f64, &SVector<f64, S>, Option<&SVector<f64, P>>) -> SVector<f64, S> + Send + Sync,
>,
method: DifferenceMethod,
perturbation: PerturbationStrategy,
}
impl<const S: usize, const P: usize> SNumericalJacobian<S, P> {
pub fn new(
dynamics_fn: Box<
dyn Fn(f64, &SVector<f64, S>, Option<&SVector<f64, P>>) -> SVector<f64, S>
+ Send
+ Sync,
>,
) -> Self {
Self {
dynamics_fn,
method: DifferenceMethod::Central,
perturbation: PerturbationStrategy::Adaptive {
scale_factor: 1.0,
min_value: 1.0,
},
}
}
pub fn forward(
dynamics_fn: Box<
dyn Fn(f64, &SVector<f64, S>, Option<&SVector<f64, P>>) -> SVector<f64, S>
+ Send
+ Sync,
>,
) -> Self {
Self {
dynamics_fn,
method: DifferenceMethod::Forward,
perturbation: PerturbationStrategy::Adaptive {
scale_factor: 1.0,
min_value: 1.0,
},
}
}
pub fn central(
dynamics_fn: Box<
dyn Fn(f64, &SVector<f64, S>, Option<&SVector<f64, P>>) -> SVector<f64, S>
+ Send
+ Sync,
>,
) -> Self {
Self::new(dynamics_fn)
}
pub fn backward(
dynamics_fn: Box<
dyn Fn(f64, &SVector<f64, S>, Option<&SVector<f64, P>>) -> SVector<f64, S>
+ Send
+ Sync,
>,
) -> Self {
Self {
dynamics_fn,
method: DifferenceMethod::Backward,
perturbation: PerturbationStrategy::Adaptive {
scale_factor: 1.0,
min_value: 1.0,
},
}
}
pub fn with_fixed_offset(mut self, offset: f64) -> Self {
self.perturbation = PerturbationStrategy::Fixed(offset);
self
}
pub fn with_percentage(mut self, percentage: f64) -> Self {
self.perturbation = PerturbationStrategy::Percentage(percentage);
self
}
pub fn with_adaptive(mut self, scale_factor: f64, min_value: f64) -> Self {
self.perturbation = PerturbationStrategy::Adaptive {
scale_factor,
min_value,
};
self
}
pub fn with_method(mut self, method: DifferenceMethod) -> Self {
self.method = method;
self
}
fn compute_offsets(&self, state: &SVector<f64, S>) -> SVector<f64, S> {
match self.perturbation {
PerturbationStrategy::Adaptive {
scale_factor,
min_value,
} => {
#[allow(non_snake_case)]
let SQRT_EPS: f64 = f64::EPSILON.sqrt();
let base_offset = scale_factor * SQRT_EPS;
state.map(|x| base_offset * x.abs().max(min_value))
}
PerturbationStrategy::Fixed(offset) => SVector::from_element(offset),
PerturbationStrategy::Percentage(pct) => state.map(|x| x.abs() * pct),
}
}
fn compute_forward(
&self,
t: f64,
state: &SVector<f64, S>,
params: Option<&SVector<f64, P>>,
) -> SMatrix<f64, S, S> {
let offsets = self.compute_offsets(state);
let f0 = (self.dynamics_fn)(t, state, params);
let mut jacobian = SMatrix::<f64, S, S>::zeros();
for i in 0..S {
let mut perturbed = *state;
perturbed[i] += offsets[i];
let fp = (self.dynamics_fn)(t, &perturbed, params);
jacobian.set_column(i, &((fp - f0) / offsets[i]));
}
jacobian
}
fn compute_central(
&self,
t: f64,
state: &SVector<f64, S>,
params: Option<&SVector<f64, P>>,
) -> SMatrix<f64, S, S> {
let offsets = self.compute_offsets(state);
let mut jacobian = SMatrix::<f64, S, S>::zeros();
for i in 0..S {
let mut perturbed_plus = *state;
let mut perturbed_minus = *state;
perturbed_plus[i] += offsets[i];
perturbed_minus[i] -= offsets[i];
let fp = (self.dynamics_fn)(t, &perturbed_plus, params);
let fm = (self.dynamics_fn)(t, &perturbed_minus, params);
jacobian.set_column(i, &((fp - fm) / (2.0 * offsets[i])));
}
jacobian
}
fn compute_backward(
&self,
t: f64,
state: &SVector<f64, S>,
params: Option<&SVector<f64, P>>,
) -> SMatrix<f64, S, S> {
let offsets = self.compute_offsets(state);
let f0 = (self.dynamics_fn)(t, state, params);
let mut jacobian = SMatrix::<f64, S, S>::zeros();
for i in 0..S {
let mut perturbed = *state;
perturbed[i] -= offsets[i];
let fm = (self.dynamics_fn)(t, &perturbed, params);
jacobian.set_column(i, &((f0 - fm) / offsets[i]));
}
jacobian
}
}
impl<const S: usize, const P: usize> SJacobianProvider<S, P> for SNumericalJacobian<S, P> {
fn compute(
&self,
t: f64,
state: &SVector<f64, S>,
params: Option<&SVector<f64, P>>,
) -> SMatrix<f64, S, S> {
match self.method {
DifferenceMethod::Forward => self.compute_forward(t, state, params),
DifferenceMethod::Central => self.compute_central(t, state, params),
DifferenceMethod::Backward => self.compute_backward(t, state, params),
}
}
}
pub struct DNumericalJacobian {
dynamics_fn:
Box<dyn Fn(f64, &DVector<f64>, Option<&DVector<f64>>) -> DVector<f64> + Send + Sync>,
method: DifferenceMethod,
perturbation: PerturbationStrategy,
}
impl DNumericalJacobian {
pub fn new(
dynamics_fn: Box<
dyn Fn(f64, &DVector<f64>, Option<&DVector<f64>>) -> DVector<f64> + Send + Sync,
>,
) -> Self {
Self {
dynamics_fn,
method: DifferenceMethod::Central,
perturbation: PerturbationStrategy::Adaptive {
scale_factor: 1.0,
min_value: 1.0,
},
}
}
pub fn forward(
dynamics_fn: Box<
dyn Fn(f64, &DVector<f64>, Option<&DVector<f64>>) -> DVector<f64> + Send + Sync,
>,
) -> Self {
Self {
dynamics_fn,
method: DifferenceMethod::Forward,
perturbation: PerturbationStrategy::Adaptive {
scale_factor: 1.0,
min_value: 1.0,
},
}
}
pub fn central(
dynamics_fn: Box<
dyn Fn(f64, &DVector<f64>, Option<&DVector<f64>>) -> DVector<f64> + Send + Sync,
>,
) -> Self {
Self::new(dynamics_fn)
}
pub fn backward(
dynamics_fn: Box<
dyn Fn(f64, &DVector<f64>, Option<&DVector<f64>>) -> DVector<f64> + Send + Sync,
>,
) -> Self {
Self {
dynamics_fn,
method: DifferenceMethod::Backward,
perturbation: PerturbationStrategy::Adaptive {
scale_factor: 1.0,
min_value: 1.0,
},
}
}
pub fn with_fixed_offset(mut self, offset: f64) -> Self {
self.perturbation = PerturbationStrategy::Fixed(offset);
self
}
pub fn with_percentage(mut self, percentage: f64) -> Self {
self.perturbation = PerturbationStrategy::Percentage(percentage);
self
}
pub fn with_adaptive(mut self, scale_factor: f64, min_value: f64) -> Self {
self.perturbation = PerturbationStrategy::Adaptive {
scale_factor,
min_value,
};
self
}
pub fn with_method(mut self, method: DifferenceMethod) -> Self {
self.method = method;
self
}
fn compute_offsets(&self, state: &DVector<f64>) -> DVector<f64> {
match self.perturbation {
PerturbationStrategy::Adaptive {
scale_factor,
min_value,
} => {
#[allow(non_snake_case)]
let SQRT_EPS: f64 = f64::EPSILON.sqrt();
let base_offset = scale_factor * SQRT_EPS;
state.map(|x| base_offset * x.abs().max(min_value))
}
PerturbationStrategy::Fixed(offset) => DVector::from_element(state.len(), offset),
PerturbationStrategy::Percentage(pct) => state.map(|x| x.abs() * pct),
}
}
fn compute_forward(
&self,
t: f64,
state: &DVector<f64>,
params: Option<&DVector<f64>>,
) -> DMatrix<f64> {
let offsets = self.compute_offsets(state);
let dimension = state.len();
let f0 = (self.dynamics_fn)(t, state, params);
let mut jacobian = DMatrix::<f64>::zeros(dimension, dimension);
for i in 0..dimension {
let mut perturbed = state.clone();
perturbed[i] += offsets[i];
let fp = (self.dynamics_fn)(t, &perturbed, params);
jacobian.set_column(i, &((fp - &f0) / offsets[i]));
}
jacobian
}
fn compute_central(
&self,
t: f64,
state: &DVector<f64>,
params: Option<&DVector<f64>>,
) -> DMatrix<f64> {
let offsets = self.compute_offsets(state);
let dimension = state.len();
let mut jacobian = DMatrix::<f64>::zeros(dimension, dimension);
for i in 0..dimension {
let mut perturbed_plus = state.clone();
let mut perturbed_minus = state.clone();
perturbed_plus[i] += offsets[i];
perturbed_minus[i] -= offsets[i];
let fp = (self.dynamics_fn)(t, &perturbed_plus, params);
let fm = (self.dynamics_fn)(t, &perturbed_minus, params);
jacobian.set_column(i, &((fp - fm) / (2.0 * offsets[i])));
}
jacobian
}
fn compute_backward(
&self,
t: f64,
state: &DVector<f64>,
params: Option<&DVector<f64>>,
) -> DMatrix<f64> {
let offsets = self.compute_offsets(state);
let dimension = state.len();
let f0 = (self.dynamics_fn)(t, state, params);
let mut jacobian = DMatrix::<f64>::zeros(dimension, dimension);
for i in 0..dimension {
let mut perturbed = state.clone();
perturbed[i] -= offsets[i];
let fm = (self.dynamics_fn)(t, &perturbed, params);
jacobian.set_column(i, &((f0.clone() - fm) / offsets[i]));
}
jacobian
}
}
impl DJacobianProvider for DNumericalJacobian {
fn compute(&self, t: f64, state: &DVector<f64>, params: Option<&DVector<f64>>) -> DMatrix<f64> {
match self.method {
DifferenceMethod::Forward => self.compute_forward(t, state, params),
DifferenceMethod::Central => self.compute_central(t, state, params),
DifferenceMethod::Backward => self.compute_backward(t, state, params),
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
fn linear_dynamics_static(
_t: f64,
state: &SVector<f64, 2>,
_params: Option<&SVector<f64, 0>>,
) -> SVector<f64, 2> {
SVector::<f64, 2>::new(state[1], -state[0])
}
fn linear_dynamics_dynamic(
_t: f64,
state: &DVector<f64>,
_params: Option<&DVector<f64>>,
) -> DVector<f64> {
DVector::from_vec(vec![state[1], -state[0]])
}
fn analytical_jacobian_static(
_t: f64,
_state: &SVector<f64, 2>,
_params: Option<&SVector<f64, 0>>,
) -> SMatrix<f64, 2, 2> {
SMatrix::<f64, 2, 2>::new(0.0, 1.0, -1.0, 0.0)
}
fn analytical_jacobian_dynamic(
_t: f64,
_state: &DVector<f64>,
_params: Option<&DVector<f64>>,
) -> DMatrix<f64> {
DMatrix::<f64>::from_row_slice(2, 2, &[0.0, 1.0, -1.0, 0.0])
}
#[test]
fn test_sanalytic_jacobian() {
let provider = SAnalyticJacobian::new(Box::new(analytical_jacobian_static));
let state = SVector::<f64, 2>::new(1.0, 0.5);
let jacobian = provider.compute(0.0, &state, None);
let expected = SMatrix::<f64, 2, 2>::new(0.0, 1.0, -1.0, 0.0);
assert_abs_diff_eq!(jacobian, expected, epsilon = 1e-10);
}
#[test]
fn test_danalytic_jacobian() {
let provider = DAnalyticJacobian::new(Box::new(analytical_jacobian_dynamic));
let state = DVector::from_vec(vec![1.0, 0.5]);
let jacobian = provider.compute(0.0, &state, None);
let expected = DMatrix::<f64>::from_row_slice(2, 2, &[0.0, 1.0, -1.0, 0.0]);
assert_abs_diff_eq!(jacobian, expected, epsilon = 1e-10);
}
#[test]
fn test_snumerical_jacobian_central() {
let provider =
SNumericalJacobian::central(Box::new(linear_dynamics_static)).with_fixed_offset(1e-6);
let state = SVector::<f64, 2>::new(1.0, 0.5);
let jacobian = provider.compute(0.0, &state, None);
let expected = SMatrix::<f64, 2, 2>::new(0.0, 1.0, -1.0, 0.0);
assert_abs_diff_eq!(jacobian, expected, epsilon = 1e-8);
}
#[test]
fn test_snumerical_jacobian_forward() {
let provider =
SNumericalJacobian::forward(Box::new(linear_dynamics_static)).with_fixed_offset(1e-6);
let state = SVector::<f64, 2>::new(1.0, 0.5);
let jacobian = provider.compute(0.0, &state, None);
let expected = SMatrix::<f64, 2, 2>::new(0.0, 1.0, -1.0, 0.0);
assert_abs_diff_eq!(jacobian, expected, epsilon = 1e-5);
}
#[test]
fn test_snumerical_jacobian_backward() {
let provider =
SNumericalJacobian::backward(Box::new(linear_dynamics_static)).with_fixed_offset(1e-6);
let state = SVector::<f64, 2>::new(1.0, 0.5);
let jacobian = provider.compute(0.0, &state, None);
let expected = SMatrix::<f64, 2, 2>::new(0.0, 1.0, -1.0, 0.0);
assert_abs_diff_eq!(jacobian, expected, epsilon = 1e-5);
}
#[test]
fn test_dnumerical_jacobian_central() {
let provider =
DNumericalJacobian::central(Box::new(linear_dynamics_dynamic)).with_fixed_offset(1e-6);
let state = DVector::from_vec(vec![1.0, 0.5]);
let jacobian = provider.compute(0.0, &state, None);
let expected = DMatrix::<f64>::from_row_slice(2, 2, &[0.0, 1.0, -1.0, 0.0]);
assert_abs_diff_eq!(jacobian, expected, epsilon = 1e-8);
}
#[test]
fn test_dnumerical_jacobian_forward() {
let provider =
DNumericalJacobian::forward(Box::new(linear_dynamics_dynamic)).with_fixed_offset(1e-6);
let state = DVector::from_vec(vec![1.0, 0.5]);
let jacobian = provider.compute(0.0, &state, None);
let expected = DMatrix::<f64>::from_row_slice(2, 2, &[0.0, 1.0, -1.0, 0.0]);
assert_abs_diff_eq!(jacobian, expected, epsilon = 1e-5);
}
#[test]
fn test_dnumerical_jacobian_backward() {
let provider =
DNumericalJacobian::backward(Box::new(linear_dynamics_dynamic)).with_fixed_offset(1e-6);
let state = DVector::from_vec(vec![1.0, 0.5]);
let jacobian = provider.compute(0.0, &state, None);
let expected = DMatrix::<f64>::from_row_slice(2, 2, &[0.0, 1.0, -1.0, 0.0]);
assert_abs_diff_eq!(jacobian, expected, epsilon = 1e-5);
}
#[test]
fn test_perturbation_strategies() {
let provider =
SNumericalJacobian::central(Box::new(linear_dynamics_static)).with_adaptive(1.0, 1.0);
let state = SVector::<f64, 2>::new(1.0, 0.5);
let jacobian = provider.compute(0.0, &state, None);
let expected = SMatrix::<f64, 2, 2>::new(0.0, 1.0, -1.0, 0.0);
assert_abs_diff_eq!(jacobian, expected, epsilon = 1e-6);
let provider =
SNumericalJacobian::central(Box::new(linear_dynamics_static)).with_percentage(1e-6);
let jacobian = provider.compute(0.0, &state, None);
assert_abs_diff_eq!(jacobian, expected, epsilon = 1e-5);
}
}