compression-safeguards
  • Home
  • Try the safeguards

API Reference

  • Overview
  • compression_safeguards
    • api
    • safeguards
      • abc
      • combinators
        • all
        • any
        • assume_safe
        • select
      • eb
      • pointwise
        • abc
        • eb
        • lossless
        • qoi
          • eb
        • same
        • sign
      • qois
      • stencil
        • abc
        • qoi
          • eb
    • utils
      • bindings
      • cast
      • error
      • intervals
      • typing
  • numcodecs_safeguards
    • checksum
    • compute
    • lossless
  • xarray_safeguards

Examples

  • Datasets
  • Special values
    • NaN values
    • Missing values
  • Regions of Interest (RoIs)
    • Latent Heat Flux
  • Quantities of Interest (QoIs)
    • Pointwise: Log-scale Specific Humidity
    • Semi-Pointwise: Kinetic energy
    • Higher-order derivatives
    • Neighbourhood: Relative Vorticity
    • Bounding the dSSIM metric
      • Lossless compression
      • Compressing u with lossy compressors
      • Compressing u using the safeguarded lossy compressors
      • Compressing u with OptZConfig
      • Visual comparison of the dSSIM score distributions
  • Topology
    • Precipitation extrema
    • Isosurfaces
  • Error bound distributions
    • Absolute error bound
  • Safeguards on chunked data
    • xarray-safeguards
  • Preview
    • Incremental safeguards

Links

  • GitHub
  • PyPI
    • compression-safeguards
    • numcodecs-safeguards
    • xarray-safeguards
compression-safeguards
  • Examples
  • Quantities of Interest (QoIs)
  • Bounding the dSSIM metric
  • Try     View Source

Preserving the structural similarity index for floating-point data (dSSIM) with safeguards¶

In this example, we compare the structural similarity index for floating-point data (dSSIM) [^1] after compressing a dataset of u wind with three different lossy compressors (ZFP, SZ3, SPERR). We then stress-test the quantity of interest (QoI) implementation of the safeguards by preserving an error bound on the dSSIM by translating the dSSIM computation into a quantity of interest, which involves rescaling and linearly quantizing values, applying a 2D Gaussian smoothing kernel, and computing variances and co-variances. We also compare the safeguards with the compressor configuration auto-tuner OptZConfig.

QPET supports mean error bounds over non-overlapping blocks of data, but not over overlapping windows of data, which are required to preserve an error over a convolution. To be safe, QPET would need to be configured with a maximally conservative pointwise error bound, which is no different than configuring SPERR (or SZ3 or ZFP) with this error bound. Furthermore, QPET does not support a rounding/quantisation/binning function. Therefore, we do not compare with QPET in this example.

[^1]: Baker, A. H., Pinard, A. and Hammerling, D. M. (2024). On a Structural Similarity Index Approach for Floating-Point Data. IEEE Transactions on Visualization and Computer Graphics. 30(9), 6261-6274. Available from: doi:10.1109/TVCG.2023.3332843.

Copied!
import ssl

ssl._create_default_https_context = ssl._create_stdlib_context
import ssl ssl._create_default_https_context = ssl._create_stdlib_context
Copied!
from collections import defaultdict
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
from collections import defaultdict 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
Copied!
# Retrieve the data
ERA5 = xr.open_dataset(Path() / "data" / "era5-uv" / "data.nc")
ERA5_U = (
    ERA5["u"]
    .sel(valid_time="2024-04-02T12:00:00", pressure_level=500)
    .astype(np.float64)
)
# Retrieve the data ERA5 = xr.open_dataset(Path() / "data" / "era5-uv" / "data.nc") ERA5_U = ( ERA5["u"] .sel(valid_time="2024-04-02T12:00:00", pressure_level=500) .astype(np.float64) )
Copied!
ERA5_U.shape
ERA5_U.shape
(721, 1440)
Copied!
def dssim_mat(
    a1: np.ndarray,
    a2: np.ndarray,
    eps: float = 1e-8,
    kernel_size: int = 3,
    reproducible: bool = True,
) -> np.ndarray:
    """
    Implementation adapted from the official dSSIM implementation at
    https://github.com/NCAR/ldcpy/blob/6c5bcb8149ec7876a4f53b0e784e9c528f6f14cb/ldcpy/calcs.py#L2516

    The official implementation makes assumptions about the input data that are
    specific to models developed at NCAR which is why we cannot use the official
    implementation directly.

    The implementation has been further adapted to ensure bit-reproducible
    evaluation with the below dSSIM quantity of interest, with the following
    notable changes:

    - we take extra care to ensure all operations are computed in the data
      dtype, i.e. without accidentally upcasting to np.float64
    - the original uses np.round, which rounds halfway cases away from zero,
      we use np.rint which rounds halfway cases to the nearest even integer
    - the original uses astropy.convolution.convolve, which internally upcasts
      to float64 - to avoid rounding differences, we manually implement it here
    - astropy.convolution.convolve handles NaN values by replacing them during
      convolution with a Gaussian kernel by interpolating with the same
      Gaussian kernel, to avoid NaNs bleeding over, and by returning NaN for
      elements that were NaN in the input. We implement a simpler version by
      excluding NaN values from the convolution and returning NaN for elements
      that were NaN in the input
    - convolution requires summation but np.sum internally uses partial sums
      for better rounding, which the safeguards currently do not use, so we
      manually sum
    - we reduce the 11x11 Gaussian kernel to 3x3 for the sake of compute time

    Parameters
    ----------
    x : np.ndarray
        Shape: (latitude, longitude)
    y : np.ndarray
        Shape: (latitude, longitude)
    kernel_size : int
        The size of the Gaussian kernel for the convolution operation in SSIM. Has to be
        an odd number. The default is 3.

    Returns
    -------
    dssim : np.ndarray
        The pointwise data-SSIM values where the k x k kernel is valid.
    """
    assert kernel_size % 2 == 1, "kernel_size must be an odd number."
    assert a1.shape == a2.shape
    assert a1.dtype == a2.dtype

    # re-scale  to [0,1] - if not constant
    smin = min(np.nanmin(a1), np.nanmin(a2))
    smax = max(np.nanmax(a1), np.nanmax(a2))
    r = smax - smin
    if r == 0:  # scale by smax if field is a constant (and smax != 0)
        if smax == 0:
            sc_a1 = a1
            sc_a2 = a2
        else:
            sc_a1 = a1 / smax
            sc_a2 = a2 / smax
    else:
        sc_a1 = (a1 - smin) / r
        sc_a2 = (a2 - smin) / r

    # now quantize to 256 bins
    sc_a1 = np.rint(sc_a1 * 255) / 255
    sc_a2 = np.rint(sc_a2 * 255) / 255

    # 2D gaussian filter
    sigma = a1.dtype.type(1.5)
    pi = a1.dtype.type(np.pi)

    i = np.arange(kernel_size).astype(a1.dtype) - (kernel_size // 2)
    k = 1 / (np.sqrt(2 * pi) * sigma) * np.exp((-np.square(i)) / (2 * np.square(sigma)))
    kernel = np.array([k]).T @ np.array([k])

    def convolve(a, k):
        # pad with zeros
        ap = np.pad(a, kernel_size // 2, mode="constant", constant_values=0)
        # reshape a to (*a.shape, *k.shape)
        av = np.lib.stride_tricks.as_strided(
            ap, a.shape + k.shape, ap.strides + ap.strides
        )

        # broadcast k to (*a.shape, *k.shape)
        kv = np.copy(
            np.broadcast_to(k.reshape((1, 1, kernel_size, kernel_size)), av.shape)
        )

        # exclude weights in kv where av is NaN
        kv[np.isnan(av)] = 0
        # renormalize kv
        kv_flat = kv.reshape(a.shape + (-1,))
        if reproducible:
            # manual np.sum over the last dimension
            kv_acc = np.copy(kv_flat[..., 0])
            for i in range(1, kv_flat.shape[-1]):
                kv_acc += kv_flat[..., i]
        else:
            kv_acc = np.sum(kv_flat, axis=-1)
        kv /= kv_acc.reshape(a.shape + (1, 1))

        # pointwise multiply av with kv, then reshape to (*a.shape, -1)
        ag = (av * kv).reshape(a.shape + (-1,))
        if reproducible:
            # manual np.sum over the last dimension
            acc = np.copy(ag[..., 0])
            for i in range(1, ag.shape[-1]):
                acc += ag[..., i]
        else:
            acc = np.sum(ag, axis=-1)

        # keep NaN for elements that are NaN in a
        return np.where(np.isnan(a), np.nan, acc)

    a1_mu = convolve(sc_a1, kernel)
    a2_mu = convolve(sc_a2, kernel)

    a1a1 = convolve(np.square(sc_a1), kernel)
    a2a2 = convolve(np.square(sc_a2), kernel)

    a1a2 = convolve(sc_a1 * sc_a2, kernel)

    ###########
    var_a1 = a1a1 - np.square(a1_mu)
    var_a2 = a2a2 - np.square(a2_mu)
    cov_a1a2 = a1a2 - a1_mu * a2_mu

    # ssim constants
    C1 = a1.dtype.type(eps)
    C2 = a1.dtype.type(eps)

    ssim_t1 = 2 * a1_mu * a2_mu + C1
    ssim_t2 = 2 * cov_a1a2 + C2

    ssim_b1 = np.square(a1_mu) + np.square(a2_mu) + C1
    ssim_b2 = var_a1 + var_a2 + C2

    ssim_1 = ssim_t1 / ssim_b1
    ssim_2 = ssim_t2 / ssim_b2
    ssim_mat = ssim_1 * ssim_2

    # Cropping the border region of the 2D field where the convolution kernel is not
    # fully overlapping with the 2D input field.
    k = (kernel_size - 1) // 2
    return ssim_mat[k : ssim_mat.shape[0] - k, k : ssim_mat.shape[1] - k]


def dssim(
    a1: np.ndarray,
    a2: np.ndarray,
    eps: float = 1e-8,
    kernel_size: int = 3,
    reproducible: bool = True,
) -> float:
    """
    Implementation adapted from the official dSSIM implementation at
    https://github.com/NCAR/ldcpy/blob/6c5bcb8149ec7876a4f53b0e784e9c528f6f14cb/ldcpy/calcs.py#L2516

    The official implementation makes assumptions about the input data that are
    specific to models developed at NCAR which is why we cannot use the official
    implementation directly.

    Parameters
    ----------
    x : np.ndarray
        Shape: (latitude, longitude)
    y : np.ndarray
        Shape: (latitude, longitude)
    kernel_size : int
        The size of the Gaussian kernel for the convolution operation in SSIM. Has to be
        an odd number. The default is 3.

    Returns
    -------
    dssim : float
        The data-SSIM value between the two input arrays.
    """
    return np.nanmean(
        dssim_mat(a1, a2, eps=eps, kernel_size=kernel_size, reproducible=reproducible)
    )
def dssim_mat( a1: np.ndarray, a2: np.ndarray, eps: float = 1e-8, kernel_size: int = 3, reproducible: bool = True, ) -> np.ndarray: """ Implementation adapted from the official dSSIM implementation at https://github.com/NCAR/ldcpy/blob/6c5bcb8149ec7876a4f53b0e784e9c528f6f14cb/ldcpy/calcs.py#L2516 The official implementation makes assumptions about the input data that are specific to models developed at NCAR which is why we cannot use the official implementation directly. The implementation has been further adapted to ensure bit-reproducible evaluation with the below dSSIM quantity of interest, with the following notable changes: - we take extra care to ensure all operations are computed in the data dtype, i.e. without accidentally upcasting to np.float64 - the original uses np.round, which rounds halfway cases away from zero, we use np.rint which rounds halfway cases to the nearest even integer - the original uses astropy.convolution.convolve, which internally upcasts to float64 - to avoid rounding differences, we manually implement it here - astropy.convolution.convolve handles NaN values by replacing them during convolution with a Gaussian kernel by interpolating with the same Gaussian kernel, to avoid NaNs bleeding over, and by returning NaN for elements that were NaN in the input. We implement a simpler version by excluding NaN values from the convolution and returning NaN for elements that were NaN in the input - convolution requires summation but np.sum internally uses partial sums for better rounding, which the safeguards currently do not use, so we manually sum - we reduce the 11x11 Gaussian kernel to 3x3 for the sake of compute time Parameters ---------- x : np.ndarray Shape: (latitude, longitude) y : np.ndarray Shape: (latitude, longitude) kernel_size : int The size of the Gaussian kernel for the convolution operation in SSIM. Has to be an odd number. The default is 3. Returns ------- dssim : np.ndarray The pointwise data-SSIM values where the k x k kernel is valid. """ assert kernel_size % 2 == 1, "kernel_size must be an odd number." assert a1.shape == a2.shape assert a1.dtype == a2.dtype # re-scale to [0,1] - if not constant smin = min(np.nanmin(a1), np.nanmin(a2)) smax = max(np.nanmax(a1), np.nanmax(a2)) r = smax - smin if r == 0: # scale by smax if field is a constant (and smax != 0) if smax == 0: sc_a1 = a1 sc_a2 = a2 else: sc_a1 = a1 / smax sc_a2 = a2 / smax else: sc_a1 = (a1 - smin) / r sc_a2 = (a2 - smin) / r # now quantize to 256 bins sc_a1 = np.rint(sc_a1 * 255) / 255 sc_a2 = np.rint(sc_a2 * 255) / 255 # 2D gaussian filter sigma = a1.dtype.type(1.5) pi = a1.dtype.type(np.pi) i = np.arange(kernel_size).astype(a1.dtype) - (kernel_size // 2) k = 1 / (np.sqrt(2 * pi) * sigma) * np.exp((-np.square(i)) / (2 * np.square(sigma))) kernel = np.array([k]).T @ np.array([k]) def convolve(a, k): # pad with zeros ap = np.pad(a, kernel_size // 2, mode="constant", constant_values=0) # reshape a to (*a.shape, *k.shape) av = np.lib.stride_tricks.as_strided( ap, a.shape + k.shape, ap.strides + ap.strides ) # broadcast k to (*a.shape, *k.shape) kv = np.copy( np.broadcast_to(k.reshape((1, 1, kernel_size, kernel_size)), av.shape) ) # exclude weights in kv where av is NaN kv[np.isnan(av)] = 0 # renormalize kv kv_flat = kv.reshape(a.shape + (-1,)) if reproducible: # manual np.sum over the last dimension kv_acc = np.copy(kv_flat[..., 0]) for i in range(1, kv_flat.shape[-1]): kv_acc += kv_flat[..., i] else: kv_acc = np.sum(kv_flat, axis=-1) kv /= kv_acc.reshape(a.shape + (1, 1)) # pointwise multiply av with kv, then reshape to (*a.shape, -1) ag = (av * kv).reshape(a.shape + (-1,)) if reproducible: # manual np.sum over the last dimension acc = np.copy(ag[..., 0]) for i in range(1, ag.shape[-1]): acc += ag[..., i] else: acc = np.sum(ag, axis=-1) # keep NaN for elements that are NaN in a return np.where(np.isnan(a), np.nan, acc) a1_mu = convolve(sc_a1, kernel) a2_mu = convolve(sc_a2, kernel) a1a1 = convolve(np.square(sc_a1), kernel) a2a2 = convolve(np.square(sc_a2), kernel) a1a2 = convolve(sc_a1 * sc_a2, kernel) ########### var_a1 = a1a1 - np.square(a1_mu) var_a2 = a2a2 - np.square(a2_mu) cov_a1a2 = a1a2 - a1_mu * a2_mu # ssim constants C1 = a1.dtype.type(eps) C2 = a1.dtype.type(eps) ssim_t1 = 2 * a1_mu * a2_mu + C1 ssim_t2 = 2 * cov_a1a2 + C2 ssim_b1 = np.square(a1_mu) + np.square(a2_mu) + C1 ssim_b2 = var_a1 + var_a2 + C2 ssim_1 = ssim_t1 / ssim_b1 ssim_2 = ssim_t2 / ssim_b2 ssim_mat = ssim_1 * ssim_2 # Cropping the border region of the 2D field where the convolution kernel is not # fully overlapping with the 2D input field. k = (kernel_size - 1) // 2 return ssim_mat[k : ssim_mat.shape[0] - k, k : ssim_mat.shape[1] - k] def dssim( a1: np.ndarray, a2: np.ndarray, eps: float = 1e-8, kernel_size: int = 3, reproducible: bool = True, ) -> float: """ Implementation adapted from the official dSSIM implementation at https://github.com/NCAR/ldcpy/blob/6c5bcb8149ec7876a4f53b0e784e9c528f6f14cb/ldcpy/calcs.py#L2516 The official implementation makes assumptions about the input data that are specific to models developed at NCAR which is why we cannot use the official implementation directly. Parameters ---------- x : np.ndarray Shape: (latitude, longitude) y : np.ndarray Shape: (latitude, longitude) kernel_size : int The size of the Gaussian kernel for the convolution operation in SSIM. Has to be an odd number. The default is 3. Returns ------- dssim : float The data-SSIM value between the two input arrays. """ return np.nanmean( dssim_mat(a1, a2, eps=eps, kernel_size=kernel_size, reproducible=reproducible) )
Copied!
dssim(ERA5_U.values, ERA5_U.values, reproducible=False)
dssim(ERA5_U.values, ERA5_U.values, reproducible=False)
np.float64(1.0)
Copied!
old_cmap_and_norm = earthkit.plots.styles.colors.cmap_and_norm
old_cmap_and_norm = earthkit.plots.styles.colors.cmap_and_norm
Copied!
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 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
Copied!
def compute_corrections_percentage(
    my_ERA5_U: xr.DataArray, orig_ERA5_U: xr.DataArray
) -> float:
    return np.mean(my_ERA5_U != orig_ERA5_U)
def compute_corrections_percentage( my_ERA5_U: xr.DataArray, orig_ERA5_U: xr.DataArray ) -> float: return np.mean(my_ERA5_U != orig_ERA5_U)
Copied!
def plot_u_wind_dssim(
    my_ERA5_U: xr.DataArray,
    cr,
    chart,
    title,
    span,
    dssim_bound,
    error=False,
    corr=None,
    my_ERA5_U_it=None,
    cr_it=None,
    inset=True,
    corr_colours=["white", "green", "lawngreen"],
):
    import copy

    if error:
        with xr.set_options(keep_attrs=True):
            da = ERA5_U[1:-1, 1:-1].copy(
                data=dssim_mat(
                    ERA5_U.values,
                    my_ERA5_U.values,
                    reproducible=False,
                )
            )
            da.attrs.update(long_name=f"dSSIM({da.long_name.lower()})", units=None)
    else:
        da = my_ERA5_U

    # 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,
        )
    )

    style._levels = earthkit.plots.styles.levels.Levels(np.linspace(*span, 22))
    style._legend_kwargs["ticks"] = np.linspace(*span, 5)

    if error:
        style._colors = "cool_r"

    extend_left = np.nanmin(da) < span[0]
    extend_right = np.nanmax(da) > span[1]

    extend = {
        (False, False): "neither",
        (True, False): "min",
        (False, True): "max",
        (True, True): "both",
    }[(extend_left, extend_right)]

    if error:
        style._legend_kwargs["extend"] = extend
        chart.pcolormesh(da, style=style, zorder=-11)

        if corr is not None:
            da_hatch = my_ERA5_U == corr

            if my_ERA5_U_it is None:
                da_corr = da_hatch.astype(float)
            else:
                with xr.set_options(keep_attrs=True):
                    da_hatch_it = my_ERA5_U_it == corr
                da_corr = (~da_hatch).astype(float) + (~da_hatch_it).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(corr_colours),
                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 = ~(da >= dssim_bound)

            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", "darkorange"]),
                vmin=0,
                vmax=1,
                rasterized=True,
            )
            axin.coastlines(color="#555555")
            axin.spines["geo"].set_edgecolor("black")
            axin.set_title(
                f"dSSIM < {dssim_bound}",
                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:
        dssim_mean = np.nanmean(da)
        err_s = ~(dssim_mean >= dssim_bound)
        err_s = r"$\times$" if err_s else r"$\checkmark$"

        t = chart.ax.text(
            0.95,
            0.1,
            f"E[dSSIM]={dssim_mean:.05} ({err_s})",
            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_it is None else rf" ($\times$ {np.round(cr_it, 2)})")
        )
        if error
        else humanize.naturalsize(ERA5_U.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:
        getattr(chart, m)()

    counts, bins = np.histogram(da.values.flatten(), range=span, bins=21)
    midpoints = bins[:-1] + np.diff(bins) / 2
    cb = chart.ax.collections[0].colorbar
    if error:
        if extend_left:
            cb._extend_patches[0].set_hatch("xx")
            cb._extend_patches[0].set_ec("white")
        cb.ax.fill_between(
            [span[0], dssim_bound],
            *cb.ax.get_ylim(),
            hatch="xx",
            ec="w",
            fc="none",
            lw=0,
        )
    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),
            1.0,
        ]
    )
    cax.bar(
        midpoints,
        height=counts,
        width=(bins[-1] - bins[0]) / len(counts),
        color=cb.cmap(cb.norm(midpoints)),
        **(
            dict(
                hatch=["xx" if np.abs(m) < dssim_bound else "" for m in midpoints],
                ec="white",
                lw=0,
            )
            if error
            else dict()
        ),
    )
    if extend_left:
        cax.bar(
            bins[0] - (bins[1] - bins[0]) / 2,
            height=np.sum(da < span[0]),
            width=(bins[-1] - bins[0]) / len(counts),
            color=cb.cmap(cb.norm(midpoints[0])),
            **(
                dict(
                    hatch="xx",
                    ec="white",
                    lw=0,
                )
                if error and span[0] < dssim_bound
                else dict()
            ),
        )
    if extend_right:
        cax.bar(
            bins[-1] + (bins[-1] - bins[-2]) / 2,
            height=np.sum(da > span[1]),
            width=(bins[-1] - bins[0]) / len(counts),
            color=cb.cmap(cb.norm(midpoints[-1])),
            **(
                dict(
                    hatch="xx",
                    ec="white",
                    lw=0,
                )
                if error and span[1] < dssim_bound
                else dict()
            ),
        )
    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[0] - (bins[-1] - bins[-2]) * extend_left,
        span[1] + (bins[-1] - bins[-2]) * extend_right,
    )
    cax.set_xticks([])
    cax.set_yticks([])
    cax.spines[:].set_visible(False)
