Part VI: Discovery in Scientific Domains
Chapter 51: Discovery AI for Climate and Earth Science

51.2 Climate Emulation and Downscaling

"The general circulation model ran for three months on a supercomputer. I reproduced its century-long simulation in forty seconds. We agreed on the global mean. We disagreed on whether this was progress or an insult."

A Climate Emulator With an Efficiency Complex
The Big Picture

Weather prediction asks "what will happen next week?" Climate science asks "how will the statistical distribution of weather change over decades?" These are fundamentally different questions. A weather model must track individual atmospheric states; a climate model must reproduce the long-term statistics (mean temperature, precipitation patterns, variability, extremes) under different forcing scenarios. General circulation models (GCMs) answer climate questions by running physics-based simulations for centuries, but this costs millions of CPU hours per scenario. Climate emulators learn to approximate GCM outputs from forcing inputs, producing century-long projections in seconds. Downscaling then translates coarse global projections to the fine regional detail that adaptation planning requires. Figure 51.6 illustrates this pipeline from forcing scenarios through emulation and downscaling to local impact assessment.

Forcing Scenarios (CO2, CH4, aerosols) Climate Emulator Neural net / pattern scaling (seconds) Coarse Projection (1-2 deg spatial grid) Downscaling PP / MOS / CNN super-resolution Local Projections (0.1 deg, catchment scale) Full GCM (weeks of compute) trains on Impact Assessment (flood, crop, health) Satellite / EO Data (Sentinel, GEDI, OCO)
Figure 51.6: The climate emulation and downscaling pipeline. Forcing scenarios feed a climate emulator (trained on full GCM output) to produce coarse spatial projections in seconds. Downscaling refines these to local resolution using statistical or neural methods, optionally conditioned on satellite and earth observation data. Local projections then drive impact assessment for adaptation planning.

1. Climate Emulation

In 2023, a team at the University of Leeds reportedly reproduced a century of climate projections from a state-of-the-art general circulation model in under forty seconds on a single GPU, matching the spatial warming patterns to within a fraction of a degree for the specific GCM and scenarios tested. The trick was not a faster supercomputer; it was a neural network trained to skip the physics entirely.

Every degree of global warming triggers cascading decisions: which coastlines to fortify, which crops to replant, which infrastructure to relocate. Policymakers need projections across hundreds of emission scenarios, but each full GCM run can consume weeks of supercomputer time, making exhaustive exploration impossible with physics-based models alone.

What. A climate emulator is a statistical or neural model that maps forcing scenarios (greenhouse gas concentrations, aerosol emissions, land-use changes) to climate response fields (spatial patterns of temperature, precipitation, sea level pressure) without running a physics-based GCM. The emulator learns the input-output relationship of the GCM from a training set of completed simulations.

Why. The Coupled Model Intercomparison Project (CMIP) produces climate projections by running dozens of GCMs under multiple Shared Socioeconomic Pathways (SSPs), where each SSP defines a plausible future trajectory of emissions, land use, and socioeconomic development. Each combination of model and scenario costs weeks to months of supercomputer time. Policy analysis, uncertainty quantification, and rapid scenario exploration all benefit from emulators that can produce comparable projections in seconds.

How. The simplest emulators use pattern scaling: assume that the spatial pattern of warming is constant and only the global mean temperature changes with forcing. More sophisticated approaches use Gaussian processes, random forests, or neural networks to learn non-linear relationships between forcing and spatial response patterns. Figure 51.2.1 illustrates climate emulation and downscaling pipeline.

Climate emulation and downscaling pipeline
Figure 51.2.1: The climate emulation and downscaling pipeline. Forcing scenarios feed either a full GCM (months of compute) or a neural emulator (seconds) to produce coarse-resolution climate fields. Statistical or learned downscaling then translates these coarse projections to the fine regional detail required for adaptation planning, optionally incorporating high-resolution topography and station observations.

Pattern Scaling in Detail

Pattern scaling starts with a GCM run that produces a spatial warming map for one reference scenario. The method then normalizes that map by the global mean temperature change, yielding a dimensionless "fingerprint." For any new forcing scenario, it multiplies this fingerprint by the predicted global mean temperature (often from a simple energy balance model). The result is an approximate spatial field produced without re-running the GCM. This matters because it collapses century-long spatial projections to a single scalar prediction problem, cutting compute by orders of magnitude. Pattern scaling works well when forcing changes are moderate and the spatial response scales linearly with global temperature. When strong aerosol forcing, abrupt ice-sheet changes, or precipitation extremes break the linearity assumption, learned emulators or full GCM runs become necessary.

When. Use emulators for scenario screening (which SSPs produce dangerous warming in which regions?), uncertainty quantification (how much does the response vary across GCMs?), and impact assessment (feeding climate projections into agricultural, hydrological, or economic models that need thousands of scenarios). In short: an emulator trades physical fidelity for the ability to ask a thousand "what if" questions in the time a GCM answers one.

Common Misconception

A frequent misconception is that a faster emulator is a better climate model. Speed is an engineering advantage, not a scientific one: the emulator inherits all of the biases, structural assumptions, and domain limitations of the GCM it was trained on. If the training GCM underestimates Arctic amplification or misrepresents monsoon dynamics, the emulator will faithfully reproduce those same errors at a thousand times the speed.

Key Insight: Emulation vs. Simulation

