#[derive(Debug, Clone)]
pub struct ScalarField2D {
pub rows: usize,
pub cols: usize,
pub data: Vec<f32>,
pub min: f32,
pub max: f32,
pub nan_value: Option<f32>,
pub generation: u64,
pub value_generation: u64,
}
impl ScalarField2D {
pub fn from_data(rows: usize, cols: usize, data: Vec<f32>) -> Self {
assert_eq!(
data.len(),
rows * cols,
"data length must equal rows * cols"
);
let (min, max) = min_max(&data, None);
Self {
rows,
cols,
data,
min,
max,
nan_value: None,
generation: 0,
value_generation: 0,
}
}
pub fn from_data_with_range(
rows: usize,
cols: usize,
data: Vec<f32>,
min: f32,
max: f32,
) -> Self {
assert_eq!(
data.len(),
rows * cols,
"data length must equal rows * cols"
);
Self {
rows,
cols,
data,
min,
max,
nan_value: None,
generation: 0,
value_generation: 0,
}
}
pub fn sample(&self, row: usize, col: usize) -> Option<f32> {
if row >= self.rows || col >= self.cols {
return None;
}
let v = self.data[row * self.cols + col];
if let Some(nan) = self.nan_value {
if (v - nan).abs() < f32::EPSILON {
return None;
}
}
if v.is_nan() {
return None;
}
Some(v)
}
pub fn normalized(&self, row: usize, col: usize) -> Option<f32> {
let v = self.sample(row, col)?;
let range = self.max - self.min;
if range.abs() < f32::EPSILON {
return Some(0.5);
}
Some(((v - self.min) / range).clamp(0.0, 1.0))
}
pub fn update_values(&mut self, new_data: Vec<f32>) {
assert_eq!(
new_data.len(),
self.rows * self.cols,
"new data length must match existing topology"
);
let (min, max) = min_max(&new_data, self.nan_value);
self.data = new_data;
self.min = min;
self.max = max;
self.value_generation = self.value_generation.wrapping_add(1);
}
}
fn min_max(data: &[f32], nan_value: Option<f32>) -> (f32, f32) {
let mut lo = f32::INFINITY;
let mut hi = f32::NEG_INFINITY;
for &v in data {
if v.is_nan() {
continue;
}
if let Some(nan) = nan_value {
if (v - nan).abs() < f32::EPSILON {
continue;
}
}
lo = lo.min(v);
hi = hi.max(v);
}
if lo > hi {
(0.0, 0.0)
} else {
(lo, hi)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_data_computes_min_max() {
let field = ScalarField2D::from_data(2, 3, vec![1.0, 5.0, 3.0, 2.0, 4.0, 0.0]);
assert!((field.min - 0.0).abs() < 1e-6);
assert!((field.max - 5.0).abs() < 1e-6);
}
#[test]
fn sample_basic() {
let field = ScalarField2D::from_data(2, 2, vec![10.0, 20.0, 30.0, 40.0]);
assert_eq!(field.sample(0, 0), Some(10.0));
assert_eq!(field.sample(1, 1), Some(40.0));
assert_eq!(field.sample(2, 0), None);
}
#[test]
fn sample_nan_value() {
let mut field = ScalarField2D::from_data(1, 3, vec![1.0, -9999.0, 3.0]);
field.nan_value = Some(-9999.0);
assert_eq!(field.sample(0, 0), Some(1.0));
assert_eq!(field.sample(0, 1), None);
assert_eq!(field.sample(0, 2), Some(3.0));
}
#[test]
fn normalized_range() {
let field = ScalarField2D::from_data(1, 3, vec![0.0, 50.0, 100.0]);
assert!((field.normalized(0, 0).unwrap() - 0.0).abs() < 1e-6);
assert!((field.normalized(0, 1).unwrap() - 0.5).abs() < 1e-6);
assert!((field.normalized(0, 2).unwrap() - 1.0).abs() < 1e-6);
}
#[test]
fn normalized_constant_field() {
let field = ScalarField2D::from_data(1, 2, vec![5.0, 5.0]);
assert!((field.normalized(0, 0).unwrap() - 0.5).abs() < 1e-6);
}
#[test]
fn update_values_bumps_generation() {
let mut field = ScalarField2D::from_data(1, 2, vec![1.0, 2.0]);
assert_eq!(field.value_generation, 0);
field.update_values(vec![3.0, 4.0]);
assert_eq!(field.value_generation, 1);
assert!((field.min - 3.0).abs() < 1e-6);
assert!((field.max - 4.0).abs() < 1e-6);
}
#[test]
fn normalized_with_nan() {
let field = ScalarField2D::from_data(1, 3, vec![0.0, f32::NAN, 100.0]);
assert!(field.normalized(0, 1).is_none());
}
}