def plot_u_wind_dssim( my_ERA5_U: xr.DataArray, cr, chart, title, span, dssim_bound, error=False, corr=None, my_ERA5_U_it=None, cr_it=None, inset=True, corr_colours=["white", "green", "lawngreen"], ): import copy if error: with xr.set_options(keep_attrs=True): da = ERA5_U[1:-1, 1:-1].copy( data=dssim_mat( ERA5_U.values, my_ERA5_U.values, reproducible=False, ) ) da.attrs.update(long_name=f"dSSIM({da.long_name.lower()})", units=None) else: da = my_ERA5_U # 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, ) ) style._levels = earthkit.plots.styles.levels.Levels(np.linspace(*span, 22)) style._legend_kwargs["ticks"] = np.linspace(*span, 5) if error: style._colors = "cool_r" extend_left = np.nanmin(da) < span[0] extend_right = np.nanmax(da) > span[1] extend = { (False, False): "neither", (True, False): "min", (False, True): "max", (True, True): "both", }[(extend_left, extend_right)] if error: style._legend_kwargs["extend"] = extend chart.pcolormesh(da, style=style, zorder=-11) if corr is not None: da_hatch = my_ERA5_U == corr if my_ERA5_U_it is None: da_corr = da_hatch.astype(float) else: with xr.set_options(keep_attrs=True): da_hatch_it = my_ERA5_U_it == corr da_corr = (~da_hatch).astype(float) + (~da_hatch_it).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(corr_colours), 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 = ~(da >= dssim_bound) 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", "darkorange"]), vmin=0, vmax=1, rasterized=True, ) axin.coastlines(color="#555555") axin.spines["geo"].set_edgecolor("black") axin.set_title( f"dSSIM < {dssim_bound}", 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: dssim_mean = np.nanmean(da) err_s = ~(dssim_mean >= dssim_bound) err_s = r"$\times$" if err_s else r"$\checkmark$" t = chart.ax.text( 0.95, 0.1, f"E[dSSIM]={dssim_mean:.05} ({err_s})", 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_it is None else rf" ($\times$ {np.round(cr_it, 2)})") ) if error else humanize.naturalsize(ERA5_U.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: getattr(chart, m)() counts, bins = np.histogram(da.values.flatten(), range=span, bins=21) midpoints = bins[:-1] + np.diff(bins) / 2 cb = chart.ax.collections[0].colorbar if error: if extend_left: cb._extend_patches[0].set_hatch("xx") cb._extend_patches[0].set_ec("white") cb.ax.fill_between( [span[0], dssim_bound], *cb.ax.get_ylim(), hatch="xx", ec="w", fc="none", lw=0, ) 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), 1.0, ] ) cax.bar( midpoints, height=counts, width=(bins[-1] - bins[0]) / len(counts), color=cb.cmap(cb.norm(midpoints)), **( dict( hatch=["xx" if np.abs(m) < dssim_bound else "" for m in midpoints], ec="white", lw=0, ) if error else dict() ), ) if extend_left: cax.bar( bins[0] - (bins[1] - bins[0]) / 2, height=np.sum(da < span[0]), width=(bins[-1] - bins[0]) / len(counts), color=cb.cmap(cb.norm(midpoints[0])), **( dict( hatch="xx", ec="white", lw=0, ) if error and span[0] < dssim_bound else dict() ), ) if extend_right: cax.bar( bins[-1] + (bins[-1] - bins[-2]) / 2, height=np.sum(da > span[1]), width=(bins[-1] - bins[0]) / len(counts), color=cb.cmap(cb.norm(midpoints[-1])), **( dict( hatch="xx", ec="white", lw=0, ) if error and span[1] < dssim_bound else dict() ), ) 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[0] - (bins[-1] - bins[-2]) * extend_left, span[1] + (bins[-1] - bins[-2]) * extend_right, ) cax.set_xticks([]) cax.set_yticks([]) cax.spines[:].set_visible(False)
Copied!
def table_u_wind_dssim(
    my_ERA5_U: xr.DataArray,
    cr,
    title,
    dssim_bound,
    corr,
) -> pd.DataFrame:
    err_inf_U = np.amax(np.abs(my_ERA5_U - ERA5_U))
    err_2_U = np.sqrt(np.mean(np.square(my_ERA5_U - ERA5_U)))

    with xr.set_options(keep_attrs=True):
        da = ERA5_U[1:-1, 1:-1].copy(
            data=dssim_mat(
                ERA5_U.values,
                my_ERA5_U.values,
                reproducible=False,
            )
        )

    dssim_mean = np.nanmean(da)
    dssim_min = np.nanmin(da)
    dssim_max = np.nanmax(da)

    dssim_pw_v = np.mean(~(da >= dssim_bound))
    dssim_pw_v = (
        0
        if dssim_pw_v == 0
        else np.format_float_positional(100 * dssim_pw_v, precision=1, min_digits=1)
        + "%"
    )
    if dssim_pw_v == "0.0%":
        dssim_pw_v = "<0.05%"

    err_s = ~(np.mean(da) >= dssim_bound)
    err_s = r"$\times$" if err_s else r"$\checkmark$"

    corr = None if corr is None else compute_corrections_percentage(my_ERA5_U, 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]],
            r"$\epsilon_{abs}$": [title[1]],
            "Safeguarded": [title[2]],
            "Corrections": [title[3]],
            r"$L_{\infty}(\hat{u})$": [
                f"{err_inf_U:.03}",
            ],
            r"$L_{2}(\hat{u})$": [
                f"{err_2_U:.03}",
            ],
            r"$E[\text{D}]$": [
                f"{dssim_mean:.05}",
            ],
            r"$\min(\text{D})$": [
                f"{dssim_min:.03}",
            ],
            r"$\max(\text{D})$": [
                f"{dssim_max:.03}",
            ],
            rf"$\text{{D}} < {dssim_bound}$": [dssim_pw_v],
            "S": [err_s],
            "C": [corr],
            "CR": [
                rf"$\times$ {np.round(cr, 2)}",
            ],
        }
    )
