use burn::prelude::ElementConversion;
use burn::tensor::{Tensor, backend::Backend};
#[derive(Debug, thiserror::Error)]
pub enum ShapingError {
#[error("shaping transform requires f32 tensor data")]
NonFloatData,
}
#[must_use]
pub fn z_score<B: Backend>(fitness: Tensor<B, 1>) -> Tensor<B, 1> {
let mean = fitness.clone().mean().into_scalar().elem::<f32>();
let n = fitness.dims()[0];
#[allow(clippy::cast_precision_loss)]
let n_f = n.max(1) as f32;
let centered = fitness - mean;
let var = centered
.clone()
.powf_scalar(2.0)
.sum()
.into_scalar()
.elem::<f32>()
/ n_f;
let std = var.sqrt().max(1e-8);
centered / std
}
pub fn centered_rank<B: Backend>(
fitness: Tensor<B, 1>,
device: &<B as burn::tensor::backend::BackendTypes>::Device,
) -> Result<Tensor<B, 1>, ShapingError> {
let raw = fitness
.into_data()
.into_vec::<f32>()
.map_err(|_| ShapingError::NonFloatData)?;
let n = raw.len();
if n == 0 {
return Ok(Tensor::<B, 1>::from_floats([0.0f32; 0], device));
}
let data: Vec<f32> = raw
.iter()
.map(|&f| crate::fitness::sanitize_fitness(f))
.collect();
let mut indices: Vec<usize> = (0..n).collect();
indices.sort_by(|&i, &j| data[i].total_cmp(&data[j]));
#[allow(clippy::cast_precision_loss)]
let n_f = (n - 1).max(1) as f32;
let mut ranks = vec![0.0_f32; n];
for (rank, &idx) in indices.iter().enumerate() {
#[allow(clippy::cast_precision_loss)]
let r = rank as f32 / n_f - 0.5;
ranks[idx] = r;
}
Ok(Tensor::<B, 1>::from_floats(ranks.as_slice(), device))
}
#[cfg(test)]
mod tests {
use super::*;
use burn::backend::Flex;
type TestBackend = Flex;
#[test]
#[allow(clippy::cast_precision_loss)]
fn z_score_zero_mean_unit_std() {
let device = Default::default();
let t = Tensor::<TestBackend, 1>::from_floats([1.0f32, 2.0, 3.0, 4.0, 5.0], &device);
let z = z_score(t);
let values = z
.into_data()
.into_vec::<f32>()
.expect("shaped tensor host-read of a tensor this test just built");
let mean: f32 = values.iter().sum::<f32>() / values.len() as f32;
approx::assert_relative_eq!(mean, 0.0, epsilon = 1e-5);
}
#[test]
fn centered_rank_spans_half_interval() {
let device = Default::default();
let t = Tensor::<TestBackend, 1>::from_floats([10.0f32, 20.0, 30.0, 40.0], &device);
let r = centered_rank(t, &device).unwrap();
let values = r
.into_data()
.into_vec::<f32>()
.expect("shaped tensor host-read of a tensor this test just built");
approx::assert_relative_eq!(values[0], -0.5, epsilon = 1e-6);
approx::assert_relative_eq!(values[3], 0.5, epsilon = 1e-6);
}
#[test]
fn centered_rank_preserves_order() {
let device = Default::default();
let t = Tensor::<TestBackend, 1>::from_floats([3.0f32, 1.0, 2.0], &device);
let r = centered_rank(t, &device).unwrap();
let values = r
.into_data()
.into_vec::<f32>()
.expect("shaped tensor host-read of a tensor this test just built");
approx::assert_relative_eq!(values[1], -0.5, epsilon = 1e-6);
approx::assert_relative_eq!(values[2], 0.0, epsilon = 1e-6);
approx::assert_relative_eq!(values[0], 0.5, epsilon = 1e-6);
}
#[test]
fn centered_rank_empty_is_ok() {
let device = Default::default();
let t = Tensor::<TestBackend, 1>::from_floats([0.0f32; 0], &device);
let r = centered_rank(t, &device).expect("empty input is not an error");
assert_eq!(r.dims()[0], 0);
}
}