use std::collections::HashMap;
use crate::error::{Error, Result};
use crate::geometry::{Geometry, Topology};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Scope {
Points,
Cells,
}
impl std::fmt::Display for Scope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Scope::Points => "points",
Scope::Cells => "cells",
})
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Method {
Average,
Majority,
Minimum,
Maximum,
}
impl Method {
pub fn name(self) -> &'static str {
match self {
Method::Average => "average",
Method::Majority => "majority",
Method::Minimum => "minimum",
Method::Maximum => "maximum",
}
}
pub fn all() -> [Method; 4] {
[
Method::Average,
Method::Majority,
Method::Minimum,
Method::Maximum,
]
}
pub fn parse(name: &str) -> Result<Method> {
Method::all()
.into_iter()
.find(|method| method.name().eq_ignore_ascii_case(name.trim()))
.ok_or_else(|| {
Error::unsupported(
"a transfer method",
format!(
"`{name}` is not one of {}",
Method::all().map(|m| m.name()).join_with(", ")
),
)
})
}
fn reduce(self, gathered: &[f64]) -> f64 {
if gathered.is_empty() {
return f64::NAN;
}
match self {
Method::Average => gathered.iter().sum::<f64>() / gathered.len() as f64,
Method::Minimum => gathered.iter().copied().fold(f64::INFINITY, f64::min),
Method::Maximum => gathered.iter().copied().fold(f64::NEG_INFINITY, f64::max),
Method::Majority => {
let mut counts: HashMap<u64, (usize, f64)> = HashMap::new();
for value in gathered {
let entry = counts.entry(value.to_bits()).or_insert((0, *value));
entry.0 += 1;
}
counts
.values()
.copied()
.reduce(|best, next| match next.0.cmp(&best.0) {
std::cmp::Ordering::Greater => next,
std::cmp::Ordering::Equal if next.1 < best.1 => next,
_ => best,
})
.map(|(_, value)| value)
.unwrap_or(f64::NAN)
}
}
}
}
trait JoinWith {
fn join_with(self, separator: &str) -> String;
}
impl<const N: usize> JoinWith for [&'static str; N] {
fn join_with(self, separator: &str) -> String {
self.join(separator)
}
}
pub fn cell_nodes(geometry: &Geometry) -> Vec<Vec<u32>> {
let mut out = Vec::with_capacity(geometry.cell_count());
for patch in &geometry.patches {
let base = patch.node_offset as u32;
let shift = |nodes: &[u32]| nodes.iter().map(|n| n + base).collect::<Vec<u32>>();
match &patch.topology {
Topology::Points => {}
Topology::Triangles(triangles) => out.extend(triangles.iter().map(|t| shift(t))),
Topology::Quads(quads) => out.extend(quads.iter().map(|q| shift(q))),
Topology::Polygons(rings) => out.extend(rings.iter().map(|r| shift(r))),
Topology::Segments(pairs) => out.extend(pairs.iter().map(|p| shift(p))),
Topology::Hexahedra { cells, .. } => out.extend(cells.iter().map(|c| shift(c))),
Topology::Polylines(runs) => {
let mut start = 0u32;
for length in runs {
out.push((start..start + length).map(|n| n + base).collect());
start += length;
}
}
Topology::Grid { fastest, slowest } => {
for row in 0..slowest.saturating_sub(1) {
for column in 0..fastest.saturating_sub(1) {
let corner = (row * fastest + column) as u32;
let next = (row * fastest + column + fastest) as u32;
out.push(vec![
corner + base,
corner + 1 + base,
next + 1 + base,
next + base,
]);
}
}
}
Topology::Polyhedra(cells) => {
for cell in 0..cells.cell_count() {
let mut nodes: Vec<u32> = cells
.cell(cell)
.flat_map(|(face, _)| cells.face(face).iter().map(|n| n + base))
.collect();
nodes.sort_unstable();
nodes.dedup();
out.push(nodes);
}
}
}
}
out
}
pub fn transfer(
values: &[f64],
components: usize,
from: Scope,
to: Scope,
method: Method,
geometry: &Geometry,
) -> Result<Vec<f64>> {
let components = components.max(1);
let point_count = geometry.point_count();
let cell_count = geometry.cell_count();
let expected = match from {
Scope::Points => point_count,
Scope::Cells => cell_count,
};
if values.len() != expected * components {
return Err(Error::malformed(
"transfer",
format!(
"{} value(s) for {expected} {from} × {components} component(s); \
a transfer cannot invent the difference",
values.len()
),
));
}
if from == to {
return Ok(values.to_vec());
}
if cell_count == 0 {
return Err(Error::unsupported(
"transferring between points and cells",
"this shape has no cells: loose points have nothing to average over".to_string(),
));
}
let map = cell_nodes(geometry);
let mut gathered: Vec<f64> = Vec::new();
match to {
Scope::Cells => {
let mut out = Vec::with_capacity(cell_count * components);
for nodes in &map {
for component in 0..components {
gathered.clear();
gathered.extend(
nodes
.iter()
.filter_map(|node| values.get(*node as usize * components + component))
.copied()
.filter(|v| !v.is_nan()),
);
out.push(method.reduce(&gathered));
}
}
Ok(out)
}
Scope::Points => {
let mut touching: Vec<Vec<usize>> = vec![Vec::new(); point_count];
for (cell, nodes) in map.iter().enumerate() {
for node in nodes {
if let Some(slot) = touching.get_mut(*node as usize) {
slot.push(cell);
}
}
}
let mut out = Vec::with_capacity(point_count * components);
for cells in &touching {
for component in 0..components {
gathered.clear();
gathered.extend(
cells
.iter()
.filter_map(|cell| values.get(cell * components + component))
.copied()
.filter(|v| !v.is_nan()),
);
out.push(method.reduce(&gathered));
}
}
Ok(out)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::Patch;
use crate::points::Point;
fn grid(fastest: usize, slowest: usize) -> Geometry {
Geometry {
kind: "test",
patches: vec![Patch {
index: 0,
node_offset: 0,
points: (0..fastest * slowest)
.map(|i| -> Point { [(i % fastest) as f64, (i / fastest) as f64, 0.0] })
.collect(),
topology: Topology::Grid { fastest, slowest },
closed: Vec::new(),
}],
}
}
#[test]
fn a_grid_cell_has_its_four_corners() {
let map = cell_nodes(&grid(3, 2));
assert_eq!(map.len(), 2);
assert_eq!(map[0], vec![0, 1, 4, 3]);
assert_eq!(map[1], vec![1, 2, 5, 4]);
}
#[test]
fn a_patch_offset_is_applied_to_cells() {
let mut geometry = grid(2, 2);
geometry.patches.push(Patch {
index: 1,
node_offset: 4,
points: (0..4).map(|_| -> Point { [0.0, 0.0, 0.0] }).collect(),
topology: Topology::Triangles(vec![[0, 1, 2]]),
closed: Vec::new(),
});
let map = cell_nodes(&geometry);
assert_eq!(map.len(), 2);
assert_eq!(map[1], vec![4, 5, 6], "the second patch counts from its offset");
}
#[test]
fn a_measurement_averages_onto_the_cells() {
let geometry = grid(3, 2);
let values: Vec<f64> = (0..6).map(|i| i as f64).collect();
let cells = transfer(
&values,
1,
Scope::Points,
Scope::Cells,
Method::Average,
&geometry,
)
.unwrap();
assert_eq!(cells, vec![2.0, 3.0]);
}
#[test]
fn a_category_takes_the_majority_and_never_the_mean() {
let geometry = grid(3, 2);
let values = vec![0.0, 0.0, 9.0, 0.0, 3.0, 9.0];
let mean = transfer(
&values,
1,
Scope::Points,
Scope::Cells,
Method::Average,
&geometry,
)
.unwrap();
let vote = transfer(
&values,
1,
Scope::Points,
Scope::Cells,
Method::Majority,
&geometry,
)
.unwrap();
assert_eq!(mean[0], 0.75, "the mean of 0, 0, 3, 0 - not one of the values");
assert_eq!(vote[0], 0.0, "the value three of the four corners hold");
assert_ne!(mean[0], vote[0]);
}
#[test]
fn a_tie_goes_to_the_smallest_value() {
let geometry = grid(2, 2);
let values = vec![7.0, 7.0, 2.0, 2.0];
let vote = transfer(
&values,
1,
Scope::Points,
Scope::Cells,
Method::Majority,
&geometry,
)
.unwrap();
assert_eq!(vote, vec![2.0]);
}
#[test]
fn an_undefined_corner_does_not_blank_the_cell() {
let geometry = grid(3, 2);
let values = vec![0.0, f64::NAN, 4.0, 2.0, 6.0, 8.0];
let cells = transfer(
&values,
1,
Scope::Points,
Scope::Cells,
Method::Average,
&geometry,
)
.unwrap();
assert_eq!(cells[0], (0.0 + 2.0 + 6.0) / 3.0);
}
#[test]
fn an_element_with_nothing_defined_around_it_is_not_zero() {
let geometry = grid(2, 2);
let values = vec![f64::NAN; 4];
let cells = transfer(
&values,
1,
Scope::Points,
Scope::Cells,
Method::Average,
&geometry,
)
.unwrap();
assert!(cells[0].is_nan(), "got {}", cells[0]);
}
#[test]
fn the_trip_back_lands_on_the_points() {
let geometry = grid(3, 2);
let cells = vec![10.0, 20.0];
let points = transfer(&cells, 1, Scope::Cells, Scope::Points, Method::Average, &geometry).unwrap();
assert_eq!(points.len(), 6);
assert_eq!(points[0], 10.0, "a corner of one cell");
assert_eq!(points[1], 15.0, "a node between the two cells");
assert_eq!(points[2], 20.0, "a corner of the other cell");
}
#[test]
fn every_component_travels_on_its_own() {
let geometry = grid(3, 2);
let values: Vec<f64> = (0..6).flat_map(|i| [i as f64, 100.0 + i as f64]).collect();
let cells = transfer(
&values,
2,
Scope::Points,
Scope::Cells,
Method::Average,
&geometry,
)
.unwrap();
assert_eq!(cells, vec![2.0, 102.0, 3.0, 103.0]);
}
#[test]
fn a_count_that_does_not_line_up_is_refused_with_both_numbers() {
let geometry = grid(3, 2);
let refused = transfer(
&[1.0, 2.0],
1,
Scope::Points,
Scope::Cells,
Method::Average,
&geometry,
)
.expect_err("2 values for 6 points");
let message = refused.to_string();
assert!(message.contains('2') && message.contains('6'), "{message}");
}
#[test]
fn the_same_scope_is_a_pass_through() {
let geometry = grid(3, 2);
let values: Vec<f64> = (0..6).map(|i| i as f64).collect();
let same = transfer(
&values,
1,
Scope::Points,
Scope::Points,
Method::Average,
&geometry,
)
.unwrap();
assert_eq!(same, values);
}
#[test]
fn an_unknown_method_is_refused_by_listing_the_known_ones() {
let refused = Method::parse("smooth").expect_err("no such method");
let message = refused.to_string();
assert!(message.contains("average"), "{message}");
assert!(message.contains("majority"), "{message}");
}
#[test]
fn a_polyline_run_is_one_cell() {
let geometry = Geometry {
kind: "test",
patches: vec![Patch {
index: 0,
node_offset: 0,
points: (0..5).map(|_| -> Point { [0.0, 0.0, 0.0] }).collect(),
topology: Topology::Polylines(vec![3, 2]),
closed: Vec::new(),
}],
};
let map = cell_nodes(&geometry);
assert_eq!(map, vec![vec![0, 1, 2], vec![3, 4]]);
let points = transfer(
&[1.0, 5.0],
1,
Scope::Cells,
Scope::Points,
Method::Average,
&geometry,
)
.unwrap();
assert_eq!(points, vec![1.0, 1.0, 1.0, 5.0, 5.0]);
}
}