def table_u_wind_dssim( my_ERA5_U: xr.DataArray, cr, title, dssim_bound, corr, ) -> pd.DataFrame: err_inf_U = np.amax(np.abs(my_ERA5_U - ERA5_U)) err_2_U = np.sqrt(np.mean(np.square(my_ERA5_U - ERA5_U))) with xr.set_options(keep_attrs=True): da = ERA5_U[1:-1, 1:-1].copy( data=dssim_mat( ERA5_U.values, my_ERA5_U.values, reproducible=False, ) ) dssim_mean = np.nanmean(da) dssim_min = np.nanmin(da) dssim_max = np.nanmax(da) dssim_pw_v = np.mean(~(da >= dssim_bound)) dssim_pw_v = ( 0 if dssim_pw_v == 0 else np.format_float_positional(100 * dssim_pw_v, precision=1, min_digits=1) + "%" ) if dssim_pw_v == "0.0%": dssim_pw_v = "<0.05%" err_s = ~(np.mean(da) >= dssim_bound) err_s = r"$\times$" if err_s else r"$\checkmark$" corr = None if corr is None else compute_corrections_percentage(my_ERA5_U, 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]], r"$\epsilon_{abs}$": [title[1]], "Safeguarded": [title[2]], "Corrections": [title[3]], r"$L_{\infty}(\hat{u})$": [ f"{err_inf_U:.03}", ], r"$L_{2}(\hat{u})$": [ f"{err_2_U:.03}", ], r"$E[\text{D}]$": [ f"{dssim_mean:.05}", ], r"$\min(\text{D})$": [ f"{dssim_min:.03}", ], r"$\max(\text{D})$": [ f"{dssim_max:.03}", ], rf"$\text{{D}} < {dssim_bound}$": [dssim_pw_v], "S": [err_s], "C": [corr], "CR": [ rf"$\times$ {np.round(cr, 2)}", ], } )
Copied!
import observe

observations = []
import observe observations = []

Lossless compression¶

We first compress the data losslessly with ZStandard at level 22, which gives maximum compression, to provide a baseline.

Copied!
from numcodecs_wasm_zstd import Zstd

zstd = Zstd(level=22)

with observe.observe(zstd, observations):
    ERA5_U_zstd_enc = zstd.encode(ERA5_U.values)
    ERA5_U_zstd = ERA5_U.copy(data=zstd.decode(ERA5_U_zstd_enc))
ERA5_U_zstd_cr = ERA5_U.nbytes / np.asarray(ERA5_U_zstd_enc).nbytes
from numcodecs_wasm_zstd import Zstd zstd = Zstd(level=22) with observe.observe(zstd, observations): ERA5_U_zstd_enc = zstd.encode(ERA5_U.values) ERA5_U_zstd = ERA5_U.copy(data=zstd.decode(ERA5_U_zstd_enc)) ERA5_U_zstd_cr = ERA5_U.nbytes / np.asarray(ERA5_U_zstd_enc).nbytes

Compressing u with lossy compressors¶

We compare each compressor with two absolute error bounds, 1 m/s and 0.1 m/s, to compare the dSSIM scores they achieve.

Copied!
from numcodecs_wasm_sperr import Sperr
from numcodecs_wasm_sz3 import Sz3
from numcodecs_wasm_zfp import Zfp
from numcodecs_zero import ZeroCodec
from numcodecs_wasm_sperr import Sperr from numcodecs_wasm_sz3 import Sz3 from numcodecs_wasm_zfp import Zfp from numcodecs_zero import ZeroCodec
Copied!
eb_abs = [1.0, 0.1]
eb_abs = [1.0, 0.1]
Copied!
ERA5_U_codec = defaultdict(dict)
ERA5_U_codec_cr = defaultdict(dict)

for ea in eb_abs:
    for codec in [
        Zfp(mode="fixed-accuracy", tolerance=ea),
        Sz3(eb_mode="abs", eb_abs=ea),
        Sperr(mode="pwe", pwe=ea),
        ZeroCodec(),
    ]:
        with observe.observe(codec, observations):
            ERA5_U_codec_enc = codec.encode(ERA5_U.values)
            ERA5_U_codec[codec.codec_id][ea] = ERA5_U.copy(
                data=codec.decode(ERA5_U_codec_enc)
            )
        ERA5_U_codec_cr[codec.codec_id][ea] = (
            ERA5_U.nbytes / np.asarray(ERA5_U_codec_enc).nbytes
        )
ERA5_U_codec = defaultdict(dict) ERA5_U_codec_cr = defaultdict(dict) for ea in eb_abs: for codec in [ Zfp(mode="fixed-accuracy", tolerance=ea), Sz3(eb_mode="abs", eb_abs=ea), Sperr(mode="pwe", pwe=ea), ZeroCodec(), ]: with observe.observe(codec, observations): ERA5_U_codec_enc = codec.encode(ERA5_U.values) ERA5_U_codec[codec.codec_id][ea] = ERA5_U.copy( data=codec.decode(ERA5_U_codec_enc) ) ERA5_U_codec_cr[codec.codec_id][ea] = ( ERA5_U.nbytes / np.asarray(ERA5_U_codec_enc).nbytes )

Compressing u using the safeguarded lossy compressors¶

We configure the safeguards to bound the dSSIM to be $\geq 0.995$ using a spatial quantity of interest (QoI). In the translation of the dSSIM calculation to a QoI, we make use of the following tricks:

  • we preserve the global minimum and maximum exactly, using sign safeguards with offsets, to ensure that the linear quantisation range is constant
  • we manually create a 2D Gaussian kernel in the QoI expression
  • we use valid boundary conditions to only compute the pointwise dSSIM for points that have a valid 3x3 neighbourhood
  • we compute the dSSIM of the (compressed) data with respect to the constant original data. When the QoI is evaluated on the original data, this computes $dSSIM(u, u) = 1$
  • we bound an absolute error of $\epsilon_{abs} = 1 - 0.995$ on the QoI to ensure that $dSSIM(u, \hat{u}) \geq 0.995$
  • we conservatively bound the pointwise dSSIM, not the mean of the dSSIM
Copied!
from compression_safeguards import SafeguardKind

# Baker et al. recommend one of [0.99, 0.995, 0.99919]
dssim_bound = 0.995

qoi_eb_stencil = SafeguardKind.qoi_eb_stencil.value(
    qoi="""
    # dssim constants
    V["sigma"] = 1.5;
    V["C1"] = 1e-8;
    V["C2"] = 1e-8;
    
    # we guarantee that
    #  min(data) = min(corrected) and
    #  max(data) = max(corrected)
    # with the sign safeguards above
    V["smin"] = c["$x_min"];
    V["smax"] = c["$x_max"];
    V["r"] = V["smax"] - V["smin"];
    
    # re-scale to [0-1] and quantize to 256 bins
    V["sc_a1"] = round_ties_even(((C["$X"] - V["smin"]) / V["r"]) * 255) / 255;
    V["sc_a2"] = round_ties_even(((X - V["smin"]) / V["r"]) * 255) / 255;

    # create a 2D 3x3 Gaussian kernel
    V["i"] = [-1, 0, 1];
    V["k"] = 1/(sqrt(2*pi)*V["sigma"]) * exp((-square(V["i"])) / (2*square(V["sigma"])));
    V["kern"] = matmul([V["k"]].T, [V["k"]]);

    # handle sparse NaN values by excluding them from the convolution,
    # then renormalize the kernel
    V["kern_no_NaN1"] = where(isnan(V["sc_a1"]), 0, V["kern"]);
    V["kernel1"] = V["kern_no_NaN1"] / sum(V["kern_no_NaN1"]);
    V["kern_no_NaN2"] = where(isnan(V["sc_a2"]), 0, V["kern"]);
    V["kernel2"] = V["kern_no_NaN2"] / sum(V["kern_no_NaN2"]);
    V["kern_no_NaN12"] = where(isnan(V["sc_a1"] * V["sc_a2"]), 0, V["kern"]);
    V["kernel12"] = V["kern_no_NaN12"] / sum(V["kern_no_NaN12"]);

    # apply the Gaussian filter via convolution
    V["a1_mu_noNaN"] = sum(V["sc_a1"] * V["kernel1"]);
    V["a2_mu_noNaN"] = sum(V["sc_a2"] * V["kernel2"]);
    V["a1a1_noNaN"] = sum(square(V["sc_a1"]) * V["kernel1"]);
    V["a2a2_noNaN"] = sum(square(V["sc_a2"]) * V["kernel2"]);
    V["a1a2_noNaN"] = sum(V["sc_a1"] * V["sc_a2"] * V["kernel12"]);

    # keep NaN for elements that are originally NaN
    V["a1_mu"] = where(isnan(V["sc_a1"])[I], NaN, V["a1_mu_noNaN"]);
    V["a2_mu"] = where(isnan(V["sc_a2"])[I], NaN, V["a2_mu_noNaN"]);
    V["a1a1"] = where(isnan(V["sc_a1"])[I], NaN, V["a1a1_noNaN"]);
    V["a2a2"] = where(isnan(V["sc_a2"])[I], NaN, V["a2a2_noNaN"]);
    V["a1a2"] = where(isnan(V["sc_a1"] * V["sc_a2"])[I], NaN, V["a1a2_noNaN"]);

    ###########
    V["var_a1"] = V["a1a1"] - square(V["a1_mu"]);
    V["var_a2"] = V["a2a2"] - square(V["a2_mu"]);
    V["cov_a1a2"] = V["a1a2"] - V["a1_mu"] * V["a2_mu"];
    
    # compute the SSIM components
    V["ssim_t1"] = 2 * V["a1_mu"] * V["a2_mu"] + V["C1"];
    V["ssim_t2"] = 2 * V["cov_a1a2"] + V["C2"];
    V["ssim_b1"] = square(V["a1_mu"]) + square(V["a2_mu"]) + V["C1"];
    V["ssim_b2"] = V["var_a1"] + V["var_a2"] + V["C2"];

    V["ssim_1"] = V["ssim_t1"] / V["ssim_b1"];
    V["ssim_2"] = V["ssim_t2"] / V["ssim_b2"];
    
    # compute the pointwise dSSIM
    return V["ssim_1"] * V["ssim_2"];
    """,
    type="abs",
    eb=1 - dssim_bound,
    # 3x3 neighbourhood
    neighbourhood=[
        # latitude
        dict(axis=0, before=1, after=1, boundary="valid"),
        # longitude
        dict(axis=1, before=1, after=1, boundary="valid"),
    ],
)
qoi_eb_stencil_expr = qoi_eb_stencil._qoi_expr._expr
from compression_safeguards import SafeguardKind # Baker et al. recommend one of [0.99, 0.995, 0.99919] dssim_bound = 0.995 qoi_eb_stencil = SafeguardKind.qoi_eb_stencil.value( qoi=""" # dssim constants V["sigma"] = 1.5; V["C1"] = 1e-8; V["C2"] = 1e-8; # we guarantee that # min(data) = min(corrected) and # max(data) = max(corrected) # with the sign safeguards above V["smin"] = c["$x_min"]; V["smax"] = c["$x_max"]; V["r"] = V["smax"] - V["smin"]; # re-scale to [0-1] and quantize to 256 bins V["sc_a1"] = round_ties_even(((C["$X"] - V["smin"]) / V["r"]) * 255) / 255; V["sc_a2"] = round_ties_even(((X - V["smin"]) / V["r"]) * 255) / 255; # create a 2D 3x3 Gaussian kernel V["i"] = [-1, 0, 1]; V["k"] = 1/(sqrt(2*pi)*V["sigma"]) * exp((-square(V["i"])) / (2*square(V["sigma"]))); V["kern"] = matmul([V["k"]].T, [V["k"]]); # handle sparse NaN values by excluding them from the convolution, # then renormalize the kernel V["kern_no_NaN1"] = where(isnan(V["sc_a1"]), 0, V["kern"]); V["kernel1"] = V["kern_no_NaN1"] / sum(V["kern_no_NaN1"]); V["kern_no_NaN2"] = where(isnan(V["sc_a2"]), 0, V["kern"]); V["kernel2"] = V["kern_no_NaN2"] / sum(V["kern_no_NaN2"]); V["kern_no_NaN12"] = where(isnan(V["sc_a1"] * V["sc_a2"]), 0, V["kern"]); V["kernel12"] = V["kern_no_NaN12"] / sum(V["kern_no_NaN12"]); # apply the Gaussian filter via convolution V["a1_mu_noNaN"] = sum(V["sc_a1"] * V["kernel1"]); V["a2_mu_noNaN"] = sum(V["sc_a2"] * V["kernel2"]); V["a1a1_noNaN"] = sum(square(V["sc_a1"]) * V["kernel1"]); V["a2a2_noNaN"] = sum(square(V["sc_a2"]) * V["kernel2"]); V["a1a2_noNaN"] = sum(V["sc_a1"] * V["sc_a2"] * V["kernel12"]); # keep NaN for elements that are originally NaN V["a1_mu"] = where(isnan(V["sc_a1"])[I], NaN, V["a1_mu_noNaN"]); V["a2_mu"] = where(isnan(V["sc_a2"])[I], NaN, V["a2_mu_noNaN"]); V["a1a1"] = where(isnan(V["sc_a1"])[I], NaN, V["a1a1_noNaN"]); V["a2a2"] = where(isnan(V["sc_a2"])[I], NaN, V["a2a2_noNaN"]); V["a1a2"] = where(isnan(V["sc_a1"] * V["sc_a2"])[I], NaN, V["a1a2_noNaN"]); ########### V["var_a1"] = V["a1a1"] - square(V["a1_mu"]); V["var_a2"] = V["a2a2"] - square(V["a2_mu"]); V["cov_a1a2"] = V["a1a2"] - V["a1_mu"] * V["a2_mu"]; # compute the SSIM components V["ssim_t1"] = 2 * V["a1_mu"] * V["a2_mu"] + V["C1"]; V["ssim_t2"] = 2 * V["cov_a1a2"] + V["C2"]; V["ssim_b1"] = square(V["a1_mu"]) + square(V["a2_mu"]) + V["C1"]; V["ssim_b2"] = V["var_a1"] + V["var_a2"] + V["C2"]; V["ssim_1"] = V["ssim_t1"] / V["ssim_b1"]; V["ssim_2"] = V["ssim_t2"] / V["ssim_b2"]; # compute the pointwise dSSIM return V["ssim_1"] * V["ssim_2"]; """, type="abs", eb=1 - dssim_bound, # 3x3 neighbourhood neighbourhood=[ # latitude dict(axis=0, before=1, after=1, boundary="valid"), # longitude dict(axis=1, before=1, after=1, boundary="valid"), ], ) qoi_eb_stencil_expr = qoi_eb_stencil._qoi_expr._expr

