Tutorial: Gridding TROPOMI SIF Data
This tutorial walks through gridding TROPOMI Solar-Induced Fluorescence (SIF) data onto a regular lat/lon grid using SatelliteGridding.jl.
Setup
First, load the package and define the time range:
using SatelliteGridding
using DatesStep 1: Load the Configuration
Configuration files (TOML format) tell the package where to find data files, which NetCDF variables to read, and what quality filters to apply.
Here's what a typical TROPOMI SIF config looks like:
filePattern = "S5P_PAL__L2B_SIF____YYYYMMDD*.nc"
folder = "/path/to/tropomi/sif/"
[basic]
lat = "PRODUCT/latitude"
lon = "PRODUCT/longitude"
lat_bnd = "PRODUCT/SUPPORT_DATA/GEOLOCATIONS/latitude_bounds"
lon_bnd = "PRODUCT/SUPPORT_DATA/GEOLOCATIONS/longitude_bounds"
[grid]
sif_743 = "PRODUCT/SIF_743"
sif_735 = "PRODUCT/SIF_735"
[filter]
"PRODUCT/SUPPORT_DATA/GEOLOCATIONS/solar_zenith_angle" = "< 80"config = load_config(joinpath(pkgdir(SatelliteGridding), "examples", "tropomi_sif.toml"))DataSourceConfig(Dict("lat" => "PRODUCT/latitude", "lon_bnd" => "PRODUCT/SUPPORT_DATA/GEOLOCATIONS/longitude_bounds", "lat_bnd" => "PRODUCT/SUPPORT_DATA/GEOLOCATIONS/latitude_bounds", "lon" => "PRODUCT/longitude"), OrderedCollections.OrderedDict("sif_735_corr" => "PRODUCT/SIF_Corr_735", "sif_743" => "PRODUCT/SIF_743", "sif_735" => "PRODUCT/SIF_735", "cloud_fraction" => "PRODUCT/SUPPORT_DATA/INPUT_DATA/cloud_fraction_L2", "sif_743_corr" => "PRODUCT/SIF_Corr_743", "sif_743_err" => "PRODUCT/SIF_ERROR_743"), FilterRule[FilterRule("PRODUCT/SUPPORT_DATA/GEOLOCATIONS/solar_zenith_angle", :lt, 80.0, NaN)], "S5P_PAL__L2B_SIF____YYYYMMDD*.nc", "/kiwi-data/Data/satellite/TROPOMI/TROPOMI_SIF_S5P-PAL/original/", Dict{String, Any}())The config object contains:
basic: Coordinate variable paths (lat, lon, and corner bounds)grid_vars: Variables to be gridded (e.g., SIF at 743nm and 735nm)filters: Quality filter rules (here: solar zenith angle < 80°)file_pattern: Glob pattern with date placeholdersfolder: Root data directory
Step 2: Define the Output Grid
A GridSpec defines the spatial extent and resolution of the output grid. All values are in degrees. The package uses Float32 for memory efficiency.
grid_spec = GridSpec(
lat_min = -60f0, # Southern bound
lat_max = 80f0, # Northern bound
lon_min = -180f0, # Western bound
lon_max = 180f0, # Eastern bound
dlat = 0.5f0, # Latitude resolution (degrees)
dlon = 0.5f0 # Longitude resolution (degrees)
)RectangularGridSpec{Float32}(-60.0f0, 80.0f0, -180.0f0, 180.0f0, 0.5f0, 0.5f0, Float32[-59.75, -59.25, -58.75, -58.25, -57.75, -57.25, -56.75, -56.25, -55.75, -55.25 … 75.25, 75.75, 76.25, 76.75, 77.25, 77.75, 78.25, 78.75, 79.25, 79.75], Float32[-179.75, -179.25, -178.75, -178.25, -177.75, -177.25, -176.75, -176.25, -175.75, -175.25 … 175.25, 175.75, 176.25, 176.75, 177.25, 177.75, 178.25, 178.75, 179.25, 179.75])This creates a 720×280 grid. The grid_spec.lat and grid_spec.lon vectors contain cell center coordinates.
println("Grid dimensions: $(length(grid_spec.lon)) × $(length(grid_spec.lat))")Grid dimensions: 720 × 280Step 3: Define the Time Specification
A TimeSpec controls temporal binning. You can use Dates.Day or Dates.Month for the time step.
time_spec = TimeSpec(
DateTime("2019-07-01"), # Start date
DateTime("2019-07-31"), # Stop date
Dates.Day(16) # 16-day composites
)TimeSpec(Dates.DateTime("2019-07-01T00:00:00"), Dates.DateTime("2019-07-31T00:00:00"), Dates.Day(16), 1.0f0, nothing, nothing)For monthly composites:
time_spec = TimeSpec(DateTime("2019-01-01"), DateTime("2019-12-31"),
Dates.Month(1))Step 4: Run the Gridding
The grid_l2 function processes all matching files, applies filters, subdivides footprints, and writes the output NetCDF file.
# grid_l2(config, grid_spec, time_spec;
# outfile = "tropomi_sif_july2019.nc",
# n_oversample = 10) # 10×10 sub-pixels per footprintKey Options
n_oversample: Sub-pixel subdivision factor. Higher = more accurate spatial distribution but slower. Default: auto-computed from footprint/grid ratio.compute_std: Set totrueto also compute per-cell standard deviation.backend: Compute backend —nothing(sequential Welford),CPU()(KA parallel),CUDABackend()(NVIDIA GPU),MetalBackend()(Apple GPU), or another compatible KernelAbstractions backend.
Using the KA Backend
For large datasets, the KernelAbstractions backend parallelizes the computation:
using KernelAbstractions
grid_l2(config, grid_spec, time_spec;
outfile = "output.nc",
backend = CPU())The KA backend uses sum-based accumulation (instead of Welford's incremental mean), which is fully parallelizable. The mean is computed at the end: mean = sum / weight.
Circular Footprints (GOSAT)
Some products, such as GOSAT SIF, describe a circular footprint with four bounding coordinates or with center coordinates plus a radius. Use CircularFootprintGridding for those products:
gosat_config = load_config("examples/gosat_sif_center_radius.toml")
grid(gosat_config, grid_spec, time_spec,
CircularFootprintGridding(n_oversample=80);
backend=resolve_backend("cpu"),
outfile="gosat_sif.nc")For center-plus-radius configs, set [basic] lat, [basic] lon, and either [basic] radius or scalar [circle] radius.
Step 5: Inspect the Output
The output NetCDF file contains:
lat,lon: Grid cell center coordinatestime: Time step datesn: Number of observations per cell- One variable per entry in
[grid](e.g.,sif_743,sif_735) - Optional
_stdsuffix variables ifcompute_std=true
using NCDatasets
ds = Dataset("tropomi_sif_july2019.nc")
sif = ds["sif_743"][:, :, 1] # First time step
weights = ds["n"][:, :, 1]
close(ds)Center-Coordinate Gridding (MODIS)
For instruments without footprint bounds (e.g., MODIS), use grid_center:
config = load_config("examples/modis_reflectance.toml")
grid_spec = GridSpec(dlat=0.05f0, dlon=0.05f0)
time_spec = TimeSpec(DateTime("2019-01-01"), DateTime("2019-12-31"),
Dates.Day(1))
grid_center(config, grid_spec, time_spec;
geo_provider=:modis,
veg_indices=true,
outfile="modis_2019.nc")Cubed-Sphere (GEOS/GCHP) Output Grid
To compare TROPOMI directly against GEOS-Chem (GCHP) output on its native grid, swap the rectangular GridSpec for a CubedSphereGridSpec — everything else (the config, the grid call) is identical. The default geometry is the GEOS/GCHP convention (equal-distance gnomonic, GEOS-native panels, -10° shift), so Nc = 360 gives a GCHP-style C360 grid. Here we build a small C24 grid:
cs_spec = CubedSphereGridSpec(; Nc = 24) # GMAOCubedSphereDefinition() by defaultCubedSphereGridSpec{Float32, CubedSphereDefinition{GMAOEqualDistanceGnomonic, FourCornerNormalizedCenter, GEOSNativePanelConvention}}(24, CubedSphereDefinition{GMAOEqualDistanceGnomonic, FourCornerNormalizedCenter, GEOSNativePanelConvention}(GMAOEqualDistanceGnomonic(), FourCornerNormalizedCenter(), GEOSNativePanelConvention(), -10.0, :gmao_equal_distance), Float32[306.57373 306.57568 … 306.57568 306.57373; 309.80566 309.80765 … 309.80765 309.80566; … ; 30.194336 30.192371 … 30.192371 30.194336; 33.42628 33.424313 … 33.424313 33.42628;;; 36.573715 36.57568 … 36.57568 36.573715; 39.805668 39.807632 … 39.807632 39.805668; … ; 120.19433 120.19236 … 120.19236 120.19433; 123.42628 123.42431 … 123.42431 123.42628;;; 34.999996 31.755072 … 308.24493 305.0; 38.244923 34.999996 … 305.0 301.75507; … ; 121.755066 124.99999 … 215.00002 218.24493; 124.99999 128.24492 … 211.75508 215.00002;;; 126.573715 129.80566 … 210.19434 213.4263; 126.575676 129.80763 … 210.19238 213.42433; … ; 126.575676 129.80763 … 210.19238 213.42433; 126.573715 129.80566 … 210.19434 213.4263;;; 216.57373 219.80568 … 300.19434 303.4263; 216.5757 219.80765 … 300.19238 303.42432; … ; 216.5757 219.80765 … 300.19238 303.42432; 216.57373 219.80568 … 300.19434 303.4263;;; 215.00002 218.24493 … 301.75507 305.0; 211.75508 215.00002 … 305.0 308.24493; … ; 128.24492 124.99999 … 34.999996 31.755072; 124.99999 121.755066 … 38.244926 34.999996], Float32[-34.50544 -31.534674 … 31.534674 34.505436; -35.865456 -32.836414 … 32.836414 35.865456; … ; -35.865456 -32.836414 … 32.836414 35.865456; -34.505444 -31.534674 … 31.534674 34.50544;;; -34.50544 -31.534674 … 31.534674 34.505436; -35.865456 -32.836414 … 32.836414 35.865456; … ; -35.865456 -32.836414 … 32.836414 35.865456; -34.505444 -31.534674 … 31.534674 34.50544;;; 36.761852 38.245205 … 38.245205 36.761852; 38.245205 39.92845 … 39.92845 38.245205; … ; 38.245205 39.92845 … 39.92845 38.245205; 36.761852 38.245205 … 38.245205 36.761852;;; 34.50544 35.865456 … 35.865456 34.505444; 31.534678 32.836414 … 32.836414 31.534674; … ; -31.534678 -32.836414 … -32.836414 -31.534674; -34.505436 -35.865456 … -35.865456 -34.50544;;; 34.50544 35.865456 … 35.865456 34.505444; 31.534678 32.836414 … 32.836414 31.534674; … ; -31.534678 -32.836414 … -32.836414 -31.534674; -34.505436 -35.865456 … -35.865456 -34.50544;;; -36.761852 -38.245205 … -38.245205 -36.761852; -38.245213 -39.92845 … -39.92845 -38.245205; … ; -38.245205 -39.92845 … -39.92845 -38.245205; -36.761852 -38.245205 … -38.245205 -36.761852], Float32[305.0 305.0 … 305.0 305.0; 308.1741 308.17413 … 308.17413 308.1741; … ; 31.82589 31.825886 … 31.825886 31.82589; 34.999996 34.999996 … 34.999996 34.999996;;; 34.999996 34.999996 … 34.999996 34.999996; 38.17411 38.174114 … 38.174114 38.17411; … ; 121.82588 121.82587 … 121.82587 121.82588; 124.99999 124.99999 … 124.99999 124.99999;;; 34.999996 31.82589 … 308.1741 305.0; 38.17411 34.999996 … 305.0 301.8259; … ; 121.82588 124.99999 … 215.00002 218.17412; 124.99999 128.1741 … 211.8259 215.00002;;; 124.99999 128.1741 … 211.8259 215.00002; 124.99999 128.1741 … 211.8259 215.00002; … ; 124.99999 128.1741 … 211.8259 215.00002; 124.99999 128.1741 … 211.8259 215.00002;;; 215.00002 218.17412 … 301.8259 305.0; 215.00002 218.17412 … 301.8259 305.0; … ; 215.00002 218.17412 … 301.8259 305.0; 215.00002 218.17412 … 301.8259 305.0;;; 215.00002 218.17412 … 301.8259 305.0; 211.8259 215.00002 … 305.0 308.1741; … ; 128.1741 124.99999 … 34.999996 31.82589; 124.99999 121.82588 … 38.17411 34.999996], Float32[-35.264393 -32.325695 … 32.325695 35.264393; -36.69255 -33.6981 … 33.6981 36.69255; … ; -36.692554 -33.6981 … 33.6981 36.692554; -35.264393 -32.325695 … 32.325695 35.264393;;; -35.264393 -32.325695 … 32.325695 35.264393; -36.69255 -33.6981 … 33.6981 36.69255; … ; -36.692554 -33.6981 … 33.6981 36.692554; -35.264393 -32.325695 … 32.325695 35.264393;;; 35.264393 36.692554 … 36.69255 35.264393; 36.69255 38.313572 … 38.313572 36.692554; … ; 36.692554 38.313572 … 38.313572 36.69255; 35.264393 36.69255 … 36.692554 35.264393;;; 35.264393 36.69255 … 36.692554 35.264393; 32.325695 33.6981 … 33.6981 32.325695; … ; -32.325695 -33.6981 … -33.6981 -32.325695; -35.264393 -36.69255 … -36.692554 -35.264393;;; 35.264393 36.69255 … 36.692554 35.264393; 32.325695 33.6981 … 33.6981 32.325695; … ; -32.325695 -33.6981 … -33.6981 -32.325695; -35.264393 -36.69255 … -36.692554 -35.264393;;; -35.264393 -36.69255 … -36.692554 -35.264393; -36.692554 -38.313572 … -38.313572 -36.69255; … ; -36.69255 -38.313572 … -38.313572 -36.692554; -35.264393 -36.692554 … -36.69255 -35.264393])The cube has Nc cells per panel edge across 6 panels. Internally it is folded into a 2D (Nc, 6·Nc) accumulator and un-folded to (Xdim, Ydim, nf) on write:
println("C$(cs_spec.Nc): grid_shape = $(grid_shape(cs_spec)), centers = $(size(cs_spec.centers_lon))")C24: grid_shape = (24, 144), centers = (24, 24, 6)Grid exactly as before — only the grid spec changed (sequential CPU path):
grid(config, cs_spec, time_spec, SubpixelGridding();
outfile = "tropomi_sif_c24.nc")Or from the CLI:
julia --project=. bin/grid.jl l2 --config examples/tropomi_sif.toml \
--gridType cs --Nc 360 --startDate 2020-07-01 --stopDate 2020-07-16 \
--dDays 16 -o tropomi_sif_c360.ncThe output matches native GCHP NetCDF (dims nf/Ydim/Xdim, 2D lons/lats and corner_lons/corner_lats, coordinates = "lons lats"), so it opens directly in Panoply and xarray. Plot one panel on its 2D coordinates:
import xarray as xr
ds = xr.open_dataset("tropomi_sif_c360.nc")
ds["sif_743"].isel(time=0, nf=2).plot(x="lons", y="lats")See the Cubed-Sphere Grids page for the full convention, output layout, and the bin/run_tropomi_c360.sh fan-out runner.