use std::{
path::{Path, PathBuf},
str::FromStr,
};
use serde::{Deserialize, Serialize};
use xdmf_elements::{
attribute,
data_item::{DataContent, Format},
};
mod ascii_writer;
mod binary_writer;
mod error;
#[cfg(feature = "hdf5")]
mod hdf5_writer;
mod paraview;
mod reader;
mod time_series_writer;
mod values;
pub mod xdmf_elements;
pub use error::{Error, Result};
pub use reader::{DataInfo, TimeSeriesReader, ValueType};
pub use time_series_writer::{SubmeshCells, TimeSeriesDataWriter, TimeSeriesWriter, TimeStep};
pub use values::{ConnectivityIndex, Coordinate, Values};
pub use xdmf_elements::CellType;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub enum DataStorage {
Ascii,
AsciiInline,
Hdf5SingleFile {
deflate_level: Option<u8>,
},
Hdf5MultipleFiles {
deflate_level: Option<u8>,
},
Binary,
}
impl FromStr for DataStorage {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"ascii" => Ok(Self::Ascii),
"asciiinline" | "ascii_inline" | "ascii-inline" => Ok(Self::AsciiInline),
"hdf5singlefile" | "hdf5_single_file" | "hdf5-single-file" => {
Ok(Self::Hdf5SingleFile {
deflate_level: None,
})
}
"hdf5multiplefiles" | "hdf5_multiple_files" | "hdf5-multiple-files" => {
Ok(Self::Hdf5MultipleFiles {
deflate_level: None,
})
}
"binary" => Ok(Self::Binary),
_ => Err(format!(
"Invalid DataStorage variant: '{s}'. Valid options are: 'Ascii', 'AsciiInline', 'Hdf5SingleFile', 'Hdf5MultipleFiles', 'Binary'"
)),
}
}
}
pub(crate) trait DataWriter: Send + Sync {
fn format(&self) -> Format;
fn data_storage(&self) -> DataStorage;
fn write_points(&mut self, submesh: Option<usize>, points: &Values<'_>) -> Result<DataContent>;
fn write_point_component(
&mut self,
_component: usize,
_coordinates: &Values<'_>,
) -> Result<DataContent> {
Err(Error::Internal(
"this storage cannot be selected out of, so its submeshes carry their own points",
))
}
fn write_connectivity(
&mut self,
submesh: Option<usize>,
cells: &Values<'_>,
) -> Result<DataContent>;
fn write_submesh_cells(&mut self, submesh: usize, cells: &Values<'_>) -> Result<DataContent>;
fn write_submesh_points(&mut self, submesh: usize, points: &Values<'_>) -> Result<DataContent>;
fn write_data(&mut self, index: usize, data: &Values<'_>) -> Result<DataContent>;
fn supports_selections(&self) -> bool {
false
}
fn write_selection(&mut self, _index: usize, _indices: &Values<'_>) -> Result<DataContent> {
Err(Error::Internal(
"this storage cannot be selected out of, so it is never asked to write a selection",
))
}
fn write_data_initialize(&mut self, _time: &str) -> Result<()> {
Ok(())
}
fn write_data_finalize(&mut self) -> Result<()> {
Ok(())
}
fn write_data_discard(&mut self) -> Result<()> {
self.write_data_finalize()
}
fn flush(&mut self) -> Result<()> {
Ok(())
}
}
fn validate_deflate_level(deflate_level: Option<u8>) -> Result<()> {
if let Some(level) = deflate_level
&& level > 9
{
return Err(Error::InvalidConfiguration {
reason: format!("deflate level {level} is out of range, must be between 0 and 9"),
});
}
Ok(())
}
pub(crate) fn create_writer(
file_name: &Path,
data_storage: DataStorage,
) -> Result<Box<dyn DataWriter>> {
match data_storage {
DataStorage::Ascii => Ok(Box::new(ascii_writer::AsciiWriter::new(file_name)?)),
DataStorage::AsciiInline => Ok(Box::new(ascii_writer::AsciiInlineWriter::new())),
DataStorage::Hdf5SingleFile { deflate_level } => {
validate_deflate_level(deflate_level)?;
cfg_select! {
feature = "hdf5" => Ok(Box::new(hdf5_writer::SingleFileHdf5Writer::new(
file_name,
deflate_level.unwrap_or(hdf5_writer::DEFAULT_DEFLATE_LEVEL),
)?)),
_ => Err(Error::InvalidConfiguration {
reason: format!(
"using {data_storage:?} DataStorage requires the 'hdf5' feature"
),
}),
}
}
DataStorage::Hdf5MultipleFiles { deflate_level } => {
validate_deflate_level(deflate_level)?;
cfg_select! {
feature = "hdf5" => Ok(Box::new(hdf5_writer::MultipleFilesHdf5Writer::new(
file_name,
deflate_level.unwrap_or(hdf5_writer::DEFAULT_DEFLATE_LEVEL),
)?)),
_ => Err(Error::InvalidConfiguration {
reason: format!(
"using {data_storage:?} DataStorage requires the 'hdf5' feature"
),
}),
}
}
DataStorage::Binary => Ok(Box::new(binary_writer::BinaryWriter::new(file_name)?)),
}
}
pub const fn is_hdf5_enabled() -> bool {
cfg_select! {
feature = "hdf5" => true,
_ => false,
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum DataAttribute {
Scalar,
Vector,
Tensor,
Tensor6,
Matrix(usize, usize),
Generic(usize),
}
impl DataAttribute {
pub(crate) fn size(&self) -> Option<usize> {
match self {
Self::Scalar => Some(1),
Self::Vector => Some(3),
Self::Tensor => Some(9),
Self::Tensor6 => Some(6),
Self::Matrix(n, m) => n.checked_mul(*m),
Self::Generic(size) => Some(*size),
}
}
}
impl From<DataAttribute> for attribute::AttributeType {
fn from(data_attr: DataAttribute) -> Self {
match data_attr {
DataAttribute::Scalar => Self::Scalar,
DataAttribute::Vector => Self::Vector,
DataAttribute::Tensor => Self::Tensor,
DataAttribute::Tensor6 => Self::Matrix, DataAttribute::Matrix(_, _) => Self::Matrix,
DataAttribute::Generic(_) => Self::Matrix,
}
}
}
pub fn mpi_safe_create_dir_all(path: impl AsRef<Path> + std::fmt::Debug) -> Result<()> {
if !&path.as_ref().exists() {
std::fs::create_dir_all(&path)
.map_err(error::io_ctx("creating directory", path.as_ref()))?;
}
if !path.as_ref().exists() {
std::thread::sleep(std::time::Duration::from_millis(50));
}
Ok(())
}
pub(crate) fn remove_step_files(step_files: &mut Vec<PathBuf>) -> Result<()> {
let mut first_error = None;
for path in step_files.drain(..) {
let result = std::fs::remove_file(&path)
.map_err(error::io_ctx("removing discarded data file", &path));
if let Err(error) = result
&& first_error.is_none()
{
first_error = Some(error);
}
}
match first_error {
Some(error) => Err(error),
None => Ok(()),
}
}
pub(crate) const POINTS: &str = "points";
pub(crate) const CELLS: &str = "cells";
pub(crate) const SUBMESH_POINTS: &str = "submesh_points";
pub(crate) const SUBMESH_CELLS: &str = "submesh_cells";
pub(crate) const SELECTIONS: &str = "selections";
pub(crate) const DATA_STORAGE: &str = "data_storage";
pub(crate) fn mesh_file_name(array: &str, submesh: Option<usize>, extension: &str) -> String {
match submesh {
Some(index) => format!("{array}_{index}.{extension}"),
None => format!("{array}.{extension}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mpi_safe_create_dir_all() {
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_data_attribute() {
let scalar = DataAttribute::Scalar;
let vector = DataAttribute::Vector;
let tensor = DataAttribute::Tensor;
let tensor6 = DataAttribute::Tensor6;
let matrix = DataAttribute::Matrix(3, 3);
let generic = DataAttribute::Generic(5);
assert_eq!(scalar.size(), Some(1));
assert_eq!(vector.size(), Some(3));
assert_eq!(tensor.size(), Some(9));
assert_eq!(tensor6.size(), Some(6));
assert_eq!(matrix.size(), Some(9));
assert_eq!(generic.size(), Some(5));
assert_eq!(DataAttribute::Matrix(usize::MAX, 2).size(), None);
assert_eq!(attribute::AttributeType::Scalar, scalar.into());
assert_eq!(attribute::AttributeType::Vector, vector.into());
assert_eq!(attribute::AttributeType::Tensor, tensor.into());
assert_eq!(attribute::AttributeType::Matrix, tensor6.into());
assert_eq!(attribute::AttributeType::Matrix, matrix.into());
assert_eq!(attribute::AttributeType::Matrix, generic.into());
}
#[test]
fn test_data_storage_from_str() {
assert_eq!("ascii".parse::<DataStorage>().unwrap(), DataStorage::Ascii);
assert_eq!("Ascii".parse::<DataStorage>().unwrap(), DataStorage::Ascii);
assert_eq!("ASCII".parse::<DataStorage>().unwrap(), DataStorage::Ascii);
assert_eq!(
"asciiinline".parse::<DataStorage>().unwrap(),
DataStorage::AsciiInline
);
assert_eq!(
"ascii_inline".parse::<DataStorage>().unwrap(),
DataStorage::AsciiInline
);
assert_eq!(
"ascii-inline".parse::<DataStorage>().unwrap(),
DataStorage::AsciiInline
);
assert_eq!(
"hdf5singlefile".parse::<DataStorage>().unwrap(),
DataStorage::Hdf5SingleFile {
deflate_level: None
}
);
assert_eq!(
"hdf5_single_file".parse::<DataStorage>().unwrap(),
DataStorage::Hdf5SingleFile {
deflate_level: None
}
);
assert_eq!(
"Hdf5-Single-File".parse::<DataStorage>().unwrap(),
DataStorage::Hdf5SingleFile {
deflate_level: None
}
);
assert_eq!(
"hdf5multiplefiles".parse::<DataStorage>().unwrap(),
DataStorage::Hdf5MultipleFiles {
deflate_level: None
}
);
assert_eq!(
"hdf5_multiple_files".parse::<DataStorage>().unwrap(),
DataStorage::Hdf5MultipleFiles {
deflate_level: None
}
);
assert_eq!(
"HDF5-Multiple-Files".parse::<DataStorage>().unwrap(),
DataStorage::Hdf5MultipleFiles {
deflate_level: None
}
);
assert_eq!(
"binary".parse::<DataStorage>().unwrap(),
DataStorage::Binary
);
assert_eq!(
"Binary".parse::<DataStorage>().unwrap(),
DataStorage::Binary
);
let err = "invalid".parse::<DataStorage>().unwrap_err();
assert_eq!(
err,
"Invalid DataStorage variant: 'invalid'. Valid options are: 'Ascii', 'AsciiInline', 'Hdf5SingleFile', 'Hdf5MultipleFiles', 'Binary'"
);
let err = "".parse::<DataStorage>().unwrap_err();
assert_eq!(
err,
"Invalid DataStorage variant: ''. Valid options are: 'Ascii', 'AsciiInline', 'Hdf5SingleFile', 'Hdf5MultipleFiles', 'Binary'"
);
}
#[test]
fn test_validate_deflate_level() {
validate_deflate_level(None).unwrap();
validate_deflate_level(Some(0)).unwrap();
validate_deflate_level(Some(9)).unwrap();
std::assert_matches!(
validate_deflate_level(Some(10)).unwrap_err(),
Error::InvalidConfiguration { reason } if reason.contains("deflate level 10")
);
std::assert_matches!(
validate_deflate_level(Some(255)).unwrap_err(),
Error::InvalidConfiguration { reason } if reason.contains("deflate level 255")
);
}
#[test]
fn create_writer_rejects_invalid_deflate_level() {
let tmp_dir = temp_dir::TempDir::new().unwrap();
let file_name = tmp_dir.path().join("test.xdmf");
for storage in [
DataStorage::Hdf5SingleFile {
deflate_level: Some(10),
},
DataStorage::Hdf5MultipleFiles {
deflate_level: Some(10),
},
] {
let Err(err) = create_writer(&file_name, storage) else {
panic!("expected an error for deflate_level 10");
};
std::assert_matches!(err, Error::InvalidConfiguration { reason } if reason.contains("deflate level 10"));
}
}
}