A climate emulator is not a climate model. It does not solve the Navier-Stokes equations (the partial differential equations governing fluid motion in the atmosphere and ocean) or represent cloud microphysics. It learns the mapping that a GCM implements, bypassing the physics entirely. This makes it fast but also means it cannot extrapolate beyond the forcing scenarios it was trained on. If the real climate enters a regime that no GCM has simulated (a sudden ice-sheet collapse, a methane feedback loop), the emulator has no mechanism to capture the new physics. This is why emulators complement rather than replace GCMs: emulators accelerate exploration within the GCM's domain of validity, while GCMs push the boundaries of physical understanding. The same principle applies to the scientific ML surrogates we built in Chapter 33.

"""
Climate emulator: predict spatial temperature response from
forcing scenarios using a neural network trained on CMIP6 output.
"""
import numpy as np
import torch
import torch.nn as nn
import xarray as xr
from pathlib import Path


class ClimateEmulator(nn.Module):
    """Neural climate emulator mapping forcing to spatial response.

    Input: time series of global forcings (CO2, CH4, SO2, etc.)
    Output: spatial temperature anomaly field (lat x lon)

    Architecture: encode the forcing time series with a 1D CNN,
    then decode to spatial patterns with a learned basis.
    """

    def __init__(self, n_forcings: int = 4, n_years: int = 86,
                 n_lat: int = 64, n_lon: int = 128,
                 n_basis: int = 32, hidden_dim: int = 256):
        super().__init__()
        self.n_lat = n_lat
        self.n_lon = n_lon
        self.n_basis = n_basis

        # Temporal encoder: process forcing time series
        self.forcing_encoder = nn.Sequential(
            nn.Conv1d(n_forcings, hidden_dim, kernel_size=5, padding=2),
            nn.ReLU(),
            nn.Conv1d(hidden_dim, hidden_dim, kernel_size=5, padding=2),
            nn.ReLU(),
            nn.AdaptiveAvgPool1d(1),  # global average over time
            nn.Flatten(),
        )

        # Map encoded forcing to basis coefficients
        self.basis_predictor = nn.Sequential(
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, n_basis),
        )

        # Learnable spatial basis patterns
        # Each basis pattern is a (lat, lon) field
        self.spatial_basis = nn.Parameter(
            torch.randn(n_basis, n_lat, n_lon) * 0.01
        )

        # Predict per-year scaling from forcing time series
        self.year_predictor = nn.Sequential(
            nn.Conv1d(n_forcings, hidden_dim, kernel_size=5, padding=2),
            nn.ReLU(),
            nn.Conv1d(hidden_dim, n_basis, kernel_size=1),
        )

    def forward(self, forcings: torch.Tensor) -> torch.Tensor:
        """Predict spatial temperature response from forcing scenario.

        Args:
            forcings: (batch, n_forcings, n_years)
                Time series of greenhouse gas concentrations,
                aerosol emissions, etc.
        Returns:
            temperature_anomaly: (batch, n_years, n_lat, n_lon)
                Predicted temperature change relative to baseline
        """
        # Global basis coefficients from forcing summary
        encoded = self.forcing_encoder(forcings)   # (B, hidden)
        global_coeff = self.basis_predictor(encoded)  # (B, n_basis)

        # Per-year modulation of basis coefficients
        year_coeff = self.year_predictor(forcings)  # (B, n_basis, n_years)

        # Combine: global pattern x yearly modulation
        coeff = global_coeff.unsqueeze(-1) * year_coeff  # (B, n_basis, years)

        # Reconstruct spatial field from basis
        # spatial_basis: (n_basis, lat, lon) -> (1, n_basis, lat*lon)
        basis_flat = self.spatial_basis.reshape(self.n_basis, -1).unsqueeze(0)

        # coeff: (B, n_basis, years) @ basis: (1, n_basis, lat*lon)
        # -> (B, years, lat*lon)
        response = torch.einsum("bkt,bkp->btp", coeff.transpose(1, 2),
                                basis_flat.expand(coeff.shape[0], -1, -1))

        return response.reshape(-1, coeff.shape[-1],
                                self.n_lat, self.n_lon)


def train_emulator(model: ClimateEmulator,
                   forcing_data: torch.Tensor,
                   target_data: torch.Tensor,
                   n_epochs: int = 200,
                   lr: float = 1e-3) -> list[float]:
    """Train the climate emulator on CMIP6 model output.

    The training set consists of multiple GCM simulations
    under different SSP scenarios. Each sample is a
    (forcing_timeseries, temperature_response) pair.

    Args:
        model: ClimateEmulator instance
        forcing_data: (n_scenarios, n_forcings, n_years)
        target_data: (n_scenarios, n_years, n_lat, n_lon)
        n_epochs: Training epochs
        lr: Learning rate
    Returns:
        List of training losses
    """
    optimizer = torch.optim.AdamW(model.parameters(), lr=lr,
                                  weight_decay=1e-5)
    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
        optimizer, T_max=n_epochs
    )
    losses = []

    for epoch in range(n_epochs):
        model.train()
        pred = model(forcing_data)
        loss = nn.functional.mse_loss(pred, target_data)

        # Add spatial smoothness regularization
        # Penalize high-frequency spatial noise
        dx = pred[:, :, :, 1:] - pred[:, :, :, :-1]
        dy = pred[:, :, 1:, :] - pred[:, :, :-1, :]
        smooth_loss = 0.01 * (dx.pow(2).mean() + dy.pow(2).mean())

        total_loss = loss + smooth_loss
        optimizer.zero_grad()
        total_loss.backward()
        optimizer.step()
        scheduler.step()

        losses.append(total_loss.item())
        if (epoch + 1) % 50 == 0:
            print(f"Epoch {epoch+1}/{n_epochs}, "
                  f"MSE: {loss.item():.6f}, "
                  f"Smooth: {smooth_loss.item():.6f}")

    return losses
