gridpack 0.5.1__tar.gz → 0.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gridpack
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: Lossless compression for gridded scientific data (ERA5 and friends)
5
5
  Author: gridpack contributors
6
6
  License: MIT
@@ -32,313 +32,6 @@ Requires-Dist: xarray>=2023.1; extra == "all"
32
32
  Requires-Dist: h5netcdf>=1.0; extra == "all"
33
33
  Dynamic: license-file
34
34
 
35
- # gridpack
36
-
37
- **Lossless compression for gridded scientific data (ERA5 and friends).**
38
-
39
- `gridpack` shrinks NetCDF/HDF5 gridded fields — `(time × lat × lon)` float arrays — without
40
- changing a single bit. `decode(encode(x)) == x` holds byte-for-byte, including `-0.0`,
41
- `NaN` and `±Inf`. It also exposes an array-level codec so other projects can use it as a
42
- drop-in compression layer.
43
-
44
- On real ERA5 10 m wind fields it reaches **10.4× vs the raw float bytes** while staying
45
- bit-exact, and stores every block with a SHA-256 so silent corruption is impossible.
46
-
47
- ---
48
-
49
- ## Why it works
50
-
51
- Most gridded products are not arbitrary floats. ERA5 fields, for example, sit on an exact
52
- binary lattice: 10 m wind is quantised to `2**-4` (0.0625 m/s), total precipitation to
53
- `2**-14`, 2 m temperature to `2**-4`. `gridpack` detects that lattice **exactly** and
54
- rewrites the field as integers `k = v / 2**E`. Small integers compress far better than
55
- 32-bit float bit patterns.
56
-
57
- Detection is not a heuristic. For a non-zero float32 value `v = sig · 2**(exp-150)` with a
58
- 24-bit significand `sig`, `v` is a multiple of `2**E` iff `tz(sig) + exp - 150 >= E`, so the
59
- largest usable exponent is `E_max = min(tz(sig) + exp - 150)`. The code verifies the
60
- rewrite is exact before committing to it, and falls back to an order-preserving bit map
61
- otherwise.
62
-
63
- | Stage | What it does | Why |
64
- |---|---|---|
65
- | 1. Representation | exact lattice detection → integers, else order-preserving bit map | shrinks the alphabet before any modelling |
66
- | 2. Prediction | 1st-order differencing along time (`lag`), latitude and longitude (separable 3-D Lorenzo) | turns smooth fields into small residuals |
67
- | 3. Zig-zag | signed residual → unsigned small integer | clears the high byte planes |
68
- | 4. Byte-plane split | de-interleave bytes into planes | gives the entropy coder near-stationary input |
69
- | 5. Entropy coding | `zstd` (default), `zlib`, `rc0`/`rc1` adaptive range coder, `huff` | last few percent |
70
-
71
- `-0.0` cannot be represented as an integer, so its positions are stored separately as a
72
- sparse bitmask — that is what keeps the lattice path bit-exact.
73
-
74
- ---
75
-
76
- ## Install
77
-
78
- ```bash
79
- pip install gridpack # core: numpy + h5py (zstandard optional, recommended)
80
- pip install "gridpack[xarray]" # + xarray backend support
81
- ```
82
-
83
- Or from source:
84
-
85
- ```bash
86
- git clone https://github.com/showkeyjar/gridpack && cd gridpack
87
- pip install -e .
88
- ```
89
-
90
- Installing the package also registers the xarray backend entry point, so
91
- `engine="gpz"` works immediately -- see [xarray](#xarray).
92
-
93
- ---
94
-
95
- ## Command line
96
-
97
- ```bash
98
- python -m gridpack compress in.nc out.gpz [options]
99
- python -m gridpack decompress in.gpz out.nc
100
- python -m gridpack verify in.nc out.gpz # byte-for-byte comparison
101
- python -m gridpack info out.gpz
102
- python -m gridpack bench in.nc --nt 48
103
- ```
104
-
105
- Example:
106
-
107
- ```bash
108
- python -m gridpack compress era5_10u.nc era5_10u.gpz --level 12 --entropy zstd --mode auto
109
- python -m gridpack verify era5_10u.nc era5_10u.gpz
110
- # EXACT
111
- ```
112
-
113
- ---
114
-
115
- ## Parameters
116
-
117
- ### `compress`
118
-
119
- | Parameter | Default | Meaning |
120
- |---|---|---|
121
- | `--level` | `12` | Entropy-coder level. zstd accepts 1–22, zlib 1–9. Level 12 is a good speed/size point; 19 is ~3 % smaller but ~18× slower. |
122
- | `--entropy` | `zstd` | Backend: `zstd`, `zlib`, `rc0`, `rc1`, `huff`. `rc*`/`huff` are pure-Python research backends — best ratio, far slower; use them on small blocks only. |
123
- | `--mode` | `auto` | Time model: `none` (byte planes only), `spatial` (lat+lon differencing), `dt` (time Δt + spatial), `lagN` (time ΔN + spatial), `auto` (tries the candidates per variable and keeps the smallest). |
124
- | `--chunk-t` | `16` | Inner time chunk for the codec. `0` disables internal chunking. |
125
- | `--file-chunk` | `0` | Time chunk used when slicing the file. `0` picks a size targeting ~96 MB per block so the time chain is not cut unnecessarily. |
126
- | `--threads` | `4` | zstd worker threads. |
127
- | `--quant-bits` | `0` | `0` = strict lossless. `>0` drops the low `quant-bits` of the ordered integer — **lossy**, opt-in only. |
128
-
129
- `--mode auto` is per-variable: the codec probes the first block with every candidate and
130
- locks in the winner for that dataset. Different variables routinely pick different models.
131
-
132
- ### Python API
133
-
134
- ```python
135
- import gridpack
136
-
137
- # file level
138
- stats = gridpack.compress_file(src, dst, level=12, chunk_t=16, threads=4,
139
- quant_bits=0, mode="auto", entropy="zstd",
140
- file_chunk=None, verify=True)
141
- gridpack.verify_file(src, dst) # -> {"exact": True, "details": [...]}
142
- gridpack.decompress_file(dst, out_nc)
143
-
144
- # array level
145
- blob = gridpack.encode_array(arr, level=12, chunk_t=16, threads=1,
146
- quant_bits=0, mode="dt", entropy="zstd")
147
- arr2 = gridpack.decode_array(blob, arr.shape, arr.dtype)
148
-
149
- # what representation will be used?
150
- gridpack.detect_lattice_e(arr) # -> -4, or None if no lattice
151
- ```
152
-
153
- `chunk_t=None` encodes the array as a single block. With `chunk_t=N` the array is split
154
- along axis 0 and each chunk is stored independently, so `decode_array` can rebuild without
155
- the whole payload in memory.
156
-
157
- ### xarray
158
-
159
- Installing the package registers an xarray backend entry point, so a `.gpz` file opens
160
- like any other dataset:
161
-
162
- ```python
163
- import xarray as xr
164
- import gridpack # registers backend + accessor
165
-
166
- ds = xr.open_dataset("era5_10u.gpz", engine="gpz")
167
- ds = xr.open_dataset("era5_10u.gpz", engine="gpz",
168
- chunks={"time0": 24}) # dask-backed, lazy chunks
169
- ```
170
-
171
- `engine="gpz"` is also selected automatically by `xr.open_dataset("...gpz")`, because the
172
- backend's `guess_can_open` matches the `.gpz` suffix. Dimensions and coordinates are rebuilt
173
- from the dimension-scale links stored in the container, and every value is bit-identical to
174
- the source `.nc`.
175
-
176
- xarray has no generic *write* backend interface, so writing goes through the accessor:
177
-
178
- ```python
179
- ds.gpz.to_file("out.gpz", level=12, threads=4) # -> compress_file() stats dict
180
- ds.gpz.info("out.gpz") # -> ratio, per-variable model, ...
181
-
182
- gridpack.from_xarray(ds, "out.gpz") # same, without the accessor
183
- gridpack.open_xarray("out.gpz") # same as xr.open_dataset(..., engine="gpz")
184
- ```
185
-
186
- `to_file` writes a temporary netCDF (needs `h5netcdf` or `netCDF4`) and compresses that, then
187
- removes it. netCDF-internal attributes (`_NCProperties`, `CLASS`, `_Netcdf4Coordinates`, ...)
188
- are stripped automatically -- if a backend rejects any further reserved name, it is dropped
189
- and the write is retried.
190
-
191
- Verified on the bundled ERA5 subset (120 time steps, 4 variables): all four variables
192
- round-trip bit-exactly through `engine="gpz"`, and a `ds.isel(time0=slice(0, 6)).gpz.to_file()`
193
- write-back re-reads bit-exactly at **10.30x**.
194
-
195
- ```bash
196
- python tests/test_xarray.py
197
- ```
198
-
199
- ---
200
-
201
- ## Benchmarks
202
-
203
- Real ERA5, 10 m eastward wind (`eastward_wind_at_10_metres`, `float32`, 120 time steps,
204
- 47.53 MB raw). Full reproducible output in [`docs/BENCHMARKS.md`](docs/BENCHMARKS.md);
205
- regenerate with `python scripts/bench_era5.py <file.nc>`.
206
-
207
- ### Representation × time model (entropy = zstd level 12)
208
-
209
- | Representation | Time model | Compressed | Ratio |
210
- |---|---|---|---|
211
- | lattice E=-4 | none | 8.35 MB | 5.69× |
212
- | lattice E=-4 | spatial | 5.05 MB | 9.40× |
213
- | **lattice E=-4** | **dt** | **4.59 MB** | **10.36×** |
214
- | order map | none | 9.05 MB | 5.25× |
215
- | order map | spatial | 10.83 MB | 4.39× |
216
- | order map | dt | 11.07 MB | 4.29× |
217
-
218
- Two things worth noting. The lattice representation roughly **doubles** the ratio
219
- (10.36× vs 5.25×). And with the plain order map, adding prediction makes things *worse*
220
- (5.25× → 4.29×): the order-preserving bit map has a large step at the zero crossing, so a
221
- field that changes sign produces enormous residuals. Integer-lattice residuals do not have
222
- that problem.
223
-
224
- ### Entropy backend (first 200 000 bytes of the same byte-plane buffer)
225
-
226
- | Backend | Compressed | vs raw | Time |
227
- |---|---|---|---|
228
- | zstd | 74 363 B | 2.69× | 0.03 s |
229
- | zlib | 75 024 B | 2.67× | 0.22 s |
230
- | rc0 | 68 216 B | 2.93× | 0.45 s |
231
- | **rc1** | **65 015 B** | **3.08×** | 0.48 s |
232
- | huff | 73 150 B | 2.73× | 0.03 s |
233
-
234
- The adaptive context range coder (`rc1`, context = previous byte × bit index) beats zstd by
235
- **12.6 %** on this data, and `rc0` by 4.7 %. That is a real gain but it costs ~15× the time
236
- in pure Python, so `zstd` stays the production default and `rc*` are there for offline
237
- re-encoding or archival runs where CPU time is cheap.
238
-
239
- ### Whole file
240
-
241
- | | |
242
- |---|---|
243
- | source `.nc` | 475.28 MB |
244
- | `.gpz` | 45.83 MB |
245
- | ratio vs raw float bytes | **10.37×** |
246
- | wall clock | 47.6 s (≈10 MB/s of raw data) |
247
- | byte-for-byte verify | **pass** |
248
- | chosen models | wind → lattice E=-4 / `dt`; `lat` → E=-2 / `spatial`; `lon` → E=-2 / `spatial`; `time0` → E=4 / `spatial` |
249
-
250
- Ratio depends heavily on how the source was written. Fields already quantised (most modern
251
- ERA5 monthly files) land at ~10×; unquantised, full-precision months land at ~4×; a `.nc`
252
- that is itself zlib-compressed by netCDF will show a smaller gain versus that `.nc`, so
253
- compare against the raw float bytes to get a like-for-like number.
254
-
255
- ---
256
-
257
- ## Correctness
258
-
259
- `tests/test_roundtrip.py` covers every path and asserts byte equality, not approximate
260
- equality:
261
-
262
- ```bash
263
- python tests/test_roundtrip.py # unit + array level
264
- GRIDPACK_TEST_NC=file.nc python tests/test_roundtrip.py # + NetCDF end-to-end
265
- ```
266
-
267
- It checks the order map on `±0.0 / ±Inf / NaN / subnormals`, lattice detection and
268
- round-trip for float32 and float64, negative-zero preservation, all five entropy backends
269
- (including single-symbol and empty inputs), all time models on random / lattice / constant /
270
- NaN-bearing / integer / 1-D arrays, chunked encoding, and a full NetCDF compress → verify →
271
- decompress cycle.
272
-
273
- Container guarantees:
274
-
275
- - every block carries a SHA-256 of its raw bytes; a mismatch raises instead of returning
276
- wrong data,
277
- - the restored `.nc` keeps variable names, shapes, dtypes, dimension scales, chunking,
278
- compression settings, `_FillValue` and all attributes,
279
- - peak memory is bounded by the file chunk size, not by the file size.
280
-
281
- ---
282
-
283
- ## Container format
284
-
285
- `.gpz` (`MAGIC = b"GPZ1"`): JSON metadata header + a payload index + the compressed blocks.
286
- Metadata records the source name/size, codec version, and per-dataset shape, dtype,
287
- chunking, attributes, dimension links, chosen mode, and the detected lattice exponent.
288
-
289
- Each block starts with a tag byte (`0` raw, `1` predicted, `2` byte-planes only,
290
- `3` lattice + byte-planes, `4` lattice + predicted), an entropy-backend id, and — for the
291
- lattice tags — the exponent and the length of the negative-zero bitmap.
292
-
293
- ---
294
-
295
- ## Limitations
296
-
297
- - The lattice path only engages when **every** value shares one exponent and the resulting
298
- integers fit comfortably in int32; otherwise the codec falls back to the order map.
299
- - `.gpz` needs this library to read. It is not a NetCDF file. If you need `.nc`
300
- compatibility, decompress back.
301
- - `rc0`/`rc1`/`huff` are pure Python and unsuitable for large files.
302
- - Throughput is ~10 MB/s of raw data per process. Parallelise across files.
303
- - Only `quant_bits=0` is lossless; anything else deliberately discards information.
304
-
305
- ---
306
-
307
- ## 中文说明
308
-
309
- `gridpack` 是面向 ERA5 这类**格点科学数据**的严格无损压缩与读写工具包:解码后与原始
310
- 数据**逐字节相同**(含 `-0.0`、`NaN`、`±Inf`),实测在真实 ERA5 10m 风场上达到
311
- **10.37 倍**压缩(相对原始 float 字节),并支持按需读取。
312
-
313
- **核心思路**:ERA5 各要素的值并不是任意浮点数,而是**精确落在同一个二进制格点 `2**E`
314
- 上**(10m 风量化到 `2**-4`,总降水 `2**-14`,2m 气温 `2**-4`)。工具会**精确判定**这个
315
- 格点并把浮点无损改写成整数 `k = v / 2**E`,再做时间/空间一阶差分、zig-zag、字节平面重排
316
- 与熵编码。负零单独用稀疏位图记录,保证位级无损。
317
-
318
- **关键参数**:`--mode`(`none`/`spatial`/`dt`/`lagN`/`auto`,按变量自动选)、
319
- `--entropy`(`zstd`/`zlib`/`rc0`/`rc1`/`huff`)、`--level`(zstd 1–22,默认 12)、
320
- `--chunk-t`(内部时间分块,默认 16)、`--file-chunk`(文件级分块,0 = 自适应约 96 MB)、
321
- `--threads`、`--quant-bits`(0 = 严格无损,>0 为有容差模式)。
322
-
323
- **实测**(真实 ERA5 10m 风场,120 时次,47.53 MB 原始):格点整数 + `dt` 达 **10.36×**,
324
- 而保序映射最好只有 5.25×——格点表示带来近一倍收益;熵编码上 `rc1` 比 `zstd` 再小
325
- **12.6%**,但纯 Python 慢约 15 倍,故生产默认仍用 `zstd`。整文件 475.28 MB → 45.83 MB,
326
- **10.37×**,耗时 47.6 s,逐字节校验通过。
327
-
328
- **xarray 支持**:`pip install gridpack` 后 xarray 后端即注册完成,`.gpz` 可直接
329
- `xr.open_dataset("x.gpz", engine="gpz")` 打开(`.gpz` 后缀也会被自动识别);维度与坐标
330
- 从容器里记录的维标关联还原,数值与原始 `.nc` 逐位一致。写回用
331
- `ds.gpz.to_file("out.gpz", level=12, threads=4)`,也可用 `gridpack.from_xarray(ds, path)`;
332
- 查看某个 `.gpz` 的压缩率与逐变量选中的模型用 `ds.gpz.info(path)`。实测 120 时次 ERA5 子集
333
- 的 4 个变量经 `engine="gpz"` 读出全部逐位通过,写回再读回同样逐位通过(10.30×)。
334
-
335
- ---
336
-
337
- ## License
338
-
339
- MIT — see [LICENSE](LICENSE).
340
-
341
-
342
35
  # gridpack
343
36
 
344
37
  **Lossless compression for gridded scientific data (ERA5 and friends).**
@@ -43,7 +43,7 @@ except Exception: # pragma: no cover
43
43
  from_xarray = None
44
44
  HAVE_XARRAY = False
45
45
 
46
- __version__ = "0.5.1"
46
+ __version__ = "0.5.2"
47
47
 
48
48
  __all__ = [
49
49
  "codec", "container", "entropy", "lattice", "netcdf_io",
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gridpack"
7
- version = "0.5.1"
7
+ version = "0.5.2"
8
8
  description = "Lossless compression for gridded scientific data (ERA5 and friends)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes