use crate::control::dense_ops::{
column_vector_norm, default_solve_tolerance, dense_mul, dense_mul_adjoint_rhs,
hermitian_project_in_place, inner_product_real, solve_left_checked, solve_right_checked,
};
use crate::control::lti::{ContinuousStateSpace, DiscreteStateSpace};
use crate::control::matrix_equations::{RiccatiError, solve_care_dense, solve_dare_dense};
use crate::sparse::compensated::CompensatedField;
use core::fmt;
use faer::{Mat, MatRef};
use faer_traits::RealField;
use num_traits::{Float, Zero};
#[derive(Clone, Debug)]
pub struct LqeSolve<T: CompensatedField>
where
T::Real: Float,
{
pub gain: Mat<T>,
pub covariance: Mat<T>,
pub estimator_a: Mat<T>,
pub residual_norm: T::Real,
pub stabilizing: bool,
}
#[derive(Clone, Debug)]
pub struct KalmanPrediction<T: CompensatedField>
where
T::Real: Float,
{
pub state: Mat<T>,
pub covariance: Mat<T>,
pub output: Mat<T>,
}
#[derive(Clone, Debug)]
pub struct KalmanUpdate<T: CompensatedField>
where
T::Real: Float,
{
pub innovation: Mat<T>,
pub innovation_norm: T::Real,
pub innovation_covariance: Mat<T>,
pub normalized_innovation_norm: T::Real,
pub gain: Mat<T>,
pub predicted_output: Mat<T>,
pub state: Mat<T>,
pub covariance: Mat<T>,
pub output: Mat<T>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum CovarianceUpdate {
Simple,
#[default]
Joseph,
}
#[derive(Clone, Debug)]
pub struct SteadyStateKalmanPrediction<T: CompensatedField>
where
T::Real: Float,
{
pub state: Mat<T>,
pub output: Mat<T>,
}
#[derive(Clone, Debug)]
pub struct SteadyStateKalmanUpdate<T: CompensatedField>
where
T::Real: Float,
{
pub innovation: Mat<T>,
pub innovation_norm: T::Real,
pub state: Mat<T>,
pub output: Mat<T>,
}
#[derive(Clone, Debug)]
pub struct ContinuousObserverDerivative<T: CompensatedField>
where
T::Real: Float,
{
pub output: Mat<T>,
pub innovation: Mat<T>,
pub innovation_norm: T::Real,
pub state_derivative: Mat<T>,
}
#[derive(Debug)]
pub enum EstimatorError {
Riccati(RiccatiError),
DimensionMismatch {
which: &'static str,
expected_nrows: usize,
expected_ncols: usize,
actual_nrows: usize,
actual_ncols: usize,
},
SingularInnovationCovariance,
NonFiniteResult {
which: &'static str,
},
}
impl fmt::Display for EstimatorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}
impl core::error::Error for EstimatorError {}
impl From<RiccatiError> for EstimatorError {
fn from(value: RiccatiError) -> Self {
Self::Riccati(value)
}
}
#[derive(Clone, Debug)]
pub struct DiscreteKalmanFilter<T: CompensatedField>
where
T::Real: Float,
{
pub a: Mat<T>,
pub b: Mat<T>,
pub c: Mat<T>,
pub d: Mat<T>,
pub w: Mat<T>,
pub v: Mat<T>,
pub covariance_update: CovarianceUpdate,
pub x_hat: Mat<T>,
pub p: Mat<T>,
}
#[derive(Clone, Debug)]
pub struct SteadyStateKalmanFilter<T: CompensatedField>
where
T::Real: Float,
{
pub a: Mat<T>,
pub b: Mat<T>,
pub c: Mat<T>,
pub d: Mat<T>,
pub gain: Mat<T>,
pub x_hat: Mat<T>,
pub steady_state_covariance: Option<Mat<T>>,
}
#[derive(Clone, Debug)]
pub struct ContinuousObserver<T: CompensatedField>
where
T::Real: Float,
{
pub a: Mat<T>,
pub b: Mat<T>,
pub c: Mat<T>,
pub d: Mat<T>,
pub gain: Mat<T>,
pub x_hat: Mat<T>,
}
pub fn lqe_dense<T>(
a: MatRef<'_, T>,
c: MatRef<'_, T>,
w: MatRef<'_, T>,
v: MatRef<'_, T>,
) -> Result<LqeSolve<T>, EstimatorError>
where
T: CompensatedField,
T::Real: Float + RealField,
{
validate_lqe_dims(a, c, w, v)?;
let dual = solve_care_dense(
a.adjoint().to_owned().as_ref(),
c.adjoint().to_owned().as_ref(),
w,
v,
)?;
let gain = dual.gain.adjoint().to_owned();
Ok(LqeSolve {
estimator_a: estimator_matrix(a, gain.as_ref(), c),
gain,
covariance: dual.solution,
residual_norm: dual.residual_norm,
stabilizing: dual.stabilizing,
})
}
pub fn dlqe_dense<T>(
a: MatRef<'_, T>,
c: MatRef<'_, T>,
w: MatRef<'_, T>,
v: MatRef<'_, T>,
) -> Result<LqeSolve<T>, EstimatorError>
where
T: CompensatedField,
T::Real: Float + RealField,
{
validate_lqe_dims(a, c, w, v)?;
let dual = solve_dare_dense(
a.adjoint().to_owned().as_ref(),
c.adjoint().to_owned().as_ref(),
w,
v,
)?;
let gain = dual.gain.adjoint().to_owned();
Ok(LqeSolve {
estimator_a: estimator_matrix(a, gain.as_ref(), c),
gain,
covariance: dual.solution,
residual_norm: dual.residual_norm,
stabilizing: dual.stabilizing,
})
}
impl<T> ContinuousStateSpace<T>
where
T: CompensatedField,
T::Real: Float + RealField,
{
pub fn lqe(&self, w: MatRef<'_, T>, v: MatRef<'_, T>) -> Result<LqeSolve<T>, EstimatorError> {
lqe_dense(self.a(), self.c(), w, v)
}
}
impl<T> DiscreteStateSpace<T>
where
T: CompensatedField,
T::Real: Float + RealField,
{
pub fn dlqe(&self, w: MatRef<'_, T>, v: MatRef<'_, T>) -> Result<LqeSolve<T>, EstimatorError> {
dlqe_dense(self.a(), self.c(), w, v)
}
pub fn steady_state_kalman(
&self,
w: MatRef<'_, T>,
v: MatRef<'_, T>,
x_hat: Mat<T>,
) -> Result<SteadyStateKalmanFilter<T>, EstimatorError> {
SteadyStateKalmanFilter::from_dlqe(self, w, v, x_hat)
}
}
impl<T> ContinuousStateSpace<T>
where
T: CompensatedField,
T::Real: Float + RealField,
{
pub fn observer(
&self,
w: MatRef<'_, T>,
v: MatRef<'_, T>,
x_hat: Mat<T>,
) -> Result<ContinuousObserver<T>, EstimatorError> {
ContinuousObserver::from_lqe(self, w, v, x_hat)
}
}
impl<T> DiscreteKalmanFilter<T>
where
T: CompensatedField,
T::Real: Float + RealField,
{
pub fn new(
a: Mat<T>,
b: Mat<T>,
c: Mat<T>,
d: Mat<T>,
w: Mat<T>,
v: Mat<T>,
x_hat: Mat<T>,
p: Mat<T>,
) -> Result<Self, EstimatorError> {
Self::new_with_covariance_update(a, b, c, d, w, v, x_hat, p, CovarianceUpdate::default())
}
pub fn new_with_covariance_update(
a: Mat<T>,
b: Mat<T>,
c: Mat<T>,
d: Mat<T>,
w: Mat<T>,
v: Mat<T>,
x_hat: Mat<T>,
p: Mat<T>,
covariance_update: CovarianceUpdate,
) -> Result<Self, EstimatorError> {
validate_filter_model(
a.as_ref(),
b.as_ref(),
c.as_ref(),
d.as_ref(),
w.as_ref(),
v.as_ref(),
x_hat.as_ref(),
p.as_ref(),
)?;
Ok(Self {
a,
b,
c,
d,
w,
v,
covariance_update,
x_hat,
p,
})
}
pub fn from_state_space(
system: &DiscreteStateSpace<T>,
w: Mat<T>,
v: Mat<T>,
x_hat: Mat<T>,
p: Mat<T>,
) -> Result<Self, EstimatorError> {
Self::new_with_covariance_update(
system.a().to_owned(),
system.b().to_owned(),
system.c().to_owned(),
system.d().to_owned(),
w,
v,
x_hat,
p,
CovarianceUpdate::default(),
)
}
pub fn from_state_space_with_covariance_update(
system: &DiscreteStateSpace<T>,
w: Mat<T>,
v: Mat<T>,
x_hat: Mat<T>,
p: Mat<T>,
covariance_update: CovarianceUpdate,
) -> Result<Self, EstimatorError> {
Self::new_with_covariance_update(
system.a().to_owned(),
system.b().to_owned(),
system.c().to_owned(),
system.d().to_owned(),
w,
v,
x_hat,
p,
covariance_update,
)
}
pub fn from_dlqe(
system: &DiscreteStateSpace<T>,
w: MatRef<'_, T>,
v: MatRef<'_, T>,
x_hat: Mat<T>,
) -> Result<Self, EstimatorError> {
let solve = system.dlqe(w, v)?;
let (gain, innovation_covariance) =
steady_state_filter_gain_dense(system.c(), v, solve.covariance.as_ref())?;
let mut posterior_covariance = updated_covariance(
CovarianceUpdate::default(),
solve.covariance.as_ref(),
gain.as_ref(),
system.c(),
v,
innovation_covariance.as_ref(),
);
hermitian_project_in_place(&mut posterior_covariance);
Self::new_with_covariance_update(
system.a().to_owned(),
system.b().to_owned(),
system.c().to_owned(),
system.d().to_owned(),
w.to_owned(),
v.to_owned(),
x_hat,
posterior_covariance,
CovarianceUpdate::default(),
)
}
#[must_use]
pub fn state_estimate(&self) -> MatRef<'_, T> {
self.x_hat.as_ref()
}
#[must_use]
pub fn covariance(&self) -> MatRef<'_, T> {
self.p.as_ref()
}
#[must_use]
pub fn covariance_update(&self) -> CovarianceUpdate {
self.covariance_update
}
pub fn predict(&self, input: MatRef<'_, T>) -> Result<KalmanPrediction<T>, EstimatorError> {
validate_column_vector("input", input, self.b.ncols())?;
let bu = dense_mul(self.b.as_ref(), input);
let state = dense_mul(self.a.as_ref(), self.x_hat.as_ref()) + &bu;
let covariance = dense_mul_adjoint_rhs(
dense_mul(self.a.as_ref(), self.p.as_ref()).as_ref(),
self.a.as_ref(),
) + &self.w;
let mut covariance = covariance;
hermitian_project_in_place(&mut covariance);
let du = dense_mul(self.d.as_ref(), input);
let output = dense_mul(self.c.as_ref(), state.as_ref()) + &du;
if !state.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "prediction.state",
});
}
if !covariance.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "prediction.covariance",
});
}
if !output.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "prediction.output",
});
}
Ok(KalmanPrediction {
state,
covariance,
output,
})
}
pub fn update(
&self,
prediction: &KalmanPrediction<T>,
input: MatRef<'_, T>,
measurement: MatRef<'_, T>,
) -> Result<KalmanUpdate<T>, EstimatorError> {
validate_column_vector("input", input, self.b.ncols())?;
validate_column_vector("measurement", measurement, self.c.nrows())?;
validate_column_vector(
"prediction.state",
prediction.state.as_ref(),
self.a.nrows(),
)?;
validate_square(
"prediction.covariance",
prediction.covariance.as_ref(),
self.a.nrows(),
)?;
validate_column_vector(
"prediction.output",
prediction.output.as_ref(),
self.c.nrows(),
)?;
let predicted_output = dense_mul(self.c.as_ref(), prediction.state.as_ref())
+ dense_mul(self.d.as_ref(), input).as_ref();
let innovation = measurement - &predicted_output;
let innovation_covariance = dense_mul_adjoint_rhs(
dense_mul(self.c.as_ref(), prediction.covariance.as_ref()).as_ref(),
self.c.as_ref(),
) + &self.v;
let mut innovation_covariance = innovation_covariance;
hermitian_project_in_place(&mut innovation_covariance);
let cross = dense_mul_adjoint_rhs(prediction.covariance.as_ref(), self.c.as_ref());
let gain = solve_right_checked(
cross.as_ref(),
innovation_covariance.as_ref(),
default_solve_tolerance::<T>(),
|| EstimatorError::SingularInnovationCovariance,
)?;
let correction = dense_mul(gain.as_ref(), innovation.as_ref());
let state = &prediction.state + &correction;
let covariance = updated_covariance(
self.covariance_update,
prediction.covariance.as_ref(),
gain.as_ref(),
self.c.as_ref(),
self.v.as_ref(),
innovation_covariance.as_ref(),
);
let mut covariance = covariance;
hermitian_project_in_place(&mut covariance);
let du = dense_mul(self.d.as_ref(), input);
let output = dense_mul(self.c.as_ref(), state.as_ref()) + &du;
let innovation_norm = column_vector_norm(innovation.as_ref());
let normalized_innovation_norm =
normalized_innovation_norm(innovation.as_ref(), innovation_covariance.as_ref())?;
if !predicted_output.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "update.predicted_output",
});
}
if !innovation.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "update.innovation",
});
}
if !gain.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "update.gain",
});
}
if !state.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "update.state",
});
}
if !covariance.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "update.covariance",
});
}
if !output.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "update.output",
});
}
Ok(KalmanUpdate {
innovation,
innovation_norm,
innovation_covariance,
normalized_innovation_norm,
gain,
predicted_output,
state,
covariance,
output,
})
}
pub fn step(
&mut self,
input: MatRef<'_, T>,
measurement: MatRef<'_, T>,
) -> Result<KalmanUpdate<T>, EstimatorError> {
let prediction = self.predict(input)?;
let update = self.update(&prediction, input, measurement)?;
self.x_hat = update.state.to_owned();
self.p = update.covariance.to_owned();
Ok(update)
}
}
impl<T> SteadyStateKalmanFilter<T>
where
T: CompensatedField,
T::Real: Float + RealField,
{
pub fn new(
a: Mat<T>,
b: Mat<T>,
c: Mat<T>,
d: Mat<T>,
gain: Mat<T>,
x_hat: Mat<T>,
steady_state_covariance: Option<Mat<T>>,
) -> Result<Self, EstimatorError> {
validate_fixed_gain_observer_model(
a.as_ref(),
b.as_ref(),
c.as_ref(),
d.as_ref(),
gain.as_ref(),
x_hat.as_ref(),
)?;
if let Some(covariance) = &steady_state_covariance {
validate_square("steady_state_covariance", covariance.as_ref(), a.nrows())?;
}
Ok(Self {
a,
b,
c,
d,
gain,
x_hat,
steady_state_covariance,
})
}
pub fn from_filter_gain(
system: &DiscreteStateSpace<T>,
gain: Mat<T>,
x_hat: Mat<T>,
steady_state_covariance: Option<Mat<T>>,
) -> Result<Self, EstimatorError> {
Self::new(
system.a().to_owned(),
system.b().to_owned(),
system.c().to_owned(),
system.d().to_owned(),
gain,
x_hat,
steady_state_covariance,
)
}
pub fn from_dlqe(
system: &DiscreteStateSpace<T>,
w: MatRef<'_, T>,
v: MatRef<'_, T>,
x_hat: Mat<T>,
) -> Result<Self, EstimatorError> {
let solve = system.dlqe(w, v)?;
let (gain, _) = steady_state_filter_gain_dense(system.c(), v, solve.covariance.as_ref())?;
Self::from_filter_gain(system, gain, x_hat, Some(solve.covariance))
}
#[must_use]
pub fn state_estimate(&self) -> MatRef<'_, T> {
self.x_hat.as_ref()
}
#[must_use]
pub fn gain(&self) -> MatRef<'_, T> {
self.gain.as_ref()
}
#[must_use]
pub fn steady_state_covariance(&self) -> Option<MatRef<'_, T>> {
self.steady_state_covariance.as_ref().map(|p| p.as_ref())
}
pub fn predict(
&self,
input: MatRef<'_, T>,
) -> Result<SteadyStateKalmanPrediction<T>, EstimatorError> {
validate_column_vector("input", input, self.b.ncols())?;
let bu = dense_mul(self.b.as_ref(), input);
let state = dense_mul(self.a.as_ref(), self.x_hat.as_ref()) + &bu;
let du = dense_mul(self.d.as_ref(), input);
let output = dense_mul(self.c.as_ref(), state.as_ref()) + &du;
if !state.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "steady_state_prediction.state",
});
}
if !output.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "steady_state_prediction.output",
});
}
Ok(SteadyStateKalmanPrediction { state, output })
}
pub fn update(
&self,
prediction: &SteadyStateKalmanPrediction<T>,
input: MatRef<'_, T>,
measurement: MatRef<'_, T>,
) -> Result<SteadyStateKalmanUpdate<T>, EstimatorError> {
validate_column_vector("input", input, self.b.ncols())?;
validate_column_vector("measurement", measurement, self.c.nrows())?;
validate_column_vector(
"prediction.state",
prediction.state.as_ref(),
self.a.nrows(),
)?;
validate_column_vector(
"prediction.output",
prediction.output.as_ref(),
self.c.nrows(),
)?;
let du = dense_mul(self.d.as_ref(), input);
let predicted_output = dense_mul(self.c.as_ref(), prediction.state.as_ref()) + &du;
let innovation = measurement - &predicted_output;
let correction = dense_mul(self.gain.as_ref(), innovation.as_ref());
let state = &prediction.state + &correction;
let output = dense_mul(self.c.as_ref(), state.as_ref()) + &du;
let innovation_norm = column_vector_norm(innovation.as_ref());
if !predicted_output.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "steady_state_update.predicted_output",
});
}
if !innovation.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "steady_state_update.innovation",
});
}
if !state.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "steady_state_update.state",
});
}
if !output.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "steady_state_update.output",
});
}
Ok(SteadyStateKalmanUpdate {
innovation,
innovation_norm,
state,
output,
})
}
pub fn step(
&mut self,
input: MatRef<'_, T>,
measurement: MatRef<'_, T>,
) -> Result<SteadyStateKalmanUpdate<T>, EstimatorError> {
let prediction = self.predict(input)?;
let update = self.update(&prediction, input, measurement)?;
self.x_hat = update.state.to_owned();
Ok(update)
}
}
impl<T> ContinuousObserver<T>
where
T: CompensatedField,
T::Real: Float + RealField,
{
pub fn new(
a: Mat<T>,
b: Mat<T>,
c: Mat<T>,
d: Mat<T>,
gain: Mat<T>,
x_hat: Mat<T>,
) -> Result<Self, EstimatorError> {
validate_fixed_gain_observer_model(
a.as_ref(),
b.as_ref(),
c.as_ref(),
d.as_ref(),
gain.as_ref(),
x_hat.as_ref(),
)?;
Ok(Self {
a,
b,
c,
d,
gain,
x_hat,
})
}
pub fn from_gain(
system: &ContinuousStateSpace<T>,
gain: Mat<T>,
x_hat: Mat<T>,
) -> Result<Self, EstimatorError> {
Self::new(
system.a().to_owned(),
system.b().to_owned(),
system.c().to_owned(),
system.d().to_owned(),
gain,
x_hat,
)
}
pub fn from_lqe(
system: &ContinuousStateSpace<T>,
w: MatRef<'_, T>,
v: MatRef<'_, T>,
x_hat: Mat<T>,
) -> Result<Self, EstimatorError> {
let solve = system.lqe(w, v)?;
Self::from_gain(system, solve.gain, x_hat)
}
#[must_use]
pub fn state_estimate(&self) -> MatRef<'_, T> {
self.x_hat.as_ref()
}
#[must_use]
pub fn gain(&self) -> MatRef<'_, T> {
self.gain.as_ref()
}
pub fn derivative(
&self,
input: MatRef<'_, T>,
measurement: MatRef<'_, T>,
) -> Result<ContinuousObserverDerivative<T>, EstimatorError> {
validate_column_vector("input", input, self.b.ncols())?;
validate_column_vector("measurement", measurement, self.c.nrows())?;
let du = dense_mul(self.d.as_ref(), input);
let output = dense_mul(self.c.as_ref(), self.x_hat.as_ref()) + &du;
let innovation = measurement - &output;
let bu = dense_mul(self.b.as_ref(), input);
let correction = dense_mul(self.gain.as_ref(), innovation.as_ref());
let state_derivative =
(dense_mul(self.a.as_ref(), self.x_hat.as_ref()) + &bu) + &correction;
let innovation_norm = column_vector_norm(innovation.as_ref());
if !output.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "continuous_observer.output",
});
}
if !innovation.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "continuous_observer.innovation",
});
}
if !state_derivative.as_ref().is_all_finite() {
return Err(EstimatorError::NonFiniteResult {
which: "continuous_observer.state_derivative",
});
}
Ok(ContinuousObserverDerivative {
output,
innovation,
innovation_norm,
state_derivative,
})
}
}
fn validate_lqe_dims<T>(
a: MatRef<'_, T>,
c: MatRef<'_, T>,
w: MatRef<'_, T>,
v: MatRef<'_, T>,
) -> Result<(), EstimatorError> {
validate_square("a", a, a.nrows())?;
validate_square("w", w, a.nrows())?;
validate_square("v", v, c.nrows())?;
if c.ncols() != a.ncols() {
return Err(EstimatorError::DimensionMismatch {
which: "c",
expected_nrows: c.nrows(),
expected_ncols: a.ncols(),
actual_nrows: c.nrows(),
actual_ncols: c.ncols(),
});
}
Ok(())
}
fn validate_filter_model<T>(
a: MatRef<'_, T>,
b: MatRef<'_, T>,
c: MatRef<'_, T>,
d: MatRef<'_, T>,
w: MatRef<'_, T>,
v: MatRef<'_, T>,
x_hat: MatRef<'_, T>,
p: MatRef<'_, T>,
) -> Result<(), EstimatorError> {
let n = a.nrows();
validate_square("a", a, n)?;
validate_square("w", w, n)?;
validate_square("p", p, n)?;
if b.nrows() != n {
return Err(EstimatorError::DimensionMismatch {
which: "b",
expected_nrows: n,
expected_ncols: b.ncols(),
actual_nrows: b.nrows(),
actual_ncols: b.ncols(),
});
}
if c.ncols() != n {
return Err(EstimatorError::DimensionMismatch {
which: "c",
expected_nrows: c.nrows(),
expected_ncols: n,
actual_nrows: c.nrows(),
actual_ncols: c.ncols(),
});
}
if d.nrows() != c.nrows() || d.ncols() != b.ncols() {
return Err(EstimatorError::DimensionMismatch {
which: "d",
expected_nrows: c.nrows(),
expected_ncols: b.ncols(),
actual_nrows: d.nrows(),
actual_ncols: d.ncols(),
});
}
validate_square("v", v, c.nrows())?;
validate_column_vector("x_hat", x_hat, n)?;
Ok(())
}
fn validate_fixed_gain_observer_model<T>(
a: MatRef<'_, T>,
b: MatRef<'_, T>,
c: MatRef<'_, T>,
d: MatRef<'_, T>,
gain: MatRef<'_, T>,
x_hat: MatRef<'_, T>,
) -> Result<(), EstimatorError> {
let n = a.nrows();
validate_square("a", a, n)?;
if b.nrows() != n {
return Err(EstimatorError::DimensionMismatch {
which: "b",
expected_nrows: n,
expected_ncols: b.ncols(),
actual_nrows: b.nrows(),
actual_ncols: b.ncols(),
});
}
if c.ncols() != n {
return Err(EstimatorError::DimensionMismatch {
which: "c",
expected_nrows: c.nrows(),
expected_ncols: n,
actual_nrows: c.nrows(),
actual_ncols: c.ncols(),
});
}
if d.nrows() != c.nrows() || d.ncols() != b.ncols() {
return Err(EstimatorError::DimensionMismatch {
which: "d",
expected_nrows: c.nrows(),
expected_ncols: b.ncols(),
actual_nrows: d.nrows(),
actual_ncols: d.ncols(),
});
}
validate_column_vector("x_hat", x_hat, n)?;
if gain.nrows() != a.nrows() || gain.ncols() != c.nrows() {
return Err(EstimatorError::DimensionMismatch {
which: "gain",
expected_nrows: a.nrows(),
expected_ncols: c.nrows(),
actual_nrows: gain.nrows(),
actual_ncols: gain.ncols(),
});
}
Ok(())
}
fn validate_square<T>(
which: &'static str,
matrix: MatRef<'_, T>,
expected_dim: usize,
) -> Result<(), EstimatorError> {
if matrix.nrows() != expected_dim || matrix.ncols() != expected_dim {
return Err(EstimatorError::DimensionMismatch {
which,
expected_nrows: expected_dim,
expected_ncols: expected_dim,
actual_nrows: matrix.nrows(),
actual_ncols: matrix.ncols(),
});
}
Ok(())
}
fn validate_column_vector<T>(
which: &'static str,
matrix: MatRef<'_, T>,
expected_nrows: usize,
) -> Result<(), EstimatorError> {
if matrix.nrows() != expected_nrows || matrix.ncols() != 1 {
return Err(EstimatorError::DimensionMismatch {
which,
expected_nrows,
expected_ncols: 1,
actual_nrows: matrix.nrows(),
actual_ncols: matrix.ncols(),
});
}
Ok(())
}
fn estimator_matrix<T>(a: MatRef<'_, T>, l: MatRef<'_, T>, c: MatRef<'_, T>) -> Mat<T>
where
T: CompensatedField,
T::Real: Float,
{
let lc = dense_mul(l, c);
a - &lc
}
pub fn steady_state_filter_gain_dense<T>(
c: MatRef<'_, T>,
v: MatRef<'_, T>,
prior_covariance: MatRef<'_, T>,
) -> Result<(Mat<T>, Mat<T>), EstimatorError>
where
T: CompensatedField,
T::Real: Float,
{
let mut innovation_covariance =
dense_mul_adjoint_rhs(dense_mul(c, prior_covariance).as_ref(), c) + v;
hermitian_project_in_place(&mut innovation_covariance);
let cross = dense_mul_adjoint_rhs(prior_covariance, c);
let gain = solve_right_checked(
cross.as_ref(),
innovation_covariance.as_ref(),
default_solve_tolerance::<T>(),
|| EstimatorError::SingularInnovationCovariance,
)?;
Ok((gain, innovation_covariance))
}
fn updated_covariance<T>(
covariance_update: CovarianceUpdate,
predicted_covariance: MatRef<'_, T>,
gain: MatRef<'_, T>,
c: MatRef<'_, T>,
v: MatRef<'_, T>,
innovation_covariance: MatRef<'_, T>,
) -> Mat<T>
where
T: CompensatedField,
T::Real: Float,
{
match covariance_update {
CovarianceUpdate::Simple => {
predicted_covariance
- dense_mul_adjoint_rhs(dense_mul(gain, innovation_covariance).as_ref(), gain)
.as_ref()
}
CovarianceUpdate::Joseph => {
let identity =
Mat::<T>::identity(predicted_covariance.nrows(), predicted_covariance.nrows());
let kc = dense_mul(gain, c);
let i_minus_kc = &identity - &kc;
let first = dense_mul_adjoint_rhs(
dense_mul(i_minus_kc.as_ref(), predicted_covariance).as_ref(),
i_minus_kc.as_ref(),
);
let second = dense_mul_adjoint_rhs(dense_mul(gain, v).as_ref(), gain);
&first + &second
}
}
}
fn normalized_innovation_norm<T>(
innovation: MatRef<'_, T>,
innovation_covariance: MatRef<'_, T>,
) -> Result<T::Real, EstimatorError>
where
T: CompensatedField,
T::Real: Float,
{
let whitened = solve_left_checked(
innovation_covariance,
innovation,
default_solve_tolerance::<T>(),
|| EstimatorError::SingularInnovationCovariance,
)?;
Ok(inner_product_real(innovation, whitened.as_ref())
.max(<T::Real as Zero>::zero())
.sqrt())
}
#[cfg(test)]
mod test {
use super::{
ContinuousObserver, CovarianceUpdate, DiscreteKalmanFilter, EstimatorError,
SteadyStateKalmanFilter, dlqe_dense, lqe_dense,
};
use crate::control::lti::state_space::{ContinuousStateSpace, DiscreteStateSpace};
use faer::Mat;
use faer_traits::ext::ComplexFieldExt;
fn assert_close<T>(lhs: &Mat<T>, rhs: &Mat<T>, tol: T::Real)
where
T: crate::sparse::compensated::CompensatedField,
T::Real: num_traits::Float,
{
assert_eq!(lhs.nrows(), rhs.nrows());
assert_eq!(lhs.ncols(), rhs.ncols());
for col in 0..lhs.ncols() {
for row in 0..lhs.nrows() {
let err = (lhs[(row, col)] - rhs[(row, col)]).abs();
assert!(
err <= tol,
"entry ({row}, {col}) mismatch: err={err:?}, tol={tol:?}",
);
}
}
}
#[test]
fn lqe_matches_scalar_dual_closed_form() {
let a = Mat::from_fn(1, 1, |_, _| 1.0f64);
let c = Mat::from_fn(1, 1, |_, _| 1.0f64);
let w = Mat::from_fn(1, 1, |_, _| 1.0f64);
let v = Mat::from_fn(1, 1, |_, _| 1.0f64);
let solve = lqe_dense(a.as_ref(), c.as_ref(), w.as_ref(), v.as_ref()).unwrap();
let expected = 1.0 + 2.0f64.sqrt();
assert!((solve.covariance[(0, 0)] - expected).abs() < 1.0e-10);
assert!((solve.gain[(0, 0)] - expected).abs() < 1.0e-10);
assert!((solve.estimator_a[(0, 0)] + 2.0f64.sqrt()).abs() < 1.0e-10);
assert!(solve.stabilizing);
}
#[test]
fn dlqe_matches_scalar_dual_closed_form() {
let a = Mat::from_fn(1, 1, |_, _| 1.2f64);
let c = Mat::from_fn(1, 1, |_, _| 1.0f64);
let w = Mat::from_fn(1, 1, |_, _| 1.0f64);
let v = Mat::from_fn(1, 1, |_, _| 1.0f64);
let solve = dlqe_dense(a.as_ref(), c.as_ref(), w.as_ref(), v.as_ref()).unwrap();
let p = (1.44 + (1.44f64 * 1.44 + 4.0).sqrt()) / 2.0;
let expected_l = 1.2 * p / (1.0 + p);
assert!((solve.covariance[(0, 0)] - p).abs() < 1.0e-10);
assert!((solve.gain[(0, 0)] - expected_l).abs() < 1.0e-10);
assert!((solve.estimator_a[(0, 0)] - (1.2 - expected_l)).abs() < 1.0e-10);
assert!(solve.stabilizing);
}
#[test]
fn lqe_and_dlqe_state_space_methods_match_free_functions() {
let a = Mat::from_fn(
2,
2,
|row, col| if row == col { 1.0 + row as f64 } else { 0.0 },
);
let b = Mat::zeros(2, 1);
let c = Mat::from_fn(2, 2, |row, col| if row == col { 1.0 } else { 0.0 });
let d = Mat::zeros(2, 1);
let w = Mat::from_fn(
2,
2,
|row, col| if row == col { 1.0 + row as f64 } else { 0.0 },
);
let v = Mat::from_fn(2, 2, |row, col| if row == col { 1.0 } else { 0.0 });
let continuous =
ContinuousStateSpace::new(a.clone(), b.clone(), c.clone(), d.clone()).unwrap();
let discrete = DiscreteStateSpace::new(a.clone(), b, c.clone(), d, 0.1).unwrap();
let free_lqe = lqe_dense(a.as_ref(), c.as_ref(), w.as_ref(), v.as_ref()).unwrap();
let method_lqe = continuous.lqe(w.as_ref(), v.as_ref()).unwrap();
assert_close(&free_lqe.gain, &method_lqe.gain, 1.0e-12);
let free_dlqe = dlqe_dense(a.as_ref(), c.as_ref(), w.as_ref(), v.as_ref()).unwrap();
let method_dlqe = discrete.dlqe(w.as_ref(), v.as_ref()).unwrap();
assert_close(&free_dlqe.gain, &method_dlqe.gain, 1.0e-12);
}
#[test]
fn discrete_kalman_predict_update_matches_scalar_reference() {
let a = Mat::from_fn(1, 1, |_, _| 1.0f64);
let b = Mat::from_fn(1, 1, |_, _| 1.0f64);
let c = Mat::from_fn(1, 1, |_, _| 1.0f64);
let d = Mat::from_fn(1, 1, |_, _| 0.0f64);
let w = Mat::from_fn(1, 1, |_, _| 0.25f64);
let v = Mat::from_fn(1, 1, |_, _| 1.0f64);
let x0 = Mat::from_fn(1, 1, |_, _| 0.0f64);
let p0 = Mat::from_fn(1, 1, |_, _| 1.0f64);
let filter = DiscreteKalmanFilter::new(a, b, c, d, w, v, x0, p0).unwrap();
let u = Mat::from_fn(1, 1, |_, _| 2.0f64);
let pred = filter.predict(u.as_ref()).unwrap();
assert!((pred.state[(0, 0)] - 2.0).abs() < 1.0e-12);
assert!((pred.covariance[(0, 0)] - 1.25).abs() < 1.0e-12);
assert!((pred.output[(0, 0)] - 2.0).abs() < 1.0e-12);
let y = Mat::from_fn(1, 1, |_, _| 1.5f64);
let update = filter.update(&pred, u.as_ref(), y.as_ref()).unwrap();
let expected_k = 1.25 / 2.25;
let expected_x = 2.0 + expected_k * (1.5 - 2.0);
let expected_p = 1.25 - expected_k * 2.25 * expected_k;
assert!((update.gain[(0, 0)] - expected_k).abs() < 1.0e-12);
assert!((update.state[(0, 0)] - expected_x).abs() < 1.0e-12);
assert!((update.covariance[(0, 0)] - expected_p).abs() < 1.0e-12);
assert!((update.predicted_output[(0, 0)] - 2.0).abs() < 1.0e-12);
assert!((update.output[(0, 0)] - expected_x).abs() < 1.0e-12);
assert!((update.innovation_norm - 0.5).abs() < 1.0e-12);
assert!(update.normalized_innovation_norm.is_finite());
}
#[test]
fn discrete_kalman_step_updates_internal_state() {
let system = DiscreteStateSpace::new(
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 0.0f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 0.0f64),
1.0,
)
.unwrap();
let mut filter = DiscreteKalmanFilter::from_state_space(
&system,
Mat::from_fn(1, 1, |_, _| 0.1f64),
Mat::from_fn(1, 1, |_, _| 0.2f64),
Mat::from_fn(1, 1, |_, _| 0.0f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
)
.unwrap();
let u = Mat::from_fn(1, 1, |_, _| 0.0f64);
let y = Mat::from_fn(1, 1, |_, _| 1.0f64);
let update = filter.step(u.as_ref(), y.as_ref()).unwrap();
assert_close(&filter.state_estimate().to_owned(), &update.state, 1.0e-12);
assert_close(&filter.covariance().to_owned(), &update.covariance, 1.0e-12);
}
#[test]
fn discrete_kalman_split_update_recomputes_feedthrough_from_update_input() {
let filter_a = DiscreteKalmanFilter::new(
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 0.0f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 2.0f64),
Mat::from_fn(1, 1, |_, _| 0.25f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 0.5f64),
)
.unwrap();
let mut filter_b = DiscreteKalmanFilter::new(
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 0.0f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 2.0f64),
Mat::from_fn(1, 1, |_, _| 0.25f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 0.5f64),
)
.unwrap();
let predict_input = Mat::from_fn(1, 1, |_, _| 0.0f64);
let update_input = Mat::from_fn(1, 1, |_, _| 1.0f64);
let measurement = Mat::from_fn(1, 1, |_, _| 3.5f64);
let prediction = filter_a.predict(predict_input.as_ref()).unwrap();
let split_update = filter_a
.update(&prediction, update_input.as_ref(), measurement.as_ref())
.unwrap();
let step_update = filter_b
.step(update_input.as_ref(), measurement.as_ref())
.unwrap();
assert_close(
&split_update.predicted_output,
&step_update.predicted_output,
1.0e-12,
);
assert_close(&split_update.state, &step_update.state, 1.0e-12);
assert_close(&split_update.covariance, &step_update.covariance, 1.0e-12);
}
#[test]
fn discrete_kalman_rejects_singular_innovation_covariance() {
let filter = DiscreteKalmanFilter::new(
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 0.0f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 0.0f64),
Mat::from_fn(1, 1, |_, _| 0.0f64),
Mat::from_fn(1, 1, |_, _| 0.0f64),
Mat::from_fn(1, 1, |_, _| 0.0f64),
Mat::from_fn(1, 1, |_, _| 0.0f64),
)
.unwrap();
let u = Mat::from_fn(1, 1, |_, _| 0.0f64);
let pred = filter.predict(u.as_ref()).unwrap();
let y = Mat::from_fn(1, 1, |_, _| 0.0f64);
let err = filter.update(&pred, u.as_ref(), y.as_ref()).unwrap_err();
assert!(matches!(err, EstimatorError::SingularInnovationCovariance));
}
#[test]
fn joseph_and_simple_updates_match_on_scalar_problem() {
let a = Mat::from_fn(1, 1, |_, _| 1.0f64);
let b = Mat::from_fn(1, 1, |_, _| 1.0f64);
let c = Mat::from_fn(1, 1, |_, _| 1.0f64);
let d = Mat::from_fn(1, 1, |_, _| 0.0f64);
let w = Mat::from_fn(1, 1, |_, _| 0.25f64);
let v = Mat::from_fn(1, 1, |_, _| 1.0f64);
let x0 = Mat::from_fn(1, 1, |_, _| 0.0f64);
let p0 = Mat::from_fn(1, 1, |_, _| 1.0f64);
let simple = DiscreteKalmanFilter::new_with_covariance_update(
a.clone(),
b.clone(),
c.clone(),
d.clone(),
w.clone(),
v.clone(),
x0.clone(),
p0.clone(),
CovarianceUpdate::Simple,
)
.unwrap();
let joseph = DiscreteKalmanFilter::new_with_covariance_update(
a,
b,
c,
d,
w,
v,
x0,
p0,
CovarianceUpdate::Joseph,
)
.unwrap();
let u = Mat::from_fn(1, 1, |_, _| 2.0f64);
let y = Mat::from_fn(1, 1, |_, _| 1.5f64);
let pred_simple = simple.predict(u.as_ref()).unwrap();
let pred_joseph = joseph.predict(u.as_ref()).unwrap();
let update_simple = simple.update(&pred_simple, u.as_ref(), y.as_ref()).unwrap();
let update_joseph = joseph.update(&pred_joseph, u.as_ref(), y.as_ref()).unwrap();
assert_close(&update_simple.state, &update_joseph.state, 1.0e-12);
assert_close(
&update_simple.covariance,
&update_joseph.covariance,
1.0e-12,
);
}
#[test]
fn steady_state_discrete_filter_matches_dlqe_gain_and_full_filter_init() {
let system = DiscreteStateSpace::new(
Mat::from_fn(1, 1, |_, _| 0.8f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 0.0f64),
0.1,
)
.unwrap();
let w = Mat::from_fn(1, 1, |_, _| 0.2f64);
let v = Mat::from_fn(1, 1, |_, _| 0.3f64);
let x0 = Mat::from_fn(1, 1, |_, _| 0.0f64);
let solve = system.dlqe(w.as_ref(), v.as_ref()).unwrap();
let steady =
SteadyStateKalmanFilter::from_dlqe(&system, w.as_ref(), v.as_ref(), x0.clone())
.unwrap();
let full = DiscreteKalmanFilter::from_dlqe(&system, w.as_ref(), v.as_ref(), x0).unwrap();
assert_close(
steady.steady_state_covariance.as_ref().unwrap(),
&solve.covariance,
1.0e-12,
);
let u = Mat::from_fn(1, 1, |_, _| 1.0f64);
let y = Mat::from_fn(1, 1, |_, _| 0.25f64);
let full_prediction = full.predict(u.as_ref()).unwrap();
assert_close(&full_prediction.covariance, &solve.covariance, 1.0e-12);
let steady_update = {
let pred = steady.predict(u.as_ref()).unwrap();
steady.update(&pred, u.as_ref(), y.as_ref()).unwrap()
};
let full_update = full
.update(&full_prediction, u.as_ref(), y.as_ref())
.unwrap();
assert_close(&steady.gain().to_owned(), &full_update.gain, 1.0e-10);
assert_close(
&full.covariance().to_owned(),
&full_update.covariance,
1.0e-10,
);
assert_close(&steady_update.state, &full_update.state, 1.0e-10);
assert_close(&steady_update.output, &full_update.output, 1.0e-10);
}
#[test]
fn steady_state_from_filter_gain_matches_dlqe_when_given_filter_gain() {
let system = DiscreteStateSpace::new(
Mat::from_fn(1, 1, |_, _| 0.8f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 0.0f64),
0.1,
)
.unwrap();
let w = Mat::from_fn(1, 1, |_, _| 0.2f64);
let v = Mat::from_fn(1, 1, |_, _| 0.3f64);
let x0 = Mat::from_fn(1, 1, |_, _| 0.0f64);
let solve = system.dlqe(w.as_ref(), v.as_ref()).unwrap();
let (filter_gain, _) = super::steady_state_filter_gain_dense(
system.c(),
v.as_ref(),
solve.covariance.as_ref(),
)
.unwrap();
let from_filter_gain = SteadyStateKalmanFilter::from_filter_gain(
&system,
filter_gain,
x0.clone(),
Some(solve.covariance.clone()),
)
.unwrap();
let from_dlqe =
SteadyStateKalmanFilter::from_dlqe(&system, w.as_ref(), v.as_ref(), x0).unwrap();
let u = Mat::from_fn(1, 1, |_, _| 1.0f64);
let y = Mat::from_fn(1, 1, |_, _| 0.25f64);
let update_from_filter_gain = {
let prediction = from_filter_gain.predict(u.as_ref()).unwrap();
from_filter_gain
.update(&prediction, u.as_ref(), y.as_ref())
.unwrap()
};
let update_from_dlqe = {
let prediction = from_dlqe.predict(u.as_ref()).unwrap();
from_dlqe
.update(&prediction, u.as_ref(), y.as_ref())
.unwrap()
};
assert_close(
&update_from_filter_gain.state,
&update_from_dlqe.state,
1.0e-10,
);
assert_close(
&update_from_filter_gain.output,
&update_from_dlqe.output,
1.0e-10,
);
}
#[test]
fn steady_state_split_update_recomputes_feedthrough_from_update_input() {
let system = DiscreteStateSpace::new(
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 0.0f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 2.0f64),
1.0,
)
.unwrap();
let filter_a = SteadyStateKalmanFilter::from_filter_gain(
&system,
Mat::from_fn(1, 1, |_, _| 0.25f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
None,
)
.unwrap();
let mut filter_b = SteadyStateKalmanFilter::from_filter_gain(
&system,
Mat::from_fn(1, 1, |_, _| 0.25f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
None,
)
.unwrap();
let predict_input = Mat::from_fn(1, 1, |_, _| 0.0f64);
let update_input = Mat::from_fn(1, 1, |_, _| 1.0f64);
let measurement = Mat::from_fn(1, 1, |_, _| 3.5f64);
let prediction = filter_a.predict(predict_input.as_ref()).unwrap();
let split_update = filter_a
.update(&prediction, update_input.as_ref(), measurement.as_ref())
.unwrap();
let step_update = filter_b
.step(update_input.as_ref(), measurement.as_ref())
.unwrap();
assert_close(&split_update.state, &step_update.state, 1.0e-12);
assert_close(&split_update.output, &step_update.output, 1.0e-12);
}
#[test]
fn continuous_observer_derivative_matches_manual_scalar_formula() {
let system = ContinuousStateSpace::new(
Mat::from_fn(1, 1, |_, _| 2.0f64),
Mat::from_fn(1, 1, |_, _| 3.0f64),
Mat::from_fn(1, 1, |_, _| 4.0f64),
Mat::from_fn(1, 1, |_, _| 5.0f64),
)
.unwrap();
let observer = ContinuousObserver::from_gain(
&system,
Mat::from_fn(1, 1, |_, _| 6.0f64),
Mat::from_fn(1, 1, |_, _| 7.0f64),
)
.unwrap();
let u = Mat::from_fn(1, 1, |_, _| 0.5f64);
let y = Mat::from_fn(1, 1, |_, _| 1.0f64);
let deriv = observer.derivative(u.as_ref(), y.as_ref()).unwrap();
let expected_output = 4.0 * 7.0 + 5.0 * 0.5;
let expected_innovation = 1.0 - expected_output;
let expected_xdot = 2.0 * 7.0 + 3.0 * 0.5 + 6.0 * expected_innovation;
assert!((deriv.output[(0, 0)] - expected_output).abs() < 1.0e-12);
assert!((deriv.innovation[(0, 0)] - expected_innovation).abs() < 1.0e-12);
assert!((deriv.state_derivative[(0, 0)] - expected_xdot).abs() < 1.0e-12);
assert!((deriv.innovation_norm - expected_innovation.abs()).abs() < 1.0e-12);
}
#[test]
fn state_space_observer_and_steady_state_methods_build_wrappers() {
let continuous = ContinuousStateSpace::new(
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 0.0f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 0.0f64),
)
.unwrap();
let discrete = DiscreteStateSpace::new(
Mat::from_fn(1, 1, |_, _| 0.8f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 0.0f64),
0.1,
)
.unwrap();
let w = Mat::from_fn(1, 1, |_, _| 1.0f64);
let v = Mat::from_fn(1, 1, |_, _| 1.0f64);
let x0 = Mat::from_fn(1, 1, |_, _| 0.0f64);
let observer = continuous
.observer(w.as_ref(), v.as_ref(), x0.clone())
.unwrap();
let steady = discrete
.steady_state_kalman(w.as_ref(), v.as_ref(), x0)
.unwrap();
assert_eq!(observer.gain.nrows(), 1);
assert_eq!(steady.gain.nrows(), 1);
assert!(steady.steady_state_covariance.is_some());
}
}