use super::{ContinuousTime, DiscreteTime, StateSpaceError, validate_blocks};
use crate::control::matrix_equations::lyapunov::{
LowRankLyapunovSolve, LyapunovError, LyapunovParams, ShiftStrategy,
controllability_gramian_low_rank, observability_gramian_low_rank,
};
use crate::control::matrix_equations::stein::{
SteinError, controllability_gramian_discrete_low_rank, observability_gramian_discrete_low_rank,
};
use crate::control::reduction::{
BalancedError, BalancedParams, BalancedTruncationResult,
balanced_truncation_continuous_low_rank, balanced_truncation_discrete_low_rank,
};
use crate::sparse::compensated::CompensatedField;
use faer::sparse::{SparseColMat, SparseColMatRef};
use faer::{Mat, MatRef};
use faer_traits::ComplexField;
use num_traits::{Float, Zero};
#[derive(Clone, Debug)]
pub struct SparseStateSpace<T, Domain> {
pub(crate) a: SparseColMat<usize, T>,
pub(crate) b: Mat<T>,
pub(crate) c: Mat<T>,
pub(crate) d: Mat<T>,
pub(crate) domain: Domain,
}
pub type SparseContinuousStateSpace<T> = SparseStateSpace<T, ContinuousTime>;
pub type SparseDiscreteStateSpace<T> = SparseStateSpace<T, DiscreteTime<<T as ComplexField>::Real>>;
impl<T, Domain> SparseStateSpace<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) -> SparseColMatRef<'_, usize, 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) -> (SparseColMat<usize, T>, Mat<T>, Mat<T>, Mat<T>, Domain) {
(self.a, self.b, self.c, self.d, self.domain)
}
}
impl<T> SparseContinuousStateSpace<T>
where
T: ComplexField,
{
pub fn new(
a: SparseColMat<usize, 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: SparseColMat<usize, 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> SparseContinuousStateSpace<T>
where
T: CompensatedField,
T::Real: Float,
{
pub fn controllability_gramian(
&self,
shifts: &ShiftStrategy<T>,
params: LyapunovParams<T::Real>,
) -> Result<LowRankLyapunovSolve<T>, LyapunovError> {
controllability_gramian_low_rank(self.a.as_ref(), self.b.as_ref(), shifts, params)
}
pub fn observability_gramian(
&self,
shifts: &ShiftStrategy<T>,
params: LyapunovParams<T::Real>,
) -> Result<LowRankLyapunovSolve<T>, LyapunovError> {
observability_gramian_low_rank(self.a.as_ref(), self.c.as_ref(), shifts, params)
}
pub fn balanced_truncation(
&self,
shifts: &ShiftStrategy<T>,
gramian_params: LyapunovParams<T::Real>,
params: &BalancedParams<T::Real>,
) -> Result<BalancedTruncationResult<T, ContinuousTime>, BalancedError<T::Real>> {
balanced_truncation_continuous_low_rank(
self.a.as_ref(),
self.b.as_ref(),
self.c.as_ref(),
self.d.as_ref(),
shifts,
gramian_params,
params,
)
}
}
impl<T> SparseDiscreteStateSpace<T>
where
T: ComplexField,
T::Real: Float,
{
pub fn new(
a: SparseColMat<usize, 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::zero() {
return Err(StateSpaceError::InvalidSampleTime);
}
Ok(Self {
a,
b,
c,
d,
domain: DiscreteTime::new(sample_time),
})
}
pub fn with_zero_feedthrough(
a: SparseColMat<usize, 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> SparseDiscreteStateSpace<T>
where
T: CompensatedField,
T::Real: Float,
{
pub fn controllability_gramian(
&self,
shifts: &ShiftStrategy<T>,
params: LyapunovParams<T::Real>,
) -> Result<LowRankLyapunovSolve<T>, SteinError> {
controllability_gramian_discrete_low_rank(self.a.as_ref(), self.b.as_ref(), shifts, params)
}
pub fn observability_gramian(
&self,
shifts: &ShiftStrategy<T>,
params: LyapunovParams<T::Real>,
) -> Result<LowRankLyapunovSolve<T>, SteinError> {
observability_gramian_discrete_low_rank(self.a.as_ref(), self.c.as_ref(), shifts, params)
}
pub fn balanced_truncation(
&self,
shifts: &ShiftStrategy<T>,
gramian_params: LyapunovParams<T::Real>,
params: &BalancedParams<T::Real>,
) -> Result<BalancedTruncationResult<T, DiscreteTime<T::Real>>, BalancedError<T::Real>> {
balanced_truncation_discrete_low_rank(
self.a.as_ref(),
self.b.as_ref(),
self.c.as_ref(),
self.d.as_ref(),
self.sample_time(),
shifts,
gramian_params,
params,
)
}
}
#[cfg(test)]
mod tests {
use super::{SparseContinuousStateSpace, SparseDiscreteStateSpace};
use crate::control::lti::state_space::StateSpaceError;
use alloc::vec::Vec;
use faer::Mat;
use faer::sparse::{SparseColMat, Triplet};
fn diagonal_csc(diag: &[f64]) -> SparseColMat<usize, f64> {
let triplets = diag
.iter()
.enumerate()
.map(|(i, &value)| Triplet::new(i, i, value))
.collect::<Vec<_>>();
SparseColMat::try_new_from_triplets(diag.len(), diag.len(), &triplets).unwrap()
}
#[test]
fn sparse_continuous_constructor_rejects_bad_dimensions() {
let a = diagonal_csc(&[-1.0, -2.0]);
let b = Mat::<f64>::zeros(3, 1);
let c = Mat::<f64>::zeros(1, 2);
let d = Mat::<f64>::zeros(1, 1);
let err = SparseContinuousStateSpace::new(a, b, c, d).unwrap_err();
assert!(matches!(
err,
StateSpaceError::DimensionMismatch { which: "b", .. }
));
}
#[test]
fn sparse_discrete_constructor_rejects_invalid_sample_time() {
let a = diagonal_csc(&[0.5]);
let b = Mat::<f64>::zeros(1, 1);
let c = Mat::<f64>::zeros(1, 1);
let d = Mat::<f64>::zeros(1, 1);
let err = SparseDiscreteStateSpace::new(a, b, c, d, 0.0).unwrap_err();
assert_eq!(err, StateSpaceError::InvalidSampleTime);
}
#[test]
fn sparse_zero_feedthrough_constructor_sizes_d_correctly() {
let a = diagonal_csc(&[-1.0, -2.0]);
let b = Mat::<f64>::zeros(2, 3);
let c = Mat::<f64>::zeros(4, 2);
let sys = SparseContinuousStateSpace::with_zero_feedthrough(a, b, c).unwrap();
assert_eq!(sys.d().nrows(), 4);
assert_eq!(sys.d().ncols(), 3);
}
}