Preserve a pointwise quantity of interest (QoI) with safeguards¶
In this example, we compare how three different lossy compressors (ZFP, SZ3, and SPERR) affect the log-scale visualisation of a 2D specific humidity slice. Finally we apply safeguards to guarantee an absolute error bound directly on the pointwise quantity of interest, the logarithm that is being visualised. We also compare the safeguards with the QoI-aware QPET-SPERR compressor and the compressor configuration auto-tuner OptZConfig. This example thus provides a simple starting point for preserving simple diagnostics and plotting.
import ssl
ssl._create_default_https_context = ssl._create_stdlib_context
from pathlib import Path
import earthkit.plots
import humanize
import matplotlib as mpl
import numpy as np
import pandas as pd
import xarray as xr
from matplotlib import patheffects as PathEffects
# Retrieve the data
ERA5 = xr.open_dataset(Path() / "data" / "era5-q" / "data.nc")
ERA5_Q = ERA5["q"].sel(valid_time="2024-04-02T12:00:00", pressure_level=850)
def compute_corrections_percentage(
my_ERA5_Q: xr.DataArray, orig_ERA5_Q: xr.DataArray
) -> float:
return np.mean(my_ERA5_Q != orig_ERA5_Q)
old_cmap_and_norm = earthkit.plots.styles.colors.cmap_and_norm
def my_cmap_and_norm(colors, levels, normalize=True, extend=None, extend_levels=True):
return old_cmap_and_norm(colors, levels, normalize, extend, True)
earthkit.plots.styles.colors.cmap_and_norm = my_cmap_and_norm
def plot_specific_humidity_log10(
my_ERA5_Q: xr.DataArray,
cr,
chart,
title,
span,
qlog10_eb_abs,
error=False,
corr=None,
my_ERA5_Q_ratio=None,
cr_ratio=None,
corr_ratio=None,
inset=True,
):
import copy
with np.errstate(divide="ignore", invalid="ignore"):
ERA5_Q_log10 = np.log10(ERA5_Q)
my_ERA5_Q_log10 = np.log10(my_ERA5_Q)
if my_ERA5_Q_ratio is not None:
my_ERA5_Q_ratio_log10 = np.log10(my_ERA5_Q_ratio)
if error:
err_v = np.mean(~(np.abs(my_ERA5_Q_log10 - ERA5_Q_log10) <= qlog10_eb_abs))
if my_ERA5_Q_ratio is not None:
err_ratio_v = np.mean(
~(np.abs(my_ERA5_Q_ratio_log10 - ERA5_Q_log10) <= qlog10_eb_abs)
)
with xr.set_options(keep_attrs=True):
da = (my_ERA5_Q_log10 - ERA5_Q_log10).compute()
da.attrs.update(
long_name=f"Absolute error over log10({da.long_name.lower()})",
units=ERA5_Q.units,
)
else:
# plot the decimal logarithm of specific humidity to better capture scale
da = np.log10(my_ERA5_Q)
da.attrs.update(
long_name=f"log10({ERA5_Q.long_name.lower()})", units=ERA5_Q.units
)
# compute the default style that earthkit.maps would apply
source = earthkit.plots.sources.XarraySource(da)
style = copy.deepcopy(
earthkit.plots.styles.auto.guess_style(
source,
units=source.units,
)
)
if error:
style._levels = earthkit.plots.styles.levels.Levels(
np.linspace(-span, span, 22)
)
style._legend_kwargs["ticks"] = np.linspace(-span, span, 5)
style._colors = "coolwarm"
else:
style._levels = earthkit.plots.styles.levels.Levels(np.linspace(*span, 22))
style._legend_kwargs["ticks"] = np.linspace(*span, 5)
style._colors = "viridis"
extend_left = np.nanmin(da) < (-span if error else span[0])
extend_right = np.nanmax(da) > (span if error else span[1])
extend = {
(False, False): "neither",
(True, False): "min",
(False, True): "max",
(True, True): "both",
}[(extend_left, extend_right)]
chart.ax.fill_between(
[0, 1],
[1, 1],
hatch="XX",
edgecolor="magenta",
facecolor="lavenderblush",
transform=chart.ax.transAxes,
zorder=-12,
)
if error:
style._legend_kwargs["extend"] = extend
chart.pcolormesh(da, style=style, zorder=-11)
if corr is not None:
da_hatch = my_ERA5_Q == corr
da_corr = (~da_hatch).astype(float)
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.pcolormesh(
da_corr.longitude.values,
da_corr.latitude.values,
np.squeeze(da_corr.values),
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:
da_err = ~(np.abs(da) <= qlog10_eb_abs)
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.pcolormesh(
da_err.longitude.values,
da_err.latitude.values,
np.squeeze(da_err.values),
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")],
)
else:
chart.quickplot(da, style=style, extend=extend, zorder=-11)
chart.ax.set_rasterization_zorder(-10)
chart.title(title)
if error:
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_ERA5_Q_ratio is not None:
err_ratio_v = (
0
if err_ratio_v == 0
else np.format_float_positional(
100 * err_ratio_v, precision=1, min_digits=1
)
+ "%"
)
if err_ratio_v == "0.0%":
err_ratio_v = "<0.05%"
t = chart.ax.text(
0.95,
0.1,
f"V={err_v}" + ("" if my_ERA5_Q_ratio is None else f" ({err_ratio_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_ratio is None else rf" ($\times$ {np.round(cr_ratio, 2)})")
if error
else humanize.naturalsize(ERA5_Q.nbytes, binary=True),
ha="right",
va="top",
transform=chart.ax.transAxes,
)
t.set_bbox(dict(facecolor="white", alpha=0.75, edgecolor="black"))
chart.contour(
x=np.broadcast_to(
da.longitude.values.reshape(1, -1),
da.shape,
),
y=np.broadcast_to(
da.latitude.values.reshape(-1, 1),
da.shape,
),
z=~(np.isfinite(da).values),
colors=["none"],
linecolors=["magenta"],
levels=[-0.5, 0.9, 1.5],
legend_style=None,
labels=False,
zorder=-10,
)
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:
getattr(chart, m)()
counts, bins = np.histogram(
da.values.flatten(),
range=(-span if error else span[0], span if error else span[1]),
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 + np.any(~np.isfinite(da)) * 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(da < (-span if error else span[0])),
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(da > (span if error else span[1])),
width=(bins[-1] - bins[0]) / len(counts),
color=cb.cmap(cb.norm(midpoints[-1])),
)
if np.any(~np.isfinite(da)):
cax.bar(
bins[-1] + (bins[-1] - bins[-2]) * (extend_right * 2 + 2 + 1) / 2,
height=np.sum(~np.isfinite(da)),
width=(bins[-1] - bins[0]) / len(counts),
color="lavenderblush",
edgecolor="magenta",
lw=0,
hatch="XXXX",
)
q1, q2, q3 = da.quantile([0.25, 0.5, 0.75]).values
cax.axvline(da.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(da.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(
(-span if error else span[0]) - (bins[-1] - bins[-2]) * extend_left,
(span if error else span[1])
+ (bins[-1] - bins[-2]) * (0 + extend_right + np.any(~np.isfinite(da)) * 2),
)
cax.set_xticks([])
cax.set_yticks([])
cax.spines[:].set_visible(False)
def table_specific_humidity_log10(
my_ERA5_Q: xr.DataArray,
cr,
title,
qlog10_eb_abs,
corr=None,
):
with np.errstate(divide="ignore", invalid="ignore"):
ERA5_Q_log10 = np.log10(ERA5_Q)
my_ERA5_Q_log10 = np.log10(my_ERA5_Q)
err_Q_inf = np.amax(np.abs(my_ERA5_Q - ERA5_Q).values)
err_Q_log10_inf = np.amax(np.abs(my_ERA5_Q_log10 - ERA5_Q_log10).values)
err_Q_log10_fin_inf = np.nanmax(
np.nan_to_num(
np.abs(my_ERA5_Q_log10 - ERA5_Q_log10),
nan=np.nan,
posinf=np.nan,
neginf=np.nan,
)
)
err_Q_log10_2 = np.sqrt(np.mean(np.square(my_ERA5_Q_log10 - ERA5_Q_log10).values))
err_Q_log10_fin_2 = np.sqrt(
np.nanmean(
np.nan_to_num(
np.square(my_ERA5_Q_log10 - ERA5_Q_log10),
nan=np.nan,
posinf=np.nan,
neginf=np.nan,
)
)
)
err_v = np.mean(~(np.abs(my_ERA5_Q_log10 - ERA5_Q_log10) <= qlog10_eb_abs))
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_ERA5_Q, 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]],
"(Bound)": [title[1]],
"Safeguarded": [title[2]],
"Corrections": [title[3]],
r"$L_{\infty}(\hat{q})$": [f"{err_Q_inf:.02}"],
r"$L_{\infty}(\log_{10}(\hat{q}))$": [
f"{err_Q_log10_inf:.03}".replace("nan", "NaN")
+ (
""
if np.isfinite(err_Q_log10_inf)
else f" [{err_Q_log10_fin_inf:.03}]"
)
],
r"$L_{2}(\log_{10}(\hat{q}))$": [
f"{err_Q_log10_2:.03}".replace("nan", "NaN")
+ ("" if np.isfinite(err_Q_log10_2) else f" [{err_Q_log10_fin_2:.03}]")
],
"V": [err_v],
"C": [corr],
"CR": [rf"$\times$ {np.round(cr, 2)}"],
}
)
import observe
observations = []
Lossless compression¶
We first compress the data losslessly with ZStandard at level 22, which gives maximum compression, to provide a baseline.
from numcodecs_wasm_zstd import Zstd
zstd = Zstd(level=22)
with observe.observe(zstd, observations):
ERA5_Q_zstd_enc = zstd.encode(ERA5_Q.values)
ERA5_Q_zstd = ERA5_Q.copy(data=zstd.decode(ERA5_Q_zstd_enc))
ERA5_Q_zstd_cr = ERA5_Q.nbytes / ERA5_Q_zstd_enc.nbytes
Compressing q with lossy compressors¶
We configure each compressor with an absolute error bound of $5 \cdot 10^{-4}$ kg/kg, which produces decent compression ratios and shows that ZFP, SZ3 and ZFP can produce negative values whose logarithm is undefined.
eb_abs = 0.0005
from numcodecs_wasm_zfp import Zfp
zfp = Zfp(mode="fixed-accuracy", tolerance=eb_abs)
with observe.observe(zfp, observations):
ERA5_Q_zfp_enc = zfp.encode(ERA5_Q.values)
ERA5_Q_zfp = ERA5_Q.copy(data=zfp.decode(ERA5_Q_zfp_enc))
ERA5_Q_zfp_cr = ERA5_Q.nbytes / ERA5_Q_zfp_enc.nbytes
from numcodecs_wasm_sz3 import Sz3
sz3 = Sz3(eb_mode="abs", eb_abs=eb_abs)
with observe.observe(sz3, observations):
ERA5_Q_sz3_enc = sz3.encode(ERA5_Q.values)
ERA5_Q_sz3 = ERA5_Q.copy(data=sz3.decode(ERA5_Q_sz3_enc))
ERA5_Q_sz3_cr = ERA5_Q.nbytes / ERA5_Q_sz3_enc.nbytes
from numcodecs_wasm_sperr import Sperr
sperr = Sperr(mode="pwe", pwe=eb_abs)
with observe.observe(sperr, observations):
ERA5_Q_sperr_enc = sperr.encode(ERA5_Q.values)
ERA5_Q_sperr = ERA5_Q.copy(data=sperr.decode(ERA5_Q_sperr_enc))
ERA5_Q_sperr_cr = ERA5_Q.nbytes / ERA5_Q_sperr_enc.nbytes
from numcodecs_zero import ZeroCodec
zero = ZeroCodec()
with observe.observe(zero, observations):
ERA5_Q_zero_enc = zero.encode(ERA5_Q.values)
ERA5_Q_zero = ERA5_Q.copy(data=zero.decode(ERA5_Q_zero_enc))
Compressing q using the safeguarded lossy compressors¶
We configure the safeguards to bound the pointwise absolute error on the decimal logarithm diagnostic, choosing an error bound of 0.25 (in logarithmic space).
qlog10_eb_abs = 0.25
from numcodecs_safeguards import SafeguardedCodec
ERA5_Q_sg = dict()
ERA5_Q_sg_cr = dict()
for codec_id, codec in {
"zero": zero,
"zfp.rs": zfp,
"sz3.rs": sz3,
"sperr.rs": sperr,
}.items():
sg = SafeguardedCodec(
codec=codec,
safeguards=[
dict(
kind="qoi_eb_pw",
qoi="log10(x)",
type="abs",
eb=qlog10_eb_abs,
)
],
)
with observe.observe(sg, observations):
ERA5_Q_sg_enc = sg.encode(ERA5_Q.values)
ERA5_Q_sg[codec_id] = ERA5_Q.copy(data=sg.decode(ERA5_Q_sg_enc))
ERA5_Q_sg_cr[codec_id] = ERA5_Q.nbytes / np.asarray(ERA5_Q_sg_enc).nbytes
from numcodecs_safeguards import SafeguardedCodec
ERA5_Q_sg_lossless = dict()
ERA5_Q_sg_lossless_cr = dict()
for codec_id, codec in {
"zero": zero,
"zfp.rs": zfp,
"sz3.rs": sz3,
"sperr.rs": sperr,
}.items():
sg = SafeguardedCodec(
codec=codec,
safeguards=[
dict(
kind="qoi_eb_pw",
qoi="log10(x)",
type="abs",
eb=qlog10_eb_abs,
)
],
# produce lossless corrections and refine them with iteration
compute=dict(unstable_iterative=True, unstable_lossless_corrections=True),
)
with observe.observe(sg, observations):
ERA5_Q_sg_lossless_enc = sg.encode(ERA5_Q.values)
ERA5_Q_sg_lossless[codec_id] = ERA5_Q.copy(
data=sg.decode(ERA5_Q_sg_lossless_enc)
)
ERA5_Q_sg_lossless_cr[codec_id] = (
ERA5_Q.nbytes / np.asarray(ERA5_Q_sg_lossless_enc).nbytes
)
Compressing q with QPET-SPERR¶
We similarly configure QPET-SPERR to bound the pointwise absolute error on the decimal logarithm diagnostic, choosing an error bound of 0.25 (in logarithmic space).
from numcodecs_wasm_qpet_sperr import QpetSperr
qpet = QpetSperr(
mode="qoi-symbolic",
qoi="log(x, 10)",
qoi_pwe=qlog10_eb_abs,
qoi_block_size=(1, 1, 1),
high_prec=True,
)
with observe.observe(qpet, observations):
ERA5_Q_qpet_enc = qpet.encode(ERA5_Q.values)
ERA5_Q_qpet = ERA5_Q.copy(data=qpet.decode(ERA5_Q_qpet_enc))
ERA5_Q_qpet_cr = ERA5_Q.nbytes / ERA5_Q_qpet_enc.nbytes
Tuning eb with qoi current_eb = 0.00026811, current_br = 0.429688 current_eb = 0.000177975, current_br = 0.484375 current_eb = 0.00013569, current_br = 0.515625 current_eb = 8.80638e-05, current_br = 0.65625 current_eb = 7.67135e-05, current_br = 0.601562 current_eb = 7.00369e-05, current_br = 0.625 current_eb = 6.53633e-05, current_br = 0.625 Selected quantile: 0.200012 Best abs eb: 0.00026811 Tuning eb with qoi current_eb = 0.000230275, current_br = 0.46875 current_eb = 0.00016796, current_br = 0.492188 current_eb = 0.000127011, current_br = 0.523438 current_eb = 9.69659e-05, current_br = 0.570312 current_eb = 8.98442e-05, current_br = 0.601562 current_eb = 8.47255e-05, current_br = 0.625 current_eb = 6.98144e-05, current_br = 0.648438 Selected quantile: 0.200012 Best abs eb: 0.000230275 Tuning eb with qoi current_eb = 0.000205349, current_br = 0.382812 current_eb = 0.000152382, current_br = 0.453125 current_eb = 0.000129459, current_br = 0.5 current_eb = 0.000106536, current_br = 0.515625 current_eb = 8.07195e-05, current_br = 0.554688 current_eb = 6.53633e-05, current_br = 0.640625 current_eb = 5.04522e-05, current_br = 0.679688 Selected quantile: 0.200012 Best abs eb: 0.000205349 Tuning eb with qoi current_eb = 0.000205349, current_br = 0.5 current_eb = 0.000205349, current_br = 0.5 current_eb = 0.00018532, current_br = 0.507812 current_eb = 0.00013569, current_br = 0.523438 current_eb = 0.000108984, current_br = 0.570312 current_eb = 9.14021e-05, current_br = 0.609375 current_eb = 7.09271e-05, current_br = 0.648438 Selected quantile: 0.0500031 Best abs eb: 0.00018532 Tuning eb with qoi current_eb = 0.00018532, current_br = 0.554688 current_eb = 0.00018532, current_br = 0.554688 current_eb = 0.00018532, current_br = 0.554688 current_eb = 0.000161061, current_br = 0.609375 current_eb = 0.000139696, current_br = 0.648438 current_eb = 0.000121447, current_br = 0.671875 current_eb = 8.24999e-05, current_br = 0.8125 Selected quantile: 0.0500031 Best abs eb: 0.00018532 Tuning eb with qoi current_eb = 0.00018532, current_br = 0.601562 current_eb = 0.00018532, current_br = 0.601562 current_eb = 0.000157055, current_br = 0.609375 current_eb = 7.91616e-05, current_br = 0.820312 current_eb = 6.80339e-05, current_br = 0.882812 current_eb = 5.93544e-05, current_br = 0.9375 current_eb = 5.3568e-05, current_br = 1.11719 Selected quantile: 0.05 Best abs eb: 0.000157055 Tuning eb with qoi current_eb = 0.000157055, current_br = 1.27344 current_eb = 0.000157055, current_br = 1.27344 current_eb = 0.000157055, current_br = 1.27344 current_eb = 0.000157055, current_br = 1.27344 current_eb = 0.000157055, current_br = 1.27344 current_eb = 0.000157055, current_br = 1.27344 current_eb = 0.000157055, current_br = 1.27344 Selected quantile: 0.0019989 Best abs eb: 0.000157055 Tuning eb with qoi current_eb = 0.000157055, current_br = 2.17188 current_eb = 0.000157055, current_br = 2.17188 current_eb = 0.000157055, current_br = 2.17188 current_eb = 0.000157055, current_br = 2.17188 current_eb = 0.000157055, current_br = 2.17188 current_eb = 0.000157055, current_br = 2.17188 current_eb = 0.000135468, current_br = 2.36719 Selected quantile: 0.00498962 Best abs eb: 0.000157055 Tuning eb with qoi current_eb = 0.000157055, current_br = 2.125 current_eb = 0.000157055, current_br = 2.125 current_eb = 0.000157055, current_br = 2.125 current_eb = 0.000157055, current_br = 2.125 current_eb = 0.000157055, current_br = 2.125 current_eb = 0.000157055, current_br = 2.125 current_eb = 0.000157055, current_br = 2.125 Selected quantile: 0.0019989 Best abs eb: 0.000157055 Tuning eb with qoi current_eb = 0.000157055, current_br = 1.73438 current_eb = 0.000157055, current_br = 1.73438 current_eb = 0.000157055, current_br = 1.73438 current_eb = 0.000157055, current_br = 1.73438 current_eb = 0.000157055, current_br = 1.73438 current_eb = 0.000119444, current_br = 2.01562 current_eb = 8.24999e-05, current_br = 2.5 Selected quantile: 0.00999451 Best abs eb: 0.000157055 Tuning eb with qoi current_eb = 0.000157055, current_br = 3.35156 current_eb = 0.000157055, current_br = 3.35156 current_eb = 0.000157055, current_br = 3.35156 current_eb = 0.000157055, current_br = 3.35156 current_eb = 0.000157055, current_br = 3.35156 current_eb = 0.000157055, current_br = 3.35156 current_eb = 0.000157055, current_br = 3.35156 Selected quantile: 0.0019989 Best abs eb: 0.000157055 Tuning eb with qoi current_eb = 0.000157055, current_br = 2.23438 current_eb = 0.000157055, current_br = 2.23438 current_eb = 0.000157055, current_br = 2.23438 current_eb = 0.000157055, current_br = 2.23438 current_eb = 0.000157055, current_br = 2.23438 current_eb = 0.000157055, current_br = 2.23438 current_eb = 0.000157055, current_br = 2.23438 Selected quantile: 0.00197754 Best abs eb: 0.000157055 Tuning eb with qoi current_eb = 2.30781e-05, current_br = 4.375 current_eb = 1.6179e-05, current_br = 4.89844 current_eb = 1.017e-05, current_br = 5.60156 current_eb = 6.83173e-06, current_br = 6.15625 current_eb = 6.16407e-06, current_br = 6.34375 current_eb = 5.71896e-06, current_br = 6.35156 current_eb = 5.27386e-06, current_br = 6.54688 Selected quantile: 0.200004 Best abs eb: 2.30781e-05 Tuning eb with qoi current_eb = 2.08526e-05, current_br = 3.73438 current_eb = 1.06151e-05, current_br = 4.69531 current_eb = 6.83173e-06, current_br = 5.26562 current_eb = 5.0513e-06, current_br = 5.80469 current_eb = 4.38364e-06, current_br = 5.92188 current_eb = 4.16109e-06, current_br = 6.01562 current_eb = 3.93854e-06, current_br = 6.0625 Selected quantile: 0.200004 Best abs eb: 2.08526e-05 Tuning eb with qoi current_eb = 2.08526e-05, current_br = 3.26562 current_eb = 2.08526e-05, current_br = 3.26562 current_eb = 2.08526e-05, current_br = 3.26562 current_eb = 1.15054e-05, current_br = 4.24219 current_eb = 9.05727e-06, current_br = 4.4375 current_eb = 7.27684e-06, current_br = 4.82031 current_eb = 6.60918e-06, current_br = 4.94531 Selected quantile: 0.0499963 Best abs eb: 2.08526e-05 Tuning eb with qoi current_eb = 2.08526e-05, current_br = 3.76562 current_eb = 2.08526e-05, current_br = 3.76562 current_eb = 2.08526e-05, current_br = 3.76562 current_eb = 2.08526e-05, current_br = 3.76562 current_eb = 2.08526e-05, current_br = 3.76562 current_eb = 2.08526e-05, current_br = 3.76562 current_eb = 2.08526e-05, current_br = 3.76562 Selected quantile: 0.00199985 Best abs eb: 2.08526e-05 Tuning eb with qoi current_eb = 2.08526e-05, current_br = 3.41406 current_eb = 2.08526e-05, current_br = 3.41406 current_eb = 2.08526e-05, current_br = 3.41406 current_eb = 2.08526e-05, current_br = 3.41406 current_eb = 2.08526e-05, current_br = 3.41406 current_eb = 2.08526e-05, current_br = 3.41406 current_eb = 2.08526e-05, current_br = 3.41406 Selected quantile: 0.00199985 Best abs eb: 2.08526e-05 Tuning eb with qoi current_eb = 2.08526e-05, current_br = 4.8125 current_eb = 2.08526e-05, current_br = 4.8125 current_eb = 2.08526e-05, current_br = 4.8125 current_eb = 2.08526e-05, current_br = 4.8125 current_eb = 2.08526e-05, current_br = 4.8125 current_eb = 2.04075e-05, current_br = 4.85938 current_eb = 2.01849e-05, current_br = 4.88281 Selected quantile: 0.00197368 Best abs eb: 2.01849e-05
Compressing q with ratio-error-bounded lossy compressors¶
In this simple case of a $\text{QoI} = \log_{10}(x)$ diagnostic with $\epsilon_{abs}(\text{QoI}) \leq 0.25$, we could have also used a ratio error bound with $\epsilon_{ratio} = 10^{0.25}$ to preserve the quantity of interest. While ZFP, SZ3, and SPERR do not natively support ratio error bounds, we can use a meta-compressor that transforms the ratio error bound into an absolute error bound. This meta-compressor was proposed by Liang et al. [^1] and has been implemented in libpressio as the pw_rel_compressor_plugin and in the numcodecs-pw-ratio package.
[^1]: Liang, X., Di, S., Tao, D., Chen, Z., & Cappello, F. (2018). An Efficient Transformation Scheme for Lossy Data Compression with Point-Wise Relative Error Bound. 2018 IEEE International Conference on Cluster Computing (CLUSTER), 179–189. Available from: doi:10.1109/cluster.2018.00036.
from numcodecs_pw_ratio import PointwiseRatioErrorBoundedCodec
zfp_ratio = PointwiseRatioErrorBoundedCodec(
eb_ratio=np.power(10, 0.25),
eb_abs_marker="$eb_abs",
log_codec={**zfp.get_config(), "tolerance": "$eb_abs"},
sign_codec=dict(id="zlib", level=9),
)
with observe.observe(zfp_ratio, observations):
ERA5_Q_zfp_ratio_enc = zfp_ratio.encode(ERA5_Q.values)
ERA5_Q_zfp_ratio = ERA5_Q.copy(data=zfp_ratio.decode(ERA5_Q_zfp_ratio_enc))
ERA5_Q_zfp_ratio_cr = ERA5_Q.nbytes / np.asarray(ERA5_Q_zfp_ratio_enc).nbytes
sz3_ratio = PointwiseRatioErrorBoundedCodec(
eb_ratio=np.power(10, 0.25),
eb_abs_marker="$eb_abs",
log_codec={**sz3.get_config(), "eb_abs": "$eb_abs"},
sign_codec=dict(id="zlib", level=9),
)
with observe.observe(sz3_ratio, observations):
ERA5_Q_sz3_ratio_enc = sz3_ratio.encode(ERA5_Q.values)
ERA5_Q_sz3_ratio = ERA5_Q.copy(data=sz3_ratio.decode(ERA5_Q_sz3_ratio_enc))
ERA5_Q_sz3_ratio_cr = ERA5_Q.nbytes / np.asarray(ERA5_Q_sz3_ratio_enc).nbytes
sperr_ratio = PointwiseRatioErrorBoundedCodec(
eb_ratio=np.power(10, 0.25),
eb_abs_marker="$eb_abs",
log_codec={**sperr.get_config(), "pwe": "$eb_abs"},
sign_codec=dict(id="zlib", level=9),
)
with observe.observe(sperr_ratio, observations):
ERA5_Q_sperr_ratio_enc = sperr_ratio.encode(ERA5_Q.values)
ERA5_Q_sperr_ratio = ERA5_Q.copy(data=sperr_ratio.decode(ERA5_Q_sperr_ratio_enc))
ERA5_Q_sperr_ratio_cr = ERA5_Q.nbytes / np.asarray(ERA5_Q_sperr_ratio_enc).nbytes
ERA5_Q_ratio_sg = dict()
ERA5_Q_ratio_sg_cr = dict()
for codec_id, codec in {
"zfp.rs": zfp_ratio,
"sz3.rs": sz3_ratio,
"sperr.rs": sperr_ratio,
}.items():
sg = SafeguardedCodec(
codec=codec,
safeguards=[
dict(
kind="qoi_eb_pw",
qoi="log10(x)",
type="abs",
eb=qlog10_eb_abs,
)
],
)
with observe.observe(sg, observations):
ERA5_Q_ratio_sg_enc = sg.encode(ERA5_Q.values)
ERA5_Q_ratio_sg[codec_id] = ERA5_Q.copy(data=sg.decode(ERA5_Q_ratio_sg_enc))
ERA5_Q_ratio_sg_cr[codec_id] = (
ERA5_Q.nbytes / np.asarray(ERA5_Q_ratio_sg_enc).nbytes
)
We can similarly compare the safeguards with a ratio-error bound instead of the $\log_{10}(x)$ quantity of interest. Using an equivalent but simpler safeguard can sometimes result in higher compression ratios (e.g. using qoi="log(x, base=10)" results in a lower compression ratio):
sg_ratio = SafeguardedCodec(
codec=ZeroCodec(),
safeguards=[
dict(
kind="eb",
type="ratio",
eb=np.power(10.0, qlog10_eb_abs),
)
],
)
with observe.observe(sg_ratio, observations):
ERA5_Q_sg_ratio_enc = sg_ratio.encode(ERA5_Q.values)
ERA5_Q_sg_ratio = ERA5_Q.copy(data=sg_ratio.decode(ERA5_Q_sg_ratio_enc))
ERA5_Q_sg_ratio_cr = ERA5_Q.nbytes / np.asarray(ERA5_Q_sg_ratio_enc).nbytes
sg_ratio_lossless = SafeguardedCodec(
codec=ZeroCodec(),
safeguards=[
dict(
kind="eb",
type="ratio",
eb=np.power(10.0, qlog10_eb_abs),
)
],
# produce lossless corrections and refine them with iteration
compute=dict(unstable_iterative=True, unstable_lossless_corrections=True),
)
with observe.observe(sg_ratio_lossless, observations):
ERA5_Q_sg_ratio_lossless_enc = sg_ratio_lossless.encode(ERA5_Q.values)
ERA5_Q_sg_ratio_lossless = ERA5_Q.copy(
data=sg_ratio_lossless.decode(ERA5_Q_sg_ratio_lossless_enc)
)
ERA5_Q_sg_ratio_lossless_cr = (
ERA5_Q.nbytes / np.asarray(ERA5_Q_sg_ratio_lossless_enc).nbytes
)
Compressing q 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
with np.errstate(divide="ignore", invalid="ignore"):
data_log10 = np.log10(self._data)
buf_log10 = np.log10(buf)
violations = np.mean(~(np.abs(buf_log10 - data_log10) <= qlog10_eb_abs))
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)
from numcodecs_wasm_pressio import Pressio
ERA5_Q_optzconfig = dict()
ERA5_Q_optzconfig_cr = dict()
for codec, parameter, lower_bound in [
(zfp, "tolerance", 1e-6), # initial guess
(sz3, "eb_abs", 1e-6), # initial guess
(sperr, "pwe", 1e-6), # initial 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):
ERA5_Q_optzconfig_enc = optzconfig.encode(ERA5_Q.values)
ERA5_Q_optzconfig[codec.codec_id] = ERA5_Q.copy(
data=optzconfig.decode(ERA5_Q_optzconfig_enc)
)
ERA5_Q_optzconfig_cr[codec.codec_id] = (
ERA5_Q.nbytes / np.asarray(ERA5_Q_optzconfig_enc).nbytes
)
rank={0,1,} iter={0} input={-10.7082,} output={5.05583,} objective={5.05583}
rank={0,1,} iter={1} input={-12.3658,} output={3.90135,} objective={3.90135}
rank={0,1,} iter={2} input={-9.07594,} output={-9.63168e-06,} objective={-9.63168e-06}
rank={0,1,} iter={3} input={-11.235,} output={4.40965,} objective={4.40965}
rank={0,1,} iter={4} input={-13.8136,} output={3.14865,} objective={3.14865}
rank={0,1,} iter={5} input={-11.7182,} output={4.40965,} objective={4.40965}
rank={0,1,} iter={6} input={-9.92084,} output={-1.92634e-06,} objective={-1.92634e-06}
rank={0,1,} iter={7} input={-7.60101,} output={-0.0149503,} objective={-0.0149503}
rank={0,1,} iter={8} input={-10.8662,} output={5.05583,} objective={5.05583}
rank={0,1,} iter={9} input={-13.0324,} output={3.49067,} objective={3.49067}
rank={0,1,} iter={10} input={-10.7872,} output={5.05583,} objective={5.05583}
rank={0,1,} iter={11} input={-12.002,} output={3.90135,} objective={3.90135}
rank={0,1,} iter={12} input={-11.4765,} output={4.40965,} objective={4.40965}
rank={0,1,} iter={13} input={-10.9995,} output={5.05583,} objective={5.05583}
rank={0,1,} iter={14} input={-12.6667,} output={3.49067,} objective={3.49067}
rank={0,1,} iter={15} input={-13.3964,} output={3.14865,} objective={3.14865}
rank={0,1,} iter={16} input={-11.067,} output={5.05583,} objective={5.05583}
rank={0,1,} iter={17} input={-10.9334,} output={5.05583,} objective={5.05583}
rank={0,1,} iter={18} input={-10.7474,} output={5.05583,} objective={5.05583}
rank={0,1,} iter={19} input={-10.8268,} output={5.05583,} objective={5.05583}
rank={0,1,} iter={20} input={-11.1007,} output={4.40965,} objective={4.40965}
rank={0,1,} iter={21} input={-8.33866,} output={-0.000198413,} objective={-0.000198413}
rank={0,1,} iter={22} input={-10.6072,} output={5.05583,} objective={5.05583}
rank={0,1,} iter={23} input={-10.5515,} output={5.05583,} objective={5.05583}
rank={0,1,} iter={24} input={-12.1839,} output={3.90135,} objective={3.90135}
final_iter={25} inputs={-10.7082,} output={5.05583,}
rank={0,1,} iter={0} input={-10.7082,} output={-0.00445465,} objective={-0.00445465}
rank={0,1,} iter={1} input={-12.3658,} output={-4.81584e-06,} objective={-4.81584e-06}
rank={0,1,} iter={2} input={-9.07594,} output={-0.0337947,} objective={-0.0337947}
rank={0,1,} iter={3} input={-12.5005,} output={-2.88951e-06,} objective={-2.88951e-06}
rank={0,1,} iter={4} input={-8.0252,} output={-0.0479128,} objective={-0.0479128}
rank={0,1,} iter={5} input={-13.3963,} output={5.06888,} objective={5.06888}
rank={0,1,} iter={6} input={-13.8155,} output={4.64574,} objective={4.64574}
rank={0,1,} iter={7} input={-13.5687,} output={4.8533,} objective={4.8533}
rank={0,1,} iter={8} input={-12.9068,} output={5.76445,} objective={5.76445}
rank={0,1,} iter={9} input={-11.5371,} output={-0.000393936,} objective={-0.000393936}
rank={0,1,} iter={10} input={-13.1108,} output={5.44979,} objective={5.44979}
rank={0,1,} iter={11} input={-9.89257,} output={-0.013546,} objective={-0.013546}
rank={0,1,} iter={12} input={-12.9789,} output={5.65946,} objective={5.65946}
rank={0,1,} iter={13} input={-8.55116,} output={-0.049634,} objective={-0.049634}
rank={0,1,} iter={14} input={-12.7844,} output={5.96214,} objective={5.96214}
rank={0,1,} iter={15} input={-7.60372,} output={-0.0961473,} objective={-0.0961473}
rank={0,1,} iter={16} input={-12.5397,} output={6.4103,} objective={6.4103}
rank={0,1,} iter={17} input={-11.1223,} output={-0.00267665,} objective={-0.00267665}
rank={0,1,} iter={18} input={-13.0292,} output={5.56314,} objective={5.56314}
rank={0,1,} iter={19} input={-11.9514,} output={-9.1501e-05,} objective={-9.1501e-05}
rank={0,1,} iter={20} input={-12.7844,} output={5.96214,} objective={5.96214}
rank={0,1,} iter={21} input={-9.48417,} output={-0.0260701,} objective={-0.0260701}
rank={0,1,} iter={22} input={-12.6621,} output={6.21454,} objective={6.21454}
rank={0,1,} iter={23} input={-10.3005,} output={-0.00687895,} objective={-0.00687895}
rank={0,1,} iter={24} input={-12.6001,} output={6.30746,} objective={6.30746}
final_iter={25} inputs={-12.5397,} output={6.4103,}
rank={0,1,} iter={0} input={-10.7082,} output={-3.17846e-05,} objective={-3.17846e-05}
rank={0,1,} iter={1} input={-12.3658,} output={6.663,} objective={6.663}
rank={0,1,} iter={2} input={-9.07594,} output={-0.000872631,} objective={-0.000872631}
rank={0,1,} iter={3} input={-13.8155,} output={4.77268,} objective={4.77268}
rank={0,1,} iter={4} input={-12.8557,} output={5.89156,} objective={5.89156}
rank={0,1,} iter={5} input={-10.8031,} output={-2.88951e-06,} objective={-2.88951e-06}
rank={0,1,} iter={6} input={-13.2039,} output={5.43658,} objective={5.43658}
rank={0,1,} iter={7} input={-11.5845,} output={8.35887,} objective={8.35887}
rank={0,1,} iter={8} input={-7.60101,} output={-0.0104311,} objective={-0.0104311}
rank={0,1,} iter={9} input={-11.8434,} output={7.71863,} objective={7.71863}
rank={0,1,} iter={10} input={-12.0549,} output={7.25465,} objective={7.25465}
rank={0,1,} iter={11} input={-10.8031,} output={-2.88951e-06,} objective={-2.88951e-06}
rank={0,1,} iter={12} input={-11.6826,} output={8.10321,} objective={8.10321}
rank={0,1,} iter={13} input={-11.1938,} output={9.51676,} objective={9.51676}
rank={0,1,} iter={14} input={-9.89257,} output={-0.000140623,} objective={-0.000140623}
rank={0,1,} iter={15} input={-11.3468,} output={9.03206,} objective={9.03206}
rank={0,1,} iter={16} input={-8.33743,} output={-0.00515199,} objective={-0.00515199}
rank={0,1,} iter={17} input={-11.239,} output={9.37463,} objective={9.37463}
rank={0,1,} iter={18} input={-13.4957,} output={5.10055,} objective={5.10055}
rank={0,1,} iter={19} input={-10.9985,} output={-1.63739e-05,} objective={-1.63739e-05}
rank={0,1,} iter={20} input={-10.3006,} output={-2.50424e-05,} objective={-2.50424e-05}
rank={0,1,} iter={21} input={-11.0961,} output={-5.77901e-06,} objective={-5.77901e-06}
rank={0,1,} iter={22} input={-9.48501,} output={-0.000330367,} objective={-0.000330367}
rank={0,1,} iter={23} input={-11.2142,} output={9.44911,} objective={9.44911}
rank={0,1,} iter={24} input={-8.7074,} output={-0.00149676,} objective={-0.00149676}
final_iter={25} inputs={-11.1938,} output={9.51676,}
Visual comparison of the error distributions for the logarithm diagnostic¶
fig = earthkit.plots.Figure(
size=(10, 23),
rows=6,
columns=2,
)
plot_specific_humidity_log10(
ERA5_Q,
1.0,
fig.add_map(0, 0),
"Original",
span=(-5.5, -1.5),
qlog10_eb_abs=qlog10_eb_abs,
)
plot_specific_humidity_log10(
ERA5_Q_zfp,
ERA5_Q_zfp_cr,
fig.add_map(1, 0),
r"ZFP($\epsilon_{{abs}}$)",
span=0.05,
qlog10_eb_abs=qlog10_eb_abs,
error=True,
my_ERA5_Q_ratio=ERA5_Q_zfp_ratio,
cr_ratio=ERA5_Q_zfp_ratio_cr,
)
plot_specific_humidity_log10(
ERA5_Q_sz3,
ERA5_Q_sz3_cr,
fig.add_map(2, 0),
r"SZ3($\epsilon_{{abs}}$)",
span=0.5,
qlog10_eb_abs=qlog10_eb_abs,
error=True,
my_ERA5_Q_ratio=ERA5_Q_sz3_ratio,
cr_ratio=ERA5_Q_sz3_ratio_cr,
)
plot_specific_humidity_log10(
ERA5_Q_sperr,
ERA5_Q_sperr_cr,
fig.add_map(3, 0),
r"SPERR($\epsilon_{{abs}}$)",
span=0.25,
qlog10_eb_abs=qlog10_eb_abs,
error=True,
my_ERA5_Q_ratio=ERA5_Q_sperr_ratio,
cr_ratio=ERA5_Q_sperr_ratio_cr,
)
plot_specific_humidity_log10(
ERA5_Q_sg["zero"],
ERA5_Q_sg_cr["zero"],
fig.add_map(0, 1),
r"Safeguarded(0, $\epsilon_{{QoI,abs}}$)",
span=qlog10_eb_abs,
qlog10_eb_abs=qlog10_eb_abs,
error=True,
corr=ERA5_Q_zero,
my_ERA5_Q_ratio=ERA5_Q_sg_ratio,
cr_ratio=ERA5_Q_sg_ratio_cr,
corr_ratio=ERA5_Q_zero,
)
plot_specific_humidity_log10(
ERA5_Q_sg["zfp.rs"],
ERA5_Q_sg_cr["zfp.rs"],
fig.add_map(1, 1),
r"Safeguarded(ZFP, $\epsilon_{{QoI,abs}}$)",
span=qlog10_eb_abs,
qlog10_eb_abs=qlog10_eb_abs,
error=True,
corr=ERA5_Q_zfp,
my_ERA5_Q_ratio=ERA5_Q_ratio_sg["zfp.rs"],
cr_ratio=ERA5_Q_ratio_sg_cr["zfp.rs"],
corr_ratio=ERA5_Q_zfp_ratio,
)
plot_specific_humidity_log10(
ERA5_Q_sg["sz3.rs"],
ERA5_Q_sg_cr["sz3.rs"],
fig.add_map(2, 1),
r"Safeguarded(SZ3, $\epsilon_{{QoI,abs}}$)",
span=qlog10_eb_abs,
qlog10_eb_abs=qlog10_eb_abs,
error=True,
corr=ERA5_Q_sz3,
my_ERA5_Q_ratio=ERA5_Q_ratio_sg["sz3.rs"],
cr_ratio=ERA5_Q_ratio_sg_cr["sz3.rs"],
corr_ratio=ERA5_Q_sz3_ratio,
)
plot_specific_humidity_log10(
ERA5_Q_sg["sperr.rs"],
ERA5_Q_sg_cr["sperr.rs"],
fig.add_map(3, 1),
r"Safeguarded(SPERR, $\epsilon_{{QoI,abs}}$)",
span=qlog10_eb_abs,
qlog10_eb_abs=qlog10_eb_abs,
error=True,
corr=ERA5_Q_sperr,
my_ERA5_Q_ratio=ERA5_Q_ratio_sg["sperr.rs"],
cr_ratio=ERA5_Q_ratio_sg_cr["sperr.rs"],
corr_ratio=ERA5_Q_sperr_ratio,
)
plot_specific_humidity_log10(
ERA5_Q_optzconfig["zfp.rs"],
ERA5_Q_optzconfig_cr["zfp.rs"],
fig.add_map(4, 0),
r"OptZConfig(ZFP, $\epsilon_{{QoI,abs}}$)",
span=qlog10_eb_abs,
qlog10_eb_abs=qlog10_eb_abs,
error=True,
inset=False,
)
plot_specific_humidity_log10(
ERA5_Q_optzconfig["sz3.rs"],
ERA5_Q_optzconfig_cr["sz3.rs"],
fig.add_map(4, 1),
r"OptZConfig(SZ3, $\epsilon_{{QoI,abs}}$)",
span=qlog10_eb_abs,
qlog10_eb_abs=qlog10_eb_abs,
error=True,
inset=False,
)
plot_specific_humidity_log10(
ERA5_Q_optzconfig["sperr.rs"],
ERA5_Q_optzconfig_cr["sperr.rs"],
fig.add_map(5, 0),
r"OptZConfig(SPERR, $\epsilon_{{QoI,abs}}$)",
span=qlog10_eb_abs,
qlog10_eb_abs=qlog10_eb_abs,
error=True,
inset=False,
)
plot_specific_humidity_log10(
ERA5_Q_qpet,
ERA5_Q_qpet_cr,
fig.add_map(5, 1),
r"QPET-SPERR($\epsilon_{{QoI,abs}}$)",
span=qlog10_eb_abs,
qlog10_eb_abs=qlog10_eb_abs,
error=True,
inset=False,
)
fig.save(Path("plots") / "specific-humidity-log10.pdf")
log10q_table = pd.concat(
[
table_specific_humidity_log10(
ERA5_Q_sg_lossless["zero"],
ERA5_Q_sg_lossless_cr["zero"],
["0", "", r"$\epsilon_{QoI,abs}$", "lossless"],
qlog10_eb_abs,
ERA5_Q_zero,
),
table_specific_humidity_log10(
ERA5_Q_sg["zero"],
ERA5_Q_sg_cr["zero"],
["0", "", r"$\epsilon_{QoI,abs}$", "one-shot"],
qlog10_eb_abs,
ERA5_Q_zero,
),
table_specific_humidity_log10(
ERA5_Q_sg_ratio_lossless,
ERA5_Q_sg_ratio_lossless_cr,
["0", "", r"$\epsilon_{ratio}$", "lossless"],
qlog10_eb_abs,
ERA5_Q_zero,
),
table_specific_humidity_log10(
ERA5_Q_sg_ratio,
ERA5_Q_sg_ratio_cr,
["0", "", r"$\epsilon_{ratio}$", "one-shot"],
qlog10_eb_abs,
ERA5_Q_zero,
),
table_specific_humidity_log10(
ERA5_Q_zfp,
ERA5_Q_zfp_cr,
["ZFP", r"$\epsilon_{abs}$", "-", ""],
qlog10_eb_abs,
None,
),
table_specific_humidity_log10(
ERA5_Q_sg_lossless["zfp.rs"],
ERA5_Q_sg_lossless_cr["zfp.rs"],
["ZFP", r"$\epsilon_{abs}$", r"$\epsilon_{QoI,abs}$", "lossless"],
qlog10_eb_abs,
ERA5_Q_zfp,
),
table_specific_humidity_log10(
ERA5_Q_sg["zfp.rs"],
ERA5_Q_sg_cr["zfp.rs"],
["ZFP", r"$\epsilon_{abs}$", r"$\epsilon_{QoI,abs}$", "one-shot"],
qlog10_eb_abs,
ERA5_Q_zfp,
),
table_specific_humidity_log10(
ERA5_Q_zfp_ratio,
ERA5_Q_zfp_ratio_cr,
["ZFP", r"$\epsilon_{ratio} \rightarrow \epsilon_{abs}$", "-", ""],
qlog10_eb_abs,
None,
),
table_specific_humidity_log10(
ERA5_Q_ratio_sg["zfp.rs"],
ERA5_Q_ratio_sg_cr["zfp.rs"],
[
"ZFP",
r"$\epsilon_{ratio} \rightarrow \epsilon_{abs}$",
r"$\epsilon_{QoI,abs}$",
"one-shot",
],
qlog10_eb_abs,
ERA5_Q_zfp_ratio,
),
table_specific_humidity_log10(
ERA5_Q_optzconfig["zfp.rs"],
ERA5_Q_optzconfig_cr["zfp.rs"],
[
"OptZConfig(ZFP)",
r"$\epsilon_{abs}$",
r"$\epsilon_{QoI,abs}$",
"",
],
qlog10_eb_abs,
None,
),
table_specific_humidity_log10(
ERA5_Q_sz3,
ERA5_Q_sz3_cr,
["SZ3", r"$\epsilon_{abs}$", "-", ""],
qlog10_eb_abs,
None,
),
table_specific_humidity_log10(
ERA5_Q_sg_lossless["sz3.rs"],
ERA5_Q_sg_lossless_cr["sz3.rs"],
["SZ3", r"$\epsilon_{abs}$", r"$\epsilon_{QoI,abs}$", "lossless"],
qlog10_eb_abs,
ERA5_Q_sz3,
),
table_specific_humidity_log10(
ERA5_Q_sg["sz3.rs"],
ERA5_Q_sg_cr["sz3.rs"],
["SZ3", r"$\epsilon_{abs}$", r"$\epsilon_{QoI,abs}$", "one-shot"],
qlog10_eb_abs,
ERA5_Q_sz3,
),
table_specific_humidity_log10(
ERA5_Q_sz3_ratio,
ERA5_Q_sz3_ratio_cr,
["SZ3", r"$\epsilon_{ratio} \rightarrow \epsilon_{abs}$", "-", ""],
qlog10_eb_abs,
None,
),
table_specific_humidity_log10(
ERA5_Q_ratio_sg["sz3.rs"],
ERA5_Q_ratio_sg_cr["sz3.rs"],
[
"SZ3",
r"$\epsilon_{ratio} \rightarrow \epsilon_{abs}$",
r"$\epsilon_{QoI,abs}$",
"one-shot",
],
qlog10_eb_abs,
ERA5_Q_sz3_ratio,
),
table_specific_humidity_log10(
ERA5_Q_optzconfig["sz3.rs"],
ERA5_Q_optzconfig_cr["sz3.rs"],
[
"OptZConfig(SZ3)",
r"$\epsilon_{abs}$",
r"$\epsilon_{QoI,abs}$",
"",
],
qlog10_eb_abs,
None,
),
table_specific_humidity_log10(
ERA5_Q_sperr,
ERA5_Q_sperr_cr,
["SPERR", r"$\epsilon_{abs}$", "-", ""],
qlog10_eb_abs,
None,
),
table_specific_humidity_log10(
ERA5_Q_sg_lossless["sperr.rs"],
ERA5_Q_sg_lossless_cr["sperr.rs"],
["SPERR", r"$\epsilon_{abs}$", r"$\epsilon_{QoI,abs}$", "lossless"],
qlog10_eb_abs,
ERA5_Q_sperr,
),
table_specific_humidity_log10(
ERA5_Q_sg["sperr.rs"],
ERA5_Q_sg_cr["sperr.rs"],
["SPERR", r"$\epsilon_{abs}$", r"$\epsilon_{QoI,abs}$", "one-shot"],
qlog10_eb_abs,
ERA5_Q_sperr,
),
table_specific_humidity_log10(
ERA5_Q_sperr_ratio,
ERA5_Q_sperr_ratio_cr,
["SPERR", r"$\epsilon_{ratio} \rightarrow \epsilon_{abs}$", "-", ""],
qlog10_eb_abs,
None,
),
table_specific_humidity_log10(
ERA5_Q_ratio_sg["sperr.rs"],
ERA5_Q_ratio_sg_cr["sperr.rs"],
[
"SPERR",
r"$\epsilon_{ratio} \rightarrow \epsilon_{abs}$",
r"$\epsilon_{QoI,abs}$",
"one-shot",
],
qlog10_eb_abs,
ERA5_Q_sperr_ratio,
),
table_specific_humidity_log10(
ERA5_Q_optzconfig["sperr.rs"],
ERA5_Q_optzconfig_cr["sperr.rs"],
[
"OptZConfig(SPERR)",
r"$\epsilon_{abs}$",
r"$\epsilon_{QoI,abs}$",
"",
],
qlog10_eb_abs,
None,
),
table_specific_humidity_log10(
ERA5_Q_qpet,
ERA5_Q_qpet_cr,
["QPET-SPERR", r"$\epsilon_{abs}$", r"$\epsilon_{QoI,abs}$", ""],
qlog10_eb_abs,
None,
),
table_specific_humidity_log10(
ERA5_Q_zstd,
ERA5_Q_zstd_cr,
["ZSTD(22)", "", "-", ""],
qlog10_eb_abs,
None,
),
]
).set_index(["Compressor", "(Bound)", "Safeguarded", "Corrections"])
Path("tables").joinpath("specific-humidity-log10.tex").write_text(
log10q_table.to_latex(escape=False)
.replace("%", r"\%")
.replace("\\cline{1-10} \\cline{2-10} \\cline{3-10}\n\\bottomrule", "\\bottomrule")
)
log10q_table
| $L_{\infty}(\hat{q})$ | $L_{\infty}(\log_{10}(\hat{q}))$ | $L_{2}(\log_{10}(\hat{q}))$ | V | C | CR | ||||
|---|---|---|---|---|---|---|---|---|---|
| Compressor | (Bound) | Safeguarded | Corrections | ||||||
| 0 | $\epsilon_{QoI,abs}$ | lossless | 0.0 | 0.0 | 0.0 | 0 | 100.0% | $\times$ 2.92 | |
| one-shot | 0.013 | 0.25 | 0.136 | 0 | 100.0% | $\times$ 89.52 | |||
| $\epsilon_{ratio}$ | lossless | 0.0 | 0.0 | 0.0 | 0 | 100.0% | $\times$ 2.92 | ||
| one-shot | 0.013 | 0.25 | 0.136 | 0 | 100.0% | $\times$ 89.52 | |||
| ZFP | $\epsilon_{abs}$ | - | 0.00029 | NaN [1.1] | NaN [0.0321] | 1.5% | $\times$ 13.25 | ||
| $\epsilon_{QoI,abs}$ | lossless | 0.00029 | 0.25 | 0.0279 | 0 | 1.5% | $\times$ 12.92 | ||
| one-shot | 0.00029 | 0.25 | 0.0328 | 0 | 1.5% | $\times$ 13.15 | |||
| $\epsilon_{ratio} \rightarrow \epsilon_{abs}$ | - | 0.0025 | 0.0943 | 0.0133 | 0 | $\times$ 14.74 | |||
| $\epsilon_{QoI,abs}$ | one-shot | 0.0025 | 0.0943 | 0.0133 | 0 | 0 | $\times$ 14.74 | ||
| OptZConfig(ZFP) | $\epsilon_{abs}$ | $\epsilon_{QoI,abs}$ | 9.4e-06 | 0.137 | 0.00346 | 0 | $\times$ 5.06 | ||
| SZ3 | $\epsilon_{abs}$ | - | 0.0005 | NaN [4.74] | NaN [0.165] | 9.6% | $\times$ 81.1 | ||
| $\epsilon_{QoI,abs}$ | lossless | 0.0005 | 0.25 | 0.0601 | 0 | 9.6% | $\times$ 9.4 | ||
| one-shot | 0.0005 | 0.25 | 0.0737 | 0 | 9.6% | $\times$ 17.03 | |||
| $\epsilon_{ratio} \rightarrow \epsilon_{abs}$ | - | 0.01 | 0.25 | 0.0684 | 0 | $\times$ 426.43 | |||
| $\epsilon_{QoI,abs}$ | one-shot | 0.01 | 0.25 | 0.0684 | 0 | 0 | $\times$ 426.29 | ||
| OptZConfig(SZ3) | $\epsilon_{abs}$ | $\epsilon_{QoI,abs}$ | 3.6e-06 | 0.229 | 0.00606 | 0 | $\times$ 6.41 | ||
| SPERR | $\epsilon_{abs}$ | - | 0.0005 | NaN [4.28] | NaN [0.0615] | 1.0% | $\times$ 73.43 | ||
| $\epsilon_{QoI,abs}$ | lossless | 0.0005 | 0.25 | 0.0353 | 0 | 1.0% | $\times$ 36.19 | ||
| one-shot | 0.0005 | 0.25 | 0.038 | 0 | 1.0% | $\times$ 55.95 | |||
| $\epsilon_{ratio} \rightarrow \epsilon_{abs}$ | - | 0.006 | 0.247 | 0.0313 | 0 | $\times$ 266.88 | |||
| $\epsilon_{QoI,abs}$ | one-shot | 0.006 | 0.247 | 0.0313 | 0 | 0 | $\times$ 266.83 | ||
| OptZConfig(SPERR) | $\epsilon_{abs}$ | $\epsilon_{QoI,abs}$ | 1.4e-05 | 0.226 | 0.0049 | 0 | $\times$ 9.52 | ||
| QPET-SPERR | $\epsilon_{abs}$ | $\epsilon_{QoI,abs}$ | 0.00027 | 0.25 | 0.0129 | 0 | $\times$ 24.05 | ||
| ZSTD(22) | - | 0.0 | 0.0 | 0.0 | 0 | $\times$ 2.23 |
import json
with Path("observations").joinpath("specific-humidity-log10.json").open("w") as f:
json.dump(observations, f)