Figure 51.7: Neural climate emulator mapping forcing time series (CO2, CH4, aerosol concentrations) to spatial temperature response fields via a learned spatial basis and temporal encoder, trained on CMIP6 GCM output.

2. Statistical Downscaling

GCMs and climate emulators produce projections on coarse grids (typically 1 to 2 degrees, or 100 to 200 km). Adaptation planning needs local detail: how will rainfall change in a specific river catchment? Will a particular city face more extreme heat days? Downscaling bridges this resolution gap.

Statistical downscaling learns relationships between large-scale atmospheric patterns and local weather variables from historical observations. The assumption is that these relationships, calibrated on the recent past, will hold under future climate conditions. This "stationarity assumption" poses the primary limitation of statistical downscaling. If climate change alters local processes (by shifting the jet stream or changing land surface properties), the learned relationships break down.

Mental Model

Think of statistical downscaling like a local weather forecaster who has spent decades learning that "when the barometric pressure drops and the wind shifts southwest, our valley gets heavy rain." She does not simulate the physics of the atmosphere; she recognizes recurring large-scale patterns and translates them into local consequences based on years of observed correlation. If the climate shifts so that entirely new wind patterns appear (ones she has never seen before), her local translation rules stop working. That is exactly the stationarity assumption: the forecaster's learned dictionary of "large pattern maps to local outcome" remains valid only as long as the large-scale regimes stay within the range she trained on.

Three families of methods dominate.

Perfect prognosis (PP) methods train a regression model from observed large-scale predictors (reanalysis data, where reanalysis is a retrospective atmospheric reconstruction that blends observations with a weather model to produce a spatially complete, physically consistent record of past weather) to observed local-scale predictands (the target variables being predicted, such as station-level temperature or precipitation). At prediction time, the GCM's large-scale output replaces the reanalysis data. This assumes that the GCM represents large-scale patterns well enough to substitute for reanalysis.

Model output statistics (MOS) methods train directly on GCM output paired with observations, learning to correct the GCM's biases. MOS is simpler but couples the downscaling model to a specific GCM.

Learned downscaling uses convolutional neural networks or diffusion models to super-resolve coarse climate fields, treating the problem as analogous to image super-resolution. This approach has the advantage of producing spatially coherent high-resolution fields rather than point predictions.

"""
Statistical downscaling: from coarse GCM output to fine-resolution
local climate using both classical and neural approaches.
"""
import numpy as np
import torch
import torch.nn as nn
import xarray as xr
from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler


class PerfectPrognosisDownscaler:
    """Classical statistical downscaling via perfect prognosis.

    Trains a Ridge regression from large-scale reanalysis
    predictors to local station observations. At inference time,
    GCM output replaces reanalysis as input.
    """

    def __init__(self, alpha: float = 1.0):
        self.scaler_X = StandardScaler()
        self.scaler_y = StandardScaler()
        self.model = Ridge(alpha=alpha)

    def fit(self, predictors: np.ndarray,
            targets: np.ndarray) -> "PerfectPrognosisDownscaler":
        """Train on reanalysis predictors and station observations.

        Args:
            predictors: (n_samples, n_features) large-scale fields
                (e.g., 500hPa geopotential, 850hPa temperature,
                sea level pressure) extracted from ERA5
            targets: (n_samples,) local variable
                (e.g., daily max temperature at a station)
        Returns:
            self
        """
        X = self.scaler_X.fit_transform(predictors)
        y = self.scaler_y.fit_transform(targets.reshape(-1, 1)).ravel()
        self.model.fit(X, y)
        return self

    def predict(self, gcm_predictors: np.ndarray) -> np.ndarray:
        """Downscale GCM output to local predictions.

        Args:
            gcm_predictors: (n_samples, n_features) large-scale
                fields from a GCM simulation
        Returns:
            Local-scale predictions
        """
        X = self.scaler_X.transform(gcm_predictors)
        y_scaled = self.model.predict(X)
        return self.scaler_y.inverse_transform(
            y_scaled.reshape(-1, 1)
        ).ravel()


