use super::error::RealizationError;
use super::hankel::{BlockHankel, ShiftedBlockHankelPair};
use crate::control::lti::{DiscreteStateSpace, SparseDiscreteStateSpace};
use crate::sparse::compensated::CompensatedField;
use alloc::vec::Vec;
use faer::{Mat, MatRef};
use faer_traits::RealField;
use num_traits::Float;
#[derive(Clone, Debug, PartialEq)]
pub struct MarkovSequence<T> {
noutputs: usize,
ninputs: usize,
blocks: Vec<Mat<T>>,
}
impl<T> MarkovSequence<T> {
#[must_use]
pub fn empty(noutputs: usize, ninputs: usize) -> Self {
Self {
noutputs,
ninputs,
blocks: Vec::new(),
}
}
pub fn from_blocks(blocks: Vec<Mat<T>>) -> Result<Self, RealizationError> {
let Some(first) = blocks.first() else {
return Err(RealizationError::EmptySequence);
};
let noutputs = first.nrows();
let ninputs = first.ncols();
for (index, block) in blocks.iter().enumerate().skip(1) {
if block.nrows() != noutputs || block.ncols() != ninputs {
return Err(RealizationError::InconsistentBlockShape {
index,
expected_nrows: noutputs,
expected_ncols: ninputs,
actual_nrows: block.nrows(),
actual_ncols: block.ncols(),
});
}
}
Ok(Self {
noutputs,
ninputs,
blocks,
})
}
#[must_use]
pub fn len(&self) -> usize {
self.blocks.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.blocks.is_empty()
}
#[must_use]
pub fn ninputs(&self) -> usize {
self.ninputs
}
#[must_use]
pub fn noutputs(&self) -> usize {
self.noutputs
}
#[must_use]
pub fn block_shape(&self) -> (usize, usize) {
(self.noutputs, self.ninputs)
}
#[must_use]
pub fn block(&self, index: usize) -> MatRef<'_, T> {
self.blocks[index].as_ref()
}
#[must_use]
pub fn get_block(&self, index: usize) -> Option<MatRef<'_, T>> {
self.blocks.get(index).map(Mat::as_ref)
}
#[must_use]
pub fn blocks(&self) -> &[Mat<T>] {
&self.blocks
}
#[must_use]
pub fn into_blocks(self) -> Vec<Mat<T>> {
self.blocks
}
pub fn block_hankel(
&self,
start_index: usize,
row_blocks: usize,
col_blocks: usize,
) -> Result<BlockHankel<T>, RealizationError>
where
T: Copy,
{
BlockHankel::from_markov(self, start_index, row_blocks, col_blocks)
}
pub fn shifted_hankel_pair(
&self,
row_blocks: usize,
col_blocks: usize,
) -> Result<ShiftedBlockHankelPair<T>, RealizationError>
where
T: Copy,
{
ShiftedBlockHankelPair::from_markov(self, row_blocks, col_blocks)
}
}
impl<T> DiscreteStateSpace<T>
where
T: CompensatedField,
T::Real: Float + RealField,
{
pub fn markov_parameters(&self, n_blocks: usize) -> MarkovSequence<T> {
if n_blocks == 0 {
return MarkovSequence::empty(self.noutputs(), self.ninputs());
}
MarkovSequence::from_blocks(self.impulse_response(n_blocks).values)
.expect("discrete impulse-response blocks should always be shape-consistent")
}
}
impl<T> SparseDiscreteStateSpace<T>
where
T: CompensatedField,
T::Real: Float + RealField,
{
pub fn markov_parameters(&self, n_blocks: usize) -> MarkovSequence<T> {
if n_blocks == 0 {
return MarkovSequence::empty(self.noutputs(), self.ninputs());
}
MarkovSequence::from_blocks(self.impulse_response(n_blocks).values)
.expect("sparse discrete impulse-response blocks should always be shape-consistent")
}
}
#[cfg(test)]
mod tests {
use super::MarkovSequence;
use crate::control::lti::state_space::{DiscreteStateSpace, SparseDiscreteStateSpace};
use faer::Mat;
use faer::MatRef;
use faer::sparse::{SparseColMat, Triplet};
fn assert_mat_eq(lhs: MatRef<'_, f64>, rhs: MatRef<'_, f64>) {
assert_eq!(lhs.nrows(), rhs.nrows());
assert_eq!(lhs.ncols(), rhs.ncols());
for row in 0..lhs.nrows() {
for col in 0..lhs.ncols() {
assert_eq!(lhs[(row, col)], rhs[(row, col)]);
}
}
}
fn scalar_system() -> DiscreteStateSpace<f64> {
DiscreteStateSpace::new(
Mat::from_fn(1, 1, |_, _| 2.0),
Mat::from_fn(1, 1, |_, _| 3.0),
Mat::from_fn(1, 1, |_, _| 5.0),
Mat::from_fn(1, 1, |_, _| 7.0),
1.0,
)
.unwrap()
}
#[test]
fn markov_sequence_validates_block_shapes() {
let err = MarkovSequence::from_blocks(vec![
Mat::from_fn(2, 1, |row, _| row as f64),
Mat::from_fn(1, 1, |_, _| 1.0),
])
.unwrap_err();
assert!(matches!(
err,
crate::control::realization::RealizationError::InconsistentBlockShape {
index: 1,
expected_nrows: 2,
expected_ncols: 1,
actual_nrows: 1,
actual_ncols: 1,
}
));
}
#[test]
fn dense_markov_parameters_match_scalar_closed_form() {
let seq = scalar_system().markov_parameters(4);
let expected = [7.0, 15.0, 30.0, 60.0];
assert_eq!(seq.len(), expected.len());
for (index, &value) in expected.iter().enumerate() {
assert_eq!(seq.block(index)[(0, 0)], value);
}
}
#[test]
fn dense_markov_parameters_match_small_mimo_reference() {
let sys = DiscreteStateSpace::new(
Mat::from_fn(2, 2, |row, col| match (row, col) {
(0, 0) => 2.0,
(1, 1) => 3.0,
_ => 0.0,
}),
Mat::from_fn(2, 2, |row, col| if row == col { 1.0 } else { 0.0 }),
Mat::from_fn(2, 2, |row, col| match (row, col) {
(0, 0) => 4.0,
(0, 1) => 5.0,
(1, 0) => 6.0,
(1, 1) => 7.0,
_ => unreachable!(),
}),
Mat::from_fn(2, 2, |row, col| match (row, col) {
(0, 0) => 8.0,
(0, 1) => 9.0,
(1, 0) => 10.0,
(1, 1) => 11.0,
_ => unreachable!(),
}),
0.5,
)
.unwrap();
let seq = sys.markov_parameters(3);
assert_mat_eq(seq.block(0), sys.d());
assert_mat_eq(seq.block(1), sys.c());
let expected_h2 = Mat::from_fn(2, 2, |row, col| match (row, col) {
(0, 0) => 8.0,
(0, 1) => 15.0,
(1, 0) => 12.0,
(1, 1) => 21.0,
_ => unreachable!(),
});
assert_mat_eq(seq.block(2), expected_h2.as_ref());
}
#[test]
fn sparse_markov_parameters_match_dense_reference() {
let dense = scalar_system();
let sparse_a =
SparseColMat::<usize, f64>::try_new_from_triplets(1, 1, &[Triplet::new(0, 0, 2.0)])
.unwrap();
let sparse = SparseDiscreteStateSpace::new(
sparse_a,
Mat::from_fn(1, 1, |_, _| 3.0),
Mat::from_fn(1, 1, |_, _| 5.0),
Mat::from_fn(1, 1, |_, _| 7.0),
1.0,
)
.unwrap();
let dense_seq = dense.markov_parameters(5);
let sparse_seq = sparse.markov_parameters(5);
assert_eq!(dense_seq, sparse_seq);
}
}