fix(format): honour each bit of a chunk's filter mask
A chunk's filter mask has one bit per pipeline filter; bit i set means filter i was not applied to that chunk (an optional filter that declined, or a direct chunk write). Every read path treated any nonzero mask as "no filters applied" and returned the stored bytes, so a chunk that skipped only gzip in a shuffle+gzip pipeline came back still shuffled (h5py write_direct_chunk with filter_mask=0b10: 8 of 32 values wrong). decompress_chunk_masked undoes the filters the mask leaves set and skips the rest; an unsupported filter is no longer an error when the chunk skipped it. The full, cached, sweep, indexed, parallel and selection (partial_read) paths all use it, and a chunk is copied straight from the file only when every filter was skipped. decompress_chunk is the mask-0 case. Regression: h5py_partial_filter_mask_skips_only_masked_filters (1-D shuffle+gzip with masks 0, 0b10 and 0b11; 2-D with 0b01; full and hyperslab reads) and filter_mask_skips_only_the_masked_filters. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
use std::process::Command;
|
||||
|
||||
use clawhdf5::File;
|
||||
use clawhdf5_format::selection::Selection;
|
||||
|
||||
/// The Python interpreter to drive interop checks with (see
|
||||
/// `h5py_interop_tests.rs`).
|
||||
@@ -103,3 +104,72 @@ for name, lengths in (("{p}", 4), ("{p}.l8", 8)):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Per-chunk filter masks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A chunk's filter mask has one bit per pipeline filter: bit i set means
|
||||
/// filter i was not applied to that chunk. Any nonzero mask used to skip the
|
||||
/// whole pipeline, so a chunk that skipped only gzip was handed back still
|
||||
/// shuffled.
|
||||
#[test]
|
||||
fn h5py_partial_filter_mask_skips_only_masked_filters() {
|
||||
skip_if_no_python!();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("mask.h5");
|
||||
let p = path.display().to_string();
|
||||
run_python(&format!(
|
||||
r#"
|
||||
import h5py, numpy as np, zlib
|
||||
def shuffle(b, es):
|
||||
a = np.frombuffer(b, dtype=np.uint8).reshape(-1, es)
|
||||
return a.T.copy().tobytes()
|
||||
with h5py.File("{p}", "w") as f:
|
||||
# shuffle (0) + gzip (1). Even chunks: both applied. Odd chunks: mask
|
||||
# 0b10, gzip skipped, shuffle applied. Chunk 3: mask 0b11, raw.
|
||||
ds = f.create_dataset("shuf_gzip", shape=(32,), chunks=(8,), dtype="<i4",
|
||||
compression="gzip", shuffle=True)
|
||||
for i in range(4):
|
||||
raw = np.arange(1000 + i * 8, 1008 + i * 8, dtype="<i4").tobytes()
|
||||
if i == 3:
|
||||
ds.id.write_direct_chunk((i * 8,), raw, filter_mask=0b11)
|
||||
elif i % 2:
|
||||
ds.id.write_direct_chunk((i * 8,), shuffle(raw, 4), filter_mask=0b10)
|
||||
else:
|
||||
ds.id.write_direct_chunk((i * 8,), zlib.compress(shuffle(raw, 4)), filter_mask=0)
|
||||
# shuffle (0) + gzip (1) on a 2-D dataset, chunk (0, 1) skips shuffle only.
|
||||
ds = f.create_dataset("grid", shape=(8, 8), chunks=(4, 4), dtype="<f8",
|
||||
compression="gzip", shuffle=True)
|
||||
full = np.arange(64, dtype="<f8").reshape(8, 8)
|
||||
for r in (0, 4):
|
||||
for c in (0, 4):
|
||||
raw = np.ascontiguousarray(full[r:r + 4, c:c + 4]).tobytes()
|
||||
if (r, c) == (0, 4):
|
||||
ds.id.write_direct_chunk((r, c), zlib.compress(raw), filter_mask=0b01)
|
||||
else:
|
||||
ds.id.write_direct_chunk((r, c), zlib.compress(shuffle(raw, 8)), filter_mask=0)
|
||||
assert (f["grid"][...] == full).all()
|
||||
assert (f["shuf_gzip"][...] == np.arange(1000, 1032)).all()
|
||||
"#
|
||||
));
|
||||
|
||||
let file = File::open(&path).unwrap();
|
||||
let want: Vec<i32> = (1000..1032).collect();
|
||||
assert_eq!(file.dataset("shuf_gzip").unwrap().read_i32().unwrap(), want);
|
||||
let grid: Vec<f64> = (0..64).map(f64::from).collect();
|
||||
assert_eq!(file.dataset("grid").unwrap().read_f64().unwrap(), grid);
|
||||
// The selection path decodes chunks on its own.
|
||||
let part = file
|
||||
.dataset("shuf_gzip")
|
||||
.unwrap()
|
||||
.read_selection(&Selection::Hyperslab {
|
||||
start: vec![6],
|
||||
stride: vec![1],
|
||||
count: vec![1],
|
||||
block: vec![20],
|
||||
})
|
||||
.unwrap();
|
||||
let want: Vec<u8> = (1006..1026i32).flat_map(i32::to_le_bytes).collect();
|
||||
assert_eq!(part, want);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user