use ndarray::{Array1, Array2};
use robust_rs_core::error::RobustError;
use robust_rs_core::solver::Control;
use super::distances_from;
use super::linalg::{center, spd_inverse_logdet, spd_logdet};
use crate::util::median;
#[derive(Debug, Clone, Default)]
pub struct Tyler {
location: Option<Array1<f64>>,
control: Control,
}
impl Tyler {
pub fn new() -> Self {
Self::default()
}
pub fn location(mut self, loc: Array1<f64>) -> Self {
self.location = Some(loc);
self
}
pub fn control(mut self, control: Control) -> Self {
self.control = control;
self
}
pub fn fit(&self, x: &Array2<f64>) -> Result<TylerFit, RobustError> {
let (n, p) = x.dim();
if p == 0 {
return Err(RobustError::SingularDesign);
}
if n < p + 1 {
return Err(RobustError::InsufficientData {
needed: p + 1,
got: n,
});
}
let loc = match &self.location {
Some(l) => {
if l.len() != p {
return Err(RobustError::DimensionMismatch {
expected: p,
got: l.len(),
});
}
l.clone()
}
None => coordinatewise_median(x),
};
let r = center(x, &loc);
let mut v = Array2::<f64>::eye(p);
let tiny = 1e-12;
let mut converged = false;
for _ in 0..self.control.max_iter {
let (v_inv, _logdet) = spd_inverse_logdet(&v)?;
let mut s = Array2::<f64>::zeros((p, p));
let mut used = 0usize;
for i in 0..n {
let ri = r.row(i);
let vinv_ri = v_inv.dot(&ri);
let d = ri.dot(&vinv_ri); if d <= tiny {
continue; }
used += 1;
for a in 0..p {
let ra = ri[a];
for b in 0..p {
s[[a, b]] += ra * ri[b] / d;
}
}
}
if used <= p {
return Err(RobustError::InsufficientData {
needed: p + 1,
got: used,
});
}
let logdet = spd_logdet(&s)?;
let scale = (logdet / p as f64).exp(); let v_next = s.mapv(|x| x / scale);
let num: f64 = (&v_next - &v).iter().map(|x| x * x).sum();
let den: f64 = v.iter().map(|x| x * x).sum::<f64>().max(1e-30);
v = v_next;
if (num / den).sqrt() <= self.control.tol {
converged = true;
break;
}
}
if !converged {
return Err(RobustError::NonConvergence {
iters: self.control.max_iter,
});
}
let distances = distances_from(x, &loc, &v)?;
Ok(TylerFit {
location: loc,
shape: v,
distances,
})
}
}
#[derive(Debug, Clone)]
pub struct TylerFit {
location: Array1<f64>,
shape: Array2<f64>,
distances: Array1<f64>,
}
impl TylerFit {
pub fn location(&self) -> &Array1<f64> {
&self.location
}
pub fn shape(&self) -> &Array2<f64> {
&self.shape
}
pub fn distances(&self) -> &Array1<f64> {
&self.distances
}
pub fn outliers_assuming_chi2_radial(&self, quantile: f64) -> Vec<bool> {
let p = self.shape.nrows() as f64;
let cut = super::chi2::chi2_quantile(quantile, p).sqrt();
self.distances.iter().map(|&d| d > cut).collect()
}
}
fn coordinatewise_median(x: &Array2<f64>) -> Array1<f64> {
let p = x.ncols();
Array1::from_shape_fn(p, |j| median(&mut x.column(j).to_vec()))
}