use std::collections::{BTreeMap, BTreeSet, HashSet};
use serde_json::{Map as JsonMap, Value as JsonValue};
use crate::types::{Coordinate, EsmFile, IndexSet, VariableType};
pub const DTYPE_FLOAT64: &str = "float64";
pub const DEFAULT_TIME_DIM: &str = "time";
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum OutputError {
#[error(
"scalar variable '{base}' maps to {count} flat indices; a scalar cell key must be unique"
)]
DuplicateScalar { base: String, count: usize },
#[error(
"variable '{base}' has cells of differing dimensionality ({got} vs {expected}); a gridded variable must be rectangular"
)]
RaggedRank {
base: String,
got: usize,
expected: usize,
},
#[error("variable '{base}' has cell index 0 on axis {axis}; cell keys are 1-based")]
ZeroIndex { base: String, axis: usize },
#[error(
"variable '{base}' supplies {got} cell(s) for a {shape:?} grid of {want}; a gridded output variable must be dense"
)]
SparseGrid {
base: String,
shape: Vec<usize>,
got: usize,
want: usize,
},
#[error("variable '{base}' maps two flat slots to grid cell {cell:?}")]
DuplicateCell { base: String, cell: Vec<usize> },
#[error(
"output metadata for '{base}' declares {declared} dim(s) {dims:?}, but the flat state grids it to {actual} axis/axes {shape:?}"
)]
DimCountMismatch {
base: String,
dims: Vec<String>,
declared: usize,
shape: Vec<usize>,
actual: usize,
},
#[error(
"output metadata: '{base}' axis {axis} is index set '{dim}' of size {size}, but the flat state grids that axis to length {len}"
)]
DimLengthMismatch {
base: String,
axis: usize,
dim: String,
size: usize,
len: usize,
},
#[error(
"dimension '{dim}' has length {a} on variable '{first}' but length {b} on variable '{second}'"
)]
DimLengthConflict {
dim: String,
first: String,
a: usize,
second: String,
b: usize,
},
#[error(
"variable '{base}' has a spatial dimension named '{dim}', which collides with the record axis"
)]
TimeDimCollision { base: String, dim: String },
#[error("coordinate '{name}' supplies {got} value(s) but dimension '{name}' has length {want}")]
CoordLengthMismatch {
name: String,
got: usize,
want: usize,
},
#[error(
"requested observed output '{name}' has no slot in the flat state; the runner must evaluate and append it before the plan is derived"
)]
UnknownObserved { name: String },
#[error(
"requested output '{name}' names no variable exactly, and its last segment is shared by {candidates:?}; name one of them in full"
)]
AmbiguousRequest {
name: String,
candidates: Vec<String>,
},
#[error("variable '{base}': {what} buffer holds {got} element(s), need {want}")]
BufferLength {
base: String,
what: &'static str,
got: usize,
want: usize,
},
}
impl OutputError {
#[must_use]
pub fn code(&self) -> Option<&'static str> {
match self {
Self::AmbiguousRequest { .. } => Some(crate::diagnostic::codes::AMBIGUOUS_OUTPUT_NAME),
_ => None,
}
}
}
pub fn parse_cell_key(key: &str) -> Option<(&str, Vec<usize>)> {
let open = key.find('[')?;
if open == 0 {
return None; }
let inner = key.strip_suffix(']')?.get(open + 1..)?;
if inner.is_empty() {
return None;
}
let mut indices = Vec::new();
for part in inner.split(',') {
if part.is_empty() || !part.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
indices.push(part.parse::<usize>().ok()?);
}
Some((&key[..open], indices))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VarGridding {
pub base: String,
pub shape: Vec<usize>,
pub dim_names: Vec<String>,
pub flat_indices: Vec<usize>,
}
impl VarGridding {
#[must_use]
pub fn n_cells(&self) -> usize {
self.flat_indices.len()
}
#[must_use]
pub fn is_scalar(&self) -> bool {
self.shape.is_empty()
}
#[must_use]
pub fn cart(&self, cell: usize) -> Vec<usize> {
let mut out = vec![0usize; self.shape.len()];
let mut rem = cell;
for d in (0..self.shape.len()).rev() {
let n = self.shape[d].max(1);
out[d] = rem % n;
rem /= n;
}
out
}
pub fn scatter(&self, flat_u: &[f64], out: &mut [f64]) -> Result<(), OutputError> {
self.scatter_record(flat_u, out, 0)
}
pub fn scatter_record(
&self,
flat_u: &[f64],
out: &mut [f64],
record: usize,
) -> Result<(), OutputError> {
let n = self.n_cells();
let start = record * n;
let have = out.len();
let slot = out
.get_mut(start..start + n)
.ok_or_else(|| OutputError::BufferLength {
base: self.base.clone(),
what: "gridded output",
got: have,
want: start + n,
})?;
for (o, &fi) in self.flat_indices.iter().enumerate() {
slot[o] = *flat_u.get(fi).ok_or_else(|| self.short_state(flat_u, fi))?;
}
Ok(())
}
pub fn gather(&self, grid: &[f64], flat_u: &mut [f64]) -> Result<(), OutputError> {
if grid.len() != self.n_cells() {
return Err(OutputError::BufferLength {
base: self.base.clone(),
what: "gridded input",
got: grid.len(),
want: self.n_cells(),
});
}
for (o, &fi) in self.flat_indices.iter().enumerate() {
let have = flat_u.len();
let cell = flat_u.get_mut(fi).ok_or(OutputError::BufferLength {
base: self.base.clone(),
what: "flat state",
got: have,
want: fi + 1,
})?;
*cell = grid[o];
}
Ok(())
}
fn short_state(&self, flat_u: &[f64], fi: usize) -> OutputError {
OutputError::BufferLength {
base: self.base.clone(),
what: "flat state",
got: flat_u.len(),
want: fi + 1,
}
}
}
pub fn scatter(g: &VarGridding, flat_u: &[f64], out: &mut [f64]) -> Result<(), OutputError> {
g.scatter(flat_u, out)
}
pub fn gather(g: &VarGridding, grid: &[f64], flat_u: &mut [f64]) -> Result<(), OutputError> {
g.gather(grid, flat_u)
}
pub fn derive_output_gridding(slot_names: &[String]) -> Result<Vec<VarGridding>, OutputError> {
derive_output_gridding_from_map(slot_names.iter().enumerate().map(|(i, n)| (n.as_str(), i)))
}
pub fn derive_output_gridding_from_map<'a, I>(entries: I) -> Result<Vec<VarGridding>, OutputError>
where
I: IntoIterator<Item = (&'a str, usize)>,
{
let mut groups: BTreeMap<String, Vec<(usize, Vec<usize>)>> = BTreeMap::new();
for (key, idx) in entries {
let (base, inds) = match parse_cell_key(key) {
Some((b, i)) => (b.to_string(), i),
None => (key.to_string(), Vec::new()),
};
groups.entry(base).or_default().push((idx, inds));
}
groups
.into_iter()
.map(|(base, entries)| grid_one_variable(base, entries))
.collect()
}
fn grid_one_variable(
base: String,
entries: Vec<(usize, Vec<usize>)>,
) -> Result<VarGridding, OutputError> {
let ndim = entries.iter().map(|(_, i)| i.len()).max().unwrap_or(0);
if ndim == 0 {
if entries.len() != 1 {
return Err(OutputError::DuplicateScalar {
base,
count: entries.len(),
});
}
let flat = entries[0].0;
return Ok(VarGridding {
base,
shape: Vec::new(),
dim_names: Vec::new(),
flat_indices: vec![flat],
});
}
let mut shape = vec![0usize; ndim];
for (_, inds) in &entries {
if inds.len() != ndim {
return Err(OutputError::RaggedRank {
base,
got: inds.len(),
expected: ndim,
});
}
for (d, &i) in inds.iter().enumerate() {
if i == 0 {
return Err(OutputError::ZeroIndex { base, axis: d });
}
shape[d] = shape[d].max(i);
}
}
let total: usize = shape.iter().product();
if entries.len() != total {
return Err(OutputError::SparseGrid {
base,
shape,
got: entries.len(),
want: total,
});
}
let mut flat_indices = vec![usize::MAX; total];
for (flat, inds) in &entries {
let mut off = 0usize;
for (d, &i) in inds.iter().enumerate() {
off = off * shape[d] + (i - 1);
}
if flat_indices[off] != usize::MAX {
return Err(OutputError::DuplicateCell {
base,
cell: inds.clone(),
});
}
flat_indices[off] = *flat;
}
let dim_names = (0..ndim).map(|d| format!("{base}_d{d}")).collect();
Ok(VarGridding {
base,
shape,
dim_names,
flat_indices,
})
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct OutputMeta {
pub model_names: Vec<String>,
pub index_sets: BTreeMap<String, usize>,
pub var_dims: BTreeMap<String, Vec<String>>,
pub var_attrs: BTreeMap<String, JsonMap<String, JsonValue>>,
pub var_types: BTreeMap<String, VariableType>,
pub observed_unknowns: std::collections::BTreeSet<String>,
pub coordinates: BTreeMap<String, Coordinate>,
pub time_dim: String,
}
impl OutputMeta {
fn lookup<'m, T>(&self, table: &'m BTreeMap<String, T>, base: &str) -> Option<&'m T> {
if let Some(v) = table.get(base) {
return Some(v);
}
let bare = base.rsplit('.').next()?;
if bare == base {
return None;
}
table.get(bare)
}
#[must_use]
pub fn dims_of(&self, base: &str) -> Option<&Vec<String>> {
self.lookup(&self.var_dims, base)
}
#[must_use]
pub fn type_of(&self, base: &str) -> Option<&VariableType> {
self.lookup(&self.var_types, base)
}
#[must_use]
pub fn is_observed(&self, base: &str) -> bool {
if self.observed_unknowns.contains(base) {
return true;
}
match base.rsplit('.').next() {
Some(bare) if bare != base => self.observed_unknowns.contains(bare),
_ => false,
}
}
#[must_use]
pub fn attrs_of(&self, base: &str) -> Option<&JsonMap<String, JsonValue>> {
self.lookup(&self.var_attrs, base)
}
}
fn index_set_axis_len(is: &IndexSet) -> usize {
match is.kind.as_str() {
"categorical" => is.members.as_ref().map_or(0, Vec::len),
_ => usize::try_from(is.size.unwrap_or(0)).unwrap_or(0),
}
}
#[must_use]
pub fn derive_output_meta(doc: &EsmFile) -> OutputMeta {
let mut meta = OutputMeta {
time_dim: doc
.domain
.as_ref()
.and_then(|d| d.independent_variable.clone())
.unwrap_or_else(|| DEFAULT_TIME_DIM.to_string()),
..OutputMeta::default()
};
if let Some(sets) = &doc.index_sets {
for (name, is) in sets {
meta.index_sets.insert(name.clone(), index_set_axis_len(is));
}
}
if let Some(coords) = &doc.coordinates {
for (name, c) in coords {
meta.coordinates.insert(name.clone(), c.clone());
}
}
let Some(models) = &doc.models else {
return meta;
};
meta.model_names = models.keys().cloned().collect();
meta.model_names.sort();
let mut bare_counts: BTreeMap<&str, usize> = BTreeMap::new();
for model in models.values() {
for vn in model.variables.keys() {
*bare_counts.entry(vn.as_str()).or_insert(0) += 1;
}
}
for mname in &meta.model_names {
let model = &models[mname];
let class = crate::classification::Classification::of(model);
let mut vnames: Vec<&String> = model.variables.keys().collect();
vnames.sort();
for vn in vnames {
let v = &model.variables[vn];
let qualified = format!("{mname}.{vn}");
let ambiguous = bare_counts.get(vn.as_str()).copied().unwrap_or(0) > 1;
let mut keys: Vec<String> = vec![qualified];
if !ambiguous {
keys.push(vn.clone());
}
let mut attrs = JsonMap::new();
if let Some(u) = &v.units {
attrs.insert("units".to_string(), JsonValue::from(u.clone()));
}
if let Some(d) = &v.description {
attrs.insert("description".to_string(), JsonValue::from(d.clone()));
}
for key in keys {
if let Some(shape) = &v.shape {
meta.var_dims.insert(key.clone(), shape.clone());
}
if !attrs.is_empty() {
meta.var_attrs.insert(key.clone(), attrs.clone());
}
if class.is_observed(vn) {
meta.observed_unknowns.insert(key.clone());
}
meta.var_types.insert(key, v.var_type);
}
}
}
meta
}
pub fn apply_output_meta(g: VarGridding, meta: &OutputMeta) -> Result<VarGridding, OutputError> {
let Some(dims) = meta.dims_of(&g.base) else {
return Ok(g); };
if dims.len() != g.shape.len() {
let actual = g.shape.len();
return Err(OutputError::DimCountMismatch {
base: g.base,
dims: dims.clone(),
declared: dims.len(),
shape: g.shape,
actual,
});
}
for (d, dim) in dims.iter().enumerate() {
if let Some(&size) = meta.index_sets.get(dim)
&& size != 0
&& size != g.shape[d]
{
return Err(OutputError::DimLengthMismatch {
base: g.base,
axis: d,
dim: dim.clone(),
size,
len: g.shape[d],
});
}
}
Ok(VarGridding {
dim_names: dims.clone(),
..g
})
}
pub fn derive_output_gridding_with_meta(
slot_names: &[String],
meta: &OutputMeta,
) -> Result<Vec<VarGridding>, OutputError> {
derive_output_gridding(slot_names)?
.into_iter()
.map(|g| apply_output_meta(g, meta))
.collect()
}
#[must_use]
pub fn group_gridding_by_grid(gridding: Vec<VarGridding>) -> Vec<Vec<VarGridding>> {
let mut order: Vec<Vec<String>> = Vec::new();
let mut groups: BTreeMap<Vec<String>, Vec<VarGridding>> = BTreeMap::new();
for g in gridding {
let sig: Vec<String> = g
.dim_names
.iter()
.cloned()
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
if !groups.contains_key(&sig) {
order.push(sig.clone());
}
groups.entry(sig).or_default().push(g);
}
order
.into_iter()
.filter_map(|sig| groups.remove(&sig))
.collect()
}
#[derive(Debug, Clone, PartialEq)]
pub struct CoordPlan {
pub name: String,
pub values: Vec<f64>,
pub attrs: JsonMap<String, JsonValue>,
}
pub fn plan_dimension_coordinates(
gridding: &[VarGridding],
meta: &OutputMeta,
) -> Result<Vec<CoordPlan>, OutputError> {
let mut order: Vec<&str> = Vec::new();
let mut dimlen: BTreeMap<&str, usize> = BTreeMap::new();
for g in gridding {
for (d, name) in g.dim_names.iter().enumerate() {
if !dimlen.contains_key(name.as_str()) {
order.push(name.as_str());
}
dimlen.insert(name.as_str(), g.shape[d]);
}
}
let mut out = Vec::new();
for name in order {
let Some(entry) = meta.coordinates.get(name) else {
continue;
};
let Some(values) = &entry.values else {
continue;
};
let want = dimlen[name];
if values.len() != want {
return Err(OutputError::CoordLengthMismatch {
name: name.to_string(),
got: values.len(),
want,
});
}
let mut attrs = JsonMap::new();
if let Some(v) = &entry.standard_name {
attrs.insert("standard_name".to_string(), JsonValue::from(v.clone()));
}
if let Some(v) = &entry.units {
attrs.insert("units".to_string(), JsonValue::from(v.clone()));
}
if let Some(v) = &entry.axis {
attrs.insert("axis".to_string(), JsonValue::from(v.clone()));
}
out.push(CoordPlan {
name: name.to_string(),
values: values.clone(),
attrs,
});
}
Ok(out)
}
#[derive(Debug, Clone, PartialEq)]
pub struct VarPlan {
pub name: String,
pub dims: Vec<String>,
pub attrs: JsonMap<String, JsonValue>,
pub dtype: &'static str,
pub gridding: VarGridding,
}
impl VarPlan {
#[must_use]
pub fn values_per_record(&self) -> usize {
self.gridding.n_cells()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct GridPlan {
pub dims: Vec<(String, usize)>,
pub time_dim: String,
pub coords: Vec<CoordPlan>,
pub vars: Vec<VarPlan>,
}
impl GridPlan {
#[must_use]
pub fn dim_len(&self, dim: &str) -> Option<usize> {
self.dims
.iter()
.find(|(n, _)| n == dim)
.map(|&(_, len)| len)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct OutputPlan {
pub grids: Vec<GridPlan>,
}
impl OutputPlan {
#[must_use]
pub fn n_vars(&self) -> usize {
self.grids.iter().map(|g| g.vars.len()).sum()
}
#[must_use]
pub fn var(&self, name: &str) -> Option<&VarPlan> {
self.grids
.iter()
.find_map(|g| g.vars.iter().find(|v| v.name == name))
}
}
pub fn derive_output_plan(
doc: &EsmFile,
slot_names: &[String],
observed: &[String],
) -> Result<OutputPlan, OutputError> {
let meta = derive_output_meta(doc);
let gridding = derive_output_gridding_with_meta(slot_names, &meta)?;
let bases: Vec<&str> = gridding.iter().map(|g| g.base.as_str()).collect();
let mut requested: HashSet<String> = HashSet::new();
for name in observed {
match match_output_request(name, &bases) {
RequestMatch::Named(base) => {
requested.insert(base.to_string());
}
RequestMatch::Ambiguous(candidates) => {
return Err(OutputError::AmbiguousRequest {
name: name.clone(),
candidates: candidates.into_iter().map(str::to_string).collect(),
});
}
RequestMatch::Missing => {
return Err(OutputError::UnknownObserved { name: name.clone() });
}
}
}
let kept: Vec<VarGridding> = gridding
.into_iter()
.filter(|g| !meta.is_observed(&g.base) || requested.contains(&g.base))
.collect();
let mut grids = Vec::new();
for group in group_gridding_by_grid(kept) {
grids.push(build_grid_plan(group, &meta)?);
}
Ok(OutputPlan { grids })
}
pub(crate) enum RequestMatch<'a> {
Named(&'a str),
Ambiguous(Vec<&'a str>),
Missing,
}
pub(crate) fn match_output_request<'a>(request: &str, candidates: &[&'a str]) -> RequestMatch<'a> {
if let Some(exact) = candidates.iter().find(|c| **c == request) {
return RequestMatch::Named(exact);
}
let last_segment = |n: &str| n.rsplit('.').next().unwrap_or(n).to_string();
let tail = last_segment(request);
let mut hits: Vec<&'a str> = candidates
.iter()
.copied()
.filter(|c| last_segment(c) == tail)
.collect();
hits.sort_unstable();
hits.dedup();
match hits.len() {
0 => RequestMatch::Missing,
1 => RequestMatch::Named(hits[0]),
_ => RequestMatch::Ambiguous(hits),
}
}
fn build_grid_plan(group: Vec<VarGridding>, meta: &OutputMeta) -> Result<GridPlan, OutputError> {
let mut dims: Vec<(String, usize)> = Vec::new();
let mut owner: BTreeMap<String, String> = BTreeMap::new();
for g in &group {
for (d, name) in g.dim_names.iter().enumerate() {
if name == &meta.time_dim {
return Err(OutputError::TimeDimCollision {
base: g.base.clone(),
dim: name.clone(),
});
}
match dims.iter().find(|(n, _)| n == name) {
Some(&(_, len)) if len != g.shape[d] => {
return Err(OutputError::DimLengthConflict {
dim: name.clone(),
first: owner[name].clone(),
a: len,
second: g.base.clone(),
b: g.shape[d],
});
}
Some(_) => {}
None => {
dims.push((name.clone(), g.shape[d]));
owner.insert(name.clone(), g.base.clone());
}
}
}
}
let coords = plan_dimension_coordinates(&group, meta)?;
let vars = group
.into_iter()
.map(|g| {
let mut var_dims = Vec::with_capacity(g.dim_names.len() + 1);
var_dims.push(meta.time_dim.clone());
var_dims.extend(g.dim_names.iter().cloned());
VarPlan {
name: g.base.clone(),
dims: var_dims,
attrs: meta.attrs_of(&g.base).cloned().unwrap_or_default(),
dtype: DTYPE_FLOAT64,
gridding: g,
}
})
.collect();
Ok(GridPlan {
dims,
time_dim: meta.time_dim.clone(),
coords,
vars,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn names(v: &[&str]) -> Vec<String> {
v.iter().map(|s| (*s).to_string()).collect()
}
fn col_major_slots(base: &str, shape: &[usize]) -> Vec<String> {
let total: usize = shape.iter().product();
(0..total)
.map(|flat| {
let mut rem = flat;
let mut idx = Vec::with_capacity(shape.len());
for &n in shape {
idx.push((rem % n) + 1);
rem /= n;
}
let joined: Vec<String> = idx.iter().map(usize::to_string).collect();
format!("{base}[{}]", joined.join(","))
})
.collect()
}
#[test]
fn parse_cell_key_round_trips_and_rejects() {
assert_eq!(parse_cell_key("u[3]"), Some(("u", vec![3])));
assert_eq!(parse_cell_key("u[2,3]"), Some(("u", vec![2, 3])));
assert_eq!(parse_cell_key("M.u[1,2,3]"), Some(("M.u", vec![1, 2, 3])));
assert_eq!(parse_cell_key("u"), None);
assert_eq!(parse_cell_key("u[]"), None);
assert_eq!(parse_cell_key("[1]"), None);
assert_eq!(parse_cell_key("u[a]"), None);
assert_eq!(parse_cell_key("u[1,]"), None);
assert_eq!(parse_cell_key("u[1"), None);
assert_eq!(parse_cell_key("u[1][2]"), None);
}
#[test]
fn inversion_transposes_a_non_square_2d_grid() {
let slots = col_major_slots("c", &[3, 2]);
assert_eq!(
slots,
names(&["c[1,1]", "c[2,1]", "c[3,1]", "c[1,2]", "c[2,2]", "c[3,2]"])
);
let g = &derive_output_gridding(&slots).unwrap()[0];
assert_eq!(g.shape, vec![3, 2]);
assert_eq!(g.dim_names, names(&["c_d0", "c_d1"]));
assert_eq!(g.flat_indices, vec![0, 3, 1, 4, 2, 5]);
let mut u = vec![0.0; 6];
for (flat, name) in slots.iter().enumerate() {
let (_, idx) = parse_cell_key(name).unwrap();
u[flat] = (10 * idx[0] + idx[1]) as f64;
}
let mut out = vec![0.0; 6];
g.scatter(&u, &mut out).unwrap();
assert_eq!(out, vec![11.0, 12.0, 21.0, 22.0, 31.0, 32.0]);
for i in 0..3 {
for j in 0..2 {
assert_eq!(out[i * 2 + j], (10 * (i + 1) + (j + 1)) as f64);
}
}
}
#[test]
fn inversion_handles_a_rank_3_grid_with_distinct_axis_lengths() {
let shape = [2usize, 3, 4];
let slots = col_major_slots("f", &shape);
assert_eq!(slots.len(), 24);
assert_eq!(slots[0], "f[1,1,1]");
assert_eq!(slots[1], "f[2,1,1]"); assert_eq!(slots[2], "f[1,2,1]");
assert_eq!(slots[23], "f[2,3,4]");
let g = &derive_output_gridding(&slots).unwrap()[0];
assert_eq!(g.shape, vec![2, 3, 4]);
assert_eq!(g.dim_names, names(&["f_d0", "f_d1", "f_d2"]));
let u: Vec<f64> = (0..24).map(|i| i as f64).collect();
let mut out = vec![0.0; 24];
g.scatter(&u, &mut out).unwrap();
for i in 0..2 {
for j in 0..3 {
for k in 0..4 {
let row = (i * 3 + j) * 4 + k;
let col = i + 2 * (j + 3 * k);
assert_eq!(out[row], col as f64, "cell ({i},{j},{k})");
assert_eq!(g.cart(row), vec![i, j, k]);
}
}
}
let mut back = vec![0.0; 24];
g.gather(&out, &mut back).unwrap();
assert_eq!(back, u);
}
#[test]
fn scalars_are_zero_dimensional_and_share_one_grid() {
let slots = names(&["a", "b"]);
let gs = derive_output_gridding(&slots).unwrap();
assert_eq!(gs.len(), 2);
for g in &gs {
assert!(g.is_scalar());
assert!(g.shape.is_empty());
assert!(g.dim_names.is_empty());
assert_eq!(g.n_cells(), 1);
}
assert_eq!(gs[0].flat_indices, vec![0]);
assert_eq!(gs[1].flat_indices, vec![1]);
let groups = group_gridding_by_grid(gs);
assert_eq!(groups.len(), 1, "every scalar shares the 0-D grid");
assert_eq!(groups[0].len(), 2);
}
#[test]
fn variables_are_sorted_and_grouped_by_spatial_signature() {
let mut slots = Vec::new();
slots.extend(col_major_slots("ocn.t", &[2, 2])); slots.extend(col_major_slots("atm.q", &[3, 2]));
slots.push("scalar".to_string());
let gs = derive_output_gridding(&slots).unwrap();
let bases: Vec<&str> = gs.iter().map(|g| g.base.as_str()).collect();
assert_eq!(bases, vec!["atm.q", "ocn.t", "scalar"]);
let groups = group_gridding_by_grid(gs);
assert_eq!(groups.len(), 3);
assert_eq!(groups[0][0].base, "atm.q");
assert_eq!(groups[1][0].base, "ocn.t");
assert_eq!(groups[2][0].base, "scalar");
}
#[test]
fn shared_axis_names_collapse_two_variables_into_one_grid() {
let a = VarGridding {
base: "a".into(),
shape: vec![2, 3],
dim_names: names(&["lat", "lon"]),
flat_indices: (0..6).collect(),
};
let b = VarGridding {
base: "b".into(),
shape: vec![3, 2],
dim_names: names(&["lon", "lat"]),
flat_indices: (6..12).collect(),
};
let groups = group_gridding_by_grid(vec![a, b]);
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].len(), 2);
}
#[test]
fn malformed_griddings_are_refused() {
let err = derive_output_gridding_from_map([("a", 0usize), ("a", 1usize)]).unwrap_err();
assert!(matches!(err, OutputError::DuplicateScalar { .. }), "{err}");
let err = derive_output_gridding(&names(&["u[1]", "u[1,2]"])).unwrap_err();
assert!(matches!(err, OutputError::RaggedRank { .. }), "{err}");
let err =
derive_output_gridding(&names(&["u[1,1]", "u[2,1]", "u[3,1]", "u[1,2]", "u[2,2]"]))
.unwrap_err();
assert!(matches!(err, OutputError::SparseGrid { .. }), "{err}");
let err = derive_output_gridding(&names(&["u[0]"])).unwrap_err();
assert!(matches!(err, OutputError::ZeroIndex { .. }), "{err}");
}
#[test]
fn scatter_refuses_a_wrong_sized_buffer() {
let slots = col_major_slots("c", &[3, 2]);
let g = &derive_output_gridding(&slots).unwrap()[0];
let u = vec![1.0; 6];
let mut too_small = vec![0.0; 5];
assert!(matches!(
g.scatter(&u, &mut too_small).unwrap_err(),
OutputError::BufferLength { .. }
));
let mut trajectory = vec![0.0; 12];
g.scatter_record(&u, &mut trajectory, 1).unwrap();
assert!(matches!(
g.scatter_record(&u, &mut trajectory, 2).unwrap_err(),
OutputError::BufferLength { .. }
));
}
}