class ConvDownscaler(nn.Module):
    """Neural downscaling via learned super-resolution.

    Treats climate downscaling as a super-resolution problem:
    upscale coarse (e.g., 1-degree) fields to fine resolution
    (e.g., 0.1-degree) using residual convolutional blocks and
    pixel shuffle upsampling.

    This produces spatially coherent high-resolution fields
    rather than independent point predictions.
    """

    def __init__(self, in_channels: int = 5, scale_factor: int = 10,
                 hidden_channels: int = 64, n_res_blocks: int = 8):
        super().__init__()
        self.scale_factor = scale_factor

        # Initial feature extraction
        self.head = nn.Sequential(
            nn.Conv2d(in_channels, hidden_channels, 3, padding=1),
            nn.ReLU(),
        )

        # Residual blocks for deep feature extraction
        self.body = nn.Sequential(*[
            ResidualBlock(hidden_channels) for _ in range(n_res_blocks)
        ])

        # Upsampling via sub-pixel convolution (pixel shuffle)
        upsample_layers = []
        remaining = scale_factor
        while remaining > 1:
            if remaining >= 5:
                factor = 5
            elif remaining >= 2:
                factor = 2
            else:
                factor = remaining
            upsample_layers.extend([
                nn.Conv2d(hidden_channels,
                          hidden_channels * factor ** 2, 3, padding=1),
                nn.PixelShuffle(factor),
                nn.ReLU(),
            ])
            remaining //= factor

        self.upsample = nn.Sequential(*upsample_layers)

        # Final output layer
        self.tail = nn.Conv2d(hidden_channels, 1, 3, padding=1)

    def forward(self, coarse: torch.Tensor,
                topography: torch.Tensor = None) -> torch.Tensor:
        """Downscale coarse climate field to fine resolution.

        Args:
            coarse: (batch, in_channels, lat_coarse, lon_coarse)
                Coarse-resolution fields (temperature, pressure,
                humidity, wind components)
            topography: Optional (batch, 1, lat_fine, lon_fine)
                High-resolution topography for conditioning
        Returns:
            fine: (batch, 1, lat_fine, lon_fine) downscaled field
        """
        x = self.head(coarse)
        x = x + self.body(x)  # global skip connection
        x = self.upsample(x)
        x = self.tail(x)

        # Add topographic correction if available
        if topography is not None:
            # Lapse rate correction: temperature decreases with altitude
            lapse_rate = -6.5e-3  # K/m, standard atmosphere
            x = x + lapse_rate * topography

        return x


class ResidualBlock(nn.Module):
    """Residual convolutional block with batch normalization."""

    def __init__(self, channels: int):
        super().__init__()
        self.block = nn.Sequential(
            nn.Conv2d(channels, channels, 3, padding=1),
            nn.BatchNorm2d(channels),
            nn.ReLU(),
            nn.Conv2d(channels, channels, 3, padding=1),
            nn.BatchNorm2d(channels),
        )

    def forward(self, x):
        return x + self.block(x)
Figure 51.8: Classical and neural statistical downscaling. PerfectPrognosisDownscaler maps large-scale reanalysis predictors to local station observations via Ridge regression. ConvDownscaler treats downscaling as learned super-resolution with residual convolutions, pixel shuffle upsampling, and optional topographic lapse-rate correction.
Practical Example: Downscaling Precipitation for Flood Risk

A European climate adaptation project needed 0.1-degree precipitation projections for flood risk modeling across Alpine river catchments. Coarse CMIP6 models at 1-degree resolution could not resolve the orographic enhancement (mountains forcing air upward, causing intense local rainfall) that drives Alpine flood risk. The project trained a ConvDownscaler on ERA5 reanalysis paired with high-resolution E-OBS station observations, incorporating a 90-meter digital elevation model (DEM) as a conditioning input. The downscaled projections captured orographic precipitation patterns that the coarse models missed entirely, enabling the first catchment-scale flood risk assessments under SSP2-4.5 and SSP5-8.5 scenarios. The key lesson: topographic conditioning is essential for precipitation downscaling in mountainous terrain.

3. Foundation Models for Earth Observation

Downscaling refines climate projections to local resolution, but those projections are only as useful as the observational data they connect to on the ground, and that ground truth increasingly arrives from space.

Earth observation satellites generate petabytes of multispectral imagery annually; Sentinel-2 alone produces 1.6 TB per day, more raw data every week than the entire Landsat archive accumulated over its first two decades. Traditional pipelines require hand-engineered features and a separate model for every task. Foundation models instead learn general-purpose representations from unlabeled imagery, then adapt with minimal labeled data.

The approach mirrors the foundation model paradigm from Chapter 27: pretrain a large model on self-supervised objectives (masked image modeling, temporal prediction), then fine-tune on downstream tasks. Three models define the current landscape.

Prithvi (IBM/NASA, 2023) is a vision Transformer pretrained on Harmonized Landsat Sentinel-2 (HLS) data. It processes multi-temporal, multi-spectral inputs and has been fine-tuned for flood mapping, wildfire scar detection, and multi-temporal crop segmentation. (As of 2024, Prithvi-2 extends the original with higher resolution, additional sensor modalities, and improved temporal encoding, supporting a broader range of geospatial tasks.)

Clay (Clay Foundation, 2024) trains on diverse satellite modalities (optical, synthetic aperture radar (SAR), DEM) with a masked autoencoder objective, producing embeddings that transfer across sensors and geographies.

SatMAE (Cong et al., 2022) adapts the Masked Autoencoder framework to multispectral satellite imagery, learning to reconstruct masked patches across spectral bands and time steps. (As of 2025, several successors, including SatMAE++ and SpectralGPT, have extended this approach with cross-sensor transfer and spectral-spatial joint pretraining, reflecting rapid progress in the earth observation foundation model space.)

Real-World Application: ClimateTrace Global Emissions Monitoring
Real-World Application: ClimateTrace Global Emissions Monitoring

Checkpoint

So far: foundation models for earth observation (Prithvi, Clay, SatMAE) learn general-purpose representations from unlabeled satellite imagery via self-supervised pretraining, then adapt to downstream tasks (flood mapping, crop segmentation, land cover classification) with minimal labeled data, following the same pretrain-then-fine-tune paradigm introduced in Chapter 27.

"""
Using Google Earth Engine for large-scale earth observation
data access and preprocessing, feeding into foundation models.
"""
import ee
import numpy as np
import xarray as xr

