use std::fmt;
use std::ops::{AddAssign, MulAssign};
use nalgebra::{ComplexField, RealField};
use num_traits::Float;
use rustfft::FftNum;
use super::{identify, Initialization, SrivcError, SrivcOptions, SrivcResult};
use crate::system_identification::validation::{Check, CheckResult, Report, Validation, ValidationError};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Structure {
pub numerator_order: usize,
pub denominator_order: usize,
pub input_delay: usize,
}
impl Structure {
pub fn new(numerator_order: usize, denominator_order: usize, input_delay: usize) -> Self {
Self { numerator_order, denominator_order, 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,
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 nk in input_delays.clone() {
structures.push(Self::new(m, n, nk));
}
}
}
structures
}
}
impl fmt::Display for Structure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "(n, m, nk) = ({}, {}, {})", self.denominator_order, self.numerator_order, 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,
}
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 }
}
}
#[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) = validation;
let mut candidates = Vec::with_capacity(structures.len());
for &structure in structures {
let srivc = SrivcOptions { input_delay: structure.input_delay, ..options.srivc.clone() };
let outcome = match identify(u, y, ts, structure.numerator_order, structure.denominator_order, &options.initialization, &srivc) {
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(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 nk | BIC | tests | iterations")?;
for (i, c) in self.candidates.iter().enumerate() {
let s = &c.structure;
write!(f, "{:3} {:2} {:3} | ", s.denominator_order, s.numerator_order, 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"),
}
}
}