Compression: add BSHUF_ZSTD_RLE_HUFF (RLE runs + Huffman literals)

New CompressionAlgorithm that emits a standard Zstandard frame: zero/0xFF runs
become RLE_Blocks (like BSHUF_ZSTD_RLE) and literal regions become
Compressed_Blocks with per-block adaptive Huffman literals and no sequences
(Number_of_Sequences=0). Short runs are absorbed into the literal stream;
incompressible literals fall back to Raw_Blocks so the worst case stays within
ZSTD_compressBound.

The Huffman tree + bitstream are produced by zstd's own HUF_compress{1,4}X_repeat
(the same calls ZSTD_compressLiterals uses); only the frame/block/literals-section
framing is hand-written, with comments citing zstd_compression_format.md so it can
be checked clause by clause. Output decodes with stock ZSTD_decompress, so no
reader changes are needed (decode routes like BSHUF_ZSTD).

On sparse diffraction this gives ~12% smaller files than bitshuffle/LZ4 at about
the same end-to-end speed, sitting between LZ4 and full ZSTD; for maximum ratio
use BSHUF_ZSTD. Robust on any input: tests round-trip pure zeros, Poisson(10),
Mersenne-Twister noise (checked against the size bound), an extreme-sparsity mask,
and a real lyso image through stock ZSTD_decompress.

API: exposed as "bszstd_rlehuf"; regenerate the Python/TS clients (update_version.sh)
to surface the new value there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-27 14:41:46 +02:00
co-authored by Claude Opus 4.8
parent 02514bb6b7
commit 7e7a73062c
14 changed files with 286 additions and 4 deletions
+62 -1
View File
@@ -10,6 +10,7 @@
#include "../compression/JFJochCompressor.h"
#include "../compression/JFJochDecompress.h"
#include "../common/DiffractionExperiment.h"
#include "../writer/HDF5Objects.h"
TEST_CASE("JFjochZstdCompressor_Raw_Block","[ZSTD]") {
uint8_t src[128*8];
@@ -315,4 +316,64 @@ TEST_CASE("Bitshuffle_ZSTD","[ZSTD]") {
REQUIRE(bshuf_decompress_zstd(compressed.data(), decompressed.data(), RAW_MODULE_SIZE, 4, 0) == out_size);
REQUIRE(memcmp(image.data(), decompressed.data(), RAW_MODULE_SIZE*sizeof(uint32_t)) == 0);
}
}
// ---- BSHUF_ZSTD_RLE_HUFF (RLE runs + adaptive Huffman literals, standard zstd frame) ----
// Round-trips through JFJochDecompress (which calls stock ZSTD_decompress per block) and checks
// the output never exceeds the advertised worst-case size. Run on good and adversarial data to
// confirm the algorithm always produces valid output, even when the ratio is poor.
template<class T>
static void RequireHuffRoundTrip(const std::vector<T> &image) {
JFJochBitShuffleCompressor compressor(CompressionAlgorithm::BSHUF_ZSTD_RLE_HUFF);
auto compressed = compressor.Compress(image);
REQUIRE(compressed.size() <= (size_t) MaxCompressedSize(CompressionAlgorithm::BSHUF_ZSTD_RLE_HUFF,
image.size(), sizeof(T)));
std::vector<T> output;
REQUIRE_NOTHROW(JFJochDecompress(output, CompressionAlgorithm::BSHUF_ZSTD_RLE_HUFF,
compressed, image.size()));
REQUIRE(output.size() == image.size());
REQUIRE(memcmp(image.data(), output.data(), image.size() * sizeof(T)) == 0);
}
TEST_CASE("ZstdHuff_PureZeros", "[ZSTD]") {
std::vector<int16_t> image(200000, 0); // all runs, no literals
RequireHuffRoundTrip(image);
}
TEST_CASE("ZstdHuff_Poisson10", "[ZSTD]") {
std::vector<uint16_t> image(200000);
std::mt19937 gen(12345);
std::poisson_distribution<int> dist(10);
for (auto &v : image) v = (uint16_t) dist(gen);
RequireHuffRoundTrip(image);
}
TEST_CASE("ZstdHuff_RandomIncompressible", "[ZSTD]") {
std::vector<uint16_t> image(200000); // Mersenne-Twister noise: ~zero compression
std::mt19937 gen(98765);
for (auto &v : image) v = (uint16_t) gen();
RequireHuffRoundTrip(image); // must still be valid and within bound
}
TEST_CASE("ZstdHuff_MaskLike", "[ZSTD]") {
std::vector<uint32_t> image(200000, 0); // extreme sparsity like a pixel_mask
std::mt19937 gen(555);
for (auto &v : image) {
uint32_t r = gen() % 100;
if (r < 5) v = 1;
else if (r == 5) v = 0x80000000u; // module-gap style flag
}
RequireHuffRoundTrip(image);
}
TEST_CASE("ZstdHuff_LysoImage", "[ZSTD]") {
RegisterHDF5Filter(); // bitshuffle filter, needed to read the compressed benchmark dataset
HDF5ReadOnlyFile data("../../tests/test_data/compression_benchmark.h5");
HDF5DataSet dataset(data, "/entry/data/data");
HDF5DataSpace file_space(dataset);
auto dims = file_space.GetDimensions();
std::vector<int16_t> image(dims[1] * dims[2]);
std::vector<hsize_t> start = {0, 0, 0}, size = {1, dims[1], dims[2]};
dataset.ReadVector(image, start, size);
RequireHuffRoundTrip(image);
}