First, we ensure that the dssim_mat function and the dSSIM quantity of interest produce equivalent results, to ensure that bounding the quantity of interest also bounds the dssim score we later showcase.

When using the original precision of the data, float32, both methods produce the same results, but due to the limited precision some pointwise dSSIM values are outside the expected range of [-1, 1].

Copied!
from compression_safeguards.utils.bindings import Bindings

a1 = ERA5_U.values.astype(np.float32)
a2 = ERA5_U_codec["zfp.rs"][eb_abs[0]].values.astype(np.float32)

dssim_py = dssim_mat(a1, a2)
dssim_qoi = qoi_eb_stencil.evaluate_qoi(
    a2,
    late_bound=Bindings(
        **{
            "$x": a1,
            "$X": a1,
            "$x_min": min(np.nanmin(a1), np.nanmin(a2)),
            "$x_max": max(np.nanmax(a1), np.nanmax(a2)),
        }
    ),
)

assert np.all(dssim_py == dssim_qoi)

print(
    f"min(dSSIM)={np.nanmin(dssim_py)} "
    f"max(dSSIM)={np.nanmax(dssim_py)} "
    f"mean(dSSIM)={np.nanmean(dssim_py)} "
    f"outside(dSSIM)={np.mean((dssim_py < 0.0) | (dssim_py > 1.0)) * 100}%"
)
from compression_safeguards.utils.bindings import Bindings a1 = ERA5_U.values.astype(np.float32) a2 = ERA5_U_codec["zfp.rs"][eb_abs[0]].values.astype(np.float32) dssim_py = dssim_mat(a1, a2) dssim_qoi = qoi_eb_stencil.evaluate_qoi( a2, late_bound=Bindings( **{ "$x": a1, "$X": a1, "$x_min": min(np.nanmin(a1), np.nanmin(a2)), "$x_max": max(np.nanmax(a1), np.nanmax(a2)), } ), ) assert np.all(dssim_py == dssim_qoi) print( f"min(dSSIM)={np.nanmin(dssim_py)} " f"max(dSSIM)={np.nanmax(dssim_py)} " f"mean(dSSIM)={np.nanmean(dssim_py)} " f"outside(dSSIM)={np.mean((dssim_py < 0.0) | (dssim_py > 1.0)) * 100}%" )
min(dSSIM)=-2.0402638912200928 max(dSSIM)=10.120443344116211 mean(dSSIM)=0.8803545832633972 outside(dSSIM)=1.963397625739659%

When using extended precision, float64, fewer dSSIM values are outside the expected range. Therefore, we use float64 precision for the data in this example.

Copied!
a1 = ERA5_U.values.astype(np.float64)
a2 = ERA5_U_codec["zfp.rs"][eb_abs[0]].values.astype(np.float64)

dssim_py = dssim_mat(a1, a2)
dssim_qoi = qoi_eb_stencil.evaluate_qoi(
    a2,
    late_bound=Bindings(
        **{
            "$x": a1,
            "$X": a1,
            "$x_min": min(np.nanmin(a1), np.nanmin(a2)),
            "$x_max": max(np.nanmax(a1), np.nanmax(a2)),
        }
    ),
)

assert np.all(dssim_py == dssim_qoi)

print(
    f"min(dSSIM)={np.nanmin(dssim_py)} "
    f"max(dSSIM)={np.nanmax(dssim_py)} "
    f"mean(dSSIM)={np.nanmean(dssim_py)} "
    f"outside(dSSIM)={np.mean((dssim_py < 0.0) | (dssim_py > 1.0)) * 100}%"
)
a1 = ERA5_U.values.astype(np.float64) a2 = ERA5_U_codec["zfp.rs"][eb_abs[0]].values.astype(np.float64) dssim_py = dssim_mat(a1, a2) dssim_qoi = qoi_eb_stencil.evaluate_qoi( a2, late_bound=Bindings( **{ "$x": a1, "$X": a1, "$x_min": min(np.nanmin(a1), np.nanmin(a2)), "$x_max": max(np.nanmax(a1), np.nanmax(a2)), } ), ) assert np.all(dssim_py == dssim_qoi) print( f"min(dSSIM)={np.nanmin(dssim_py)} " f"max(dSSIM)={np.nanmax(dssim_py)} " f"mean(dSSIM)={np.nanmean(dssim_py)} " f"outside(dSSIM)={np.mean((dssim_py < 0.0) | (dssim_py > 1.0)) * 100}%" )
min(dSSIM)=-0.9969480854354561 max(dSSIM)=1.0 mean(dSSIM)=0.8812509543261983 outside(dSSIM)=0.28232303790808205%

Since computing the safeguards correction for the dSSIM quantity of interest takes a while, we also use a development reporting API to inject a progress bar into the QoI computation.

Copied!
import tqdm

from compression_safeguards.safeguards._qois.expr import map_expr
from compression_safeguards.safeguards._qois.expr.abc import AnyExpr
from compression_safeguards.safeguards._qois.expr.reporter import (
    Reporter,
    ReportingExpr,
)


def data_expr_size(expr: AnyExpr) -> int:
    if not expr.has_data:
        return 0

    args_size = sum(data_expr_size(a) for a in expr.args)

    if isinstance(expr, ReportingExpr):
        return args_size

    return args_size + 1


class TqdmReporter(Reporter):
    __slots__ = ("_tqdm", "_current", "_stack")
    _tqdm: None | tqdm.tqdm
    _current: int
    _stack: list[int]

    def __init__(self) -> None:
        self._tqdm = None
        self._current = 0
        self._stack = []

    def enter(self, expr: AnyExpr) -> None:
        size = data_expr_size(expr)
        if self._tqdm is None:
            self._tqdm = tqdm.tqdm(
                total=size, desc="QoI", postfix=dict(depth=len(self._stack))
            )
            self._current = 0
        self._stack.append(self._current + size)
        self._tqdm.set_postfix(refresh=False, depth=len(self._stack))

    def exit(self, expr: AnyExpr) -> None:
        after = self._stack.pop()
        self._tqdm.set_postfix(refresh=False, depth=len(self._stack))
        self._tqdm.update(after - self._current)
        self._current = after
        if len(self._stack) > 0:
            return
        self._tqdm.close()
        self._tqdm = None
        self._current = 0


reporter = TqdmReporter()
qoi_eb_stencil._qoi_expr._expr = map_expr(
    qoi_eb_stencil_expr, mapper=lambda e: ReportingExpr(e, reporter)
)
import tqdm from compression_safeguards.safeguards._qois.expr import map_expr from compression_safeguards.safeguards._qois.expr.abc import AnyExpr from compression_safeguards.safeguards._qois.expr.reporter import ( Reporter, ReportingExpr, ) def data_expr_size(expr: AnyExpr) -> int: if not expr.has_data: return 0 args_size = sum(data_expr_size(a) for a in expr.args) if isinstance(expr, ReportingExpr): return args_size return args_size + 1 class TqdmReporter(Reporter): __slots__ = ("_tqdm", "_current", "_stack") _tqdm: None | tqdm.tqdm _current: int _stack: list[int] def __init__(self) -> None: self._tqdm = None self._current = 0 self._stack = [] def enter(self, expr: AnyExpr) -> None: size = data_expr_size(expr) if self._tqdm is None: self._tqdm = tqdm.tqdm( total=size, desc="QoI", postfix=dict(depth=len(self._stack)) ) self._current = 0 self._stack.append(self._current + size) self._tqdm.set_postfix(refresh=False, depth=len(self._stack)) def exit(self, expr: AnyExpr) -> None: after = self._stack.pop() self._tqdm.set_postfix(refresh=False, depth=len(self._stack)) self._tqdm.update(after - self._current) self._current = after if len(self._stack) > 0: return self._tqdm.close() self._tqdm = None self._current = 0 reporter = TqdmReporter() qoi_eb_stencil._qoi_expr._expr = map_expr( qoi_eb_stencil_expr, mapper=lambda e: ReportingExpr(e, reporter) )

Finally, we use the numcodecs-safeguards frontend to wrap the safeguards around several different lossy compressors.

Copied!
from numcodecs_safeguards import SafeguardedCodec
from numcodecs_safeguards import SafeguardedCodec
Copied!
ERA5_U_sg = defaultdict(dict)
ERA5_U_sg_cr = defaultdict(dict)

for ea in eb_abs:
    for codec in [
        ZeroCodec(),
        Zfp(mode="fixed-accuracy", tolerance=ea),
        Sz3(eb_mode="abs", eb_abs=ea),
        Sperr(mode="pwe", pwe=ea),
    ]:
        codec_sg = SafeguardedCodec(
            codec=codec,
            safeguards=[
                # guarantee that the global minimum and maximum are preserved,
                #  which simplifies the rescaling
                dict(kind="sign", offset="$x_min"),
                dict(kind="sign", offset="$x_max"),
                qoi_eb_stencil,
            ],
        )

        with observe.observe(codec_sg, observations):
            ERA5_U_sg_enc = codec_sg.encode(ERA5_U.values)
            ERA5_U_sg[codec.codec_id][ea] = ERA5_U.copy(
                data=codec_sg.decode(ERA5_U_sg_enc)
            )

        ERA5_U_sg_cr[codec.codec_id][ea] = (
            ERA5_U.nbytes / np.asarray(ERA5_U_sg_enc).nbytes
        )
