use crate::error::Error;
use crate::math::reduction::det_reduce;
use crate::parallel_gates::{
CHEAP_MAP_F64_PARALLEL_THRESHOLD, SCAN_F64_PARALLEL_MIN_ELEMS, SUM_F64_PARALLEL_MIN_ELEMS,
};
use ndarray::{Array, ArrayBase, ArrayViewMut1, Axis, Data, Dimension};
use rayon::iter::ParallelIterator;
use rayon::prelude::IntoParallelRefMutIterator;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StandardizationAxis {
Row,
Column,
Global,
}
impl StandardizationAxis {
fn apply<D>(&self, data: &mut Array<f64, D>, epsilon: f64) -> Result<(), Error>
where
D: Dimension,
{
match self {
StandardizationAxis::Global => standardize_global(data, epsilon),
StandardizationAxis::Row => standardize_lanes(data, 1, epsilon, "Row standardization"),
StandardizationAxis::Column => {
standardize_lanes(data, 2, epsilon, "Column standardization")
}
}
}
}
pub fn standardize<S, D>(
data: &ArrayBase<S, D>,
axis: StandardizationAxis,
epsilon: f64,
) -> Result<Array<f64, D>, Error>
where
S: Data<Elem = f64>,
D: Dimension,
{
if data.is_empty() {
return Err(Error::empty_input("Cannot standardize empty array"));
}
if data.iter().any(|&x| !x.is_finite()) {
return Err(Error::non_finite("input data"));
}
if epsilon <= 0.0 || !epsilon.is_finite() {
return Err(Error::invalid_parameter(
"epsilon",
"Epsilon must be positive and finite",
));
}
let mut result = data.to_owned();
axis.apply(&mut result, epsilon)?;
Ok(result)
}
type WelfordState = (f64, f64, f64);
#[inline]
fn welford_step((count, mean, m2): WelfordState, x: f64) -> WelfordState {
let count = count + 1.0;
let delta = x - mean;
let mean = mean + delta / count;
let m2 = m2 + delta * (x - mean);
(count, mean, m2)
}
#[inline]
fn welford_merge(a: WelfordState, b: WelfordState) -> WelfordState {
let (na, ma, m2a) = a;
let (nb, mb, m2b) = b;
if na == 0.0 {
return b;
}
if nb == 0.0 {
return a;
}
let n = na + nb;
let delta = mb - ma;
let mean = ma + delta * nb / n;
let m2 = m2a + m2b + delta * delta * na * nb / n;
(n, mean, m2)
}
fn standardize_global<D>(data: &mut Array<f64, D>, epsilon: f64) -> Result<(), Error>
where
D: Dimension,
{
let n = data.len() as f64;
if n == 0.0 {
return Err(Error::computation("No values to standardize"));
}
let (_, mean, m2) = match data.as_slice() {
Some(slice) => det_reduce(
slice,
slice.len() >= SUM_F64_PARALLEL_MIN_ELEMS,
|block| {
block
.iter()
.fold((0.0, 0.0, 0.0), |acc, &x| welford_step(acc, x))
},
welford_merge,
(0.0, 0.0, 0.0),
),
_ => data
.iter()
.fold((0.0, 0.0, 0.0), |acc, &x| welford_step(acc, x)),
};
let std_dev = (m2 / n + epsilon * epsilon).sqrt();
if data.len() >= CHEAP_MAP_F64_PARALLEL_THRESHOLD {
data.par_mapv_inplace(|x| (x - mean) / std_dev);
} else {
data.mapv_inplace(|x| (x - mean) / std_dev);
}
Ok(())
}
fn lane_mean_and_std(lane: &ArrayViewMut1<f64>, epsilon: f64) -> (f64, f64) {
let n = lane.len() as f64;
let (_, mean, m2) = lane
.iter()
.fold((0.0, 0.0, 0.0), |acc, &x| welford_step(acc, x));
let std_dev = (m2 / n + epsilon * epsilon).sqrt();
(mean, std_dev)
}
fn standardize_lanes<D>(
data: &mut Array<f64, D>,
axis_from_end: usize,
epsilon: f64,
operation_name: &str,
) -> Result<(), Error>
where
D: Dimension,
{
let ndim = data.ndim();
if ndim < 2 {
return Err(Error::invalid_input(format!(
"{} requires at least 2 dimensions",
operation_name
)));
}
let axis = Axis(ndim - axis_from_end);
let data_len = data.len();
let mut lanes: Vec<ArrayViewMut1<f64>> = data.lanes_mut(axis).into_iter().collect();
let process = |lane: &mut ArrayViewMut1<f64>| {
let (mean, std_dev) = lane_mean_and_std(lane, epsilon);
lane.mapv_inplace(|x| (x - mean) / std_dev);
};
if data_len >= SCAN_F64_PARALLEL_MIN_ELEMS {
lanes.par_iter_mut().for_each(process);
} else {
lanes.iter_mut().for_each(process);
}
Ok(())
}