# Initialize Earth Engine (requires authentication)
# ee.Authenticate()
# ee.Initialize(project='your-project-id')


def get_ndvi_timeseries(
    geometry: ee.Geometry,
    start_date: str,
    end_date: str,
    cloud_threshold: float = 20.0,
) -> ee.ImageCollection:
    """Extract cloud-filtered NDVI time series from Sentinel-2.

    Normalized Difference Vegetation Index (NDVI) tracks
    vegetation health and phenology from space:
    NDVI = (NIR - Red) / (NIR + Red)

    Args:
        geometry: Region of interest as an Earth Engine geometry
        start_date: Start date (YYYY-MM-DD)
        end_date: End date (YYYY-MM-DD)
        cloud_threshold: Maximum cloud cover percentage
    Returns:
        ImageCollection of NDVI images
    """
    s2 = (
        ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
        .filterBounds(geometry)
        .filterDate(start_date, end_date)
        .filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE",
                             cloud_threshold))
    )

    def add_ndvi(image):
        ndvi = image.normalizedDifference(["B8", "B4"]).rename("NDVI")
        return image.addBands(ndvi)

    return s2.map(add_ndvi).select("NDVI")


def compute_land_surface_temperature(
    geometry: ee.Geometry,
    start_date: str,
    end_date: str,
) -> ee.Image:
    """Compute mean land surface temperature from MODIS.

    Land surface temperature (LST) from the MODIS satellite
    provides a direct measure of surface energy balance,
    critical for urban heat island studies and agriculture.

    Args:
        geometry: Region of interest
        start_date: Start date (YYYY-MM-DD)
        end_date: End date (YYYY-MM-DD)
    Returns:
        Mean LST image in Celsius
    """
    lst = (
        ee.ImageCollection("MODIS/061/MOD11A2")
        .filterBounds(geometry)
        .filterDate(start_date, end_date)
        .select("LST_Day_1km")
    )

    # Convert from scaled integer to Celsius
    mean_lst = lst.mean().multiply(0.02).subtract(273.15)
    return mean_lst.clip(geometry)


def carbon_stock_estimation(
    geometry: ee.Geometry,
    year: int = 2023,
) -> dict:
    """Estimate above-ground carbon stocks using satellite data.

    Combines Global Forest Canopy Height (GEDI/Landsat),
    land cover classification, and allometric models (which
    estimate whole-tree biomass from measurable quantities
    like canopy height using species-specific power-law
    relationships) to estimate carbon stored in vegetation.

    Args:
        geometry: Region of interest
        year: Target year
    Returns:
        Dictionary with area and carbon estimates
    """
    # Global forest canopy height from GEDI/Landsat
    canopy_height = ee.Image(
        "users/nlang/ETH_GlobalCanopyHeight_2020_10m_v1"
    ).clip(geometry)

    # Simple allometric model: biomass ~ height^2.5
    # (Real applications use species-specific allometrics)
    biomass_density = canopy_height.pow(2.5).multiply(0.5)

    # Carbon is approximately 50% of dry biomass
    carbon_density = biomass_density.multiply(0.5)  # tonnes C / ha

    # Compute zonal statistics
    stats = carbon_density.reduceRegion(
        reducer=ee.Reducer.mean().combine(
            ee.Reducer.sum(), sharedInputs=True
        ),
        geometry=geometry,
        scale=30,
        maxPixels=1e9,
    )

    return {
        "mean_carbon_density_tC_per_ha": stats.get("b1_mean"),
        "total_carbon_tC": stats.get("b1_sum"),
        "area_ha": geometry.area().divide(10000),
    }
Figure 51.9: Google Earth Engine workflows for earth observation data access. NDVI time series from Sentinel-2 tracks vegetation health; MODIS provides land surface temperature; and GEDI/Landsat canopy height feeds allometric carbon stock estimation for downstream climate applications.

4. Carbon Accounting with Satellite Data

The same satellite imagery and foundation model representations that power land cover classification and crop monitoring also enable a more consequential application: tracking the carbon that drives the climate projections we have been emulating and downscaling.

Carbon accounting quantifies greenhouse gas sources, sinks, and stocks across landscapes. Satellite observations provide three complementary measurements. First, atmospheric CO2 concentrations from the Orbiting Carbon Observatory missions (OCO-2/OCO-3) reveal spatial patterns of carbon sources and sinks at regional scale. Second, land cover change from Landsat/Sentinel-2 tracks deforestation, reforestation, and land-use transitions. Third, biomass estimation from Global Ecosystem Dynamics Investigation (GEDI) lidar (light detection and ranging, which measures surface structure by timing laser pulse reflections) and SAR (Sentinel-1) quantifies the carbon stored in vegetation.

AI enters carbon accounting in two ways. Machine learning models fuse these heterogeneous data sources into consistent carbon flux estimates (how much CO2 is entering or leaving each grid cell). And foundation models pretrained on satellite imagery can classify land cover, detect disturbances, and estimate biomass with far less labeled training data than traditional approaches.

"""
Zarr-based climate data pipeline with the Pangeo stack.
Demonstrates cloud-native access to analysis-ready climate data.
"""
import xarray as xr
import numpy as np
import dask


