Compression Safeguards [1] are a user-centric framework that empowers users to (1) declare which properties of their data they need lossy compression to preserve, and then (2) guarantees that these safety requirements are always upheld when (3) compressing with any user-chosen compressor. When wrapped in the appropriate safeguards, even unsafe compressors can be used safely.
During compression, the safeguards work by checking if the compressor-decompressed data would violate the user’s safety requirements. If there are any violations, the safeguards produce pointwise corrections, which are then checked again. These corrections are typically losslessly compressed and stored alongside the compressed data. During decompression, the corrections are applied, thereby ensuring that the decompressed result satisfies all user safety requirements. Importantly, the corrections are are not just lossless encodings of the original data but are instead chosen to compress well, based on an analysis of the safety requirements.
The compression safeguards are implemented in the compression-safeguards Python package, with convenient user-facing frontends for a safeguarded meta-compressor in numcodecs-safeguards and for working with chunked data in xarray-safeguards. These implementations can safeguard are variety of safety requirements, including regionally varying error bounds over the data and quantities of interest , missing values, isosurfaces, and much more. The documentation provides several examples for inspiration, e.g. for preserving the relative vorticity computed over a compressed u-v wind field.
While the safeguards work best when safeguarding an existing scientific compressor, they can also be used to perform all of the compression themselves by using a compressor that decompresses to a constant-zero array, which then needs to be corrected for (almost) all data elements. Such a compressor is provided by the numcodecs_zero.ZeroCodec.
from pathlib import Path
import netCDF4
import numpy as np
import xarray as xrdata = Path("data")import earthkit.plots
from quickplot import quickplotImporting the SafeguardedCodec meta-compressor¶
from numcodecs_safeguards import SafeguardedCodec
from numcodecs_zero import ZeroCodec?SafeguardedCodecInit signature:
SafeguardedCodec(
*,
codec: dict[str, None | int | float | str | bool | list['JSON'] | dict[str, 'JSON']] | numcodecs.abc.Codec,
safeguards: collections.abc.Collection[dict[str, None | int | float | str | bool | list['JSON'] | dict[str, 'JSON']] | compression_safeguards.safeguards.abc.Safeguard],
fixed_constants: collections.abc.Mapping[str | compression_safeguards.utils.bindings.Parameter, int | float | numpy.number | numpy.ndarray[tuple[int, ...], numpy.dtype[numpy.number]]] | compression_safeguards.utils.bindings.Bindings = <compression_safeguards.utils.bindings.Bindings object at 0x1146fc340>,
lossless: None | dict[str, None | int | float | str | bool | list['JSON'] | dict[str, 'JSON']] | numcodecs_safeguards.lossless.Lossless = None,
compute: None | dict[str, None | int | float | str | bool | list['JSON'] | dict[str, 'JSON']] = None,
_version: None | str | semver.version.Version = None,
) -> None
Docstring:
An adaptor codec that uses
[`Safeguards`][compression_safeguards.api.Safeguards] to guarantee certain
properties / safety requirements are upheld by the wrapped codec.
Parameters
----------
codec : dict[str, JSON] | Codec
The codec that will be wrapped with safeguards. It can either be passed
as a codec configuration [`dict`][dict], which is passed to
[`numcodecs.registry.get_codec(config)`][numcodecs.registry.get_codec],
or an already initialized [`Codec`][numcodecs.abc.Codec]. If you want to
wrap a sequence or stack of codecs, you can use the
[`numcodecs_combinators.stack.CodecStack`][numcodecs_combinators.stack.CodecStack]
combinator.
The codec must be deterministic during decoding (but can be
non-deterministic during encoding) such that decoding the same bytes
always produces the same bitwise equivalent decoded result.
It is desirable to perform lossless compression after applying the
safeguards (rather than before), e.g. by customising the
[`Lossless.for_codec`][..lossless.Lossless.for_codec] field of the
`lossless` parameter.
The `codec` combined with its `lossless` encoding must encode to a 1D
buffer of bytes. It is also recommended that the `codec` can
[`decode`][numcodecs.abc.Codec.decode]
without receiving the output data type and shape via the `out`
parameter. If the `codec` does not fulfil these requirements, it can be
wrapped inside the
[`numcodecs_combinators.framed.FramedCodecStack`][numcodecs_combinators.framed.FramedCodecStack]
combinator.
It is also possible to compress the data with *just* the safeguards
(i.e. without a `codec` that provides proper lossy compression) by
passing [`numcodecs_zero.ZeroCodec()`][numcodecs_zero.ZeroCodec] or
`dict(id="zero")` to `codec`. The zero codec only encodes the data
type and shape, not the data values themselves, and decodes to all-
zero values, forcing the safeguards to correct (almost) all values.
With this configuration, the safeguards thus act as a safe lossy
compressor in their own right, as any size reduction comes from the
`lossless` compression of the safeguards corrections (which the
safeguards produce to be highly-compressible, if possible).
safeguards : Collection[dict[str, JSON] | Safeguard]
The safeguards that will be applied to the codec. They can either be
passed as a safeguard configuration [`dict`][dict] or an already
initialized
[`Safeguard`][compression_safeguards.safeguards.abc.Safeguard].
Please refer to the
[`SafeguardKind`][compression_safeguards.safeguards.SafeguardKind]
for an enumeration of all supported safeguards.
The `SafeguardedCodec` supports safeguards with late-bound parameters,
e.g. the
[`SelectSafeguard`][compression_safeguards.safeguards.combinators.select.SelectSafeguard],
but they must be provided as `fixed_constants` that are *fixed* and
must be compatible with *any* data that will be encoded with this
codec. Therefore, fixed constants should only be used for late-bound
parameters that can be fixed across all uses of the codec.
fixed_constants : Mapping[str | Parameter, Value] | Bindings
Mapping of parameter names to *fixed* constant scalars or arrays that
will be provided as late-bound parameters to the safeguards.
The mapping must resolve all late-bound parameters of the safeguards
and include no extraneous parameters.
The provided values must have a compatible shape and values for *any*
data that will be encoded with this codec, otherwise
[`encode`][.encode] will fail.
You can use the [`update_fixed_constants`][.update_fixed_constants]
method inside a `with` statement to temporarily update the late-bound
parameters.
The fixed constants are included in the [config][.get_config] of the
codec. While [`int`][int] and [`float`][float] scalars are included
as-is, numpy scalars and arrays are encoded to the losslessly
compressed [`.npz`][numpy.savez_compressed] format and stored in a
`data:application/x-npz;base64,<data>`
[data URI](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data).
lossless : None | dict[str, JSON] | Lossless, optional
The lossless encoding that is applied after the codec and the
safeguards:
- [`Lossless.for_codec`][..lossless.Lossless.for_codec] specifies the
lossless encoding that is applied to the encoded output of the
wrapped `codec`. By default, no additional lossless encoding is
applied.
- [`Lossless.for_corrections`][..lossless.Lossless.for_corrections]
specifies the lossless encoding that is applied to the corrections
that the safeguards produce. By default, Zstandard compression is
applied after entropy coding.
The lossless encoding must encode to a 1D buffer of bytes.
compute : None | dict[str, JSON] | Compute, optional
Compute configuration with options that may affect the compression
ratio and time cost of computing the safeguards corrections.
While these options can change the particular corrections that are
produced, the resulting corrections always satisfy the safety
requirements.
_version : ...
The version of the codec. Do not provide this parameter explicitly.
Raises
------
ValueError
if `codec` wraps another `SafeguardedCodec`, which may create a printer
problem.
LateBoundParameterResolutionError
if `fixed_constants` does not resolve all late-bound parameters of the
safeguards or includes any extraneous parameters.
...
if instantiating the `codec` or a safeguard raises an exception.
File: ~/egu26-compression-sc2.5/.venv/lib/python3.13/site-packages/numcodecs_safeguards/__init__.py
Type: ABCMeta
Subclasses: Bounding the pointwise absolute error¶
A pointwise absolute error bound can be guaranteed using the eb safeguard with type abs:
eb_abs = 0.1
SafeguardedCodec(
codec=ZeroCodec(), # for example, any numcodecs codec works
safeguards=[
{"kind": "eb", "type": "abs", "eb": eb_abs},
],
)SafeguardedCodec(codec=ZeroCodec(), safeguards=[ErrorBoundSafeguard(type='abs', eb=0.1, equal_nan=False)], fixed_constants={}, lossless=Lossless(for_codec=None, for_corrections=PickBestCodec(CodecStack(TokenizeCodec(), BitmapIndexCodec(), TypedByteShuffleCodec(), FramedCodecStack(Zstd(level=3))), CodecStack(BinaryDeltaCodec(), TokenizeCodec(), BitmapIndexCodec(), TypedByteShuffleCodec(), FramedCodecStack(Zstd(level=3))))), compute=Compute(unstable_iterative=False, unstable_lossless_corrections=False))Bounding the pointwise relative error¶
A pointwise relative error bound can be guaranteed using the eb safeguard with type rel:
eb_rel = 0.01 # 1 %
SafeguardedCodec(
codec=ZeroCodec(), # for example, any numcodecs codec works
safeguards=[
{"kind": "eb", "type": "rel", "eb": eb_rel},
],
)SafeguardedCodec(codec=ZeroCodec(), safeguards=[ErrorBoundSafeguard(type='rel', eb=0.01, equal_nan=False)], fixed_constants={}, lossless=Lossless(for_codec=None, for_corrections=PickBestCodec(CodecStack(TokenizeCodec(), BitmapIndexCodec(), TypedByteShuffleCodec(), FramedCodecStack(Zstd(level=3))), CodecStack(BinaryDeltaCodec(), TokenizeCodec(), BitmapIndexCodec(), TypedByteShuffleCodec(), FramedCodecStack(Zstd(level=3))))), compute=Compute(unstable_iterative=False, unstable_lossless_corrections=False))Bounding the pointwise ratio error¶
A pointwise ratio / logarithmic error bound can be guaranteed using the eb safeguard with type ratio:
eb_ratio = 1.01 # between x / 1.01 and x * 1.01
SafeguardedCodec(
codec=ZeroCodec(), # for example, any numcodecs codec works
safeguards=[
{"kind": "eb", "type": "ratio", "eb": eb_ratio},
],
)SafeguardedCodec(codec=ZeroCodec(), safeguards=[ErrorBoundSafeguard(type='ratio', eb=1.01, equal_nan=False)], fixed_constants={}, lossless=Lossless(for_codec=None, for_corrections=PickBestCodec(CodecStack(TokenizeCodec(), BitmapIndexCodec(), TypedByteShuffleCodec(), FramedCodecStack(Zstd(level=3))), CodecStack(BinaryDeltaCodec(), TokenizeCodec(), BitmapIndexCodec(), TypedByteShuffleCodec(), FramedCodecStack(Zstd(level=3))))), compute=Compute(unstable_iterative=False, unstable_lossless_corrections=False))Preserving Missing Values¶
The error-bounding safeguards automatically preserve NaN missing values. By default, the exact NaN bit pattern is preserved. To only preserve whether a value is NaN but not its exact bit pattern, the error bounding safeguards accept a equal_nan=True option.
If the codec that is wrapped in safeguards raises an exception when the data includes NaN values, the numcodecs_replace.ReplaceFilterCodec can be used to first replace all NaN values, while leaving it up to the safeguards to reconstruct the NaN values during decompression:
from numcodecs_combinators.stack import CodecStack
from numcodecs_replace import Replacement, ReplaceFilterCodec
eb_abs = 0.1
SafeguardedCodec(
# the replace filter codec must be used *inside* the safeguarded codec
# so that the safeguards see the NaN values in the original data and
# can reconstruct them during decompression
codec=CodecStack(
ReplaceFilterCodec(replacements={np.nan: "finite_mean"}),
ZeroCodec(), # for example, any numcodecs codec works
),
safeguards=[
{"kind": "eb", "type": "abs", "eb": eb_abs},
],
)SafeguardedCodec(codec=CodecStack(ReplaceFilterCodec(replacements={nan: 'finite_mean'}), ZeroCodec()), safeguards=[ErrorBoundSafeguard(type='abs', eb=0.1, equal_nan=False)], fixed_constants={}, lossless=Lossless(for_codec=None, for_corrections=PickBestCodec(CodecStack(TokenizeCodec(), BitmapIndexCodec(), TypedByteShuffleCodec(), FramedCodecStack(Zstd(level=3))), CodecStack(BinaryDeltaCodec(), TokenizeCodec(), BitmapIndexCodec(), TypedByteShuffleCodec(), FramedCodecStack(Zstd(level=3))))), compute=Compute(unstable_iterative=False, unstable_lossless_corrections=False))Missing values / other special values that use a different sentinel value than NaN can be preserved using the same safeguard, e.g. together with an absolute error bound:
eb_abs = 0.1
sentinel = 9999
SafeguardedCodec(
codec=ZeroCodec(), # for example, any numcodecs codec works
safeguards=[
{"kind": "eb", "type": "abs", "eb": eb_abs},
{"kind": "same", "value": sentinel, "exclusive": True},
],
)SafeguardedCodec(codec=ZeroCodec(), safeguards=[ErrorBoundSafeguard(type='abs', eb=0.1, equal_nan=False), SameValueSafeguard(value=9999, exclusive=True)], fixed_constants={}, lossless=Lossless(for_codec=None, for_corrections=PickBestCodec(CodecStack(TokenizeCodec(), BitmapIndexCodec(), TypedByteShuffleCodec(), FramedCodecStack(Zstd(level=3))), CodecStack(BinaryDeltaCodec(), TokenizeCodec(), BitmapIndexCodec(), TypedByteShuffleCodec(), FramedCodecStack(Zstd(level=3))))), compute=Compute(unstable_iterative=False, unstable_lossless_corrections=False))The exclusive mode guarantees that every missing value in the input is still missing after decompression and that every non-missing value in the input stays non-missing after decompression.
Preserving Quantities of Interest (QoI)¶
An error bound over a quantity of interest can be guaranteed using the qoi_eb_pw (for pointwise QoIs) and qoi_eb_stencil (for QoIs that are computed over a local neighbourhood around each point) safeguards. For instance, preserving an absolute error bound over is as simple as:
eb_abs_qoi = 0.1
SafeguardedCodec(
codec=ZeroCodec(), # for example, any numcodecs codec works
safeguards=[
{"kind": "qoi_eb_pw", "qoi": "square(x)", "type": "abs", "eb": eb_abs_qoi},
],
)SafeguardedCodec(codec=ZeroCodec(), safeguards=[PointwiseQuantityOfInterestErrorBoundSafeguard(qoi='square(x)', type='abs', eb=0.1, qoi_dtype='lossless')], fixed_constants={}, lossless=Lossless(for_codec=None, for_corrections=PickBestCodec(CodecStack(TokenizeCodec(), BitmapIndexCodec(), TypedByteShuffleCodec(), FramedCodecStack(Zstd(level=3))), CodecStack(BinaryDeltaCodec(), TokenizeCodec(), BitmapIndexCodec(), TypedByteShuffleCodec(), FramedCodecStack(Zstd(level=3))))), compute=Compute(unstable_iterative=False, unstable_lossless_corrections=False))Like in numpy, we could have also used x**2 instead of the specialised square(x) function, though such specialised functions may provide higher performance and better-compressible corrections.
Preserving so much more¶
The compression safeguards can also be used to preserve isosurfaces, regions of interest, combinations of different safety requirements, topology via the local order of points in a neighbourhood [2], and so much more. Please see the documentation and examples for inspiration.
Example¶
# Load the data
ds = xr.open_dataset(
data / "hplp" / "hplp_sfc_regridded_t_025deg_levels_steps_204_216_228_240.nc",
engine="netcdf4",
decode_timedelta=True,
)
da = ds["2t"]eb_abs = 0.1 # 0.1 K
codec = SafeguardedCodec(
codec=ZeroCodec(), # safeguards need to correct constant zero prediction
safeguards=[
# bound absolute error over the data
{"kind": "eb", "type": "abs", "eb": eb_abs},
# preserve global minimum and maximum
{"kind": "sign", "offset": "$x_min"},
{"kind": "sign", "offset": "$x_max"},
],
)# encode and decode the data
da_enc = codec.encode(da.values)
da_dec = da.copy(data=codec.decode(da_enc))# plot a comparison figure
fig = earthkit.plots.Figure(
size=(15, 4),
rows=1,
columns=3,
)
quickplot(da, fig.add_map(0, 0), title="Original {default_title}")
quickplot(
da_dec,
fig.add_map(0, 1),
title="Safeguarded(0)",
cr=da.nbytes / np.array(da_enc).nbytes,
)
quickplot(da_dec - da, fig.add_map(0, 2), error=True, title="Compression Error")
fig.show()
Chunked Example¶
The Compression Safeguards can also be used to preserve safety requirements across chunk boundaries in chunked data using e.g. the xarray-safeguards package. For this example, we rechunk the data into chunks (mostly) of size 70x100 and then bound the relative error over the first-order second-order-accurate central difference along the periodic longitude axis.
da_chunked = da.chunk(lat=70, lon=100)
display(da_chunked.data)
da_chunked.dims('time', 'lat', 'lon')import xarray_safeguardseb_rel_qoi = 0.01 # 1 % over the quantity of interest
safeguards = [
{
"kind": "qoi_eb_stencil",
"qoi": """
return finite_difference(
x, # over the data
order=1, # 1st order derivative
accuracy=2, # 2nd order accurate
type=0, # central
axis=-1, # longitude axis
grid_centre=c["$d_lon"], # coordinate, provided by xarray-safeguards
grid_period=360, # periodic over 360 degrees
);
""",
"neighbourhood": [
# periodic longitude axis, -1,+1 stencil for finite difference
{"axis": -1, "before": 1, "after": 1, "boundary": "wrap"},
],
"type": "rel",
"eb": eb_rel_qoi,
}
]from numcodecs_combinators.stack import CodecStack
# lazily produce a prediction, i.e. the lossy decompressed data
da_prediction = CodecStack(
ZeroCodec(), # for example, any numcodecs codec works
).encode_decode_data_array(da_chunked)# lazily produce the correction, later persist the result
da_correction = xarray_safeguards.produce_data_array_correction(
data=da_chunked,
prediction=da_prediction,
safeguards=safeguards,
).persist()# lazily apply the correction, later persist the result
da_corrected = xarray_safeguards.apply_data_array_correction(
prediction=da_prediction,
correction=da_correction,
).persist()# xarray-safeguards, like compression-safeguards, performs no lossless
# compression of the corrections
# here we use the lossless corrections compression from numcodecs-safeguards
import numcodecs_safeguards
da_correction_nbytes = np.array(
numcodecs_safeguards.lossless._default_lossless_for_corrections().encode(
da_correction.values
)
).nbytesdef differentiate_along_longitude(da: xr.DataArray) -> xr.DataArray:
da_wrapped = da.pad(lon=1, mode="wrap").assign_coords(
lon=da.lon.pad(lon=1, mode="reflect", reflect_type="odd"),
)
da_dXdLon = da_wrapped.differentiate("lon")
da_dXdLon.attrs.update(
long_name=f"{da.long_name} derivative along longitude",
units=f"{da.units} degree**-1",
)
return da_dXdLon.sel(lon=slice(da.lon.min(), da.lon.max()))# plot a comparison figure
fig = earthkit.plots.Figure(
size=(15, 4),
rows=1,
columns=3,
)
da_deriv = differentiate_along_longitude(da_chunked.compute())
da_corrected_deriv = differentiate_along_longitude(da_corrected.compute())
quickplot(
da_deriv,
fig.add_map(0, 0),
title="Original {default_title}",
error=True,
vrange=(-10, 10),
)
quickplot(
da_corrected_deriv,
fig.add_map(0, 1),
title="Safeguarded(0), chunked",
cr=da.nbytes / da_correction_nbytes,
error=True,
vrange=(-10, 10),
)
quickplot(
((da_corrected_deriv - da_deriv) / da_deriv).assign_attrs(
long_name="relative error", units="%"
),
fig.add_map(0, 2),
error=True,
title="Relative Compression Error",
)
fig.show()
Tyree, J., Köhler, D., Underwood, R., Bouvier, C., Reichelt, T., Järvinen, H., & Klöwer, M. (2026). Compression Safeguards - Towards Safe, Trusted, and Fearless Lossy Compression of Earth Science Data. EGU General Assembly 2026. Available from: Tyree et al. (2026).
Inspired by: Fallin, A., Gorski, N., Agarwal, T., Wang, B., Gopalakrishnan, G., & Burtscher, M. (2026). Fast Topology-Aware Lossy Data Compression with Full Preservation of Critical Points and Local Order. arXiv. Available from: Fallin et al. (2026).
- Tyree, J., Köhler, D., Underwood, R., Bouvier, C., Reichelt, T., Järvinen, H., & Klöwer, M. (2026). Compression Safeguards - Towards Safe and Fearless Lossy Compression of Earth System Data. 10.5194/egusphere-egu26-9673
- Fallin, A., Gorski, N., Agarwal, T., Wang, B., Gopalakrishnan, G., & Burtscher, M. (2026). Fast Topology-Aware Lossy Data Compression with Full Preservation of Critical Points and Local Order. arXiv. 10.48550/ARXIV.2603.26968