use ndarray::Array2;
#[derive(Debug, Clone)]
pub struct SoftmaskBuilder<'a> {
x: &'a Array2<f32>,
x_ref: &'a Array2<f32>,
power: f32,
split_zeros: bool,
}
impl SoftmaskBuilder<'_> {
#[must_use]
pub fn power(mut self, v: f32) -> Self {
self.power = v;
self
}
#[must_use]
pub fn split_zeros(mut self, v: bool) -> Self {
self.split_zeros = v;
self
}
pub fn compute(self) -> Array2<f32> {
softmask_impl(self.x, self.x_ref, self.power, self.split_zeros)
}
}
pub fn softmask<'a>(x: &'a Array2<f32>, x_ref: &'a Array2<f32>) -> SoftmaskBuilder<'a> {
SoftmaskBuilder { x, x_ref, power: 1.0, split_zeros: false }
}
fn softmask_impl(
x: &Array2<f32>,
x_ref: &Array2<f32>,
power: f32,
split_zeros: bool,
) -> Array2<f32> {
const EPS: f32 = 1e-16;
Array2::from_shape_fn(x.raw_dim(), |(i, j)| {
let a = x[[i, j]];
let b = x_ref[[i, j]];
if power == f32::INFINITY {
if a > b { 1.0 } else if a < b { 0.0 } else { 0.5 }
} else {
let ap = a.powf(power);
let bp = b.powf(power);
let total = ap + bp;
if total < EPS {
if split_zeros { 0.5 } else { 0.0 }
} else {
ap / total
}
}
})
}
#[derive(Debug, Clone, Copy)]
pub enum FeatureNorm {
L1,
L2,
LInf,
}
#[derive(Debug, Clone)]
pub struct NormalizeBuilder<'a> {
x: &'a Array2<f32>,
norm: FeatureNorm,
axis: usize,
threshold: Option<f32>,
}
impl NormalizeBuilder<'_> {
#[must_use]
pub fn norm(mut self, v: FeatureNorm) -> Self {
self.norm = v;
self
}
#[must_use]
pub fn axis(mut self, v: usize) -> Self {
self.axis = v;
self
}
#[must_use]
pub fn threshold(mut self, v: f32) -> Self {
self.threshold = Some(v);
self
}
pub fn compute(self) -> Array2<f32> {
normalize_impl(self.x, self.norm, self.axis, self.threshold.unwrap_or(1e-10))
}
}
pub fn normalize(x: &Array2<f32>) -> NormalizeBuilder<'_> {
NormalizeBuilder { x, norm: FeatureNorm::L2, axis: 0, threshold: None }
}
fn normalize_impl(x: &Array2<f32>, norm: FeatureNorm, axis: usize, threshold: f32) -> Array2<f32> {
let mut out = x.to_owned();
match axis {
0 => {
for j in 0..x.ncols() {
let col: Vec<f32> = x.column(j).iter().copied().collect();
let n = norm_value(&col, norm);
if n > threshold {
for i in 0..x.nrows() {
out[[i, j]] /= n;
}
}
}
}
_ => {
for i in 0..x.nrows() {
let row: Vec<f32> = x.row(i).iter().copied().collect();
let n = norm_value(&row, norm);
if n > threshold {
for j in 0..x.ncols() {
out[[i, j]] /= n;
}
}
}
}
}
out
}
fn norm_value(v: &[f32], norm: FeatureNorm) -> f32 {
match norm {
FeatureNorm::L1 => v.iter().map(|&x| x.abs()).sum(),
FeatureNorm::L2 => v.iter().map(|&x| x * x).sum::<f32>().sqrt(),
FeatureNorm::LInf => v.iter().map(|&x| x.abs()).fold(0.0_f32, f32::max),
}
}
#[derive(Debug, Clone)]
pub struct SparsifyRowsBuilder<'a> {
x: &'a Array2<f32>,
quantile: f32,
}
impl SparsifyRowsBuilder<'_> {
#[must_use]
pub fn quantile(mut self, v: f32) -> Self {
self.quantile = v;
self
}
pub fn compute(self) -> Array2<f32> {
sparsify_rows_impl(self.x, self.quantile)
}
}
pub fn sparsify_rows(x: &Array2<f32>) -> SparsifyRowsBuilder<'_> {
SparsifyRowsBuilder { x, quantile: 0.01 }
}
fn sparsify_rows_impl(x: &Array2<f32>, quantile: f32) -> Array2<f32> {
let mut out = x.to_owned();
for i in 0..x.nrows() {
let mut vals: Vec<f32> = x.row(i).iter().map(|&v| v.abs()).collect();
if vals.is_empty() {
continue;
}
vals.sort_by(f32::total_cmp);
let idx = ((vals.len() as f32 * quantile.clamp(0.0, 1.0)) as usize)
.min(vals.len().saturating_sub(1));
let threshold = vals[idx];
for j in 0..x.ncols() {
if out[[i, j]].abs() < threshold {
out[[i, j]] = 0.0;
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::{arr2, Array2};
#[test]
fn test_softmask_ratio() {
let x = Array2::from_elem((2, 3), 3.0_f32);
let r = Array2::from_elem((2, 3), 1.0_f32);
let m = softmask_impl(&x, &r, 1.0, false);
assert!((m[[0, 0]] - 0.75).abs() < 1e-5);
}
#[test]
fn test_softmask_split_zeros() {
let x: Array2<f32> = Array2::zeros((2, 2));
let r: Array2<f32> = Array2::zeros((2, 2));
let m_no_split = softmask_impl(&x, &r, 1.0, false);
let m_split = softmask_impl(&x, &r, 1.0, true);
assert_eq!(m_no_split[[0, 0]], 0.0);
assert_eq!(m_split[[0, 0]], 0.5);
}
#[test]
fn test_softmask_hard_mask() {
let x = arr2(&[[2.0_f32, 0.5], [1.0, 3.0]]);
let r = arr2(&[[1.0_f32, 1.5], [1.0, 1.0]]);
let m = softmask_impl(&x, &r, f32::INFINITY, false);
assert_eq!(m[[0, 0]], 1.0); assert_eq!(m[[0, 1]], 0.0); }
#[test]
fn test_normalize_l2_columns() {
let x = arr2(&[[3.0_f32, 0.0], [4.0, 0.0]]);
let n = normalize_impl(&x, FeatureNorm::L2, 0, 1e-10);
assert!((n[[0, 0]] - 0.6).abs() < 1e-5);
assert!((n[[1, 0]] - 0.8).abs() < 1e-5);
assert_eq!(n[[0, 1]], 0.0);
}
#[test]
fn test_normalize_l1_rows() {
let x = arr2(&[[1.0_f32, 2.0, 3.0]]);
let n = normalize_impl(&x, FeatureNorm::L1, 1, 1e-10);
assert!((n[[0, 0]] - 1.0 / 6.0).abs() < 1e-5);
}
#[test]
fn test_normalize_linf() {
let x = arr2(&[[1.0_f32, -4.0, 2.0]]);
let n = normalize_impl(&x, FeatureNorm::LInf, 1, 1e-10);
assert!((n[[0, 1]] + 1.0).abs() < 1e-5); }
#[test]
fn test_sparsify_zeros_smallest() {
let x = arr2(&[[0.001_f32, 1.0, 2.0, 3.0]]);
let s = sparsify_rows_impl(&x, 0.25);
assert_eq!(s[[0, 0]], 0.0, "0.001 is below 25th percentile and should be zeroed");
}
#[test]
fn test_sparsify_zero_quantile_preserves_all() {
let x = Array2::from_shape_fn((3, 5), |(i, j)| (i * j) as f32 + 0.1);
let s = sparsify_rows_impl(&x, 0.0);
for ((i, j), &v) in x.indexed_iter() {
assert_eq!(s[[i, j]], v);
}
}
}