use std::path::Path;
use hdf5rs::Datatype;
use hdf5rs::format::Sizes;
use hdf5rs::write::{EncodeContext, FileWriter, GroupWriter, H5Write, WriteAt};
use crate::engine::{DumpPosition, DumpQuantity, FieldSampler, FreqDump, Snapshot};
use crate::post::Complex64;
use crate::{Axis, DumpData, DumpResult, Error, ProbeKind, ProbeResult, ProbeValues, Result};
pub const LAYOUT_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct H5Options {
pub deflate: Option<u32>,
}
impl H5Options {
#[cfg(feature = "hdf5-deflate")]
#[must_use]
pub fn deflate(mut self, level: u32) -> Self {
self.deflate = Some(level.min(9));
self
}
}
fn h5(e: hdf5rs::Error) -> Error {
Error::Io(format!("HDF5: {e}"))
}
struct C64(Complex64);
impl H5Write for C64 {
fn datatype(sizes: Sizes) -> Datatype {
Datatype::packed_compound(vec![
("r".into(), f64::datatype(sizes)),
("i".into(), f64::datatype(sizes)),
])
.expect("two f64 members form a valid compound")
}
fn encode(&self, out: &mut Vec<u8>, ctx: &mut EncodeContext<'_>) -> hdf5rs::Result<()> {
self.0.re.encode(out, ctx)?;
self.0.im.encode(out, ctx)
}
}
fn quantity_unit(q: DumpQuantity, freq: bool) -> &'static str {
match (q, freq) {
(DumpQuantity::E, false) => "V/m",
(DumpQuantity::H, false) => "A/m",
(DumpQuantity::E, true) => "V*s/m",
(DumpQuantity::H, true) => "A*s/m",
}
}
fn dump_header<S: WriteAt>(
f: &mut FileWriter<S>,
name: &str,
id: u32,
s: &FieldSampler,
freq: bool,
) -> hdf5rs::Result<()> {
let mut root = f.root();
root.set_attr("format", "ems-dump")?;
root.set_attr("version", &LAYOUT_VERSION)?;
root.set_attr("name", name)?;
root.set_attr("id", &id)?;
root.set_attr("quantity", s.quantity().name())?;
root.set_attr("domain", if freq { "frequency" } else { "time" })?;
root.set_attr(
"position",
match s.position() {
DumpPosition::Nodes => "nodes",
DumpPosition::CellCenters => "cell_centers",
},
)?;
root.set_attr("unit", quantity_unit(s.quantity(), freq))?;
let mut mesh = root.create_group("mesh")?;
for (axis, n) in Axis::ALL.into_iter().zip(["x", "y", "z"]) {
let c = s.coords(axis);
let mut ds = mesh
.new_dataset::<f64>(n)
.shape(&[c.len() as u64])
.write(c)?;
ds.set_attr("unit", "m")?;
}
Ok(())
}
fn field_dataset<'w, S: WriteAt, T: H5Write>(
root: &'w mut GroupWriter<'_, S>,
s: &FieldSampler,
opts: &H5Options,
) -> hdf5rs::Result<hdf5rs::write::DatasetWriter<'w, S>> {
let [nx, ny, nz] = s.dims().map(|n| n as u64);
let mut b = root
.new_dataset::<T>(s.quantity().name())
.shape(&[0, nx, ny, nz, 3])
.max_shape(&[None, Some(nx), Some(ny), Some(nz), Some(3)])
.chunk(&[1, nx, ny, nz, 3]);
if let Some(level) = opts.deflate {
b = b.shuffle().deflate(level);
}
b.create()
}
fn fill_time<S: WriteAt>(
f: &mut FileWriter<S>,
name: &str,
id: u32,
s: &FieldSampler,
snapshots: &[Snapshot],
opts: &H5Options,
) -> hdf5rs::Result<()> {
dump_header(f, name, id, s, false)?;
let mut root = f.root();
let n = snapshots.len() as u64;
let steps: Vec<u64> = snapshots.iter().map(|x| x.step).collect();
let times: Vec<f64> = snapshots.iter().map(|x| x.time).collect();
root.new_dataset::<u64>("step").shape(&[n]).write(&steps)?;
root.new_dataset::<f64>("time")
.shape(&[n])
.write(×)?
.set_attr("unit", "s")?;
let mut ds = field_dataset::<S, f32>(&mut root, s, opts)?;
for snap in snapshots {
if snap.data.len() != s.len() {
return Err(hdf5rs::Error::invalid(format!(
"snapshot of step {} has {} points, the sampler {}",
snap.step,
snap.data.len(),
s.len()
)));
}
ds.append(snap.data.as_flattened())?;
}
ds.set_attr("unit", quantity_unit(s.quantity(), false))
}
fn fill_freq<S: WriteAt>(
f: &mut FileWriter<S>,
name: &str,
id: u32,
d: &FreqDump,
opts: &H5Options,
) -> hdf5rs::Result<()> {
let s = d.sampler();
dump_header(f, name, id, s, true)?;
let mut root = f.root();
root.set_attr("samples", &d.samples())?;
root.new_dataset::<f64>("frequency")
.shape(&[d.freqs().len() as u64])
.write(d.freqs())?
.set_attr("unit", "Hz")?;
let mut ds = field_dataset::<S, C64>(&mut root, s, opts)?;
for k in 0..d.freqs().len() {
let v: Vec<C64> = d.values(k).into_iter().flatten().map(C64).collect();
ds.append(&v)?;
}
ds.set_attr("unit", quantity_unit(s.quantity(), true))
}
fn finish_to(
path: &Path,
fill: impl FnOnce(&mut FileWriter<std::fs::File>) -> hdf5rs::Result<()>,
) -> Result<()> {
let mut f = FileWriter::create(path).map_err(h5)?;
fill(&mut f).map_err(h5)?;
f.finish().map_err(h5)?;
Ok(())
}
fn finish_bytes(
fill: impl FnOnce(&mut FileWriter<Vec<u8>>) -> hdf5rs::Result<()>,
) -> Result<Vec<u8>> {
let mut f = FileWriter::in_memory();
fill(&mut f).map_err(h5)?;
f.finish().map_err(h5)
}
pub fn write_time_dump(
path: impl AsRef<Path>,
name: &str,
id: u32,
sampler: &FieldSampler,
snapshots: &[Snapshot],
opts: &H5Options,
) -> Result<()> {
finish_to(path.as_ref(), |f| {
fill_time(f, name, id, sampler, snapshots, opts)
})
}
pub fn time_dump_bytes(
name: &str,
id: u32,
sampler: &FieldSampler,
snapshots: &[Snapshot],
opts: &H5Options,
) -> Result<Vec<u8>> {
finish_bytes(|f| fill_time(f, name, id, sampler, snapshots, opts))
}
pub fn write_freq_dump(
path: impl AsRef<Path>,
name: &str,
id: u32,
dump: &FreqDump,
opts: &H5Options,
) -> Result<()> {
finish_to(path.as_ref(), |f| fill_freq(f, name, id, dump, opts))
}
pub fn freq_dump_bytes(name: &str, id: u32, dump: &FreqDump, opts: &H5Options) -> Result<Vec<u8>> {
finish_bytes(|f| fill_freq(f, name, id, dump, opts))
}
pub fn write_dump(path: impl AsRef<Path>, dump: &DumpResult, opts: &H5Options) -> Result<()> {
match dump.data() {
DumpData::Time { sampler, snapshots } => {
write_time_dump(path, dump.name(), dump.id(), sampler, snapshots, opts)
}
DumpData::Freq(d) => write_freq_dump(path, dump.name(), dump.id(), d, opts),
}
}
pub fn dump_bytes(dump: &DumpResult, opts: &H5Options) -> Result<Vec<u8>> {
match dump.data() {
DumpData::Time { sampler, snapshots } => {
time_dump_bytes(dump.name(), dump.id(), sampler, snapshots, opts)
}
DumpData::Freq(d) => freq_dump_bytes(dump.name(), dump.id(), d, opts),
}
}
fn group_name(name: &str) -> String {
let s: String = name
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || "-_.".contains(c) {
c
} else {
'_'
}
})
.collect();
if s.is_empty() || s == "." || s == ".." {
format!("_{s}")
} else {
s
}
}
fn fill_probes<'a, S: WriteAt>(
f: &mut FileWriter<S>,
probes: impl IntoIterator<Item = &'a ProbeResult>,
) -> hdf5rs::Result<()> {
let mut root = f.root();
root.set_attr("format", "ems-probes")?;
root.set_attr("version", &LAYOUT_VERSION)?;
let mut used = std::collections::HashSet::new();
for p in probes {
let mut g = group_name(p.name());
if !used.insert(g.clone()) {
g = format!("{g}_{}", p.id());
let mut k = 2;
while !used.insert(g.clone()) {
g = format!("{}_{}_{k}", group_name(p.name()), p.id());
k += 1;
}
}
let mut grp = root.create_group(&g)?;
grp.set_attr("name", p.name())?;
grp.set_attr(
"kind",
match p.kind() {
ProbeKind::Voltage => "voltage",
ProbeKind::Current => "current",
ProbeKind::EField => "efield",
ProbeKind::HField => "hfield",
},
)?;
grp.set_attr("id", &p.id())?;
grp.set_attr("number", &p.number())?;
grp.set_attr("dt", &p.dt())?;
let n = p.times().len() as u64;
grp.new_dataset::<f64>("time")
.shape(&[n])
.write(p.times())?
.set_attr("unit", "s")?;
let unit = match p.kind() {
ProbeKind::Voltage => "V",
ProbeKind::Current => "A",
ProbeKind::EField => "V/m",
ProbeKind::HField => "A/m",
};
let mut ds = match p.values() {
ProbeValues::Scalar(v) => grp
.new_dataset::<f64>("values")
.shape(&[v.len() as u64])
.write(v)?,
ProbeValues::Vector(v) => grp
.new_dataset::<f64>("values")
.shape(&[v.len() as u64, 3])
.write(v.as_flattened())?,
};
ds.set_attr("unit", unit)?;
if !p.frequencies().is_empty() {
grp.new_dataset::<f64>("frequencies")
.shape(&[p.frequencies().len() as u64])
.write(p.frequencies())?;
}
}
Ok(())
}
pub fn write_probes<'a>(
path: impl AsRef<Path>,
probes: impl IntoIterator<Item = &'a ProbeResult>,
) -> Result<()> {
finish_to(path.as_ref(), |f| fill_probes(f, probes))
}
pub fn probes_bytes<'a>(probes: impl IntoIterator<Item = &'a ProbeResult>) -> Result<Vec<u8>> {
finish_bytes(|f| fill_probes(f, probes))
}
#[cfg(test)]
mod tests {
use super::group_name;
#[test]
fn group_names() {
assert_eq!(group_name("port_ut1"), "port_ut1");
assert_eq!(group_name("a/b c"), "a_b_c");
assert_eq!(group_name(""), "_");
assert_eq!(group_name(".."), "_..");
}
}