pub(crate) mod convert;
mod domain;
mod error;
mod sparse;
pub use convert::{ContinuousizationMethod, DiscretizationMethod};
pub use domain::{ContinuousTime, DiscreteTime};
pub use error::StateSpaceError;
pub use sparse::{SparseContinuousStateSpace, SparseDiscreteStateSpace, SparseStateSpace};
use crate::control::dense_ops::dense_mul_plain;
use crate::control::matrix_equations::lyapunov::{
DenseLyapunovSolve, LyapunovError, controllability_gramian_dense, observability_gramian_dense,
};
use crate::control::matrix_equations::stein::{
DenseSteinSolve, SteinError, controllability_gramian_discrete_dense,
observability_gramian_discrete_dense,
};
use crate::control::synthesis::{LqrError, LqrSolve, dlqr_dense, lqr_dense};
use crate::sparse::compensated::CompensatedField;
use faer::{Mat, MatRef};
use faer_traits::ComplexField;
use faer_traits::ext::ComplexFieldExt;
use num_traits::{Float, NumCast, Zero};
#[derive(Clone, Debug, PartialEq)]
pub struct StateSpace<T, Domain> {
pub(crate) a: Mat<T>,
pub(crate) b: Mat<T>,
pub(crate) c: Mat<T>,
pub(crate) d: Mat<T>,
pub(crate) domain: Domain,
}
pub type ContinuousStateSpace<T> = StateSpace<T, ContinuousTime>;
pub type DiscreteStateSpace<T> = StateSpace<T, DiscreteTime<<T as ComplexField>::Real>>;
#[derive(Clone, Debug, PartialEq)]
pub struct ObserverControllerComposition<T, Domain> {
pub controller: StateSpace<T, Domain>,
pub closed_loop: StateSpace<T, Domain>,
}
impl<T, Domain> StateSpace<T, Domain> {
#[must_use]
pub fn nstates(&self) -> usize {
self.a.nrows()
}
#[must_use]
pub fn ninputs(&self) -> usize {
self.b.ncols()
}
#[must_use]
pub fn noutputs(&self) -> usize {
self.c.nrows()
}
#[must_use]
pub fn is_siso(&self) -> bool {
self.ninputs() == 1 && self.noutputs() == 1
}
#[must_use]
pub fn a(&self) -> MatRef<'_, T> {
self.a.as_ref()
}
#[must_use]
pub fn b(&self) -> MatRef<'_, T> {
self.b.as_ref()
}
#[must_use]
pub fn c(&self) -> MatRef<'_, T> {
self.c.as_ref()
}
#[must_use]
pub fn d(&self) -> MatRef<'_, T> {
self.d.as_ref()
}
#[must_use]
pub fn domain(&self) -> &Domain {
&self.domain
}
#[must_use]
pub fn into_parts(self) -> (Mat<T>, Mat<T>, Mat<T>, Mat<T>, Domain) {
(self.a, self.b, self.c, self.d, self.domain)
}
}
impl<T> ContinuousStateSpace<T>
where
T: ComplexField,
{
pub fn new(a: Mat<T>, b: Mat<T>, c: Mat<T>, d: Mat<T>) -> Result<Self, StateSpaceError> {
validate_blocks(
a.nrows(),
a.ncols(),
b.nrows(),
b.ncols(),
c.nrows(),
c.ncols(),
d.nrows(),
d.ncols(),
)?;
Ok(Self {
a,
b,
c,
d,
domain: ContinuousTime,
})
}
pub fn with_zero_feedthrough(a: Mat<T>, b: Mat<T>, c: Mat<T>) -> Result<Self, StateSpaceError> {
let d = Mat::zeros(c.nrows(), b.ncols());
Self::new(a, b, c, d)
}
}
impl<T> ContinuousStateSpace<T>
where
T: CompensatedField,
T::Real: Float,
{
pub fn try_cast<U>(&self) -> Result<ContinuousStateSpace<U>, StateSpaceError>
where
U: CompensatedField,
U::Real: Float,
{
ContinuousStateSpace::new(
cast_mat(self.a(), "state_space.a")?,
cast_mat(self.b(), "state_space.b")?,
cast_mat(self.c(), "state_space.c")?,
cast_mat(self.d(), "state_space.d")?,
)
}
}
impl<T> ContinuousStateSpace<T>
where
T: ComplexField + Copy,
{
pub fn parallel_add(&self, rhs: &Self) -> Result<Self, StateSpaceError> {
let (a, b, c, d) = parallel_parts(self, rhs, false)?;
Self::new(a, b, c, d)
}
pub fn parallel_sub(&self, rhs: &Self) -> Result<Self, StateSpaceError> {
let (a, b, c, d) = parallel_parts(self, rhs, true)?;
Self::new(a, b, c, d)
}
pub fn series(&self, next: &Self) -> Result<Self, StateSpaceError> {
let (a, b, c, d) = series_parts(self, next)?;
Self::new(a, b, c, d)
}
pub fn append(&self, rhs: &Self) -> Result<Self, StateSpaceError> {
let (a, b, c, d) = append_parts(self, rhs);
Self::new(a, b, c, d)
}
pub fn with_state_feedback(&self, k: MatRef<'_, T>) -> Result<Self, StateSpaceError> {
validate_state_feedback_gain(self, k)?;
let bk = dense_mul(self.b(), k)?;
let dk = dense_mul(self.d(), k)?;
let a = self.a() - &bk;
let c = self.c() - &dk;
Self::new(a, self.b().to_owned(), c, self.d().to_owned())
}
pub fn with_output_injection(&self, l: MatRef<'_, T>) -> Result<Self, StateSpaceError> {
let (a, b, c, d) = output_injection_parts(self, l)?;
Self::new(a, b, c, d)
}
pub fn observer_controller_augmented(
&self,
k: MatRef<'_, T>,
l: MatRef<'_, T>,
) -> Result<ObserverControllerComposition<T, ContinuousTime>, StateSpaceError> {
let (controller, closed_loop) = observer_controller_parts(self, k, l)?;
Ok(ObserverControllerComposition {
controller: ContinuousStateSpace::new(
controller.0,
controller.1,
controller.2,
controller.3,
)?,
closed_loop: ContinuousStateSpace::new(
closed_loop.0,
closed_loop.1,
closed_loop.2,
closed_loop.3,
)?,
})
}
}
impl<T> ContinuousStateSpace<T>
where
T: CompensatedField,
T::Real: Float + faer_traits::RealField,
{
pub fn controllability_gramian(&self) -> Result<DenseLyapunovSolve<T>, LyapunovError> {
controllability_gramian_dense(self.a.as_ref(), self.b.as_ref())
}
pub fn observability_gramian(&self) -> Result<DenseLyapunovSolve<T>, LyapunovError> {
observability_gramian_dense(self.a.as_ref(), self.c.as_ref())
}
pub fn discretize(
&self,
sample_time: T::Real,
method: DiscretizationMethod<T::Real>,
) -> Result<DiscreteStateSpace<T>, StateSpaceError> {
convert::discretize(self, sample_time, method)
}
pub fn lqr(&self, q: MatRef<'_, T>, r: MatRef<'_, T>) -> Result<LqrSolve<T>, LqrError> {
lqr_dense(self.a.as_ref(), self.b.as_ref(), q, r)
}
}
impl<T> DiscreteStateSpace<T>
where
T: ComplexField,
T::Real: Float,
{
pub fn new(
a: Mat<T>,
b: Mat<T>,
c: Mat<T>,
d: Mat<T>,
sample_time: T::Real,
) -> Result<Self, StateSpaceError> {
validate_blocks(
a.nrows(),
a.ncols(),
b.nrows(),
b.ncols(),
c.nrows(),
c.ncols(),
d.nrows(),
d.ncols(),
)?;
if !sample_time.is_finite() || sample_time <= <T::Real as Zero>::zero() {
return Err(StateSpaceError::InvalidSampleTime);
}
Ok(Self {
a,
b,
c,
d,
domain: DiscreteTime::new(sample_time),
})
}
pub fn with_zero_feedthrough(
a: Mat<T>,
b: Mat<T>,
c: Mat<T>,
sample_time: T::Real,
) -> Result<Self, StateSpaceError> {
let d = Mat::zeros(c.nrows(), b.ncols());
Self::new(a, b, c, d, sample_time)
}
#[must_use]
pub fn sample_time(&self) -> T::Real {
self.domain.sample_time()
}
}
impl<T> DiscreteStateSpace<T>
where
T: CompensatedField,
T::Real: Float,
{
pub fn try_cast<U>(&self) -> Result<DiscreteStateSpace<U>, StateSpaceError>
where
U: CompensatedField,
U::Real: Float,
{
DiscreteStateSpace::new(
cast_mat(self.a(), "state_space.a")?,
cast_mat(self.b(), "state_space.b")?,
cast_mat(self.c(), "state_space.c")?,
cast_mat(self.d(), "state_space.d")?,
NumCast::from(self.sample_time()).ok_or(StateSpaceError::ScalarConversionFailed {
which: "state_space.sample_time",
})?,
)
}
}
impl<T> DiscreteStateSpace<T>
where
T: ComplexField + Copy,
T::Real: Float,
{
pub fn delay(
samples: usize,
sample_time: T::Real,
channels: usize,
) -> Result<Self, StateSpaceError> {
if channels == 0 {
return Self::new(
Mat::zeros(0, 0),
Mat::zeros(0, 0),
Mat::zeros(0, 0),
Mat::zeros(0, 0),
sample_time,
);
}
if samples == 0 {
return Self::new(
Mat::zeros(0, 0),
Mat::zeros(0, channels),
Mat::zeros(channels, 0),
Mat::identity(channels, channels),
sample_time,
);
}
let nstates = samples * channels;
let a = Mat::from_fn(nstates, nstates, |row, col| {
let row_block = row / channels;
let col_block = col / channels;
if row_block == col_block + 1 && row % channels == col % channels {
T::one()
} else {
T::zero()
}
});
let b = Mat::from_fn(nstates, channels, |row, col| {
if row < channels && row == col {
T::one()
} else {
T::zero()
}
});
let c = Mat::from_fn(channels, nstates, |row, col| {
if col / channels == samples - 1 && row == col % channels {
T::one()
} else {
T::zero()
}
});
let d = Mat::zeros(channels, channels);
Self::new(a, b, c, d, sample_time)
}
pub fn parallel_add(&self, rhs: &Self) -> Result<Self, StateSpaceError> {
ensure_sample_time_match(self.sample_time(), rhs.sample_time())?;
let (a, b, c, d) = parallel_parts(self, rhs, false)?;
Self::new(a, b, c, d, self.sample_time())
}
pub fn parallel_sub(&self, rhs: &Self) -> Result<Self, StateSpaceError> {
ensure_sample_time_match(self.sample_time(), rhs.sample_time())?;
let (a, b, c, d) = parallel_parts(self, rhs, true)?;
Self::new(a, b, c, d, self.sample_time())
}
pub fn series(&self, next: &Self) -> Result<Self, StateSpaceError> {
ensure_sample_time_match(self.sample_time(), next.sample_time())?;
let (a, b, c, d) = series_parts(self, next)?;
Self::new(a, b, c, d, self.sample_time())
}
pub fn append(&self, rhs: &Self) -> Result<Self, StateSpaceError> {
ensure_sample_time_match(self.sample_time(), rhs.sample_time())?;
let (a, b, c, d) = append_parts(self, rhs);
Self::new(a, b, c, d, self.sample_time())
}
pub fn with_input_delay(&self, samples: usize) -> Result<Self, StateSpaceError> {
if samples == 0 {
return Ok(self.clone());
}
Self::delay(samples, self.sample_time(), self.ninputs())?.series(self)
}
pub fn with_output_delay(&self, samples: usize) -> Result<Self, StateSpaceError> {
if samples == 0 {
return Ok(self.clone());
}
self.series(&Self::delay(samples, self.sample_time(), self.noutputs())?)
}
pub fn with_state_feedback(&self, k: MatRef<'_, T>) -> Result<Self, StateSpaceError> {
validate_state_feedback_gain(self, k)?;
let bk = dense_mul(self.b(), k)?;
let dk = dense_mul(self.d(), k)?;
let a = self.a() - &bk;
let c = self.c() - &dk;
Self::new(
a,
self.b().to_owned(),
c,
self.d().to_owned(),
self.sample_time(),
)
}
pub fn with_output_injection(&self, l: MatRef<'_, T>) -> Result<Self, StateSpaceError> {
let (a, b, c, d) = output_injection_parts(self, l)?;
Self::new(a, b, c, d, self.sample_time())
}
pub fn observer_controller_augmented(
&self,
k: MatRef<'_, T>,
l: MatRef<'_, T>,
) -> Result<ObserverControllerComposition<T, DiscreteTime<T::Real>>, StateSpaceError> {
let (controller, closed_loop) = observer_controller_parts(self, k, l)?;
Ok(ObserverControllerComposition {
controller: DiscreteStateSpace::new(
controller.0,
controller.1,
controller.2,
controller.3,
self.sample_time(),
)?,
closed_loop: DiscreteStateSpace::new(
closed_loop.0,
closed_loop.1,
closed_loop.2,
closed_loop.3,
self.sample_time(),
)?,
})
}
}
impl<T> DiscreteStateSpace<T>
where
T: CompensatedField,
T::Real: Float + faer_traits::RealField,
{
pub fn controllability_gramian(&self) -> Result<DenseSteinSolve<T>, SteinError> {
controllability_gramian_discrete_dense(self.a.as_ref(), self.b.as_ref())
}
pub fn observability_gramian(&self) -> Result<DenseSteinSolve<T>, SteinError> {
observability_gramian_discrete_dense(self.a.as_ref(), self.c.as_ref())
}
pub fn continuousize(
&self,
method: ContinuousizationMethod<T::Real>,
) -> Result<ContinuousStateSpace<T>, StateSpaceError> {
convert::continuousize(self, method)
}
pub fn dlqr(&self, q: MatRef<'_, T>, r: MatRef<'_, T>) -> Result<LqrSolve<T>, LqrError> {
dlqr_dense(self.a.as_ref(), self.b.as_ref(), q, r)
}
}
fn validate_blocks(
a_nrows: usize,
a_ncols: usize,
b_nrows: usize,
b_ncols: usize,
c_nrows: usize,
c_ncols: usize,
d_nrows: usize,
d_ncols: usize,
) -> Result<(), StateSpaceError> {
if a_nrows != a_ncols {
return Err(StateSpaceError::NonSquareA {
nrows: a_nrows,
ncols: a_ncols,
});
}
if b_nrows != a_nrows {
return Err(StateSpaceError::DimensionMismatch {
which: "b",
expected_nrows: a_nrows,
expected_ncols: b_ncols,
actual_nrows: b_nrows,
actual_ncols: b_ncols,
});
}
if c_ncols != a_ncols {
return Err(StateSpaceError::DimensionMismatch {
which: "c",
expected_nrows: c_nrows,
expected_ncols: a_ncols,
actual_nrows: c_nrows,
actual_ncols: c_ncols,
});
}
if d_nrows != c_nrows || d_ncols != b_ncols {
return Err(StateSpaceError::DimensionMismatch {
which: "d",
expected_nrows: c_nrows,
expected_ncols: b_ncols,
actual_nrows: d_nrows,
actual_ncols: d_ncols,
});
}
Ok(())
}
fn ensure_sample_time_match<R: Float>(lhs: R, rhs: R) -> Result<(), StateSpaceError> {
let scale = R::one().max(lhs.abs()).max(rhs.abs());
let tol = R::epsilon() * scale * R::from(128.0).unwrap_or_else(R::one);
if (lhs - rhs).abs() <= tol {
Ok(())
} else {
Err(StateSpaceError::MismatchedSampleTime)
}
}
fn parallel_parts<T, Domain>(
lhs: &StateSpace<T, Domain>,
rhs: &StateSpace<T, Domain>,
subtract_rhs: bool,
) -> Result<(Mat<T>, Mat<T>, Mat<T>, Mat<T>), StateSpaceError>
where
T: ComplexField + Copy,
{
ensure_same_io(
lhs,
rhs,
if subtract_rhs {
"parallel_sub"
} else {
"parallel_add"
},
)?;
let a = block_diag(lhs.a(), rhs.a());
let b = vcat(lhs.b(), rhs.b())?;
let rhs_c = if subtract_rhs {
negated(rhs.c())
} else {
rhs.c().to_owned()
};
let rhs_d = if subtract_rhs {
negated(rhs.d())
} else {
rhs.d().to_owned()
};
let c = hcat(lhs.c(), rhs_c.as_ref())?;
let d = lhs.d() + &rhs_d;
Ok((a, b, c, d))
}
fn series_parts<T, Domain>(
lhs: &StateSpace<T, Domain>,
rhs: &StateSpace<T, Domain>,
) -> Result<(Mat<T>, Mat<T>, Mat<T>, Mat<T>), StateSpaceError>
where
T: ComplexField + Copy,
{
if lhs.noutputs() != rhs.ninputs() {
return Err(StateSpaceError::DimensionMismatch {
which: "series.io",
expected_nrows: lhs.noutputs(),
expected_ncols: 1,
actual_nrows: rhs.ninputs(),
actual_ncols: 1,
});
}
let top_right = Mat::zeros(lhs.nstates(), rhs.nstates());
let bottom_left = dense_mul(rhs.b(), lhs.c())?;
let a = block_matrix2x2(lhs.a(), top_right.as_ref(), bottom_left.as_ref(), rhs.a())?;
let b_lower = dense_mul(rhs.b(), lhs.d())?;
let b = vcat(lhs.b(), b_lower.as_ref())?;
let c_left = dense_mul(rhs.d(), lhs.c())?;
let c = hcat(c_left.as_ref(), rhs.c())?;
let d = dense_mul(rhs.d(), lhs.d())?;
Ok((a, b, c, d))
}
fn append_parts<T, Domain>(
lhs: &StateSpace<T, Domain>,
rhs: &StateSpace<T, Domain>,
) -> (Mat<T>, Mat<T>, Mat<T>, Mat<T>)
where
T: ComplexField + Copy,
{
(
block_diag(lhs.a(), rhs.a()),
block_diag(lhs.b(), rhs.b()),
block_diag(lhs.c(), rhs.c()),
block_diag(lhs.d(), rhs.d()),
)
}
fn output_injection_parts<T, Domain>(
system: &StateSpace<T, Domain>,
l: MatRef<'_, T>,
) -> Result<(Mat<T>, Mat<T>, Mat<T>, Mat<T>), StateSpaceError>
where
T: ComplexField + Copy,
{
validate_output_injection_gain(system, l)?;
let lc = dense_mul(l, system.c())?;
let ld = dense_mul(l, system.d())?;
let a = system.a() - &lc;
let u_block = system.b() - &ld;
let y_block = l.to_owned();
let b = hcat(u_block.as_ref(), y_block.as_ref())?;
let d = hcat(
system.d(),
Mat::<T>::zeros(system.noutputs(), system.noutputs()).as_ref(),
)?;
Ok((a, b, system.c().to_owned(), d))
}
type Parts<T> = (Mat<T>, Mat<T>, Mat<T>, Mat<T>);
fn observer_controller_parts<T, Domain>(
plant: &StateSpace<T, Domain>,
k: MatRef<'_, T>,
l: MatRef<'_, T>,
) -> Result<(Parts<T>, Parts<T>), StateSpaceError>
where
T: ComplexField + Copy,
{
validate_state_feedback_gain(plant, k)?;
validate_output_injection_gain(plant, l)?;
let bk = dense_mul(plant.b(), k)?;
let lc = dense_mul(l, plant.c())?;
let ld = dense_mul(l, plant.d())?;
let ldk = dense_mul(ld.as_ref(), k)?;
let controller_a = (plant.a() - &bk - &lc) + &ldk;
let controller_b_r = plant.b() - &ld;
let controller_b = hcat(controller_b_r.as_ref(), l)?;
let controller_c = negated(k);
let controller_d = hcat(
Mat::identity(plant.ninputs(), plant.ninputs()).as_ref(),
Mat::<T>::zeros(plant.ninputs(), plant.noutputs()).as_ref(),
)?;
let top_right = negated(bk.as_ref());
let bottom_right = plant.a() - &bk - &lc;
let closed_loop_a = block_matrix2x2(
plant.a(),
top_right.as_ref(),
lc.as_ref(),
bottom_right.as_ref(),
)?;
let closed_loop_b = vcat(plant.b(), plant.b())?;
let closed_loop_c = hcat(
plant.c(),
negated(dense_mul(plant.d(), k)?.as_ref()).as_ref(),
)?;
let closed_loop_d = plant.d().to_owned();
Ok((
(controller_a, controller_b, controller_c, controller_d),
(closed_loop_a, closed_loop_b, closed_loop_c, closed_loop_d),
))
}
fn validate_state_feedback_gain<T, Domain>(
system: &StateSpace<T, Domain>,
k: MatRef<'_, T>,
) -> Result<(), StateSpaceError> {
if k.nrows() != system.ninputs() || k.ncols() != system.nstates() {
return Err(StateSpaceError::DimensionMismatch {
which: "state_feedback_gain",
expected_nrows: system.ninputs(),
expected_ncols: system.nstates(),
actual_nrows: k.nrows(),
actual_ncols: k.ncols(),
});
}
Ok(())
}
fn validate_output_injection_gain<T, Domain>(
system: &StateSpace<T, Domain>,
l: MatRef<'_, T>,
) -> Result<(), StateSpaceError> {
if l.nrows() != system.nstates() || l.ncols() != system.noutputs() {
return Err(StateSpaceError::DimensionMismatch {
which: "output_injection_gain",
expected_nrows: system.nstates(),
expected_ncols: system.noutputs(),
actual_nrows: l.nrows(),
actual_ncols: l.ncols(),
});
}
Ok(())
}
fn ensure_same_io<T, Domain>(
lhs: &StateSpace<T, Domain>,
rhs: &StateSpace<T, Domain>,
which: &'static str,
) -> Result<(), StateSpaceError> {
if lhs.ninputs() != rhs.ninputs() {
return Err(StateSpaceError::DimensionMismatch {
which,
expected_nrows: lhs.ninputs(),
expected_ncols: 1,
actual_nrows: rhs.ninputs(),
actual_ncols: 1,
});
}
if lhs.noutputs() != rhs.noutputs() {
return Err(StateSpaceError::DimensionMismatch {
which,
expected_nrows: lhs.noutputs(),
expected_ncols: 1,
actual_nrows: rhs.noutputs(),
actual_ncols: 1,
});
}
Ok(())
}
fn dense_mul<T>(lhs: MatRef<'_, T>, rhs: MatRef<'_, T>) -> Result<Mat<T>, StateSpaceError>
where
T: ComplexField + Copy,
{
if lhs.ncols() != rhs.nrows() {
return Err(StateSpaceError::DimensionMismatch {
which: "dense_mul",
expected_nrows: lhs.ncols(),
expected_ncols: 1,
actual_nrows: rhs.nrows(),
actual_ncols: 1,
});
}
Ok(dense_mul_plain(lhs, rhs))
}
fn cast_mat<T, U>(matrix: MatRef<'_, T>, which: &'static str) -> Result<Mat<U>, StateSpaceError>
where
T: CompensatedField,
U: CompensatedField,
T::Real: Float,
U::Real: Float,
{
for row in 0..matrix.nrows() {
for col in 0..matrix.ncols() {
let value = matrix[(row, col)];
let _: U::Real = NumCast::from(value.real())
.ok_or(StateSpaceError::ScalarConversionFailed { which })?;
if U::IS_REAL && value.imag() != <T::Real as Zero>::zero() {
return Err(StateSpaceError::ScalarConversionFailed { which });
}
let _: U::Real = NumCast::from(value.imag())
.ok_or(StateSpaceError::ScalarConversionFailed { which })?;
}
}
Ok(Mat::from_fn(matrix.nrows(), matrix.ncols(), |row, col| {
let value = matrix[(row, col)];
let real = NumCast::from(value.real()).expect("matrix entries validated before cast");
let imag = NumCast::from(value.imag()).expect("matrix entries validated before cast");
U::from_real_imag(real, imag)
}))
}
fn negated<T>(matrix: MatRef<'_, T>) -> Mat<T>
where
T: ComplexField + Copy,
{
Mat::from_fn(matrix.nrows(), matrix.ncols(), |row, col| {
-matrix[(row, col)]
})
}
fn hcat<T>(lhs: MatRef<'_, T>, rhs: MatRef<'_, T>) -> Result<Mat<T>, StateSpaceError>
where
T: ComplexField + Copy,
{
if lhs.nrows() != rhs.nrows() {
return Err(StateSpaceError::DimensionMismatch {
which: "hcat",
expected_nrows: lhs.nrows(),
expected_ncols: 1,
actual_nrows: rhs.nrows(),
actual_ncols: 1,
});
}
Ok(Mat::from_fn(
lhs.nrows(),
lhs.ncols() + rhs.ncols(),
|row, col| {
if col < lhs.ncols() {
lhs[(row, col)]
} else {
rhs[(row, col - lhs.ncols())]
}
},
))
}
fn vcat<T>(lhs: MatRef<'_, T>, rhs: MatRef<'_, T>) -> Result<Mat<T>, StateSpaceError>
where
T: ComplexField + Copy,
{
if lhs.ncols() != rhs.ncols() {
return Err(StateSpaceError::DimensionMismatch {
which: "vcat",
expected_nrows: 1,
expected_ncols: lhs.ncols(),
actual_nrows: 1,
actual_ncols: rhs.ncols(),
});
}
Ok(Mat::from_fn(
lhs.nrows() + rhs.nrows(),
lhs.ncols(),
|row, col| {
if row < lhs.nrows() {
lhs[(row, col)]
} else {
rhs[(row - lhs.nrows(), col)]
}
},
))
}
fn block_diag<T>(lhs: MatRef<'_, T>, rhs: MatRef<'_, T>) -> Mat<T>
where
T: ComplexField + Copy,
{
Mat::from_fn(
lhs.nrows() + rhs.nrows(),
lhs.ncols() + rhs.ncols(),
|row, col| {
if row < lhs.nrows() && col < lhs.ncols() {
lhs[(row, col)]
} else if row >= lhs.nrows() && col >= lhs.ncols() {
rhs[(row - lhs.nrows(), col - lhs.ncols())]
} else {
T::zero()
}
},
)
}
fn block_matrix2x2<T>(
top_left: MatRef<'_, T>,
top_right: MatRef<'_, T>,
bottom_left: MatRef<'_, T>,
bottom_right: MatRef<'_, T>,
) -> Result<Mat<T>, StateSpaceError>
where
T: ComplexField + Copy,
{
if top_left.nrows() != top_right.nrows()
|| bottom_left.nrows() != bottom_right.nrows()
|| top_left.ncols() != bottom_left.ncols()
|| top_right.ncols() != bottom_right.ncols()
{
return Err(StateSpaceError::DimensionMismatch {
which: "block_matrix2x2",
expected_nrows: top_left.nrows() + bottom_left.nrows(),
expected_ncols: top_left.ncols() + top_right.ncols(),
actual_nrows: top_right.nrows() + bottom_right.nrows(),
actual_ncols: bottom_left.ncols() + bottom_right.ncols(),
});
}
Ok(Mat::from_fn(
top_left.nrows() + bottom_left.nrows(),
top_left.ncols() + top_right.ncols(),
|row, col| {
if row < top_left.nrows() {
if col < top_left.ncols() {
top_left[(row, col)]
} else {
top_right[(row, col - top_left.ncols())]
}
} else if col < top_left.ncols() {
bottom_left[(row - top_left.nrows(), col)]
} else {
bottom_right[(row - top_left.nrows(), col - top_left.ncols())]
}
},
))
}
#[cfg(test)]
mod tests {
use super::{
ContinuousStateSpace, ContinuousizationMethod, DiscreteStateSpace, DiscretizationMethod,
StateSpaceError,
};
use crate::control::lti::ContinuousTransferFunction;
use faer::{Mat, c64};
use faer_traits::ext::ComplexFieldExt;
fn assert_close(lhs: &Mat<f64>, rhs: &Mat<f64>, tol: f64) {
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}) differs: lhs={}, rhs={}, err={err}, tol={tol}",
lhs[(row, col)],
rhs[(row, col)],
);
}
}
}
fn assert_close_c64(lhs: &Mat<c64>, rhs: &Mat<c64>, tol: f64) {
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)]).abs1();
assert!(
err <= tol,
"entry ({row}, {col}) differs: lhs={:?}, rhs={:?}, err={err}, tol={tol}",
lhs[(row, col)],
rhs[(row, col)],
);
}
}
}
#[test]
fn continuous_constructor_rejects_bad_dimensions() {
let a = Mat::<f64>::identity(2, 2);
let b = Mat::<f64>::zeros(3, 1);
let c = Mat::<f64>::zeros(1, 2);
let d = Mat::<f64>::zeros(1, 1);
let err = ContinuousStateSpace::new(a, b, c, d).unwrap_err();
assert!(matches!(
err,
StateSpaceError::DimensionMismatch { which: "b", .. }
));
}
#[test]
fn discrete_constructor_rejects_invalid_sample_time() {
let a = Mat::<f64>::identity(1, 1);
let b = Mat::<f64>::zeros(1, 1);
let c = Mat::<f64>::zeros(1, 1);
let d = Mat::<f64>::zeros(1, 1);
let err = DiscreteStateSpace::new(a, b, c, d, 0.0).unwrap_err();
assert_eq!(err, StateSpaceError::InvalidSampleTime);
}
#[test]
fn zero_feedthrough_constructor_sizes_d_correctly() {
let a = Mat::<f64>::identity(2, 2);
let b = Mat::<f64>::zeros(2, 3);
let c = Mat::<f64>::zeros(4, 2);
let sys = ContinuousStateSpace::with_zero_feedthrough(a, b, c).unwrap();
assert_eq!(sys.d().nrows(), 4);
assert_eq!(sys.d().ncols(), 3);
}
#[test]
fn zoh_discretize_matches_diagonal_closed_form_real_case() {
let a = Mat::from_fn(2, 2, |row, col| match (row, col) {
(0, 0) => -1.0,
(1, 1) => -2.0,
_ => 0.0,
});
let b = Mat::from_fn(2, 2, |row, col| if row == col { 1.0 } else { 0.0 });
let c = Mat::<f64>::identity(2, 2);
let d = Mat::<f64>::zeros(2, 2);
let sys = ContinuousStateSpace::new(a, b, c, d).unwrap();
let dt = 0.1;
let disc = sys
.discretize(dt, DiscretizationMethod::ZeroOrderHold)
.unwrap();
let expected_a = Mat::from_fn(2, 2, |row, col| match (row, col) {
(0, 0) => (-dt).exp(),
(1, 1) => (-2.0 * dt).exp(),
_ => 0.0,
});
let expected_b = Mat::from_fn(2, 2, |row, col| match (row, col) {
(0, 0) => 1.0 - (-dt).exp(),
(1, 1) => (1.0 - (-2.0 * dt).exp()) / 2.0,
_ => 0.0,
});
assert_close(&disc.a, &expected_a, 1e-12);
assert_close(&disc.b, &expected_b, 1e-12);
}
#[test]
fn bilinear_round_trip_recovers_original_real_system() {
let a = Mat::from_fn(2, 2, |row, col| match (row, col) {
(0, 0) => -2.0,
(0, 1) => 1.0,
(1, 0) => 0.0,
(1, 1) => -3.0,
_ => 0.0,
});
let b = Mat::from_fn(2, 1, |row, _| if row == 0 { 1.0 } else { -0.25 });
let c = Mat::from_fn(1, 2, |_, col| if col == 0 { 0.5 } else { -1.0 });
let d = Mat::from_fn(1, 1, |_, _| 0.2);
let sys = ContinuousStateSpace::new(a, b, c, d).unwrap();
let dt = 0.05;
let disc = sys
.discretize(
dt,
DiscretizationMethod::Bilinear {
prewarp_frequency: None,
},
)
.unwrap();
let recovered = disc
.continuousize(ContinuousizationMethod::Bilinear {
prewarp_frequency: None,
})
.unwrap();
assert_close(&recovered.a, &sys.a, 1e-11);
assert_close(&recovered.b, &sys.b, 1e-11);
assert_close(&recovered.c, &sys.c, 1e-11);
assert_close(&recovered.d, &sys.d, 1e-11);
}
#[test]
fn continuous_parallel_and_series_match_transfer_function_arithmetic() {
let lhs = ContinuousTransferFunction::continuous(vec![1.0], vec![1.0, 1.0]).unwrap();
let rhs = ContinuousTransferFunction::continuous(vec![2.0], vec![1.0, 2.0]).unwrap();
let lhs_ss = lhs.to_state_space().unwrap();
let rhs_ss = rhs.to_state_space().unwrap();
let parallel = lhs_ss
.parallel_add(&rhs_ss)
.unwrap()
.to_transfer_function()
.unwrap();
let series = lhs_ss
.series(&rhs_ss)
.unwrap()
.to_transfer_function()
.unwrap();
let parallel_tf = lhs.add(&rhs).unwrap();
let series_tf = lhs.mul(&rhs).unwrap();
assert_close(
&Mat::from_fn(parallel.numerator().len(), 1, |row, _| {
parallel.numerator()[row]
}),
&Mat::from_fn(parallel_tf.numerator().len(), 1, |row, _| {
parallel_tf.numerator()[row]
}),
1.0e-10,
);
assert_close(
&Mat::from_fn(parallel.denominator().len(), 1, |row, _| {
parallel.denominator()[row]
}),
&Mat::from_fn(parallel_tf.denominator().len(), 1, |row, _| {
parallel_tf.denominator()[row]
}),
1.0e-10,
);
assert_close(
&Mat::from_fn(series.numerator().len(), 1, |row, _| {
series.numerator()[row]
}),
&Mat::from_fn(series_tf.numerator().len(), 1, |row, _| {
series_tf.numerator()[row]
}),
1.0e-10,
);
assert_close(
&Mat::from_fn(series.denominator().len(), 1, |row, _| {
series.denominator()[row]
}),
&Mat::from_fn(series_tf.denominator().len(), 1, |row, _| {
series_tf.denominator()[row]
}),
1.0e-10,
);
}
#[test]
fn discrete_structural_composition_rejects_sample_time_mismatch() {
let lhs = DiscreteStateSpace::with_zero_feedthrough(
Mat::from_fn(1, 1, |_, _| 0.5f64),
Mat::from_fn(1, 1, |_, _| 1.0),
Mat::from_fn(1, 1, |_, _| 1.0),
0.1,
)
.unwrap();
let rhs = DiscreteStateSpace::with_zero_feedthrough(
Mat::from_fn(1, 1, |_, _| 0.25f64),
Mat::from_fn(1, 1, |_, _| 1.0),
Mat::from_fn(1, 1, |_, _| 1.0),
0.2,
)
.unwrap();
let err = lhs.parallel_add(&rhs).unwrap_err();
assert_eq!(err, StateSpaceError::MismatchedSampleTime);
}
#[test]
fn pure_discrete_delay_simulation_matches_shifted_signal() {
let delay = DiscreteStateSpace::<f64>::delay(2, 0.1, 2).unwrap();
let inputs = Mat::from_fn(2, 5, |row, col| match (row, col) {
(0, 0) => 1.0,
(0, 1) => 2.0,
(0, 2) => 3.0,
(0, 3) => 4.0,
(0, 4) => 5.0,
(1, 0) => -1.0,
(1, 1) => -2.0,
(1, 2) => -3.0,
(1, 3) => -4.0,
_ => -5.0,
});
let sim = delay
.simulate(&vec![0.0; delay.nstates()], inputs.as_ref())
.unwrap();
let expected = Mat::from_fn(
2,
5,
|row, col| {
if col < 2 { 0.0 } else { inputs[(row, col - 2)] }
},
);
assert_eq!(delay.sample_time(), 0.1);
assert_close(&sim.outputs, &expected, 1.0e-12);
}
#[test]
fn zero_sample_delay_is_identity_map() {
let delay = DiscreteStateSpace::<f64>::delay(0, 0.1, 1).unwrap();
let inputs = Mat::from_fn(1, 4, |_, col| [1.0, -2.0, 3.0, -4.0][col]);
let sim = delay.simulate(&[], inputs.as_ref()).unwrap();
assert_eq!(delay.nstates(), 0);
assert_close(&sim.outputs, &inputs, 1.0e-12);
}
#[test]
fn input_and_output_delay_helpers_match_shifted_reference_sequences() {
let plant = DiscreteStateSpace::new(
Mat::from_fn(1, 1, |_, _| 0.5),
Mat::from_fn(1, 1, |_, _| 1.0),
Mat::from_fn(1, 1, |_, _| 1.0),
Mat::from_fn(1, 1, |_, _| 0.25),
0.1,
)
.unwrap();
let inputs = Mat::from_fn(1, 6, |_, col| [1.0, 2.0, -1.0, 0.5, 3.0, -2.0][col]);
let delayed_input = plant.with_input_delay(2).unwrap();
let shifted_inputs = Mat::from_fn(1, inputs.ncols(), |_, col| {
if col < 2 { 0.0 } else { inputs[(0, col - 2)] }
});
let delayed_input_sim = delayed_input
.simulate(&vec![0.0; delayed_input.nstates()], inputs.as_ref())
.unwrap();
let shifted_input_sim = plant
.simulate(&vec![0.0; plant.nstates()], shifted_inputs.as_ref())
.unwrap();
assert_close(
&delayed_input_sim.outputs,
&shifted_input_sim.outputs,
1.0e-12,
);
let delayed_output = plant.with_output_delay(2).unwrap();
let plant_sim = plant
.simulate(&vec![0.0; plant.nstates()], inputs.as_ref())
.unwrap();
let delayed_output_sim = delayed_output
.simulate(&vec![0.0; delayed_output.nstates()], inputs.as_ref())
.unwrap();
let expected_outputs = Mat::from_fn(1, plant_sim.outputs.ncols(), |_, col| {
if col < 2 {
0.0
} else {
plant_sim.outputs[(0, col - 2)]
}
});
assert_close(&delayed_output_sim.outputs, &expected_outputs, 1.0e-12);
}
#[test]
fn state_feedback_matches_lqr_closed_loop_matrix() {
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, |_, _| 3.0f64);
let d = Mat::from_fn(1, 1, |_, _| 2.0f64);
let sys = ContinuousStateSpace::new(a, b, c, d).unwrap();
let q = Mat::from_fn(1, 1, |_, _| 1.0f64);
let r = Mat::from_fn(1, 1, |_, _| 1.0f64);
let lqr = sys.lqr(q.as_ref(), r.as_ref()).unwrap();
let closed = sys.with_state_feedback(lqr.gain.as_ref()).unwrap();
assert_close(&closed.a, &lqr.closed_loop_a, 1.0e-12);
assert_close(&closed.b, &sys.b, 1.0e-12);
assert_close(
&closed.c,
&Mat::from_fn(1, 1, |_, _| {
sys.c[(0, 0)] - sys.d[(0, 0)] * lqr.gain[(0, 0)]
}),
1.0e-12,
);
assert_close(&closed.d, &sys.d, 1.0e-12);
}
#[test]
fn output_injection_matches_lqe_estimator_dynamics() {
let a = Mat::from_fn(1, 1, |_, _| 1.0f64);
let b = Mat::from_fn(1, 1, |_, _| 2.0f64);
let c = Mat::from_fn(1, 1, |_, _| 3.0f64);
let d = Mat::from_fn(1, 1, |_, _| 4.0f64);
let sys = ContinuousStateSpace::new(a, b, c, d).unwrap();
let w = Mat::from_fn(1, 1, |_, _| 1.0f64);
let v = Mat::from_fn(1, 1, |_, _| 1.0f64);
let lqe = sys.lqe(w.as_ref(), v.as_ref()).unwrap();
let injected = sys.with_output_injection(lqe.gain.as_ref()).unwrap();
assert_close(&injected.a, &lqe.estimator_a, 1.0e-12);
assert_close(
&injected.b,
&Mat::from_fn(1, 2, |_, col| {
if col == 0 {
sys.b[(0, 0)] - lqe.gain[(0, 0)] * sys.d[(0, 0)]
} else {
lqe.gain[(0, 0)]
}
}),
1.0e-12,
);
assert_close(&injected.c, &sys.c, 1.0e-12);
assert_close(
&injected.d,
&Mat::from_fn(1, 2, |_, col| if col == 0 { sys.d[(0, 0)] } else { 0.0 }),
1.0e-12,
);
}
#[test]
fn observer_controller_augmented_matches_scalar_manual_formula() {
let plant = ContinuousStateSpace::new(
Mat::from_fn(1, 1, |_, _| 1.0f64),
Mat::from_fn(1, 1, |_, _| 2.0f64),
Mat::from_fn(1, 1, |_, _| 3.0f64),
Mat::from_fn(1, 1, |_, _| 4.0f64),
)
.unwrap();
let k = Mat::from_fn(1, 1, |_, _| 5.0f64);
let l = Mat::from_fn(1, 1, |_, _| 6.0f64);
let composed = plant
.observer_controller_augmented(k.as_ref(), l.as_ref())
.unwrap();
assert_close(
&composed.controller.a,
&Mat::from_fn(1, 1, |_, _| 93.0),
1.0e-12,
);
assert_close(
&composed.controller.b,
&Mat::from_fn(1, 2, |_, col| if col == 0 { -22.0 } else { 6.0 }),
1.0e-12,
);
assert_close(
&composed.controller.c,
&Mat::from_fn(1, 1, |_, _| -5.0),
1.0e-12,
);
assert_close(
&composed.controller.d,
&Mat::from_fn(1, 2, |_, col| if col == 0 { 1.0 } else { 0.0 }),
1.0e-12,
);
assert_close(
&composed.closed_loop.a,
&Mat::from_fn(2, 2, |row, col| match (row, col) {
(0, 0) => 1.0,
(0, 1) => -10.0,
(1, 0) => 18.0,
(1, 1) => -27.0,
_ => 0.0,
}),
1.0e-12,
);
assert_close(
&composed.closed_loop.b,
&Mat::from_fn(2, 1, |_, _| 2.0),
1.0e-12,
);
assert_close(
&composed.closed_loop.c,
&Mat::from_fn(1, 2, |_, col| if col == 0 { 3.0 } else { -20.0 }),
1.0e-12,
);
assert_close(
&composed.closed_loop.d,
&Mat::from_fn(1, 1, |_, _| 4.0),
1.0e-12,
);
}
#[test]
fn bilinear_round_trip_recovers_original_complex_system() {
let a = Mat::from_fn(1, 1, |_, _| c64::new(-1.0, 0.25));
let b = Mat::from_fn(1, 1, |_, _| c64::new(0.5, -0.1));
let c = Mat::from_fn(1, 1, |_, _| c64::new(1.25, 0.4));
let d = Mat::from_fn(1, 1, |_, _| c64::new(-0.2, 0.1));
let sys = ContinuousStateSpace::new(a, b, c, d).unwrap();
let disc = sys
.discretize(
0.1,
DiscretizationMethod::Bilinear {
prewarp_frequency: None,
},
)
.unwrap();
let recovered = disc
.continuousize(ContinuousizationMethod::Bilinear {
prewarp_frequency: None,
})
.unwrap();
assert_close_c64(&recovered.a, &sys.a, 1e-11);
assert_close_c64(&recovered.b, &sys.b, 1e-11);
assert_close_c64(&recovered.c, &sys.c, 1e-11);
assert_close_c64(&recovered.d, &sys.d, 1e-11);
}
#[test]
fn discrete_state_space_try_cast_to_f32_preserves_sample_time_and_blocks() {
let sys = DiscreteStateSpace::new(
Mat::from_fn(2, 2, |row, col| match (row, col) {
(0, 0) => 0.9,
(0, 1) => 0.2,
(1, 0) => -0.1,
_ => 0.8,
}),
Mat::from_fn(2, 1, |row, _| if row == 0 { 0.5 } else { -0.25 }),
Mat::from_fn(1, 2, |_, col| if col == 0 { 1.0 } else { -0.3 }),
Mat::from_fn(1, 1, |_, _| 0.125),
0.05,
)
.unwrap();
let cast = sys.try_cast::<f32>().unwrap();
assert!((cast.sample_time() - 0.05f32).abs() <= 1.0e-6);
assert!((cast.a()[(0, 0)] - 0.9f32).abs() <= 1.0e-6);
assert!((cast.a()[(0, 1)] - 0.2f32).abs() <= 1.0e-6);
assert!((cast.a()[(1, 0)] + 0.1f32).abs() <= 1.0e-6);
assert!((cast.a()[(1, 1)] - 0.8f32).abs() <= 1.0e-6);
assert!((cast.b()[(0, 0)] - 0.5f32).abs() <= 1.0e-6);
assert!((cast.b()[(1, 0)] + 0.25f32).abs() <= 1.0e-6);
assert!((cast.c()[(0, 0)] - 1.0f32).abs() <= 1.0e-6);
assert!((cast.c()[(0, 1)] + 0.3f32).abs() <= 1.0e-6);
assert!((cast.d()[(0, 0)] - 0.125f32).abs() <= 1.0e-6);
}
#[test]
fn state_space_try_cast_rejects_complex_to_real_data_loss() {
let sys = ContinuousStateSpace::new(
Mat::from_fn(1, 1, |_, _| c64::new(-1.0, 0.25)),
Mat::from_fn(1, 1, |_, _| c64::new(1.0, 0.0)),
Mat::from_fn(1, 1, |_, _| c64::new(1.0, 0.0)),
Mat::from_fn(1, 1, |_, _| c64::new(0.0, 0.0)),
)
.unwrap();
let err = sys.try_cast::<f64>().unwrap_err();
assert_eq!(
err,
StateSpaceError::ScalarConversionFailed {
which: "state_space.a"
}
);
}
#[test]
fn continuous_gramian_adapters_call_existing_dense_lyapunov_paths() {
let a = Mat::from_fn(2, 2, |row, col| match (row, col) {
(0, 0) => -1.0,
(1, 1) => -2.0,
_ => 0.0,
});
let b = Mat::<f64>::identity(2, 2);
let c = Mat::<f64>::identity(2, 2);
let sys = ContinuousStateSpace::with_zero_feedthrough(a, b, c).unwrap();
let wc = sys.controllability_gramian().unwrap();
let wo = sys.observability_gramian().unwrap();
assert!(wc.residual_norm <= 1e-12);
assert!(wo.residual_norm <= 1e-12);
}
#[test]
fn discrete_gramian_adapters_call_dense_stein_paths() {
let a = Mat::from_fn(2, 2, |row, col| match (row, col) {
(0, 0) => 0.25,
(1, 1) => -0.5,
_ => 0.0,
});
let b = Mat::<f64>::identity(2, 2);
let c = Mat::<f64>::identity(2, 2);
let sys = DiscreteStateSpace::with_zero_feedthrough(a, b, c, 0.1).unwrap();
let wc = sys.controllability_gramian().unwrap();
let wo = sys.observability_gramian().unwrap();
assert!(wc.residual_norm <= 1e-12);
assert!(wo.residual_norm <= 1e-12);
}
#[test]
fn exact_zoh_d2c_is_explicitly_unsupported_for_now() {
let a = Mat::<f64>::identity(1, 1);
let b = Mat::<f64>::zeros(1, 1);
let c = Mat::<f64>::zeros(1, 1);
let d = Mat::<f64>::zeros(1, 1);
let sys = DiscreteStateSpace::new(a, b, c, d, 0.1).unwrap();
let err = sys
.continuousize(ContinuousizationMethod::ZeroOrderHold)
.unwrap_err();
assert!(matches!(err, StateSpaceError::UnsupportedConversion(_)));
}
}