Source code for fluxfootprints.ls_footprint_adapter
# src/fluxfootprints/ls_footprint_adapter.py
"""
Adapter for Lagrangian Stochastic footprint model.
"""
from typing import Optional
import numpy as np
import pandas as pd
import xarray as xr
from .base_footprint_model import BaseFootprintModel
from .ls_footprint_model import LSFootprintConfig, BackwardLSModel
[docs]
class LSFootprintModelAdapter(BaseFootprintModel):
"""
Lagrangian Stochastic footprint model adapter.
Wraps the BackwardLSModel in the standard interface.
"""
REQUIRED_COLUMNS = ["ustar", "ol", "wind_dir"]
def __init__(self, *args, n_particles: int = 20000, **kwargs):
super().__init__(*args, **kwargs)
self.n_particles = n_particles
def _validate_input_df(self, df: pd.DataFrame) -> pd.DataFrame:
"""Validate required columns."""
df = df.copy()
# Rename columns
rename_map = {
"USTAR": "ustar",
"MO_LENGTH": "ol",
"WD": "wind_dir",
}
df = df.rename(columns={k: v for k, v in rename_map.items() if k in df.columns})
# Check required columns
missing = [col for col in self.REQUIRED_COLUMNS if col not in df.columns]
if missing:
raise ValueError(f"Missing required columns: {missing}")
df = df.replace(-9999, np.nan)
df = df.dropna(subset=self.REQUIRED_COLUMNS)
return df
[docs]
def run(self, return_result: bool = True) -> Optional[xr.Dataset]:
"""Execute Lagrangian footprint calculation."""
self.logger.info("Starting Lagrangian Stochastic footprint calculation...")
self.df = self._validate_input_df(self.df)
# Calculate displacement and measurement height
d = 10 ** (0.979 * np.log10(self.crop_height) - 0.154)
zm = self.inst_height - d
z0 = self.crop_height * 0.123
# Setup domain
xmin, xmax, ymin, ymax = self.domain
domain_extent = (abs(xmin), abs(ymax))
# Initialize accumulator
footprint_sum = None
x_bins = None
y_bins = None
n_valid = 0
# Process each timestep (or use mean conditions)
for idx, row in self.df.iterrows():
try:
# Create configuration
cfg = LSFootprintConfig(
zm=zm,
ustar=row["ustar"],
L=row["ol"],
h=self.atm_bound_height,
wind_dir_deg=row["wind_dir"] if "wind_dir" in row else 270.0,
z0=z0,
n_particles=self.n_particles,
domain=domain_extent,
dx=self.dx,
dy=self.dy,
)
# Run model
model = BackwardLSModel(cfg)
model.run()
# Get footprint
if footprint_sum is None:
footprint_sum = model.footprint_2d
x_bins = model.x_bins
y_bins = model.y_bins
else:
footprint_sum += model.footprint_2d
n_valid += 1
except Exception as e:
self.logger.warning(f"Failed to process timestep {idx}: {e}")
continue
if n_valid == 0:
raise RuntimeError("No valid footprints calculated")
# Create coordinate arrays (bin centers)
self.x = 0.5 * (x_bins[:-1] + x_bins[1:])
self.y = 0.5 * (y_bins[:-1] + y_bins[1:])
# Create climatology
self.fclim_2d = xr.DataArray(
footprint_sum / n_valid,
dims=("x", "y"),
coords={"x": self.x, "y": self.y},
)
# Store results
if return_result:
self.results = xr.Dataset({
"footprint_climatology": self.fclim_2d,
"domain_x": ("x", self.x),
"domain_y": ("y", self.y),
})
self.results.attrs["model"] = "Lagrangian Stochastic"
self.results.attrs["n_particles"] = self.n_particles
self.results.attrs["n_timesteps"] = n_valid
return self.results
return None