Source code for fluxfootprints.wang_footprint_adapter
# src/fluxfootprints/wang_footprint_adapter.py
"""
Adapter for Wang et al. (2006) footprint model.
"""
from typing import Optional
import numpy as np
import pandas as pd
import xarray as xr
from scipy.ndimage import gaussian_filter
from .base_footprint_model import BaseFootprintModel
from .wang_footprint import wang2006_fy, reconstruct_gaussian_2d
[docs]
class WangFootprintModel(BaseFootprintModel):
"""
Wang et al. (2006) convective footprint model adapter.
Note: Valid only for daytime convective conditions.
"""
REQUIRED_COLUMNS = ["ol", "sigmav", "umean"]
def _validate_input_df(self, df: pd.DataFrame) -> pd.DataFrame:
"""Validate required columns."""
df = df.copy()
rename_map = {
"MO_LENGTH": "ol",
"V_SIGMA": "sigmav",
"WS": "umean",
}
df = df.rename(columns={k: v for k, v in rename_map.items() if k in df.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)
# Filter for convective conditions only
df = df[df["ol"] < 0]
if len(df) == 0:
raise ValueError("Wang model requires convective conditions (L < 0)")
return df
[docs]
def run(self, return_result: bool = True) -> Optional[xr.Dataset]:
"""Execute Wang footprint calculation."""
self.logger.info("Starting Wang et al. (2006) footprint calculation...")
self.df = self._validate_input_df(self.df)
# Calculate measurement height
d = 10 ** (0.979 * np.log10(self.crop_height) - 0.154)
zm = self.inst_height - d
# Setup x domain (positive upwind)
xmin, xmax, ymin, ymax = self.domain
self.x = np.arange(0, abs(xmin) + self.dx, self.dx)
# Initialize accumulator
footprint_sum_2d = None
n_valid = 0
for idx, row in self.df.iterrows():
try:
# Calculate 1D footprint
fy = wang2006_fy(
self.x,
zm=zm,
h=self.atm_bound_height,
L=row["ol"],
)
# Reconstruct 2D
X, Y, F = reconstruct_gaussian_2d(
self.x,
fy,
sigma_v=row["sigmav"],
U=row["umean"],
y_max=abs(ymax),
ny=int((abs(ymax) - abs(ymin)) / self.dy),
)
if footprint_sum_2d is None:
footprint_sum_2d = F
self.y = Y[0, :] # Extract y coordinates
else:
footprint_sum_2d += F
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")
# Flip x to match standard convention (negative upwind)
self.x = -self.x[::-1]
footprint_sum_2d = footprint_sum_2d[:, ::-1]
# Create climatology
self.fclim_2d = xr.DataArray(
footprint_sum_2d.T / n_valid,
dims=("x", "y"),
coords={"x": self.x, "y": self.y},
)
# Apply smoothing if requested
if self.smooth_data:
self.fclim_2d = xr.DataArray(
gaussian_filter(self.fclim_2d.values, sigma=1.0),
dims=("x", "y"),
coords={"x": self.x, "y": self.y},
)
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"] = "Wang et al. (2006)"
self.results.attrs["note"] = "Valid for convective conditions only"
self.results.attrs["n_timesteps"] = n_valid
return self.results
return None