ERA5_U_sg = defaultdict(dict) ERA5_U_sg_cr = defaultdict(dict) for ea in eb_abs: for codec in [ ZeroCodec(), Zfp(mode="fixed-accuracy", tolerance=ea), Sz3(eb_mode="abs", eb_abs=ea), Sperr(mode="pwe", pwe=ea), ]: codec_sg = SafeguardedCodec( codec=codec, safeguards=[ # guarantee that the global minimum and maximum are preserved, # which simplifies the rescaling dict(kind="sign", offset="$x_min"), dict(kind="sign", offset="$x_max"), qoi_eb_stencil, ], ) with observe.observe(codec_sg, observations): ERA5_U_sg_enc = codec_sg.encode(ERA5_U.values) ERA5_U_sg[codec.codec_id][ea] = ERA5_U.copy( data=codec_sg.decode(ERA5_U_sg_enc) ) ERA5_U_sg_cr[codec.codec_id][ea] = ( ERA5_U.nbytes / np.asarray(ERA5_U_sg_enc).nbytes )
QoI: 100%|██████████████████████████████████████████████████████████████████████| 7453/7453 [03:00<00:00, 41.29it/s, depth=0]
QoI: 100%|██████████████████████████████████████████████████████████████████████| 7453/7453 [03:00<00:00, 41.34it/s, depth=0]
QoI: 100%|██████████████████████████████████████████████████████████████████████| 7453/7453 [03:00<00:00, 41.20it/s, depth=0]
QoI: 100%|██████████████████████████████████████████████████████████████████████| 7453/7453 [03:22<00:00, 36.89it/s, depth=0]
QoI: 100%|██████████████████████████████████████████████████████████████████████| 7453/7453 [03:13<00:00, 38.54it/s, depth=0]
QoI: 100%|██████████████████████████████████████████████████████████████████████| 7453/7453 [03:03<00:00, 40.56it/s, depth=0]
QoI: 100%|██████████████████████████████████████████████████████████████████████| 7453/7453 [03:01<00:00, 41.01it/s, depth=0]
QoI: 100%|██████████████████████████████████████████████████████████████████████| 7453/7453 [03:04<00:00, 40.47it/s, depth=0]
Copied!
ERA5_U_sg_it = defaultdict(dict)
ERA5_U_sg_it_cr = defaultdict(dict)

for ea in eb_abs:
    for codec in [
        ZeroCodec(),
        Zfp(mode="fixed-accuracy", tolerance=ea),
        Sz3(eb_mode="abs", eb_abs=ea),
        Sperr(mode="pwe", pwe=ea),
    ]:
        codec_sg = SafeguardedCodec(
            codec=codec,
            safeguards=[
                # guarantee that the global minimum and maximum are preserved,
                #  which simplifies the rescaling
                dict(kind="sign", offset="$x_min"),
                dict(kind="sign", offset="$x_max"),
                qoi_eb_stencil,
            ],
            # use iteration to refine the corrections
            compute=dict(unstable_iterative=True),
        )

        with observe.observe(codec_sg, observations):
            ERA5_U_sg_it_enc = codec_sg.encode(ERA5_U.values)
            ERA5_U_sg_it[codec.codec_id][ea] = ERA5_U.copy(
                data=codec_sg.decode(ERA5_U_sg_it_enc)
            )

        ERA5_U_sg_it_cr[codec.codec_id][ea] = (
            ERA5_U.nbytes / np.asarray(ERA5_U_sg_it_enc).nbytes
        )
ERA5_U_sg_it = defaultdict(dict) ERA5_U_sg_it_cr = defaultdict(dict) for ea in eb_abs: for codec in [ ZeroCodec(), Zfp(mode="fixed-accuracy", tolerance=ea), Sz3(eb_mode="abs", eb_abs=ea), Sperr(mode="pwe", pwe=ea), ]: codec_sg = SafeguardedCodec( codec=codec, safeguards=[ # guarantee that the global minimum and maximum are preserved, # which simplifies the rescaling dict(kind="sign", offset="$x_min"), dict(kind="sign", offset="$x_max"), qoi_eb_stencil, ], # use iteration to refine the corrections compute=dict(unstable_iterative=True), ) with observe.observe(codec_sg, observations): ERA5_U_sg_it_enc = codec_sg.encode(ERA5_U.values) ERA5_U_sg_it[codec.codec_id][ea] = ERA5_U.copy( data=codec_sg.decode(ERA5_U_sg_it_enc) ) ERA5_U_sg_it_cr[codec.codec_id][ea] = ( ERA5_U.nbytes / np.asarray(ERA5_U_sg_it_enc).nbytes )
QoI: 100%|██████████████████████████████████████████████████████████████████████| 7453/7453 [03:05<00:00, 40.18it/s, depth=0]
QoI: 100%|██████████████████████████████████████████████████████████████████████| 7453/7453 [03:02<00:00, 40.89it/s, depth=0]
QoI: 100%|██████████████████████████████████████████████████████████████████████| 7453/7453 [03:13<00:00, 38.47it/s, depth=0]
QoI: 100%|██████████████████████████████████████████████████████████████████████| 7453/7453 [03:04<00:00, 40.38it/s, depth=0]
QoI: 100%|██████████████████████████████████████████████████████████████████████| 7453/7453 [03:00<00:00, 41.23it/s, depth=0]
QoI: 100%|██████████████████████████████████████████████████████████████████████| 7453/7453 [03:38<00:00, 34.04it/s, depth=0]
QoI: 100%|██████████████████████████████████████████████████████████████████████| 7453/7453 [03:12<00:00, 38.67it/s, depth=0]
QoI: 100%|██████████████████████████████████████████████████████████████████████| 7453/7453 [03:21<00:00, 37.06it/s, depth=0]
Copied!
ERA5_U_sg_lossless = defaultdict(dict)
ERA5_U_sg_lossless_cr = defaultdict(dict)

for ea in eb_abs:
    for codec in [
        ZeroCodec(),
        Zfp(mode="fixed-accuracy", tolerance=ea),
        Sz3(eb_mode="abs", eb_abs=ea),
        Sperr(mode="pwe", pwe=ea),
    ]:
        codec_sg = SafeguardedCodec(
            codec=codec,
            safeguards=[
                # guarantee that the global minimum and maximum are preserved,
                #  which simplifies the rescaling
                dict(kind="sign", offset="$x_min"),
                dict(kind="sign", offset="$x_max"),
                qoi_eb_stencil,
            ],
            # produce lossless corrections and refine them with iteration
            compute=dict(unstable_iterative=True, unstable_lossless_corrections=True),
        )

        with observe.observe(codec_sg, observations):
            ERA5_U_sg_lossless_enc = codec_sg.encode(ERA5_U.values)
            ERA5_U_sg_lossless[codec.codec_id][ea] = ERA5_U.copy(
                data=codec_sg.decode(ERA5_U_sg_lossless_enc)
            )

        ERA5_U_sg_lossless_cr[codec.codec_id][ea] = (
            ERA5_U.nbytes / np.asarray(ERA5_U_sg_lossless_enc).nbytes
        )
ERA5_U_sg_lossless = defaultdict(dict) ERA5_U_sg_lossless_cr = defaultdict(dict) for ea in eb_abs: for codec in [ ZeroCodec(), Zfp(mode="fixed-accuracy", tolerance=ea), Sz3(eb_mode="abs", eb_abs=ea), Sperr(mode="pwe", pwe=ea), ]: codec_sg = SafeguardedCodec( codec=codec, safeguards=[ # guarantee that the global minimum and maximum are preserved, # which simplifies the rescaling dict(kind="sign", offset="$x_min"), dict(kind="sign", offset="$x_max"), qoi_eb_stencil, ], # produce lossless corrections and refine them with iteration compute=dict(unstable_iterative=True, unstable_lossless_corrections=True), ) with observe.observe(codec_sg, observations): ERA5_U_sg_lossless_enc = codec_sg.encode(ERA5_U.values) ERA5_U_sg_lossless[codec.codec_id][ea] = ERA5_U.copy( data=codec_sg.decode(ERA5_U_sg_lossless_enc) ) ERA5_U_sg_lossless_cr[codec.codec_id][ea] = ( ERA5_U.nbytes / np.asarray(ERA5_U_sg_lossless_enc).nbytes )

For comparison, we also check how the SZ3 compressor would perform with a pointwise normalised absolute error (NOA) bound of 1/255, corresponding to the width of the bins that the dSSIM metric first quantizes the data into.

Copied!
noa = Sz3(
    eb_mode="abs", eb_abs=(np.nanmax(ERA5_U.values) - np.nanmin(ERA5_U.values)) / 255
)

with observe.observe(noa, observations):
    ERA5_U_noa_enc = noa.encode(ERA5_U.values)
    ERA5_U_noa = ERA5_U.copy(data=noa.decode(ERA5_U_noa_enc))

ERA5_U_noa_cr = ERA5_U.nbytes / ERA5_U_noa_enc.nbytes
noa = Sz3( eb_mode="abs", eb_abs=(np.nanmax(ERA5_U.values) - np.nanmin(ERA5_U.values)) / 255 ) with observe.observe(noa, observations): ERA5_U_noa_enc = noa.encode(ERA5_U.values) ERA5_U_noa = ERA5_U.copy(data=noa.decode(ERA5_U_noa_enc)) ERA5_U_noa_cr = ERA5_U.nbytes / ERA5_U_noa_enc.nbytes

We also compare a much simpler and cheaper quantity of interest, which ensures that the quantized data values remain the same. In effect, this method guarantees a perfect dSSIM score of $1.0$. At the time of writing, this method produces the same results as the more complex dSSIM quantity of interest (the dSSIM QoI contains several sums and products that need need to handle worst case error propagation, which likely reduces the error bound on the quantized values to near zero, thus arriving at the same result). However, future improvements in the quantity of interest safeguard, e.g. incremental corrections, could allow the dSSIM quantity of interest to compress better in the future.

Copied!
codec_sg_rint = SafeguardedCodec(
    codec=ZeroCodec(),
    safeguards=[
        # guarantee that the global minimum and maximum are preserved,
        #  which simplifies the rescaling
        dict(kind="sign", offset="$x_min"),
        dict(kind="sign", offset="$x_max"),
        dict(
            kind="qoi_eb_pw",
            qoi="""
            # we guarantee that
            #  min(data) = min(corrected) and
            #  max(data) = max(corrected)
            # with the sign safeguards above
            v["smin"] = c["$x_min"];
            v["smax"] = c["$x_max"];
            v["r"] = v["smax"] - v["smin"];
            
            # re-scale to [0-1] and quantize to 256 bins
            v["sc_a2"] = round_ties_even(((x - v["smin"]) / v["r"]) * 255) / 255;
            
            # force the quantized value to stay the same
            return v["sc_a2"];
            """,
            type="abs",
            eb=0,
        ),
    ],
)

with observe.observe(codec_sg_rint, observations):
    ERA5_U_sg_rint_enc = codec_sg_rint.encode(ERA5_U.values)
    ERA5_U_sg_rint = ERA5_U.copy(data=codec_sg_rint.decode(ERA5_U_sg_rint_enc))

ERA5_U_sg_rint_cr = ERA5_U.nbytes / np.asarray(ERA5_U_sg_rint_enc).nbytes
codec_sg_rint = SafeguardedCodec( codec=ZeroCodec(), safeguards=[ # guarantee that the global minimum and maximum are preserved, # which simplifies the rescaling dict(kind="sign", offset="$x_min"), dict(kind="sign", offset="$x_max"), dict( kind="qoi_eb_pw", qoi=""" # we guarantee that # min(data) = min(corrected) and # max(data) = max(corrected) # with the sign safeguards above v["smin"] = c["$x_min"]; v["smax"] = c["$x_max"]; v["r"] = v["smax"] - v["smin"]; # re-scale to [0-1] and quantize to 256 bins v["sc_a2"] = round_ties_even(((x - v["smin"]) / v["r"]) * 255) / 255; # force the quantized value to stay the same return v["sc_a2"]; """, type="abs", eb=0, ), ], ) with observe.observe(codec_sg_rint, observations): ERA5_U_sg_rint_enc = codec_sg_rint.encode(ERA5_U.values) ERA5_U_sg_rint = ERA5_U.copy(data=codec_sg_rint.decode(ERA5_U_sg_rint_enc)) ERA5_U_sg_rint_cr = ERA5_U.nbytes / np.asarray(ERA5_U_sg_rint_enc).nbytes
Copied!
codec_sg_it_rint = SafeguardedCodec(
    codec=ZeroCodec(),
    safeguards=codec_sg_rint.safeguards,
    # use iteration to refine the corrections
    compute=dict(unstable_iterative=True),
)

with observe.observe(codec_sg_it_rint, observations):
    ERA5_U_sg_it_rint_enc = codec_sg_it_rint.encode(ERA5_U.values)
    ERA5_U_sg_it_rint = ERA5_U.copy(data=codec_sg_it_rint.decode(ERA5_U_sg_it_rint_enc))

ERA5_U_sg_it_rint_cr = ERA5_U.nbytes / np.asarray(ERA5_U_sg_it_rint_enc).nbytes
codec_sg_it_rint = SafeguardedCodec( codec=ZeroCodec(), safeguards=codec_sg_rint.safeguards, # use iteration to refine the corrections compute=dict(unstable_iterative=True), ) with observe.observe(codec_sg_it_rint, observations): ERA5_U_sg_it_rint_enc = codec_sg_it_rint.encode(ERA5_U.values) ERA5_U_sg_it_rint = ERA5_U.copy(data=codec_sg_it_rint.decode(ERA5_U_sg_it_rint_enc)) ERA5_U_sg_it_rint_cr = ERA5_U.nbytes / np.asarray(ERA5_U_sg_it_rint_enc).nbytes
Copied!
codec_sg_lossless_rint = SafeguardedCodec(
    codec=ZeroCodec(),
    safeguards=codec_sg_rint.safeguards,
    # produce lossless corrections and refine them with iteration
    compute=dict(unstable_iterative=True, unstable_lossless_corrections=True),
)

with observe.observe(codec_sg_lossless_rint, observations):
    ERA5_U_sg_lossless_rint_enc = codec_sg_lossless_rint.encode(ERA5_U.values)
    ERA5_U_sg_lossless_rint = ERA5_U.copy(
        data=codec_sg_lossless_rint.decode(ERA5_U_sg_lossless_rint_enc)
    )

ERA5_U_sg_lossless_rint_cr = (
    ERA5_U.nbytes / np.asarray(ERA5_U_sg_lossless_rint_enc).nbytes
)
codec_sg_lossless_rint = SafeguardedCodec( codec=ZeroCodec(), safeguards=codec_sg_rint.safeguards, # produce lossless corrections and refine them with iteration compute=dict(unstable_iterative=True, unstable_lossless_corrections=True), ) with observe.observe(codec_sg_lossless_rint, observations): ERA5_U_sg_lossless_rint_enc = codec_sg_lossless_rint.encode(ERA5_U.values) ERA5_U_sg_lossless_rint = ERA5_U.copy( data=codec_sg_lossless_rint.decode(ERA5_U_sg_lossless_rint_enc) ) ERA5_U_sg_lossless_rint_cr = ( ERA5_U.nbytes / np.asarray(ERA5_U_sg_lossless_rint_enc).nbytes )

Compressing u 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.

Copied!
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
        buf_dssim = np.nanmean(
            dssim_mat(
                self._data,
                buf,
                reproducible=False,
            )
        )
        # use distance to the dSSIM threshold as the violations score
        violations = dssim_bound - buf_dssim
        self._data = None
        # return the violations score metric
        return numcodecs.compat.ndarray_copy(np.float64(violations), out)


numcodecs.registry.register_codec(SafetyViolationsMetric)
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 buf_dssim = np.nanmean( dssim_mat( self._data, buf, reproducible=False, ) ) # use distance to the dSSIM threshold as the violations score violations = dssim_bound - buf_dssim self._data = None # return the violations score metric return numcodecs.compat.ndarray_copy(np.float64(violations), out) numcodecs.registry.register_codec(SafetyViolationsMetric)
Copied!
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)
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)
Copied!
from numcodecs_wasm_pressio import Pressio

ERA5_U_optzconfig = dict()
ERA5_U_optzconfig_cr = dict()

for codec, parameter, lower_bound in [
    (Zfp(mode="fixed-accuracy", tolerance=1.0), "tolerance", 1e-4),  # decent guess
    (Sz3(eb_mode="abs", eb_abs=1.0), "eb_abs", 1e-4),  # decent guess
    (Sperr(mode="pwe", pwe=1.0), "pwe", 1e-4),  # 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(1.0),
            "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_U_optzconfig_enc = optzconfig.encode(ERA5_U.values)
        ERA5_U_optzconfig[codec.codec_id] = ERA5_U.copy(
            data=optzconfig.decode(ERA5_U_optzconfig_enc)
        )

    ERA5_U_optzconfig_cr[codec.codec_id] = (
        ERA5_U.nbytes / np.asarray(ERA5_U_optzconfig_enc).nbytes
    )
from numcodecs_wasm_pressio import Pressio ERA5_U_optzconfig = dict() ERA5_U_optzconfig_cr = dict() for codec, parameter, lower_bound in [ (Zfp(mode="fixed-accuracy", tolerance=1.0), "tolerance", 1e-4), # decent guess (Sz3(eb_mode="abs", eb_abs=1.0), "eb_abs", 1e-4), # decent guess (Sperr(mode="pwe", pwe=1.0), "pwe", 1e-4), # 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(1.0), "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_U_optzconfig_enc = optzconfig.encode(ERA5_U.values) ERA5_U_optzconfig[codec.codec_id] = ERA5_U.copy( data=optzconfig.decode(ERA5_U_optzconfig_enc) ) ERA5_U_optzconfig_cr[codec.codec_id] = ( ERA5_U.nbytes / np.asarray(ERA5_U_optzconfig_enc).nbytes )
rank={0,1,} iter={0} input={-4.60517,} output={7.83912,} objective={7.83912}
rank={0,1,} iter={1} input={-7.06182,} output={5.25999,} objective={5.25999}
rank={0,1,} iter={2} input={-2.18607,} output={-0.00754271,} objective={-0.00754271}
rank={0,1,} iter={3} input={-5.23695,} output={6.98307,} objective={6.98307}
rank={0,1,} iter={4} input={-9.20755,} output={4.2205,} objective={4.2205}
rank={0,1,} iter={5} input={-4.47128,} output={7.83912,} objective={7.83912}
rank={0,1,} iter={6} input={-5.89774,} output={6.2941,} objective={6.2941}
rank={0,1,} iter={7} input={-4.79639,} output={7.83912,} objective={7.83912}
rank={0,1,} iter={8} input={-7.9821,} output={4.86091,} objective={4.86091}
rank={0,1,} iter={9} input={-4.89178,} output={6.98307,} objective={6.98307}
rank={0,1,} iter={10} input={-0.000130486,} output={-0.0688416,} objective={-0.0688416}
rank={0,1,} iter={11} input={-3.86497,} output={8.89944,} objective={8.89944}
rank={0,1,} iter={12} input={-3.16177,} output={-0.00174731,} objective={-0.00174731}
rank={0,1,} iter={13} input={-1.09024,} output={-0.0371666,} objective={-0.0371666}
rank={0,1,} iter={14} input={-4.08863,} output={8.89944,} objective={8.89944}
rank={0,1,} iter={15} input={-6.43996,} output={5.73067,} objective={5.73067}
rank={0,1,} iter={16} input={-3.9768,} output={8.89944,} objective={8.89944}
rank={0,1,} iter={17} input={-8.56972,} output={4.51812,} objective={4.51812}
rank={0,1,} iter={18} input={-7.50494,} output={5.25999,} objective={5.25999}
rank={0,1,} iter={19} input={-5.54042,} output={6.98307,} objective={6.98307}
rank={0,1,} iter={20} input={-4.23618,} output={7.83912,} objective={7.83912}
rank={0,1,} iter={21} input={-4.03289,} output={8.89944,} objective={8.89944}
rank={0,1,} iter={22} input={-3.92083,} output={8.89944,} objective={8.89944}
rank={0,1,} iter={23} input={-6.14733,} output={6.2941,} objective={6.2941}
rank={0,1,} iter={24} input={-6.73156,} output={5.73067,} objective={5.73067}
final_iter={25} inputs={-3.86497,} output={8.89944,}
rank={0,1,} iter={0} input={-4.60517,} output={-0.0045849,} objective={-0.0045849}
rank={0,1,} iter={1} input={-7.06182,} output={8.56878,} objective={8.56878}
rank={0,1,} iter={2} input={-2.18607,} output={-0.0788497,} objective={-0.0788497}
rank={0,1,} iter={3} input={-9.21034,} output={6.27985,} objective={6.27985}
rank={0,1,} iter={4} input={-7.80758,} output={7.2684,} objective={7.2684}
rank={0,1,} iter={5} input={-4.74584,} output={-0.00341721,} objective={-0.00341721}
rank={0,1,} iter={6} input={-8.37615,} output={6.474,} objective={6.474}
rank={0,1,} iter={7} input={-5.90383,} output={11.5394,} objective={11.5394}
rank={0,1,} iter={8} input={-0.000166178,} output={-0.273373,} objective={-0.273373}
rank={0,1,} iter={9} input={-6.24575,} output={10.4801,} objective={10.4801}
rank={0,1,} iter={10} input={-6.55825,} output={9.78282,} objective={9.78282}
rank={0,1,} iter={11} input={-4.74584,} output={-0.00341721,} objective={-0.00341721}
rank={0,1,} iter={12} input={-6.0214,} output={11.1628,} objective={11.1628}
rank={0,1,} iter={13} input={-5.32483,} output={13.6472,} objective={13.6472}
rank={0,1,} iter={14} input={-3.39636,} output={-0.0247463,} objective={-0.0247463}
rank={0,1,} iter={15} input={-6.48282,} output={9.79808,} objective={9.79808}
rank={0,1,} iter={16} input={-1.09157,} output={-0.136103,} objective={-0.136103}
rank={0,1,} iter={17} input={-5.90383,} output={11.5394,} objective={11.5394}
rank={0,1,} iter={18} input={-5.56962,} output={12.6513,} objective={12.6513}
rank={0,1,} iter={19} input={-5.3866,} output={13.3787,} objective={13.3787}
rank={0,1,} iter={20} input={-7.40727,} output={7.93355,} objective={7.93355}
rank={0,1,} iter={21} input={-5.18009,} output={-0.000540528,} objective={-0.000540528}
rank={0,1,} iter={22} input={-4.00091,} output={-0.0118845,} objective={-0.0118845}
rank={0,1,} iter={23} input={-5.35117,} output={13.5535,} objective={13.5535}
rank={0,1,} iter={24} input={-2.79109,} output={-0.0457674,} objective={-0.0457674}
final_iter={25} inputs={-5.32483,} output={13.6472,}
rank={0,1,} iter={0} input={-4.60517,} output={-0.000829658,} objective={-0.000829658}
rank={0,1,} iter={1} input={-7.06182,} output={9.51461,} objective={9.51461}
rank={0,1,} iter={2} input={-2.18607,} output={-0.0396817,} objective={-0.0396817}
rank={0,1,} iter={3} input={-9.21034,} output={6.52341,} objective={6.52341}
rank={0,1,} iter={4} input={-7.75045,} output={8.29013,} objective={8.29013}
rank={0,1,} iter={5} input={-4.74584,} output={-0.000211268,} objective={-0.000211268}
rank={0,1,} iter={6} input={-8.26569,} output={7.56854,} objective={7.56854}
rank={0,1,} iter={7} input={-5.90383,} output={12.6564,} objective={12.6564}
rank={0,1,} iter={8} input={-0.000166178,} output={-0.15309,} objective={-0.15309}
rank={0,1,} iter={9} input={-6.25254,} output={11.5681,} objective={11.5681}
rank={0,1,} iter={10} input={-6.56292,} output={10.6872,} objective={10.6872}
rank={0,1,} iter={11} input={-4.74584,} output={-0.000211268,} objective={-0.000211268}
rank={0,1,} iter={12} input={-6.02767,} output={12.2583,} objective={12.2583}
rank={0,1,} iter={13} input={-5.32483,} output={14.8321,} objective={14.8321}
rank={0,1,} iter={14} input={-3.39636,} output={-0.012263,} objective={-0.012263}
rank={0,1,} iter={15} input={-6.48282,} output={10.9031,} objective={10.9031}
rank={0,1,} iter={16} input={-1.09157,} output={-0.0862977,} objective={-0.0862977}
rank={0,1,} iter={17} input={-5.90383,} output={12.6564,} objective={12.6564}
rank={0,1,} iter={18} input={-5.57192,} output={13.8353,} objective={13.8353}
rank={0,1,} iter={19} input={-5.39218,} output={14.5469,} objective={14.5469}
rank={0,1,} iter={20} input={-8.71635,} output={7.02976,} objective={7.02976}
rank={0,1,} iter={21} input={-5.18009,} output={15.4727,} objective={15.4727}
rank={0,1,} iter={22} input={-4.00091,} output={-0.00527798,} objective={-0.00527798}
rank={0,1,} iter={23} input={-4.89059,} output={16.891,} objective={16.891}
rank={0,1,} iter={24} input={-2.79109,} output={-0.0233931,} objective={-0.0233931}
final_iter={25} inputs={-4.89059,} output={16.891,}

Visual comparison of the dSSIM score distributions¶

Copied!
fig = earthkit.plots.Figure(
    size=(10, 23),
    rows=6,
    columns=2,
)

plot_u_wind_dssim(
    ERA5_U, 1.0, fig.add_map(0, 0), "Original", span=(-60, 60), dssim_bound=dssim_bound
)
plot_u_wind_dssim(
    ERA5_U_codec["zfp.rs"][eb_abs[1]],
    ERA5_U_codec_cr["zfp.rs"][eb_abs[1]],
    fig.add_map(1, 0),
    r"ZFP($\epsilon_{{abs}}$)",
    span=(0.95, 1.0),
    dssim_bound=dssim_bound,
    error=True,
)
plot_u_wind_dssim(
    ERA5_U_codec["sz3.rs"][eb_abs[1]],
    ERA5_U_codec_cr["sz3.rs"][eb_abs[1]],
    fig.add_map(2, 0),
    r"SZ3($\epsilon_{{abs}}$)",
    span=(0.95, 1.0),
    dssim_bound=dssim_bound,
    error=True,
)
plot_u_wind_dssim(
    ERA5_U_codec["sperr.rs"][eb_abs[1]],
    ERA5_U_codec_cr["sperr.rs"][eb_abs[1]],
    fig.add_map(3, 0),
    r"SPERR($\epsilon_{{abs}}$)",
    span=(0.95, 1.0),
    dssim_bound=dssim_bound,
    error=True,
)

plot_u_wind_dssim(
    ERA5_U_sg["zero"][eb_abs[1]],
    ERA5_U_sg_cr["zero"][eb_abs[1]],
    fig.add_map(0, 1),
    r"Safeguarded(0, dSSIM)",
    span=(0.999, 1.0),
    dssim_bound=dssim_bound,
    error=True,
    corr=ERA5_U_codec["zero"][eb_abs[1]],
    my_ERA5_U_it=ERA5_U_sg_it["zero"][eb_abs[1]],
    cr_it=ERA5_U_sg_it_cr["zero"][eb_abs[1]],
)
plot_u_wind_dssim(
    ERA5_U_sg["zfp.rs"][eb_abs[1]],
    ERA5_U_sg_cr["zfp.rs"][eb_abs[1]],
    fig.add_map(1, 1),
    r"Safeguarded(ZFP($\epsilon_{{abs}}$), dSSIM)",
    span=(0.999, 1.0),
    dssim_bound=dssim_bound,
    error=True,
    corr=ERA5_U_codec["zfp.rs"][eb_abs[1]],
    my_ERA5_U_it=ERA5_U_sg_it["zfp.rs"][eb_abs[1]],
    cr_it=ERA5_U_sg_it_cr["zfp.rs"][eb_abs[1]],
    corr_colours=["white", "green", "limegreen"],
)
plot_u_wind_dssim(
    ERA5_U_sg["sz3.rs"][eb_abs[1]],
    ERA5_U_sg_cr["sz3.rs"][eb_abs[1]],
    fig.add_map(2, 1),
    r"Safeguarded(SZ3($\epsilon_{{abs}}$), dSSIM)",
    span=(0.999, 1.0),
    dssim_bound=dssim_bound,
    error=True,
    corr=ERA5_U_codec["sz3.rs"][eb_abs[1]],
    my_ERA5_U_it=ERA5_U_sg_it["sz3.rs"][eb_abs[1]],
    cr_it=ERA5_U_sg_it_cr["sz3.rs"][eb_abs[1]],
)
plot_u_wind_dssim(
    ERA5_U_sg["sperr.rs"][eb_abs[1]],
    ERA5_U_sg_cr["sperr.rs"][eb_abs[1]],
    fig.add_map(3, 1),
    r"Safeguarded(SPERR($\epsilon_{{abs}}$), dSSIM)",
    span=(0.999, 1.0),
    dssim_bound=dssim_bound,
    error=True,
    corr=ERA5_U_codec["sperr.rs"][eb_abs[1]],
    my_ERA5_U_it=ERA5_U_sg_it["sperr.rs"][eb_abs[1]],
    cr_it=ERA5_U_sg_it_cr["sperr.rs"][eb_abs[1]],
)

plot_u_wind_dssim(
    ERA5_U_optzconfig["zfp.rs"],
    ERA5_U_optzconfig_cr["zfp.rs"],
    fig.add_map(4, 0),
    r"OptZConfig(ZFP, dSSIM)",
    span=(0.999, 1.0),
    dssim_bound=dssim_bound,
    error=True,
)
plot_u_wind_dssim(
    ERA5_U_optzconfig["sz3.rs"],
    ERA5_U_optzconfig_cr["sz3.rs"],
    fig.add_map(4, 1),
    r"OptZConfig(SZ3, dSSIM)",
    span=(0.999, 1.0),
    dssim_bound=dssim_bound,
    error=True,
)
plot_u_wind_dssim(
    ERA5_U_optzconfig["sperr.rs"],
    ERA5_U_optzconfig_cr["sperr.rs"],
    fig.add_map(5, 0),
    r"OptZConfig(SPER, dSSIM)",
    span=(0.999, 1.0),
    dssim_bound=dssim_bound,
    error=True,
)

fig.save(Path("plots") / "dssim.pdf")
fig = earthkit.plots.Figure( size=(10, 23), rows=6, columns=2, ) plot_u_wind_dssim( ERA5_U, 1.0, fig.add_map(0, 0), "Original", span=(-60, 60), dssim_bound=dssim_bound ) plot_u_wind_dssim( ERA5_U_codec["zfp.rs"][eb_abs[1]], ERA5_U_codec_cr["zfp.rs"][eb_abs[1]], fig.add_map(1, 0), r"ZFP($\epsilon_{{abs}}$)", span=(0.95, 1.0), dssim_bound=dssim_bound, error=True, ) plot_u_wind_dssim( ERA5_U_codec["sz3.rs"][eb_abs[1]], ERA5_U_codec_cr["sz3.rs"][eb_abs[1]], fig.add_map(2, 0), r"SZ3($\epsilon_{{abs}}$)", span=(0.95, 1.0), dssim_bound=dssim_bound, error=True, ) plot_u_wind_dssim( ERA5_U_codec["sperr.rs"][eb_abs[1]], ERA5_U_codec_cr["sperr.rs"][eb_abs[1]], fig.add_map(3, 0), r"SPERR($\epsilon_{{abs}}$)", span=(0.95, 1.0), dssim_bound=dssim_bound, error=True, ) plot_u_wind_dssim( ERA5_U_sg["zero"][eb_abs[1]], ERA5_U_sg_cr["zero"][eb_abs[1]], fig.add_map(0, 1), r"Safeguarded(0, dSSIM)", span=(0.999, 1.0), dssim_bound=dssim_bound, error=True, corr=ERA5_U_codec["zero"][eb_abs[1]], my_ERA5_U_it=ERA5_U_sg_it["zero"][eb_abs[1]], cr_it=ERA5_U_sg_it_cr["zero"][eb_abs[1]], ) plot_u_wind_dssim( ERA5_U_sg["zfp.rs"][eb_abs[1]], ERA5_U_sg_cr["zfp.rs"][eb_abs[1]], fig.add_map(1, 1), r"Safeguarded(ZFP($\epsilon_{{abs}}$), dSSIM)", span=(0.999, 1.0), dssim_bound=dssim_bound, error=True, corr=ERA5_U_codec["zfp.rs"][eb_abs[1]], my_ERA5_U_it=ERA5_U_sg_it["zfp.rs"][eb_abs[1]], cr_it=ERA5_U_sg_it_cr["zfp.rs"][eb_abs[1]], corr_colours=["white", "green", "limegreen"], ) plot_u_wind_dssim( ERA5_U_sg["sz3.rs"][eb_abs[1]], ERA5_U_sg_cr["sz3.rs"][eb_abs[1]], fig.add_map(2, 1), r"Safeguarded(SZ3($\epsilon_{{abs}}$), dSSIM)", span=(0.999, 1.0), dssim_bound=dssim_bound, error=True, corr=ERA5_U_codec["sz3.rs"][eb_abs[1]], my_ERA5_U_it=ERA5_U_sg_it["sz3.rs"][eb_abs[1]], cr_it=ERA5_U_sg_it_cr["sz3.rs"][eb_abs[1]], ) plot_u_wind_dssim( ERA5_U_sg["sperr.rs"][eb_abs[1]], ERA5_U_sg_cr["sperr.rs"][eb_abs[1]], fig.add_map(3, 1), r"Safeguarded(SPERR($\epsilon_{{abs}}$), dSSIM)", span=(0.999, 1.0), dssim_bound=dssim_bound, error=True, corr=ERA5_U_codec["sperr.rs"][eb_abs[1]], my_ERA5_U_it=ERA5_U_sg_it["sperr.rs"][eb_abs[1]], cr_it=ERA5_U_sg_it_cr["sperr.rs"][eb_abs[1]], ) plot_u_wind_dssim( ERA5_U_optzconfig["zfp.rs"], ERA5_U_optzconfig_cr["zfp.rs"], fig.add_map(4, 0), r"OptZConfig(ZFP, dSSIM)", span=(0.999, 1.0), dssim_bound=dssim_bound, error=True, ) plot_u_wind_dssim( ERA5_U_optzconfig["sz3.rs"], ERA5_U_optzconfig_cr["sz3.rs"], fig.add_map(4, 1), r"OptZConfig(SZ3, dSSIM)", span=(0.999, 1.0), dssim_bound=dssim_bound, error=True, ) plot_u_wind_dssim( ERA5_U_optzconfig["sperr.rs"], ERA5_U_optzconfig_cr["sperr.rs"], fig.add_map(5, 0), r"OptZConfig(SPER, dSSIM)", span=(0.999, 1.0), dssim_bound=dssim_bound, error=True, ) fig.save(Path("plots") / "dssim.pdf")
No description has been provided for this image
Copied!
dssim_table = pd.concat(
    [
        table_u_wind_dssim(
            ERA5_U_sg_lossless["zero"][eb_abs[0]],
            ERA5_U_sg_lossless_cr["zero"][eb_abs[0]],
            # no eb_abs in Safeguarded(0), so results for eb_abs[0] and
            #  eb_abs[1] are equivalent
            ["0", "", rf"$\text{{dSSIM}} \geq {dssim_bound}$", "lossless"],
            dssim_bound,
            ERA5_U_codec["zero"][eb_abs[0]],
        ),
        table_u_wind_dssim(
            ERA5_U_sg["zero"][eb_abs[0]],
            ERA5_U_sg_cr["zero"][eb_abs[0]],
            # no eb_abs in Safeguarded(0), so results for eb_abs[0] and
            #  eb_abs[1] are equivalent
            ["0", "", rf"$\text{{dSSIM}} \geq {dssim_bound}$", "one-shot"],
            dssim_bound,
            ERA5_U_codec["zero"][eb_abs[0]],
        ),
        table_u_wind_dssim(
            ERA5_U_sg_it["zero"][eb_abs[0]],
            ERA5_U_sg_it_cr["zero"][eb_abs[0]],
            # no eb_abs in Safeguarded(0), so results for eb_abs[0] and
            #  eb_abs[1] are equivalent
            ["0", "", rf"$\text{{dSSIM}} \geq {dssim_bound}$", "iterative"],
            dssim_bound,
            ERA5_U_codec["zero"][eb_abs[0]],
        ),
    ]
    + [
        table_u_wind_dssim(
            ERA5_U_sg_lossless_rint,
            ERA5_U_sg_lossless_rint_cr,
            ["0", "", "bin(256)", "lossless"],
            dssim_bound,
            ERA5_U_codec["zero"][eb_abs[0]],
        ),
        table_u_wind_dssim(
            ERA5_U_sg_rint,
            ERA5_U_sg_rint_cr,
            ["0", "", "bin(256)", "one-shot"],
            dssim_bound,
            ERA5_U_codec["zero"][eb_abs[0]],
        ),
        table_u_wind_dssim(
            ERA5_U_sg_it_rint,
            ERA5_U_sg_it_rint_cr,
            ["0", "", "bin(256)", "iterative"],
            dssim_bound,
            ERA5_U_codec["zero"][eb_abs[0]],
        ),
    ]
    + [
        table_u_wind_dssim(
            # for SZ3(eb_noa), we only need the non-safeguarded results
            ERA5_U_noa,
            ERA5_U_noa_cr,
            [r"SZ3($\epsilon_{noa}$)", "", "-", ""],
            dssim_bound,
            None,
        ),
    ]
    + [
        x
        for eb_abs_zfp in eb_abs
        for x in [
            table_u_wind_dssim(
                ERA5_U_codec["zfp.rs"][eb_abs_zfp],
                ERA5_U_codec_cr["zfp.rs"][eb_abs_zfp],
                [r"ZFP($\epsilon_{abs}$)", eb_abs_zfp, "-", ""],
                dssim_bound,
                None,
            ),
            table_u_wind_dssim(
                ERA5_U_sg_lossless["zfp.rs"][eb_abs_zfp],
                ERA5_U_sg_lossless_cr["zfp.rs"][eb_abs_zfp],
                [
                    r"ZFP($\epsilon_{abs}$)",
                    eb_abs_zfp,
                    rf"$\text{{dSSIM}} \geq {dssim_bound}$",
                    "lossless",
                ],
                dssim_bound,
                ERA5_U_codec["zfp.rs"][eb_abs_zfp],
            ),
            table_u_wind_dssim(
                ERA5_U_sg["zfp.rs"][eb_abs_zfp],
                ERA5_U_sg_cr["zfp.rs"][eb_abs_zfp],
                [
                    r"ZFP($\epsilon_{abs}$)",
                    eb_abs_zfp,
                    rf"$\text{{dSSIM}} \geq {dssim_bound}$",
                    "one-shot",
                ],
                dssim_bound,
                ERA5_U_codec["zfp.rs"][eb_abs_zfp],
            ),
            table_u_wind_dssim(
                ERA5_U_sg_it["zfp.rs"][eb_abs_zfp],
                ERA5_U_sg_it_cr["zfp.rs"][eb_abs_zfp],
                [
                    r"ZFP($\epsilon_{abs}$)",
                    eb_abs_zfp,
                    rf"$\text{{dSSIM}} \geq {dssim_bound}$",
                    "iterative",
                ],
                dssim_bound,
                ERA5_U_codec["zfp.rs"][eb_abs_zfp],
            ),
        ]
    ]
    + [
        table_u_wind_dssim(
            ERA5_U_optzconfig["zfp.rs"],
            ERA5_U_optzconfig_cr["zfp.rs"],
            [
                "OptZConfig(ZFP)",
                "",
                rf"$\text{{E}}[\text{{dSSIM}}] \geq {dssim_bound}$",
                "",
            ],
            dssim_bound,
            None,
        )
    ]
    + [
        x
        for eb_abs_sz3 in eb_abs
        for x in [
            table_u_wind_dssim(
                ERA5_U_codec["sz3.rs"][eb_abs_sz3],
                ERA5_U_codec_cr["sz3.rs"][eb_abs_sz3],
                [r"SZ3($\epsilon_{abs}$)", eb_abs_sz3, "-", ""],
                dssim_bound,
                None,
            ),
            table_u_wind_dssim(
                ERA5_U_sg_lossless["sz3.rs"][eb_abs_sz3],
                ERA5_U_sg_lossless_cr["sz3.rs"][eb_abs_sz3],
                [
                    r"SZ3($\epsilon_{abs}$)",
                    eb_abs_sz3,
                    rf"$\text{{dSSIM}} \geq {dssim_bound}$",
                    "lossless",
                ],
                dssim_bound,
                ERA5_U_codec["sz3.rs"][eb_abs_sz3],
            ),
            table_u_wind_dssim(
                ERA5_U_sg["sz3.rs"][eb_abs_sz3],
                ERA5_U_sg_cr["sz3.rs"][eb_abs_sz3],
                [
                    r"SZ3($\epsilon_{abs}$)",
                    eb_abs_sz3,
                    rf"$\text{{dSSIM}} \geq {dssim_bound}$",
                    "one-shot",
                ],
                dssim_bound,
                ERA5_U_codec["sz3.rs"][eb_abs_sz3],
            ),
            table_u_wind_dssim(
                ERA5_U_sg_it["sz3.rs"][eb_abs_sz3],
                ERA5_U_sg_it_cr["sz3.rs"][eb_abs_sz3],
                [
                    r"SZ3($\epsilon_{abs}$)",
                    eb_abs_sz3,
                    rf"$\text{{dSSIM}} \geq {dssim_bound}$",
                    "iterative",
                ],
                dssim_bound,
                ERA5_U_codec["sz3.rs"][eb_abs_sz3],
            ),
        ]
    ]
    + [
        table_u_wind_dssim(
            ERA5_U_optzconfig["sz3.rs"],
            ERA5_U_optzconfig_cr["sz3.rs"],
            [
                "OptZConfig(SZ3)",
                "",
                rf"$\text{{E}}[\text{{dSSIM}}] \geq {dssim_bound}$",
                "",
            ],
            dssim_bound,
            None,
        )
    ]
    + [
        x
        for eb_abs_sperr in eb_abs
        for x in [
            table_u_wind_dssim(
                ERA5_U_codec["sperr.rs"][eb_abs_sperr],
                ERA5_U_codec_cr["sperr.rs"][eb_abs_sperr],
                [r"SPERR($\epsilon_{abs}$)", eb_abs_sperr, "-", ""],
                dssim_bound,
                None,
            ),
            table_u_wind_dssim(
                ERA5_U_sg_lossless["sperr.rs"][eb_abs_sperr],
                ERA5_U_sg_lossless_cr["sperr.rs"][eb_abs_sperr],
                [
                    r"SPERR($\epsilon_{abs}$)",
                    eb_abs_sperr,
                    rf"$\text{{dSSIM}} \geq {dssim_bound}$",
                    "lossless",
                ],
                dssim_bound,
                ERA5_U_codec["sperr.rs"][eb_abs_sperr],
            ),
            table_u_wind_dssim(
                ERA5_U_sg["sperr.rs"][eb_abs_sperr],
                ERA5_U_sg_cr["sperr.rs"][eb_abs_sperr],
                [
                    r"SPERR($\epsilon_{abs}$)",
                    eb_abs_sperr,
                    rf"$\text{{dSSIM}} \geq {dssim_bound}$",
                    "one-shot",
                ],
                dssim_bound,
                ERA5_U_codec["sperr.rs"][eb_abs_sperr],
            ),
            table_u_wind_dssim(
                ERA5_U_sg_it["sperr.rs"][eb_abs_sperr],
                ERA5_U_sg_it_cr["sperr.rs"][eb_abs_sperr],
                [
                    r"SPERR($\epsilon_{abs}$)",
                    eb_abs_sperr,
                    rf"$\text{{dSSIM}} \geq {dssim_bound}$",
                    "iterative",
                ],
                dssim_bound,
                ERA5_U_codec["sperr.rs"][eb_abs_sperr],
            ),
        ]
    ]
    + [
        table_u_wind_dssim(
            ERA5_U_optzconfig["sperr.rs"],
            ERA5_U_optzconfig_cr["sperr.rs"],
            [
                "OptZConfig(SPERR)",
                "",
                rf"$\text{{E}}[\text{{dSSIM}}] \geq {dssim_bound}$",
                "",
            ],
            dssim_bound,
            None,
        )
    ]
    + [
        table_u_wind_dssim(
            ERA5_U_zstd,
            ERA5_U_zstd_cr,
            ["ZSTD(22)", "", "-", ""],
            dssim_bound,
            None,
        ),
    ]
).set_index(["Compressor", r"$\epsilon_{abs}$", "Safeguarded", "Corrections"])

Path("tables").joinpath("dssim.tex").write_text(
    dssim_table.to_latex(escape=False)
    .replace("%", r"\%")
    .replace("\\cline{1-13} \\cline{2-13} \\cline{3-13}\n\\bottomrule", "\\bottomrule")
)

dssim_table
dssim_table = pd.concat( [ table_u_wind_dssim( ERA5_U_sg_lossless["zero"][eb_abs[0]], ERA5_U_sg_lossless_cr["zero"][eb_abs[0]], # no eb_abs in Safeguarded(0), so results for eb_abs[0] and # eb_abs[1] are equivalent ["0", "", rf"$\text{{dSSIM}} \geq {dssim_bound}$", "lossless"], dssim_bound, ERA5_U_codec["zero"][eb_abs[0]], ), table_u_wind_dssim( ERA5_U_sg["zero"][eb_abs[0]], ERA5_U_sg_cr["zero"][eb_abs[0]], # no eb_abs in Safeguarded(0), so results for eb_abs[0] and # eb_abs[1] are equivalent ["0", "", rf"$\text{{dSSIM}} \geq {dssim_bound}$", "one-shot"], dssim_bound, ERA5_U_codec["zero"][eb_abs[0]], ), table_u_wind_dssim( ERA5_U_sg_it["zero"][eb_abs[0]], ERA5_U_sg_it_cr["zero"][eb_abs[0]], # no eb_abs in Safeguarded(0), so results for eb_abs[0] and # eb_abs[1] are equivalent ["0", "", rf"$\text{{dSSIM}} \geq {dssim_bound}$", "iterative"], dssim_bound, ERA5_U_codec["zero"][eb_abs[0]], ), ] + [ table_u_wind_dssim( ERA5_U_sg_lossless_rint, ERA5_U_sg_lossless_rint_cr, ["0", "", "bin(256)", "lossless"], dssim_bound, ERA5_U_codec["zero"][eb_abs[0]], ), table_u_wind_dssim( ERA5_U_sg_rint, ERA5_U_sg_rint_cr, ["0", "", "bin(256)", "one-shot"], dssim_bound, ERA5_U_codec["zero"][eb_abs[0]], ), table_u_wind_dssim( ERA5_U_sg_it_rint, ERA5_U_sg_it_rint_cr, ["0", "", "bin(256)", "iterative"], dssim_bound, ERA5_U_codec["zero"][eb_abs[0]], ), ] + [ table_u_wind_dssim( # for SZ3(eb_noa), we only need the non-safeguarded results ERA5_U_noa, ERA5_U_noa_cr, [r"SZ3($\epsilon_{noa}$)", "", "-", ""], dssim_bound, None, ), ] + [ x for eb_abs_zfp in eb_abs for x in [ table_u_wind_dssim( ERA5_U_codec["zfp.rs"][eb_abs_zfp], ERA5_U_codec_cr["zfp.rs"][eb_abs_zfp], [r"ZFP($\epsilon_{abs}$)", eb_abs_zfp, "-", ""], dssim_bound, None, ), table_u_wind_dssim( ERA5_U_sg_lossless["zfp.rs"][eb_abs_zfp], ERA5_U_sg_lossless_cr["zfp.rs"][eb_abs_zfp], [ r"ZFP($\epsilon_{abs}$)", eb_abs_zfp, rf"$\text{{dSSIM}} \geq {dssim_bound}$", "lossless", ], dssim_bound, ERA5_U_codec["zfp.rs"][eb_abs_zfp], ), table_u_wind_dssim( ERA5_U_sg["zfp.rs"][eb_abs_zfp], ERA5_U_sg_cr["zfp.rs"][eb_abs_zfp], [ r"ZFP($\epsilon_{abs}$)", eb_abs_zfp, rf"$\text{{dSSIM}} \geq {dssim_bound}$", "one-shot", ], dssim_bound, ERA5_U_codec["zfp.rs"][eb_abs_zfp], ), table_u_wind_dssim( ERA5_U_sg_it["zfp.rs"][eb_abs_zfp], ERA5_U_sg_it_cr["zfp.rs"][eb_abs_zfp], [ r"ZFP($\epsilon_{abs}$)", eb_abs_zfp, rf"$\text{{dSSIM}} \geq {dssim_bound}$", "iterative", ], dssim_bound, ERA5_U_codec["zfp.rs"][eb_abs_zfp], ), ] ] + [ table_u_wind_dssim( ERA5_U_optzconfig["zfp.rs"], ERA5_U_optzconfig_cr["zfp.rs"], [ "OptZConfig(ZFP)", "", rf"$\text{{E}}[\text{{dSSIM}}] \geq {dssim_bound}$", "", ], dssim_bound, None, ) ] + [ x for eb_abs_sz3 in eb_abs for x in [ table_u_wind_dssim( ERA5_U_codec["sz3.rs"][eb_abs_sz3], ERA5_U_codec_cr["sz3.rs"][eb_abs_sz3], [r"SZ3($\epsilon_{abs}$)", eb_abs_sz3, "-", ""], dssim_bound, None, ), table_u_wind_dssim( ERA5_U_sg_lossless["sz3.rs"][eb_abs_sz3], ERA5_U_sg_lossless_cr["sz3.rs"][eb_abs_sz3], [ r"SZ3($\epsilon_{abs}$)", eb_abs_sz3, rf"$\text{{dSSIM}} \geq {dssim_bound}$", "lossless", ], dssim_bound, ERA5_U_codec["sz3.rs"][eb_abs_sz3], ), table_u_wind_dssim( ERA5_U_sg["sz3.rs"][eb_abs_sz3], ERA5_U_sg_cr["sz3.rs"][eb_abs_sz3], [ r"SZ3($\epsilon_{abs}$)", eb_abs_sz3, rf"$\text{{dSSIM}} \geq {dssim_bound}$", "one-shot", ], dssim_bound, ERA5_U_codec["sz3.rs"][eb_abs_sz3], ), table_u_wind_dssim( ERA5_U_sg_it["sz3.rs"][eb_abs_sz3], ERA5_U_sg_it_cr["sz3.rs"][eb_abs_sz3], [ r"SZ3($\epsilon_{abs}$)", eb_abs_sz3, rf"$\text{{dSSIM}} \geq {dssim_bound}$", "iterative", ], dssim_bound, ERA5_U_codec["sz3.rs"][eb_abs_sz3], ), ] ] + [ table_u_wind_dssim( ERA5_U_optzconfig["sz3.rs"], ERA5_U_optzconfig_cr["sz3.rs"], [ "OptZConfig(SZ3)", "", rf"$\text{{E}}[\text{{dSSIM}}] \geq {dssim_bound}$", "", ], dssim_bound, None, ) ] + [ x for eb_abs_sperr in eb_abs for x in [ table_u_wind_dssim( ERA5_U_codec["sperr.rs"][eb_abs_sperr], ERA5_U_codec_cr["sperr.rs"][eb_abs_sperr], [r"SPERR($\epsilon_{abs}$)", eb_abs_sperr, "-", ""], dssim_bound, None, ), table_u_wind_dssim( ERA5_U_sg_lossless["sperr.rs"][eb_abs_sperr], ERA5_U_sg_lossless_cr["sperr.rs"][eb_abs_sperr], [ r"SPERR($\epsilon_{abs}$)", eb_abs_sperr, rf"$\text{{dSSIM}} \geq {dssim_bound}$", "lossless", ], dssim_bound, ERA5_U_codec["sperr.rs"][eb_abs_sperr], ), table_u_wind_dssim( ERA5_U_sg["sperr.rs"][eb_abs_sperr], ERA5_U_sg_cr["sperr.rs"][eb_abs_sperr], [ r"SPERR($\epsilon_{abs}$)", eb_abs_sperr, rf"$\text{{dSSIM}} \geq {dssim_bound}$", "one-shot", ], dssim_bound, ERA5_U_codec["sperr.rs"][eb_abs_sperr], ), table_u_wind_dssim( ERA5_U_sg_it["sperr.rs"][eb_abs_sperr], ERA5_U_sg_it_cr["sperr.rs"][eb_abs_sperr], [ r"SPERR($\epsilon_{abs}$)", eb_abs_sperr, rf"$\text{{dSSIM}} \geq {dssim_bound}$", "iterative", ], dssim_bound, ERA5_U_codec["sperr.rs"][eb_abs_sperr], ), ] ] + [ table_u_wind_dssim( ERA5_U_optzconfig["sperr.rs"], ERA5_U_optzconfig_cr["sperr.rs"], [ "OptZConfig(SPERR)", "", rf"$\text{{E}}[\text{{dSSIM}}] \geq {dssim_bound}$", "", ], dssim_bound, None, ) ] + [ table_u_wind_dssim( ERA5_U_zstd, ERA5_U_zstd_cr, ["ZSTD(22)", "", "-", ""], dssim_bound, None, ), ] ).set_index(["Compressor", r"$\epsilon_{abs}$", "Safeguarded", "Corrections"]) Path("tables").joinpath("dssim.tex").write_text( dssim_table.to_latex(escape=False) .replace("%", r"\%") .replace("\\cline{1-13} \\cline{2-13} \\cline{3-13}\n\\bottomrule", "\\bottomrule") ) dssim_table
$L_{\infty}(\hat{u})$ $L_{2}(\hat{u})$ $E[\text{D}]$ $\min(\text{D})$ $\max(\text{D})$ $\text{D} < 0.995$ S C CR
Compressor $\epsilon_{abs}$ Safeguarded Corrections
0 $\text{dSSIM} \geq 0.995$ lossless 0.207 0.00151 1.0 1.0 1.0 0 $\checkmark$ 99.7% $\times$ 5.51
one-shot 0.356 0.137 1.0 1.0 1.0 0 $\checkmark$ 98.6% $\times$ 28.27
iterative 0.356 0.137 1.0 1.0 1.0 0 $\checkmark$ 98.6% $\times$ 28.27
bin(256) lossless 0.226 0.0123 1.0 1.0 1.0 0 $\checkmark$ 98.6% $\times$ 5.64
one-shot 0.356 0.137 1.0 1.0 1.0 0 $\checkmark$ 98.6% $\times$ 28.27
iterative 0.356 0.137 1.0 1.0 1.0 0 $\checkmark$ 98.6% $\times$ 28.27
SZ3($\epsilon_{noa}$) - 0.358 0.173 0.82942 -0.997 1.0 92.3% $\times$ $\times$ 110.98
ZFP($\epsilon_{abs}$) 1.0 - 0.543 0.0987 0.88125 -0.997 1.0 80.4% $\times$ $\times$ 24.35
$\text{dSSIM} \geq 0.995$ lossless 0.469 0.033 0.99995 0.995 1.0 0 $\checkmark$ 81.3% $\times$ 5.05
one-shot 0.355 0.0789 1.0 1.0 1.0 0 $\checkmark$ 21.5% $\times$ 15.73
iterative 0.469 0.0792 0.99998 0.995 1.0 0 $\checkmark$ 21.2% $\times$ 15.77
0.1 - 0.0392 0.00825 0.98746 -0.183 1.0 13.6% $\times$ $\times$ 11.94
$\text{dSSIM} \geq 0.995$ lossless 0.0392 0.00756 0.99999 0.995 1.0 0 $\checkmark$ 14.2% $\times$ 8.9
one-shot 0.227 0.0178 1.0 1.0 1.0 0 $\checkmark$ 1.8% $\times$ 11.34
iterative 0.227 0.0175 0.99999 0.995 1.0 0 $\checkmark$ 1.8% $\times$ 11.35
OptZConfig(ZFP) $\text{E}[\text{dSSIM}] \geq 0.995$ 0.00975 0.0022 0.99654 -0.149 1.0 3.9% $\checkmark$ $\times$ 8.9
SZ3($\epsilon_{abs}$) 1.0 - 1.0 0.328 0.78367 -0.999 1.0 94.9% $\times$ $\times$ 262.5
$\text{dSSIM} \geq 0.995$ lossless 0.973 0.0269 0.99997 0.995 1.0 0 $\checkmark$ 97.4% $\times$ 0.94
one-shot 0.358 0.135 1.0 1.0 1.0 0 $\checkmark$ 62.4% $\times$ 17.29
iterative 0.973 0.137 0.99998 0.995 1.0 0 $\checkmark$ 62.2% $\times$ 17.33
0.1 - 0.1 0.0515 0.92396 -0.997 1.0 61.8% $\times$ $\times$ 50.69
$\text{dSSIM} \geq 0.995$ lossless 0.1 0.0287 0.99996 0.995 1.0 0 $\checkmark$ 62.9% $\times$ 1.42
one-shot 0.226 0.0527 1.0 1.0 1.0 0 $\checkmark$ 12.1% $\times$ 28.35
iterative 0.226 0.0527 0.99998 0.995 1.0 0 $\checkmark$ 11.9% $\times$ 28.48
OptZConfig(SZ3) $\text{E}[\text{dSSIM}] \geq 0.995$ 0.00487 0.00275 0.9952 -0.149 1.0 5.3% $\checkmark$ $\times$ 13.65
SPERR($\epsilon_{abs}$) 1.0 - 0.998 0.176 0.84181 -0.997 1.0 89.2% $\times$ $\times$ 195.53
$\text{dSSIM} \geq 0.995$ lossless 0.857 0.0333 0.99995 0.995 1.0 0 $\checkmark$ 90.1% $\times$ 1.01
one-shot 0.357 0.107 1.0 1.0 1.0 0 $\checkmark$ 36.4% $\times$ 24.74
iterative 0.857 0.108 0.99998 0.995 1.0 0 $\checkmark$ 36.0% $\times$ 24.86
0.1 - 0.1 0.0293 0.95924 -0.442 1.0 40.0% $\times$ $\times$ 49.01
$\text{dSSIM} \geq 0.995$ lossless 0.1 0.021 0.99997 0.995 1.0 0 $\checkmark$ 41.0% $\times$ 2.14
one-shot 0.227 0.0362 1.0 1.0 1.0 0 $\checkmark$ 6.4% $\times$ 32.36
iterative 0.227 0.0361 0.99998 0.995 1.0 0 $\checkmark$ 6.2% $\times$ 32.55
OptZConfig(SPERR) $\text{E}[\text{dSSIM}] \geq 0.995$ 0.00752 0.00281 0.99552 -0.149 1.0 5.1% $\checkmark$ $\times$ 16.89
ZSTD(22) - 0.0 0.0 1.0 1.0 1.0 0 $\checkmark$ $\times$ 4.51
Copied!
import json

with Path("observations").joinpath("dssim.json").open("w") as f:
    json.dump(observations, f)
import json with Path("observations").joinpath("dssim.json").open("w") as f: json.dump(observations, f)
Copied!

Next

Built with ProperDocs using a theme provided by Read the Docs.