Resampling & Raster Kernel Quick Reference
Almost every step in the solar and wind resource modeling workflows touches a resampling kernel, usually without saying so out loud. Reprojecting a satellite irradiance grid onto a DEM, coarsening a 5-minute time series to monthly means, mosaicking tiled GHI rasters, or aligning a land-use mask to the resource grid — each of these silently picks an interpolation rule, and the wrong rule corrupts the numbers a project finance model treats as ground truth. Bilinear smoothing over a categorical land-cover mask invents land classes that never existed; a plain nearest-neighbour downsample of an irradiance field throws away conserved energy; summing an intensity field where you meant to average it inflates yield by orders of magnitude.
This page is the quick-reference the rest of the site links into when a workflow needs to justify a kernel choice. It collects three decision tables — spatial resampling methods, temporal aggregation methods, and dtype/nodata conventions — plus a decision matrix and a runnable helper. It pairs naturally with the projection and CRS quick reference: pick the projection there, pick the kernel here, and every reproject in your pipeline is fully specified.
Spatial resampling methods
The core rule: continuous fields interpolate, categorical fields do not, and downsampling conserves the aggregate, upsampling reconstructs detail. The rasterio.enums.Resampling value in the last column is what you pass to rioxarray’s .rio.reproject(resampling=...) or rasterio’s WarpedVRT.
| Kernel | Best for | Continuity | Edge / overshoot behavior | Resampling value |
|---|---|---|---|---|
| Nearest | Categorical land-use, cloud/QA flags, integer masks | Preserves exact input values (no new values) | Blocky; hard edges kept intact | Resampling.nearest (0) |
| Bilinear | Continuous irradiance (GHI/DNI/DHI), wind speed — reproject or modest upsample | C0 continuous; smooths | No overshoot; slight edge blur | Resampling.bilinear (1) |
| Cubic | Continuous fields where smooth gradients matter (terrain-driven wind) | C1 smoother than bilinear | Can overshoot near sharp gradients | Resampling.cubic (2) |
| Cubic spline | Very smooth surfaces (interpolated pressure, temperature) | C2 smoothest | Larger overshoot; ringing risk | Resampling.cubic_spline (3) |
| Lanczos | High-quality resize of continuous rasters for display/reporting | Sharp yet smooth | Sinc ringing near strong edges | Resampling.lanczos (4) |
| Average | Downsampling continuous irradiance/wind — conserves the mean | Smooths; mean-preserving | Averages across boundaries (blurs class edges) | Resampling.average (5) |
| Mode | Downsampling categorical land-use / masks — majority class | Preserves valid class values | Majority wins per coarse cell | Resampling.mode (6) |
| Min / Max | Conservative masks (worst-case shading, exclusion coverage) | Preserves extremes | Biases toward the extreme value | Resampling.min (9) / Resampling.max (8) |
Two failure modes dominate in practice. First, using nearest when downsampling a continuous field: it point-samples one fine pixel per coarse cell and discards the rest, so a 10× coarsening keeps only 1% of the data and the mean drifts unpredictably — use average instead. Second, using bilinear or average on a categorical mask: interpolating class codes 1 and 3 yields 2, a class that may mean something entirely different — always use nearest (reproject) or mode (downsample) for anything discrete.
Temporal aggregation methods
Temporal reduction is where units bite. Irradiance and wind speed are intensities (instantaneous rates) and aggregate by mean; energy is an accumulation and aggregates by sum. Confusing the two is the most common yield error in resource assessment. The full rolling/exceedance machinery lives in temporal data aggregation; this table is the cheat sheet.
| Variable | Hourly → daily | Daily → monthly | → annual | Notes |
|---|---|---|---|---|
| GHI / DNI (W/m²) | mean | mean | mean | Intensity — never sum W/m²; convert to Wh/m² first if you need energy |
| Irradiation (Wh/m²) | sum | sum | sum | Already energy-per-area; additive across time |
| Wind speed (m/s) | mean | mean | mean | Report mean; also keep the Weibull/percentile spread, not just the mean |
| Wind power density (W/m²) | mean | mean | mean | Mean of the cube ⟨½ρv³⟩ — compute per timestep, then average |
| Generation / energy (MWh) | sum | sum | sum | Additive; annual energy production (AEP) is a pure sum over the year |
| Resource risk bands | — | — | P50 / P90 percentile | Compute across the yearly totals, not within a year |
Percentiles apply to a distribution of annual totals, not to raw sub-hourly samples. Aggregate to annual energy first (by sum), collect one value per simulated year, then take the P50 (median) and P90 (10th percentile — the conservative exceedance band financiers underwrite against).
For average resampling and for mean temporal reduction, the coarse or aggregated value is the arithmetic mean of the contributing samples:
For energy, the aggregate is a sum of power over the interval, which is fundamentally additive and must never be replaced by a mean:
With hourly steps , so in MWh is just in MW — the distinction between averaging an intensity (, W/m²) and summing an accumulation (, MWh) is exactly the mean-vs-sum choice above.
Dtype, nodata & compression guidance
Kernel choice interacts with storage. Interpolating across a nodata value silently bleeds it into neighbours, and an integer dtype cannot hold a NaN sentinel — so the dtype and nodata policy is part of the resampling decision, not an afterthought.
| Concern | Recommendation | Why |
|---|---|---|
| Working dtype (continuous) | float32 |
Halves memory vs float64 with negligible loss for irradiance/wind; supports NaN |
| Working dtype (categorical) | smallest int (uint8/int16) |
Class codes need no float precision; keeps masks compact |
| Nodata (continuous) | nodata = np.nan |
average/bilinear propagate NaN cleanly instead of blending a magic number like -9999 into real pixels |
| Nodata (categorical) | reserved int (e.g. 255 for uint8) |
NaN is invalid for integers; pick a code outside the valid class range |
| Compression | LZW (or DEFLATE), predictor=3 for floats |
Lossless; predictor=2 for ints, 3 for floating point improves ratio |
| Block layout | tiled, blockxsize=blockysize=256 (or 512) |
Enables windowed reads so large mosaics never load whole |
Always mask before you resample continuous data (rasterio masked=True or rioxarray’s nodata-aware read). With nodata=NaN and float32, average and bilinear skip missing pixels rather than averaging in a sentinel, and downstream statistics stay honest. This is the same discipline the spatial data quality and validation workflow enforces on vector inputs, applied to the raster side.
Decision matrix: data type + operation → kernel
The two questions that fully determine a kernel are what does the pixel value mean (continuous vs categorical) and which way are you resampling (reproject/upsample vs downsample/coarsen). This matrix collapses both into a single lookup.
Runnable helper: dispatch the right kernel
The helper below wires the matrix into code. It resolves a kernel from the data semantics and the operation direction, then runs an explicit rioxarray reproject-and-resample onto a target grid — the same reproject_match pattern used across solar irradiance raster processing so that harmonized layers share one affine transform before any pixel-wise math.
import numpy as np
import xarray as xr
import rioxarray # noqa: F401 (registers the .rio accessor)
from rasterio.enums import Resampling
def choose_kernel(is_categorical: bool, downsampling: bool) -> Resampling:
"""Map (data semantics, operation direction) -> rasterio resampling kernel."""
if is_categorical:
return Resampling.mode if downsampling else Resampling.nearest
return Resampling.average if downsampling else Resampling.bilinear
def resample_to_grid(source_da: xr.DataArray, target_grid: xr.DataArray,
is_categorical: bool = False) -> xr.DataArray:
"""Reproject/resample a raster onto a reference grid with the correct kernel.
source_da / target_grid carry a CRS via .rio; e.g. source in EPSG:4326,
target DEM grid in EPSG:32615 (UTM 15N). Direction is inferred from
resolution: coarser target => downsampling => conserve the aggregate.
"""
src_res = abs(source_da.rio.resolution()[0])
tgt_res = abs(target_grid.rio.resolution()[0])
downsampling = tgt_res > src_res
kernel = choose_kernel(is_categorical, downsampling)
if is_categorical:
ghi_or_mask = source_da.astype("int16")
nodata = 255
else:
ghi_or_mask = source_da.astype("float32")
nodata = np.float32("nan")
aligned = ghi_or_mask.rio.write_nodata(nodata).rio.reproject_match(
target_grid, resampling=kernel,
)
assert aligned.rio.crs.to_epsg() == target_grid.rio.crs.to_epsg()
assert aligned.rio.transform() == target_grid.rio.transform(), "Affine drift"
return aligned
# Example: snap a coarse EPSG:4326 GHI field onto a fine UTM DEM grid (upsample)
# ghi_aligned = resample_to_grid(ghi_array, dem_grid, is_categorical=False)
# -> bilinear, float32, nodata=NaN, co-registered with the DEM
Persist the result with a lossless codec and float-friendly predictor so the kernel’s output is not undone by storage:
ghi_aligned.rio.to_raster(
"ghi_aligned_utm15n.tif",
dtype="float32",
compress="LZW",
predictor=3, # 3 for floating point, 2 for integer rasters
tiled=True, blockxsize=256, blockysize=256,
nodata=np.float32("nan"),
)
Guidance notes
- Reproject once, resample once. Chaining reprojections compounds interpolation error. Snap every layer to a single reference grid (usually the DEM) with
reproject_match, then keep that grid fixed for the rest of the run. - Match nodata to dtype.
NaNforfloat32, a reserved integer for categorical rasters. An out-of-band-9999fed toaverageorbilinearwill bleed into real pixels and quietly bias every downstream statistic. - Downsample conserves, upsample reconstructs. When coarsening a continuous field,
averagepreserves the domain mean that becomes the denominator in capacity factor;nearestdoes not. When refining,bilinear/cubicreconstruct a plausible surface but add no real information. - Never smooth a mask. Cloud flags, QA bands, land-use codes, and exclusion masks are categorical —
nearestto reproject,modeto downsample, full stop. - Averages vs sums are a units decision. Resample intensities (W/m², m/s) by mean; aggregate energy (MWh, Wh/m²) by sum. Verify the physical unit before choosing, not after.
- Lanczos and cubic spline are for display, not for the analytical grid — their overshoot can push irradiance below zero or above the extraterrestrial limit near sharp cloud edges.
Related
- Solar & Wind Resource Modeling Workflows — the pipeline overview these kernel choices feed into at every stage.
- Solar Irradiance Raster Processing — where reproject-and-resample harmonizes GHI/DNI grids onto the terrain.
- Temporal Data Aggregation — the mean-vs-sum and P50/P90 rules applied to full time series.
- Projection & CRS Quick Reference — pick the target projection before you pick the kernel.
- Spatial Data Quality & Validation — the masking and nodata discipline that keeps resampling honest.