Regridding

regrid() maps coefficients in one space to another along an axis of an array; regrid_nd() does several axes, and rebasis.xarray.regrid() works on xarray objects by dimension name. Methods:

  • "project" (default): the \(L^2\) projection, conservative.

  • "measure": the target elements are averaging kernels.

  • "interpolate": point values at the target centers.

  • "fit": the source elements are the kernels that measured the data (e.g. bin averages), the target a model space: smooth conservative upsampling.

import matplotlib.pyplot as plt
import numpy as np

import rebasis as rb
from _plot import BLUE, MUTED, ORANGE

Bounded projection

\(L^2\) projection onto a coarse smooth basis rings near discontinuities. bounded=True finds the best approximation that conserves the integral and creates no new extrema (each coefficient within the range of the inputs it overlaps); bounded="positive" and bounded=(lo, hi) apply global bounds.

x = np.linspace(0, 1, 4001)
f = ((x > 0.3) & (x < 0.55)).astype(float)
src = rb.Space.hats(x)
dst = rb.Space.bsplines_clamped(np.linspace(0, 1, 16), 3)
plain = rb.regrid(f, src, dst)
bounded = rb.regrid(f, src, dst, bounded=True)

xx = np.linspace(0, 0.9999, 1000)
E = dst.evaluate(xx)
m = dst.integrals()
fig, ax = plt.subplots(figsize=(7, 3))
ax.plot(x, f, color=MUTED, lw=1, label=f"input (∫ = {f @ src.integrals():.6f})")
ax.plot(xx, E @ plain, color=BLUE, label=f"L2 projection (∫ = {plain @ m:.6f})")
ax.plot(xx, E @ bounded, color=ORANGE, label=f"bounded (∫ = {bounded @ m:.6f})")
ax.legend(loc="upper right");
_images/032fbeab90f05aa90cc4c0f996e0e9e5db13eb648c615ac22d8ac49deddc3edd.png

Smooth conservative upsampling

From bin averages to a spline whose bin averages reproduce them:

edges = np.linspace(0, 2 * np.pi, 9)
avg = np.diff(-np.cos(edges)) / np.diff(edges)  # bin averages of sin
bins = rb.Space.boxes(edges)
model = rb.Space.bsplines_clamped(edges, 3)
coef = rb.regrid(avg, bins, model, method="fit", smoothing=1e-8)

xx = np.linspace(0, 2 * np.pi * 0.9999, 500)
fig, ax = plt.subplots(figsize=(7, 3))
ax.stairs(avg, edges, color=MUTED, label="bin averages")
ax.plot(xx, model.evaluate(xx) @ coef, color=BLUE, label="spline with the same bin averages")
ax.legend(loc="lower left")
print("bin averages reproduced to", np.abs(rb.measure(bins, model) @ coef - avg).max())
bin averages reproduced to 4.289765209719576e-10
_images/2832bff30ef054e5969a14ce6fdb3dffabe0af5b9025d896da1ce54e71a2ec76.png

Area-weighted latitude–longitude regridding

Grids that are tensor products are regridded one axis at a time, which is exact for separable weights. "cos_lat_deg" weights latitude by area:

lat_f, lon_f = rb.Space.boxes(np.linspace(-90, 90, 181)), rb.Space.boxes(np.linspace(0, 360, 361))
lat_c, lon_c = rb.Space.boxes(np.linspace(-90, 90, 13)), rb.Space.boxes(np.linspace(0, 360, 25))
lat, lon = np.meshgrid(lat_f.centers, lon_f.centers, indexing="ij")
field = np.cos(np.radians(lat)) ** 2 * (1 + 0.3 * np.sin(np.radians(3 * lon)))

coarse = rb.regrid_nd(field, {0: (lat_f, lat_c, "cos_lat_deg"), 1: (lon_f, lon_c)})


def global_mean(v, lat_s, lon_s):
    w = np.outer(lat_s.integrals("cos_lat_deg"), lon_s.integrals())
    return (v * w).sum() / w.sum()


print(global_mean(field, lat_f, lon_f), global_mean(coarse, lat_c, lon_c))
0.6666582044272394 0.6666582044272394

xarray

import xarray as xr

import rebasis.xarray

da = xr.DataArray(field, dims=("lat", "lon"), coords={"lat": lat_f.centers, "lon": lon_f.centers})
rebasis.xarray.regrid(
    da,
    {"lat": np.arange(-87.5, 90, 5.0), "lon": np.arange(2.5, 360, 5.0)},
    weight={"lat": "cos_lat_deg"},
)
<xarray.DataArray (lat: 36, lon: 72)> Size: 21kB
array([[0.00387015, 0.00415114, 0.00440307, ..., 0.00304633, 0.00329826,
        0.00357925],
       [0.0195573 , 0.02097725, 0.02225034, ..., 0.01539421, 0.0166673 ,
        0.01808725],
       [0.05045494, 0.0541182 , 0.0574026 , ..., 0.03971479, 0.04299918,
        0.04666245],
       ...,
       [0.05045494, 0.0541182 , 0.0574026 , ..., 0.03971479, 0.04299918,
        0.04666245],
       [0.0195573 , 0.02097725, 0.02225034, ..., 0.01539421, 0.0166673 ,
        0.01808725],
       [0.00387015, 0.00415114, 0.00440307, ..., 0.00304633, 0.00329826,
        0.00357925]], shape=(36, 72))
Coordinates:
  * lat      (lat) float64 288B -87.5 -82.5 -77.5 -72.5 ... 72.5 77.5 82.5 87.5
  * lon      (lon) float64 576B 2.5 7.5 12.5 17.5 ... 342.5 347.5 352.5 357.5

Missing data: skipna=True regrids with NaNs set to zero and divides by the regridded validity mask, so outputs are averages of the valid inputs (na_thres sets the minimum valid fraction).