use energyml_core::value::{FieldValue, Reflect, Scalar};
use energyml_storage::array::ArrayLocation;
use crate::error::{Error, Result};
use crate::geometry::{Patch, Topology};
use crate::points::Point;
use crate::source::ValueSource;
use crate::values::scalar_i64;
pub(crate) fn ijk_grid(
representation: &dyn Reflect,
source: &dyn ValueSource,
dimensions: [usize; 3],
at: &str,
) -> Result<Vec<Patch>> {
let [ni, nj, nk] = dimensions;
let shape = format!("{at} {ni}×{nj}×{nk}");
let Some(window) = crate::geometry::field_object(representation, "ParentWindow") else {
return Err(Error::unsupported(
"an IJK grid with neither a `Geometry` nor a `ParentWindow`",
shape,
));
};
let kind = window.type_info().name;
if !kind.ends_with("IjkParentWindow") {
return Err(Error::unsupported(kind, format!("{shape}.ParentWindow")));
}
if crate::geometry::field_object(representation, "KGaps").is_some() {
return Err(Error::unsupported("a local grid with K gaps of its own", shape));
}
let axes = [
Axis::of(window, source, "IRegrid", ni, &shape)?,
Axis::of(window, source, "JRegrid", nj, &shape)?,
Axis::of(window, source, "KRegrid", nk, &shape)?,
];
let Some(parent_uri) = energyml_domain::crs::PARENT_GRID_FIELDS
.iter()
.find_map(|field| crate::well::reference_of(window, field))
else {
return Err(Error::malformed(
&shape,
"a parent window that names no parent grid",
));
};
let Some(parent) = source.object_at(&parent_uri) else {
return Err(Error::Domain(energyml_domain::Error::Missing(format!(
"the parent grid {parent_uri}"
))));
};
let parent = parent.as_reflect();
let lattice = Lattice::of(parent, source, &axes, &shape)?;
build(&axes, &lattice, dimensions)
}
struct Axis {
start: usize,
parents: usize,
nodes: Vec<f64>,
}
impl Axis {
fn of(
window: &dyn Reflect,
source: &dyn ValueSource,
field: &str,
children: usize,
at: &str,
) -> Result<Self> {
let Some(regrid) = crate::geometry::field_object(window, field) else {
return Err(Error::malformed(at, format!("a parent window with no `{field}`")));
};
let start = scalar_i64(regrid, "InitialIndexOnParentGrid").unwrap_or(0).max(0) as usize;
let Some(intervals) = crate::geometry::field_object(regrid, "Intervals") else {
return Ok(Self {
start,
parents: children,
nodes: (0..=children).map(|n| n as f64).collect(),
});
};
let count = scalar_i64(intervals, "IntervalCount").unwrap_or(0).max(0) as usize;
let parents_per = interval_counts(intervals, source, "ParentCountPerInterval", at, field)?;
let children_per = interval_counts(intervals, source, "ChildCountPerInterval", at, field)?;
if parents_per.len() != count || children_per.len() != count {
return Err(Error::malformed(
at,
format!(
"`{field}` declares {count} interval(s), with {} parent count(s) and {} child \
count(s)",
parents_per.len(),
children_per.len()
),
));
}
let weights = match crate::geometry::field_object(intervals, "ChildCellWeights") {
Some(array) => Some(crate::values::read_doubles(
array,
source,
&format!("{at}.{field}.ChildCellWeights"),
)?),
None => None,
};
let mut nodes = vec![0.0f64];
let mut parents = 0usize;
let mut child = 0usize;
for interval in 0..count {
let span = parents_per[interval];
let cut = children_per[interval];
if cut == 0 {
return Err(Error::malformed(
at,
format!("`{field}` interval {interval} cuts {span} parent cell(s) into none"),
));
}
let slice = weights.as_ref().and_then(|w| w.get(child..child + cut));
let total: f64 = slice.map(|w| w.iter().sum()).unwrap_or(cut as f64);
if total.is_nan() || total <= 0.0 {
return Err(Error::malformed(
at,
format!("`{field}` interval {interval} has child weights summing to {total}"),
));
}
let base = parents as f64;
let mut run = 0.0f64;
for step in 0..cut {
run += slice.map(|w| w[step]).unwrap_or(1.0);
nodes.push(base + span as f64 * run / total);
}
parents += span;
child += cut;
}
if child != children {
return Err(Error::malformed(
at,
format!("`{field}` cuts {child} child cell(s) for a dimension of {children}"),
));
}
Ok(Self {
start,
parents,
nodes,
})
}
}
fn interval_counts(
intervals: &dyn Reflect,
source: &dyn ValueSource,
field: &str,
at: &str,
axis: &str,
) -> Result<Vec<usize>> {
let Some(array) = crate::geometry::field_object(intervals, field) else {
return Err(Error::malformed(at, format!("`{axis}` with no `{field}`")));
};
let values = crate::values::read_integers(array, source, &format!("{at}.{axis}.{field}"))?;
Ok(values.into_iter().map(|v| v.max(0) as usize).collect())
}
struct Lattice {
points: Vec<Point>,
span: [usize; 2],
}
impl Lattice {
fn at(&self, di: usize, dj: usize, level: usize) -> Point {
let [ni, _] = self.span;
self.points[(level * (self.span[1] + 1) + dj) * (ni + 1) + di]
}
fn of(parent: &dyn Reflect, source: &dyn ValueSource, axes: &[Axis; 3], at: &str) -> Result<Self> {
let ni = dimension(parent, "Ni", at)?;
let nj = dimension(parent, "Nj", at)?;
let nk = dimension(parent, "Nk", at)?;
for (axis, (name, extent)) in axes.iter().zip([("I", ni), ("J", nj), ("K", nk)]) {
if axis.start + axis.parents > extent {
return Err(Error::malformed(
at,
format!(
"a window covering parent cells {}..{} on {name}, of a parent {extent} cell(s) wide",
axis.start,
axis.start + axis.parents
),
));
}
}
let Some(geometry) = crate::geometry::field_object(parent, "Geometry") else {
return Err(Error::unsupported(
"a parent grid that has no geometry of its own either",
at.to_string(),
));
};
let Some(array) = crate::geometry::field_object(geometry, "Points") else {
return Err(Error::malformed(at, "a parent geometry with no `Points`"));
};
let pillars = (ni + 1) * (nj + 1);
let splits = crate::ijk::split_lines(geometry, source, pillars, at)?;
if let Some(column) = splits.within(
ni,
[axes[0].start, axes[1].start],
[axes[0].parents, axes[1].parents],
) {
return Err(Error::unsupported(
"a parent window over a split coordinate line",
format!("{at}, parent column {column}"),
));
}
let tops = crate::ijk::layer_tops(parent, source, nk, at)?;
let levels = tops.last().map(|top| top + 2).unwrap_or(nk + 1);
let (k0, pk) = (axes[2].start, axes[2].parents);
if let Some(layer) = (k0..(k0 + pk).saturating_sub(1)).find(|&k| tops[k + 1] != tops[k] + 1) {
return Err(Error::unsupported(
"a parent window across a K gap",
format!("{at}, gap after parent layer {layer}"),
));
}
let origin = [axes[0].start, axes[1].start, tops.get(k0).copied().unwrap_or(k0)];
let span = [axes[0].parents, axes[1].parents, axes[2].parents];
let lines = pillars + splits.count;
let vertical = crate::parametric::Vertical::read(geometry, source);
let points = window_points(array, source, [ni, levels], lines, origin, span, at, vertical)?;
Ok(Self {
points,
span: [span[0], span[1]],
})
}
}
#[allow(clippy::too_many_arguments)]
fn window_points(
array: &dyn Reflect,
source: &dyn ValueSource,
parent: [usize; 2],
lines: usize,
origin: [usize; 3],
span: [usize; 3],
at: &str,
vertical: crate::parametric::Vertical,
) -> Result<Vec<Point>> {
let [ni, levels] = parent;
let [i0, j0, k0] = origin;
let [pi, pj, pk] = span;
let wanted = (pk + 1) * (pj + 1) * (pi + 1);
if let Some(location) = sliceable(array, source, levels, lines) {
let mut points = Vec::with_capacity(wanted);
for dj in 0..=pj {
let first = (j0 + dj) * (ni + 1) + i0;
let mut part = location.clone();
part.start = vec![k0 as i64, first as i64, 0];
part.count = vec![(pk + 1) as i64, (pi + 1) as i64, 3];
let data = source.read(&part)?;
let values = data.to_f64();
if values.len() != (pk + 1) * (pi + 1) * 3 {
return Err(Error::malformed(
at,
format!(
"a window row of {} value(s) where {} were selected",
values.len(),
(pk + 1) * (pi + 1) * 3
),
));
}
points.push(values);
}
let mut out = vec![[0.0f64; 3]; wanted];
for (dj, values) in points.iter().enumerate() {
for level in 0..=pk {
for di in 0..=pi {
let from = (level * (pi + 1) + di) * 3;
out[(level * (pj + 1) + dj) * (pi + 1) + di] =
[values[from], values[from + 1], values[from + 2]];
}
}
}
return Ok(out);
}
let whole = crate::points::read(array, source, &format!("{at}.Geometry.Points"), vertical)?;
if whole.len() != levels * lines {
return Err(Error::malformed(
at,
format!(
"a parent holding {} point(s) for {levels} level(s) of {lines} line(s)",
whole.len()
),
));
}
let mut out = Vec::with_capacity(wanted);
for level in k0..=k0 + pk {
for dj in 0..=pj {
for di in 0..=pi {
let pillar = (j0 + dj) * (ni + 1) + (i0 + di);
out.push(whole[level * lines + pillar]);
}
}
}
Ok(out)
}
fn sliceable(
array: &dyn Reflect,
source: &dyn ValueSource,
levels: usize,
lines: usize,
) -> Option<ArrayLocation> {
let name = array.type_info().name;
if !(name.ends_with("ExternalArray") || name.ends_with("Hdf5Array")) {
return None;
}
let locations = source.locate(array);
let [location] = locations.as_slice() else {
return None;
};
let metadata = source.metadata(location).ok()?;
let dimensions: Vec<usize> = metadata.dimensions.iter().map(|d| (*d).max(0) as usize).collect();
(dimensions == [levels, lines, 3]).then(|| location.clone())
}
fn build(axes: &[Axis; 3], lattice: &Lattice, dimensions: [usize; 3]) -> Result<Vec<Patch>> {
let [ni, nj, nk] = dimensions;
let mut points = Vec::with_capacity((ni + 1) * (nj + 1) * (nk + 1));
for w in &axes[2].nodes {
for v in &axes[1].nodes {
for u in &axes[0].nodes {
points.push(interpolate(lattice, *u, *v, *w));
}
}
}
let plane = (ni + 1) * (nj + 1);
let row = ni + 1;
let mut cells = Vec::with_capacity(ni * nj * nk);
for k in 0..nk {
for j in 0..nj {
for i in 0..ni {
let corner =
|dk: usize, dj: usize, di: usize| ((k + dk) * plane + (j + dj) * row + i + di) as u32;
let node = [
corner(0, 0, 0),
corner(0, 0, 1),
corner(0, 1, 1),
corner(0, 1, 0),
corner(1, 0, 0),
corner(1, 0, 1),
corner(1, 1, 1),
corner(1, 1, 0),
];
cells.push(crate::ijk::oriented(node, &points));
}
}
}
Ok(vec![Patch {
index: 0,
node_offset: 0,
points,
topology: Topology::Hexahedra {
cells,
dimensions: [ni, nj, nk],
index: Vec::new(),
},
closed: Vec::new(),
}])
}
fn interpolate(lattice: &Lattice, u: f64, v: f64, w: f64) -> Point {
let [pi, pj] = lattice.span;
let split = |value: f64, extent: usize| {
let cell = (value.floor().max(0.0) as usize).min(extent.saturating_sub(1));
(cell, value - cell as f64)
};
let (i, a) = split(u, pi);
let (j, b) = split(v, pj);
let (k, c) = split(w, lattice.points.len() / ((pi + 1) * (pj + 1)) - 1);
let mut out = [0.0f64; 3];
for (dk, wk) in [(0usize, 1.0 - c), (1, c)] {
for (dj, wj) in [(0usize, 1.0 - b), (1, b)] {
for (di, wi) in [(0usize, 1.0 - a), (1, a)] {
let weight = wi * wj * wk;
if weight == 0.0 {
continue;
}
let point = lattice.at(i + di, j + dj, k + dk);
for axis in 0..3 {
out[axis] += point[axis] * weight;
}
}
}
}
out
}
pub(crate) fn sub_representation(
representation: &dyn Reflect,
source: &dyn ValueSource,
) -> Result<Vec<Patch>> {
let at = "SubRepresentation";
let object_element = text_of(representation, "IndexableElement");
if let Some(count) = extra_supports(representation)
&& count > 0
{
return Err(Error::unsupported(
"SupportingRepresentation in ExtraMetadata",
format!(
"this sub-representation names {count} support(s) through the ExtraMetadata \
convention of the Technical Usage Guide §4.4.1, which then replaces the schema's \
own `SupportingRepresentation`"
),
));
}
let object_support = crate::well::reference_of(representation, "SupportingRepresentation");
let patches = list_of(representation, "SubRepresentationPatch");
if patches.is_empty() {
return Err(Error::malformed(at, "a sub-representation with no patch"));
}
let mut out = Vec::with_capacity(patches.len());
let mut offset = 0usize;
for (index, patch) in patches.iter().enumerate() {
let where_ = format!("{at}.SubRepresentationPatch[{index}]");
let elements = list_of(*patch, "ElementIndices");
if elements.len() > 1 {
return Err(Error::unsupported(
"a sub-representation listing pairs of elements",
where_,
));
}
let (element, indices) = match elements.first() {
Some(one) => (
text_of(*one, "IndexableElement"),
crate::geometry::field_object(*one, "Indices"),
),
None => (
object_element.clone(),
crate::geometry::field_object(*patch, "Indices"),
),
};
let Some(element) = element else {
return Err(Error::malformed(&where_, "a patch naming no indexable element"));
};
let Some(element) = energyml_domain::property::IndexableElement::parse(&element) else {
return Err(Error::malformed(
&where_,
format!("`{element}` is not an indexable element"),
));
};
let Some(indices) = indices else {
return Err(Error::malformed(&where_, "a patch with no `Indices`"));
};
let indices = crate::values::read_integers(indices, source, &where_)?;
let Some(support_uri) =
crate::well::reference_of(*patch, "SupportingRepresentation").or_else(|| object_support.clone())
else {
return Err(Error::malformed(
&where_,
"a patch that names no supporting representation",
));
};
let Some(support) = source.object_at(&support_uri) else {
return Err(Error::Domain(energyml_domain::Error::Missing(format!(
"the supporting representation {support_uri}"
))));
};
let mut patch = select(support.as_reflect(), element, &indices, source, &where_)?;
patch.index = index;
patch.node_offset = offset;
offset += patch.points.len();
out.push(patch);
}
Ok(out)
}
fn select(
support: &dyn Reflect,
element: energyml_domain::property::IndexableElement,
indices: &[i64],
source: &dyn ValueSource,
at: &str,
) -> Result<Patch> {
use energyml_domain::property::IndexableElement::*;
let available = energyml_domain::property::element_count(support, element)? as i64;
if let Some(bad) = indices.iter().find(|i| **i < 0 || **i >= available) {
return Err(Error::malformed(
at,
format!("an index of {bad} into {available} `{element}` of the support"),
));
}
let geometry = crate::geometry::of(support, source)?;
let (points, triangles) = geometry.merged();
match element {
Nodes => Ok(Patch {
index: 0,
node_offset: 0,
points: indices.iter().map(|i| points[*i as usize]).collect(),
topology: Topology::Points,
closed: Vec::new(),
}),
Triangles => {
let chosen: Vec<[u32; 3]> = indices.iter().map(|i| triangles[*i as usize]).collect();
Ok(compact(points, chosen))
}
Cells => {
let cells: Vec<[u32; 8]> = geometry
.patches
.iter()
.flat_map(|patch| match &patch.topology {
Topology::Hexahedra { cells, .. } => cells.clone(),
_ => Vec::new(),
})
.collect();
if cells.len() as i64 != available {
return Err(Error::malformed(
at,
format!(
"a support declaring {available} cell(s) and drawing {}",
cells.len()
),
));
}
let chosen: Vec<[u32; 8]> = indices.iter().map(|i| cells[*i as usize]).collect();
let count = chosen.len();
let mut patch = compact_cells(points, chosen);
patch.topology = match patch.topology {
Topology::Hexahedra { cells, .. } => Topology::Hexahedra {
cells,
dimensions: [count, 1, 1],
index: Vec::new(),
},
other => other,
};
Ok(patch)
}
other => Err(Error::unsupported(
"a sub-representation of an element with no geometry here",
format!("{at}, `{other}`"),
)),
}
}
fn compact(points: Vec<Point>, triangles: Vec<[u32; 3]>) -> Patch {
let mut map = std::collections::HashMap::new();
let mut kept = Vec::new();
let mut out = Vec::with_capacity(triangles.len());
for triangle in triangles {
let mut renumbered = [0u32; 3];
for (slot, node) in triangle.iter().enumerate() {
let next = map.len() as u32;
let index = *map.entry(*node).or_insert_with(|| {
kept.push(points[*node as usize]);
next
});
renumbered[slot] = index;
}
out.push(renumbered);
}
Patch {
index: 0,
node_offset: 0,
points: kept,
topology: Topology::Triangles(out),
closed: Vec::new(),
}
}
fn compact_cells(points: Vec<Point>, cells: Vec<[u32; 8]>) -> Patch {
let mut map = std::collections::HashMap::new();
let mut kept = Vec::new();
let mut out = Vec::with_capacity(cells.len());
for cell in cells {
let mut renumbered = [0u32; 8];
for (slot, node) in cell.iter().enumerate() {
let next = map.len() as u32;
let index = *map.entry(*node).or_insert_with(|| {
kept.push(points[*node as usize]);
next
});
renumbered[slot] = index;
}
out.push(renumbered);
}
let count = out.len();
Patch {
index: 0,
node_offset: 0,
points: kept,
topology: Topology::Hexahedra {
cells: out,
dimensions: [count, 1, 1],
index: Vec::new(),
},
closed: Vec::new(),
}
}
fn text_of(object: &dyn Reflect, field: &str) -> Option<String> {
match object.field(field)? {
FieldValue::Scalar(scalar) => scalar.as_str().map(str::to_string),
_ => None,
}
}
fn list_of<'a>(object: &'a dyn Reflect, field: &str) -> Vec<&'a dyn Reflect> {
match object.field(field) {
Some(FieldValue::List(items)) => items.iter().filter_map(|item| item.as_object()).collect(),
Some(FieldValue::Object(one)) => vec![one],
_ => Vec::new(),
}
}
fn dimension(object: &dyn Reflect, field: &str, at: &str) -> Result<usize> {
match scalar_i64(object, field) {
Some(value) if value > 0 => Ok(value as usize),
Some(value) => Err(Error::malformed(
at,
format!("a parent whose `{field}` is {value}"),
)),
None => Err(Error::malformed(at, format!("a parent with no `{field}`"))),
}
}
fn extra_supports(object: &dyn Reflect) -> Option<usize> {
let FieldValue::List(items) = object.field("ExtraMetadata")? else {
return None;
};
Some(
items
.iter()
.filter_map(|item| item.as_object())
.filter(|pair| {
matches!(
pair.field("Name"),
Some(FieldValue::Scalar(Scalar::Str(name))) if name == "SupportingRepresentation"
)
})
.count(),
)
}