use std::fmt;
use std::ops::{AddAssign, MulAssign};
use nalgebra::{ComplexField, RealField};
use num_traits::Float;
use rustfft::FftNum;
use super::{identify, identify_with_prefilter, Initialization, Prefilter, SrivcError, SrivcOptions, SrivcResult};
use crate::system_identification::preprocessing::high_pass;
use crate::system_identification::validation::{Check, CheckResult, Report, Validation, ValidationError};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Structure {
pub denominator_order: usize,
pub numerator_order: usize,
pub integrators: usize,
pub input_delay: usize,
}
impl Structure {
pub fn new(denominator_order: usize, numerator_order: usize, integrators: usize, input_delay: usize) -> Self {
Self { denominator_order, numerator_order, integrators, input_delay }
}
pub fn parameters(&self) -> usize {
self.denominator_order + self.numerator_order + 1
}
pub fn grid(
denominator_orders: impl IntoIterator<Item = usize>,
numerator_orders: impl IntoIterator<Item = usize> + Clone,
integrators: impl IntoIterator<Item = usize> + Clone,
input_delays: impl IntoIterator<Item = usize> + Clone,
) -> Vec<Self> {
let mut structures = Vec::new();
for n in denominator_orders {
for m in numerator_orders.clone().into_iter().filter(|&m| m <= n) {
for q in integrators.clone() {
for nk in input_delays.clone() {
structures.push(Self::new(n, m, q, nk));
}
}
}
}
structures
}
}
impl fmt::Display for Structure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "(n, m, q, nk) = ({}, {}, {}, {})", self.denominator_order, self.numerator_order, self.integrators, self.input_delay)
}
}
#[derive(Clone, Debug)]
pub struct SearchOptions<T> {
pub initialization: Initialization<T>,
pub srivc: SrivcOptions<T>,
pub checks: Vec<Check<T>>,
pub confidence: T,
pub evaluated_from: usize,
pub prefilter: Option<T>,
}
impl<T: Float> SearchOptions<T> {
pub fn new(initialization: Initialization<T>, checks: Vec<Check<T>>, confidence: T) -> Self {
Self { initialization, srivc: SrivcOptions::default(), checks, confidence, evaluated_from: 0, prefilter: None }
}
}
#[derive(Clone, Debug)]
pub enum Outcome<T> {
NotIdentified(SrivcError),
Unstable(SrivcResult<T>),
Validated { result: SrivcResult<T>, report: Report<T> },
}
#[derive(Clone, Debug)]
pub struct Candidate<T> {
pub structure: Structure,
pub outcome: Outcome<T>,
}
impl<T: Copy> Candidate<T> {
pub fn result(&self) -> Option<&SrivcResult<T>> {
match &self.outcome {
Outcome::NotIdentified(_) => None,
Outcome::Unstable(result) | Outcome::Validated { result, .. } => Some(result),
}
}
pub fn report(&self) -> Option<&Report<T>> {
match &self.outcome {
Outcome::Validated { report, .. } => Some(report),
_ => None,
}
}
pub fn bic(&self) -> Option<T> {
self.report().and_then(Report::bic)
}
pub fn selectable(&self) -> bool {
matches!(&self.outcome, Outcome::Validated { result, report } if result.converged && report.passed())
}
}
#[derive(Clone, Debug)]
pub struct StructureSearch<T> {
pub candidates: Vec<Candidate<T>>,
pub selected: Option<usize>,
}
impl<T> StructureSearch<T> {
pub fn selected(&self) -> Option<&Candidate<T>> {
self.selected.map(|i| &self.candidates[i])
}
}
pub fn search<T>(
identification: (&[T], &[T]),
validation: (&[T], &[T]),
ts: T,
structures: &[Structure],
options: &SearchOptions<T>,
) -> Result<StructureSearch<T>, ValidationError>
where
T: Float + AddAssign + MulAssign + ComplexField + RealField + FftNum,
{
let (u, y) = identification;
let (u_val, y_val) = match options.prefilter {
Some(cutoff) => {
let order = structures.iter().map(|s| s.integrators).max().unwrap_or(0) + 1;
high_pass(validation.0, validation.1, cutoff / T::from(2.0 * std::f64::consts::PI).unwrap(), ts, order)
}
None => (validation.0.to_vec(), validation.1.to_vec()),
};
let mut candidates = Vec::with_capacity(structures.len());
for &structure in structures {
let srivc = SrivcOptions { input_delay: structure.input_delay, ..options.srivc.clone() };
let (n, m, q) = (structure.denominator_order, structure.numerator_order, structure.integrators);
let identified = match options.prefilter {
Some(cutoff) => Prefilter::new(q, cutoff).and_then(|prefilter| identify_with_prefilter(u, y, ts, n, m, &prefilter, &options.initialization, &srivc)),
None if q == 0 => identify(u, y, ts, n, m, &options.initialization, &srivc),
None => Err(SrivcError::NoPrefilter { integrators: q }),
};
let outcome = match identified {
Err(error) => Outcome::NotIdentified(error),
Ok(result) if result.parameter.iter().any(|v| !Float::is_finite(*v)) => Outcome::Unstable(result),
Ok(result) => {
let validation = Validation::continuous(&result.model, ts, &u_val, &y_val)?.evaluated_from_sample(options.evaluated_from);
if !Float::is_finite(validation.mse()) {
Outcome::Unstable(result)
} else {
let mut checks = vec![Check::InformationCriteria { parameters: structure.parameters() }];
checks.extend(options.checks.iter().cloned());
let report = validation.check(&checks, options.confidence)?;
Outcome::Validated { result, report }
}
}
};
candidates.push(Candidate { structure, outcome });
}
let selected = candidates
.iter()
.enumerate()
.filter(|(_, c)| c.selectable())
.min_by(|(_, a), (_, b)| a.bic().partial_cmp(&b.bic()).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i);
Ok(StructureSearch { candidates, selected })
}
impl<T: Float + fmt::Display> fmt::Display for StructureSearch<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, " n m q nk | BIC | tests | iterations")?;
for (i, c) in self.candidates.iter().enumerate() {
let s = &c.structure;
write!(f, "{:3} {:2} {:2} {:3} | ", s.denominator_order, s.numerator_order, s.integrators, s.input_delay)?;
match &c.outcome {
Outcome::NotIdentified(error) => write!(f, "not identified: {error}")?,
Outcome::Unstable(result) => write!(f, "unstable model ({} iterations)", result.iterations)?,
Outcome::Validated { result, report } => {
let bic = report.bic().map(|b| format!("{b:12.1}")).unwrap_or_else(|| format!("{:>12}", "-"));
let tests: String = report
.results
.iter()
.filter_map(CheckResult::passed)
.map(|p| if p { '+' } else { '-' })
.collect();
write!(f, "{bic} | {tests:5} | {}{}", result.iterations, if result.converged { "" } else { " (not converged)" })?;
if !c.selectable() {
write!(f, " rejected")?;
}
}
}
if self.selected == Some(i) {
write!(f, " <- selected")?;
}
writeln!(f)?;
}
match self.selected() {
Some(c) => write!(f, "selected: {}", c.structure),
None => write!(f, "no structure passes every test"),
}
}
}