use ndarray::{Array1, Array2, Axis};
use rand::seq::SliceRandom;
use rand::Rng;
use robust_rs_core::error::RobustError;
use robust_rs_core::solver::Control;
#[cfg(feature = "rayon")]
use rayon::prelude::*;
use super::correction::{consistency_factor, hard_reweight};
use super::linalg::{mahalanobis_sq, mean_covariance, spd_inverse_logdet, spd_logdet};
use super::{distances_from, RobustScatter};
use crate::util::substream;
const DEFAULT_SEED: u64 = 0x11CD_5EED;
const INITIAL_CSTEPS: usize = 2;
const N_KEEP: usize = 10;
#[derive(Debug, Clone, Copy)]
pub struct Mcd {
coverage: Option<f64>,
n_subsamples: usize,
reweight: bool,
reweight_quantile: f64,
seed: u64,
control: Control,
}
impl Default for Mcd {
fn default() -> Self {
Self {
coverage: None,
n_subsamples: 500,
reweight: true,
reweight_quantile: 0.975,
seed: DEFAULT_SEED,
control: Control::default(),
}
}
}
impl Mcd {
pub fn new() -> Self {
Self::default()
}
pub fn coverage(mut self, fraction: f64) -> Self {
self.coverage = Some(fraction);
self
}
pub fn n_subsamples(mut self, n: usize) -> Self {
self.n_subsamples = n;
self
}
pub fn reweight(mut self, on: bool) -> Self {
self.reweight = on;
self
}
pub fn seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
pub fn control(mut self, control: Control) -> Self {
self.control = control;
self
}
pub fn fit(&self, x: &Array2<f64>) -> Result<McdFit, RobustError> {
self.fit_from_seed(x, self.seed)
}
pub fn fit_with_rng<G: Rng>(
&self,
x: &Array2<f64>,
rng: &mut G,
) -> Result<McdFit, RobustError> {
self.fit_from_seed(x, rng.random::<u64>())
}
fn fit_from_seed(&self, x: &Array2<f64>, master_seed: u64) -> Result<McdFit, RobustError> {
let (n, p) = x.dim();
if p == 0 {
return Err(RobustError::SingularDesign);
}
if n < p + 2 {
return Err(RobustError::InsufficientData {
needed: p + 2,
got: n,
});
}
let h = match self.coverage {
Some(fraction) => {
if !(fraction.is_finite() && fraction > 0.5 && fraction <= 1.0) {
return Err(RobustError::InvalidTuning { value: fraction });
}
((fraction * n as f64).floor() as usize).clamp(p + 1, n)
}
None => (n + p).div_ceil(2), };
let eval_start = |i: u64| -> Option<Candidate> {
let mut rng = substream(master_seed, i);
let (mu0, cov0) = elemental_start(x, p, &mut rng)?;
let mut state = State { mu: mu0, cov: cov0 };
for _ in 0..INITIAL_CSTEPS {
state = c_step(x, &state, h).ok()?;
}
let logdet = spd_logdet(&state.cov).ok()?;
Some(Candidate { logdet, state })
};
#[cfg(feature = "rayon")]
let mut candidates: Vec<Candidate> = (0..self.n_subsamples as u64)
.into_par_iter()
.filter_map(eval_start)
.collect();
#[cfg(not(feature = "rayon"))]
let mut candidates: Vec<Candidate> = (0..self.n_subsamples as u64)
.filter_map(eval_start)
.collect();
if candidates.is_empty() {
return Err(RobustError::SubsampleFailure);
}
candidates.sort_by(|a, b| a.logdet.total_cmp(&b.logdet));
candidates.truncate(N_KEEP);
let mut best: Option<(f64, State, Vec<usize>)> = None;
for cand in candidates {
if let Ok((state, subset, logdet)) =
concentrate(x, cand.state, h, self.control.max_iter)
{
if best.as_ref().map_or(true, |(bl, _, _)| logdet < *bl) {
best = Some((logdet, state, subset));
}
}
}
let (objective, raw_state, mut support) = best.ok_or(RobustError::SubsampleFailure)?;
support.sort_unstable();
let alpha = h as f64 / n as f64;
let c_raw = consistency_factor(alpha, p as f64);
let raw_location = raw_state.mu.clone();
let raw_scatter = &raw_state.cov * c_raw;
let breakdown_point = (n - h + 1) as f64 / n as f64;
let reweighted = if self.reweight {
hard_reweight(
x,
&raw_location,
&raw_scatter,
self.reweight_quantile,
false,
)?
} else {
None
};
let (location, scatter, distances, weights) = match reweighted {
Some(rw) => (rw.location, rw.scatter, rw.distances, rw.weights),
None => {
let d = distances_from(x, &raw_location, &raw_scatter)?;
(
raw_location.clone(),
raw_scatter.clone(),
d,
Array1::ones(n),
)
}
};
Ok(McdFit {
location,
scatter,
distances,
weights,
raw_location,
raw_scatter,
support,
coverage: h,
objective,
breakdown_point,
})
}
}
#[derive(Clone)]
struct State {
mu: Array1<f64>,
cov: Array2<f64>,
}
struct Candidate {
logdet: f64,
state: State,
}
fn elemental_start<G: Rng>(
x: &Array2<f64>,
p: usize,
rng: &mut G,
) -> Option<(Array1<f64>, Array2<f64>)> {
let n = x.nrows();
let mut idx: Vec<usize> = (0..n).collect();
idx.shuffle(rng);
let mut k = p + 1;
loop {
let sub = &idx[..k];
let xs = x.select(Axis(0), sub);
let (mu, cov) = mean_covariance(&xs);
if spd_logdet(&cov).is_ok() {
return Some((mu, cov));
}
k += 1;
if k > n {
return None; }
}
}
fn c_step(x: &Array2<f64>, state: &State, h: usize) -> Result<State, RobustError> {
let (inv, _logdet) = spd_inverse_logdet(&state.cov)?;
let d2 = mahalanobis_sq(x, &state.mu, &inv);
let subset = h_smallest(&d2, h);
let xs = x.select(Axis(0), &subset);
let (mu, cov) = mean_covariance(&xs);
Ok(State { mu, cov })
}
fn concentrate(
x: &Array2<f64>,
start: State,
h: usize,
max_steps: usize,
) -> Result<(State, Vec<usize>, f64), RobustError> {
let mut state = start;
let mut prev: Option<Vec<usize>> = None;
for _ in 0..max_steps.max(1) {
let (inv, _ld) = spd_inverse_logdet(&state.cov)?;
let d2 = mahalanobis_sq(x, &state.mu, &inv);
let subset = h_smallest(&d2, h);
let xs = x.select(Axis(0), &subset);
let (mu, cov) = mean_covariance(&xs);
let logdet = spd_logdet(&cov)?;
state = State { mu, cov };
if prev.as_ref() == Some(&subset) {
return Ok((state, subset, logdet));
}
prev = Some(subset);
}
let (inv, _ld) = spd_inverse_logdet(&state.cov)?;
let d2 = mahalanobis_sq(x, &state.mu, &inv);
let subset = h_smallest(&d2, h);
let logdet = spd_logdet(&state.cov)?;
Ok((state, subset, logdet))
}
fn h_smallest(v: &Array1<f64>, h: usize) -> Vec<usize> {
let mut idx: Vec<usize> = (0..v.len()).collect();
idx.sort_by(|&a, &b| v[a].total_cmp(&v[b]));
idx.truncate(h);
idx.sort_unstable();
idx
}
#[derive(Debug, Clone)]
pub struct McdFit {
pub location: Array1<f64>,
pub scatter: Array2<f64>,
pub distances: Array1<f64>,
pub weights: Array1<f64>,
pub raw_location: Array1<f64>,
pub raw_scatter: Array2<f64>,
pub support: Vec<usize>,
pub coverage: usize,
pub objective: f64,
pub breakdown_point: f64,
}
impl McdFit {
pub fn location(&self) -> &Array1<f64> {
&self.location
}
pub fn scatter(&self) -> &Array2<f64> {
&self.scatter
}
pub fn support(&self) -> &[usize] {
&self.support
}
pub fn breakdown_point(&self) -> f64 {
self.breakdown_point
}
}
impl RobustScatter for McdFit {
fn location(&self) -> &Array1<f64> {
&self.location
}
fn scatter(&self) -> &Array2<f64> {
&self.scatter
}
fn distances(&self) -> &Array1<f64> {
&self.distances
}
}