Tutorial: Footprint Aggregation And Flux Estimation#

This tutorial shows the next common step after generating footprints: combine them with a flux description to estimate concentration enhancements.

What you’ll learn#

  • how to aggregate footprint sensitivity at discrete source locations

  • how to convolve integrated footprints with a gridded inventory

  • how to build a simple modeled enhancement time series

Starting point#

Assume you already have a project with footprints, for example from Tutorial: Stationary Station (WBB).

Point-source aggregation#

Use stilt.Footprint.aggregate() when you have discrete source locations:

import numpy as np
import pandas as pd

footprints = model.footprints["wbb"].load(mets="hrrr")

sources = [
    (-111.970, 40.515, 45.0),
    (-112.015, 40.779, 120.0),
    (-111.890, 40.650, 30.0),
]
coords = [(lon, lat) for lon, lat, _ in sources]
fluxes = np.array([flux for _, _, flux in sources])

rows = []
for foot in footprints:
    start, end = foot.time_range
    bins = pd.interval_range(start=start, end=end, freq="1h")
    sensitivity = foot.aggregate(coords=coords, time_bins=bins)
    enhancement = (sensitivity.to_numpy() * fluxes[:, None]).sum(axis=0)
    rows.append(
        pd.Series(
            enhancement,
            index=[interval.mid for interval in bins],
            name=foot.receptor.id,
        )
    )

modeled = pd.concat(rows, axis=1).T

Gridded inventory convolution#

For gridded inventories, integrate the footprint over time and multiply cell by cell:

import xarray as xr

inventory = xr.open_dataarray("inventory.nc")

enhancements = []
for foot in footprints:
    integrated = foot.integrate_over_time().data
    inventory_on_grid = inventory.interp(
        lat=integrated.lat,
        lon=integrated.lon,
        method="linear",
    )
    enhancement = float((integrated * inventory_on_grid).sum(["lat", "lon"]))
    enhancements.append(
        {"time": foot.receptor.time, "enhancement_ppm": enhancement}
    )

modeled = pd.DataFrame(enhancements).set_index("time").sort_index()

Comparing with observations#

import matplotlib.pyplot as plt

observed = pd.read_csv("observations.csv", index_col="time", parse_dates=True)

fig, ax = plt.subplots(figsize=(12, 4))
observed["ch4_ppm"].plot(ax=ax, label="Observed", color="k", alpha=0.7)
modeled["enhancement_ppm"].plot(ax=ax, label="Modeled enhancement", color="tab:red")
ax.legend()
ax.set_ylabel("CH4 (ppm)")
plt.tight_layout()

This is still only the footprint-times-flux part of an inversion workflow, but it is the central bridge from transport to emissions analysis.