def load_era5_from_zarr(
    variable: str = "2m_temperature",
    time_range: tuple[str, str] = ("2020-01-01", "2020-12-31"),
    region: dict = None,
) -> xr.DataArray:
    """Load ERA5 reanalysis data from cloud-hosted Zarr store.

    ERA5 is ECMWF's fifth-generation global atmospheric
    reanalysis, covering 1940 to the present at 0.25-degree
    resolution. The Pangeo ecosystem hosts ERA5 data in Zarr
    format on Google Cloud Storage, enabling lazy loading of
    multi-terabyte datasets without downloading anything to
    local disk.

    Args:
        variable: ERA5 variable name
        time_range: (start, end) date strings
        region: Optional dict with lat/lon bounds
            e.g., {"lat": slice(60, 30), "lon": slice(-10, 40)}
    Returns:
        Lazy-loaded xarray DataArray backed by Dask
    """
    # Cloud-hosted ERA5 in Zarr format (Pangeo)
    store = "gs://gcp-public-data-arco-era5/ar/full_37-1h-0p25deg-chunk-1.zarr-v3"

    ds = xr.open_zarr(store, chunks="auto")
    da = ds[variable].sel(time=slice(*time_range))

    if region:
        da = da.sel(**region)

    return da


def compute_climatology(da: xr.DataArray,
                        baseline: tuple[str, str] = ("1991", "2020")
                        ) -> xr.DataArray:
    """Compute day-of-year climatology over a baseline period.

    The climatology is the expected value for each calendar day,
    used as the reference for anomaly calculations and as a
    baseline forecast in WeatherBench2 evaluations.

    Args:
        da: Input DataArray with a time dimension
        baseline: (start_year, end_year) for the baseline period
    Returns:
        Climatology indexed by day-of-year (1 to 366)
    """
    baseline_data = da.sel(
        time=slice(baseline[0], baseline[1])
    )
    climatology = baseline_data.groupby("time.dayofyear").mean("time")
    return climatology


def compute_anomaly(da: xr.DataArray,
                    climatology: xr.DataArray) -> xr.DataArray:
    """Compute anomaly relative to day-of-year climatology.

    Anomalies are departures from the expected seasonal cycle.
    They isolate the weather signal from the climatological
    background, making it easier to detect trends, extreme
    events, and teleconnection patterns (where teleconnections
    are statistical links between weather anomalies at widely
    separated locations, such as the El Nino/Southern Oscillation
    connecting tropical Pacific sea surface temperatures to
    rainfall patterns across multiple continents).

    Args:
        da: Input DataArray
        climatology: Day-of-year climatology from compute_climatology
    Returns:
        Anomaly DataArray
    """
    return da.groupby("time.dayofyear") - climatology


def spatial_average(da: xr.DataArray,
                    weights: xr.DataArray = None) -> xr.DataArray:
    """Area-weighted spatial average.

    Without area weighting, a naive average over a regular
    lat-lon grid overweights polar regions where grid cells
    have less physical area.

    Args:
        da: Input DataArray with lat/lon dimensions
        weights: Optional weights; defaults to cos(lat)
    Returns:
        Spatially averaged time series
    """
    if weights is None:
        weights = np.cos(np.deg2rad(da.lat))
        weights = weights / weights.sum()

    weighted = da.weighted(weights)
    return weighted.mean(["lat", "lon"])
Figure 51.10: Cloud-native climate data pipeline using the Pangeo stack. Lazy-loaded ERA5 reanalysis in Zarr format, day-of-year climatology and anomaly computation, and area-weighted spatial averaging provide the baseline calculations for weather evaluation and extreme event detection.
Library Shortcut: The Pangeo Ecosystem

The code above builds climate analysis primitives from scratch, but the Pangeo ecosystem provides production-ready alternatives. xarray handles labeled multidimensional arrays with built-in groupby, resampling, and weighted operations. zarr enables chunked, compressed, cloud-native storage. dask parallelizes computations transparently across cores or clusters. intake-esm provides a catalog interface for discovering CMIP6 and other climate datasets. xesmf wraps the ESMF regridding library for conservative interpolation between grids. climpred provides forecast verification metrics. Together, these libraries reduce the pipeline above from dozens of lines to a few configuration calls, while handling edge cases (calendar types, missing data, grid singularities) that production code must address. What took 80 lines here is about 10 lines with the full Pangeo stack.

5. ClimateBench: Evaluating Emulators

With emulators, downscalers, foundation models, and carbon pipelines all competing for adoption, a natural question arises: how do we know which emulator to trust?

ClimateBench (Watson-Parris et al., 2022) provides a standardized evaluation framework for climate emulators, analogous to WeatherBench2 for weather models. The benchmark defines four forcing scenarios (historical + SSP1-2.6, SSP2-4.5, SSP3-7.0, SSP5-8.5) and evaluates emulators on their ability to reproduce the spatial patterns of temperature and precipitation response from NorESM2 (a Norwegian Earth System Model contributing to CMIP6) GCM simulations.

The primary metrics are the normalized global RMSE (NRMSE, where RMSE is the root mean square error across all grid cells) for spatial pattern fidelity and the global mean bias for overall calibration. A good emulator captures both the large-scale pattern (warming amplification over land, Arctic amplification, wet-get-wetter/dry-get-drier precipitation) and the regional detail (the North Atlantic warming hole, the Southern Ocean delay).

"""
ClimateBench-style evaluation for climate emulators.
"""
import numpy as np
import xarray as xr


