Portable operator artifacts¶
Stencil, ReduceOperator, Resampler, RestrictedOperator, and BiasTree
all expose the same byte-oriented API:
from pathlib import Path
import geohalo as ghl
# Build once, then store in a file, database bytea column, or object store.
operator = ghl.ReduceOperator.compute(stencil, source_lat, source_lon)
blob: bytes = operator.to_npz()
Path("operator.npz").write_bytes(blob)
# Restore without geometry extraction, resampler construction, or fusion.
restored = ghl.ReduceOperator.from_npz(Path("operator.npz").read_bytes())
result = restored.apply_grid(values, how="mean")
to_npz() returns bytes; from_npz(blob) accepts bytes, not a filename or an
open file. Use the class matching the stored object. Wrong classes, unknown
schema versions, missing fields, object arrays, and malformed structures raise
ValueError. Unsupported custom key types raise TypeError when exporting.
Both cache backends use this same format. Standalone exports retain the object's
current row order, unlike canonical cache storage; always interpret rows
using the restored keys. Digests identify build inputs, not row permutations.
What survives a roundtrip¶
- CSR coefficient and index arrays, including dtypes, accumulation order, duplicate entries, and explicit zeros; serialization never sorts or merges matrix entries.
- Source/target coordinates in their stored order, input digests, normalizers, and the object's apply-time configuration.
- Restricted windows, per-window gather positions, and spatial chunk sizes.
- Both resampler representations: the mean-preserving full CSR transform or conservative latitude/longitude factors plus normalization. Conservative exports never materialize a grid-to-grid matrix.
- Index names, dtypes, duplicates, order, MultiIndex levels/codes (including unused levels and missing codes), categorical categories/order, and date/time index frequency and timezone metadata.
Apply-time caches such as compact matrix views are not serialized. They are rebuilt lazily from the restored arrays. Input geometries and original tree edges are not included: these are application-ready operators, not complete recipes for rebuilding them. Options already baked into coefficients (such as resampler period and cell bounds) are represented by those coefficients and the input digest, not duplicated as a build recipe.
Supported keys¶
Flat indexes support NumPy numeric/object dtypes and pandas nullable numeric,
boolean, and string dtypes. RangeIndex, CategoricalIndex, MultiIndex,
DatetimeIndex, TimedeltaIndex, PeriodIndex, and IntervalIndex retain their
specialized metadata. Reading a pandas string index with Arrow storage requires
the same optional Arrow dependency as constructing that index normally.
Likewise, pandas 3's NaN-based str dtype needs pandas 3 to restore that exact
dtype; older pandas can still be used with object indexes or nullable string
indexes. Unsupported reader-side dtype features fail rather than silently
changing labels or missing-value semantics.
Object labels and index names use a fixed typed encoding for Python strings,
integers, floats, complex numbers, booleans, bytes, tuples (recursively), None,
pd.NA, pd.NaT, NumPy numeric/string/date/time scalars, Python dates,
naive/fixed-offset datetimes and timedeltas, and pandas Timestamp, Timedelta, Period and Interval
scalars. Non-finite numbers are encoded as tagged strings, not nonstandard JSON
numbers. Integers use decimal strings, preserving values beyond JavaScript's
integer precision. NumPy scalars include their dtype and hex-encoded bytes.
Pandas date/time keys support IANA timezone names and UTC fixed offsets; convert
custom timezone implementations to one of those before exporting. Use pandas
Timestamp rather than Python datetime labels for IANA zones.
Arbitrary Python objects, custom Index subclasses, and unlisted extension dtypes
are deliberately unsupported. Convert such labels to supported values before
building the operator. There is no implicit repr conversion, dynamic class
import, eval, or fallback to executable object serialization.
NPZ schema version 1¶
The container is an uncompressed NumPy NPZ archive: named .npy arrays in a ZIP
file. Every array is non-object. It can be inspected with NumPy and the standard
library alone, without importing geohalo, pandas, SciPy, or exactextract:
import io
import json
import numpy as np
with np.load(io.BytesIO(blob), allow_pickle=False) as archive:
metadata = json.loads(archive["metadata"].tobytes().decode("utf-8"))
print(metadata["kind"], metadata["version"], metadata["geohalo_version"])
print(archive["digest"].tobytes().hex())
data = archive["matrix_data"] # ReduceOperator example
indices = archive["matrix_indices"]
indptr = archive["matrix_indptr"]
shape = tuple(archive["matrix_shape"])
This uses NumPy's documented
allow_pickle=False loading mode.
Reading the arrays is independent of Python class layout; restoring a geohalo
object and its pandas index still requires the relevant libraries.
Common members¶
metadata is a one-dimensional uint8 array of UTF-8 JSON bytes. Required fields:
| Field | Meaning |
|---|---|
format |
The string "geohalo" |
version |
Integer NPZ schema version, currently 1 |
kind |
Stencil, ReduceOperator, Resampler, RestrictedOperator, or BiasTree |
geohalo_version |
Writer's package version; informational, not a compatibility gate |
keys |
Typed index metadata for every object except Resampler |
digest is a one-dimensional uint8 array holding the object's digest bytes.
Builder-produced digests are SHA-256 (32 bytes). Each CSR matrix named prefix
uses prefix_data, prefix_indices, prefix_indptr, and prefix_shape.
shape is a two-element int64 array; coefficient and index buffers retain their
original dtypes. CSR indices are zero-based.
Object-specific members¶
| Kind | Array members beyond metadata and digest |
Extra JSON fields |
|---|---|---|
| Stencil | matrix_*, row_sums, lats, lons |
spherical_correction |
| ReduceOperator | matrix_*, row_sums, source_lat, source_lon |
iterations |
| RestrictedOperator | matrix_*, row_sums, source_lat, source_lon, windows, gather_0 … gather_N, lat_chunks, lon_chunks |
— |
| BiasTree | matrix_* |
how |
| Mean-preserving Resampler | matrix_*, source_lat, source_lon, target_lat, target_lon |
method="meanpreserving", normalization="destination" |
| Conservative Resampler | latitude_*, longitude_*, source_lat, source_lon, target_lat, target_lon |
method="conservative", normalization |
Restricted windows is an (n_windows, 4) int64 array with rows
[latitude_start, latitude_stop, longitude_start, longitude_stop]. Bounds are
half-open, with unit steps, in stored source order. gather_i is a 1-D integer
array of C-order positions within window i; concatenating the gathered arrays
defines compact matrix column order. No windows means shape (0, 4) and no
gather_i members. Chunk arrays list consecutive positive sizes along each axis.
Key metadata is tagged JSON. Flat indexes have kind, a tagged name, tagged
values, and a dtype descriptor. Specialized kinds are range, categorical,
multi, datetime, timedelta, period, and interval; MultiIndex stores
recursively encoded levels, integer codes, tagged names, and sortorder.
For example, a tuple label ("zone", 7) is
["tuple", [["str", "zone"], ["int", "7"]]], not a string representation to
evaluate. The allowlisted codec is defined in geohalo._index_codec.
The schema version is separate from geohalo's release version and from legacy cache payload versions. Readers reject unknown schema versions rather than guessing. Future incompatible format changes require a new schema version; support for older schemas must be explicit. This is not a promise that future versions of every dependency will retain every pandas dtype or timezone name.
Validation and trust¶
Loading always disables pickle. The reader checks object kind/version, required
arrays and dtypes, CSR bounds and monotonic pointers, matrix/grid/key dimensions,
normalizer lengths, and restricted chunk/window/gather bounds before use.
Unexpected or duplicate archive members are rejected. Invalid NPZ cache hits
raise errors; force_recompute=True explicitly replaces them.
This prevents the executable-object loading inherent in pickle. It does not authenticate a writer, verify geometric correctness, or impose an archive size limit. A validly shaped matrix can still contain wrong coefficients, and a large or compressed archive can exhaust memory. Authenticate shared artifacts, limit input sizes/resources, and maintain dependency security updates. Use an external checksum or signature if you need integrity/provenance checks. The embedded input digest is not a checksum of the archive contents.
Migrating existing caches¶
LocalCache now reads/writes path/<namespace>/<key>.npz. RedisCache uses
geohalo:<namespace>:npz:v1:<key>. Existing .pkl files and old Redis prefixes
are neither read nor deleted. The first request rebuilds each operator once and
writes NPZ; old and new deployments can coexist without reading each other's
formats. Input digests themselves are unchanged.
There is no automatic legacy loader or migration fallback. For externally
managed artifacts, export to_npz() from an already available object using this
version, or rebuild from inputs. Remove obsolete cache entries yourself after
confirming older deployments no longer need them.