Preserving (non-NaN) missing values with safeguards¶
In this example, we compare how three lossy compressors (ZFP, SZ3, and SPERR) handle missing values that are not encoded as NaNs. We compress a single time step of the energy release component of the fire danger index. The data is on an unstructured grid and contains missing values over sea. We then apply safeguards to guarantee that special missing values are preserved. Next, we show how a masking meta-compressor can also be used to preserve missing values. We also compare the safeguards with the compressor configuration auto-tuner OptZConfig.
Missing values are often encoded in GRIB files as the value 9999. Since tools like cfgrib hide this detail and replace missing values with NaNs when loading the data, we manually read in the GRIB file using eccodes to observe the non-NaN missing values.
import ssl
ssl._create_default_https_context = ssl._create_stdlib_context
import copy
from pathlib import Path
import earthkit.plots
import eccodes
import humanize
import matplotlib as mpl
import numpy as np
import pandas as pd
from earthkit.plots.resample import Interpolate
from matplotlib import patheffects as PathEffects
from matplotlib import pyplot as plt
from numcodecs_safeguards import SafeguardedCodec
from numcodecs_wasm_pressio import Pressio
from numcodecs_wasm_sperr import Sperr
from numcodecs_wasm_sz3 import Sz3
from numcodecs_wasm_zfp import Zfp
from numcodecs_wasm_zstd import Zstd
from numcodecs_zero import ZeroCodec
data = Path("data") / "cems-ercnfdr" / "data.grib"
with data.open("rb") as f:
gid = eccodes.codes_grib_new_from_file(f)
grib_keys = []
it = eccodes.codes_keys_iterator_new(gid)
while eccodes.codes_keys_iterator_next(it):
grib_keys.append(eccodes.codes_keys_iterator_get_name(it))
eccodes.codes_keys_iterator_delete(it)
eccodes.codes_release(gid)
with data.open("rb") as f:
gid = eccodes.codes_grib_new_from_file(f)
(long_name,) = eccodes.codes_get_array(gid, "name", ktype=str)
(missing_value,) = eccodes.codes_get_array(gid, "missingValue")
latitudes = eccodes.codes_get_array(gid, "latitudes")
longitudes = eccodes.codes_get_array(gid, "longitudes")
ercnfdr = eccodes.codes_get_array(gid, "values")
eccodes.codes_release(gid)
missing_value
np.int64(9999)
def compute_corrections_percentage(
my_ercnfdr: np.ndarray, orig_ercnfdr: np.ndarray
) -> float:
return np.mean(
~(
(my_ercnfdr == orig_ercnfdr)
| (np.isnan(my_ercnfdr) & np.isnan(orig_ercnfdr))
)
)
import observe
observations = []
Lossless compression¶
We first compress the data losslessly with ZStandard at level 22, which gives maximum compression, to provide a baseline.
zstd = Zstd(level=22)
with observe.observe(zstd, observations):
ercnfdr_zstd_enc = zstd.encode(ercnfdr)
ercnfdr_zstd = zstd.decode(ercnfdr_zstd_enc)
ercnfdr_zstd_cr = ercnfdr.nbytes / ercnfdr_zstd_enc.nbytes
Compressing energy release with lossy compressors and safeguards¶
We configure each compressor with an absolute error bound of 1, the step size between the discrete metric values.
eb_abs = 1
zfp = Zfp(mode="fixed-accuracy", tolerance=eb_abs)
with observe.observe(zfp, observations):
ercnfdr_zfp_enc = zfp.encode(ercnfdr)
ercnfdr_zfp = zfp.decode(ercnfdr_zfp_enc)
ercnfdr_zfp_cr = ercnfdr.nbytes / ercnfdr_zfp_enc.nbytes
sz3 = Sz3(eb_mode="abs", eb_abs=eb_abs)
with observe.observe(sz3, observations):
ercnfdr_sz3_enc = sz3.encode(ercnfdr)
ercnfdr_sz3 = sz3.decode(ercnfdr_sz3_enc)
ercnfdr_sz3_cr = ercnfdr.nbytes / ercnfdr_sz3_enc.nbytes
sperr = Sperr(mode="pwe", pwe=eb_abs)
with observe.observe(sperr, observations):
ercnfdr_sperr_enc = sperr.encode(ercnfdr)
ercnfdr_sperr = sperr.decode(ercnfdr_sperr_enc)
ercnfdr_sperr_cr = ercnfdr.nbytes / ercnfdr_sperr_enc.nbytes
zero = ZeroCodec()
with observe.observe(zero, observations):
ercnfdr_zero_enc = zero.encode(ercnfdr)
ercnfdr_zero = zero.decode(ercnfdr_zero_enc)
Compressing energy release using the safeguarded lossy compressors¶
We configure the safeguards to preserve the special missing value, so that missing values are preserved and no non-missing values take on the special value, and to preserve an absolute error bound of 1.
ercnfdr_sg = dict()
ercnfdr_sg_cr = dict()
for codec in [
zero,
zfp,
sz3,
sperr,
]:
sg = SafeguardedCodec(
codec=codec,
safeguards=[
dict(kind="eb", type="abs", eb=eb_abs),
dict(kind="same", value="missing_value", exclusive=True),
],
fixed_constants=dict(missing_value=missing_value),
)
with observe.observe(sg, observations):
ercnfdr_sg_enc = sg.encode(ercnfdr)
ercnfdr_sg[codec.codec_id] = sg.decode(ercnfdr_sg_enc)
ercnfdr_sg_cr[codec.codec_id] = ercnfdr.nbytes / np.asarray(ercnfdr_sg_enc).nbytes
ercnfdr_sg_lossless = dict()
ercnfdr_sg_lossless_cr = dict()
for codec in [
zero,
zfp,
sz3,
sperr,
]:
sg = SafeguardedCodec(
codec=codec,
safeguards=[
dict(kind="eb", type="abs", eb=eb_abs),
dict(kind="same", value="missing_value", exclusive=True),
],
fixed_constants=dict(missing_value=missing_value),
# produce lossless corrections and refine them with iteration
compute=dict(unstable_iterative=True, unstable_lossless_corrections=True),
)
with observe.observe(sg, observations):
ercnfdr_sg_lossless_enc = sg.encode(ercnfdr)
ercnfdr_sg_lossless[codec.codec_id] = sg.decode(ercnfdr_sg_lossless_enc)
ercnfdr_sg_lossless_cr[codec.codec_id] = (
ercnfdr.nbytes / np.asarray(ercnfdr_sg_lossless_enc).nbytes
)
Compressing energy release with masked lossy compressors¶
Missing values in the data can also be preserved using meta-compressors that store the location of the missing values to restore them during decompression. Such a metacompressor is implemented in libpressio as the mask_interpolation_compressor_plugin and in the numcodecs_mask.MaskMetaCodec.
from numcodecs.packbits import PackBits
from numcodecs_combinators.stack import CodecStack
from numcodecs_mask import MaskMetaCodec
ercnfdr_mask = dict()
ercnfdr_mask_cr = dict()
for codec in [
zfp,
sz3,
sperr,
]:
mask = MaskMetaCodec(
mask=int(missing_value),
codec=codec,
# bitpack the mask and compress it with default Zstd
bitmap_codec=CodecStack(PackBits(), Zstd(level=3)),
)
with observe.observe(mask, observations):
ercnfdr_mask_enc = mask.encode(ercnfdr)
ercnfdr_mask[codec.codec_id] = mask.decode(ercnfdr_mask_enc)
ercnfdr_mask_cr[codec.codec_id] = (
ercnfdr.nbytes / np.asarray(ercnfdr_mask_enc).nbytes
)
ercnfdr_mask_sg = dict()
ercnfdr_mask_sg_cr = dict()
for codec in [
zfp,
sz3,
sperr,
]:
mask_sg = SafeguardedCodec(
codec=MaskMetaCodec(
mask=int(missing_value),
codec=codec,
# bitpack the mask and compress it with default Zstd
bitmap_codec=CodecStack(PackBits(), Zstd(level=3)),
),
safeguards=[
dict(kind="eb", type="abs", eb=eb_abs),
dict(kind="same", value="missing_value", exclusive=True),
],
fixed_constants=dict(missing_value=missing_value),
)
with observe.observe(mask_sg, observations):
ercnfdr_mask_sg_enc = mask_sg.encode(ercnfdr)
ercnfdr_mask_sg[codec.codec_id] = mask_sg.decode(ercnfdr_mask_sg_enc)
ercnfdr_mask_sg_cr[codec.codec_id] = (
ercnfdr.nbytes / np.asarray(ercnfdr_mask_sg_enc).nbytes
)
Compressing energy release with OptZConfig¶
We configure OptZConfig with a custom safety violations metric, implemented in Python, that computes the percentage of violations $V$. We then maximise the score
$$ \textrm{score} = \begin{cases} -\textrm{V} \quad &\text{if } \textrm{V} > 0 \\ \textrm{CR} \quad &\text{otherwise} \end{cases} $$
using the FRAZ search algorithm with 25 iterations, where CR is the achieved compression ratio. Since FRAZ seems to struggle with finding sufficient absolute error bounds spread across several orders of magnitude, we search for bounds in logarithmic space by wrapping each codec in an Exponential<CODEC> meta-codec.
import numcodecs
class SafetyViolationsMetric(numcodecs.abc.Codec):
codec_id = "safety-violations-metric"
def __init__(self):
self._data = None
def encode(self, buf):
# store the original data for later
self._data = np.array(buf, copy=True)
# return no metric
return np.empty(0, dtype=np.float64)
def decode(self, buf, out=None):
# compute the violations
data_nan = np.where(self._data == missing_value, np.nan, self._data)
buf_nan = np.where(buf == missing_value, np.nan, buf)
violations = np.mean(
~(
(np.abs(buf_nan - data_nan) <= eb_abs)
| (np.isnan(buf_nan) & np.isnan(data_nan))
)
)
self._data = None
# return the violations score metric
return numcodecs.compat.ndarray_copy(np.float64(violations), out)
numcodecs.registry.register_codec(SafetyViolationsMetric)
class ExponentialZfp(Zfp):
codec_id = "e-zfp.rs"
def __new__(cls, tolerance: float, **kwargs):
codec = super().__new__(cls, tolerance=np.exp(tolerance), **kwargs)
codec._tolerance = tolerance
return codec
def get_config(self):
return {
**super().get_config(),
"id": type(self).codec_id,
"tolerance": self._tolerance,
}
class ExponentialSz3(Sz3):
codec_id = "e-sz3.rs"
def __new__(cls, eb_abs: float, **kwargs):
codec = super().__new__(cls, eb_abs=np.exp(eb_abs), **kwargs)
codec._eb_abs = eb_abs
return codec
def get_config(self):
return {
**super().get_config(),
"id": type(self).codec_id,
"eb_abs": self._eb_abs,
}
class ExponentialSperr(Sperr):
codec_id = "e-sperr.rs"
def __new__(cls, pwe: float, **kwargs):
codec = super().__new__(cls, pwe=np.exp(pwe), **kwargs)
codec._pwe = pwe
return codec
def get_config(self):
return {**super().get_config(), "id": type(self).codec_id, "pwe": self._pwe}
numcodecs.registry.register_codec(ExponentialZfp)
numcodecs.registry.register_codec(ExponentialSz3)
numcodecs.registry.register_codec(ExponentialSperr)
ercnfdr_optzconfig = dict()
ercnfdr_optzconfig_cr = dict()
for codec, parameter, lower_bound in [
(zfp, "tolerance", 1e-4), # decent guess
(sz3, "eb_abs", 1e-4), # decent guess
(sperr, "pwe", 1e-12), # decent guess
]:
optzconfig = Pressio(
compressor_id="opt",
compressor_config={
"opt:output": ["composite:score"],
"opt:inputs": [f"numcodecs.rs:{parameter}"],
"opt:lower_bound": np.log(lower_bound),
"opt:upper_bound": np.log(eb_abs),
"opt:max_iterations": 25,
"opt:objective_mode_name": "max",
},
early_config={
"opt:compressor": "pressio",
"pressio:compressor": "numcodecs.rs",
**{
f"numcodecs.rs:{k}": f"e-{v}" if k == "id" else v
for k, v in codec.get_config().items()
},
"opt:search": "fraz",
"pressio:metric": "composite",
"composite:plugins": ["size", "numcodecs.rs-metric"],
"composite:scripts": [
"""
violations = metrics["numcodecs.rs-metric:decompression"]
if violations > 0 then
return "score", -violations
else
return "score", metrics["size:compression_ratio"]
end
"""
],
"numcodecs.rs-metric:id": "safety-violations-metric",
},
)
with observe.observe(optzconfig, observations):
ercnfdr_optzconfig_enc = optzconfig.encode(ercnfdr)
ercnfdr_optzconfig[codec.codec_id] = optzconfig.decode(ercnfdr_optzconfig_enc)
ercnfdr_optzconfig_cr[codec.codec_id] = (
ercnfdr.nbytes / np.asarray(ercnfdr_optzconfig_enc).nbytes
)
rank={0,1,} iter={0} input={-4.60517,} output={4.496,} objective={4.496}
rank={0,1,} iter={1} input={-7.06182,} output={3.8708,} objective={3.8708}
rank={0,1,} iter={2} input={-2.18607,} output={5.11573,} objective={5.11573}
rank={0,1,} iter={3} input={0,} output={-0.0184899,} objective={-0.0184899}
rank={0,1,} iter={4} input={-9.20755,} output={3.50522,} objective={3.50522}
rank={0,1,} iter={5} input={-3.16842,} output={4.89099,} objective={4.89099}
rank={0,1,} iter={6} input={-5.70006,} output={4.16004,} objective={4.16004}
rank={0,1,} iter={7} input={-2.53616,} output={5.11573,} objective={5.11573}
rank={0,1,} iter={8} input={-3.80305,} output={4.68521,} objective={4.68521}
rank={0,1,} iter={9} input={-2.36112,} output={5.11573,} objective={5.11573}
rank={0,1,} iter={10} input={-8.05593,} output={3.74074,} objective={3.74074}
rank={0,1,} iter={11} input={-2.80431,} output={4.89099,} objective={4.89099}
rank={0,1,} iter={12} input={-6.3189,} output={4.01023,} objective={4.01023}
rank={0,1,} iter={13} input={-5.08077,} output={4.32152,} objective={4.32152}
rank={0,1,} iter={14} input={-3.43939,} output={4.89099,} objective={4.89099}
rank={0,1,} iter={15} input={-4.16385,} output={4.496,} objective={4.496}
rank={0,1,} iter={16} input={-2.27369,} output={5.11573,} objective={5.11573}
rank={0,1,} iter={17} input={-2.62146,} output={5.11573,} objective={5.11573}
rank={0,1,} iter={18} input={-2.44808,} output={5.11573,} objective={5.11573}
rank={0,1,} iter={19} input={-2.98829,} output={4.89099,} objective={4.89099}
rank={0,1,} iter={20} input={-2.49227,} output={5.11573,} objective={5.11573}
rank={0,1,} iter={21} input={-2.23002,} output={5.11573,} objective={5.11573}
rank={0,1,} iter={22} input={-2.40451,} output={5.11573,} objective={5.11573}
rank={0,1,} iter={23} input={-2.57823,} output={5.11573,} objective={5.11573}
rank={0,1,} iter={24} input={-2.3168,} output={5.11573,} objective={5.11573}
final_iter={25} inputs={-2.18607,} output={5.11573,}
rank={0,1,} iter={0} input={-4.60517,} output={57.2305,} objective={57.2305}
rank={0,1,} iter={1} input={-7.06182,} output={49.381,} objective={49.381}
rank={0,1,} iter={2} input={-2.18607,} output={49.8321,} objective={49.8321}
rank={0,1,} iter={3} input={-4.55271,} output={50.0362,} objective={50.0362}
rank={0,1,} iter={4} input={-6.93899,} output={50.0484,} objective={50.0484}
rank={0,1,} iter={5} input={-0.00161371,} output={-0.705348,} objective={-0.705348}
rank={0,1,} iter={6} input={-5.74589,} output={50.094,} objective={50.094}
rank={0,1,} iter={7} input={-9.20686,} output={43.8669,} objective={43.8669}
rank={0,1,} iter={8} input={-5.1495,} output={49.9377,} objective={49.9377}
rank={0,1,} iter={9} input={-3.37332,} output={49.6433,} objective={49.6433}
rank={0,1,} iter={10} input={-4.85078,} output={51.3479,} objective={51.3479}
rank={0,1,} iter={11} input={-8.08266,} output={49.0892,} objective={49.0892}
rank={0,1,} iter={12} input={-4.70582,} output={49.682,} objective={49.682}
rank={0,1,} iter={13} input={-1.44973,} output={50.9061,} objective={50.9061}
rank={0,1,} iter={14} input={-4.62843,} output={50.0374,} objective={50.0374}
rank={0,1,} iter={15} input={-6.34249,} output={50.2653,} objective={50.2653}
rank={0,1,} iter={16} input={-4.59057,} output={50.1172,} objective={50.1172}
rank={0,1,} iter={17} input={-2.78186,} output={51.5671,} objective={51.5671}
rank={0,1,} iter={18} input={-4.60945,} output={50.3447,} objective={50.3447}
rank={0,1,} iter={19} input={-3.96564,} output={49.4531,} objective={49.4531}
rank={0,1,} iter={20} input={-4.60007,} output={50.1508,} objective={50.1508}
rank={0,1,} iter={21} input={-8.61098,} output={46.2284,} objective={46.2284}
rank={0,1,} iter={22} input={-4.60479,} output={54.7881,} objective={54.7881}
rank={0,1,} iter={23} input={-7.57029,} output={49.3844,} objective={49.3844}
rank={0,1,} iter={24} input={-4.60684,} output={50.5807,} objective={50.5807}
final_iter={25} inputs={-4.60517,} output={57.2305,}
rank={0,1,} iter={0} input={-13.8155,} output={-0.503931,} objective={-0.503931}
rank={0,1,} iter={1} input={-21.1855,} output={-0.50281,} objective={-0.50281}
rank={0,1,} iter={2} input={-6.55821,} output={-0.503931,} objective={-0.503931}
rank={0,1,} iter={3} input={-21.7845,} output={-0.50147,} objective={-0.50147}
rank={0,1,} iter={4} input={-1.88648,} output={-0.503931,} objective={-0.503931}
rank={0,1,} iter={5} input={-25.7671,} output={-0.393894,} objective={-0.393894}
rank={0,1,} iter={6} input={-2.75189,} output={-0.503931,} objective={-0.503931}
rank={0,1,} iter={7} input={-2.20811,} output={-0.503931,} objective={-0.503931}
rank={0,1,} iter={8} input={-14.0812,} output={-0.503931,} objective={-0.503931}
rank={0,1,} iter={9} input={-23.2414,} output={-0.490516,} objective={-0.490516}
rank={0,1,} iter={10} input={-5.2064,} output={-0.503931,} objective={-0.503931}
rank={0,1,} iter={11} input={-13.9117,} output={-0.503931,} objective={-0.503931}
rank={0,1,} iter={12} input={-23.5603,} output={-0.492809,} objective={-0.492809}
rank={0,1,} iter={13} input={-4.43149,} output={-0.503931,} objective={-0.503931}
rank={0,1,} iter={14} input={-25.3187,} output={-0.436805,} objective={-0.436805}
rank={0,1,} iter={15} input={-21.3734,} output={-0.503429,} objective={-0.503429}
rank={0,1,} iter={16} input={-17.6174,} output={-0.503931,} objective={-0.503931}
rank={0,1,} iter={17} input={-0.375093,} output={-0.503931,} objective={-0.503931}
rank={0,1,} iter={18} input={-13.3532,} output={-0.503931,} objective={-0.503931}
rank={0,1,} iter={19} input={-2.37466,} output={-0.503931,} objective={-0.503931}
rank={0,1,} iter={20} input={-27.4076,} output={-0.00373008,} objective={-0.00373008}
rank={0,1,} iter={21} input={-23.1945,} output={-0.495842,} objective={-0.495842}
rank={0,1,} iter={22} input={-5.1187,} output={-0.503931,} objective={-0.503931}
rank={0,1,} iter={23} input={-25.3987,} output={-0.417429,} objective={-0.417429}
rank={0,1,} iter={24} input={-0.821889,} output={-0.503931,} objective={-0.503931}
final_iter={25} inputs={-13.8155,} output={-0.503931,}
Visual comparison of the decompressed missing values¶
Since ZFP, SZ3, and SPERR have no knowledge of the special missing value, the large magnitude of 9999 bleeds into surrounding grid cells and some missing values have large non-missing-value values.
old_scale_dashes = mpl.lines._scale_dashes
def scale_dashes(offset, dashes, lw):
if lw == 0:
return offset, dashes
return old_scale_dashes(offset, dashes, lw)
mpl.lines._scale_dashes = scale_dashes
def plot_ercnfdr(
my_ercnfdr: np.ndarray,
cr,
chart,
title,
eb_abs,
error=False,
corr=None,
my_ercnfdr_mask=None,
cr_mask=None,
inset=True,
):
x = longitudes
y = latitudes
z = np.where(my_ercnfdr == missing_value, np.nan, my_ercnfdr)
source = earthkit.plots.sources.get_source(x=x, y=y, z=z)
style = copy.deepcopy(earthkit.plots.styles.auto.guess_style(source))
style._levels = earthkit.plots.styles.levels.Levels(np.linspace(-1, 81, 22))
style._legend_kwargs["ticks"] = np.linspace(-1, 81, 5)
extend_left = np.nanmin(z) < -1
extend_right = np.nanmax(z) > 81
extend = {
(False, False): "neither",
(True, False): "min",
(False, True): "max",
(True, True): "both",
}[(extend_left, extend_right)]
style._legend_kwargs["extend"] = extend
interpolate = Interpolate()
x, y, z = interpolate.apply(
x=x,
y=y,
z=z,
source_crs=source.crs,
target_crs=chart.crs,
)
ercnfdr_x = longitudes
ercnfdr_y = latitudes
ercnfdr_z = np.where(ercnfdr == missing_value, np.nan, ercnfdr)
ercnfdr_x, ercnfdr_y, ercnfdr_z = interpolate.apply(
x=ercnfdr_x,
y=ercnfdr_y,
z=ercnfdr_z,
source_crs=source.crs,
target_crs=chart.crs,
)
chart.ax.set_global()
chart.ax.fill_between(
[0, 1],
[1, 1],
hatch="X",
edgecolor="white",
facecolor="lightgrey",
transform=chart.ax.transAxes,
zorder=-12,
)
chart.pcolormesh(
x=x,
y=y,
z=z,
style=style,
norm=mpl.colors.BoundaryNorm(np.linspace(-1, 81, 22), ncolors=21, clip=True),
zorder=-11,
rasterized=True,
)
with plt.rc_context(
{
"hatch.color": "black",
"hatch.linewidth": 2,
}
):
chart.contourf(
x=ercnfdr_x,
y=ercnfdr_y,
z=np.isnan(ercnfdr_z) & ~np.isnan(z),
colors=["none"],
levels=[-0.5, 0.9, 1.5],
hatches=[None, "X"],
legend_style=None,
zorder=-10,
)
with plt.rc_context(
{
"hatch.color": "white",
"hatch.linewidth": 1,
}
):
chart.contourf(
x=ercnfdr_x,
y=ercnfdr_y,
z=np.isnan(ercnfdr_z) & ~np.isnan(z),
colors=["none"],
levels=[-0.5, 0.9, 1.5],
hatches=[None, "X"],
legend_style=None,
zorder=-10,
)
if error:
if corr is not None:
old_process_projection_requirements = (
chart.ax.get_figure()._process_projection_requirements
)
def _process_projection_requirements(
*, axes_class=None, polar=False, projection=None, **kwargs
):
if axes_class is not None and projection is not None:
return axes_class, dict(projection=projection, **kwargs)
return old_process_projection_requirements(
axes_class=axes_class, polar=polar, projection=projection, **kwargs
)
chart.ax.get_figure()._process_projection_requirements = (
_process_projection_requirements
)
axin = chart.ax.inset_axes(
[0.025, 0.05, 1 / 3, 1 / 3],
xticklabels=[],
yticklabels=[],
axes_class=type(chart.ax),
projection=chart.ax.projection,
)
axin.set_global()
axin.scatter(
longitudes,
latitudes,
s=1,
c=~(my_ercnfdr == corr),
cmap=mpl.colors.ListedColormap(["white", "green", "lawngreen"]),
vmin=0,
vmax=2,
rasterized=True,
)
axin.coastlines(color="#555555")
axin.spines["geo"].set_edgecolor("black")
axin.set_title(
"Corrections",
path_effects=[PathEffects.withStroke(linewidth=3, foreground="white")],
)
elif inset:
old_process_projection_requirements = (
chart.ax.get_figure()._process_projection_requirements
)
def _process_projection_requirements(
*, axes_class=None, polar=False, projection=None, **kwargs
):
if axes_class is not None and projection is not None:
return axes_class, dict(projection=projection, **kwargs)
return old_process_projection_requirements(
axes_class=axes_class, polar=polar, projection=projection, **kwargs
)
chart.ax.get_figure()._process_projection_requirements = (
_process_projection_requirements
)
axin = chart.ax.inset_axes(
[0.025, 0.05, 1 / 3, 1 / 3],
xticklabels=[],
yticklabels=[],
axes_class=type(chart.ax),
projection=chart.ax.projection,
)
axin.set_global()
axin.scatter(
longitudes,
latitudes,
s=1,
c=~(
(np.abs(my_ercnfdr - ercnfdr) <= eb_abs)
& ((my_ercnfdr == missing_value) == (ercnfdr == missing_value))
),
cmap=mpl.colors.ListedColormap(["white", "red"]),
vmin=0,
vmax=1,
rasterized=True,
)
axin.coastlines(color="#555555")
axin.spines["geo"].set_edgecolor("black")
axin.set_title(
"Violations",
path_effects=[PathEffects.withStroke(linewidth=3, foreground="white")],
)
chart.title(title)
if error:
my_ercnfdr_nan = np.where(my_ercnfdr == missing_value, np.nan, my_ercnfdr)
ercnfdr_nan = np.where(ercnfdr == missing_value, np.nan, ercnfdr)
err_v = np.mean(
~(
(np.abs(my_ercnfdr_nan - ercnfdr_nan) <= eb_abs)
| (np.isnan(my_ercnfdr_nan) & np.isnan(ercnfdr_nan))
)
)
err_v = (
0
if err_v == 0
else np.format_float_positional(100 * err_v, precision=1, min_digits=1)
+ "%"
)
if err_v == "0.0%":
err_v = "<0.05%"
if my_ercnfdr_mask is not None:
my_ercnfdr_mask_nan = np.where(
my_ercnfdr_mask == missing_value, np.nan, my_ercnfdr_mask
)
err_mask_v = np.mean(
~(
(np.abs(my_ercnfdr_mask_nan - ercnfdr_nan) <= eb_abs)
| (np.isnan(my_ercnfdr_mask_nan) & np.isnan(ercnfdr_nan))
)
)
err_mask_v = (
0
if err_mask_v == 0
else np.format_float_positional(
100 * err_mask_v, precision=1, min_digits=1
)
+ "%"
)
if err_mask_v == "0.0%":
err_mask_v = "<0.05%"
t = chart.ax.text(
0.95,
0.1,
f"V={err_v}" + ("" if my_ercnfdr_mask is None else f" ({err_mask_v})"),
ha="right",
va="bottom",
transform=chart.ax.transAxes,
)
t.set_bbox(dict(facecolor="white", alpha=0.75, edgecolor="black"))
t = chart.ax.text(
0.95,
0.9,
rf"$\times$ {np.round(cr, 2)}"
+ ("" if cr_mask is None else rf" ($\times$ {np.round(cr_mask, 2)})")
if error
else humanize.naturalsize(ercnfdr.nbytes, binary=True),
ha="right",
va="top",
transform=chart.ax.transAxes,
)
t.set_bbox(dict(facecolor="white", alpha=0.75, edgecolor="black"))
for m in earthkit.plots.schemas.schema.quickmap_subplot_workflow:
if m != "title":
getattr(chart, m)()
for m in earthkit.plots.schemas.schema.quickmap_figure_workflow:
if m != "legend":
getattr(chart, m)()
chart.legend(
label=long_name.split(" (")[0] if error else long_name.replace(" (", "\n(")
)
counts, bins = np.histogram(z.flatten(), range=(-1, 81), bins=21)
midpoints = bins[:-1] + np.diff(bins) / 2
cb = chart.ax.collections[1].colorbar
extend_width = (bins[-1] - bins[-2]) / (bins[-1] - bins[0])
cax = cb.ax.inset_axes(
[
0.0 - extend_width * extend_left,
1.25,
1.0 + extend_width * (0 + extend_left + extend_right + 2),
1.0,
]
)
cax.bar(
midpoints,
height=counts,
width=(bins[-1] - bins[0]) / len(counts),
color=cb.cmap(cb.norm(midpoints)),
)
if extend_left:
cax.bar(
bins[0] - (bins[1] - bins[0]) / 2,
height=np.sum(z < -1),
width=(bins[-1] - bins[0]) / len(counts),
color=cb.cmap(cb.norm(midpoints[0])),
)
if extend_right:
cax.bar(
bins[-1] + (bins[-1] - bins[-2]) / 2,
height=np.sum(z > 81),
width=(bins[-1] - bins[0]) / len(counts),
color=cb.cmap(cb.norm(midpoints[-1])),
)
cax.bar(
bins[-1] + (bins[-1] - bins[-2]) * (extend_right * 2 + 2 + 1) / 2,
height=np.sum(np.isnan(z)),
width=(bins[-1] - bins[0]) / len(counts),
color="lightgrey",
edgecolor="white",
lw=0,
hatch="XXXX",
)
q1, q2, q3 = np.quantile(z.flatten(), [0.25, 0.5, 0.75])
cax.axvline(z.mean().item(), ls=":", ymin=0.1, ymax=0.9, c="w", lw=2)
cax.axvline(q1, ymin=0.25, ymax=0.75, c="w", lw=2)
cax.axvline(q2, ymin=0.1, ymax=0.9, c="w", lw=2)
cax.axvline(q3, ymin=0.25, ymax=0.75, c="w", lw=2)
cax.axvline(z.mean().item(), ymin=0.1, ymax=0.9, ls=":", c="k", lw=1)
cax.axvline(q1, ymin=0.25, ymax=0.75, c="k", lw=1)
cax.axvline(q2, ymin=0.1, ymax=0.9, c="k", lw=1)
cax.axvline(q3, ymin=0.25, ymax=0.75, c="k", lw=1)
cax.set_xlim(
-1 - (bins[-1] - bins[-2]) * extend_left,
81 + (bins[-1] - bins[-2]) * (extend_right + 2),
)
cax.set_xticks([])
cax.set_yticks([])
cax.spines[:].set_visible(False)
def table_ercnfdr(
my_ercnfdr: np.ndarray,
cr,
title,
eb_abs,
corr=None,
):
err_inf = np.amax(np.abs(my_ercnfdr - ercnfdr))
err_2 = np.sqrt(np.mean(np.square(my_ercnfdr - ercnfdr)))
my_ercnfdr_nan = np.where(my_ercnfdr == missing_value, np.nan, my_ercnfdr)
ercnfdr_nan = np.where(ercnfdr == missing_value, np.nan, ercnfdr)
err_v = np.mean(
~(
(np.abs(my_ercnfdr_nan - ercnfdr_nan) <= eb_abs)
| (np.isnan(my_ercnfdr_nan) & np.isnan(ercnfdr_nan))
)
)
err_v = (
0
if err_v == 0
else np.format_float_positional(100 * err_v, precision=1, min_digits=1) + "%"
)
if err_v == "0.0%":
err_v = "<0.05%"
corr = None if corr is None else compute_corrections_percentage(my_ercnfdr, corr)
corr = (
""
if corr is None
else (
0
if corr == 0
else np.format_float_positional(100 * corr, precision=1, min_digits=1) + "%"
)
)
if corr == "0.0%":
corr = "<0.05%"
return pd.DataFrame(
{
"Compressor": [title[0]],
"Meta": [title[1]],
"Safeguarded": [title[2]],
"Corrections": [title[3]],
r"$L_{\infty}$": [
f"{err_inf:.02}",
],
r"$L_{2}$": [
f"{err_2:.02}",
],
"V": [err_v],
"C": [corr],
"CR": [
rf"$\times$ {np.round(cr, 2)}",
],
}
)
fig = earthkit.plots.Figure(
size=(10, 23),
rows=6,
columns=2,
)
plot_ercnfdr(
ercnfdr,
1.0,
fig.add_map(0, 0),
"Original",
eb_abs=eb_abs,
)
plot_ercnfdr(
ercnfdr_zfp,
ercnfdr_zfp_cr,
fig.add_map(1, 0),
r"ZFP($\epsilon_{{abs}}$)",
eb_abs=eb_abs,
error=True,
my_ercnfdr_mask=ercnfdr_mask["zfp.rs"],
cr_mask=ercnfdr_mask_cr["zfp.rs"],
)
plot_ercnfdr(
ercnfdr_sz3,
ercnfdr_sz3_cr,
fig.add_map(2, 0),
r"SZ3($\epsilon_{{abs}}$)",
eb_abs=eb_abs,
error=True,
my_ercnfdr_mask=ercnfdr_mask["sz3.rs"],
cr_mask=ercnfdr_mask_cr["sz3.rs"],
)
plot_ercnfdr(
ercnfdr_sperr,
ercnfdr_sperr_cr,
fig.add_map(3, 0),
r"SPERR($\epsilon_{{abs}}$)",
eb_abs=eb_abs,
error=True,
my_ercnfdr_mask=ercnfdr_mask["sperr.rs"],
cr_mask=ercnfdr_mask_cr["sperr.rs"],
)
plot_ercnfdr(
ercnfdr_sg["zero"],
ercnfdr_sg_cr["zero"],
fig.add_map(0, 1),
rf"Safeguarded(0, $\epsilon_{{{{abs}}}} \cup \overset{{{{{missing_value}}}}}{{{{\leftrightarrow}}}}$ )",
eb_abs=eb_abs,
error=True,
corr=ercnfdr_zero,
)
plot_ercnfdr(
ercnfdr_sg["zfp.rs"],
ercnfdr_sg_cr["zfp.rs"],
fig.add_map(1, 1),
rf"Safeguarded(ZFP, $\epsilon_{{{{abs}}}} \cup \overset{{{{{missing_value}}}}}{{{{\leftrightarrow}}}}$ )",
eb_abs=eb_abs,
error=True,
corr=ercnfdr_zfp,
my_ercnfdr_mask=ercnfdr_mask_sg["zfp.rs"],
cr_mask=ercnfdr_mask_sg_cr["zfp.rs"],
)
plot_ercnfdr(
ercnfdr_sg["sz3.rs"],
ercnfdr_sg_cr["sz3.rs"],
fig.add_map(2, 1),
rf"Safeguarded(SZ3, $\epsilon_{{{{abs}}}} \cup \overset{{{{{missing_value}}}}}{{{{\leftrightarrow}}}}$ )",
eb_abs=eb_abs,
error=True,
corr=ercnfdr_sz3,
my_ercnfdr_mask=ercnfdr_mask_sg["sz3.rs"],
cr_mask=ercnfdr_mask_sg_cr["sz3.rs"],
)
plot_ercnfdr(
ercnfdr_sg["sperr.rs"],
ercnfdr_sg_cr["sperr.rs"],
fig.add_map(3, 1),
rf"Safeguarded(SPERR, $\epsilon_{{{{abs}}}} \cup \overset{{{{{missing_value}}}}}{{{{\leftrightarrow}}}}$ )",
eb_abs=eb_abs,
error=True,
corr=ercnfdr_sperr,
my_ercnfdr_mask=ercnfdr_mask_sg["sperr.rs"],
cr_mask=ercnfdr_mask_sg_cr["sperr.rs"],
)
plot_ercnfdr(
ercnfdr_optzconfig["zfp.rs"],
ercnfdr_optzconfig_cr["zfp.rs"],
fig.add_map(4, 0),
rf"OptZConfig(ZFP, $\epsilon_{{{{abs}}}} \cup \overset{{{{{missing_value}}}}}{{{{\leftrightarrow}}}}$ )",
eb_abs=eb_abs,
error=True,
inset=False,
)
plot_ercnfdr(
ercnfdr_optzconfig["sz3.rs"],
ercnfdr_optzconfig_cr["sz3.rs"],
fig.add_map(4, 1),
rf"OptZConfig(SZ3, $\epsilon_{{{{abs}}}} \cup \overset{{{{{missing_value}}}}}{{{{\leftrightarrow}}}}$ )",
eb_abs=eb_abs,
error=True,
inset=False,
)
plot_ercnfdr(
ercnfdr_optzconfig["sperr.rs"],
ercnfdr_optzconfig_cr["sperr.rs"],
fig.add_map(5, 0),
rf"OptZConfig(SPERR, $\epsilon_{{{{abs}}}} \cup \overset{{{{{missing_value}}}}}{{{{\leftrightarrow}}}}$ )",
eb_abs=eb_abs,
error=True,
inset=True,
)
fig.save(Path("plots") / "missing.pdf")
ercnfdr_table = pd.concat(
[
table_ercnfdr(
ercnfdr_sg_lossless["zero"],
ercnfdr_sg_lossless_cr["zero"],
[
"0",
"-",
rf"$\epsilon_{{abs}} \cup \overset{{{missing_value}}}{{\leftrightarrow}}$",
"lossless",
],
eb_abs=eb_abs,
corr=ercnfdr_zero,
),
table_ercnfdr(
ercnfdr_sg["zero"],
ercnfdr_sg_cr["zero"],
[
"0",
"-",
rf"$\epsilon_{{abs}} \cup \overset{{{missing_value}}}{{\leftrightarrow}}$",
"one-shot",
],
eb_abs=eb_abs,
corr=ercnfdr_zero,
),
table_ercnfdr(
ercnfdr_zfp,
ercnfdr_zfp_cr,
[r"ZFP($\epsilon_{abs}$)", "-", "-", ""],
eb_abs=eb_abs,
),
table_ercnfdr(
ercnfdr_sg_lossless["zfp.rs"],
ercnfdr_sg_lossless_cr["zfp.rs"],
[
r"ZFP($\epsilon_{abs}$)",
"-",
rf"$\epsilon_{{abs}} \cup \overset{{{missing_value}}}{{\leftrightarrow}}$",
"lossless",
],
eb_abs=eb_abs,
corr=ercnfdr_zfp,
),
table_ercnfdr(
ercnfdr_sg["zfp.rs"],
ercnfdr_sg_cr["zfp.rs"],
[
r"ZFP($\epsilon_{abs}$)",
"-",
rf"$\epsilon_{{abs}} \cup \overset{{{missing_value}}}{{\leftrightarrow}}$",
"one-shot",
],
eb_abs=eb_abs,
corr=ercnfdr_zfp,
),
table_ercnfdr(
ercnfdr_mask["zfp.rs"],
ercnfdr_mask_cr["zfp.rs"],
[
r"ZFP($\epsilon_{abs}$)",
rf"$\overset{{{missing_value}}}{{\leftrightarrow}}$",
"-",
"",
],
eb_abs=eb_abs,
),
table_ercnfdr(
ercnfdr_mask_sg["zfp.rs"],
ercnfdr_mask_sg_cr["zfp.rs"],
[
r"ZFP($\epsilon_{abs}$)",
rf"$\overset{{{missing_value}}}{{\leftrightarrow}}$",
rf"$\epsilon_{{abs}} \cup \overset{{{missing_value}}}{{\leftrightarrow}}$",
"one-shot",
],
eb_abs=eb_abs,
corr=ercnfdr_mask["zfp.rs"],
),
table_ercnfdr(
ercnfdr_optzconfig["zfp.rs"],
ercnfdr_optzconfig_cr["zfp.rs"],
[
r"OptZConfig(ZFP)",
"-",
rf"$\epsilon_{{abs}} \cup \overset{{{missing_value}}}{{\leftrightarrow}}$",
"",
],
eb_abs=eb_abs,
),
table_ercnfdr(
ercnfdr_sz3,
ercnfdr_sz3_cr,
[r"SZ3($\epsilon_{abs}$)", "-", "-", ""],
eb_abs=eb_abs,
),
table_ercnfdr(
ercnfdr_sg_lossless["sz3.rs"],
ercnfdr_sg_lossless_cr["sz3.rs"],
[
r"SZ3($\epsilon_{abs}$)",
"-",
rf"$\epsilon_{{abs}} \cup \overset{{{missing_value}}}{{\leftrightarrow}}$",
"lossless",
],
eb_abs=eb_abs,
corr=ercnfdr_sz3,
),
table_ercnfdr(
ercnfdr_sg["sz3.rs"],
ercnfdr_sg_cr["sz3.rs"],
[
r"SZ3($\epsilon_{abs}$)",
"-",
rf"$\epsilon_{{abs}} \cup \overset{{{missing_value}}}{{\leftrightarrow}}$",
"one-shot",
],
eb_abs=eb_abs,
corr=ercnfdr_sz3,
),
table_ercnfdr(
ercnfdr_mask["sz3.rs"],
ercnfdr_mask_cr["sz3.rs"],
[
r"SZ3($\epsilon_{abs}$)",
rf"$\overset{{{missing_value}}}{{\leftrightarrow}}$",
"-",
"",
],
eb_abs=eb_abs,
),
table_ercnfdr(
ercnfdr_mask_sg["sz3.rs"],
ercnfdr_mask_sg_cr["sz3.rs"],
[
r"SZ3($\epsilon_{abs}$)",
rf"$\overset{{{missing_value}}}{{\leftrightarrow}}$",
rf"$\epsilon_{{abs}} \cup \overset{{{missing_value}}}{{\leftrightarrow}}$",
"one-shot",
],
eb_abs=eb_abs,
corr=ercnfdr_mask["sz3.rs"],
),
table_ercnfdr(
ercnfdr_optzconfig["sz3.rs"],
ercnfdr_optzconfig_cr["sz3.rs"],
[
r"OptZConfig(SZ3)",
"-",
rf"$\epsilon_{{abs}} \cup \overset{{{missing_value}}}{{\leftrightarrow}}$",
"",
],
eb_abs=eb_abs,
),
table_ercnfdr(
ercnfdr_sperr,
ercnfdr_sperr_cr,
[r"SPERR($\epsilon_{abs}$)", "-", "-", ""],
eb_abs=eb_abs,
),
table_ercnfdr(
ercnfdr_sg_lossless["sperr.rs"],
ercnfdr_sg_lossless_cr["sperr.rs"],
[
r"SPERR($\epsilon_{abs}$)",
"-",
rf"$\epsilon_{{abs}} \cup \overset{{{missing_value}}}{{\leftrightarrow}}$",
"lossless",
],
eb_abs=eb_abs,
corr=ercnfdr_sperr,
),
table_ercnfdr(
ercnfdr_sg["sperr.rs"],
ercnfdr_sg_cr["sperr.rs"],
[
r"SPERR($\epsilon_{abs}$)",
"-",
rf"$\epsilon_{{abs}} \cup \overset{{{missing_value}}}{{\leftrightarrow}}$",
"one-shot",
],
eb_abs=eb_abs,
corr=ercnfdr_sperr,
),
table_ercnfdr(
ercnfdr_mask["sperr.rs"],
ercnfdr_mask_cr["sperr.rs"],
[
r"SPERR($\epsilon_{abs}$)",
rf"$\overset{{{missing_value}}}{{\leftrightarrow}}$",
"-",
"",
],
eb_abs=eb_abs,
),
table_ercnfdr(
ercnfdr_mask_sg["sperr.rs"],
ercnfdr_mask_sg_cr["sperr.rs"],
[
r"SPERR($\epsilon_{abs}$)",
rf"$\overset{{{missing_value}}}{{\leftrightarrow}}$",
rf"$\epsilon_{{abs}} \cup \overset{{{missing_value}}}{{\leftrightarrow}}$",
"one-shot",
],
eb_abs=eb_abs,
corr=ercnfdr_mask["sperr.rs"],
),
table_ercnfdr(
ercnfdr_optzconfig["sperr.rs"],
ercnfdr_optzconfig_cr["sperr.rs"],
[
r"OptZConfig(SPERR)",
"-",
rf"$\epsilon_{{abs}} \cup \overset{{{missing_value}}}{{\leftrightarrow}}$",
"",
],
eb_abs=eb_abs,
),
table_ercnfdr(
ercnfdr_zstd,
ercnfdr_zstd_cr,
["ZSTD(22)", "-", "-", ""],
eb_abs=eb_abs,
),
]
).set_index(["Compressor", "Meta", "Safeguarded", "Corrections"])
Path("tables").joinpath("missing.tex").write_text(
ercnfdr_table.to_latex(escape=False)
.replace("%", r"\%")
.replace("\\cline{1-9} \\cline{2-9} \\cline{3-9}\n\\bottomrule", "\\bottomrule")
)
ercnfdr_table
| $L_{\infty}$ | $L_{2}$ | V | C | CR | ||||
|---|---|---|---|---|---|---|---|---|
| Compressor | Meta | Safeguarded | Corrections | |||||
| 0 | - | $\epsilon_{abs} \cup \overset{9999}{\leftrightarrow}$ | lossless | 1.0 | 0.12 | 0 | 87.6% | $\times$ 62.89 |
| one-shot | 1.0 | 0.31 | 0 | 87.6% | $\times$ 72.72 | |||
| ZFP($\epsilon_{abs}$) | - | - | 0.75 | 0.16 | 1.8% | $\times$ 6.27 | ||
| $\epsilon_{abs} \cup \overset{9999}{\leftrightarrow}$ | lossless | 0.75 | 0.15 | 0 | 1.8% | $\times$ 6.16 | ||
| one-shot | 0.75 | 0.15 | 0 | 1.8% | $\times$ 6.16 | |||
| $\overset{9999}{\leftrightarrow}$ | - | 0.75 | 0.15 | 0 | $\times$ 6.18 | |||
| $\epsilon_{abs} \cup \overset{9999}{\leftrightarrow}$ | one-shot | 0.75 | 0.15 | 0 | 0 | $\times$ 6.18 | ||
| OptZConfig(ZFP) | - | $\epsilon_{abs} \cup \overset{9999}{\leftrightarrow}$ | 0.0 | 0.0 | 0 | $\times$ 5.12 | ||
| SZ3($\epsilon_{abs}$) | - | - | 1.0 | 0.89 | 70.5% | $\times$ 59.96 | ||
| $\epsilon_{abs} \cup \overset{9999}{\leftrightarrow}$ | lossless | 1.0 | 0.31 | 0 | 70.5% | $\times$ 46.0 | ||
| one-shot | 1.0 | 0.31 | 0 | 70.5% | $\times$ 46.0 | |||
| $\overset{9999}{\leftrightarrow}$ | - | 1.0 | 0.31 | 0 | $\times$ 53.1 | |||
| $\epsilon_{abs} \cup \overset{9999}{\leftrightarrow}$ | one-shot | 1.0 | 0.31 | 0 | 0 | $\times$ 53.1 | ||
| OptZConfig(SZ3) | - | $\epsilon_{abs} \cup \overset{9999}{\leftrightarrow}$ | 6.4e-05 | 1.3e-06 | 0 | $\times$ 57.23 | ||
| SPERR($\epsilon_{abs}$) | - | - | 0.75 | 0.38 | 50.3% | $\times$ 5.63 | ||
| $\epsilon_{abs} \cup \overset{9999}{\leftrightarrow}$ | lossless | 0.75 | 0.24 | 0 | 50.3% | $\times$ 5.5 | ||
| one-shot | 0.75 | 0.24 | 0 | 50.3% | $\times$ 5.5 | |||
| $\overset{9999}{\leftrightarrow}$ | - | 0.75 | 0.24 | 0 | $\times$ 5.57 | |||
| $\epsilon_{abs} \cup \overset{9999}{\leftrightarrow}$ | one-shot | 0.75 | 0.24 | 0 | 0 | $\times$ 5.57 | ||
| OptZConfig(SPERR) | - | $\epsilon_{abs} \cup \overset{9999}{\leftrightarrow}$ | 7.5e-07 | 3.9e-07 | 50.4% | $\times$ 2.35 | ||
| ZSTD(22) | - | - | 0.0 | 0.0 | 0 | $\times$ 49.94 |
import json
with Path("observations").joinpath("missing.json").open("w") as f:
json.dump(observations, f)