def normalized_global_rmse(
    predicted: xr.DataArray,
    target: xr.DataArray,
    lat: xr.DataArray = None,
) -> float:
    """Normalized global RMSE for spatial pattern evaluation.

    Normalizes by the spatial standard deviation of the target
    field, so NRMSE = 1.0 means the emulator's error is as
    large as the signal's spatial variability.

    Args:
        predicted: Emulated spatial field
        target: GCM target spatial field
        lat: Latitude array for area weighting
    Returns:
        NRMSE score (lower is better, 0 = perfect)
    """
    if lat is None:
        lat = predicted.lat

    weights = np.cos(np.deg2rad(lat))
    weights = weights / weights.sum()
    weights_da = xr.DataArray(weights, dims=["lat"],
                              coords={"lat": lat})

    # Weighted RMSE
    sq_error = (predicted - target) ** 2
    wmse = float((sq_error * weights_da).sum(["lat", "lon"])
                 / weights_da.sum())
    rmse = np.sqrt(wmse)

    # Normalize by target spatial standard deviation
    target_var = float(
        ((target - (target * weights_da).sum(["lat", "lon"])) ** 2
         * weights_da).sum(["lat", "lon"])
    )
    target_std = np.sqrt(target_var)

    return rmse / (target_std + 1e-10)


def pattern_correlation(
    predicted: xr.DataArray,
    target: xr.DataArray,
) -> float:
    """Spatial pattern correlation coefficient.

    Measures whether the emulator reproduces the spatial
    structure of the climate response, regardless of amplitude.

    Args:
        predicted: Emulated spatial field
        target: GCM target spatial field
    Returns:
        Correlation coefficient (-1 to 1, higher is better)
    """
    weights = np.cos(np.deg2rad(predicted.lat))

    p_anom = predicted - float((predicted * weights).sum()
                               / weights.sum())
    t_anom = target - float((target * weights).sum()
                            / weights.sum())

    numerator = float((p_anom * t_anom * weights).sum())
    denom = np.sqrt(
        float((p_anom ** 2 * weights).sum())
        * float((t_anom ** 2 * weights).sum())
    )
    return numerator / (denom + 1e-10)
Figure 51.11: ClimateBench evaluation metrics for climate emulator benchmarking. Normalized global RMSE quantifies spatial pattern fidelity relative to target variability; pattern correlation captures structural agreement independent of amplitude scaling errors.
Practical Example: Rapid Scenario Screening for IPCC

The IPCC's Working Group I evaluates climate projections across dozens of models and scenarios. Running all combinations with full GCMs is computationally prohibitive. Climate emulators trained on the CMIP6 multi-model ensemble enable rapid screening: given a new emissions pathway, the emulator produces spatial temperature and precipitation projections in seconds, along with inter-model uncertainty ranges. This allows policy-relevant questions ("What is the probability that warming exceeds 2C over South Asia under this pathway?") to be answered in real time during negotiations, rather than requiring months of supercomputer allocation.

Research Frontier

ACE2 (Watt-Meyer et al., 2024, "ACE2: Accurately Learning the Earth's Climatic Variables in a Fully Coupled Global AI Framework") demonstrated the first fully coupled global AI climate model capable of producing stable, physically consistent multi-decade simulations. Unlike emulators that learn a single GCM's input-output mapping, ACE2 autoregressively steps the full atmospheric state forward in time, coupled to a slab ocean model (a simplified ocean representation that treats the ocean as a single mixed layer of fixed depth, exchanging heat with the atmosphere but omitting deep-ocean circulation). Trained on ERA5 reanalysis and AMIP-style runs, ACE2 reproduces realistic climatology, interannual variability, and the spatial structure of warming under increased CO2, all while running roughly 100 times faster than a comparable GCM. This pushes beyond the emulation paradigm covered in this section: rather than approximating a GCM's outputs, ACE2 replaces the GCM's dynamical core entirely with a learned model, raising questions about whether future climate projections can be produced without traditional numerical solvers.

Try It: Build a Pattern-Scaling Emulator from CMIP6 Data

1. Install the required libraries (pip install xarray zarr intake-esm matplotlib numpy) and use intake-esm to load a CMIP6 dataset: search the Pangeo CMIP6 catalog for the NorESM2-LM model, historical experiment, surface air temperature variable (tas), and one ensemble member. Load the data lazily with xr.open_zarr.
2. Compute a baseline climatology (1981 to 2010 mean) and subtract it from the full time series to get annual-mean temperature anomalies on the model's native grid. Also load the corresponding SSP2-4.5 run for the same model.
3. For each year in the historical and SSP2-4.5 records, compute the global-mean temperature anomaly (area-weighted using cos(latitude)) and the corresponding spatial anomaly field. Divide each spatial field by its global mean to obtain dimensionless "fingerprint" patterns.
4. Average all fingerprint patterns across years to get a single mean fingerprint. Multiply this fingerprint by an arbitrary global-mean trajectory (for example, a linear ramp from 0 to 4 K) to produce a synthetic spatial projection. Plot the result on a map using matplotlib with a diverging colormap.
5. Evaluate your pattern-scaling emulator against the actual SSP2-4.5 spatial field: compute the NRMSE and pattern correlation using the functions from Figure 51.11. Observe where pattern scaling succeeds (land/ocean contrast, Arctic amplification) and where it fails (precipitation, regional aerosol effects).

Exercise 51.2.1

