use crate::core::{Cell, CellKind, Dataset, Error, Mesh, Point, Result};
use std::collections::BTreeMap;
use std::io::Write;
#[derive(Debug, Clone, PartialEq)]
pub struct Projection {
pub dataset: Dataset,
pub omitted_datasets: Vec<i64>,
pub tagged_elements: usize,
}
fn err(message: impl Into<String>) -> Error {
Error::new("E_UNV", message)
}
fn integers(line: &str, count: usize) -> Result<Vec<i64>> {
let words = line.split_whitespace().collect::<Vec<_>>();
if words.len() != count {
return Err(err(format!("expected {count} integer values")));
}
words
.into_iter()
.map(|word| word.parse().map_err(|_| err("invalid UNV integer")))
.collect()
}
fn label(number: i64) -> Result<u64> {
u64::try_from(number)
.ok()
.filter(|&number| number > 0)
.ok_or_else(|| err("UNV labels must be positive"))
}
fn coordinate(word: &str) -> Result<f64> {
word.replace(['D', 'd'], "E")
.parse::<f64>()
.ok()
.filter(|number| number.is_finite())
.ok_or_else(|| err("invalid UNV coordinate"))
}
fn kind(descriptor: i64) -> Result<CellKind> {
match descriptor {
11 | 21 | 22 | 31 => Ok(CellKind::Line2),
41 | 51 | 61 | 74 | 81 | 91 => Ok(CellKind::Triangle3),
44 | 54 | 64 | 71 | 84 | 94 => Ok(CellKind::Quad4),
111 => Ok(CellKind::Tet4),
101 | 112 => Ok(CellKind::Wedge6),
104 | 115 => Ok(CellKind::Hex8),
_ => Err(err(format!(
"unsupported UNV element descriptor {descriptor}"
))),
}
}
fn descriptor(kind: CellKind) -> Result<i64> {
match kind {
CellKind::Line2 => Ok(21),
CellKind::Triangle3 => Ok(91),
CellKind::Quad4 => Ok(94),
CellKind::Tet4 => Ok(111),
CellKind::Wedge6 => Ok(112),
CellKind::Hex8 => Ok(115),
CellKind::Pyramid5 => Err(err("linear pyramids have no supported UNV descriptor")),
}
}
fn next<'a>(lines: &mut impl Iterator<Item = &'a str>, what: &str) -> Result<&'a str> {
lines.next().ok_or_else(|| err(format!("truncated {what}")))
}
fn nodes<'a>(lines: &mut impl Iterator<Item = &'a str>, mesh: &mut Mesh, max: usize) -> Result<()> {
loop {
let header = next(lines, "2411 node section")?.trim();
if header == "-1" {
break;
}
let header = integers(header, 4)?;
let id = label(header[0])?;
let values = next(lines, "UNV node coordinates")?
.split_whitespace()
.map(coordinate)
.collect::<Result<Vec<_>>>()?;
if values.len() != 3 {
return Err(err("UNV node needs three coordinates"));
}
if mesh.points.len() >= max {
return Err(err("node count exceeds input size"));
}
mesh.points.push(Point {
id,
position: [values[0], values[1], values[2]],
});
}
Ok(())
}
fn elements<'a>(
lines: &mut impl Iterator<Item = &'a str>,
mesh: &mut Mesh,
max: usize,
) -> Result<usize> {
let positions = mesh
.points
.iter()
.enumerate()
.map(|(index, point)| (point.id, index))
.collect::<BTreeMap<_, _>>();
let mut tagged = 0;
loop {
let header = next(lines, "2412 element section")?.trim();
if header == "-1" {
break;
}
let header = integers(header, 6)?;
let id = label(header[0])?;
let kind = kind(header[1])?;
let arity = usize::try_from(header[5]).map_err(|_| err("invalid UNV node count"))?;
if arity != kind.node_count() {
return Err(err("UNV descriptor and node count disagree"));
}
if header[2..5].iter().any(|&tag| tag != 0) {
tagged += 1;
}
if matches!(header[1], 11 | 21 | 22 | 31) {
integers(next(lines, "UNV beam orientation")?, 3)?;
}
let mut connectivity = Vec::with_capacity(arity);
while connectivity.len() < arity {
let words = next(lines, "UNV connectivity")?
.split_whitespace()
.collect::<Vec<_>>();
if words.is_empty() || words.len() > arity - connectivity.len() {
return Err(err("invalid UNV connectivity row width"));
}
for word in words {
let id = label(word.parse().map_err(|_| err("invalid UNV node label"))?)?;
connectivity.push(
*positions
.get(&id)
.ok_or_else(|| err(format!("element references missing node {id}")))?,
);
}
}
if mesh.cells.len() >= max {
return Err(err("element count exceeds input size"));
}
mesh.cells.push(Cell {
id,
kind,
connectivity,
property_id: None,
});
}
Ok(tagged)
}
pub fn read_projection(source: &str) -> Result<Projection> {
let mut lines = source.lines();
let mut mesh = Mesh::default();
let mut seen_nodes = false;
let mut seen_elements = false;
let mut omitted_datasets = Vec::new();
let mut tagged_elements = 0;
while let Some(line) = lines.next() {
if line.trim() != "-1" {
return Err(err("expected UNV dataset delimiter -1"));
}
let number = next(&mut lines, "UNV dataset number")?
.trim()
.parse::<i64>()
.map_err(|_| err("invalid UNV dataset number"))?;
match number {
2411 if !seen_nodes && !seen_elements => {
nodes(&mut lines, &mut mesh, source.len() / 2)?;
seen_nodes = true;
}
2412 if seen_nodes && !seen_elements => {
tagged_elements = elements(&mut lines, &mut mesh, source.len() / 2)?;
seen_elements = true;
}
2411 | 2412 => return Err(err("duplicate or out-of-order UNV geometry section")),
_ => {
omitted_datasets.push(number);
loop {
if next(&mut lines, "UNV dataset")?.trim() == "-1" {
break;
}
}
}
}
}
if !seen_nodes || !seen_elements {
return Err(err("UNV requires 2411 nodes and 2412 elements"));
}
let dataset = Dataset {
mesh,
fields: Vec::new(),
};
dataset.validate()?;
Ok(Projection {
dataset,
omitted_datasets,
tagged_elements,
})
}
pub fn write_data(dataset: &Dataset, mut writer: impl Write) -> Result<()> {
dataset.validate()?;
dataset.mesh.require_no_sets("E_UNV")?;
if !dataset.fields.is_empty() {
return Err(err("UNV geometry writer cannot encode numeric fields"));
}
if dataset
.mesh
.cells
.iter()
.any(|cell| cell.property_id.is_some())
{
return Err(err("UNV geometry writer cannot encode property IDs"));
}
for point in &dataset.mesh.points {
i32::try_from(point.id).map_err(|_| err("node label exceeds Int32"))?;
}
for cell in &dataset.mesh.cells {
i32::try_from(cell.id).map_err(|_| err("element label exceeds Int32"))?;
descriptor(cell.kind)?;
}
writeln!(writer, " -1\n 2411")?;
for point in &dataset.mesh.points {
writeln!(writer, "{:10}{:10}{:10}{:10}", point.id, 1, 1, 11)?;
writeln!(
writer,
"{:25.16E}{:25.16E}{:25.16E}",
point.position[0], point.position[1], point.position[2]
)?;
}
writeln!(writer, " -1\n -1\n 2412")?;
for cell in &dataset.mesh.cells {
writeln!(
writer,
"{:10}{:10}{:10}{:10}{:10}{:10}",
cell.id,
descriptor(cell.kind)?,
0,
0,
7,
cell.kind.node_count()
)?;
if cell.kind == CellKind::Line2 {
writeln!(writer, " 0 0 0")?;
}
for chunk in cell.connectivity.chunks(8) {
for &index in chunk {
write!(writer, "{:10}", dataset.mesh.points[index].id)?;
}
writeln!(writer)?;
}
}
writeln!(writer, " -1")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::{read_projection, write_data};
use crate::core::{CellKind, Field, FieldLocation};
#[test]
fn gmsh_six_kind_fixture_round_trips() {
let source = include_str!("../../../tests/fixtures/gmsh-six-kind.unv");
let read = read_projection(source).unwrap();
assert_eq!(read.dataset.mesh.points.len(), 9);
assert_eq!(read.dataset.mesh.cells.len(), 6);
assert_eq!(read.omitted_datasets, vec![2477]);
assert_eq!(read.dataset.mesh.cells[0].kind, CellKind::Line2);
let mut output = Vec::new();
write_data(&read.dataset, &mut output).unwrap();
let second = read_projection(std::str::from_utf8(&output).unwrap()).unwrap();
assert_eq!(second.dataset, read.dataset);
}
#[test]
fn rejects_malformed_and_unsupported_records() {
assert!(read_projection(" -1\n 2411\n1 1 1 11\n0 0 0\n").is_err());
let source = include_str!("../../../tests/fixtures/gmsh-six-kind.unv");
assert!(read_projection(&source.replace(" 111", " 999")).is_err());
let missing_node = source.replacen(" 1 2", " 1 999", 1);
assert_ne!(missing_node, source);
assert!(read_projection(&missing_node).is_err());
}
#[test]
fn malformed_geometry_sections_fail_before_projection() {
let base = "-1\n2411\n1 1 1 11\n0 0 0\n2 1 1 11\n1 0 0\n-1\n-1\n2412\n1 21 0 0 7 2\n0 0 0\n1 2\n-1\n";
assert_eq!(read_projection(base).unwrap().dataset.mesh.cells.len(), 1);
for (old, new, expected) in [
("-1\n2411", "0\n2411", "expected UNV dataset delimiter"),
("1 1 1 11", "1 1 1", "expected 4 integer values"),
("0 0 0", "0 0", "UNV node needs three coordinates"),
(
"1 21 0 0 7 2",
"1 21 0 0 7 3",
"descriptor and node count disagree",
),
("1 2\n-1", "1 2 1\n-1", "invalid UNV connectivity row width"),
("-1\n2411", "-1\n2412", "duplicate or out-of-order"),
] {
let changed = base.replacen(old, new, 1);
let error = read_projection(&changed).unwrap_err();
assert!(error.message.contains(expected), "{new}: {error}");
}
assert!(
read_projection("-1\n2411\n-1\n")
.unwrap_err()
.message
.contains("requires 2411 nodes and 2412 elements")
);
}
#[test]
fn writer_rejects_results_and_solver_properties() {
let base = "-1\n2411\n1 1 1 11\n0 0 0\n2 1 1 11\n1 0 0\n-1\n-1\n2412\n1 21 0 0 7 2\n0 0 0\n1 2\n-1\n";
let original = read_projection(base).unwrap().dataset;
let mut invalid = original.clone();
invalid.fields.push(Field {
name: "temperature".into(),
location: FieldLocation::Point,
components: vec!["C1".into()],
values: vec![1.0, 2.0],
step: None,
time: None,
});
assert!(
write_data(&invalid, Vec::new())
.unwrap_err()
.message
.contains("numeric fields")
);
invalid = original;
invalid.mesh.cells[0].property_id = Some(7);
assert!(
write_data(&invalid, Vec::new())
.unwrap_err()
.message
.contains("property IDs")
);
}
}