use std::{
borrow::Cow,
collections::{HashMap, HashSet},
fmt,
io::{BufWriter, Write},
ops::Range,
path::{Path, PathBuf},
};
use crate::{
CellType, ConnectivityIndex, Coordinate, DATA_STORAGE, DataAttribute, DataStorage, DataWriter,
Error, Result, SELECTIONS, SUBMESH_CELLS, SUBMESH_POINTS, Values, create_writer,
error::io_ctx,
mpi_safe_create_dir_all, paraview,
values::GatherBuffers,
xdmf_elements::{
Information, Xdmf, attribute,
data_item::{DataContent, DataItem, Format, ItemType, NumberType},
dimensions::Dimensions,
geometry::{Geometry, GeometryType},
grid::{CollectionType, Grid, Time},
topology::{Topology, TopologyType},
},
};
pub struct TimeSeriesWriter {
xdmf_file_name: PathBuf,
writer: Box<dyn DataWriter>,
}
impl fmt::Debug for TimeSeriesWriter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TimeSeriesWriter")
.field("xdmf_file_name", &self.xdmf_file_name)
.field("data_storage", &self.writer.data_storage())
.finish()
}
}
impl TimeSeriesWriter {
pub fn new(file_name: impl AsRef<Path>, data_storage: DataStorage) -> Result<Self> {
let xdmf_file_name = file_name.as_ref().to_path_buf().with_extension("xdmf2");
validate_file_name(&xdmf_file_name)?;
if let Some(parent) = xdmf_file_name.parent() {
mpi_safe_create_dir_all(parent)?;
}
Ok(Self {
xdmf_file_name,
writer: create_writer(file_name.as_ref(), data_storage)?,
})
}
pub fn file_name(&self) -> &Path {
&self.xdmf_file_name
}
pub fn write_mesh<C: Coordinate, I: ConnectivityIndex>(
mut self,
points: &[C],
connectivity: &[I],
cell_types: &[CellType],
) -> Result<TimeSeriesDataWriter> {
validate_points_and_cells(points.len(), connectivity, cell_types)?;
let mesh = self.prepare_mesh(points, connectivity, cell_types)?;
let points_item = self.points_data_item(None, &C::as_values(points))?;
let connectivity_item = self.connectivity_data_item(None, &mesh.cells)?;
let topology = Topology {
topology_type: mesh.topology_type,
nodes_per_element: mesh.nodes_per_element,
number_of_elements: mesh.num_cells.to_string(),
data_item: DataItem::new_reference(&connectivity_item, DOMAIN_DATA_ITEMS),
};
let grid = Grid::new_uniform("mesh", geometry(&points_item), topology);
self.finish_mesh(
grid,
vec![points_item, connectivity_item],
Vec::new(),
mesh.num_points,
mesh.num_cells,
)
}
pub fn write_mesh_with_submeshes<'c, C, I, N, B>(
mut self,
points: &[C],
connectivity: &[I],
cell_types: &[CellType],
submeshes: impl IntoIterator<Item = (N, B)>,
) -> Result<TimeSeriesDataWriter>
where
C: Coordinate,
I: ConnectivityIndex,
N: AsRef<str>,
B: Into<SubmeshCells<'c>>,
{
validate_points_and_cells(points.len(), connectivity, cell_types)?;
let submeshes = prepare_submeshes(submeshes, num_cells(points.len(), cell_types))?;
let mesh = self.prepare_mesh(points, connectivity, cell_types)?;
let offsets = cell_offsets(cell_types, mesh.topology_type, mesh.num_cells);
let points = C::as_values(points);
let mut data_items = Vec::with_capacity(2 * submeshes.len());
let mut grids = Vec::with_capacity(submeshes.len());
let mut prepared = Vec::with_capacity(submeshes.len());
let mut gather_buffers = GatherBuffers::default();
let mut local_points = LocalPoints::default();
let mesh_coordinates = if self.writer.supports_selections() {
Some(self.write_mesh_coordinates(&points)?)
} else {
None
};
for (index, submesh) in submeshes.into_iter().enumerate() {
let points_of_submesh = submesh_points(
&mesh.cells,
&offsets,
cell_types,
mesh.topology_type,
&submesh.cells,
)?;
let (topology_type, nodes_per_element) = submesh_topology(
cell_types,
mesh.topology_type,
mesh.nodes_per_element,
&submesh.cells,
);
let mut cells = extract_connectivity(
&mesh.cells,
&offsets,
cell_types,
mesh.topology_type,
topology_type,
&submesh.cells,
);
renumber_connectivity(
&mut cells,
cell_types,
topology_type,
&submesh.cells,
&points_of_submesh,
&mut local_points,
)?;
let geometry = match &mesh_coordinates {
Some(coordinates) => {
let selected = selected_coordinates(
index,
coordinates,
&points_of_submesh,
mesh.num_points,
);
let geometry = selected_geometry(&selected);
data_items.extend(selected);
geometry
}
None => {
let submesh_coords = match &points_of_submesh {
IndexList::Contiguous { start, len } => points.slice(start * 3, len * 3),
IndexList::Scattered(indices) => gather_buffers.gather(&points, 3, indices),
};
let points_item = self.points_data_item(Some(index), &submesh_coords)?;
let geometry = geometry(&points_item);
data_items.push(points_item);
geometry
}
};
let connectivity_item = self.connectivity_data_item(Some(index), &cells)?;
let topology = Topology {
topology_type,
nodes_per_element,
number_of_elements: submesh.cells.len().to_string(),
data_item: DataItem::new_reference(&connectivity_item, DOMAIN_DATA_ITEMS),
};
grids.push(Grid::new_uniform(&submesh.name, geometry, topology));
data_items.push(connectivity_item);
prepared.push(Submesh {
name: submesh.name,
cells: submesh.cells,
points: points_of_submesh,
});
}
let grid = Grid::new_collection("mesh", CollectionType::Spatial, Some(grids));
self.finish_mesh(grid, data_items, prepared, mesh.num_points, mesh.num_cells)
}
fn prepare_mesh<'c, C: Coordinate, I: ConnectivityIndex>(
&mut self,
points: &[C],
connectivity: &'c [I],
cell_types: &[CellType],
) -> Result<PreparedMesh<'c, I>> {
let num_cells = num_cells(points.len(), cell_types);
let points = C::as_values(points);
let num_points = points.len() / 3;
let (topology_type, cells) = prepare_cells(connectivity, cell_types, num_points)?;
paraview::validate(&I::as_values(&cells), self.writer.format())?;
let nodes_per_element = (topology_type != TopologyType::Mixed)
.then(|| poly_cell_points(cell_types.first().copied().unwrap_or(CellType::Vertex)))
.flatten();
Ok(PreparedMesh {
num_points,
num_cells,
topology_type,
nodes_per_element,
cells,
})
}
fn points_data_item(
&mut self,
submesh: Option<usize>,
points: &Values<'_>,
) -> Result<DataItem> {
let format = self.writer.format();
let name = match submesh {
Some(index) => format!("coords_{index}"),
None => "coords".to_string(),
};
Ok(DataItem {
name: Some(name),
item_type: None,
dimensions: Some(Dimensions(vec![points.len() / 3, 3])),
data: self.writer.write_points(submesh, points)?,
number_type: Some(points.number_type()),
precision: Some(points.precision()),
format: Some(format),
endian: format.endian(),
reference: None,
})
}
fn write_mesh_coordinates(&mut self, points: &Values<'_>) -> Result<[DataItem; 3]> {
let mut buffers = GatherBuffers::default();
Ok([
self.write_coordinate_component(points, 0, &mut buffers)?,
self.write_coordinate_component(points, 1, &mut buffers)?,
self.write_coordinate_component(points, 2, &mut buffers)?,
])
}
fn write_coordinate_component(
&mut self,
points: &Values<'_>,
component: usize,
buffers: &mut GatherBuffers,
) -> Result<DataItem> {
let format = self.writer.format();
let coordinates = buffers.component(points, 3, component);
Ok(DataItem {
name: None,
item_type: None,
dimensions: Some(Dimensions(vec![coordinates.len()])),
data: self.writer.write_point_component(component, &coordinates)?,
number_type: Some(coordinates.number_type()),
precision: Some(coordinates.precision()),
format: Some(format),
endian: format.endian(),
reference: None,
})
}
fn connectivity_data_item<I: ConnectivityIndex>(
&mut self,
submesh: Option<usize>,
cells: &[I],
) -> Result<DataItem> {
let values = I::as_values(cells);
let format = self.writer.format();
let name = match submesh {
Some(index) => format!("connectivity_{index}"),
None => "connectivity".to_string(),
};
Ok(DataItem {
name: Some(name),
item_type: None,
dimensions: Some(Dimensions(vec![cells.len()])),
data: self.writer.write_connectivity(submesh, &values)?,
number_type: Some(values.number_type()),
precision: Some(values.precision()),
format: Some(format),
endian: format.endian(),
reference: None,
})
}
fn write_submesh_index_lists(
&mut self,
submeshes: &[Submesh],
data_items: &mut Vec<DataItem>,
selections: &mut HashMap<SelectionKey, DataItem>,
) -> Result<Vec<Information>> {
if submeshes.is_empty() {
return Ok(Vec::new());
}
let cells = self.write_submesh_index_list(
SUBMESH_CELLS,
submeshes,
|submesh| &submesh.cells,
|writer, index, values| writer.write_submesh_cells(index, values),
data_items,
selections,
)?;
let points = self.write_submesh_index_list(
SUBMESH_POINTS,
submeshes,
|submesh| &submesh.points,
|writer, index, values| writer.write_submesh_points(index, values),
data_items,
selections,
)?;
if self.writer.supports_selections() {
return Ok(vec![cells]);
}
Ok(vec![cells, points])
}
fn write_submesh_index_list(
&mut self,
array: &str,
submeshes: &[Submesh],
select: fn(&Submesh) -> &IndexList,
write: fn(&mut dyn DataWriter, usize, &Values<'_>) -> Result<DataContent>,
data_items: &mut Vec<DataItem>,
selections: &mut HashMap<SelectionKey, DataItem>,
) -> Result<Information> {
let format = self.writer.format();
let mut entries = Vec::with_capacity(submeshes.len());
for (index, submesh) in submeshes.iter().enumerate() {
let indices = match select(submesh) {
IndexList::Contiguous { start, len } => {
entries.push(format!("{start}:{len}"));
continue;
}
IndexList::Scattered(indices) => indices,
};
let values = index_values(indices)?;
let name = submesh_index_name(array, index);
let item = DataItem {
name: Some(name.clone()),
item_type: None,
dimensions: Some(Dimensions(vec![values.len()])),
data: write(self.writer.as_mut(), index, &values)?,
number_type: Some(values.number_type()),
precision: Some(values.precision()),
format: Some(format),
endian: format.endian(),
reference: None,
};
selections.insert(
SelectionKey {
submesh: index,
point_data: array == SUBMESH_POINTS,
components: 1,
},
item.clone(),
);
data_items.push(item);
entries.push(name);
}
Ok(Information::new(array, entries.join(" ")))
}
fn finish_mesh(
mut self,
grid: Grid,
mut data_items: Vec<DataItem>,
submeshes: Vec<Submesh>,
num_points: usize,
num_cells: usize,
) -> Result<TimeSeriesDataWriter> {
let mut selections = HashMap::new();
let submesh_lists =
self.write_submesh_index_lists(&submeshes, &mut data_items, &mut selections)?;
let mut xdmf = new_document(grid.clone(), data_items, self.writer.data_storage());
xdmf.information.extend(submesh_lists);
let mut ts_writer = TimeSeriesDataWriter {
xdmf_file_name: self.xdmf_file_name,
writer: self.writer,
xdmf,
grid,
step_times: Vec::new(),
submeshes,
selections,
next_selection_index: 0,
gather_buffers: GatherBuffers::default(),
written_times: HashMap::new(),
num_points,
num_cells,
};
ts_writer.write_xdmf_file()?;
Ok(ts_writer)
}
}
fn append_to_collection(collection: &mut Grid, grids: Vec<Grid>) {
collection.grids.get_or_insert_with(Vec::new).extend(grids);
}
fn new_document(grid: Grid, data_items: Vec<DataItem>, data_storage: DataStorage) -> Xdmf {
let mut xdmf = Xdmf {
information: vec![
Information::new(DATA_STORAGE, format!("{data_storage:?}")),
Information::new("version", env!("CARGO_PKG_VERSION")),
],
..Default::default()
};
xdmf.domains[0].grids.push(grid);
xdmf.domains[0].data_items = data_items;
xdmf
}
fn num_cells(num_coordinates: usize, cell_types: &[CellType]) -> usize {
if cell_types.is_empty() {
num_coordinates / 3
} else {
cell_types.len()
}
}
const DOMAIN_DATA_ITEMS: &str = "/Xdmf/Domain/DataItem";
fn index_values(indices: &[usize]) -> Result<Values<'static>> {
if let Some(indices) = indices
.iter()
.map(|&index| i32::try_from(index).ok())
.collect::<Option<Vec<i32>>>()
{
return Ok(Values::from(indices));
}
let indices = indices
.iter()
.map(|&index| i64::try_from(index).ok())
.collect::<Option<Vec<i64>>>()
.ok_or(Error::Internal("an index does not fit into 64 bits"))?;
Ok(Values::from(indices))
}
struct PreparedMesh<'c, I: Clone> {
num_points: usize,
num_cells: usize,
topology_type: TopologyType,
nodes_per_element: Option<u8>,
cells: Cow<'c, [I]>,
}
fn selected_coordinates(
submesh: usize,
coordinates: &[DataItem; 3],
points: &IndexList,
num_points: usize,
) -> Vec<DataItem> {
coordinates
.iter()
.zip(["x", "y", "z"])
.map(|(source, direction)| {
let selector = match points {
IndexList::Contiguous { start, len } => hyper_slab(*start, *len, 1),
IndexList::Scattered(_) => submesh_index_reference(SUBMESH_POINTS, submesh),
};
let mut item = selection(selector, source, points.len(), &[num_points]);
item.name = Some(format!("coords_{submesh}_{direction}"));
item
})
.collect()
}
fn selected_geometry(coordinate_items: &[DataItem]) -> Geometry {
Geometry {
geometry_type: GeometryType::XYZSeparate,
data_items: coordinate_items
.iter()
.map(|item| DataItem::new_reference(item, DOMAIN_DATA_ITEMS))
.collect(),
}
}
fn submesh_index_name(array: &str, submesh: usize) -> String {
format!("{array}_{submesh}")
}
fn submesh_index_reference(array: &str, submesh: usize) -> DataItem {
DataItem::new_reference(
&DataItem {
name: Some(submesh_index_name(array, submesh)),
..Default::default()
},
DOMAIN_DATA_ITEMS,
)
}
fn geometry(points_item: &DataItem) -> Geometry {
Geometry {
geometry_type: GeometryType::XYZ,
data_items: vec![DataItem::new_reference(points_item, DOMAIN_DATA_ITEMS)],
}
}
#[derive(Debug)]
struct PreparedSubmesh {
name: String,
cells: IndexList,
}
#[derive(Debug)]
struct Submesh {
name: String,
cells: IndexList,
points: IndexList,
}
fn entities_of(submesh: &Submesh, point_data: bool) -> &IndexList {
if point_data {
&submesh.points
} else {
&submesh.cells
}
}
#[derive(Debug, Eq, Hash, PartialEq)]
struct SelectionKey {
submesh: usize,
point_data: bool,
components: usize,
}
#[derive(Debug)]
enum IndexList {
Contiguous { start: usize, len: usize },
Scattered(Vec<usize>),
}
impl IndexList {
fn len(&self) -> usize {
match self {
Self::Contiguous { len, .. } => *len,
Self::Scattered(indices) => indices.len(),
}
}
fn is_ascending(&self) -> bool {
match self {
Self::Contiguous { .. } => true,
Self::Scattered(indices) => indices.windows(2).all(|pair| pair[0] < pair[1]),
}
}
fn iter(&self) -> impl Iterator<Item = usize> + '_ {
let (run, indices) = match self {
Self::Contiguous { start, len } => (*start..*start + *len, [].as_slice()),
Self::Scattered(indices) => (0..0, indices.as_slice()),
};
run.chain(indices.iter().copied())
}
}
#[derive(Clone, Debug)]
pub enum SubmeshCells<'a> {
Range(Range<usize>),
Indices(Cow<'a, [usize]>),
}
impl SubmeshCells<'_> {
fn into_index_list(self) -> IndexList {
match self {
Self::Range(range) => IndexList::Contiguous {
start: range.start,
len: range.end.saturating_sub(range.start),
},
Self::Indices(Cow::Borrowed(indices)) => collapse_indices(indices),
Self::Indices(Cow::Owned(indices)) => {
if is_contiguous(&indices) {
return IndexList::Contiguous {
start: indices.first().copied().unwrap_or(0),
len: indices.len(),
};
}
IndexList::Scattered(indices)
}
}
}
}
impl From<Range<usize>> for SubmeshCells<'_> {
fn from(range: Range<usize>) -> Self {
Self::Range(range)
}
}
impl<'a> From<&'a [usize]> for SubmeshCells<'a> {
fn from(indices: &'a [usize]) -> Self {
Self::Indices(Cow::Borrowed(indices))
}
}
impl<'a> From<&'a Vec<usize>> for SubmeshCells<'a> {
fn from(indices: &'a Vec<usize>) -> Self {
Self::Indices(Cow::Borrowed(indices))
}
}
impl<'a, const N: usize> From<&'a [usize; N]> for SubmeshCells<'a> {
fn from(indices: &'a [usize; N]) -> Self {
Self::Indices(Cow::Borrowed(indices))
}
}
impl From<Vec<usize>> for SubmeshCells<'_> {
fn from(indices: Vec<usize>) -> Self {
Self::Indices(Cow::Owned(indices))
}
}
impl<const N: usize> From<[usize; N]> for SubmeshCells<'_> {
fn from(indices: [usize; N]) -> Self {
Self::Indices(Cow::Owned(indices.to_vec()))
}
}
fn prepare_submeshes<'c, N: AsRef<str>, B: Into<SubmeshCells<'c>>>(
submeshes: impl IntoIterator<Item = (N, B)>,
num_cells: usize,
) -> Result<Vec<PreparedSubmesh>> {
let mut prepared: Vec<PreparedSubmesh> = Vec::new();
let mut names = HashSet::new();
let mut covered = CellBitSet::new(num_cells);
let mut claimed_here = CellBitSet::new(num_cells);
for (name, cells) in submeshes {
let name = name.as_ref();
let cells = cells.into().into_index_list();
if !is_valid_data_name(name) {
return Err(Error::InvalidMesh {
reason: format!(
"submesh name '{name}' is not valid, must contain a non-whitespace character \
and must not contain control characters"
),
});
}
if !names.insert(name.to_string()) {
return Err(Error::InvalidMesh {
reason: format!("submesh name '{name}' is used more than once"),
});
}
if cells.len() == 0 {
return Err(Error::InvalidMesh {
reason: format!("submesh '{name}' is empty, it must contain at least one cell"),
});
}
match &cells {
IndexList::Contiguous { start, len } => {
let end = start.checked_add(*len).ok_or(Error::Internal(
"a submesh's cell range does not fit a usize",
))?;
if end > num_cells {
return Err(Error::InvalidMesh {
reason: format!(
"submesh '{name}' references cell {}, but the mesh only has \
{num_cells} cells",
end - 1
),
});
}
for index in *start..end {
covered.insert(index);
}
}
IndexList::Scattered(indices) => {
for &index in indices {
if index >= num_cells {
return Err(Error::InvalidMesh {
reason: format!(
"submesh '{name}' references cell {index}, but the mesh only has \
{num_cells} cells"
),
});
}
if claimed_here.contains(index) {
return Err(Error::InvalidMesh {
reason: format!(
"submesh '{name}' contains cell {index} more than once"
),
});
}
claimed_here.insert(index);
covered.insert(index);
}
for &index in indices {
claimed_here.remove(index);
}
}
}
prepared.push(PreparedSubmesh {
name: name.to_string(),
cells,
});
}
if prepared.is_empty() {
return Err(Error::InvalidMesh {
reason: "at least one submesh is required".to_string(),
});
}
check_all_cells_covered(&covered)?;
Ok(prepared)
}
struct CellBitSet {
words: Vec<u64>,
len: usize,
}
impl CellBitSet {
const BITS: usize = u64::BITS as usize;
fn new(len: usize) -> Self {
Self {
words: vec![0; len.div_ceil(Self::BITS)],
len,
}
}
fn contains(&self, index: usize) -> bool {
self.words[index / Self::BITS] & (1 << (index % Self::BITS)) != 0
}
fn insert(&mut self, index: usize) {
self.words[index / Self::BITS] |= 1 << (index % Self::BITS);
}
fn remove(&mut self, index: usize) {
self.words[index / Self::BITS] &= !(1 << (index % Self::BITS));
}
fn missing(&self) -> impl Iterator<Item = usize> + '_ {
(0..self.len).filter(move |&index| !self.contains(index))
}
}
fn is_contiguous(cells: &[usize]) -> bool {
cells.first().is_none_or(|&start| {
cells
.iter()
.enumerate()
.all(|(offset, &index)| index == start + offset)
})
}
fn collapse_indices(cells: &[usize]) -> IndexList {
if is_contiguous(cells) {
IndexList::Contiguous {
start: cells.first().copied().unwrap_or(0),
len: cells.len(),
}
} else {
IndexList::Scattered(cells.to_vec())
}
}
fn check_all_cells_covered(covered: &CellBitSet) -> Result<()> {
const MAX_LISTED: usize = 10;
let mut uncovered = covered.missing();
let listed_indices: Vec<usize> = uncovered.by_ref().take(MAX_LISTED).collect();
if listed_indices.is_empty() {
return Ok(());
}
let num_not_listed = uncovered.count();
let num_uncovered = listed_indices.len() + num_not_listed;
let listed = listed_indices
.iter()
.map(usize::to_string)
.collect::<Vec<_>>()
.join(", ");
let ellipsis = if num_not_listed > 0 {
format!(", ... ({num_not_listed} more)")
} else {
String::new()
};
Err(Error::InvalidMesh {
reason: format!(
"{num_uncovered} of {} cells belong to no submesh: {listed}{ellipsis}. Every cell \
must be in at least one submesh; leave the others out of the mesh instead",
covered.len
),
})
}
fn cell_offsets(
cell_types: &[CellType],
topology_type: TopologyType,
num_cells: usize,
) -> Vec<usize> {
let mut offsets = Vec::with_capacity(num_cells + 1);
let mut offset = 0;
for cell in 0..num_cells {
offsets.push(offset);
offset += cell_span(cell_types, topology_type, cell);
}
offsets.push(offset);
offsets
}
fn cell_span(cell_types: &[CellType], topology_type: TopologyType, cell: usize) -> usize {
let Some(cell_type) = cell_types.get(cell) else {
return 1;
};
leading_entries(cell_types, topology_type, cell) + cell_type.num_points()
}
fn extract_connectivity<I: ConnectivityIndex>(
cells: &[I],
offsets: &[usize],
cell_types: &[CellType],
mesh_topology: TopologyType,
submesh_topology: TopologyType,
submesh: &IndexList,
) -> Vec<I> {
let size = submesh
.iter()
.map(|cell| cell_span(cell_types, submesh_topology, cell))
.sum();
let mut extracted = Vec::with_capacity(size);
for cell in submesh.iter() {
let dropped = leading_entries(cell_types, mesh_topology, cell)
- leading_entries(cell_types, submesh_topology, cell);
extracted.extend_from_slice(&cells[offsets[cell] + dropped..offsets[cell + 1]]);
}
extracted
}
fn submesh_topology(
cell_types: &[CellType],
mesh_topology: TopologyType,
mesh_nodes_per_element: Option<u8>,
cells: &IndexList,
) -> (TopologyType, Option<u8>) {
let mesh = (mesh_topology, mesh_nodes_per_element);
if mesh_topology != TopologyType::Mixed {
return mesh;
}
let mut cells = cells.iter();
let Some(first) = cells.next().map(|cell| cell_types[cell]) else {
return mesh;
};
if cells.any(|cell| cell_types[cell] != first) {
return mesh;
}
(TopologyType::from(first), poly_cell_points(first))
}
fn leading_entries(cell_types: &[CellType], topology_type: TopologyType, cell: usize) -> usize {
if topology_type != TopologyType::Mixed {
return 0;
}
1 + usize::from(poly_cell_points(cell_types[cell]).is_some())
}
fn submesh_points<I: ConnectivityIndex>(
cells: &[I],
offsets: &[usize],
cell_types: &[CellType],
topology_type: TopologyType,
submesh: &IndexList,
) -> Result<IndexList> {
let mut points = Vec::new();
for cell in submesh.iter() {
let start = offsets[cell] + leading_entries(cell_types, topology_type, cell);
for entry in &cells[start..offsets[cell + 1]] {
points.push(index_as_usize(*entry)?);
}
}
points.sort_unstable();
points.dedup();
Ok(collapse_indices(&points))
}
#[derive(Default)]
struct LocalPoints {
of_point: Vec<usize>,
}
impl LocalPoints {
fn fill(&mut self, points: &[usize]) {
let needed = points.last().map_or(0, |last| last + 1);
if self.of_point.len() < needed {
self.of_point.resize(needed, 0);
}
for (local, &point) in points.iter().enumerate() {
self.of_point[point] = local;
}
}
}
fn renumber_connectivity<I: ConnectivityIndex>(
cells: &mut [I],
cell_types: &[CellType],
submesh_topology: TopologyType,
submesh: &IndexList,
points: &IndexList,
local_points: &mut LocalPoints,
) -> Result<()> {
if let IndexList::Scattered(points) = points {
local_points.fill(points);
}
let mut position = 0;
for cell in submesh.iter() {
let leading = leading_entries(cell_types, submesh_topology, cell);
let span = cell_span(cell_types, submesh_topology, cell);
for entry in &mut cells[position + leading..position + span] {
let point = index_as_usize(*entry)?;
let local = match points {
IndexList::Contiguous { start, .. } => point - start,
IndexList::Scattered(_) => local_points.of_point[point],
};
*entry = I::from_index(local).ok_or(Error::Internal(
"a point index does not fit the connectivity type",
))?;
}
position += span;
}
Ok(())
}
fn index_as_usize<I: ConnectivityIndex>(index: I) -> Result<usize> {
usize::try_from(index.as_i128())
.ok()
.ok_or(Error::Internal("a connectivity entry is not a point index"))
}
fn validate_points_and_cells<I: ConnectivityIndex>(
num_coordinates: usize,
connectivity: &[I],
cell_types: &[CellType],
) -> Result<()> {
if num_coordinates == 0 {
return Err(Error::InvalidMesh {
reason: "at least one point is required".to_string(),
});
}
if !num_coordinates.is_multiple_of(3) {
return Err(Error::InvalidMesh {
reason: format!(
"points must have 3 dimensions, but {num_coordinates} is not a multiple of 3"
),
});
}
let num_points = num_coordinates / 3;
if num_points as i128 - 1 > I::MAX_INDEX {
return Err(Error::InvalidMesh {
reason: format!(
"the mesh has {num_points} points, but its connectivity type can only index up \
to {}; a wider one is needed to write it",
I::MAX_INDEX
),
});
}
for index in connectivity {
let index = index.as_i128();
if index < 0 {
return Err(Error::InvalidMesh {
reason: format!("connectivity index {index} is negative"),
});
}
if index >= num_points as i128 {
return Err(Error::InvalidMesh {
reason: format!(
"connectivity index {index} is out of bounds, the mesh only has \
{num_points} points"
),
});
}
}
let exp_num_points: usize = cell_types.iter().map(|ct| ct.num_points()).sum();
if exp_num_points != connectivity.len() {
return Err(Error::InvalidMesh {
reason: format!(
"size of connectivity ({}) does not match the number expected from the cell types ({exp_num_points})",
connectivity.len()
),
});
}
Ok(())
}
fn poly_cell_points(cell_type: CellType) -> Option<u8> {
match cell_type {
CellType::Vertex => {
Some(1)
}
CellType::Edge => {
Some(2)
}
_ => None,
}
}
fn prepare_cells<'c, I: ConnectivityIndex>(
connectivity: &'c [I],
cell_types: &[CellType],
num_points: usize,
) -> Result<(TopologyType, Cow<'c, [I]>)> {
let index_fits = || Error::Internal("a point index does not fit the connectivity type");
if cell_types.is_empty() {
let indices = (0..num_points)
.map(|index| I::from_index(index).ok_or_else(index_fits))
.collect::<Result<Vec<_>>>()?;
return Ok((TopologyType::Polyvertex, Cow::Owned(indices)));
}
if let [first, rest @ ..] = cell_types
&& rest.iter().all(|cell_type| cell_type == first)
{
return Ok((TopologyType::from(*first), Cow::Borrowed(connectivity)));
}
let mut cells_with_types = Vec::with_capacity(connectivity.len() + cell_types.len());
let mut index = 0_usize;
for cell_type in cell_types {
let num_points = cell_type.num_points();
cells_with_types.push(I::from_u8(*cell_type as u8));
if let Some(n_points_poly) = poly_cell_points(*cell_type) {
cells_with_types.push(I::from_u8(n_points_poly));
}
cells_with_types.extend_from_slice(&connectivity[index..index + num_points]);
index += num_points; }
Ok((TopologyType::Mixed, Cow::Owned(cells_with_types)))
}
pub struct TimeSeriesDataWriter {
xdmf_file_name: PathBuf,
writer: Box<dyn DataWriter>,
xdmf: Xdmf,
grid: Grid,
step_times: Vec<String>,
submeshes: Vec<Submesh>,
selections: HashMap<SelectionKey, DataItem>,
next_selection_index: usize,
gather_buffers: GatherBuffers,
written_times: HashMap<u64, String>,
num_points: usize,
num_cells: usize,
}
impl fmt::Debug for TimeSeriesDataWriter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TimeSeriesDataWriter")
.field("xdmf_file_name", &self.xdmf_file_name)
.field("data_storage", &self.writer.data_storage())
.field("num_points", &self.num_points)
.field("num_cells", &self.num_cells)
.field(
"submeshes",
&self
.submeshes
.iter()
.map(|submesh| submesh.name.as_str())
.collect::<Vec<_>>(),
)
.field("written_times", &self.step_times)
.finish_non_exhaustive()
}
}
impl TimeSeriesDataWriter {
pub fn file_name(&self) -> &Path {
&self.xdmf_file_name
}
fn push_step(
&mut self,
time: &str,
shared: Vec<attribute::Attribute>,
per_submesh: Vec<Vec<attribute::Attribute>>,
) -> Result<()> {
let step_grids = self.build_step_grids(time, shared, per_submesh);
if self.step_times.is_empty() {
self.xdmf.domains[0].grids = vec![self.wrap_first_step(step_grids)];
return Ok(());
}
let root = self.xdmf.domains[0]
.grids
.first_mut()
.ok_or(Error::Internal(
"the document lost the collection holding the time steps",
))?;
if root.collection_type == Some(CollectionType::Temporal) {
append_to_collection(root, step_grids);
} else {
for (collection, grid) in root.grids.iter_mut().flatten().zip(step_grids) {
append_to_collection(collection, vec![grid]);
}
}
Ok(())
}
fn build_step_grids(
&self,
time: &str,
shared: Vec<attribute::Attribute>,
per_submesh: Vec<Vec<attribute::Attribute>>,
) -> Vec<Grid> {
if self.submeshes.is_empty() {
let mut grid = self.grid.clone();
grid.name = format!("time_series-t{time}");
grid.time = Some(Time::new(time));
grid.attributes = Some(shared);
return vec![grid];
}
self.grid
.grids
.iter()
.flatten()
.zip(per_submesh)
.map(|(submesh_grid, cell_attributes)| {
let mut grid = submesh_grid.clone();
grid.name = format!("{}-t{time}", submesh_grid.name);
grid.time = Some(Time::new(time));
let mut attributes = shared.clone();
attributes.extend(cell_attributes);
grid.attributes = Some(attributes);
grid
})
.collect()
}
fn wrap_first_step(&self, step_grids: Vec<Grid>) -> Grid {
if self.submeshes.is_empty() {
return Grid::new_collection("time_series", CollectionType::Temporal, Some(step_grids));
}
let collections = self
.submeshes
.iter()
.zip(step_grids)
.map(|(submesh, grid)| {
Grid::new_collection(&submesh.name, CollectionType::Temporal, Some(vec![grid]))
})
.collect();
Grid::new_collection("mesh", CollectionType::Spatial, Some(collections))
}
pub fn write_time_step<F, E>(&mut self, time: &str, write_step: F) -> Result<(), E>
where
F: FnOnce(&mut TimeStep<'_>) -> Result<(), E>,
E: From<Error>,
{
let parsed_time = time
.parse::<f64>()
.map_err(|_parse_error| Error::InvalidTimeStep {
time: time.to_string(),
reason: "must be a valid float".to_string(),
})?;
if !parsed_time.is_finite() {
return Err(Error::InvalidTimeStep {
time: time.to_string(),
reason: "must be a finite float".to_string(),
}
.into());
}
let time_bits = if parsed_time == 0.0 { 0.0 } else { parsed_time }.to_bits();
if let Some(existing) = self.written_times.get(&time_bits) {
let reason = if existing == time {
"already written".to_string()
} else {
format!("already written (as '{existing}')")
};
return Err(Error::InvalidTimeStep {
time: time.to_string(),
reason,
}
.into());
}
let mut step = TimeStep {
per_submesh: vec![Vec::new(); self.submeshes.len()],
writer: self,
time: time.to_string(),
time_bits,
attributes: Vec::new(),
point_names: HashSet::new(),
cell_names: HashSet::new(),
initialized: false,
next_array_index: 0,
};
match write_step(&mut step) {
Ok(()) => step.finish().map_err(E::from),
Err(error) => {
let _discard_result = step.discard();
Err(error)
}
}
}
fn write_xdmf_file(&mut self) -> Result<()> {
self.writer.flush()?;
let temp_xdmf_file_name = self.xdmf_file_name.with_extension("xdmf.tmp");
let mut xdmf_file = BufWriter::new(
std::fs::File::create(&temp_xdmf_file_name)
.map_err(io_ctx("creating XDMF file", &temp_xdmf_file_name))?,
);
self.xdmf
.write_to(&mut xdmf_file)
.map_err(io_ctx("writing XDMF XML", &temp_xdmf_file_name))?;
xdmf_file
.flush()
.map_err(io_ctx("flushing XDMF file", &temp_xdmf_file_name))?;
std::fs::rename(&temp_xdmf_file_name, &self.xdmf_file_name)
.map_err(io_ctx("renaming XDMF file", &temp_xdmf_file_name))
}
}
pub struct TimeStep<'a> {
writer: &'a mut TimeSeriesDataWriter,
time: String,
time_bits: u64,
attributes: Vec<attribute::Attribute>,
per_submesh: Vec<Vec<attribute::Attribute>>,
point_names: HashSet<String>,
cell_names: HashSet<String>,
initialized: bool,
next_array_index: usize,
}
impl fmt::Debug for TimeStep<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TimeStep")
.field("time", &self.time)
.field("point_data", &sorted_names(&self.point_names))
.field("cell_data", &sorted_names(&self.cell_names))
.finish_non_exhaustive()
}
}
fn sorted_names(names: &HashSet<String>) -> Vec<&str> {
let mut names: Vec<&str> = names.iter().map(String::as_str).collect();
names.sort_unstable();
names
}
impl TimeStep<'_> {
pub fn point_data<'v>(
&mut self,
name: &str,
attribute: DataAttribute,
data: impl Into<Values<'v>>,
) -> Result<()> {
self.write_attribute(name, attribute, data.into(), attribute::Center::Node)
}
pub fn cell_data<'v>(
&mut self,
name: &str,
attribute: DataAttribute,
data: impl Into<Values<'v>>,
) -> Result<()> {
self.write_attribute(name, attribute, data.into(), attribute::Center::Cell)
}
fn write_attribute(
&mut self,
name: &str,
data_attribute: DataAttribute,
values: Values<'_>,
center: attribute::Center,
) -> Result<()> {
let is_point_data = center == attribute::Center::Node;
let (label, num_entities) = if is_point_data {
(POINT_DATA, self.writer.num_points)
} else {
(CELL_DATA, self.writer.num_cells)
};
if !is_valid_data_name(name) {
return Err(Error::InvalidData {
reason: format!(
"data name '{name}' of {label} is not valid, must contain a \
non-whitespace character and must not contain control characters"
),
});
}
let seen_names = if is_point_data {
&self.point_names
} else {
&self.cell_names
};
if seen_names.contains(name) {
return Err(Error::InvalidData {
reason: format!("name '{name}' of {label} is used more than once"),
});
}
let stride = data_attribute
.size()
.filter(|size| *size != 0)
.ok_or_else(|| Error::InvalidData {
reason: format!(
"attribute type {data_attribute:?} of {label} '{name}' has no usable size: \
its number of components must be non-zero and must itself fit a usize"
),
})?;
let exp_size = num_entities
.checked_mul(stride)
.ok_or_else(|| Error::InvalidData {
reason: format!(
"attribute type {data_attribute:?} of {label} '{name}' describes \
{num_entities} entities of {stride} components each, whose total does not \
fit a usize"
),
})?;
if values.len() != exp_size {
return Err(Error::InvalidData {
reason: format!(
"size of {label} '{name}' must be {exp_size}, but is {}",
values.len()
),
});
}
paraview::validate(&values, self.writer.writer.format())?;
if !self.initialized {
self.writer.writer.write_data_initialize(&self.time)?;
self.initialized = true;
}
if self.writer.submeshes.is_empty() {
let index = self.take_array_index();
let attribute = build_attribute(
self.writer.writer.as_mut(),
index,
name,
data_attribute,
&values,
center,
)?;
self.attributes.push(attribute);
} else {
self.write_data_per_submesh(name, data_attribute, stride, &values, center)?;
}
if is_point_data {
self.point_names.insert(name.to_string());
} else {
self.cell_names.insert(name.to_string());
}
Ok(())
}
fn take_array_index(&mut self) -> usize {
let index = self.next_array_index;
self.next_array_index += 1;
index
}
fn write_data_per_submesh(
&mut self,
name: &str,
data_attribute: DataAttribute,
stride: usize,
values: &Values<'_>,
center: attribute::Center,
) -> Result<()> {
let point_data = center == attribute::Center::Node;
let selects = self.writer.writer.supports_selections()
&& self
.writer
.submeshes
.iter()
.any(|submesh| entities_of(submesh, point_data).is_ascending());
if selects {
return self.write_data_selected(name, data_attribute, stride, values, center);
}
let TimeSeriesDataWriter {
writer,
submeshes,
gather_buffers,
..
} = &mut *self.writer;
let next_array_index = &mut self.next_array_index;
let mut written = Vec::with_capacity(submeshes.len());
for submesh in submeshes {
let entities = entities_of(submesh, center == attribute::Center::Node);
let submesh_values = match entities {
IndexList::Contiguous { start, len } => values.slice(start * stride, len * stride),
IndexList::Scattered(indices) => gather_buffers.gather(values, stride, indices),
};
let index = *next_array_index;
*next_array_index += 1;
written.push(build_attribute(
writer.as_mut(),
index,
name,
data_attribute,
&submesh_values,
center,
)?);
}
for (attributes, attribute) in self.per_submesh.iter_mut().zip(written) {
attributes.push(attribute);
}
Ok(())
}
fn write_data_selected(
&mut self,
name: &str,
data_attribute: DataAttribute,
components: usize,
values: &Values<'_>,
center: attribute::Center,
) -> Result<()> {
let index = self.take_array_index();
let mut source =
build_data_item(self.writer.writer.as_mut(), index, data_attribute, values)?;
let dimensions = source
.dimensions
.replace(Dimensions(vec![values.len()]))
.ok_or(Error::Internal("a written array has no dimensions"))?
.0;
let point_data = center == attribute::Center::Node;
let TimeSeriesDataWriter {
writer,
submeshes,
selections,
next_selection_index,
gather_buffers,
xdmf,
..
} = &mut *self.writer;
let next_array_index = &mut self.next_array_index;
let mut written = Vec::with_capacity(submeshes.len());
for (submesh_index, submesh) in submeshes.iter().enumerate() {
let entities = entities_of(submesh, point_data);
if let IndexList::Scattered(unordered) = entities
&& !entities.is_ascending()
{
let index = *next_array_index;
*next_array_index += 1;
let gathered = gather_buffers.gather(values, components, unordered);
written.push(build_attribute(
writer.as_mut(),
index,
name,
data_attribute,
&gathered,
center,
)?);
continue;
}
let selector = match entities {
IndexList::Contiguous { start, len } => hyper_slab(*start, *len, components),
IndexList::Scattered(_) => {
let key = SelectionKey {
submesh: submesh_index,
point_data,
components,
};
let item = match selections.get(&key) {
Some(item) => item,
None => {
let item = write_selection_indices(
writer.as_mut(),
next_selection_index,
entities,
components,
)?;
xdmf.domains[0].data_items.push(item.clone());
selections.entry(key).or_insert(item)
}
};
DataItem::new_reference(item, DOMAIN_DATA_ITEMS)
}
};
written.push(attribute::Attribute {
name: name.to_string(),
attribute_type: data_attribute.into(),
center,
data_items: vec![selection(selector, &source, entities.len(), &dimensions)],
});
}
for (attributes, attribute) in self.per_submesh.iter_mut().zip(written) {
attributes.push(attribute);
}
Ok(())
}
fn finish(self) -> Result<()> {
if self.attributes.is_empty() && self.per_submesh.iter().all(Vec::is_empty) {
let time = self.time.clone();
let _discard_result = self.discard();
return Err(Error::InvalidTimeStep {
time,
reason: format!("no data written, needs at least one {POINT_DATA} or {CELL_DATA}"),
});
}
if let Err(error) = self.writer.writer.write_data_finalize() {
let _discard_result = self.discard();
return Err(error);
}
let TimeStep {
writer,
time,
time_bits,
attributes,
per_submesh,
..
} = self;
writer.push_step(&time, attributes, per_submesh)?;
writer.step_times.push(time.clone());
writer.written_times.insert(time_bits, time);
writer.write_xdmf_file()
}
fn discard(self) -> Result<()> {
if !self.initialized {
return Ok(());
}
self.writer.writer.write_data_discard()
}
}
fn build_attribute(
writer: &mut dyn DataWriter,
index: usize,
name: &str,
data_attribute: DataAttribute,
values: &Values<'_>,
center: attribute::Center,
) -> Result<attribute::Attribute> {
Ok(attribute::Attribute {
name: name.to_string(),
attribute_type: data_attribute.into(),
center,
data_items: vec![build_data_item(writer, index, data_attribute, values)?],
})
}
fn selection(
selector: DataItem,
source: &DataItem,
num_entities: usize,
dimensions: &[usize],
) -> DataItem {
let item_type = if selector.reference.is_some() {
ItemType::Coordinates
} else {
ItemType::HyperSlab
};
let mut selected = Vec::with_capacity(dimensions.len());
selected.push(num_entities);
selected.extend_from_slice(&dimensions[1..]);
DataItem {
name: None,
item_type: Some(item_type),
dimensions: Some(Dimensions(selected)),
number_type: source.number_type,
format: None,
precision: source.precision,
endian: None,
data: vec![selector, source.clone()].into(),
reference: None,
}
}
fn hyper_slab(start: usize, len: usize, components: usize) -> DataItem {
DataItem {
name: None,
item_type: None,
dimensions: Some(Dimensions(vec![3])),
number_type: Some(NumberType::Int),
format: Some(Format::XML),
precision: Some(4),
endian: None,
data: format!("{} 1 {}", start * components, len * components).into(),
reference: None,
}
}
fn write_selection_indices(
writer: &mut dyn DataWriter,
next_index: &mut usize,
entities: &IndexList,
components: usize,
) -> Result<DataItem> {
let mut indices = Vec::with_capacity(entities.len() * components);
for entity in entities.iter() {
indices.extend(entity * components..(entity + 1) * components);
}
let values = index_values(&indices)?;
let index = *next_index;
*next_index += 1;
let format = writer.format();
Ok(DataItem {
name: Some(format!("{SELECTIONS}_{index}")),
item_type: None,
dimensions: Some(Dimensions(vec![values.len()])),
data: writer.write_selection(index, &values)?,
number_type: Some(values.number_type()),
precision: Some(values.precision()),
format: Some(format),
endian: format.endian(),
reference: None,
})
}
fn build_data_item(
writer: &mut dyn DataWriter,
index: usize,
data_attribute: DataAttribute,
values: &Values<'_>,
) -> Result<DataItem> {
let format = writer.format();
Ok(DataItem {
name: None,
item_type: None,
dimensions: Some(values.dimensions(data_attribute)),
number_type: Some(values.number_type()),
format: Some(format),
precision: Some(values.precision()),
endian: format.endian(),
data: writer.write_data(index, values)?,
reference: None,
})
}
const POINT_DATA: &str = "point_data";
const CELL_DATA: &str = "cell_data";
fn is_valid_data_name(name: &str) -> bool {
if name.trim().is_empty() {
return false;
}
!name.chars().any(char::is_control)
}
const INVALID_FILE_NAME_CHARS: [char; 8] = ['?', '\0', ':', '*', '"', '<', '>', '|'];
fn validate_file_name(file_name: &Path) -> Result<()> {
let Some(name) = file_name.file_name() else {
return Err(Error::InvalidFileName {
path: file_name.to_path_buf(),
reason: "path has no file name component".to_string(),
});
};
let Some(name) = name.to_str() else {
return Err(Error::InvalidFileName {
path: file_name.to_path_buf(),
reason: "file name component is not valid UTF-8".to_string(),
});
};
if name.chars().any(|c| INVALID_FILE_NAME_CHARS.contains(&c)) {
return Err(Error::InvalidFileName {
path: file_name.to_path_buf(),
reason: format!(
"file name component must not contain any of the following characters: \
{INVALID_FILE_NAME_CHARS:?}"
),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
DataAttribute,
xdmf_elements::{
data_item::{DataContent, Format, NumberType},
grid::Grid,
},
};
#[test]
fn test_poly_cell_points() {
assert_eq!(poly_cell_points(CellType::Vertex), Some(1));
assert_eq!(poly_cell_points(CellType::Edge), Some(2));
assert_eq!(poly_cell_points(CellType::Triangle), None);
assert_eq!(poly_cell_points(CellType::Quadrilateral), None);
assert_eq!(poly_cell_points(CellType::Tetrahedron), None);
assert_eq!(poly_cell_points(CellType::Pyramid), None);
assert_eq!(poly_cell_points(CellType::Wedge), None);
assert_eq!(poly_cell_points(CellType::Hexahedron), None);
assert_eq!(poly_cell_points(CellType::Edge3), None);
assert_eq!(poly_cell_points(CellType::Quadrilateral9), None);
assert_eq!(poly_cell_points(CellType::Triangle6), None);
assert_eq!(poly_cell_points(CellType::Quadrilateral8), None);
assert_eq!(poly_cell_points(CellType::Tetrahedron10), None);
assert_eq!(poly_cell_points(CellType::Pyramid13), None);
assert_eq!(poly_cell_points(CellType::Wedge15), None);
assert_eq!(poly_cell_points(CellType::Wedge18), None);
assert_eq!(poly_cell_points(CellType::Hexahedron20), None);
assert_eq!(poly_cell_points(CellType::Hexahedron24), None);
assert_eq!(poly_cell_points(CellType::Hexahedron27), None);
}
fn prepare_cells_vec<I: ConnectivityIndex>(
connectivity: &[I],
cell_types: &[CellType],
num_points: usize,
) -> Result<(TopologyType, Vec<I>)> {
let (topology_type, cells) = prepare_cells(connectivity, cell_types, num_points)?;
Ok((topology_type, cells.into_owned()))
}
#[test]
fn test_prepare_cells() {
let (topo_type, cells_prep) = prepare_cells_vec(
&[0_u64, 1, 2, 3, 4, 5, 6, 7, 8, 9],
&[
CellType::Vertex,
CellType::Edge,
CellType::Triangle,
CellType::Quadrilateral,
],
0,
)
.unwrap();
assert_eq!(topo_type, TopologyType::Mixed);
assert_eq!(
cells_prep,
vec![1, 1, 0, 2, 2, 1, 2, 4, 3, 4, 5, 5, 6, 7, 8, 9]
);
}
#[test]
fn prepare_cells_by_celltype() {
assert_eq!(
prepare_cells_vec(&[5_u64], &[CellType::Vertex], 0).unwrap(),
(TopologyType::Polyvertex, vec![5])
);
assert_eq!(
prepare_cells_vec(&[5_u64, 6], &[CellType::Edge], 0).unwrap(),
(TopologyType::Polyline, vec![5, 6])
);
assert_eq!(
prepare_cells_vec(&[5_u64, 6, 7], &[CellType::Triangle], 0).unwrap(),
(TopologyType::Triangle, vec![5, 6, 7])
);
assert_eq!(
prepare_cells_vec(&[5_u64, 6, 7, 8], &[CellType::Quadrilateral], 0).unwrap(),
(TopologyType::Quadrilateral, vec![5, 6, 7, 8])
);
assert_eq!(
prepare_cells_vec(&[5_u64, 6, 7, 8], &[CellType::Tetrahedron], 0).unwrap(),
(TopologyType::Tetrahedron, vec![5, 6, 7, 8])
);
assert_eq!(
prepare_cells_vec(&[5_u64, 6, 7, 8, 9], &[CellType::Pyramid], 0).unwrap(),
(TopologyType::Pyramid, vec![5, 6, 7, 8, 9])
);
assert_eq!(
prepare_cells_vec(&[5_u64, 6, 7, 8, 9, 10], &[CellType::Wedge], 0).unwrap(),
(TopologyType::Wedge, vec![5, 6, 7, 8, 9, 10])
);
assert_eq!(
prepare_cells_vec(&[5_u64, 6, 7, 8, 9, 10, 11, 12], &[CellType::Hexahedron], 0)
.unwrap(),
(TopologyType::Hexahedron, vec![5, 6, 7, 8, 9, 10, 11, 12])
);
assert_eq!(
prepare_cells_vec(&[5_u64, 6, 7], &[CellType::Edge3], 0).unwrap(),
(TopologyType::Edge3, vec![5, 6, 7])
);
assert_eq!(
prepare_cells_vec(
&[5_u64, 6, 7, 8, 9, 10, 11, 12, 13],
&[CellType::Quadrilateral9],
0
)
.unwrap(),
(
TopologyType::Quadrilateral9,
vec![5, 6, 7, 8, 9, 10, 11, 12, 13]
)
);
assert_eq!(
prepare_cells_vec(&[5_u64, 6, 7, 8, 9, 10], &[CellType::Triangle6], 0).unwrap(),
(TopologyType::Triangle6, vec![5, 6, 7, 8, 9, 10])
);
assert_eq!(
prepare_cells_vec(
&[5_u64, 6, 7, 8, 9, 10, 11, 12],
&[CellType::Quadrilateral8],
0
)
.unwrap(),
(
TopologyType::Quadrilateral8,
vec![5, 6, 7, 8, 9, 10, 11, 12]
)
);
assert_eq!(
prepare_cells_vec(
&[5_u64, 6, 7, 8, 9, 10, 11, 12, 13, 14],
&[CellType::Tetrahedron10],
0
)
.unwrap(),
(
TopologyType::Tetrahedron10,
vec![5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
)
);
assert_eq!(
prepare_cells_vec(
&[5_u64, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
&[CellType::Pyramid13],
0
)
.unwrap(),
(
TopologyType::Pyramid13,
vec![5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
)
);
assert_eq!(
prepare_cells_vec(
&[5_u64, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
&[CellType::Wedge15],
0
)
.unwrap(),
(
TopologyType::Wedge15,
vec![5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
)
);
assert_eq!(
prepare_cells_vec(
&[
5_u64, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22
],
&[CellType::Wedge18],
0
)
.unwrap(),
(
TopologyType::Wedge18,
vec![
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22
]
)
);
assert_eq!(
prepare_cells_vec(
&[
5_u64, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24
],
&[CellType::Hexahedron20],
0
)
.unwrap(),
(
TopologyType::Hexahedron20,
vec![
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24
]
)
);
assert_eq!(
prepare_cells_vec(
&[
5_u64, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28
],
&[CellType::Hexahedron24],
0
)
.unwrap(),
(
TopologyType::Hexahedron24,
vec![
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28
]
)
);
assert_eq!(
prepare_cells_vec(
&[
5_u64, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31
],
&[CellType::Hexahedron27],
0
)
.unwrap(),
(
TopologyType::Hexahedron27,
vec![
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31
]
)
);
}
#[test]
fn prepare_cells_borrows_a_uniform_connectivity() {
let connectivity = [0_u64, 1, 2, 1, 2, 3];
let (_topology_type, cells) =
prepare_cells(&connectivity, &[CellType::Triangle; 2], 4).unwrap();
std::assert_matches!(cells, Cow::Borrowed(borrowed) if borrowed.as_ptr() == connectivity.as_ptr());
}
#[test]
fn prepare_cells_mixed_when_types_differ() {
let (topo_type, cells_prep) = prepare_cells_vec(
&[0_u64, 1, 2, 3, 4, 5, 6, 7],
&[CellType::Triangle, CellType::Triangle, CellType::Edge],
0,
)
.unwrap();
assert_eq!(topo_type, TopologyType::Mixed);
assert_eq!(cells_prep, vec![4, 0, 1, 2, 4, 3, 4, 5, 2, 2, 6, 7]);
}
#[test]
fn test_prepare_cells_no_cells() {
let (topo_type, cells_prep) = prepare_cells_vec(&[] as &[u64], &[], 5).unwrap();
assert_eq!(topo_type, TopologyType::Polyvertex);
assert_eq!(cells_prep, vec![0, 1, 2, 3, 4]);
}
#[test]
fn test_validate_points_and_cells() {
validate_points_and_cells(
33,
&[0, 1, 2, 3, 4, 5, 6, 7],
&[
CellType::Vertex,
CellType::Triangle,
CellType::Quadrilateral,
],
)
.unwrap();
}
#[cfg(target_pointer_width = "64")]
#[test]
fn validate_points_and_cells_too_many_points() {
let too_many_u32 = usize::try_from(u32::MAX).unwrap() + 2;
let too_many_i32 = usize::try_from(i32::MAX).unwrap() + 2;
std::assert_matches!(
validate_points_and_cells(3 * too_many_u32, &[] as &[u32], &[]).unwrap_err(),
Error::InvalidMesh { reason } if reason.contains("can only index up to 4294967295")
);
std::assert_matches!(
validate_points_and_cells(3 * too_many_i32, &[] as &[i32], &[]).unwrap_err(),
Error::InvalidMesh { reason } if reason.contains("can only index up to 2147483647")
);
validate_points_and_cells(3 * (too_many_u32 - 1), &[] as &[u32], &[]).unwrap();
validate_points_and_cells(3 * (too_many_i32 - 1), &[] as &[i32], &[]).unwrap();
validate_points_and_cells(3 * too_many_u32, &[] as &[u64], &[]).unwrap();
validate_points_and_cells(3 * too_many_u32, &[] as &[i64], &[]).unwrap();
}
#[test]
fn connectivity_above_the_paraview_uint_cap_is_rejected() {
let too_large = Values::from(vec![u64::from(u32::MAX) + 1]);
std::assert_matches!(
paraview::validate(&too_large, Format::XML).unwrap_err(),
Error::IntegerOutOfRange { value, reason }
if value == i128::from(u32::MAX) + 1 && reason.contains("no DataStorage avoids this")
);
paraview::validate(&Values::from(vec![u64::from(u32::MAX)]), Format::XML).unwrap();
}
#[test]
fn validate_points_and_cells_negative_index() {
std::assert_matches!(
validate_points_and_cells(9, &[0_i32, -1, 2], &[CellType::Triangle]).unwrap_err(),
Error::InvalidMesh { reason } if reason == "connectivity index -1 is negative"
);
}
#[test]
fn validate_points_and_cells_only_points() {
validate_points_and_cells(33, &[] as &[u64], &[]).unwrap();
}
#[test]
fn validate_points_and_cells_points_empty() {
let res = validate_points_and_cells(
0,
&[0, 1, 2, 3, 4, 5, 6, 7],
&[
CellType::Vertex,
CellType::Triangle,
CellType::Quadrilateral,
],
);
std::assert_matches!(
res.unwrap_err(),
Error::InvalidMesh { reason } if reason.contains("at least one point")
);
}
#[test]
fn validate_points_and_cells_points_not_3d() {
let res = validate_points_and_cells(
22,
&[0, 1, 2, 3, 4, 5, 6, 7],
&[
CellType::Vertex,
CellType::Triangle,
CellType::Quadrilateral,
],
);
std::assert_matches!(
res.unwrap_err(),
Error::InvalidMesh { reason } if reason.contains("22 is not a multiple of 3")
);
}
#[test]
fn validate_points_and_cells_conn_index_out_of_bounds() {
let res = validate_points_and_cells(
33,
&[0, 1, 2, 3, 4, 5, 6, 70],
&[
CellType::Vertex,
CellType::Triangle,
CellType::Quadrilateral,
],
);
std::assert_matches!(
res.unwrap_err(),
Error::InvalidMesh { reason }
if reason.contains("connectivity index 70")
&& reason.contains("only has 11 points")
);
}
#[test]
fn validate_points_and_cells_conn_mismatch() {
let res = validate_points_and_cells(
33,
&[0, 1, 2, 3, 4, 5, 6, 7],
&[
CellType::Vertex,
CellType::Edge,
CellType::Triangle,
CellType::Quadrilateral,
],
);
std::assert_matches!(
res.unwrap_err(),
Error::InvalidMesh { reason }
if reason.contains("connectivity (8)") && reason.contains("cell types (10)")
);
}
fn document_for(grid: &Grid) -> Xdmf {
new_document(grid.clone(), Vec::new(), DataStorage::AsciiInline)
}
fn last_step_grids(writer: &TimeSeriesDataWriter) -> Vec<&Grid> {
let root = writer.xdmf.domains[0]
.grids
.first()
.expect("a step was written");
let collections: Vec<&Grid> = if root.collection_type == Some(CollectionType::Temporal) {
vec![root]
} else {
root.grids.iter().flatten().collect()
};
collections
.into_iter()
.map(|collection| {
collection
.grids
.iter()
.flatten()
.next_back()
.expect("a step was written")
})
.collect()
}
fn attribute_names(grid: &Grid) -> Vec<&str> {
grid.attributes
.iter()
.flatten()
.map(|attribute| attribute.name.as_str())
.collect()
}
fn submeshes<'a>(entries: &'a [(&'a str, &'a [usize])]) -> Vec<(&'a str, &'a [usize])> {
entries.to_vec()
}
#[test]
fn prepare_submeshes_collapses_an_ascending_run() {
let prepared = prepare_submeshes(submeshes(&[("all", &[0, 1, 2, 3])]), 4).unwrap();
assert_eq!(prepared.len(), 1);
assert_eq!(prepared[0].name, "all");
std::assert_matches!(
prepared[0].cells,
IndexList::Contiguous { start: 0, len: 4 }
);
}
#[test]
fn prepare_submeshes_collapses_a_run_that_does_not_start_at_zero() {
let prepared =
prepare_submeshes(submeshes(&[("low", &[0, 1]), ("high", &[2, 3, 4])]), 5).unwrap();
std::assert_matches!(
prepared[1].cells,
IndexList::Contiguous { start: 2, len: 3 }
);
}
#[test]
fn prepare_submeshes_keeps_a_scattered_list_in_the_given_order() {
let prepared = prepare_submeshes(submeshes(&[("all", &[2, 0, 1])]), 3).unwrap();
std::assert_matches!(
&prepared[0].cells,
IndexList::Scattered(indices) if indices == &[2, 0, 1]
);
}
#[test]
fn prepare_submeshes_takes_a_range_without_materialising_its_indices() {
let prepared = prepare_submeshes([("lower", 0..2), ("upper", 2..3)], 3).unwrap();
std::assert_matches!(
prepared[0].cells,
IndexList::Contiguous { start: 0, len: 2 }
);
std::assert_matches!(
prepared[1].cells,
IndexList::Contiguous { start: 2, len: 1 }
);
}
#[test]
fn prepare_submeshes_rejects_a_range_past_the_end_of_the_mesh() {
let res = prepare_submeshes([("all", 0..4)], 3);
std::assert_matches!(
res.unwrap_err(),
Error::InvalidMesh { reason } if reason.contains("references cell 3")
);
}
#[test]
fn prepare_submeshes_rejects_an_empty_range() {
let res = prepare_submeshes([("all", 0..3), ("none", 1..1)], 3);
std::assert_matches!(
res.unwrap_err(),
Error::InvalidMesh { reason } if reason.contains("submesh 'none' is empty")
);
}
#[test]
fn prepare_submeshes_moves_an_owned_scattered_list_in() {
let prepared = prepare_submeshes([("all", vec![2, 0, 1])], 3).unwrap();
std::assert_matches!(
&prepared[0].cells,
IndexList::Scattered(indices) if indices == &[2, 0, 1]
);
}
#[test]
fn prepare_submeshes_allows_overlapping_submeshes() {
let prepared = prepare_submeshes(
submeshes(&[("left", &[0, 1]), ("right", &[1, 2]), ("all", &[0, 1, 2])]),
3,
)
.unwrap();
assert_eq!(prepared.len(), 3);
}
#[test]
fn prepare_submeshes_rejects_no_submeshes() {
let empty: Vec<(&str, &[usize])> = Vec::new();
std::assert_matches!(
prepare_submeshes(empty, 3).unwrap_err(),
Error::InvalidMesh { reason } if reason.contains("at least one submesh is required")
);
}
#[test]
fn prepare_submeshes_rejects_an_invalid_name() {
std::assert_matches!(
prepare_submeshes(submeshes(&[("has space", &[0])]), 1),
Ok(_)
);
std::assert_matches!(
prepare_submeshes(submeshes(&[("has\u{9}tab", &[0])]), 1).unwrap_err(),
Error::InvalidMesh { reason } if reason.contains("is not valid")
);
}
#[test]
fn prepare_submeshes_rejects_a_duplicate_name() {
std::assert_matches!(
prepare_submeshes(submeshes(&[("part", &[0]), ("part", &[1])]), 2).unwrap_err(),
Error::InvalidMesh { reason } if reason.contains("submesh name 'part' is used more than once")
);
}
#[test]
fn prepare_submeshes_rejects_an_empty_submesh() {
std::assert_matches!(
prepare_submeshes(submeshes(&[("empty", &[])]), 1).unwrap_err(),
Error::InvalidMesh { reason } if reason.contains("submesh 'empty' is empty")
);
}
#[test]
fn prepare_submeshes_rejects_an_out_of_range_cell() {
std::assert_matches!(
prepare_submeshes(submeshes(&[("part", &[0, 5])]), 3).unwrap_err(),
Error::InvalidMesh { reason }
if reason.contains("submesh 'part' references cell 5")
&& reason.contains("only has 3 cells")
);
}
#[test]
fn prepare_submeshes_rejects_a_cell_repeated_within_one_submesh() {
std::assert_matches!(
prepare_submeshes(submeshes(&[("part", &[0, 1, 0])]), 2).unwrap_err(),
Error::InvalidMesh { reason }
if reason.contains("submesh 'part' contains cell 0 more than once")
);
}
#[test]
fn prepare_submeshes_spans_the_bit_sets_words() {
let low: Vec<usize> = (0..70).collect();
let high: Vec<usize> = (64..150).collect();
let prepared = prepare_submeshes(submeshes(&[("low", &low), ("high", &high)]), 150);
assert_eq!(prepared.unwrap().len(), 2);
let repeated: Vec<usize> = high.iter().copied().chain([149]).collect();
std::assert_matches!(
prepare_submeshes(submeshes(&[("low", &low), ("high", &repeated)]), 150).unwrap_err(),
Error::InvalidMesh { reason }
if reason.contains("submesh 'high' contains cell 149 more than once")
);
let gapped: Vec<usize> = high.iter().copied().filter(|index| *index != 130).collect();
std::assert_matches!(
prepare_submeshes(submeshes(&[("low", &low), ("high", &gapped)]), 150).unwrap_err(),
Error::InvalidMesh { reason }
if reason.contains("1 of 150 cells belong to no submesh: 130")
);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn cell_indices_are_written_at_the_narrowest_type_that_holds_them() {
let small = index_values(&[0, 7, 12]).unwrap();
std::assert_matches!(&small, Values::I32(indices) if **indices == [0, 7, 12]);
let large = index_values(&[1, usize::try_from(i32::MAX).unwrap() + 1]).unwrap();
std::assert_matches!(
&large,
Values::I64(indices) if **indices == [1, i64::from(i32::MAX) + 1]
);
}
#[test]
fn prepare_submeshes_rejects_cells_in_no_submesh() {
std::assert_matches!(
prepare_submeshes(submeshes(&[("part", &[0, 2])]), 4).unwrap_err(),
Error::InvalidMesh { reason }
if reason.contains("2 of 4 cells belong to no submesh: 1, 3")
);
}
#[test]
fn prepare_submeshes_truncates_a_long_list_of_uncovered_cells() {
std::assert_matches!(
prepare_submeshes(submeshes(&[("part", &[0])]), 20).unwrap_err(),
Error::InvalidMesh { reason }
if reason.contains("19 of 20 cells belong to no submesh")
&& reason.contains("1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ... (9 more)")
);
}
#[test]
fn cell_offsets_of_mixed_cells() {
let offsets = cell_offsets(
&[CellType::Triangle, CellType::Edge, CellType::Vertex],
TopologyType::Mixed,
3,
);
assert_eq!(offsets, vec![0, 4, 8, 11]);
}
#[test]
fn cell_offsets_of_uniform_cells() {
let offsets = cell_offsets(
&[CellType::Quadrilateral; 3],
TopologyType::Quadrilateral,
3,
);
assert_eq!(offsets, vec![0, 4, 8, 12]);
}
#[test]
fn cell_offsets_of_a_point_mesh() {
assert_eq!(
cell_offsets(&[], TopologyType::Polyvertex, 3),
vec![0, 1, 2, 3]
);
}
const QUAD_CELLS: [u32; 15] = [5, 0, 1, 2, 3, 5, 4, 5, 6, 7, 5, 8, 9, 10, 11];
const QUAD_OFFSETS: [usize; 4] = [0, 5, 10, 15];
const QUAD_TYPES: [CellType; 3] = [CellType::Quadrilateral; 3];
#[test]
fn extract_connectivity_takes_a_contiguous_submesh() {
let extracted = extract_connectivity(
&QUAD_CELLS,
&QUAD_OFFSETS,
&QUAD_TYPES,
TopologyType::Mixed,
TopologyType::Mixed,
&IndexList::Contiguous { start: 1, len: 2 },
);
assert_eq!(extracted, &[5, 4, 5, 6, 7, 5, 8, 9, 10, 11]);
}
#[test]
fn extract_connectivity_gathers_a_scattered_submesh() {
let extracted = extract_connectivity(
&QUAD_CELLS,
&QUAD_OFFSETS,
&QUAD_TYPES,
TopologyType::Mixed,
TopologyType::Mixed,
&IndexList::Scattered(vec![2, 0]),
);
assert_eq!(extracted, &[5, 8, 9, 10, 11, 5, 0, 1, 2, 3]);
}
#[test]
fn extract_connectivity_drops_the_type_codes_of_a_uniform_submesh() {
let contiguous = extract_connectivity(
&QUAD_CELLS,
&QUAD_OFFSETS,
&QUAD_TYPES,
TopologyType::Mixed,
TopologyType::Quadrilateral,
&IndexList::Contiguous { start: 1, len: 2 },
);
assert_eq!(contiguous, &[4, 5, 6, 7, 8, 9, 10, 11]);
let scattered = extract_connectivity(
&QUAD_CELLS,
&QUAD_OFFSETS,
&QUAD_TYPES,
TopologyType::Mixed,
TopologyType::Quadrilateral,
&IndexList::Scattered(vec![2, 0]),
);
assert_eq!(scattered, &[8, 9, 10, 11, 0, 1, 2, 3]);
}
#[test]
fn submesh_topology_is_the_type_its_own_cells_share() {
let cell_types = [
CellType::Hexahedron,
CellType::Quadrilateral,
CellType::Quadrilateral,
];
assert_eq!(
submesh_topology(
&cell_types,
TopologyType::Mixed,
None,
&IndexList::Contiguous { start: 1, len: 2 }
),
(TopologyType::Quadrilateral, None)
);
assert_eq!(
submesh_topology(
&cell_types,
TopologyType::Mixed,
None,
&IndexList::Contiguous { start: 0, len: 3 }
),
(TopologyType::Mixed, None)
);
assert_eq!(
submesh_topology(
&cell_types,
TopologyType::Mixed,
None,
&IndexList::Scattered(vec![2, 0])
),
(TopologyType::Mixed, None)
);
assert_eq!(
submesh_topology(
&[CellType::Hexahedron, CellType::Edge],
TopologyType::Mixed,
None,
&IndexList::Contiguous { start: 1, len: 1 }
),
(TopologyType::Polyline, Some(2))
);
assert_eq!(
submesh_topology(
&[CellType::Edge; 2],
TopologyType::Polyline,
Some(2),
&IndexList::Contiguous { start: 0, len: 1 }
),
(TopologyType::Polyline, Some(2))
);
}
#[test]
fn time_series_writer_create_folder() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let subfolder = Path::new("out/xdmf"); let xdmf_folder = tmp_dir.path().join(subfolder);
let xdmf_file_path = xdmf_folder.join("test_output");
assert!(!xdmf_folder.exists());
let writer = TimeSeriesWriter::new(&xdmf_file_path, DataStorage::AsciiInline).unwrap();
assert!(xdmf_folder.exists());
assert_eq!(
writer.xdmf_file_name,
xdmf_file_path.with_extension("xdmf2")
);
}
#[test]
fn mpi_safe_create_dir_all_works() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let dirs_to_create = tmp_dir.path().join("out/xdmf/test/folder/random/testing");
let handles: Vec<_> = (0..100)
.map(|_| {
std::thread::spawn({
let dir_thread_local = dirs_to_create.clone();
move || mpi_safe_create_dir_all(dir_thread_local).unwrap()
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
assert!(dirs_to_create.exists());
}
#[test]
fn test_validate_data() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let xdmf_file_path = tmp_dir.path().join("test_output.xdmf");
let writer = TimeSeriesWriter::new(&xdmf_file_path, DataStorage::AsciiInline).unwrap();
const NUM_POINTS: usize = 10;
let mut writer = writer
.write_mesh(
&[0.0; NUM_POINTS * 3],
&[0, 2, 3, 4],
&[CellType::Vertex; 4],
)
.unwrap();
let values = vec![5.0; NUM_POINTS];
writer
.write_time_step("0.1", |step| {
step.point_data("point_data1", DataAttribute::Scalar, &values)
})
.unwrap();
let res = writer.write_time_step("1.0", |_step| Ok(()));
std::assert_matches!(
res.unwrap_err(),
Error::InvalidTimeStep { time, reason }
if time == "1.0" && reason.contains("no data written")
);
let res = writer.write_time_step("0.1", |_step| Ok(()));
std::assert_matches!(
res.unwrap_err(),
Error::InvalidTimeStep { time, reason }
if time == "0.1" && reason == "already written"
);
let res = writer.write_time_step("invalid_time", |_step| Ok(()));
std::assert_matches!(
res.unwrap_err(),
Error::InvalidTimeStep { time, reason }
if time == "invalid_time" && reason.contains("must be a valid float")
);
let res = writer.write_time_step("", |_step| Ok(()));
std::assert_matches!(
res.unwrap_err(),
Error::InvalidTimeStep { time, reason }
if time.is_empty() && reason.contains("must be a valid float")
);
}
#[test]
fn write_time_step_rejects_non_finite_times() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let mut writer = flaky_writer(tmp_dir.path().join("non_finite_times.xdmf2"), None, None);
for time in ["NaN", "inf", "-infinity", "1e400"] {
let res = writer.write_time_step(time, |step| {
step.point_data("data", DataAttribute::Scalar, vec![0.0; 0])
});
std::assert_matches!(
res.unwrap_err(),
Error::InvalidTimeStep { time: rejected, reason }
if rejected == time && reason == "must be a finite float"
);
}
assert!(writer.step_times.is_empty());
}
#[test]
fn write_time_step_treats_negative_zero_as_the_time_already_written() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let mut writer = flaky_writer(tmp_dir.path().join("negative_zero.xdmf2"), None, None);
writer
.write_time_step("0.0", |step| {
step.point_data("data", DataAttribute::Scalar, vec![0.0; 0])
})
.unwrap();
let res = writer.write_time_step("-0.0", |_step| Ok(()));
std::assert_matches!(
res.unwrap_err(),
Error::InvalidTimeStep { time, reason }
if time == "-0.0" && reason == "already written (as '0.0')"
);
}
#[test]
fn write_time_step_erroring_out_discards_the_data_it_already_wrote() {
#[derive(Debug)]
enum CallerError {
ChangedItsMind,
Xdmf(Error),
}
impl From<Error> for CallerError {
fn from(error: Error) -> Self {
Self::Xdmf(error)
}
}
let tmp_dir = temp_dir::TempDir::new().unwrap();
let xdmf_file_path = tmp_dir.path().join("test_output.xdmf");
let writer = TimeSeriesWriter::new(&xdmf_file_path, DataStorage::Ascii).unwrap();
const NUM_POINTS: usize = 10;
let mut writer = writer
.write_mesh(
&[0.0; NUM_POINTS * 3],
&[0, 2, 3, 4],
&[CellType::Vertex; 4],
)
.unwrap();
let values = vec![5.0; NUM_POINTS];
let res = writer.write_time_step("0.1", |step| {
step.point_data("abandoned", DataAttribute::Scalar, &values)?;
Err(CallerError::ChangedItsMind)
});
std::assert_matches!(res.unwrap_err(), CallerError::ChangedItsMind);
let txt_dir = xdmf_file_path.with_extension("txt");
assert!(!txt_dir.join("data_t_0.1_0.txt").exists());
writer
.write_time_step("0.1", |step| {
step.point_data("kept", DataAttribute::Scalar, &values)
})
.unwrap();
writer
.write_time_step("0.2", |step| {
step.point_data("kept", DataAttribute::Scalar, &values)
})
.unwrap();
let res = writer.write_time_step("0.3", |step| {
step.point_data("wrong_size", DataAttribute::Scalar, &[1.0])?;
Err(CallerError::ChangedItsMind)
});
std::assert_matches!(
res.unwrap_err(),
CallerError::Xdmf(Error::InvalidData { reason })
if reason == "size of point_data 'wrong_size' must be 10, but is 1"
);
let xdmf = std::fs::read_to_string(xdmf_file_path.with_extension("xdmf2")).unwrap();
assert!(!xdmf.contains("abandoned"));
assert_eq!(xdmf.matches("<Grid Name=\"time_series-t").count(), 2);
}
#[test]
fn write_time_step_erroring_out_before_writing_anything_needs_no_cleanup() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let xdmf_file_path = tmp_dir.path().join("test_output.xdmf");
let writer = TimeSeriesWriter::new(&xdmf_file_path, DataStorage::Ascii).unwrap();
let mut writer = writer
.write_mesh(&[0.0; 3], &[0], &[CellType::Vertex])
.unwrap();
let res = writer.write_time_step("0.1", |_step| {
Err(Error::InvalidData {
reason: "nothing to write".to_string(),
})
});
std::assert_matches!(
res.unwrap_err(),
Error::InvalidData { reason } if reason == "nothing to write"
);
writer
.write_time_step("0.1", |step| {
step.point_data("data", DataAttribute::Scalar, &[1.0])
})
.unwrap();
}
#[test]
fn write_time_step_mixes_value_types_within_one_step() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let xdmf_file_path = tmp_dir.path().join("test_output.xdmf");
let writer = TimeSeriesWriter::new(&xdmf_file_path, DataStorage::AsciiInline).unwrap();
const NUM_POINTS: usize = 4;
let mut writer = writer
.write_mesh(
&[0.0; NUM_POINTS * 3],
&[0, 1, 2, 3],
&[CellType::Vertex; 4],
)
.unwrap();
let floats = vec![1.5; NUM_POINTS];
let ids: Vec<u64> = (0..NUM_POINTS as u64).collect();
writer
.write_time_step("0.0", |step| {
step.point_data("floats", DataAttribute::Scalar, &floats)?;
step.cell_data("ids", DataAttribute::Scalar, &ids)
})
.unwrap();
let xdmf = std::fs::read_to_string(xdmf_file_path.with_extension("xdmf2")).unwrap();
assert!(xdmf.contains(r#"Name="floats" AttributeType="Scalar" Center="Node""#));
assert!(xdmf.contains(r#"Name="ids" AttributeType="Scalar" Center="Cell""#));
assert!(xdmf.contains(r#"NumberType="UInt""#));
}
#[test]
fn write_time_step_reuses_a_single_buffer_across_attributes() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let xdmf_file_path = tmp_dir.path().join("test_output.xdmf");
let writer = TimeSeriesWriter::new(&xdmf_file_path, DataStorage::AsciiInline).unwrap();
const NUM_POINTS: usize = 3;
let mut writer = writer
.write_mesh(&[0.0; NUM_POINTS * 3], &[0, 1, 2], &[CellType::Vertex; 3])
.unwrap();
let mut buf = vec![0.0; NUM_POINTS];
writer
.write_time_step("0.0", |step| {
buf.fill(1.0);
step.point_data("first", DataAttribute::Scalar, &buf)?;
buf.fill(2.0);
step.point_data("second", DataAttribute::Scalar, &buf)
})
.unwrap();
let xdmf = std::fs::read_to_string(xdmf_file_path.with_extension("xdmf2")).unwrap();
let one = "1e0";
let two = "2e0";
assert!(xdmf.contains(&format!(">{one} {one} {one}<")));
assert!(xdmf.contains(&format!(">{two} {two} {two}<")));
}
#[test]
fn test_validate_data_dedup_is_numeric_not_textual() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let xdmf_file_path = tmp_dir.path().join("test_output.xdmf");
let writer = TimeSeriesWriter::new(&xdmf_file_path, DataStorage::AsciiInline).unwrap();
const NUM_POINTS: usize = 10;
let mut writer = writer
.write_mesh(
&[0.0; NUM_POINTS * 3],
&[0, 2, 3, 4],
&[CellType::Vertex; 4],
)
.unwrap();
let values = vec![5.0; NUM_POINTS];
let write_step = |writer: &mut TimeSeriesDataWriter, time: &str| -> Result<()> {
writer.write_time_step(time, |step| {
step.point_data("point_data1", DataAttribute::Scalar, &values)
})
};
write_step(&mut writer, "0.1").unwrap();
let res = write_step(&mut writer, "0.10");
std::assert_matches!(
res.unwrap_err(),
Error::InvalidTimeStep { time, reason }
if time == "0.10" && reason == "already written (as '0.1')"
);
write_step(&mut writer, "0.2").unwrap();
}
#[test]
fn test_validate_data_duplicate_names() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let xdmf_file_path = tmp_dir.path().join("test_output.xdmf");
let writer = TimeSeriesWriter::new(&xdmf_file_path, DataStorage::AsciiInline).unwrap();
const NUM_POINTS: usize = 10;
let mut writer = writer
.write_mesh(
&[0.0; NUM_POINTS * 3],
&[0, 2, 3, 4],
&[CellType::Vertex; 4],
)
.unwrap();
let values = vec![5.0; NUM_POINTS];
let res = writer.write_time_step("0.0", |step| {
step.point_data("duplicate", DataAttribute::Scalar, &values)?;
step.point_data("duplicate", DataAttribute::Scalar, &values)
});
std::assert_matches!(
res.unwrap_err(),
Error::InvalidData { reason }
if reason.contains("name 'duplicate' of point_data is used more than once")
);
let cell_values = vec![5.0; 4];
writer
.write_time_step("0.0", |step| {
step.point_data("data", DataAttribute::Scalar, &values)?;
step.cell_data("data", DataAttribute::Scalar, &cell_values)
})
.unwrap();
}
#[test]
fn test_validate_data_wrong_point_data_sizes() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let xdmf_file_path = tmp_dir.path().join("test_output.xdmf");
let writer = TimeSeriesWriter::new(&xdmf_file_path, DataStorage::AsciiInline).unwrap();
const NUM_POINTS: usize = 10;
let mut writer = writer
.write_mesh(
&[0.0; NUM_POINTS * 3],
&[0, 2, 3, 4],
&[CellType::Vertex; 4],
)
.unwrap();
let mut err_for = |name: &str, attribute: DataAttribute, len: usize| -> Error {
writer
.write_time_step("0.0", |step| {
step.point_data(name, attribute, vec![5.0; len])
})
.unwrap_err()
};
std::assert_matches!(
err_for("point_data_sca", DataAttribute::Scalar, NUM_POINTS - 1),
Error::InvalidData { reason }
if reason == "size of point_data 'point_data_sca' must be 10, but is 9"
);
std::assert_matches!(
err_for("point_data_vec", DataAttribute::Vector, NUM_POINTS * 2),
Error::InvalidData { reason }
if reason == "size of point_data 'point_data_vec' must be 30, but is 20"
);
std::assert_matches!(
err_for("point_data_ten", DataAttribute::Tensor, NUM_POINTS * 3),
Error::InvalidData { reason }
if reason == "size of point_data 'point_data_ten' must be 90, but is 30"
);
std::assert_matches!(
err_for("point_data_ten6", DataAttribute::Tensor6, NUM_POINTS * 3),
Error::InvalidData { reason }
if reason == "size of point_data 'point_data_ten6' must be 60, but is 30"
);
std::assert_matches!(
err_for(
"point_data_mat",
DataAttribute::Matrix(2, 1),
NUM_POINTS * 3 - 1
),
Error::InvalidData { reason }
if reason == "size of point_data 'point_data_mat' must be 20, but is 29"
);
}
#[test]
fn test_validate_data_wrong_cell_data_sizes() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let xdmf_file_path = tmp_dir.path().join("test_output.xdmf");
let writer = TimeSeriesWriter::new(&xdmf_file_path, DataStorage::AsciiInline).unwrap();
const NUM_CELLS: usize = 4;
let mut writer = writer
.write_mesh(
&[0.0; 10 * 3],
&[0, 2, 3, 4],
&[CellType::Vertex; NUM_CELLS],
)
.unwrap();
let mut err_for = |name: &str, attribute: DataAttribute, len: usize| -> Error {
writer
.write_time_step("0.0", |step| {
step.cell_data(name, attribute, vec![5.0; len])
})
.unwrap_err()
};
std::assert_matches!(
err_for("cell_data_sca", DataAttribute::Scalar, NUM_CELLS - 1),
Error::InvalidData { reason }
if reason == "size of cell_data 'cell_data_sca' must be 4, but is 3"
);
std::assert_matches!(
err_for("cell_data_vec", DataAttribute::Vector, NUM_CELLS * 2),
Error::InvalidData { reason }
if reason == "size of cell_data 'cell_data_vec' must be 12, but is 8"
);
std::assert_matches!(
err_for("cell_data_ten", DataAttribute::Tensor, NUM_CELLS * 3),
Error::InvalidData { reason }
if reason == "size of cell_data 'cell_data_ten' must be 36, but is 12"
);
std::assert_matches!(
err_for("cell_data_ten6", DataAttribute::Tensor6, NUM_CELLS * 3),
Error::InvalidData { reason }
if reason == "size of cell_data 'cell_data_ten6' must be 24, but is 12"
);
std::assert_matches!(
err_for(
"cell_data_mat",
DataAttribute::Matrix(2, 1),
NUM_CELLS * 3 - 1
),
Error::InvalidData { reason }
if reason == "size of cell_data 'cell_data_mat' must be 8, but is 11"
);
}
#[test]
fn test_validate_data_names() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let xdmf_file_path = tmp_dir.path().join("test_output.xdmf");
let writer = TimeSeriesWriter::new(&xdmf_file_path, DataStorage::AsciiInline).unwrap();
let mut writer = writer
.write_mesh(&[0.0; 3], &[0], &[CellType::Vertex])
.unwrap();
let res = writer.write_time_step("0.0", |step| {
step.cell_data("cell_data_ten", DataAttribute::Scalar, vec![0.0; 1])?;
step.point_data("cell\u{9}data_ten", DataAttribute::Scalar, vec![0.0; 1])
});
std::assert_matches!(
res.unwrap_err(),
Error::InvalidData { reason }
if reason.contains("of point_data is not valid")
&& reason.contains("control characters")
);
}
#[test]
fn test_is_valid_data_name() {
assert!(is_valid_data_name("valid_name"));
assert!(is_valid_data_name("valid-name"));
assert!(is_valid_data_name("valid_name_123"));
assert!(is_valid_data_name("Quantity('SOOT DENSITY')"));
assert!(is_valid_data_name("U.component_0"));
assert!(is_valid_data_name("stress [Pa]"));
assert!(is_valid_data_name("T_max, avg"));
assert!(is_valid_data_name("\u{394}\u{3b8}"));
assert!(is_valid_data_name("a/b"));
assert!(is_valid_data_name("a\\b"));
assert!(is_valid_data_name("a:b"));
assert!(is_valid_data_name("a#b"));
assert!(is_valid_data_name("a%b"));
assert!(is_valid_data_name("a*b"));
assert!(is_valid_data_name("a?b"));
assert!(is_valid_data_name("a\"b"));
assert!(is_valid_data_name("a<b>c"));
assert!(is_valid_data_name("a|b"));
assert!(is_valid_data_name(" padded name "));
assert!(!is_valid_data_name(""));
assert!(!is_valid_data_name(" ")); assert!(!is_valid_data_name(" ")); assert!(!is_valid_data_name("\u{a0}")); assert!(!is_valid_data_name("invalid\0name")); assert!(!is_valid_data_name("invalid\nname")); assert!(!is_valid_data_name("invalid\tname")); assert!(!is_valid_data_name("invalid\u{7f}name")); }
#[test]
fn test_validate_file_name() {
validate_file_name(Path::new("asdf.txt")).unwrap();
validate_file_name(Path::new("valid-name.txt")).unwrap();
validate_file_name(Path::new("valid_name.txt")).unwrap();
validate_file_name(Path::new("valid_name-123.txt")).unwrap();
validate_file_name(Path::new("C:/some:dir/valid_name.txt")).unwrap();
let res = validate_file_name(Path::new("valid_name:123.txt"));
std::assert_matches!(
res.unwrap_err(),
Error::InvalidFileName { path, reason }
if path == Path::new("valid_name:123.txt")
&& reason.contains("file name component must not contain any of")
);
let res = validate_file_name(Path::new(""));
std::assert_matches!(
res.unwrap_err(),
Error::InvalidFileName { path, reason }
if path == Path::new("") && reason == "path has no file name component"
);
}
fn dummy_geometry() -> Geometry {
Geometry {
geometry_type: GeometryType::XYZ,
data_items: vec![DataItem {
dimensions: Some(Dimensions(vec![5, 3])),
data: "0 1 0 0 1.5 0 0.5 1.5 0.5 1 1.5 0 1 1 0".into(),
number_type: Some(NumberType::Float),
..Default::default()
}],
}
}
fn dummy_topology() -> Topology {
Topology {
topology_type: TopologyType::Triangle,
nodes_per_element: None,
number_of_elements: "2".into(),
data_item: DataItem {
dimensions: Some(Dimensions(vec![6])),
number_type: Some(NumberType::Int),
data: "0 1 2 2 3 4".into(),
..Default::default()
},
}
}
#[test]
fn test_write_data_preserve_order() {
struct DummyWriter;
impl DataWriter for DummyWriter {
fn format(&self) -> Format {
Format::XML
}
fn data_storage(&self) -> DataStorage {
DataStorage::AsciiInline
}
fn write_points(
&mut self,
_submesh: Option<usize>,
_points: &Values<'_>,
) -> Result<DataContent> {
Ok(DataContent::Raw("points".to_string()))
}
fn write_connectivity(
&mut self,
_submesh: Option<usize>,
_cells: &Values<'_>,
) -> Result<DataContent> {
Ok(DataContent::Raw("cells".to_string()))
}
fn write_submesh_cells(
&mut self,
submesh: usize,
_cells: &Values<'_>,
) -> Result<DataContent> {
Ok(DataContent::Raw(format!("submesh_cells_{submesh}")))
}
fn write_submesh_points(
&mut self,
submesh: usize,
_points: &Values<'_>,
) -> Result<DataContent> {
Ok(DataContent::Raw(format!("submesh_points_{submesh}")))
}
fn write_data(&mut self, index: usize, _data: &Values<'_>) -> Result<DataContent> {
Ok(DataContent::Raw(format!("data_for_{index}")))
}
}
let tmp_dir = temp_dir::TempDir::new().unwrap();
let xdmf_file_path = tmp_dir.path().join("test_write_data_preserve_order.xdmf2");
let grid = Grid::new_uniform("test", dummy_geometry(), dummy_topology());
let mut writer = TimeSeriesDataWriter {
xdmf_file_name: xdmf_file_path.clone(),
writer: Box::new(DummyWriter),
xdmf: document_for(&grid),
grid,
step_times: Vec::new(),
num_points: 0,
num_cells: 0,
submeshes: Vec::new(),
selections: HashMap::new(),
next_selection_index: 0,
gather_buffers: GatherBuffers::default(),
written_times: HashMap::new(),
};
let write_step = |writer: &mut TimeSeriesDataWriter, time: &str| {
writer
.write_time_step(time, |step| {
step.point_data("scalar_data", DataAttribute::Scalar, vec![0.0; 0])
})
.unwrap();
};
write_step(&mut writer, "0.0");
write_step(&mut writer, "1.0");
write_step(&mut writer, "2.0");
write_step(&mut writer, "10.0");
let expected_xdmf = r#"
<Xdmf Version="2.0" xmlns:xi="http://www.w3.org/2001/XInclude">
<Domain>
<Grid Name="time_series" GridType="Collection" CollectionType="Temporal">
<Grid Name="time_series-t0.0" GridType="Uniform">
<Geometry GeometryType="XYZ">
<DataItem Dimensions="5 3" NumberType="Float" Format="XML" Precision="4">0 1 0 0 1.5 0 0.5 1.5 0.5 1 1.5 0 1 1 0</DataItem>
</Geometry>
<Topology TopologyType="Triangle" NumberOfElements="2">
<DataItem Dimensions="6" NumberType="Int" Format="XML" Precision="4">0 1 2 2 3 4</DataItem>
</Topology>
<Time Value="0.0"/>
<Attribute Name="scalar_data" AttributeType="Scalar" Center="Node">
<DataItem Dimensions="0" NumberType="Float" Format="XML" Precision="8">data_for_0</DataItem>
</Attribute>
</Grid>
<Grid Name="time_series-t1.0" GridType="Uniform">
<Geometry GeometryType="XYZ">
<DataItem Dimensions="5 3" NumberType="Float" Format="XML" Precision="4">0 1 0 0 1.5 0 0.5 1.5 0.5 1 1.5 0 1 1 0</DataItem>
</Geometry>
<Topology TopologyType="Triangle" NumberOfElements="2">
<DataItem Dimensions="6" NumberType="Int" Format="XML" Precision="4">0 1 2 2 3 4</DataItem>
</Topology>
<Time Value="1.0"/>
<Attribute Name="scalar_data" AttributeType="Scalar" Center="Node">
<DataItem Dimensions="0" NumberType="Float" Format="XML" Precision="8">data_for_0</DataItem>
</Attribute>
</Grid>
<Grid Name="time_series-t2.0" GridType="Uniform">
<Geometry GeometryType="XYZ">
<DataItem Dimensions="5 3" NumberType="Float" Format="XML" Precision="4">0 1 0 0 1.5 0 0.5 1.5 0.5 1 1.5 0 1 1 0</DataItem>
</Geometry>
<Topology TopologyType="Triangle" NumberOfElements="2">
<DataItem Dimensions="6" NumberType="Int" Format="XML" Precision="4">0 1 2 2 3 4</DataItem>
</Topology>
<Time Value="2.0"/>
<Attribute Name="scalar_data" AttributeType="Scalar" Center="Node">
<DataItem Dimensions="0" NumberType="Float" Format="XML" Precision="8">data_for_0</DataItem>
</Attribute>
</Grid>
<Grid Name="time_series-t10.0" GridType="Uniform">
<Geometry GeometryType="XYZ">
<DataItem Dimensions="5 3" NumberType="Float" Format="XML" Precision="4">0 1 0 0 1.5 0 0.5 1.5 0.5 1 1.5 0 1 1 0</DataItem>
</Geometry>
<Topology TopologyType="Triangle" NumberOfElements="2">
<DataItem Dimensions="6" NumberType="Int" Format="XML" Precision="4">0 1 2 2 3 4</DataItem>
</Topology>
<Time Value="10.0"/>
<Attribute Name="scalar_data" AttributeType="Scalar" Center="Node">
<DataItem Dimensions="0" NumberType="Float" Format="XML" Precision="8">data_for_0</DataItem>
</Attribute>
</Grid>
</Grid>
</Domain>
<Information Name="data_storage" Value="AsciiInline"/>
<Information Name="version" Value="0.2.0"/>
</Xdmf>"#;
let xdmf_file = xdmf_file_path.with_extension("xdmf2");
let read_xdmf = std::fs::read_to_string(&xdmf_file).unwrap();
pretty_assertions::assert_eq!(expected_xdmf, read_xdmf);
}
struct FlakyWriter {
write_time: Option<String>,
fail_finalize_at: Option<&'static str>,
fail_array: Option<usize>,
}
impl DataWriter for FlakyWriter {
fn format(&self) -> Format {
Format::XML
}
fn data_storage(&self) -> DataStorage {
DataStorage::AsciiInline
}
fn write_points(
&mut self,
_submesh: Option<usize>,
_points: &Values<'_>,
) -> Result<DataContent> {
Ok(DataContent::Raw("points".to_string()))
}
fn write_connectivity(
&mut self,
_submesh: Option<usize>,
_cells: &Values<'_>,
) -> Result<DataContent> {
Ok(DataContent::Raw("cells".to_string()))
}
fn write_submesh_cells(
&mut self,
submesh: usize,
_cells: &Values<'_>,
) -> Result<DataContent> {
Ok(DataContent::Raw(format!("submesh_cells_{submesh}")))
}
fn write_submesh_points(
&mut self,
submesh: usize,
_points: &Values<'_>,
) -> Result<DataContent> {
Ok(DataContent::Raw(format!("submesh_points_{submesh}")))
}
fn write_data(&mut self, index: usize, _data: &Values<'_>) -> Result<DataContent> {
if self.fail_array == Some(index) {
self.fail_array = None;
return Err(Error::Io {
operation: "writing data (simulated)",
path: PathBuf::from("boom"),
source: std::io::Error::other("simulated mid-write failure"),
});
}
Ok(DataContent::Raw(format!("data_for_{index}")))
}
fn write_data_initialize(&mut self, time: &str) -> Result<()> {
if self.write_time.is_some() {
return Err(Error::Internal("writing data was already initialized"));
}
self.write_time = Some(time.to_string());
Ok(())
}
fn write_data_finalize(&mut self) -> Result<()> {
let Some(time) = self.write_time.as_deref() else {
return Err(Error::Internal("writing data was not initialized"));
};
if self.fail_finalize_at == Some(time) {
return Err(Error::Io {
operation: "finalizing data (simulated)",
path: PathBuf::from("finalize"),
source: std::io::Error::other("simulated finalize failure"),
});
}
self.write_time = None;
Ok(())
}
fn write_data_discard(&mut self) -> Result<()> {
if self.write_time.is_none() {
return Err(Error::Internal("writing data was not initialized"));
}
self.write_time = None;
Ok(())
}
}
fn flaky_writer(
xdmf_file_name: PathBuf,
fail_finalize_at: Option<&'static str>,
fail_array: Option<usize>,
) -> TimeSeriesDataWriter {
let grid = Grid::new_uniform("test", dummy_geometry(), dummy_topology());
TimeSeriesDataWriter {
xdmf_file_name,
writer: Box::new(FlakyWriter {
write_time: None,
fail_finalize_at,
fail_array,
}),
xdmf: document_for(&grid),
grid,
step_times: Vec::new(),
num_points: 0,
num_cells: 0,
submeshes: Vec::new(),
selections: HashMap::new(),
next_selection_index: 0,
gather_buffers: GatherBuffers::default(),
written_times: HashMap::new(),
}
}
fn flaky_writer_with_submeshes(xdmf_file_name: PathBuf) -> TimeSeriesDataWriter {
let mut writer = flaky_writer(xdmf_file_name, None, Some(1));
writer.num_cells = 3;
writer.submeshes = ["first", "mid", "last"]
.into_iter()
.enumerate()
.map(|(index, name)| Submesh {
name: name.to_string(),
cells: IndexList::Contiguous {
start: index,
len: 1,
},
points: IndexList::Contiguous {
start: index,
len: 1,
},
})
.collect();
writer.grid = Grid::new_collection(
"mesh",
CollectionType::Spatial,
Some(
writer
.submeshes
.iter()
.map(|submesh| {
Grid::new_uniform(&submesh.name, dummy_geometry(), dummy_topology())
})
.collect(),
),
);
writer
}
#[test]
fn a_failure_partway_through_the_submeshes_writes_no_attribute_at_all() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let mut writer = flaky_writer_with_submeshes(tmp_dir.path().join("partial.xdmf2"));
let result = writer.write_time_step("0.0", |step| {
let _swallowed = step.cell_data("boom", DataAttribute::Scalar, &[1.0, 2.0, 3.0]);
Ok::<(), Error>(())
});
std::assert_matches!(
result.unwrap_err(),
Error::InvalidTimeStep { time, reason }
if time == "0.0" && reason.contains("no data written")
);
assert!(writer.step_times.is_empty());
}
#[test]
fn a_failure_partway_through_the_submeshes_leaves_later_attributes_aligned() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let mut writer = flaky_writer_with_submeshes(tmp_dir.path().join("aligned.xdmf2"));
writer
.write_time_step("0.0", |step| {
let _swallowed = step.cell_data("boom", DataAttribute::Scalar, &[1.0, 2.0, 3.0]);
step.cell_data("fine", DataAttribute::Scalar, &[1.0, 2.0, 3.0])
})
.unwrap();
let sub_grids = last_step_grids(&writer);
assert_eq!(sub_grids.len(), 3);
for sub_grid in sub_grids {
assert_eq!(attribute_names(sub_grid), ["fine"]);
}
}
#[test]
fn write_data_survives_a_mid_write_failure() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let mut writer = flaky_writer(
tmp_dir.path().join("mid_write_failure.xdmf2"),
None,
Some(1),
);
let res = writer.write_time_step("0.0", |step| {
step.point_data("ok", DataAttribute::Scalar, vec![0.0; 0])?;
step.point_data("boom", DataAttribute::Scalar, vec![0.0; 0])
});
std::assert_matches!(res.unwrap_err(), Error::Io { .. });
writer
.write_time_step("0.0", |step| {
step.point_data("ok", DataAttribute::Scalar, vec![0.0; 0])
})
.unwrap();
}
#[test]
fn write_time_step_discards_when_the_closure_swallows_an_attribute_error() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let mut writer = flaky_writer(tmp_dir.path().join("swallowed_error.xdmf2"), None, Some(0));
let res = writer.write_time_step("0.0", |step| {
let _write_result = step.point_data("boom", DataAttribute::Scalar, vec![0.0; 0]);
Ok(())
});
std::assert_matches!(
res.unwrap_err(),
Error::InvalidTimeStep { time, reason }
if time == "0.0" && reason.contains("no data written")
);
assert!(writer.step_times.is_empty());
assert!(writer.written_times.is_empty());
writer
.write_time_step("0.0", |step| {
step.point_data("ok", DataAttribute::Scalar, vec![0.0; 0])
})
.unwrap();
}
#[test]
fn write_time_step_keeps_a_step_whose_closure_swallowed_an_attribute_error() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let mut writer = flaky_writer(tmp_dir.path().join("swallowed_error.xdmf2"), None, Some(1));
writer
.write_time_step("0.0", |step| {
step.point_data("ok", DataAttribute::Scalar, vec![0.0; 0])?;
let _write_result = step.point_data("boom", DataAttribute::Scalar, vec![0.0; 0]);
Ok::<(), Error>(())
})
.unwrap();
let [step_grid] = last_step_grids(&writer)[..] else {
panic!("a mesh without submeshes contributes one grid per step")
};
assert_eq!(
step_grid.time.as_ref().map(|time| time.value.as_str()),
Some("0.0")
);
assert_eq!(attribute_names(step_grid), ["ok"]);
}
#[test]
fn write_time_step_discards_when_finalizing_fails() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let mut writer = flaky_writer(
tmp_dir.path().join("finalize_failure.xdmf2"),
Some("0.0"),
None,
);
let res = writer.write_time_step("0.0", |step| {
step.point_data("ok", DataAttribute::Scalar, vec![0.0; 0])
});
std::assert_matches!(
res.unwrap_err(),
Error::Io {
operation: "finalizing data (simulated)",
..
}
);
assert!(writer.step_times.is_empty());
assert!(writer.written_times.is_empty());
writer
.write_time_step("1.0", |step| {
step.point_data("ok", DataAttribute::Scalar, vec![0.0; 0])
})
.unwrap();
}
}