use std::ops::AddAssign;
use nalgebra::{ComplexField, RealField, Scalar};
use num_traits::Float;
use thiserror::Error;
use crate::{Discrete, Polynomial, SimulationError, TransferFunction, TransferFunctionWithDelay};
use crate::system_identification::arx::Arx;
mod check;
mod coherence;
mod information_criteria;
mod cross_correlation;
mod frequency_response;
mod line_test;
mod spectra;
mod whiteness;
pub use check::{Check, CheckKind, CheckResult, Report};
pub use coherence::CoherenceTest;
pub use cross_correlation::CrossCorrelation;
pub use frequency_response::FrequencyResponseComparison;
pub use line_test::LineTest;
pub use whiteness::WhitenessTest;
#[derive(Clone, Debug, Error, PartialEq)]
pub enum ValidationError {
#[error("input and output lengths differ: {input} vs {output}")]
LengthMismatch { input: usize, output: usize },
#[error("improper model: numerator degree {numerator} > denominator degree {denominator}")]
Improper { numerator: usize, denominator: usize },
#[error("zero denominator")]
ZeroDenominator,
#[error("{periods} whole period(s) evaluated; at least 2 are needed")]
TooFewPeriods { periods: usize },
#[error("line {line} is not between 1 and period / 2 = {half}")]
InvalidLine { line: usize, half: usize },
#[error("{segments} segment(s) of {segment_len} samples evaluated; at least 2 are needed")]
TooFewSegments { segments: usize, segment_len: usize },
#[error(transparent)]
Simulation(#[from] SimulationError),
}
#[derive(Clone, Debug)]
pub struct Validation<T> {
pub input: Vec<T>,
pub simulated: Vec<T>,
pub residual: Vec<T>,
pub start: usize,
}
impl<T: Float> Validation<T> {
pub fn continuous(model: impl Into<TransferFunctionWithDelay<T>>, ts: T, u: &[T], y: &[T]) -> Result<Self, ValidationError>
where
T: AddAssign + ComplexField + RealField,
{
if u.len() != y.len() {
return Err(ValidationError::LengthMismatch { input: u.len(), output: y.len() });
}
let simulated = model.into().simulate(ts, u)?;
Self::new(u, simulated, y)
}
pub fn one_step_prediction(arx: &Arx<T>, u: &[T], y: &[T]) -> Result<Self, ValidationError>
where
T: AddAssign + Scalar,
{
if u.len() != y.len() {
return Err(ValidationError::LengthMismatch { input: u.len(), output: y.len() });
}
let mut model = arx.clone();
model.clear_history();
let predicted = (0..u.len())
.map(|k| {
model.push(u[k], if k > 0 { y[k - 1] } else { T::zero() });
model.predict()
})
.collect();
Self::new(u, predicted, y)
}
pub fn discrete(model: &TransferFunction<T, Discrete>, u: &[T], y: &[T]) -> Result<Self, ValidationError> {
let (numer, denom) = monic(&model.numerator, &model.denominator)?;
let r = denom.len() - numer.len();
let mut simulated: Vec<T> = Vec::with_capacity(u.len());
for k in 0..u.len() {
let mut yk = T::zero();
for (i, &c) in numer.iter().enumerate() {
if k >= r + i {
yk = yk + c * u[k - r - i];
}
}
for (i, &c) in denom.iter().enumerate().skip(1) {
if k >= i {
yk = yk - c * simulated[k - i];
}
}
simulated.push(yk);
}
Self::new(u, simulated, y)
}
fn new(u: &[T], simulated: Vec<T>, y: &[T]) -> Result<Self, ValidationError> {
if u.len() != y.len() {
return Err(ValidationError::LengthMismatch { input: u.len(), output: y.len() });
}
let residual = y.iter().zip(&simulated).map(|(&a, &b)| a - b).collect();
Ok(Self { input: u.to_vec(), simulated, residual, start: 0 })
}
pub fn evaluated_from(mut self, start: usize) -> Self {
self.start = start;
self
}
pub fn evaluated(&self) -> &[T] {
&self.residual[self.start.min(self.residual.len())..]
}
pub fn samples(&self) -> usize {
self.evaluated().len()
}
pub fn mse(&self) -> T {
let e = self.evaluated();
e.iter().fold(T::zero(), |acc, &v| acc + v * v) / T::from(e.len()).unwrap()
}
}
fn monic<T: Float>(numerator: &Polynomial<T>, denominator: &Polynomial<T>) -> Result<(Vec<T>, Vec<T>), ValidationError> {
let trim = |p: &Polynomial<T>| p.iter().copied().skip_while(|c| c.is_zero()).collect::<Vec<T>>();
let (numer, denom) = (trim(numerator), trim(denominator));
let Some(&lead) = denom.first() else {
return Err(ValidationError::ZeroDenominator);
};
if numer.len() > denom.len() {
return Err(ValidationError::Improper { numerator: numer.len() - 1, denominator: denom.len() - 1 });
}
let numer = if numer.is_empty() { vec![T::zero()] } else { numer };
Ok((numer.iter().map(|&c| c / lead).collect(), denom.iter().map(|&c| c / lead).collect()))
}