reorganize datasets and algs, add seismic
20 changed files+358−189
.gitignoremodified+6−1View file
@@ -1,4 +1,9 @@
11 *.out
22 benchmark_results
33 .benchmark_cache
4-__pycache__
4+__pycache__
5+*.dat
6+*.npy
7+*.segy
8+*.mat
9+*.edf
README.mdmodified+3−3View file
@@ -1,12 +1,12 @@
11 # benchcompress
22
3-A benchmarking framework for evaluating compression algorithms on numeric timeseries datasets, with a focus on scientific data.
3+A benchmarking framework for evaluating compression algorithms on data arrays, with a focus on scientific data.
44
55 Latest benchmark results: https://magland.github.io/benchcompress/
66
77 ## Overview
88
9-Benchcompress is a comprehensive benchmarking framework for evaluating compression algorithms on numeric timeseries datasets. The system follows an automated workflow:
9+Benchcompress is a comprehensive benchmarking framework for evaluating compression algorithms on scientific data arrays. The system follows an automated workflow:
1010
1111 1. **Defining Components**
1212 - Algorithms are implemented in `benchcompress/src/benchcompress/algorithms/`
@@ -91,7 +91,7 @@ H(X) = -\sum_{i} p(x_i) \log p(x_i).
9191 $$
9292 Here, $p(x_i)$ represents the probability of occurrence of the $i$-th symbol $x_i$ in the discrete distribution.
9393
94-In practice, achieving this theoretical compression ratio often requires sophisticated encoding techniques. While arithmetic encoding provides one such method, it is challenging to implement and can be computationally inefficient. A more modern and efficient alternative is Asymmetric Numeric Systems (ANS), which closely approaches the theoretical limit and is incorporated into state-of-the-art compressors such as ZStandard. However, these algorithms are primarily optimized for structured data types, such as text, rather than for scientific numerical timeseries data.
94+In practice, achieving this theoretical compression ratio often requires sophisticated encoding techniques. While arithmetic encoding provides one such method, it is challenging to implement and can be computationally inefficient. A more modern and efficient alternative is Asymmetric Numeric Systems (ANS), which closely approaches the theoretical limit and is incorporated into state-of-the-art compressors such as ZStandard. However, these algorithms are primarily optimized for structured data types, such as text, rather than for floating point or integer arrays.
9595
9696 In our benchmarks, we evaluate a simple implementation of ANS using a Python package we developed, called \texttt{simple\_ans}. As anticipated, ANS demonstrates superior performance when compressing i.i.d. samples from a discrete distribution. However, its efficiency diminishes when handling more structured data, such as continuous signals (e.g., voltage traces in electrophysiology).
9797
benchcompress/pyproject.tomlmodified+1−1View file
@@ -5,7 +5,7 @@ build-backend = "scikit_build_core.build"
55 [project]
66 name = "benchcompress"
77 version = "0.1.0"
8-description = "Benchmarking compression methods for numeric timeseries data"
8+description = "Benchmarking compression methods for integer timeseries data"
99 readme = "README.md"
1010 requires-python = ">=3.8"
1111 authors = [
benchcompress/setup.pymodified+1−1View file
@@ -61,7 +61,7 @@ setup(
6161 ],
6262 },
6363 author="Jeremy Magland",
64- description="Benchmarking compression methods for numeric time series data",
64+ description="Benchmarking compression methods for scientific data arrays",
6565 long_description=open("README.md").read(),
6666 long_description_content_type="text/markdown",
6767 keywords="compression, signal processing",
benchcompress/src/benchcompress/algorithms/ans/__init__.pymodified+20−13View file
@@ -13,7 +13,6 @@ SOURCE_FILE = "ans/__init__.py"
1313 def ans_encode(x: np.ndarray) -> bytes:
1414 from simple_ans import ans_encode
1515
16- assert x.ndim == 1
1716 encoded = ans_encode(x)
1817 if x.dtype == np.uint8:
1918 dtype_code = 0
@@ -43,7 +42,7 @@ def ans_encode(x: np.ndarray) -> bytes:
4342 return header_size.tobytes() + header_bytes + encoded.bitstream
4443
4544
46-def ans_decode(x: bytes, dtype: str) -> np.ndarray:
45+def ans0_decode(x: bytes, dtype: str, shape: tuple) -> np.ndarray:
4746 from simple_ans import ans_decode, EncodedSignal
4847
4948 header_size = np.frombuffer(x[:4], dtype=np.uint32)[0]
@@ -73,13 +72,14 @@ def ans_decode(x: bytes, dtype: str) -> np.ndarray:
7372 symbol_values=symbol_values.astype(dtype),
7473 bitstream=bitstream,
7574 )
76- return ans_decode(encoded)
75+ return ans_decode(encoded).reshape(shape)
7776
7877
7978 def ans_delta_encode(x: np.ndarray) -> bytes:
8079 from simple_ans import ans_encode
8180
8281 assert x.ndim == 1
82+
8383 y = np.diff(x)
8484 # Encode just the differences
8585 encoded = ans_encode(y)
@@ -113,9 +113,11 @@ def ans_delta_encode(x: np.ndarray) -> bytes:
113113 return header_size.tobytes() + header_bytes + encoded.bitstream
114114
115115
116-def ans_delta_decode(x: bytes, dtype: str) -> np.ndarray:
116+def ans_delta_decode(x: bytes, dtype: str, shape: tuple) -> np.ndarray:
117117 from simple_ans import ans_decode, EncodedSignal
118118
119+ assert len(shape) == 1
120+
119121 header_size = np.frombuffer(x[:4], dtype=np.uint32)[0]
120122 header = np.frombuffer(x[4 : 4 + header_size], dtype=np.int64)
121123 dtype_code, num_bits, signal_length, state, num_symbols, x0 = header[
@@ -155,6 +157,7 @@ def ans_markov_encode(x: np.ndarray) -> bytes:
155157 from simple_ans import ans_encode
156158
157159 assert x.ndim == 1
160+
158161 coeffs, initial, resid = markov_predict_cpp(x, M=6, num_training_samples=10000)
159162 # Encode just the differences
160163 encoded = ans_encode(resid)
@@ -191,9 +194,11 @@ def ans_markov_encode(x: np.ndarray) -> bytes:
191194 return header_size.tobytes() + header_bytes + encoded.bitstream
192195
193196
194-def ans_markov_decode(x: bytes, dtype: str) -> np.ndarray:
197+def ans_markov_decode(x: bytes, dtype: str, shape: tuple) -> np.ndarray:
195198 from simple_ans import ans_decode, EncodedSignal
196199
200+ assert len(shape) == 1
201+
197202 header_size = np.frombuffer(x[:4], dtype=np.uint32)[0]
198203 header = np.frombuffer(x[4 : 4 + header_size], dtype=np.float64)
199204 dtype_code, num_bits, signal_length, state, num_symbols, num_coeffs, num_initial = (
@@ -321,9 +326,11 @@ def ans_markov_sparse_encode(x: np.ndarray) -> bytes:
321326 )
322327
323328
324-def ans_markov_sparse_decode(x: bytes, dtype: str) -> np.ndarray:
329+def ans_markov_sparse_decode(x: bytes, dtype: str, shape: tuple) -> np.ndarray:
325330 from simple_ans import ans_decode, EncodedSignal
326331
332+ assert len(shape) == 1
333+
327334 header_size = np.frombuffer(x[:4], dtype=np.uint32)[0]
328335 header = np.frombuffer(x[4 : 4 + header_size], dtype=np.float64)
329336 (
@@ -432,7 +439,7 @@ algorithms = [
432439 "name": "ANS",
433440 "version": "3",
434441 "encode": lambda x: ans_encode(x),
435- "decode": lambda x, dtype: ans_decode(x, dtype),
442+ "decode": lambda x, dtype, shape: ans0_decode(x, dtype, shape),
436443 "description": "ANS compression via simple_ans for efficient data compression.",
437444 "tags": ["ANS"],
438445 "source_file": SOURCE_FILE,
@@ -441,27 +448,27 @@ algorithms = [
441448 "name": "ANS-delta",
442449 "version": "3",
443450 "encode": lambda x: ans_delta_encode(x),
444- "decode": lambda x, dtype: ans_delta_decode(x, dtype),
451+ "decode": lambda x, dtype, shape: ans_delta_decode(x, dtype, shape),
445452 "description": "ANS compression via simple_ans with delta encoding for improved compression of sequential data.",
446- "tags": ["ANS", "delta_encoding"],
453+ "tags": ["ANS", "delta_encoding", "1d"],
447454 "source_file": SOURCE_FILE,
448455 },
449456 {
450457 "name": "ANS-markov",
451458 "version": "6",
452459 "encode": lambda x: ans_markov_encode(x),
453- "decode": lambda x, dtype: ans_markov_decode(x, dtype),
460+ "decode": lambda x, dtype, shape: ans_markov_decode(x, dtype, shape),
454461 "description": "ANS compression via simple_ans with Markov prediction for exploiting temporal correlations in the data.",
455- "tags": ["ANS", "markov_prediction"],
462+ "tags": ["ANS", "markov_prediction", "1d"],
456463 "source_file": SOURCE_FILE,
457464 },
458465 {
459466 "name": "ANS-markov-zrle",
460467 "version": "6",
461468 "encode": lambda x: ans_markov_sparse_encode(x),
462- "decode": lambda x, dtype: ans_markov_sparse_decode(x, dtype),
469+ "decode": lambda x, dtype, shape: ans_markov_sparse_decode(x, dtype, shape),
463470 "description": "ANS compression via simple_ans with Markov prediction and zero run-length encoding for sparse data.",
464- "tags": ["ANS", "markov_prediction", "zero_rle"],
471+ "tags": ["ANS", "markov_prediction", "zero_rle", "1d"],
465472 "source_file": SOURCE_FILE,
466473 },
467474 ]
benchcompress/src/benchcompress/algorithms/brotli/__init__.pymodified+10−10View file
@@ -14,23 +14,23 @@ def brotli_delta_encode(x: np.ndarray, level: int) -> bytes:
1414 return compressed
1515
1616
17-def brotli_delta_decode(x: bytes, dtype: str) -> np.ndarray:
17+def brotli_delta_decode(x: bytes, dtype: str, shape: tuple) -> np.ndarray:
18+ assert len(shape) == 1
1819 buf = brotli.decompress(x)
1920 y = np.frombuffer(buf, dtype=dtype)
2021 return np.cumsum(y)
2122
2223
2324 def brotli_encode(x: np.ndarray, level: int) -> bytes:
24- assert x.ndim == 1
2525 buf = x.tobytes()
2626 compressed = brotli.compress(buf, quality=level)
2727 return compressed
2828
2929
30-def brotli_decode(x: bytes, dtype: str) -> np.ndarray:
30+def brotli_decode(x: bytes, dtype: str, shape: tuple) -> np.ndarray:
3131 buf = brotli.decompress(x)
3232 y = np.frombuffer(buf, dtype=dtype)
33- return y
33+ return y.reshape(shape)
3434
3535
3636 algorithms = [
@@ -38,7 +38,7 @@ algorithms = [
3838 "name": "brotli-4",
3939 "version": "1",
4040 "encode": lambda x: brotli_encode(x, level=4),
41- "decode": lambda x, dtype: brotli_decode(x, dtype),
41+ "decode": lambda x, dtype, shape: brotli_decode(x, dtype, shape),
4242 "description": "Brotli compression at level 4 (faster).",
4343 "tags": ["brotli"],
4444 "source_file": SOURCE_FILE,
@@ -47,7 +47,7 @@ algorithms = [
4747 "name": "brotli-6",
4848 "version": "1",
4949 "encode": lambda x: brotli_encode(x, level=6),
50- "decode": lambda x, dtype: brotli_decode(x, dtype),
50+ "decode": lambda x, dtype, shape: brotli_decode(x, dtype, shape),
5151 "description": "Brotli compression at level 6 (balanced).",
5252 "tags": ["brotli"],
5353 "source_file": SOURCE_FILE,
@@ -56,7 +56,7 @@ algorithms = [
5656 "name": "brotli-8",
5757 "version": "1",
5858 "encode": lambda x: brotli_encode(x, level=8),
59- "decode": lambda x, dtype: brotli_decode(x, dtype),
59+ "decode": lambda x, dtype, shape: brotli_decode(x, dtype, shape),
6060 "description": "Brotli compression at level 8 (better compression).",
6161 "tags": ["brotli"],
6262 "source_file": SOURCE_FILE,
@@ -65,7 +65,7 @@ algorithms = [
6565 "name": "brotli-11",
6666 "version": "1",
6767 "encode": lambda x: brotli_encode(x, level=11),
68- "decode": lambda x, dtype: brotli_decode(x, dtype),
68+ "decode": lambda x, dtype, shape: brotli_decode(x, dtype, shape),
6969 "description": "Brotli compression at maximum level 11.",
7070 "tags": ["brotli"],
7171 "source_file": SOURCE_FILE,
@@ -74,9 +74,9 @@ algorithms = [
7474 "name": "brotli-11-delta",
7575 "version": "1",
7676 "encode": lambda x: brotli_delta_encode(x, level=11),
77- "decode": lambda x, dtype: brotli_delta_decode(x, dtype),
77+ "decode": lambda x, dtype, shape: brotli_delta_decode(x, dtype, shape),
7878 "description": "Brotli compression at level 11 with delta encoding.",
79- "tags": ["brotli", "delta_encoding"],
79+ "tags": ["brotli", "delta_encoding", "1d"],
8080 "source_file": SOURCE_FILE,
8181 },
8282 ]
benchcompress/src/benchcompress/algorithms/lzma/__init__.pymodified+8−7View file
@@ -15,9 +15,11 @@ def lzma_delta_encode(x: np.ndarray, preset: int) -> bytes:
1515 return compressed
1616
1717
18-def lzma_delta_decode(x: bytes, dtype: str) -> np.ndarray:
18+def lzma_delta_decode(x: bytes, dtype: str, shape: tuple) -> np.ndarray:
1919 import lzma
2020
21+ assert len(shape) == 1
22+
2123 buf = lzma.decompress(x)
2224 y = np.frombuffer(buf, dtype=dtype)
2325 return np.cumsum(y)
@@ -26,18 +28,17 @@ def lzma_delta_decode(x: bytes, dtype: str) -> np.ndarray:
2628 def lzma_encode(x: np.ndarray, preset: int) -> bytes:
2729 import lzma
2830
29- assert x.ndim == 1
3031 buf = x.tobytes()
3132 compressed = lzma.compress(buf, preset=preset)
3233 return compressed
3334
3435
35-def lzma_decode(x: bytes, dtype: str) -> np.ndarray:
36+def lzma_decode(x: bytes, dtype: str, shape: tuple) -> np.ndarray:
3637 import lzma
3738
3839 buf = lzma.decompress(x)
3940 y = np.frombuffer(buf, dtype=dtype)
40- return y
41+ return y.reshape(shape)
4142
4243
4344 algorithms = [
@@ -45,7 +46,7 @@ algorithms = [
4546 "name": "lzma-9",
4647 "version": "1",
4748 "encode": lambda x: lzma_encode(x, preset=9),
48- "decode": lambda x, dtype: lzma_decode(x, dtype),
49+ "decode": lambda x, dtype, shape: lzma_decode(x, dtype, shape),
4950 "description": "LZMA compression at maximum preset 9 for highest compression ratio.",
5051 "tags": ["lzma"],
5152 "source_file": SOURCE_FILE,
@@ -54,9 +55,9 @@ algorithms = [
5455 "name": "lzma-9-delta",
5556 "version": "1",
5657 "encode": lambda x: lzma_delta_encode(x, preset=9),
57- "decode": lambda x, dtype: lzma_delta_decode(x, dtype),
58+ "decode": lambda x, dtype, shape: lzma_delta_decode(x, dtype, shape),
5859 "description": "LZMA compression at preset 9 with delta encoding for improved compression of sequential data.",
59- "tags": ["lzma", "delta_encoding"],
60+ "tags": ["lzma", "delta_encoding", "1d"],
6061 "source_file": SOURCE_FILE,
6162 },
6263 ]
benchcompress/src/benchcompress/algorithms/zlib/__init__.pymodified+12−11View file
@@ -7,18 +7,17 @@ SOURCE_FILE = "zlib/__init__.py"
77 def zlib_encode(x: np.ndarray, level: int) -> bytes:
88 import zlib
99
10- assert x.ndim == 1
1110 buf = x.tobytes()
1211 compressed = zlib.compress(buf, level=level)
1312 return compressed
1413
1514
16-def zlib_decode(x: bytes, dtype: str) -> np.ndarray:
15+def zlib_decode(x: bytes, dtype: str, shape: tuple) -> np.ndarray:
1716 import zlib
1817
1918 buf = zlib.decompress(x)
2019 y = np.frombuffer(buf, dtype=dtype)
21- return y
20+ return y.reshape(shape)
2221
2322
2423 def zlib_delta_encode(x: np.ndarray, level: int) -> bytes:
@@ -32,9 +31,11 @@ def zlib_delta_encode(x: np.ndarray, level: int) -> bytes:
3231 return compressed
3332
3433
35-def zlib_delta_decode(x: bytes, dtype: str) -> np.ndarray:
34+def zlib_delta_decode(x: bytes, dtype: str, shape: tuple) -> np.ndarray:
3635 import zlib
3736
37+ assert len(shape) == 1
38+
3839 buf = zlib.decompress(x)
3940 y = np.frombuffer(buf, dtype=dtype)
4041 return np.cumsum(y)
@@ -45,7 +46,7 @@ algorithms = [
4546 "name": "zlib-1",
4647 "version": "1",
4748 "encode": lambda x: zlib_encode(x, level=1),
48- "decode": lambda x, dtype: zlib_decode(x, dtype),
49+ "decode": lambda x, dtype, shape: zlib_decode(x, dtype, shape),
4950 "description": "Zlib DEFLATE compression at level 1 (fastest).",
5051 "tags": ["zlib"],
5152 "source_file": SOURCE_FILE,
@@ -54,7 +55,7 @@ algorithms = [
5455 "name": "zlib-3",
5556 "version": "1",
5657 "encode": lambda x: zlib_encode(x, level=3),
57- "decode": lambda x, dtype: zlib_decode(x, dtype),
58+ "decode": lambda x, dtype, shape: zlib_decode(x, dtype, shape),
5859 "description": "Zlib DEFLATE compression at level 3.",
5960 "tags": ["zlib"],
6061 "source_file": SOURCE_FILE,
@@ -63,7 +64,7 @@ algorithms = [
6364 "name": "zlib-5",
6465 "version": "1",
6566 "encode": lambda x: zlib_encode(x, level=5),
66- "decode": lambda x, dtype: zlib_decode(x, dtype),
67+ "decode": lambda x, dtype, shape: zlib_decode(x, dtype, shape),
6768 "description": "Zlib DEFLATE compression at level 5 (medium).",
6869 "tags": ["zlib"],
6970 "source_file": SOURCE_FILE,
@@ -72,7 +73,7 @@ algorithms = [
7273 "name": "zlib-7",
7374 "version": "1",
7475 "encode": lambda x: zlib_encode(x, level=7),
75- "decode": lambda x, dtype: zlib_decode(x, dtype),
76+ "decode": lambda x, dtype, shape: zlib_decode(x, dtype, shape),
7677 "description": "Zlib DEFLATE compression at level 7.",
7778 "tags": ["zlib"],
7879 "source_file": SOURCE_FILE,
@@ -81,7 +82,7 @@ algorithms = [
8182 "name": "zlib-9",
8283 "version": "1",
8384 "encode": lambda x: zlib_encode(x, level=9),
84- "decode": lambda x, dtype: zlib_decode(x, dtype),
85+ "decode": lambda x, dtype, shape: zlib_decode(x, dtype, shape),
8586 "description": "Zlib DEFLATE compression at maximum level 9.",
8687 "tags": ["zlib"],
8788 "source_file": SOURCE_FILE,
@@ -90,9 +91,9 @@ algorithms = [
9091 "name": "zlib-9-delta",
9192 "version": "1",
9293 "encode": lambda x: zlib_delta_encode(x, level=9),
93- "decode": lambda x, dtype: zlib_delta_decode(x, dtype),
94+ "decode": lambda x, dtype, shape: zlib_delta_decode(x, dtype, shape),
9495 "description": "Zlib DEFLATE compression at level 9 with delta encoding.",
95- "tags": ["zlib", "delta_encoding"],
96+ "tags": ["zlib", "delta_encoding", "1d"],
9697 "source_file": SOURCE_FILE,
9798 },
9899 ]
benchcompress/src/benchcompress/algorithms/zstd/__init__.pymodified+25−19View file
@@ -11,6 +11,7 @@ def zstd_delta_encode(x: np.ndarray, level: int) -> bytes:
1111 import zstandard as zstd
1212
1313 assert x.ndim == 1
14+
1415 y = np.diff(x)
1516 y = np.insert(y, 0, x[0])
1617 buf = y.tobytes()
@@ -19,9 +20,11 @@ def zstd_delta_encode(x: np.ndarray, level: int) -> bytes:
1920 return compressed
2021
2122
22-def zstd_delta_decode(x: bytes, dtype: str) -> np.ndarray:
23+def zstd_delta_decode(x: bytes, dtype: str, shape: tuple) -> np.ndarray:
2324 import zstandard as zstd
2425
26+ assert len(shape) == 1
27+
2528 decompressor = zstd.ZstdDecompressor()
2629 buf = decompressor.decompress(x)
2730 y = np.frombuffer(buf, dtype=dtype)
@@ -31,20 +34,19 @@ def zstd_delta_decode(x: bytes, dtype: str) -> np.ndarray:
3134 def zstd_encode(x: np.ndarray, level: int) -> bytes:
3235 import zstandard as zstd
3336
34- assert x.ndim == 1
3537 buf = x.tobytes()
3638 compressor = zstd.ZstdCompressor(level=level)
3739 compressed = compressor.compress(buf)
3840 return compressed
3941
4042
41-def zstd_decode(x: bytes, dtype: str) -> np.ndarray:
43+def zstd_decode(x: bytes, dtype: str, shape: tuple) -> np.ndarray:
4244 import zstandard as zstd
4345
4446 decompressor = zstd.ZstdDecompressor()
4547 buf = decompressor.decompress(x)
4648 y = np.frombuffer(buf, dtype=dtype)
47- return y
49+ return y.reshape(shape)
4850
4951
5052 def zstd_markov_encode(x: np.ndarray, level: int) -> bytes:
@@ -70,10 +72,12 @@ def zstd_markov_encode(x: np.ndarray, level: int) -> bytes:
7072 return header + coeffs_bytes + initial_bytes + compressed_resid
7173
7274
73-def zstd_markov_decode(x: bytes, dtype: str) -> np.ndarray:
75+def zstd_markov_decode(x: bytes, dtype: str, shape: tuple) -> np.ndarray:
7476 import zstandard as zstd
7577 import struct
7678
79+ assert len(shape) == 1
80+
7781 # Extract header
7882 header_size = struct.calcsize("QQ")
7983 coeffs_len, initial_len = struct.unpack("QQ", x[:header_size])
@@ -159,10 +163,12 @@ def zstd_markov_zrle_encode(x: np.ndarray, level: int) -> bytes:
159163 return header + coeffs_bytes + initial_bytes + run_lengths_bytes + compressed_resid
160164
161165
162-def zstd_markov_zrle_decode(x: bytes, dtype: str) -> np.ndarray:
166+def zstd_markov_zrle_decode(x: bytes, dtype: str, shape: tuple) -> np.ndarray:
163167 import zstandard as zstd
164168 import struct
165169
170+ assert len(shape) == 1
171+
166172 # Extract header
167173 header_size = struct.calcsize("QQQQB")
168174 coeffs_len, initial_len, run_lengths_len, num_run_lengths, run_length_dtype_code = (
@@ -220,7 +226,7 @@ algorithms = [
220226 "name": "zstd-4",
221227 "version": "1",
222228 "encode": lambda x: zstd_encode(x, level=4),
223- "decode": lambda x, dtype: zstd_decode(x, dtype),
229+ "decode": lambda x, dtype, shape: zstd_decode(x, dtype, shape),
224230 "description": "Zstandard compression at level 4 (fast compression).",
225231 "tags": ["zstd"],
226232 "source_file": SOURCE_FILE,
@@ -229,7 +235,7 @@ algorithms = [
229235 "name": "zstd-7",
230236 "version": "1",
231237 "encode": lambda x: zstd_encode(x, level=7),
232- "decode": lambda x, dtype: zstd_decode(x, dtype),
238+ "decode": lambda x, dtype, shape: zstd_decode(x, dtype, shape),
233239 "description": "Zstandard compression at level 7 (balanced speed/compression).",
234240 "tags": ["zstd"],
235241 "source_file": SOURCE_FILE,
@@ -238,7 +244,7 @@ algorithms = [
238244 "name": "zstd-10",
239245 "version": "1",
240246 "encode": lambda x: zstd_encode(x, level=10),
241- "decode": lambda x, dtype: zstd_decode(x, dtype),
247+ "decode": lambda x, dtype, shape: zstd_decode(x, dtype, shape),
242248 "description": "Zstandard compression at level 10 (better compression).",
243249 "tags": ["zstd"],
244250 "source_file": SOURCE_FILE,
@@ -247,7 +253,7 @@ algorithms = [
247253 "name": "zstd-13",
248254 "version": "1",
249255 "encode": lambda x: zstd_encode(x, level=13),
250- "decode": lambda x, dtype: zstd_decode(x, dtype),
256+ "decode": lambda x, dtype, shape: zstd_decode(x, dtype, shape),
251257 "description": "Zstandard compression at level 13 (high compression).",
252258 "tags": ["zstd"],
253259 "source_file": SOURCE_FILE,
@@ -256,7 +262,7 @@ algorithms = [
256262 "name": "zstd-16",
257263 "version": "1",
258264 "encode": lambda x: zstd_encode(x, level=16),
259- "decode": lambda x, dtype: zstd_decode(x, dtype),
265+ "decode": lambda x, dtype, shape: zstd_decode(x, dtype, shape),
260266 "description": "Zstandard compression at level 16 (very high compression).",
261267 "tags": ["zstd"],
262268 "source_file": SOURCE_FILE,
@@ -265,7 +271,7 @@ algorithms = [
265271 "name": "zstd-19",
266272 "version": "1",
267273 "encode": lambda x: zstd_encode(x, level=19),
268- "decode": lambda x, dtype: zstd_decode(x, dtype),
274+ "decode": lambda x, dtype, shape: zstd_decode(x, dtype, shape),
269275 "description": "Zstandard compression at level 19 (ultra high compression).",
270276 "tags": ["zstd"],
271277 "source_file": SOURCE_FILE,
@@ -274,7 +280,7 @@ algorithms = [
274280 "name": "zstd-22",
275281 "version": "1",
276282 "encode": lambda x: zstd_encode(x, level=22),
277- "decode": lambda x, dtype: zstd_decode(x, dtype),
283+ "decode": lambda x, dtype, shape: zstd_decode(x, dtype, shape),
278284 "description": "Zstandard compression at maximum level 22 (highest compression).",
279285 "tags": ["zstd"],
280286 "source_file": SOURCE_FILE,
@@ -283,27 +289,27 @@ algorithms = [
283289 "name": "zstd-22-delta",
284290 "version": "1",
285291 "encode": lambda x: zstd_delta_encode(x, level=22),
286- "decode": lambda x, dtype: zstd_delta_decode(x, dtype),
292+ "decode": lambda x, dtype, shape: zstd_delta_decode(x, dtype, shape),
287293 "description": "Zstandard compression at level 22 with delta encoding for improved compression of sequential data.",
288- "tags": ["zstd", "delta_encoding"],
294+ "tags": ["zstd", "delta_encoding", "1d"],
289295 "source_file": SOURCE_FILE,
290296 },
291297 {
292298 "name": "zstd-22-markov",
293299 "version": "1",
294300 "encode": lambda x: zstd_markov_encode(x, level=22),
295- "decode": lambda x, dtype: zstd_markov_decode(x, dtype),
301+ "decode": lambda x, dtype, shape: zstd_markov_decode(x, dtype, shape),
296302 "description": "Zstandard compression at level 22 with Markov prediction for exploiting temporal correlations in the data.",
297- "tags": ["zstd", "markov_prediction"],
303+ "tags": ["zstd", "markov_prediction", "1d"],
298304 "source_file": SOURCE_FILE,
299305 },
300306 {
301307 "name": "zstd-22-markov-zrle",
302308 "version": "1",
303309 "encode": lambda x: zstd_markov_zrle_encode(x, level=22),
304- "decode": lambda x, dtype: zstd_markov_zrle_decode(x, dtype),
310+ "decode": lambda x, dtype, shape: zstd_markov_zrle_decode(x, dtype, shape),
305311 "description": "Zstandard compression at level 22 with Markov prediction and zero run-length encoding for sparse data.",
306- "tags": ["zstd", "markov_prediction", "zero_rle"],
312+ "tags": ["zstd", "markov_prediction", "zero_rle", "1d"],
307313 "source_file": SOURCE_FILE,
308314 },
309315 ]
benchcompress/src/benchcompress/cli.pymodified+1−1View file
@@ -59,7 +59,7 @@ def validate_datasets(ctx, param, value):
5959
6060 @click.group()
6161 def cli():
62- """Benchmark compression algorithms for numeric timeseries data"""
62+ """Benchmark compression algorithms for scientific data arrays"""
6363 pass
6464
6565
benchcompress/src/benchcompress/datasets/__init__.pymodified+7−2View file
@@ -1,5 +1,10 @@
11 from .bernoulli import datasets as bernoulli_datasets
22 from .gaussian import datasets as gaussian_datasets
3-from .real import datasets as real_datasets
3+from .electrophysiology import datasets as real_datasets
4+from .seismic import datasets as seismic_datasets
45
5-datasets = bernoulli_datasets + gaussian_datasets + real_datasets
6+datasets_list = [bernoulli_datasets, gaussian_datasets, real_datasets, seismic_datasets]
7+
8+datasets = []
9+for d in datasets_list:
10+ datasets.extend(d)
benchcompress/src/benchcompress/datasets/bernoulli/__init__.pymodified+1−1View file
@@ -10,7 +10,7 @@ def create_bernoulli(*, n_samples: int, p: float, seed: int) -> np.ndarray:
1010 return x
1111
1212
13-tags = ["binary", "integer", "discrete", "synthetic", "i.i.d."]
13+tags = ["bernoulli", "timeseries", "1d", "integer", "discrete", "synthetic", "i.i.d."]
1414
1515 datasets = [
1616 {
benchcompress/src/benchcompress/datasets/real/__init__.py →benchcompress/src/benchcompress/datasets/electrophysiology/__init__.pyrenamed+11−11View file
@@ -5,9 +5,9 @@ from ..._filters import bandpass_filter
55 from ..._analysis import estimate_noise_level
66
77
8-SOURCE_FILE = "real/__init__.py"
8+SOURCE_FILE = "electrophysiology/__init__.py"
99
10-tags = ["integer", "continuous", "neurophysiology", "real"]
10+tags = ["real", "electrophysiology", "timeseries", "1d", "integer", "continuous"]
1111
1212
1313 def _load_real_000876(
@@ -170,7 +170,7 @@ def _create_sparse_version(X: np.ndarray) -> np.ndarray:
170170
171171 datasets = [
172172 {
173- "name": "real-000876-ch45",
173+ "name": "ephys-000876-ch45",
174174 "version": "1",
175175 "description": "Raw extracellular electrophysiology recording from DANDI:000876.",
176176 "create": lambda: _load_real_000876(
@@ -180,7 +180,7 @@ datasets = [
180180 "source_file": SOURCE_FILE,
181181 },
182182 {
183- "name": "real-000409-ch101",
183+ "name": "ephys-000409-ch101",
184184 "version": "1",
185185 "description": "Raw extracellular electrophysiology recording from DANDI:000409.",
186186 "create": lambda: _load_real_000409(
@@ -190,7 +190,7 @@ datasets = [
190190 "source_file": SOURCE_FILE,
191191 },
192192 {
193- "name": "real-001290-ch0",
193+ "name": "ephys-001290-ch0",
194194 "version": "1",
195195 "description": "Raw extracellular electrophysiology recording from DANDI:001290.",
196196 "create": lambda: _load_real_001290(
@@ -200,7 +200,7 @@ datasets = [
200200 "source_file": SOURCE_FILE,
201201 },
202202 {
203- "name": "real-000876-ch45-filtered",
203+ "name": "ephys-000876-ch45-filtered",
204204 "version": "1",
205205 "description": "Preprocessed version of real-000876-ch45. Bandpass filtered (300-6000 Hz).",
206206 "create": lambda: _create_filtered_version(
@@ -212,7 +212,7 @@ datasets = [
212212 "source_file": SOURCE_FILE,
213213 },
214214 {
215- "name": "real-000409-ch101-filtered",
215+ "name": "ephys-000409-ch101-filtered",
216216 "version": "1",
217217 "description": "Preprocessed version of real-000409-ch101. Bandpass filtered (300-6000 Hz).",
218218 "create": lambda: _create_filtered_version(
@@ -224,7 +224,7 @@ datasets = [
224224 "source_file": SOURCE_FILE,
225225 },
226226 {
227- "name": "real-001290-ch0-filtered",
227+ "name": "ephys-001290-ch0-filtered",
228228 "version": "1",
229229 "description": "Preprocessed version of real-001290-ch0. Bandpass filtered (300-6000 Hz).",
230230 "create": lambda: _create_filtered_version(
@@ -236,7 +236,7 @@ datasets = [
236236 "source_file": SOURCE_FILE,
237237 },
238238 {
239- "name": "real-000876-ch45-sparse",
239+ "name": "ephys-000876-ch45-sparse",
240240 "version": "1",
241241 "description": "Sparse version of real-000876-ch45. Activity-based suppression applied.",
242242 "create": lambda: _create_sparse_version(
@@ -248,7 +248,7 @@ datasets = [
248248 "source_file": SOURCE_FILE,
249249 },
250250 {
251- "name": "real-000409-ch101-sparse",
251+ "name": "ephys-000409-ch101-sparse",
252252 "version": "1",
253253 "description": "Sparse version of real-000409-ch101. Activity-based suppression applied.",
254254 "create": lambda: _create_sparse_version(
@@ -260,7 +260,7 @@ datasets = [
260260 "source_file": SOURCE_FILE,
261261 },
262262 {
263- "name": "real-001290-ch0-sparse",
263+ "name": "ephys-001290-ch0-sparse",
264264 "version": "1",
265265 "description": "Sparse version of real-001290-ch0. Activity-based suppression applied.",
266266 "create": lambda: _create_sparse_version(
benchcompress/src/benchcompress/datasets/gaussian/__init__.pymodified+52−17View file
@@ -4,53 +4,88 @@ import numpy as np
44 SOURCE_FILE = "gaussian/__init__.py"
55
66
7-def create_gaussian(*, n_samples: int, stddev: float, seed: int) -> np.ndarray:
7+def create_gaussian_quantized(
8+ *, n_samples: int, stddev: float, seed: int
9+) -> np.ndarray:
810 rng = np.random.default_rng(seed)
911 x = np.round(rng.normal(0, stddev, n_samples)).astype(np.int16)
1012 return x
1113
1214
13-tags = ["integer", "discrete", "synthetic", "i.i.d."]
15+def create_gaussian_float(*, n_samples: int, stddev: float, seed: int) -> np.ndarray:
16+ rng = np.random.default_rng(seed)
17+ x = rng.normal(0, stddev, n_samples).astype(np.float32)
18+ return x
19+
20+
21+tags_quantized = [
22+ "gaussian",
23+ "integer",
24+ "discrete",
25+ "timeseries",
26+ "1d",
27+ "synthetic",
28+ "i.i.d.",
29+]
30+tags_float = ["gaussian", "float", "timeseries", "1d", "synthetic", "i.i.d."]
1431
1532 datasets = [
1633 {
17- "name": "gaussian-1",
34+ "name": "gaussian-q1",
1835 "version": "1",
19- "create": lambda: create_gaussian(n_samples=1_000_000, stddev=1, seed=0),
36+ "create": lambda: create_gaussian_quantized(
37+ n_samples=1_000_000, stddev=1, seed=0
38+ ),
2039 "description": "Rounded Gaussian integers with σ=1.",
21- "tags": tags,
40+ "tags": tags_quantized,
2241 "source_file": SOURCE_FILE,
2342 },
2443 {
25- "name": "gaussian-2",
44+ "name": "gaussian-q2",
2645 "version": "1",
27- "create": lambda: create_gaussian(n_samples=1_000_000, stddev=2, seed=0),
46+ "create": lambda: create_gaussian_quantized(
47+ n_samples=1_000_000, stddev=2, seed=0
48+ ),
2849 "description": "Rounded Gaussian integers with σ=2.",
29- "tags": tags,
50+ "tags": tags_quantized,
3051 "source_file": SOURCE_FILE,
3152 },
3253 {
33- "name": "gaussian-3",
54+ "name": "gaussian-q3",
3455 "version": "1",
35- "create": lambda: create_gaussian(n_samples=1_000_000, stddev=3, seed=0),
56+ "create": lambda: create_gaussian_quantized(
57+ n_samples=1_000_000, stddev=3, seed=0
58+ ),
3659 "description": "Rounded Gaussian integers with σ=3.",
37- "tags": tags,
60+ "tags": tags_quantized,
3861 "source_file": SOURCE_FILE,
3962 },
4063 {
41- "name": "gaussian-5",
64+ "name": "gaussian-q5",
4265 "version": "1",
43- "create": lambda: create_gaussian(n_samples=1_000_000, stddev=5, seed=0),
66+ "create": lambda: create_gaussian_quantized(
67+ n_samples=1_000_000, stddev=5, seed=0
68+ ),
4469 "description": "Rounded Gaussian integers with σ=5.",
45- "tags": tags,
70+ "tags": tags_quantized,
4671 "source_file": SOURCE_FILE,
4772 },
4873 {
49- "name": "gaussian-8",
74+ "name": "gaussian-q8",
5075 "version": "1",
51- "create": lambda: create_gaussian(n_samples=1_000_000, stddev=8, seed=0),
76+ "create": lambda: create_gaussian_quantized(
77+ n_samples=1_000_000, stddev=8, seed=0
78+ ),
5279 "description": "Rounded Gaussian integers with σ=8.",
53- "tags": tags,
80+ "tags": tags_quantized,
81+ "source_file": SOURCE_FILE,
82+ },
83+ {
84+ "name": "gaussian-flt1",
85+ "version": "1",
86+ "create": lambda: create_gaussian_float(n_samples=1_000_000, stddev=8, seed=0),
87+ "description": "Floating point Gaussian numbers with σ=1.",
88+ "tags": tags_float,
5489 "source_file": SOURCE_FILE,
5590 },
5691 ]
benchcompress/src/benchcompress/datasets/seismic/__init__.pyadded+57−0View file
@@ -0,0 +1,57 @@
1+import numpy as np
2+import segyio
3+import os
4+import requests
5+
6+SOURCE_FILE = "seismic/__init__.py"
7+
8+tags = ["real", "seismic", "float", "continuous", "timeseries", "2d"]
9+
10+
11+def _load_seismic_data() -> np.ndarray:
12+ """Load seismic data from the SEG-Y file.
13+
14+ Returns:
15+ Array containing the loaded seismic data
16+ """
17+ file_path = "04A+04B.segy"
18+ if not os.path.exists(file_path):
19+ # Download the SEG-Y file
20+ url = "https://zenodo.org/records/8152964/files/04A+04B.segy?download=1"
21+ response = requests.get(url)
22+ with open(file_path, "wb") as f:
23+ f.write(response.content)
24+ print(f"Downloaded {file_path}")
25+ else:
26+ print(f"{file_path} already exists locally.")
27+
28+ # Open the SEG-Y file
29+ with segyio.open(file_path, "r", ignore_geometry=True) as f:
30+ # Read the seismic data
31+ data = f.trace.raw[:]
32+
33+ # Consider only the first 3000 traces, because the others have a bunch of zeros
34+ X = data[:3000]
35+ # The first part of each trace is zeros
36+ first_nonzero_indices = []
37+ for j in range(X.shape[0]):
38+ inds = np.where(X[j] != 0)[0]
39+ first_nonzero_indices.append(inds[0] if len(inds) > 0 else -1)
40+ # plt.figure(figsize=(10, 5))
41+ # plt.hist(first_nonzero_indices, bins=20)
42+ # print(np.max(first_nonzero_indices)) # 1607
43+ X = X[:, 1700:]
44+
45+ return X
46+
47+
48+datasets = [
49+ {
50+ "name": "seismic-04A-04B",
51+ "version": "1",
52+ "description": "Seismic data from Roger Revelle voyage RR1508.",
53+ "create": lambda: _load_seismic_data(),
54+ "tags": tags,
55+ "source_file": SOURCE_FILE,
56+ }
57+]
benchcompress/src/benchcompress/datasets/seismic/explore_seismic.pyadded+61−0View file
@@ -0,0 +1,61 @@
1+# %%
2+import numpy as np
3+import matplotlib.pyplot as plt
4+import os
5+import requests
6+import segyio
7+
8+file_path = "04A+04B.segy"
9+if not os.path.exists(file_path):
10+ # Download the SEG-Y file
11+ url = "https://zenodo.org/records/8152964/files/04A+04B.segy?download=1"
12+ response = requests.get(url)
13+ with open(file_path, "wb") as f:
14+ f.write(response.content)
15+ print(f"Downloaded {file_path}")
16+else:
17+ print(f"{file_path} already exists locally.")
18+
19+# Open the SEG-Y file
20+with segyio.open(file_path, "r", ignore_geometry=True) as f:
21+ # Read the seismic data
22+ data = f.trace.raw[:]
23+# %%
24+print(data.shape) # (4101, 3751)
25+print(data.dtype) # float32
26+# %%
27+# Consider only the first 3000 traces, because the others have a bunch of zeros
28+X = data[:3000]
29+# The first part of each trace is zeros
30+first_nonzero_indices = []
31+for j in range(X.shape[0]):
32+ inds = np.where(X[j] != 0)[0]
33+ first_nonzero_indices.append(inds[0] if len(inds) > 0 else -1)
34+# plt.figure(figsize=(10, 5))
35+# plt.hist(first_nonzero_indices, bins=20)
36+print(np.max(first_nonzero_indices)) # 1607
37+X = X[:, 1700:]
38+# %%
39+print(X.shape) # (4101, 2051)
40+print(X[0, :5]) # [-2891.2651 -2550.8093 -2744.9373 -111.93254 6441.383 ]
41+# %%
42+plt.figure(figsize=(10, 5))
43+plt.plot(X[0])
44+plt.figure(figsize=(10, 5))
45+plt.plot(X[1])
46+plt.figure(figsize=(10, 5))
47+plt.plot(X[1000])
48+plt.figure(figsize=(10, 5))
49+plt.plot(X[1001])
50+plt.figure(figsize=(10, 5))
51+plt.plot(X[2000])
52+plt.figure(figsize=(10, 5))
53+plt.plot(X[-1])
54+# %%
55+plt.figure(figsize=(10, 5))
56+plt.plot(X[:, 0])
57+plt.figure(figsize=(10, 5))
58+plt.plot(X[:, 1])
59+plt.figure(figsize=(10, 5))
60+plt.plot(X[:, 1000])
61+# %%
benchcompress/src/benchcompress/run_benchmarks.pymodified+75−83View file
@@ -15,7 +15,7 @@ from ._memobin import (
1515 )
1616
1717
18-system_version = "v5"
18+system_version = "v6"
1919 GITHUB_ALGORITHMS_PREFIX = "https://github.com/magland/benchcompress/blob/main/benchcompress/src/benchcompress/algorithms/"
2020 GITHUB_DATASETS_PREFIX = "https://github.com/magland/benchcompress/blob/main/benchcompress/src/benchcompress/datasets/"
2121
@@ -81,11 +81,6 @@ def run_benchmarks(
8181 dataset_tags = dataset.get("tags", [])
8282 print(f"\n*** Dataset: {dataset['name']} (tags: {dataset_tags}) ***")
8383
84- # data will only be created if needed
85- data = None
86- original_size = None
87- dtype = None
88-
8984 for algorithm in algorithms_to_run:
9085 alg_name = algorithm["name"]
9186 alg_tags = algorithm.get("tags", [])
@@ -158,85 +153,81 @@ def run_benchmarks(
158153 continue
159154
160155 print(" Running new benchmark...")
161- if data is None:
162- # only create data if needed
163- data = dataset["create"]()
164- dtype = str(data.dtype)
165- original_size = len(data.tobytes())
166- print(f"Created dataset: shape={data.shape}, dtype={dtype}")
167- print(f"Original size: {original_size:,} bytes")
168-
169- # Upload dataset to memobin if enabled
170- memobin_api_key = os.environ.get("MEMOBIN_API_KEY")
171- upload_enabled = os.environ.get("UPLOAD_TO_MEMOBIN") == "1"
172- if memobin_api_key and upload_enabled:
173- try:
174- # Upload array metadata as JSON
175- dataset_url_json = construct_dataset_url(
176- dataset["name"], dataset["version"], "json"
177- )
178- if not exists_in_memobin(dataset_url_json):
179- if verbose:
180- print(" Uploading dataset metadata to memobin...")
181- metadata = {"dtype": str(data.dtype), "shape": data.shape}
182- upload_to_memobin(
183- metadata,
184- dataset_url_json,
185- memobin_api_key,
186- content_type="application/json",
187- )
188- if verbose:
189- print(" Successfully uploaded metadata")
190-
191- # Upload raw .dat format
192- dataset_url_raw = construct_dataset_url(
193- dataset["name"], dataset["version"], "dat"
156+ data = dataset["create"]()
157+ dtype = str(data.dtype)
158+ original_size = len(data.tobytes())
159+ print(f"Created dataset: shape={data.shape}, dtype={dtype}")
160+ print(f"Original size: {original_size:,} bytes")
161+
162+ # Upload dataset to memobin if enabled
163+ memobin_api_key = os.environ.get("MEMOBIN_API_KEY")
164+ upload_enabled = os.environ.get("UPLOAD_TO_MEMOBIN") == "1"
165+ if memobin_api_key and upload_enabled:
166+ try:
167+ # Upload array metadata as JSON
168+ dataset_url_json = construct_dataset_url(
169+ dataset["name"], dataset["version"], "json"
170+ )
171+ if not exists_in_memobin(dataset_url_json):
172+ if verbose:
173+ print(" Uploading dataset metadata to memobin...")
174+ metadata = {"dtype": str(data.dtype), "shape": data.shape}
175+ upload_to_memobin(
176+ metadata,
177+ dataset_url_json,
178+ memobin_api_key,
179+ content_type="application/json",
194180 )
195- if not exists_in_memobin(dataset_url_raw):
196- if verbose:
197- print(" Uploading dataset (raw) to memobin...")
198- upload_to_memobin(
199- data.tobytes(),
200- dataset_url_raw,
201- memobin_api_key,
202- content_type="application/octet-stream",
203- )
204- if verbose:
205- print(" Successfully uploaded raw dataset")
206-
207- # Upload .npy format
208- dataset_url_npy = construct_dataset_url(
209- dataset["name"], dataset["version"], "npy"
181+ if verbose:
182+ print(" Successfully uploaded metadata")
183+
184+ # Upload raw .dat format
185+ dataset_url_raw = construct_dataset_url(
186+ dataset["name"], dataset["version"], "dat"
187+ )
188+ if not exists_in_memobin(dataset_url_raw):
189+ if verbose:
190+ print(" Uploading dataset (raw) to memobin...")
191+ upload_to_memobin(
192+ data.tobytes(),
193+ dataset_url_raw,
194+ memobin_api_key,
195+ content_type="application/octet-stream",
210196 )
211- if not exists_in_memobin(dataset_url_npy):
212- if verbose:
213- print(" Uploading dataset (npy) to memobin...")
214- # Save array to a temporary .npy file
215- temp_npy = os.path.join(cache_dir, "temp.npy")
216- np.save(temp_npy, data)
217- with open(temp_npy, "rb") as f:
218- npy_bytes = f.read()
219- os.remove(temp_npy) # Clean up temp file
220-
221- upload_to_memobin(
222- npy_bytes,
223- dataset_url_npy,
224- memobin_api_key,
225- content_type="application/octet-stream",
226- )
227- if verbose:
228- print(" Successfully uploaded npy dataset")
229- except Exception as e:
230- print(
231- f" Warning: Failed to upload dataset to memobin: {str(e)}"
197+ if verbose:
198+ print(" Successfully uploaded raw dataset")
199+
200+ # Upload .npy format
201+ dataset_url_npy = construct_dataset_url(
202+ dataset["name"], dataset["version"], "npy"
203+ )
204+ if not exists_in_memobin(dataset_url_npy):
205+ if verbose:
206+ print(" Uploading dataset (npy) to memobin...")
207+ # Save array to a temporary .npy file
208+ temp_npy = os.path.join(cache_dir, "temp.npy")
209+ np.save(temp_npy, data)
210+ with open(temp_npy, "rb") as f:
211+ npy_bytes = f.read()
212+ os.remove(temp_npy) # Clean up temp file
213+
214+ upload_to_memobin(
215+ npy_bytes,
216+ dataset_url_npy,
217+ memobin_api_key,
218+ content_type="application/octet-stream",
232219 )
220+ if verbose:
221+ print(" Successfully uploaded npy dataset")
222+ except Exception as e:
223+ print(f" Warning: Failed to upload dataset to memobin: {str(e)}")
233224
234225 assert data is not None
235226 assert isinstance(data, np.ndarray)
236227 assert isinstance(original_size, int)
237228 assert isinstance(dtype, str)
238229
239- def run_timed_trials(operation, *args) -> Tuple[float, float]:
230+ def run_timed_trials(operation, *args) -> Tuple[float, float, Any]:
240231 """Run multiple trials of an operation until total time exceeds 1 second.
241232 Returns (median_time, mb_per_sec)"""
242233 assert data is not None
@@ -245,20 +236,22 @@ def run_benchmarks(
245236 total_time = 0
246237 array_size_mb = data.nbytes / (1024 * 1024) # Convert to MB
247238
239+ ret = None
248240 while total_time < 1.0:
249241 start_time = time.perf_counter()
250- _ = operation(*args) # Execute operation but discard result
242+ ret = operation(*args) # Execute operation but discard result
251243 trial_time = time.perf_counter() - start_time
252244 times.append(trial_time)
253245 total_time += trial_time
254246
255247 median_time = median(times)
256248 mb_per_sec = array_size_mb / median_time
257- return median_time, mb_per_sec
249+ return median_time, mb_per_sec, ret
258250
259251 # Measure encoding with multiple trials
260- encode_time, encode_mb_per_sec = run_timed_trials(algorithm["encode"], data)
261- encoded = algorithm["encode"](data) # One final encode to get the result
252+ encode_time, encode_mb_per_sec, encoded = run_timed_trials(
253+ algorithm["encode"], data
254+ )
262255 compressed_size = len(encoded)
263256 compression_ratio = original_size / compressed_size
264257 print(" Compression complete:")
@@ -269,10 +262,9 @@ def run_benchmarks(
269262
270263 print(" Verifying decompression...")
271264 # Measure decoding with multiple trials
272- decode_time, decode_mb_per_sec = run_timed_trials(
273- algorithm["decode"], encoded, dtype
265+ decode_time, decode_mb_per_sec, decoded = run_timed_trials(
266+ algorithm["decode"], encoded, dtype, data.shape
274267 )
275- decoded = algorithm["decode"](encoded, dtype) # One final decode to verify
276268 print(f" Decode time: {decode_time*1000:.2f}ms")
277269 print(f" Decode throughput: {decode_mb_per_sec:.2f} MB/s")
278270
web-ui/src/App.tsxmodified+1−2View file
@@ -107,8 +107,7 @@ function App() {
107107 <span className="app-header-subtitle">
108108 {" · "}
109109 <span style={{ fontSize: "1rem", color: "#777" }}>
110- Comparing compression algorithms for numeric time series
111- data
110+ Comparing compression algorithms for scientific data arrays
112111 </span>
113112 </span>
114113 </span>
web-ui/src/pages/About.tsxmodified+3−3View file
@@ -13,9 +13,9 @@ const About: React.FC = () => {
1313 style={{ fontSize: "1.1rem", lineHeight: "1.6", marginBottom: "2rem" }}
1414 >
1515 Benchcompress is a benchmarking framework for evaluating compression
16- algorithms on numeric timeseries datasets, with a focus on scientific
17- data. It measures compression ratios and performance metrics across
18- various algorithms and datasets.
16+ algorithms on data arrays, with a focus on scientific data. It measures
17+ compression ratios and performance metrics across various algorithms and
18+ datasets.
1919 </p>
2020
2121 <p>
web-ui/src/pages/paper.mdmodified+3−3View file
@@ -1,4 +1,4 @@
1-# Benchmarking Compression Algorithms for Numeric Scientific Data
1+# Benchmarking Compression Algorithms for Scientific Data Arrays
22
33 *Jeremy Magland, Center for Computational Mathematics, Flatiron Institute*
44
@@ -8,7 +8,7 @@
88
99 ## Abstract
1010
11-*Benchcompress* is a benchmarking framework designed to evaluate the performance of various compression algorithms on numeric timeseries datasets, with a particular focus on scientific data. The framework automates the benchmarking process, measuring compression ratio, encoding throughput, and decoding throughput for each algorithm-dataset pair. Results are verified through decompression and comparison with the original data, ensuring accuracy and reliability. The benchmark results are stored and visualized through an interactive web interface, allowing users to filter, sort, and explore the data. This paper presents the design, implementation, and preliminary results of Benchcompress, highlighting its utility in identifying optimal compression techniques for scientific datasets.
11+*Benchcompress* is a benchmarking framework designed to evaluate the performance of various compression algorithms on scientific data arrays. The framework automates the benchmarking process, measuring compression ratio, encoding throughput, and decoding throughput for each algorithm-dataset pair. Results are verified through decompression and comparison with the original data, ensuring accuracy and reliability. The benchmark results are stored and visualized through an interactive web interface, allowing users to filter, sort, and explore the data. This paper presents the design, implementation, and preliminary results of Benchcompress, highlighting its utility in identifying optimal compression techniques for scientific datasets.
1212
1313 ## Introduction
1414
@@ -37,7 +37,7 @@ H_{\text{Bernoulli, p=0.5}} = -0.5 \log_2 0.5 - 0.5 \log_2 0.5 = 1
3737 $$
3838 bits per sample. This means that the optimal compression ratio for such a dataset is 8, assuming the samples are stored as 8-bit integers. On the other hand, if $p\neq 0.5$, the entropy becomes lower and we can achieve compression at a rate of less than 1 bit per sample (e.g., for $p=0.1$, the entropy is around 0.47 bits per sample, so the compression ratio would be around 17).
3939
40-In practice, achieving this theoretical compression ratio requires sophisticated encoding techniques. Arithmetic encoding [ref] is one such method, but it is challenging to implement and can be computationally inefficient. A more modern and efficient alternative is Asymmetric Numeric Systems (ANS) [ref], which closely approaches the theoretical limit and is incorporated into state-of-the-art compressors such as ZStandard [ref]. However, these algorithms are primarily optimized for structured data types, such as text, rather than for scientific numeric data.
40+In practice, achieving this theoretical compression ratio requires sophisticated encoding techniques. Arithmetic encoding [ref] is one such method, but it is challenging to implement and can be computationally inefficient. A more modern and efficient alternative is Asymmetric Numeric Systems (ANS) [ref], which closely approaches the theoretical limit and is incorporated into state-of-the-art compressors such as ZStandard [ref]. However, these algorithms are primarily optimized for structured data types, such as text, rather than for numeric scientific data.
4141
4242 In our benchmarks, we evaluate a simple implementation of ANS using a Python package we developed, called `simple_ans`. As anticipated, ANS demonstrates superior performance when compressing i.i.d. samples from a discrete distribution. However, its efficiency diminishes when handling more structured data, such as continuous signals (e.g., voltage traces in electrophysiology).
4343