A climate emulator trained on CMIP6 produces a spatial temperature fingerprint \(F(x,y)\) normalized so that its area-weighted global mean equals 1.0. Under SSP2-4.5, an energy balance model predicts a global mean warming of 2.3 K by 2100. Using pattern scaling, what is the predicted local warming at a grid cell where \(F(x,y) = 1.8\)? Now suppose the same grid cell shows 3.1 K of warming in the actual GCM run. What is the pattern-scaling error at that grid cell, and what physical process might explain why pattern scaling underestimates the response there?

Hint

Pattern scaling predicts local warming as \(\Delta T_{\text{local}} = F(x,y) \times \Delta T_{\text{global}}\). For the second part, consider locations where local feedbacks (such as snow/ice albedo feedback or soil moisture depletion) amplify warming beyond what a static fingerprint captures.

Step-Through: Pattern Scaling with a 2x2 Grid

Trace through pattern scaling on a tiny 2x2 latitude-longitude grid. Suppose a GCM reference run under a +1 K global mean warming produces the following spatial temperature anomalies (in K):

Grid cell (60N, 0E): 1.6 K  |  Grid cell (60N, 90E): 1.2 K
Grid cell (30N, 0E): 0.8 K  |  Grid cell (30N, 90E): 0.4 K

Step 1 (Normalize): Compute the area-weighted global mean. Weights are \(\cos(60°) = 0.5\) for the top row and \(\cos(30°) = 0.866\) for the bottom row. Weighted mean = \((0.5 \times (1.6 + 1.2) + 0.866 \times (0.8 + 0.4)) / (2 \times 0.5 + 2 \times 0.866)\) = \((1.4 + 1.0392) / 2.732\) = \(0.893\) K.

Step 2 (Fingerprint): Divide each cell by 0.893 to get \(F\): (60N,0E) = 1.79, (60N,90E) = 1.34, (30N,0E) = 0.90, (30N,90E) = 0.45.

Step 3 (Project): For a new scenario with \(\Delta T_{\text{global}} = 3.0\) K, multiply: (60N,0E) = 5.37 K, (60N,90E) = 4.03 K, (30N,0E) = 2.69 K, (30N,90E) = 1.34 K. The Arctic amplification pattern (top row warming more) is preserved at any global mean, which is exactly the strength and the limitation of the method.

Real-World Application: ClimateTrace Global Emissions Monitoring

Climate TRACE (launched 2021) combines satellite imagery, remote sensing, and ML models to independently monitor greenhouse gas emissions from over 80,000 individual facilities worldwide, including power plants, steel mills, and oil fields. The system uses foundation-model-derived features from Sentinel-2 and Landsat imagery, paired with atmospheric observations from OCO-2 and the TROPOspheric Monitoring Instrument (TROPOMI), to estimate facility-level CO2 and methane emissions without relying on self-reported data. This satellite-based accounting has in several cases indicated that actual emissions from some countries and sectors may be 50 to 100 percent higher than officially reported figures. (As of 2025, Climate TRACE tracks over 350 million assets across all major sectors and countries, and its data feeds directly into the Global Stocktake process under the Paris Agreement.)

The Fastest Climate Model Runs on a Spreadsheet

Before neural emulators existed, the most widely used "fast climate model" was MAGICC (Model for the Assessment of Greenhouse gas Induced Climate Change), first written in 1987. MAGICC is essentially a box model: it represents the entire ocean as a handful of thermal reservoirs connected by diffusion, the atmosphere as a single layer, and the carbon cycle as a few coupled differential equations. Despite this radical simplification, MAGICC has been used in every IPCC assessment report since 1990 to translate emissions scenarios into global temperature projections. Its predictions have generally tracked the actual observed warming to within a few tenths of a degree, though its skill at regional scales and for non-temperature variables is more limited. The lesson: for global mean temperature, a well-calibrated simple model can outperform a poorly calibrated complex one, a theme that recurs whenever we compare emulators to their parent GCMs.

Lab: Train and Evaluate a Minimal Climate Emulator

Goal: Build a pattern-scaling emulator from publicly available CMIP6 data and measure its accuracy against a held-out scenario.

Tools needed: Python 3.10+, xarray, zarr, intake-esm, numpy, matplotlib (install via pip install xarray zarr intake-esm numpy matplotlib).

Procedure (25 minutes):

1. Use intake-esm to load the Pangeo CMIP6 catalog and retrieve surface air temperature (tas) from the NorESM2-LM model for the historical run and two SSP scenarios (SSP2-4.5 and SSP5-8.5). Compute annual means and a 1981 to 2010 baseline climatology.

2. Using the historical run, compute the area-weighted global mean anomaly for each year and divide each spatial field by it to produce yearly fingerprints. Average these into a single mean fingerprint.

3. Hold out SSP5-8.5 as your test set. Predict its spatial warming by multiplying your fingerprint by the SSP5-8.5 global mean trajectory. Compute the NRMSE and pattern correlation against the actual GCM spatial fields.

What to vary: Try building separate fingerprints for land and ocean cells, or for different latitude bands. Try using SSP2-4.5 instead of the historical run to derive the fingerprint.

What to observe: Where does pattern scaling work well (temperature over mid-latitude land)? Where does it break down (precipitation, polar regions, areas with strong aerosol forcing)? How does the NRMSE change across decades as the forcing grows larger?

What's Next

Climate emulation provides the statistical backbone for understanding long-term trends. Section 51.3 focuses on the tails of those distributions: extreme weather events. We examine how AI detects rare events in high-dimensional climate data and how attribution science quantifies the contribution of human activities to their changing probability.