HDF5 2.0 native complex as a first-class type on read; Python libver=
- Datatype::parse returns Datatype::Complex for class 11 (also inside
compounds, arrays and VL types) instead of the {r, i} compound view.
- Facade: DType::Complex(Box<DType>); read_complex_f32/f64 accept it.
- h5rs dump/ls/diff print native complex as h5dump/h5ls/h5diff 2.2.0 do
(checked against a fixture written by h5py 3.16 / libhdf5 2.0.0);
dump --json keeps the {r, i} compound (hdf5-json has no complex class).
- clawhdf5-wasm reads native complex datasets as [re, im] pairs.
- Python: clawhdf5.File(path, 'w', libver=...) with h5py's values,
mapped to FileBuilder::libver_bounds; 'v108' output opens in HDF5 1.8.23.
- Docs: known-issues entry moved to Fixed (history), CHANGELOG, READMEs.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -603,7 +603,22 @@ impl Diff {
|
||||
let dif = match (int_of(&va), int_of(&vb), number(&va), number(&vb)) {
|
||||
(Some(x), Some(y), ..) => x.abs_diff(y).to_string(),
|
||||
(_, _, Some(x), Some(y)) => value::fmt_float((x - y).abs(), 64),
|
||||
_ => String::new(),
|
||||
// Each part's absolute difference, as h5diff 2.x prints
|
||||
// it (`1+0i`).
|
||||
_ => match (&va, &vb) {
|
||||
(
|
||||
Value::Complex { re: ar, im: ai, .. },
|
||||
Value::Complex { re: br, im: bi, .. },
|
||||
) => match (number(ar), number(ai), number(br), number(bi)) {
|
||||
(Some(ar), Some(ai), Some(br), Some(bi)) => format!(
|
||||
"{}+{}i",
|
||||
value::fmt_float((ar - br).abs(), 64),
|
||||
value::fmt_float((ai - bi).abs(), 64)
|
||||
),
|
||||
_ => String::new(),
|
||||
},
|
||||
_ => String::new(),
|
||||
},
|
||||
};
|
||||
rows.push(format!("{pos:<24}{ta:<24}{tb:<24}{dif}"));
|
||||
}
|
||||
@@ -713,6 +728,9 @@ impl Diff {
|
||||
(Value::Array(p), Value::Array(q)) | (Value::Seq(p), Value::Seq(q)) => {
|
||||
p.len() == q.len() && p.iter().zip(q).all(|(u, v)| self.equal(a, b, u, v))
|
||||
}
|
||||
(Value::Complex { re: pr, im: pi, .. }, Value::Complex { re: qr, im: qi, .. }) => {
|
||||
self.equal(a, b, pr, qr) && self.equal(a, b, pi, qi)
|
||||
}
|
||||
(Value::Ref(None), Value::Ref(None)) => true,
|
||||
(Value::Ref(Some(p)), Value::Ref(Some(q))) => {
|
||||
// Addresses mean nothing across files: compare the paths the
|
||||
@@ -846,7 +864,10 @@ fn types_comparable(a: &Datatype, b: &Datatype) -> bool {
|
||||
(
|
||||
Datatype::Enumeration { base_type: x, .. },
|
||||
Datatype::Enumeration { base_type: y, .. },
|
||||
) => types_comparable(x, y),
|
||||
)
|
||||
| (Datatype::Complex { base_type: x, .. }, Datatype::Complex { base_type: y, .. }) => {
|
||||
types_comparable(x, y)
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,9 +135,16 @@ pub fn short(dt: &Datatype) -> String {
|
||||
return f.short.into();
|
||||
}
|
||||
match dt {
|
||||
Datatype::Complex { size, base_type } => {
|
||||
short(&Datatype::complex_as_compound(*size, base_type))
|
||||
}
|
||||
// numpy's names (`complex64` is two `float32`) for IEEE parts,
|
||||
// `complex<part>` otherwise.
|
||||
Datatype::Complex { size, base_type } => match base_type.as_ref() {
|
||||
Datatype::FloatingPoint { byte_order, .. } if is_ieee(base_type) => format!(
|
||||
"complex{}{}",
|
||||
u64::from(*size) * 8,
|
||||
if be(byte_order) { "-be" } else { "" }
|
||||
),
|
||||
_ => format!("complex<{}>", short(base_type)),
|
||||
},
|
||||
Datatype::FixedPoint {
|
||||
size,
|
||||
signed,
|
||||
@@ -216,8 +223,9 @@ pub fn long(dt: &Datatype) -> String {
|
||||
return f.long.into();
|
||||
}
|
||||
match dt {
|
||||
Datatype::Complex { size, base_type } => {
|
||||
long(&Datatype::complex_as_compound(*size, base_type))
|
||||
// As h5ls 2.x prints a complex type it has no native name for.
|
||||
Datatype::Complex { base_type, .. } => {
|
||||
format!("complex number of\n {}", long(base_type))
|
||||
}
|
||||
Datatype::FixedPoint {
|
||||
size,
|
||||
@@ -361,6 +369,19 @@ fn atomic_ddl(dt: &Datatype) -> Option<String> {
|
||||
)),
|
||||
// As h5dump 2.x names them (checked against h5dump 2.2.0).
|
||||
Datatype::FloatingPoint { .. } => small_float(dt).map(|f| f.ddl.to_string()),
|
||||
// h5dump 2.x's predefined complex names: IEEE binary16/32/64 parts.
|
||||
Datatype::Complex { base_type, .. } => match base_type.as_ref() {
|
||||
Datatype::FloatingPoint {
|
||||
size, byte_order, ..
|
||||
} if is_ieee(base_type) && !matches!(byte_order, DatatypeByteOrder::Vax) => {
|
||||
Some(format!(
|
||||
"H5T_COMPLEX_IEEE_F{}{}",
|
||||
u64::from(*size) * 8,
|
||||
order_suffix(byte_order)
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
Datatype::BitField {
|
||||
size, byte_order, ..
|
||||
} => Some(format!(
|
||||
@@ -418,6 +439,10 @@ pub fn ddl(dt: &Datatype, ind: usize) -> String {
|
||||
Datatype::VariableLength { base_type, .. } => {
|
||||
format!("H5T_VLEN {{ {} }}", ddl(base_type, ind))
|
||||
}
|
||||
// Parts h5dump has no predefined complex name for.
|
||||
Datatype::Complex { base_type, .. } => {
|
||||
format!("H5T_COMPLEX {{ {} }}", ddl(base_type, ind))
|
||||
}
|
||||
Datatype::Opaque { size, tag } => {
|
||||
let tag = String::from_utf8_lossy(tag);
|
||||
format!(
|
||||
@@ -498,6 +523,9 @@ fn string_ddl(
|
||||
/// hdf5-json type object.
|
||||
pub fn json(dt: &Datatype) -> J {
|
||||
match dt {
|
||||
// hdf5-json (h5json 2.0.0) has no complex class: a native complex
|
||||
// is written as the `{r, i}` compound h5py uses for complex numbers,
|
||||
// its values as `[re, im]` pairs.
|
||||
Datatype::Complex { size, base_type } => {
|
||||
json(&Datatype::complex_as_compound(*size, base_type))
|
||||
}
|
||||
@@ -598,7 +626,7 @@ fn pad_json(p: &StringPadding) -> &'static str {
|
||||
/// Class name used to decide whether two datatypes can be compared.
|
||||
pub fn class(dt: &Datatype) -> &'static str {
|
||||
match dt {
|
||||
Datatype::Complex { .. } => "compound",
|
||||
Datatype::Complex { .. } => "complex",
|
||||
Datatype::FixedPoint { .. } => "integer",
|
||||
Datatype::FloatingPoint { .. } => "float",
|
||||
Datatype::Time { .. } => "time",
|
||||
|
||||
@@ -27,6 +27,15 @@ pub enum Value {
|
||||
/// An enum member (name, when the value matches one) and its value.
|
||||
Enum(Option<String>, i128),
|
||||
Compound(Vec<(String, Value)>),
|
||||
/// A native complex number (HDF5 2.0 class 11): real and imaginary
|
||||
/// part. `sign_free` is set when h5dump joins the parts with a bare `+`
|
||||
/// (parts it has no native C type for: binary16, non-IEEE), giving
|
||||
/// `1+-2i`; otherwise the imaginary part carries its sign (`1-2i`).
|
||||
Complex {
|
||||
re: Box<Value>,
|
||||
im: Box<Value>,
|
||||
sign_free: bool,
|
||||
},
|
||||
Array(Vec<Value>),
|
||||
/// A variable-length sequence.
|
||||
Seq(Vec<Value>),
|
||||
@@ -183,11 +192,18 @@ impl<'a> Decoder<'a> {
|
||||
return Value::Error("short element".into());
|
||||
};
|
||||
match dt {
|
||||
Datatype::Complex { size, base_type } => self.decode(
|
||||
&Datatype::complex_as_compound(*size, base_type),
|
||||
b,
|
||||
depth + 1,
|
||||
),
|
||||
Datatype::Complex { base_type, .. } => {
|
||||
let part = base_type.type_size() as usize;
|
||||
let (re, im) = b.split_at(part.min(b.len()));
|
||||
Value::Complex {
|
||||
re: Box::new(self.decode(base_type, re, depth + 1)),
|
||||
im: Box::new(self.decode(base_type, im, depth + 1)),
|
||||
// h5dump prints `float`/`double` complex (whatever the
|
||||
// byte order) with `%g%+gi`, anything else as
|
||||
// `<re>+<im>i`.
|
||||
sign_free: !(dtype::is_ieee(base_type) && matches!(part, 4 | 8)),
|
||||
}
|
||||
}
|
||||
Datatype::FixedPoint { .. } => match decode_int(dt, b) {
|
||||
Some(v) => Value::Int(v),
|
||||
None => Value::Bytes(b.to_vec()),
|
||||
@@ -354,6 +370,15 @@ pub fn text(v: &Value, h5paths: &dyn Fn(u64) -> Option<String>) -> String {
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
),
|
||||
Value::Complex { re, im, sign_free } => {
|
||||
let (re, im) = (text(re, h5paths), text(im, h5paths));
|
||||
let plus = if *sign_free || !im.starts_with('-') {
|
||||
"+"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!("{re}{plus}{im}i")
|
||||
}
|
||||
Value::Array(vs) => format!(
|
||||
"[ {} ]",
|
||||
vs.iter()
|
||||
@@ -408,6 +433,7 @@ pub fn to_json(v: &Value, h5paths: &dyn Fn(u64) -> Option<String>) -> J {
|
||||
Value::Bytes(b) | Value::OtherRef(b) => J::from(hex(b)),
|
||||
Value::Enum(_, i) => to_json(&Value::Int(*i), h5paths),
|
||||
Value::Compound(ms) => J::Array(ms.iter().map(|(_, v)| to_json(v, h5paths)).collect()),
|
||||
Value::Complex { re, im, .. } => J::Array(vec![to_json(re, h5paths), to_json(im, h5paths)]),
|
||||
Value::Array(vs) | Value::Seq(vs) => {
|
||||
J::Array(vs.iter().map(|v| to_json(v, h5paths)).collect())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
//! `h5rs dump` and `h5rs ls` of HDF5 2.0 native complex numbers (datatype
|
||||
//! class 11), against the output of h5dump 2.2.0 and h5ls 2.2.0 (the Debian
|
||||
//! h5dump CI installs, 1.14.x, predates the type). The h5dump output is
|
||||
//! stored next to the fixture; see
|
||||
//! `crates/clawhdf5/tests/fixtures/gen_native_complex.py`.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn fixture(name: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../clawhdf5/tests/fixtures")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
fn h5rs(args: &[&str]) -> String {
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_h5rs"))
|
||||
.args(args)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(out.status.success(), "h5rs {args:?}: {out:?}");
|
||||
String::from_utf8(out.stdout).unwrap()
|
||||
}
|
||||
|
||||
/// Split a dump into its lines, with the data lines (`(i): ...`) replaced
|
||||
/// by their index, and the data values in order.
|
||||
fn split(ddl: &str) -> (Vec<String>, Vec<String>) {
|
||||
let (mut frame, mut data) = (Vec::new(), Vec::new());
|
||||
for line in ddl.lines() {
|
||||
match line.split_once("):") {
|
||||
Some((idx, body)) if idx.trim_start().starts_with('(') => {
|
||||
frame.push(format!("{idx}):"));
|
||||
// Values are split at `, `; array elements come bracketed.
|
||||
data.extend(
|
||||
body.split(", ")
|
||||
.map(|s| s.trim().trim_matches(|c| c == '[' || c == ']' || c == ','))
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty() && *s != "{")
|
||||
.map(String::from),
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
let t = line.trim().trim_end_matches(',');
|
||||
if t.ends_with('i') || t.parse::<f64>().is_ok() {
|
||||
// A compound member's value on its own line.
|
||||
data.push(t.to_string());
|
||||
} else {
|
||||
frame.push(line.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(frame, data)
|
||||
}
|
||||
|
||||
/// `a+bi`, `a-bi` or `a+-bi` as its two parts (a real number as one).
|
||||
fn parts(v: &str) -> Vec<f64> {
|
||||
let Some(z) = v.strip_suffix('i') else {
|
||||
return vec![v.parse().unwrap()];
|
||||
};
|
||||
let b = z.as_bytes();
|
||||
let at = (1..b.len())
|
||||
.find(|&k| (b[k] == b'+' || b[k] == b'-') && !matches!(b[k - 1], b'e' | b'E' | b'+'))
|
||||
.unwrap_or_else(|| panic!("not a complex value: {v}"));
|
||||
let (re, im) = z.split_at(at);
|
||||
let im = im.strip_prefix('+').unwrap_or(im);
|
||||
vec![re.parse().unwrap(), im.parse().unwrap()]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dump_matches_h5dump_2_2() {
|
||||
let path = fixture("native_complex_hdf5_2.h5");
|
||||
let ours = h5rs(&["dump", path.to_str().unwrap()]);
|
||||
let reference = std::fs::read_to_string(fixture("native_complex_hdf5_2.ddl")).unwrap();
|
||||
let (mut our_frame, our_data) = split(&ours);
|
||||
let (mut ref_frame, ref_data) = split(&reference);
|
||||
// The first line names the file as it was given.
|
||||
our_frame.remove(0);
|
||||
ref_frame.remove(0);
|
||||
// Everything but the values is h5dump's byte for byte: the types print
|
||||
// as H5T_COMPLEX_IEEE_F64LE, H5T_ARRAY { [2] H5T_COMPLEX_IEEE_F32LE }, ...
|
||||
assert_eq!(our_frame, ref_frame);
|
||||
assert_eq!(our_data.len(), ref_data.len(), "{our_data:?}\n{ref_data:?}");
|
||||
assert_eq!(our_data.len(), 36);
|
||||
// Values: h5dump prints `%g` (6 digits), inf/nan, and the binary16
|
||||
// parts it has no C type for as `<re>+<im>i` (`1+-2i`); h5rs prints
|
||||
// the shortest round-trip string and Inf/NaN, joined the same way.
|
||||
for (i, (o, r)) in our_data.iter().zip(&ref_data).enumerate() {
|
||||
assert_eq!(o.contains("+-"), r.contains("+-"), "[{i}] {o} vs {r}");
|
||||
let (op, rp) = (parts(o), parts(r));
|
||||
assert_eq!(op.len(), rp.len(), "[{i}] {o} vs {r}");
|
||||
for (x, y) in op.iter().zip(&rp) {
|
||||
let same = if y.is_nan() || y.is_infinite() {
|
||||
x.is_nan() == y.is_nan() && (y.is_nan() || x == y)
|
||||
} else {
|
||||
*x as f32 == *y as f32 || (x - y).abs() <= 5e-6 * y.abs()
|
||||
};
|
||||
assert!(same, "[{i}] h5rs {o}, h5dump {r}");
|
||||
assert_eq!(
|
||||
x.is_sign_negative(),
|
||||
y.is_sign_negative(),
|
||||
"[{i}] {o} vs {r}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ls_describes_complex_like_h5ls_2_2() {
|
||||
let path = fixture("native_complex_hdf5_2.h5");
|
||||
// h5ls 2.2.0 -v, `Type:` of the types it has no native C name for (it
|
||||
// prints `native double _Complex` for the others, as it prints `native
|
||||
// double` where h5rs prints `IEEE 64-bit little-endian float`).
|
||||
for (name, long) in [
|
||||
(
|
||||
"c128be",
|
||||
"complex number of\n IEEE 64-bit big-endian float",
|
||||
),
|
||||
(
|
||||
"c128",
|
||||
"complex number of\n IEEE 64-bit little-endian float",
|
||||
),
|
||||
(
|
||||
"c32",
|
||||
"complex number of\n IEEE 16-bit little-endian float",
|
||||
),
|
||||
(
|
||||
"array",
|
||||
"[2] complex number of\n IEEE 32-bit little-endian float",
|
||||
),
|
||||
] {
|
||||
let target = format!("{}/{name}", path.display());
|
||||
let verbose = h5rs(&["ls", "-v", &target]);
|
||||
assert!(
|
||||
verbose.contains(&format!(" Type: {long}\n")),
|
||||
"{name}:\n{verbose}"
|
||||
);
|
||||
}
|
||||
let listing = h5rs(&["ls", path.to_str().unwrap()]);
|
||||
for (name, short) in [
|
||||
("c64", "complex64"),
|
||||
("c128", "complex128"),
|
||||
("c128be", "complex128-be"),
|
||||
("c32", "complex32"),
|
||||
("scalar", "complex128"),
|
||||
("array", "array[2]<complex64>"),
|
||||
("compound", "compound{z: complex128, k: int64}"),
|
||||
] {
|
||||
assert!(
|
||||
listing
|
||||
.lines()
|
||||
.any(|l| l.starts_with(&format!("{name} ")) && l.ends_with(&format!(" {short}"))),
|
||||
"{name}:\n{listing}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_dump_uses_the_r_i_compound() {
|
||||
// hdf5-json (h5json 2.0.0) has no complex class: the type is written as
|
||||
// the `{r, i}` compound h5py uses, the values as `[re, im]` pairs.
|
||||
let path = fixture("native_complex_hdf5_2.h5");
|
||||
let j: serde_json::Value =
|
||||
serde_json::from_str(&h5rs(&["dump", "--json", path.to_str().unwrap()])).unwrap();
|
||||
let ds = j["datasets"]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.values()
|
||||
.find(|d| d["alias"][0] == "/scalar")
|
||||
.unwrap();
|
||||
assert_eq!(ds["type"]["class"], "H5T_COMPOUND");
|
||||
assert_eq!(ds["type"]["fields"][0]["name"], "r");
|
||||
assert_eq!(ds["type"]["fields"][1]["type"]["base"], "H5T_IEEE_F64LE");
|
||||
assert_eq!(ds["value"], serde_json::json!([2.5, -0.5]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_compares_complex_values() {
|
||||
let path = fixture("native_complex_hdf5_2.h5");
|
||||
let p = path.to_str().unwrap();
|
||||
// Same values, different byte order: no differences.
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_h5rs"))
|
||||
.args(["diff", p, p, "/c128", "/c128be"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(out.status.success(), "{out:?}");
|
||||
// One element differs in its imaginary part: one difference, exit
|
||||
// status 1, as h5diff 2.2.0 reports it.
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_h5rs"))
|
||||
.args(["diff", "-r", p, p, "/c128", "/c128x"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(out.status.code(), Some(1), "{out:?}");
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(text.contains("1 difference(s) found"), "{text}");
|
||||
// h5diff 2.2.0: `[ 1 1 ] 0+1i 1+1i 1+0i`.
|
||||
let row = text.lines().find(|l| l.starts_with("[ 1 1 ]")).unwrap();
|
||||
assert_eq!(
|
||||
row.split_whitespace().collect::<Vec<_>>(),
|
||||
["[", "1", "1", "]", "0+1i", "1+1i", "1+0i"]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user