use std::collections::HashMap;
use energyml_core::value::{FieldValue, Reflect};
use energyml_domain::values;
use crate::error::{Error, Result};
use crate::geometry::{Patch, Topology};
use crate::points::{self, Point};
use crate::source::ValueSource;
use crate::values::scalar_i64;
pub fn read(representation: &dyn Reflect, source: &dyn ValueSource) -> Result<Vec<Patch>> {
let at = "IjkGridRepresentation";
let ni = dimension(representation, "Ni", at)?;
let nj = dimension(representation, "Nj", at)?;
let nk = dimension(representation, "Nk", at)?;
let Some(geometry) = field_object(representation, "Geometry") else {
return crate::parent::ijk_grid(representation, source, [ni, nj, nk], at);
};
let Some(array) = field_object(geometry, "Points") else {
return Err(Error::malformed(at, "a geometry with no `Points`"));
};
let pillars = (ni + 1) * (nj + 1);
let splits = split_lines(geometry, source, pillars, at)?;
let lines = pillars + splits.count;
let where_ = format!("{at}.Geometry.Points");
let vertical = crate::parametric::Vertical::read(geometry, source);
let coordinates = if array.type_info().name.ends_with("ParametricArray") {
let on: Vec<usize> = (0..pillars).chain(splits.of_pillar.iter().copied()).collect();
crate::parametric::read_on_lines(array, source, &where_, Some(&on), vertical)?
} else {
points::read(array, source, &where_, vertical)?
};
let tops = layer_tops(representation, source, nk, at)?;
let levels = tops.last().map(|top| top + 2).unwrap_or(nk + 1);
if coordinates.len() != levels * lines {
return Err(Error::malformed(
at,
format!(
"{} point(s) for a {ni}×{nj}×{nk} grid with {pillars} pillar(s), {} split \
line(s) and {levels} level(s): expected {}",
coordinates.len(),
splits.count,
levels * lines
),
));
}
let pillar_defined = flags(geometry, source, "PillarGeometryIsDefined");
let cell_defined = flags(geometry, source, "CellGeometryIsDefined");
let mut cells: Vec<[u32; 8]> = Vec::with_capacity(ni * nj * nk);
let mut numbering: Vec<u32> = Vec::new();
let mut skipped = 0usize;
let mut layer = 0usize;
for top in &tops {
for j in 0..nj {
for i in 0..ni {
let column = j * ni + i;
let cell_index = layer * nj * ni + column;
if says_undefined(&cell_defined, cell_index) {
skipped += 1;
continue;
}
let corners = [(i, j), (i + 1, j), (i + 1, j + 1), (i, j + 1)];
let mut node = [0u32; 8];
let (top, bottom) = (*top, top + 1);
let mut undefined_pillar = false;
for (corner, (ci, cj)) in corners.iter().enumerate() {
let pillar = cj * (ni + 1) + ci;
undefined_pillar |= says_undefined(&pillar_defined, pillar);
let line = splits.line_for(pillar, column).unwrap_or(pillar);
node[corner] = (top * lines + line) as u32;
node[corner + 4] = (bottom * lines + line) as u32;
}
if undefined_pillar
|| node
.iter()
.any(|n| !coordinates[*n as usize].iter().all(|c| c.is_finite()))
{
skipped += 1;
continue;
}
numbering.push(cell_index as u32);
cells.push(oriented(node, &coordinates));
}
}
layer += 1;
}
if cells.is_empty() && skipped > 0 {
return Err(Error::malformed(
at,
format!("all {skipped} cell(s) have at least one undefined pillar"),
));
}
Ok(vec![Patch {
index: 0,
node_offset: 0,
points: coordinates,
topology: Topology::Hexahedra {
cells,
dimensions: [ni, nj, nk],
index: if skipped == 0 { Vec::new() } else { numbering },
},
closed: Vec::new(),
}])
}
pub(crate) fn oriented(node: [u32; 8], points: &[Point]) -> [u32; 8] {
if signed_volume(node, points) < 0.0 {
[
node[4], node[5], node[6], node[7], node[0], node[1], node[2], node[3],
]
} else {
node
}
}
fn signed_volume(node: [u32; 8], points: &[Point]) -> f64 {
const FACES: [[usize; 4]; 6] = [
[0, 3, 2, 1],
[4, 5, 6, 7],
[0, 1, 5, 4],
[1, 2, 6, 5],
[2, 3, 7, 6],
[3, 0, 4, 7],
];
let mut origin = [0.0f64; 3];
for index in node {
let point = points[index as usize];
for (axis, value) in origin.iter_mut().enumerate() {
*value += point[axis] / 8.0;
}
}
let at = |corner: usize| {
let point = points[node[corner] as usize];
[point[0] - origin[0], point[1] - origin[1], point[2] - origin[2]]
};
let mut volume = 0.0;
for face in FACES {
let mut middle = [0.0f64; 3];
for corner in face {
let point = at(corner);
for (axis, value) in middle.iter_mut().enumerate() {
*value += point[axis] / 4.0;
}
}
for index in 0..4 {
let (a, b) = (at(face[index]), at(face[(index + 1) % 4]));
let u = [a[0] - middle[0], a[1] - middle[1], a[2] - middle[2]];
let w = [b[0] - middle[0], b[1] - middle[1], b[2] - middle[2]];
let normal = [
u[1] * w[2] - u[2] * w[1],
u[2] * w[0] - u[0] * w[2],
u[0] * w[1] - u[1] * w[0],
];
volume += (normal[0] * middle[0] + normal[1] * middle[1] + normal[2] * middle[2]) / 6.0;
}
}
volume
}
#[derive(Default)]
pub(crate) struct Splits {
pub(crate) count: usize,
lookup: HashMap<(usize, usize), usize>,
pub(crate) of_pillar: Vec<usize>,
}
impl Splits {
fn line_for(&self, pillar: usize, column: usize) -> Option<usize> {
self.lookup.get(&(pillar, column)).copied()
}
pub(crate) fn within(&self, ni: usize, origin: [usize; 2], span: [usize; 2]) -> Option<usize> {
self.lookup
.keys()
.map(|(_, column)| *column)
.filter(|column| {
let (i, j) = (column % ni.max(1), column / ni.max(1));
i >= origin[0] && i < origin[0] + span[0] && j >= origin[1] && j < origin[1] + span[1]
})
.min()
}
}
const SPLIT_FIELDS: &[&str] = &["SplitCoordinateLines", "ColumnLayerSplitCoordinateLines"];
pub(crate) fn split_lines(
geometry: &dyn Reflect,
source: &dyn ValueSource,
pillars: usize,
at: &str,
) -> Result<Splits> {
let Some(element) = SPLIT_FIELDS
.iter()
.find_map(|field| field_object(geometry, field))
else {
return Ok(Splits::default());
};
let count = scalar_i64(element, "Count").unwrap_or(0).max(0) as usize;
if count == 0 {
return Ok(Splits::default());
}
let Some(indices) = field_object(element, "PillarIndices") else {
return Err(Error::malformed(
at,
"split coordinate lines with no `PillarIndices`",
));
};
let split_of = crate::values::read_integers(indices, source, &format!("{at}.PillarIndices"))?;
if split_of.len() != count {
return Err(Error::malformed(
at,
format!(
"{count} split coordinate line(s) declared, {} pillar index/indices given",
split_of.len()
),
));
}
let Some(jagged) = field_object(element, "ColumnsPerSplitCoordinateLine") else {
return Err(Error::malformed(
at,
"split coordinate lines with no `ColumnsPerSplitCoordinateLine`",
));
};
let rows = values::jagged(jagged, source, &format!("{at}.ColumnsPerSplitCoordinateLine"))?;
if rows.len() != count {
return Err(Error::malformed(
at,
format!(
"{count} split coordinate line(s) declared, {} row(s) of columns",
rows.len()
),
));
}
let mut lookup = HashMap::new();
for (split, row) in rows.iter().enumerate() {
let pillar = split_of[split].max(0) as usize;
let Some(columns) = row.integer() else {
return Err(Error::malformed(
at,
"the columns of a split line are not integers",
));
};
for column in columns {
lookup.insert((pillar, (*column).max(0) as usize), pillars + split);
}
}
Ok(Splits {
count,
lookup,
of_pillar: split_of.iter().map(|pillar| (*pillar).max(0) as usize).collect(),
})
}
pub(crate) fn layer_tops(
representation: &dyn Reflect,
source: &dyn ValueSource,
nk: usize,
at: &str,
) -> Result<Vec<usize>> {
let Some(gaps) = field_object(representation, "KGaps") else {
return Ok((0..nk).collect());
};
let count = scalar_i64(gaps, "Count").unwrap_or(0).max(0) as usize;
if count == 0 {
return Ok((0..nk).collect());
}
let Some(array) = field_object(gaps, "GapAfterLayer") else {
return Err(Error::malformed(at, "`KGaps` with no `GapAfterLayer`"));
};
let after = values::read(array, source, &format!("{at}.KGaps.GapAfterLayer"))?;
let after = after
.boolean()
.ok_or_else(|| Error::malformed(at, "`GapAfterLayer` is not a boolean array"))?
.to_vec();
if after.len() + 1 != nk {
return Err(Error::malformed(
at,
format!(
"`GapAfterLayer` holds {} flag(s) for {nk} layer(s): expected {}",
after.len(),
nk.saturating_sub(1)
),
));
}
let declared = after.iter().filter(|gap| **gap).count();
if declared != count {
return Err(Error::malformed(
at,
format!("`KGaps` declares {count} gap(s), `GapAfterLayer` marks {declared}"),
));
}
let mut tops = Vec::with_capacity(nk);
let mut level = 0usize;
for k in 0..nk {
tops.push(level);
level += 1;
if after.get(k).copied().unwrap_or(false) {
level += 1; }
}
Ok(tops)
}
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!("`{field}` is {value}"))),
None => Err(Error::malformed(at, format!("no `{field}`"))),
}
}
fn field_object<'a>(object: &'a dyn Reflect, field: &str) -> Option<&'a dyn Reflect> {
match object.field(field)? {
FieldValue::Object(child) => Some(child),
_ => None,
}
}
fn flags(geometry: &dyn Reflect, source: &dyn ValueSource, field: &str) -> Option<Vec<bool>> {
let FieldValue::Object(array) = geometry.field(field)? else {
return None;
};
let values = values::read(array, source, field).ok()?;
values.boolean().map(|flags| flags.to_vec())
}
fn says_undefined(flags: &Option<Vec<bool>>, index: usize) -> bool {
flags
.as_ref()
.and_then(|flags| flags.get(index))
.is_some_and(|defined| !defined)
}
#[cfg(test)]
mod tests {
use super::*;
fn cube() -> Vec<Point> {
vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[1.0, 0.0, 1.0],
[1.0, 1.0, 1.0],
[0.0, 1.0, 1.0],
]
}
#[test]
fn a_cube_in_vtks_order_is_left_alone_and_its_mirror_is_put_back() {
let points = cube();
let right = [0, 1, 2, 3, 4, 5, 6, 7];
let wrong = [4, 5, 6, 7, 0, 1, 2, 3];
assert_eq!(oriented(right, &points), right);
assert_eq!(oriented(wrong, &points), right);
assert!((signed_volume(right, &points) - 1.0).abs() < 1e-12);
assert!((signed_volume(wrong, &points) + 1.0).abs() < 1e-12);
}
#[test]
fn the_twisted_cell_of_a_collapsed_pillar_is_oriented_by_its_volume() {
let points: Vec<Point> = vec![
[13591.28125, 13687.5, 3637.706298828125],
[13568.0126953125, 13850.400390625, 3622.7626953125],
[13585.7861328125, 13711.5, 3628.719482421875],
[13591.95703125, 13574.7880859375, 3645.5654296875],
[13608.0986328125, 13687.5, 3658.21923828125],
[13569.248046875, 13850.7216796875, 3624.921875],
[13586.5732421875, 13712.0, 3630.77099609375],
[13593.09375, 13575.03515625, 3647.5302734375],
];
let natural = [0, 1, 2, 3, 4, 5, 6, 7];
let swapped = [4, 5, 6, 7, 0, 1, 2, 3];
let (a, b) = (signed_volume(natural, &points), signed_volume(swapped, &points));
assert!(
a > 0.0 && b < 0.0,
"the two orderings must disagree about the sign: {a} and {b}"
);
assert!((a + b).abs() < 1e-6 * a.abs(), "{a} is not the negative of {b}");
assert_eq!(oriented(natural, &points), natural);
assert_eq!(oriented(swapped, &points), natural);
assert!(face_normal_measure(natural, &points) < 0.0);
assert!(face_normal_measure(swapped, &points) < 0.0);
}
fn face_normal_measure(node: [u32; 8], points: &[Point]) -> f64 {
let at = |corner: usize| points[node[corner] as usize];
let mut normal = [0.0f64; 3];
for index in 0..4 {
let (current, next) = (at(index), at((index + 1) % 4));
normal[0] += (current[1] - next[1]) * (current[2] + next[2]);
normal[1] += (current[2] - next[2]) * (current[0] + next[0]);
normal[2] += (current[0] - next[0]) * (current[1] + next[1]);
}
let centre = |from: usize| {
let mut sum = [0.0f64; 3];
for corner in from..from + 4 {
let point = at(corner);
for (axis, value) in sum.iter_mut().enumerate() {
*value += point[axis] / 4.0;
}
}
sum
};
let (a, b) = (centre(0), centre(4));
(0..3).map(|axis| normal[axis] * (b[axis] - a[axis])).sum()
}
#[test]
fn a_flat_cell_is_not_flipped_by_a_rounding_error() {
let flat: Vec<Point> = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
];
let node = [0, 1, 2, 3, 4, 5, 6, 7];
assert_eq!(signed_volume(node, &flat), 0.0);
assert_eq!(oriented(node, &flat), node);
}
}