fluxfootprints package
API reference for every module in the fluxfootprints package.
Model interface
fluxfootprints.base_footprint_model module
base_footprint_model.py
Base class defining standard interface for all footprint models.
- class fluxfootprints.base_footprint_model.BaseFootprintModel(df: DataFrame, domain: list | None = None, dx: float = 10.0, dy: float = 10.0, rs: list | None = None, smooth_data: bool = True, verbosity: int = 2, logger: Logger | None = None, **kwargs)[source]
Bases:
ABCAbstract base class for flux footprint models.
All footprint model implementations should inherit from this class and implement the required methods to ensure API consistency.
- Attributes:
- dfpandas.DataFrame
Input meteorological data
- domainlist
Spatial domain [xmin, xmax, ymin, ymax]
- dx, dyfloat
Grid resolution in x and y directions
- rslist
Source area fractions to compute
- loggerlogging.Logger
Logger instance
- x, ynumpy.ndarray
Grid coordinate arrays
- f_2dxarray.DataArray or None
Time-resolved 2D footprint (time, x, y)
- fclim_2dxarray.DataArray or None
Climatological 2D footprint (x, y)
Methods
Return x and y coordinate arrays.
Return the climatological footprint as xarray DataArray.
Return time-resolved footprint if available.
Return complete results as xarray Dataset.
run([return_result])Execute footprint calculation.
to_netcdf(filepath)Save results to netCDF file.
- REQUIRED_COLUMNS = []
- get_coordinates() tuple[ndarray, ndarray][source]
Return x and y coordinate arrays.
- Returns:
- x, ynumpy.ndarray
1D coordinate arrays in meters
- get_footprint_climatology() DataArray[source]
Return the climatological footprint as xarray DataArray.
- Returns:
- xarray.DataArray
2D footprint climatology with dims (x, y)
- get_footprint_timeseries() DataArray | None[source]
Return time-resolved footprint if available.
- Returns:
- xarray.DataArray or None
3D footprint with dims (time, x, y) if available
- get_results() Dataset[source]
Return complete results as xarray Dataset.
- Returns:
- xarray.Dataset
Dataset containing all footprint outputs and metadata
- abstractmethod run(return_result: bool = True) Dataset | None[source]
Execute footprint calculation.
- Parameters:
- return_resultbool
If True, return xarray Dataset with results
- Returns:
- xarray.Dataset or None
Dataset containing footprint_climatology (x, y), optionally footprint_2d (time, x, y), and metadata
Footprint models
fluxfootprints.improved_ffp module
Kljun et al. (2015) FFP parameterisation.
Improved Flux Footprint Prediction (FFP) Module
This module implements the FFP model described in: Kljun, N., P. Calanca, M.W. Rotach, H.P. Schmid, 2015: A simple two-dimensional parameterisation for Flux Footprint Prediction (FFP). Geosci. Model Dev. 8, 3695-3713, doi:10.5194/gmd-8-3695-2015
This version includes improvements for better alignment with the theoretical framework.
- class fluxfootprints.improved_ffp.FFPModel(df: DataFrame, domain: list | None = None, dx: float = 10.0, dy: float = 10.0, nx: int = 1000, ny: int = 1000, rs: list | None = None, rslayer: bool = False, smooth_data: bool = True, crop: bool = False, verbosity: int = 2, logger=None, **kwargs)[source]
Bases:
BaseFootprintModelFlux Footprint Prediction (FFP) model based on Kljun et al. (2015).
This class implements an improved version of the flux footprint model for determining the source area of scalar fluxes measured by eddy covariance towers. It uses atmospheric stability, wind parameters, and terrain configurations to calculate spatial footprints.
- Attributes:
- dfpandas.DataFrame
Copy of the input DataFrame used for modeling.
- domainlist of float
Spatial domain defined as [xmin, xmax, ymin, ymax].
- fclim_2dxarray.DataArray or None
Final footprint climatology after all processing.
- f_2dxarray.DataArray or None
Instantaneous 2D footprint estimate.
- loggerlogging.Logger
Logger instance for model-level debugging.
Methods
Apply 3x3 smoothing from Kljun et al. paper with mass conservation.
Apply roughness sublayer (RSL) corrections to crosswind dispersion.
calc_2d_footprint(f_ci_dummy, sigy_dummy)Calculate the two-dimensional footprint distribution f(x,y).
calc_crosswind_extent(r, x_ru, x_rd)Calculate crosswind extent of source area using parameterized relationships.
Calculate crosswind-integrated footprint extent for relative contribution r.
Calculate the scaled crosswind-integrated footprint F̂y*(X̂*).
Calculate the standard deviation of cross-wind spread \(\sigma_y\).
Calculate upwind and downwind distances from the peak location for fraction r.
Calculate the dimensionless wind-speed group Π4 = u(zm)/(u* k) = ln(zm/z0) - ψM, following Kljun et al. (2015).
calc_real_footprint_peak(zm, h[, u_zm, ...])Calculate the real-scale footprint peak location.
Calculate scaled distance X* with roughness sublayer correction.
Calculate the scaled footprint peak location (X*max).
Calculate the scaled distance X* based on wind and height parameters.
calc_unstable_psi(stability_param)Calculate ψM for unstable atmospheric conditions.
Compute the flux footprint using xarray operations.
Calculate dimensionless Π groups for analysis.
Compute source area contour datasets for all specified relative contributions, derived directly from the fclim_2d climatology grid.
Check if the measurement height is above the roughness sublayer.
Check validity ranges according to equation 27 and other constraints from Kljun et al. (2015).
Create xarray Dataset from input DataFrame.
Define the spatial computational domain and polar coordinate grids.
get_coordinates()Return x and y coordinate arrays.
get_footprint_climatology()Return the climatological footprint as xarray DataArray.
get_footprint_timeseries()Return time-resolved footprint if available.
get_results()Return complete results as xarray Dataset.
get_scalar_value(arr)Convert an array-like object to a scalar float value.
Compute the source area contour threshold for fraction r of the climatological footprint, operating directly on the pre-computed fclim_2d grid.
Classify stability regimes for diagnostics.
Initialize core model parameters (a, b, c, d, ac, bc, cc).
Ensure footprint integrates to 1.0 after calculation.
plot_footprint([config, ax, show_contours, ...])Plot the footprint climatology with optional contours.
Sanitize DataFrame fields, compute RSL parameters, and drop invalid timesteps.
run([return_result])Execute the FFP model calculations.
save_results(filename)Save model results to a netCDF file.
scale_to_real_distance(x_star)Convert scaled distance to real distance using Eq.
to_netcdf(filepath)Save results to netCDF file.
verify_data(data, name)Verify the content and structure of a DataArray.
verify_results(data[, name])Check for NaN or infinite values in the given data.
- apply_paper_smoothing()[source]
Apply 3x3 smoothing from Kljun et al. paper with mass conservation.
- apply_rsl_corrections()[source]
Apply roughness sublayer (RSL) corrections to crosswind dispersion.
- calc_2d_footprint(f_ci_dummy, sigy_dummy)[source]
Calculate the two-dimensional footprint distribution f(x,y).
Implementation of Equation 10 from Kljun et al. (2015): f(x,y) = fy(x) * (1/(sqrt(2π)σy)) * exp(-y^2/(2σy^2))
Where: - fy(x) is the crosswind-integrated footprint - σy is the standard deviation of crosswind spread - y is the crosswind distance from the centerline
- Parameters:
f_ci_dummy – Crosswind-integrated footprint
sigy_dummy – Scaled crosswind dispersion
- Returns:
Two-dimensional footprint [m^-2]
- Return type:
xr.DataArray
- calc_crosswind_extent(r: float, x_ru: DataArray, x_rd: DataArray) DataArray[source]
Calculate crosswind extent of source area using parameterized relationships.
- Parameters:
r – Relative contribution
x_ru – Upwind distance from peak
x_rd – Downwind distance from peak
- Returns:
Crosswind extent at each distance
- Return type:
xr.DataArray
- calc_crosswind_integrated_extent(r: float) DataArray[source]
Calculate crosswind-integrated footprint extent for relative contribution r. Following Equations 23-25 of the paper.
- Parameters:
r – Relative contribution (between 0.1 and 0.9)
- Returns:
Distance from receptor containing fraction r of footprint
- Return type:
xr.DataArray
- calc_crosswind_integrated_footprint(x_star)[source]
Calculate the scaled crosswind-integrated footprint F̂y*(X̂*).
- Parameters:
- x_starxarray.DataArray
Scaled distance from the receptor.
- Returns:
- xarray.DataArray
The scaled crosswind-integrated footprint.
- calc_crosswind_spread(x: float | ndarray | DataArray) float | ndarray | DataArray[source]
Calculate the standard deviation of cross-wind spread \(\sigma_y\).
Implements Eqs. 18-19 from Kljun et al., 2015:
\[ \begin{align}\begin{aligned}\sigma_y^* = a_c \sqrt{\frac{b_c \lvert X^* \rvert^2} {1 + c_c \lvert X^* \rvert}}\\\sigma_y = \frac{\sigma_y^*}{\text{scale\_const}} \; z_m \; \sigma_v / u_*\end{aligned}\end{align} \]where \(a_c = 2.17\), \(b_c = 1.66\), \(c_c = 20.0\), and
scale_constdepends on stability (see paper).The method dispatches on the type of x:
xarray DataArray – x is treated as the pre-scaled dimensionless distance \(X^*\). Per-timestep stability parameters from
self.dsare used, and anxr.DataArrayis returned. Use this path for climatology calculations where time-varying parameters matter.numpy array / float – x is treated as the raw upwind distance [m].
calc_scaled_xconverts it to \(X^*\) using time-mean parameters, and a numpy array / float is returned. Use this path for single-footprint visualisation where a representative spread is needed.
- Parameters:
- xfloat, ndarray, or xr.DataArray
For numpy/float: raw upwind distance [m]. For DataArray: pre-scaled dimensionless distance \(X^*\).
- Returns:
- float, ndarray, or xr.DataArray
Cross-wind spread \(\sigma_y\) [m].
- calc_peak_based_limits(r: float) tuple[DataArray, DataArray][source]
Calculate upwind and downwind distances from the peak location for fraction r. Following Eq. 26 of the paper.
- Parameters:
r – Relative contribution (between 0.1 and 0.9)
- Returns:
Tuple containing upwind and downwind distances from peak
- calc_pi_4()[source]
Calculate the dimensionless wind-speed group Π4 = u(zm)/(u* k) = ln(zm/z0) - ψM, following Kljun et al. (2015).
Π4 is the scaled<->real conversion factor for the along-wind distance (Eq. 6/7) and must be positive. Callers use the return value of this method directly as Π4 (do not subtract it from
ln(zm/z0)again).- Returns:
- xr.DataArray
Π4 = ln(zm/z0) - ψM. In neutral conditions ψM = 0 so Π4 = ln(zm/z0).
- calc_real_footprint_peak(zm: float | DataArray, h: float | DataArray, u_zm: float | DataArray = None, ustar: float | DataArray = None, z0: float | DataArray = None, k: float = 0.4) float | DataArray[source]
Calculate the real-scale footprint peak location. Based on Equations 20-22 of Kljun et al. (2015). Works with both scalar values and xarray DataArrays.
- Parameters:
zm – Measurement height [m]
h – Boundary layer height [m]
u_zm – Mean wind speed at measurement height [m/s], optional
ustar – Friction velocity [m/s], optional
z0 – Roughness length [m], optional
k – von Karman constant (default=0.4)
- Returns:
Real distance to footprint peak [m] Returns DataArray if inputs are DataArrays, float if inputs are scalars
- calc_scaled_distance_rsl()[source]
Calculate scaled distance X* with roughness sublayer correction.
- Returns:
- xarray.DataArray
Corrected scaled distance.
- calc_scaled_footprint_peak() float[source]
Calculate the scaled footprint peak location (X*max). Based on Equation 20 of Kljun et al. (2015).
- Returns:
Scaled distance to footprint peak (constant)
- Return type:
float
- calc_scaled_x(x: float | ndarray) float | ndarray[source]
Calculate the scaled distance X* based on wind and height parameters.
Implements Eq. 7 of Kljun et al. (2015):
\[X^* = \frac{x}{z_m}\left(1 - \frac{z_m}{h}\right) \left[\ln\frac{z_m}{z_0} - \psi_M\right]^{-1}\]i.e.
X* = x/zm * (1 - zm/h) / Pi_4withPi_4 = ln(zm/z0) - psi_M = u(zm)/(u* k)(Eqs. 6/7). This is the same scaled<->real conversion used incalc_xr_footprint(), so the auxiliary source-area contour helpers stay consistent with the main climatology path.- Parameters:
- xfloat or ndarray
Upwind distance from the receptor.
- Returns:
- float or ndarray
Scaled distance X*.
- calc_unstable_psi(stability_param)[source]
Calculate ψM for unstable atmospheric conditions.
- Parameters:
- stability_paramxarray.DataArray
Stability parameter zm/L.
- Returns:
- xarray.DataArray
Stability correction ψM.
- calc_xr_footprint()[source]
Compute the flux footprint using xarray operations.
- Returns:
- xarray.DataArray
Climatological flux footprint.
- calculate_pi_groups()[source]
Calculate dimensionless Π groups for analysis.
- Returns:
- tuple of xarray.DataArray
Π₁, Π₂, Π₃, Π₄ values.
- calculate_source_areas()[source]
Compute source area contour datasets for all specified relative contributions, derived directly from the fclim_2d climatology grid.
Must be called after
run()orcalc_xr_footprint().- Returns:
- dict
Mapping of contribution label (e.g.
'r_80') to the xarray.Dataset returned byget_source_area_contour().
- Raises:
- RuntimeError
If fclim_2d has not been computed yet.
- check_rsl_validity()[source]
Check if the measurement height is above the roughness sublayer.
- Returns:
- pandas.Series
Boolean mask of valid rows.
- check_validity_ranges()[source]
Check validity ranges according to equation 27 and other constraints from Kljun et al. (2015).
Implements the following validity checks: 1. Height validity (Eq. 27): 20z₀ < zm < he
where he ≈ 0.8h is the entrainment height
Stability validity (Eq. 27): -15.5 ≤ zm/L
Turbulence validity: u* > 0.1 m/s, σv > 0
Roughness sublayer validity: zm > z* ≈ n*hrs where hrs ≈ 10z₀ and 2 ≤ n ≤ 5 (Section 2)
- Returns:
- xr.Dataset: Dataset containing per-timestep boolean masks for:
- height_valid: True where 20z₀ < zm < he
- stability_valid: True where -15.5 ≤ zm/L
- turbulence_valid: True where u* > 0.1 and σv > 0
- rsl_valid: True where measurement height above RSL
- Combined per-timestep validity is stored in self.valid_footprint (dims: time).
- get_scalar_value(arr)[source]
Convert an array-like object to a scalar float value.
- Parameters:
- arrarray-like or scalar
The value to be converted.
- Returns:
- float
A scalar representation of the input.
- get_source_area_contour(r)[source]
Compute the source area contour threshold for fraction r of the climatological footprint, operating directly on the pre-computed fclim_2d grid.
- Parameters:
- rfloat
Relative contribution (0 < r < 1), e.g. 0.8 for the 80 % footprint.
- Returns:
- xarray.Dataset
Dataset containing: -
contour_level: the fclim_2d value that encloses fraction rof the total flux (use this as the
levelsargument toax.contour).x,y: the domain coordinate arrays.f: the full fclim_2d climatology grid.
- Raises:
- RuntimeError
If fclim_2d has not been computed yet (call
run()first).- ValueError
If r lies outside (0, 1), raised by the shared threshold kernel.
See also
fluxfootprints.representativeness.truncate_to_contourMask and renormalise a climatology at this contour, after Chu et al. (2021).
- handle_stability_regimes()[source]
Classify stability regimes for diagnostics.
- Returns:
- xarray.Dataset
Dataset with regime-specific masks.
Notes
Kljun et al. (2015) deliberately use a single, stability-independent parameter set (a, b, c, d) for the scaled crosswind-integrated footprint (Appendix A: a=1.4524, b=-1.9914, c=1.4622, d=0.1359). The stability dependence enters only through the scaled distance X* and the crosswind-spread scaling. Therefore this method no longer overrides the universal coefficients; it only classifies regimes and records a diagnostic flag.
- initialize_model_parameters()[source]
Initialize core model parameters (a, b, c, d, ac, bc, cc). Ensures scalar values are assigned.
- plot_footprint(config=None, ax=None, show_contours=True, levels=10)[source]
Plot the footprint climatology with optional contours.
- Parameters:
config – Configuration object containing metadata (optional)
ax – Matplotlib axis to plot on (optional)
show_contours – Whether to show contour lines
levels – Number of contour levels or list of levels
- Returns:
matplotlib.figure.Figure, matplotlib.axes.Axes
- prep_df_fields()[source]
Sanitize DataFrame fields, compute RSL parameters, and drop invalid timesteps.
Assumes df already contains standardized column names from build_climatology.
- run(return_result: bool = True) Dataset | None[source]
Execute the FFP model calculations.
- Parameters:
return_result – If True, returns complete results dataset
- Returns:
Dataset containing footprint results
- Return type:
xr.Dataset | None
- save_results(filename: str)[source]
Save model results to a netCDF file.
- Parameters:
filename – Path where the results should be saved
- scale_to_real_distance(x_star: DataArray) DataArray[source]
Convert scaled distance to real distance using Eq. 6/7.
- Parameters:
x_star – Scaled distance X*
- Returns:
Real-scale distance
- Return type:
xr.DataArray
fluxfootprints.ffp_xr module
xarray-based rewrite of the Kljun et al. (2015) parameterisation.
- class fluxfootprints.ffp_xr.ffp_climatology_new(df: DataFrame, domain: list | ndarray | tuple | None = None, dx: int = 10.0, dy: int = 10.0, nx: int = 1000, ny: int = 1000, rs: list | ndarray | None = None, rslayer: bool = False, smooth_data: bool = True, crop: bool = False, verbosity: int = 2, fig: bool = False, logger=None, time=None, **kwargs)[source]
Bases:
BaseFootprintModelCreate a footprint‑climatology from a series of individual flux‑footprint estimates using the simple 2‑D parameterisation of Kljun et al. (2015).
The routine
computes a footprint for every time step,
rotates each footprint into its observed wind direction,
aggregates all footprints onto a common grid, and
derives user‑requested source‑area contours (
rs).
The implementation is based on:
Kljun, N., Calanca, P., Rotach, M. W., & Schmid, H. P. (2015). A simple two‑dimensional parameterisation for flux‑footprint predictions (FFP). Geoscientific Model Development, 8, 3695‑3713. https://doi.org/10.5194/gmd‑8‑3695‑2015
- Parameters:
- dfpandas.DataFrame
DataFrame containing time series of micrometeorological inputs. Must contain the following required columns:
- zmfloat or Series
Measurement height above displacement height (i.e. z – d) [m].
- z0float or Series or None
Surface roughness length [m]. Either z0 or umean must be supplied per timestep; if z0 is missing it is derived from the log-wind profile using umean (see
calc_xr_footprint()).- umeanfloat or Series or None
Mean wind speed at zm [m s⁻¹]. Either umean or z0 must be supplied per timestep.
- hfloat or Series
Boundary‑layer height [m].
- olfloat or Series
Obukhov length [m].
- sigmavfloat or Series
Standard deviation of lateral velocity fluctuations [m s⁻¹].
- ustarfloat or Series
Friction velocity [m s⁻¹].
- wind_dirfloat or Series
Wind direction in degrees (0–360°, meteorological convention).
- domainarray_like of float, default=[-1000.0, 1000.0, -1000.0, 1000.0]
Domain limits
[xmin, xmax, ymin, ymax][m].- dx, dyfloat, default=10.0
Grid‑cell size in x and y directions [m].
- nx, nyint, default=1000
Number of grid cells in x and y. Ignored if dx/dy are supplied.
- rslist or ndarray, default=[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]
Source‑area contour fractions (e.g., 0.8 for 80%).
- rslayerbool, default=False
If True, allow calculations when zm lies in the roughness sub‑layer (RS). Warning: Model is formally invalid within the RS.
- smooth_databool, default=True
Apply a Gaussian convolution filter to smooth the footprint climatology.
- cropbool, default=False
Crop the output grid to the extent of the largest requested contour.
- verbosity{0, 1, 2}, default=2
Verbosity level: 0 = silent, 1 = fatal only, 2 = warning/info.
- figbool, default=False
Plot an example footprint on screen.
- Returns
- ——-
- resultsxarray.Dataset
Dataset containing: - footprint_climatology : DataArray (x, y)
Normalised 2D footprint climatology density values [m⁻²].
- domain_x1D coordinate array
x-axis grid coordinates [m].
- domain_y1D coordinate array
y-axis grid coordinates [m].
Methods
get_coordinates()Return x and y coordinate arrays.
get_footprint_climatology()Return the climatological footprint as xarray DataArray.
get_footprint_timeseries()Return time-resolved footprint if available.
get_results()Return complete results as xarray Dataset.
Sanitize DataFrame input range bounds and drop invalid timesteps.
run([return_result])Execute footprint calculation.
to_netcdf(filepath)Save results to netCDF file.
calc_xr_footprint
create_xr_dataset
define_domain
raise_ffp_exception
Notes
Implemented by N. Kljun & G. Fratini, originally in MATLAB, ported to Python 3.x (v 1.4, 11 Dec 2019).
Examples
>>> import pandas as pd >>> from fluxfootprints import ffp_climatology_new >>> # Prepare DataFrame with required columns >>> df = pd.DataFrame({ ... "zm": [2.5], "z0": [0.1], "umean": [3.2], "h": [800.0], ... "ol": [-50.0], "sigmav": [0.4], "ustar": [0.25], "wind_dir": [180.0] ... }) >>> model = ffp_climatology_new(df=df, dx=5.0, dy=5.0) >>> results = model.run()
- prep_df_fields()[source]
Sanitize DataFrame input range bounds and drop invalid timesteps.
Assumes df already contains standardized column names from build_climatology. z0 and umean are not both required: at least one must be available per timestep, since a missing z0 can be derived from the log-wind profile using umean (see
calc_xr_footprint()). Kljun et al. (2015) Eq. 27 validity bounds (stability, height, roughness sublayer) are evaluated per-timestep incalc_xr_footprint(), not here.
fluxfootprints.kormannmeixner module
Kormann & Meixner (2001) analytical model.
kormann_meixner_footprint.py
Python implementation of the analytical flux-footprint model of Kormann & Meixner (2001).
This script provides utilities to estimate the scalar-flux footprint of an eddy-covariance measurement using the closed-form solutions derived in:
Kormann, R., & Meixner, F. X. (2001). An analytical footprint model for non-neutral stratification. Boundary-Layer Meteorology, 99, 207-224. https://doi.org/10.1023/A:1018991015119
Only standard scientific-Python packages are required (numpy and scipy).
The implementation follows the analytical approach described in Section 4 of the
paper to relate Monin-Obukhov similarity profiles to the power-law formulation used
in the footprint derivation. If you require the more accurate (but slower)
numerical approach, see the companion functions in
analytical_power_law_parameters() and :func:`numerical_power_law_parameters`—the
remainder of the code is agnostic to which parameter-estimation routine is used.
- fluxfootprints.kormannmeixner.analytical_power_law_parameters(z_m: float | ndarray, z_0: float | ndarray, L: float | ndarray, u_star: float | ndarray, u_zm: float | ndarray) Tuple[float | ndarray, float | ndarray, float | ndarray, float | ndarray][source]
Return m, n, U, κ using the analytical matching approach.
Accepts scalars or same-shaped arrays for every argument, so a whole column of per-timestep inputs can be resolved in one vectorized call instead of looping row by row.
- Parameters:
- z_m
Eddy-covariance measurement height (m).
- z_0
Aerodynamic roughness length (m).
- L
Obukhov length (m) (negative ⇒ unstable).
- u_star
Friction velocity (m sâ»Â¹).
- u_zm
Mean wind speed at z_m (m sâ»Â¹).
- Returns:
- m, n, U, kappa
Power-law exponents and proportionality constants for
u(z) = U z**mandK(z) = kappa z**n.
- fluxfootprints.kormannmeixner.crosswind_integrated_footprint(x: ndarray | float, xi: float, m: float, n: float) ndarray | float[source]
Compute the cross-wind-integrated footprint f(x, z).
This function implements Eq. (21) from Kormann & Meixner (2001), which describes the probability density function of source area contributions in the along-wind direction, integrated over the cross-wind direction.
- Parameters:
- xfloat or np.ndarray
Downwind distance(s) from the tower (m).
- xifloat
Footprint length-scale ξ(z) computed using length_scale_xi (m).
- mfloat
Power law exponent for wind speed profile.
- nfloat
Power law exponent for eddy diffusivity profile.
- Returns:
- float or np.ndarray
Cross-wind-integrated footprint value(s) at distance x.
- fluxfootprints.kormannmeixner.cumulative_fetch(x_p: float, xi: float, m: float, n: float) float[source]
Calculate the cumulative fetch P(x_p), the fraction of flux originating upwind of x_p.
Implements Eq. (29) from Kormann & Meixner (2001), returning the cumulative contribution to the flux footprint up to a specified downwind distance.
- Parameters:
- x_pfloat
Downwind distance from the tower (m) at which the cumulative flux contribution is evaluated.
- xifloat
Characteristic length-scale ξ(z) computed using length_scale_xi (m).
- mfloat
Power law exponent for wind speed profile.
- nfloat
Power law exponent for eddy diffusivity profile.
- Returns:
- float
Fraction of total flux (between 0 and 1) originating upwind of x_p.
- fluxfootprints.kormannmeixner.effective_fetch(fraction: float, xi: float, m: float, n: float) float[source]
Invert the cumulative fetch function to determine the fetch distance x_p for a given flux fraction.
Solves for x_p such that cumulative_fetch(x_p) = fraction, which identifies the distance upwind from which a specified fraction of the total flux originates.
- Parameters:
- fractionfloat
Desired cumulative flux contribution (must be in the open interval (0, 1)).
- xifloat
Characteristic length-scale ξ(z) computed using length_scale_xi (m).
- mfloat
Power law exponent for wind speed profile.
- nfloat
Power law exponent for eddy diffusivity profile.
- Returns:
- float
Effective fetch distance x_p (m) upwind of the sensor that contributes the given flux fraction.
- Raises:
- ValueError
If fraction is not in the open interval (0, 1).
- fluxfootprints.kormannmeixner.footprint_2d(x: ndarray, y: ndarray, xi: float, m: float, n: float, u_zm: float, sigma_v: float) Tuple[ndarray, ndarray, ndarray][source]
Return 2-D footprint density φ(x, y, z_m).
- Parameters:
- x, y
1-D arrays of upstream and cross-stream distances (m). Positive x is up-wind.
- xi, m, n
Parameters returned by
length_scale_xi()andanalytical_power_law_parameters().- u_zm
Mean wind speed at measurement height (m sâ»Â¹).
- sigma_v
Standard deviation of cross-wind velocity fluctuations (m sâ»Â¹).
- Returns:
- X, Y, phi
Meshgrids of x, y and the footprint density φ (mâ»Â²).
- fluxfootprints.kormannmeixner.footprint_at_points(x: ndarray, y: ndarray, xi: float, m: float, n: float, u_zm: float, sigma_v: float) ndarray[source]
Evaluate the closed-form 2-D footprint density directly at given points.
Implements the same Eq. (19)/(21) formulation as
crosswind_integrated_footprint()/footprint_2d(), but takesx/yas same-shaped arrays of arbitrary points (e.g. an already-rotated output grid) rather than building the density on the outer product of separate 1-D wind-aligned axes. This lets a footprint grid in a rotated/translated frame be filled by direct evaluation instead of computing it on a wind-aligned grid and then interpolating (e.g. viascipy.interpolate.griddata()) onto the target frame.- Parameters:
- x, y
Arrays of downwind distance and crosswind offset (m) in the wind-aligned frame, same shape. Points with
x <= 0are upwind of the sensor, where the model is undefined, and are assigned zero density.- xi, m, n
Parameters returned by
length_scale_xi()andanalytical_power_law_parameters().- u_zm
Mean wind speed at measurement height (m sâ»Â¹).
- sigma_v
Standard deviation of cross-wind velocity fluctuations (m sâ»Â¹).
- Returns:
- np.ndarray
Footprint density φ (mâ»Â²), same shape as
x/y.
- fluxfootprints.kormannmeixner.length_scale_xi(z: float, U: float, kappa: float, m: float, n: float) float[source]
Calculate the characteristic footprint length-scale ξ(z).
This function implements Eq. (19) from Kormann & Meixner (2001) to compute the length scale based on measurement height and atmospheric parameters.
- Parameters:
- zfloat
Measurement height above displacement height (m).
- Ufloat
Mean horizontal wind speed at height z (m/s).
- kappafloat
von Kármán constant (typically ~0.4).
- mfloat
Power law exponent for wind speed profile.
- nfloat
Power law exponent for eddy diffusivity profile.
- Returns:
- float
Characteristic length-scale ξ(z) (m).
fluxfootprints.kormannmeixner_adapter module
Adapter for the Kormann & Meixner (2001) analytical footprint model.
- class fluxfootprints.kormannmeixner_adapter.KormannMeixnerModel(df: DataFrame, domain: list | None = None, dx: float = 10.0, dy: float = 10.0, rs: list | None = None, smooth_data: bool = True, verbosity: int = 2, logger: Logger | None = None, **kwargs)[source]
Bases:
BaseFootprintModelKormann & Meixner (2001) analytical footprint model adapter.
Wraps the analytical functions in
fluxfootprints.kormannmeixnerin the standardBaseFootprintModelinterface. Intended to be driven throughfluxfootprints.ffp_daily_monthly_helper.build_climatology(), which resolves per-timestep inputs into a DataFrame with the standardized column names below; the model reads those columns directly.- Parameters:
- dfpandas.DataFrame
Input data. Must contain the following required columns:
- zmfloat or Series
Measurement height above displacement height (i.e. z - d) [m].
- z0float or Series
Aerodynamic roughness length [m].
- ustarfloat or Series
Friction velocity [m s⁻¹].
- olfloat or Series
Obukhov length [m].
- sigmavfloat or Series
Standard deviation of lateral velocity fluctuations [m s⁻¹].
- umeanfloat or Series
Mean wind speed at zm [m s⁻¹].
- wind_dirfloat or Series, optional
Mean wind direction the wind blows from [deg]. When present, the wind-aligned footprint is rotated into the (x, y) output domain for that timestep; when absent, it is placed directly using the standard negative-upwind x convention (no rotation).
Methods
get_coordinates()Return x and y coordinate arrays.
get_footprint_climatology()Return the climatological footprint as xarray DataArray.
get_footprint_timeseries()Return time-resolved footprint if available.
get_results()Return complete results as xarray Dataset.
run([return_result])Execute Kormann-Meixner footprint calculation.
to_netcdf(filepath)Save results to netCDF file.
- REQUIRED_COLUMNS = ['zm', 'z0', 'ustar', 'ol', 'sigmav', 'umean']
fluxfootprints.wang_footprint module
Wang et al. (2006) convective boundary-layer analytical model. Valid for daytime convective conditions only.
wang2006_footprint.py
Semi-empirical flux-footprint parameterisation (cross-wind integrated) after Wang, W., Davis, K.J., Ricciuto, D.M. & Butler, M.P. (2006): An Approximate Footprint Model for Flux Measurements in the Convective Boundary Layer. J. Atmos. Oceanic Technol., 23(10), 1384-1394, doi:10.1175/JTECH1911.1, plus an optional Gaussian 2-D reconstruction utility.
Note: this is not the later Wang & Davis (2008) analytical CBL footprint model (Boundary-Layer Meteorology, 126, 213-233), which this package does not implement.
- This module provides:
wang2006_fy(…) – 1‑D cross‑wind‑integrated footprint f(x)
reconstruct_gaussian_2d(…) – convert f(x) to f(x,y) by assuming a laterally Gaussian spread with σ_y(x).
The code is intended for daytime convective boundary‑layer (CBL) conditions within the validity range stated by Wang et al. ( −L/h ≈ 0.01–0.1, 0.1 h ≤ zₘ ≤ 0.6 h ). Use with caution outside that range.
- fluxfootprints.wang_footprint.reconstruct_gaussian_2d(x: ndarray, fy: ndarray, sigma_v: float | None = None, U: float | None = None, y_max: float | None = None, ny: int = 201, alpha: float = 0.3) Tuple[ndarray, ndarray, ndarray][source]
Reconstruct a 2‑D footprint
f(x,y)from a 1‑Df_y(x).Assumes the lateral distribution at each stream‑wise distance
xis Gaussian with standard deviation σ_y(x).The default dispersion law is σ_y = α·x (α≈0.3). If turbulence statistics are available, pass
sigma_v(lateral velocity std, m s⁻¹) andU(mean wind speed, m s⁻¹) to use σ_y = (σ_v/U)·x.- Parameters:
- xndarray
Stream‑wise distances (m), same grid used for fy.
- fyndarray
Cross‑wind‑integrated footprint (m⁻¹), e.g. output of wang2006_fy.
- sigma_vfloat, optional
Lateral velocity standard deviation (m s⁻¹). If given with U overrides the default α‑law.
- Ufloat, optional
Mean wind speed at sensor height (m s⁻¹). Required with sigma_v.
- y_maxfloat, optional
Half‑width of y domain (m). Default = 3·max(σ_y).
- nyint, default 201
Number of grid points in y direction (symmetric about 0).
- alphafloat, default 0.3
Proportionality constant for σ_y = α·x when sigma_v/U unavailable.
- Returns:
- (X, Y, F)Tuple[np.ndarray, np.ndarray, np.ndarray]
X, Y : 2‑D coordinate grids (shape len(x) × ny).
- F : 2‑D footprint array f(x_i, y_j) (m⁻²) normalised so that
∬ F dx dy = 1.
- fluxfootprints.wang_footprint.wang2006_fy(x: ndarray, zm: float, h: float, L: float) ndarray[source]
Cross‑wind integrated footprint
f_y(x)(m⁻¹).Implements Wang et al. (2006) adjusted CWI footprint.
- Parameters:
- xndarray
1‑D array of stream‑wise distances (m, positive upwind).
- zmfloat
Measurement height (m).
- hfloat
Boundary‑layer height (m).
- Lfloat
Obukhov length (m). Must be negative for convective cases.
- Returns:
- ndarray
Footprint values f_y(x) (same shape as x) with ∫ f_y dx = 1.
fluxfootprints.wang_footprint_adapter module
Adapter for Wang et al. (2006) footprint model.
- class fluxfootprints.wang_footprint_adapter.WangFootprintModel(df: DataFrame, domain: list | None = None, dx: float = 10.0, dy: float = 10.0, rs: list | None = None, smooth_data: bool = True, verbosity: int = 2, logger: Logger | None = None, **kwargs)[source]
Bases:
BaseFootprintModelWang et al. (2006) convective footprint model adapter.
Wraps
wang2006_fy()andreconstruct_gaussian_2d()in the standardBaseFootprintModelinterface. Intended to be driven throughfluxfootprints.ffp_daily_monthly_helper.build_climatology(), which resolves per-timestep inputs into a DataFrame with the standardized column names below; the model reads those columns directly.Note: Valid only for daytime convective conditions (Obukhov length < 0).
- Parameters:
- dfpandas.DataFrame
Input data. Must contain the following required columns:
- zmfloat or Series
Measurement height above displacement height (i.e. z - d) [m].
- hfloat or Series
Boundary-layer height [m].
- olfloat or Series
Obukhov length [m]. Must be negative (convective) per timestep.
- sigmavfloat or Series
Standard deviation of lateral velocity fluctuations [m s⁻¹].
- umeanfloat or Series
Mean wind speed at zm [m s⁻¹].
Methods
get_coordinates()Return x and y coordinate arrays.
get_footprint_climatology()Return the climatological footprint as xarray DataArray.
get_footprint_timeseries()Return time-resolved footprint if available.
get_results()Return complete results as xarray Dataset.
run([return_result])Execute Wang footprint calculation.
to_netcdf(filepath)Save results to netCDF file.
- REQUIRED_COLUMNS = ['zm', 'h', 'ol', 'sigmav', 'umean']
fluxfootprints.ls_footprint_model module
Backward Lagrangian stochastic particle model.
ls_footprint_model.py
A minimal, research‑grade 3‑D backward Lagrangian stochastic (LS) footprint model for eddy‑covariance flux measurements.
The implementation follows the principles of Kljun et al. (2002) but is written from scratch in pure Python/Numpy for clarity and ease of extension. It is not optimised for production and should be validated against benchmark cases before scientific use. Nevertheless, it demonstrates all core ideas:
stochastic integration of Langevin equations obeying the well‑mixed criterion
backward‑in‑time particle trajectory simulation
touchdown detection and gridded accumulation of the 2‑D flux footprint
cross‑wind integration and basic contour utilities
- class fluxfootprints.ls_footprint_model.BackwardLSModel(cfg: LSFootprintConfig)[source]
Bases:
objectA 3‑D backward Lagrangian stochastic footprint model.
Methods
Return upstream distance array and integrated footprint f(x).
Return (x_centres, y_centres, footprint_density).
run()Run the LS footprint simulation and accumulate 2‑D footprint.
- crosswind_integrated() Tuple[ndarray, ndarray][source]
Return upstream distance array and integrated footprint f(x).
- class fluxfootprints.ls_footprint_model.LSFootprintConfig(zm: float, ustar: float, L: float, h: float, wind_dir_deg: float, z0: float = 0.1, n_particles: int = 20000, dt: float = 0.25, t_max: float = 600.0, domain: Tuple[float, float] = (2000.0, 2000.0), dx: float = 10.0, dy: float = 10.0, seed: int | None = None)[source]
Bases:
objectContainer for model parameters fixed for one simulation.
- Attributes:
- seed
Methods
rng
- L: float
- domain: Tuple[float, float] = (2000.0, 2000.0)
- dt: float = 0.25
- dx: float = 10.0
- dy: float = 10.0
- h: float
- n_particles: int = 20000
- seed: int | None = None
- t_max: float = 600.0
- ustar: float
- wind_dir_deg: float
- z0: float = 0.1
- zm: float
- fluxfootprints.ls_footprint_model.lagrangian_timescale(z: float, zm: float, ustar: float) float[source]
Simple height‑dependent Lagrangian timescale (Hunt, 1984 style).
T_L = 0.15 z / σ_w + 0.3 z_m / σ_w(z_m) (heuristic)
- fluxfootprints.ls_footprint_model.log_wind_profile(z: float, ustar: float, z0: float) float[source]
Neutral logarithmic mean‑wind profile.
- Parameters:
- zfloat
Height above displacement (m).
- ustarfloat
Friction velocity (m s⁻¹).
- z0float
Aerodynamic roughness length (m).
- Returns:
- float
Mean wind speed at z (m s⁻¹).
fluxfootprints.ls_footprint_adapter module
Adapter for Lagrangian Stochastic footprint model.
- class fluxfootprints.ls_footprint_adapter.LSFootprintModelAdapter(*args, n_particles: int = 20000, **kwargs)[source]
Bases:
BaseFootprintModelLagrangian Stochastic footprint model adapter.
Wraps
BackwardLSModelin the standardBaseFootprintModelinterface. Intended to be driven throughfluxfootprints.ffp_daily_monthly_helper.build_climatology(), which resolves per-timestep inputs into a DataFrame with the standardized column names below; the model reads those columns directly.- Parameters:
- dfpandas.DataFrame
Input data. Must contain the following required columns:
- zmfloat or Series
Measurement height above displacement height (i.e. z - d) [m].
- z0float or Series
Aerodynamic roughness length [m].
- ustarfloat or Series
Friction velocity [m s⁻¹].
- olfloat or Series
Obukhov length [m].
- wind_dirfloat or Series
Mean wind direction the wind blows from [deg].
- hfloat or Series
Boundary-layer height [m].
- n_particlesint, default 20000
Number of stochastic particles released per timestep.
Methods
get_coordinates()Return x and y coordinate arrays.
get_footprint_climatology()Return the climatological footprint as xarray DataArray.
get_footprint_timeseries()Return time-resolved footprint if available.
get_results()Return complete results as xarray Dataset.
run([return_result])Execute Lagrangian footprint calculation.
to_netcdf(filepath)Save results to netCDF file.
- REQUIRED_COLUMNS = ['zm', 'z0', 'ustar', 'ol', 'wind_dir', 'h']
fluxfootprints.ep_footprint module
One-dimensional EddyPro-style footprint estimates (Kljun 2004, Kormann & Meixner 2001, Hsieh et al. 2000).
ep_footprint.py
Module for calculating footprint characteristics based on different models.
- class fluxfootprints.ep_footprint.Footprint(peak: float, offset: float, x10: float, x30: float, x50: float, x70: float, x80: float, x90: float)[source]
Bases:
objectClass to store footprint calculation results
Methods
error()Create an error footprint with all values set to -9999
- offset: float
- peak: float
- x10: float
- x30: float
- x50: float
- x70: float
- x80: float
- x90: float
- fluxfootprints.ep_footprint.handle_footprint(var_w: float, ustar: float, zL: float, wind_speed: float, MO_length: float, sonic_height: float, disp_height: float, rough_length: float, foot_model: str = 'kljun_04') Footprint[source]
Hub for footprint model calculations.
- Parameters:
var_w – Variance of vertical wind
ustar – Friction velocity
zL – Stability parameter (z/L)
wind_speed – Wind speed
MO_length – Monin-Obukhov length
sonic_height – Height of sonic anemometer
disp_height – Displacement height
rough_length – Roughness length
foot_model – Footprint model to use (‘kljun_04’, ‘kormann_meixner_01’, ‘hsieh_00’)
- Returns:
Footprint object containing calculation results
- fluxfootprints.ep_footprint.hsieh_00(MO_length: float, sonic_height: float, disp_height: float, rough_length: float) Footprint[source]
Footprint calculations based on Hsieh et al. (2000)
- Parameters:
MO_length – Monin-Obukhov length
sonic_height – Height of sonic anemometer
disp_height – Displacement height
rough_length – Roughness length
- Returns:
Footprint object containing calculation results
- fluxfootprints.ep_footprint.kljun_04(std_w: float, ustar: float, zL: float, sonic_height: float, disp_height: float, rough_length: float) Footprint[source]
Footprint calculations based on Kljun et al. (2004, BLM)
- fluxfootprints.ep_footprint.kormann_meixner_01(ustar: float, zL: float, wind_speed: float, sonic_height: float, disp_height: float) Footprint[source]
Footprint calculations based on Kormann and Meixner (2001)
- Parameters:
ustar – Friction velocity
zL – Stability parameter (z/L)
wind_speed – Wind speed
sonic_height – Height of sonic anemometer
disp_height – Displacement height
- Returns:
Footprint object containing calculation results
Workflow and export helpers
fluxfootprints.ffp_daily_monthly_helper module
Config loading, climatology construction, daily/monthly summaries, and GeoPackage / GeoTIFF / CSV export.
ffp_daily_monthly_helper.py (Refactored for FFPModel)
Summarize flux footprints from improved_ffp.FFPModel to daily/monthly periods and (optionally) export 80% source-area contours to a GeoPackage.
Updated to use FFPModel as the canonical implementation.
- class fluxfootprints.ffp_daily_monthly_helper.SummaryResult(f_daily_mean: 'xr.DataArray | None' = None, f_monthly_mean: 'xr.DataArray | None' = None, f_daily_et_weighted: 'xr.DataArray | None' = None, f_monthly_et_weighted: 'xr.DataArray | None' = None, daily_domain_coverage: 'pd.DataFrame | None' = None, monthly_domain_coverage: 'pd.DataFrame | None' = None)[source]
Bases:
object- Attributes:
- daily_domain_coverage
- f_daily_et_weighted
- f_daily_mean
- f_monthly_et_weighted
- f_monthly_mean
- monthly_domain_coverage
- daily_domain_coverage: DataFrame | None = None
- f_daily_et_weighted: DataArray | None = None
- f_daily_mean: DataArray | None = None
- f_monthly_et_weighted: DataArray | None = None
- f_monthly_mean: DataArray | None = None
- monthly_domain_coverage: DataFrame | None = None
- fluxfootprints.ffp_daily_monthly_helper.build_climatology(df: DataFrame, model_type: str = 'ffp', ustar: str | float | int | Series | None = 'ustar_1_1_1', ol: str | float | int | Series | None = 'mo_length_1_1_1', umean: str | float | int | Series | None = 'ws_1_1_1', sigmav: str | float | int | Series | None = 'v_sigma_1_1_1', wind_dir: str | float | int | Series | None = 'wd_1_1_1', zm: str | float | int | Series | None = 'zm', z0: str | float | int | Series | None = 'z0', h: str | float | int | Series | None = 2000.0, dx: float = 10.0, dy: float = 10.0, domain: tuple[float, float, float, float] | None = None, rs: list[float] | None = None, smooth_data: bool = True, verbosity: int = 2, logger: Logger | None = None, **model_kwargs) BaseFootprintModel[source]
Prepare inputs, validate per-model requirements, and run the footprint model.
- fluxfootprints.ffp_daily_monthly_helper.compute_aerodynamic_params(df: DataFrame, inst_height: float | str | Series | ndarray, crop_height: float | str | Series | ndarray, veg_type: str | None = None, z0_ratio: float | str | Series | ndarray | None = None, d_h_ratio: float | str | Series | ndarray | None = None) DataFrame[source]
Calculate effective measurement height (zm) and roughness length (z0).
Requires inst_height and crop_height, plus EITHER a veg_type preset OR custom ratio parameters (z0_ratio / d_h_ratio).
If both veg_type and z0_ratio/d_h_ratio are provided, the user-provided ratios will be used instead of the VEG_PRESETS parameters.
- fluxfootprints.ffp_daily_monthly_helper.export_contour_stats_csv(df: DataFrame, model: FFPModel, summaries: SummaryResult, station_lat: float, station_lon: float, csv_path: str | Path, rn_col: str | None = None, h_col: str | None = None, le_col: str | None = None, g_col: str | None = None, method: str = 'auto', crs_out: str | int = 'auto', levels=(0.8,)) Path[source]
Write the area and centroid of footprint source-area contours to a CSV.
For each layer, contour level and time slice, the smallest region holding fraction
rof the normalised footprint is traced as a polygon. The polygon is shifted from tower-relative metres intocrs_out, and its area and centroid are written as one row.- Parameters:
- dfpd.DataFrame
Not used. Kept for signature compatibility with the other export helpers.
- modelFFPModel
A footprint model that has been run. Its
xandyattributes are 1-D coordinate arrays in metres relative to the tower.- summariesSummaryResult
Output of the daily/monthly summary step. The attributes
f_daily_mean,f_monthly_mean,f_daily_et_weightedandf_monthly_et_weightedare read asxr.DataArrayobjects with atimedimension.Nonelayers are skipped.- station_lat, station_lonfloat
Tower latitude and longitude in decimal degrees (WGS 84).
- csv_pathstr or Path
Path of the CSV file to write. Overwritten if it exists.
- rn_col, h_col, le_col, g_colstr, optional
Not used. Kept for signature compatibility.
- method{“auto”, “skimage”, “rasterio”}, default “auto”
Contour extraction backend passed to
_make_contour_polygon_from_field_alt."auto"tries scikit-image first and falls back to rasterio.- crs_outstr or int, default “auto”
CRS the polygons are placed in before area and centroid are computed.
"auto"uses the local WGS 84 UTM zone. Any other value must be a projected CRS in metres, because the grid offsets are added directly to the tower’s projected coordinates and area is converted from m^2.- levelsiterable of float, default (0.8,)
Cumulative source-area fractions (0-1) to contour.
- Returns:
- Path
Path of the written CSV.
Notes
The CSV has the columns
layer,time(ISO 8601),r(contour level),area_ha(polygon area in hectares),centroid_lonandcentroid_lat(WGS 84 degrees). Only the largest polygon is kept for each slice, so a footprint split into several patches is under-reported. Slices where no contour can be traced, such as an all-zero or all-NaN field, are skipped without a row.
- fluxfootprints.ffp_daily_monthly_helper.export_contours_gpkg(model: FFPModel, summaries: SummaryResult, df: DataFrame, station_lat: float, station_lon: float, gpkg_path: str | Path, crs_out: str | int = 'auto', levels: tuple = (0.8,), stats_csv: str | Path | None = None, centroid_out: str | int = 4326, rn_col: str | None = None, h_col: str | None = None, le_col: str | None = None, g_col: str | None = None, contour_method: str = 'auto') Path[source]
Export source-area contours to GeoPackage with energy balance stats.
Adapted to work with FFPModel output structure.
- fluxfootprints.ffp_daily_monthly_helper.export_rasters_geotiff(model: FFPModel, summaries: SummaryResult, station_lat: float, station_lon: float, out_dir: str | Path, crs_out: str | int = 'auto', which=('daily_mean', 'monthly_mean', 'daily_etw', 'monthly_etw'), prefix='ffp', dtype='float32', nodata=0.0) Path[source]
Export each time slice of the footprint summaries as a single-band GeoTIFF.
The footprint grid (
model.x,model.y) is given in metres relative to the tower. It is first placed in the local WGS 84 UTM zone by offsetting it with the tower’s projected coordinates. Ifcrs_outnames a different CRS, each slice is then reprojected onto that CRS with bilinear resampling.- Parameters:
- modelFFPModel
A footprint model that has been run. Its
xandyattributes are 1-D, evenly spaced coordinate arrays (metres from the tower) with at least two points each.- summariesSummaryResult
Output of the daily/monthly summary step. The attributes
f_daily_mean,f_monthly_mean,f_daily_et_weightedandf_monthly_et_weightedare read asxr.DataArrayobjects with dims("time", "x", "y"). Missing orNoneattributes are skipped.- station_lat, station_lonfloat
Tower latitude and longitude in decimal degrees (WGS 84). Used to pick the UTM zone and to anchor the grid.
- out_dirstr or Path
Output directory. Created if it does not exist.
- crs_outstr or int, default “auto”
Target CRS for the output rasters.
"auto"writes in the local UTM zone without resampling. Any other value accepted bypyproj.CRS.from_user_input(e.g.4326,"EPSG:5070") triggers reprojection.- whichiterable of str, default (“daily_mean”, “monthly_mean”, “daily_etw”, “monthly_etw”)
Layers to export. Valid names are
"daily_mean","monthly_mean","daily_etw"and"monthly_etw".- prefixstr, default “ffp”
Filename prefix.
- dtypestr, default “float32”
Output raster data type. Also selects the deflate predictor: 3 for floating-point types, 2 for integer types.
- nodatafloat, default 0.0
Nodata value written to the raster metadata. Reprojected cells outside the source extent are also filled with this value.
- Returns:
- Path
The output directory.
- Raises:
- ValueError
If
model.xormodel.yhas fewer than two points, or ifwhichcontains an unknown layer name.
Notes
Files are named
{prefix}_{layer}_{YYYYMMDD}.tiffor daily layers and{prefix}_{layer}_{YYYYMM}.tiffor monthly layers. Rasters at least 256 x 256 pixels are written tiled with 256-pixel blocks; smaller rasters are written striped.
- fluxfootprints.ffp_daily_monthly_helper.load_amf_df(csv_path: str | Path, cfg: dict[str, Any]) DataFrame[source]
Load AMF half-hourly CSV and return a tidy DataFrame indexed by time.
- fluxfootprints.ffp_daily_monthly_helper.load_config(ini_path: str | Path) dict[str, Any][source]
Read a minimal INI for site metadata and column names.
- fluxfootprints.ffp_daily_monthly_helper.summarize_periods(model: BaseFootprintModel, df: DataFrame, et_source: str | None = 'LE', is_le: bool = True, calc_et_weighted: bool = True, daily: bool = True, monthly: bool = True, normalize_each_time: bool = False) SummaryResult[source]
Build daily/monthly summaries matching Kljun canonical denominator logic.
Aggregates a model’s time-resolved 2-D footprint into daily and/or monthly mean footprints, ET-weighted footprints (optional), and domain-coverage statistics. Timesteps whose footprint integrates to zero (e.g. model failures or filtered records) are treated as invalid and excluded from every average and from the ET-weighting denominator.
- Parameters:
- modelBaseFootprintModel
A footprint model that has already been run, so that
model.f_2dis populated andmodel.get_footprint_timeseries()returns anxr.DataArraywith dims("time", "x", "y")holding footprint density (m^-2). If the model exposesdxanddythey are used as the grid cell size; otherwise cell size is inferred from thex/ycoordinate spacing.- dfpd.DataFrame
Time-indexed input data. Only used to supply the ET/LE column; it is reindexed onto the footprint
timecoordinate using exact matches, so timestamps absent fromdfget NaN weights.- et_sourcestr or None, default “LE”
Name of the column in
dfused for ET weighting. If None or not present indf, ET-weighted outputs are silently skipped.- is_lebool, default True
If True,
et_sourceis latent energy (W m^-2) and is converted to ET (mm hr^-1) by dividing by 680.6. Set False whenet_sourcealready holds ET in mm hr^-1 (e.g. NLDAS).- calc_et_weightedbool, default True
Toggle ET-weighted footprints and ET-weighted coverage on/off.
- dailybool, default True
Compute daily (
"1D") aggregates.- monthlybool, default True
Compute monthly (
"MS", month-start labelled) aggregates.- normalize_each_timebool, default False
If True, rescale each valid time slice so it sums to 1 over x, y before averaging. Note this normalizes the raw sum (not the area-integral), so the resulting values are per-cell weights rather than densities. Coverage statistics are always computed on the un-normalized footprint.
- Returns:
- SummaryResult
Fields are populated according to the
daily,monthlyand ET flags; unrequested outputs remain None.f_daily_mean/f_monthly_meanxr.DataArrayUnweighted mean of valid footprint slices per period.
f_daily_et_weighted/f_monthly_et_weightedxr.DataArraysum(f * ET) / sum(ET)over valid slices per period. NaN for periods whose ET sum is <= 0.
daily_domain_coverage/monthly_domain_coveragepd.DataFrameIndexed by period, with columns
mean_coverage_pct,et_weighted_coverage_pct(only when ET is used),min_coverage_pct,max_coverage_pctandvalid_timesteps. Coverage is the percentage of the footprint captured inside the model domain (sum(f) * dx * dy * 100).
- Raises:
- ValueError
If the model has no computed
f_2dor does not provide a time-resolved footprint.
Notes
Coverage values can exceed 100% close to the tower, where a narrow crosswind Gaussian is under-resolved by the grid. This discretization artifact is intentionally left unclamped.
Negative ET values are not clipped and therefore enter the weighting as-is; filter or clip
et_sourcebeforehand if that is undesirable.
fluxfootprints.openet_masking module
Masking of footprint fields and exported GeoTIFFs by the valid-data masks carried in daily OpenET rasters.
openet_masking.py
Mask footprint output – both the in-memory xarray fields and the exported GeoTIFFs – with the valid-data masks carried by daily OpenET rasters.
OpenET distributes daily ET as one raster per day with the date in the file
name (ensemble_et_20200615.tif, 2020-06-15_openet.tif, …). Cells with
no retrievable ET value – outside the modelled crop mask, cloud screened, or
off-tile – are stored as nodata. Footprint weight that lands on those cells
cannot be paired with an ET value, so it should normally be dropped before the
footprint is used to attribute flux to the landscape.
The helpers here reproject that valid/invalid pattern from each daily OpenET raster onto the footprint grid, apply it per time step (daily slices use the matching day; monthly slices combine every OpenET day in that month), and report the fraction of footprint weight that survives so the loss stays visible.
Typical use:
model = build_climatology(df, ...)
summaries = summarize_periods(model, df)
masked = mask_summaries(summaries, "/data/openet/daily",
station_lat=40.0, station_lon=-111.9)
export_rasters_geotiff(model, masked, 40.0, -111.9, out_dir="out")
or, to mask GeoTIFFs that were already written to disk:
mask_rasters_geotiff("out", "/data/openet/daily")
- class fluxfootprints.openet_masking.GridGeometry(crs: CRS, transform: Affine, width: int, height: int, x_origin: float, y_origin: float, y_ascending: bool)[source]
Bases:
objectGeoreferencing for a footprint grid expressed in metres from the tower.
- Attributes:
shape(height, width), i.e. numpy row/column order.
- crs: CRS
- height: int
- property shape: tuple[int, int]
(height, width), i.e. numpy row/column order.
- transform: Affine
- width: int
- x_origin: float
- y_ascending: bool
- y_origin: float
- class fluxfootprints.openet_masking.MaskedFootprint(data: DataArray, mask: DataArray, retained_fraction: DataArray, missing_dates: list[Timestamp])[source]
Bases:
objectResult of masking a footprint field with OpenET valid-data masks.
- data: DataArray
- mask: DataArray
- missing_dates: list[Timestamp]
- retained_fraction: DataArray
- fluxfootprints.openet_masking.apply_openet_mask(target: DataArray | SummaryResult | str | Path, openet: str | Path | Sequence[str | Path] | Mapping[Any, str | Path | Sequence[str | Path]], station_lat: float | None = None, station_lon: float | None = None, **kwargs: Any) MaskedFootprint | SummaryResult | list[Path][source]
Mask footprint output with daily OpenET valid-data masks.
Convenience entry point that dispatches on target:
target
behaviour
xarray.DataArraySummaryResultpath to a directory or GeoTIFF
- Parameters:
- targetxarray.DataArray, SummaryResult, or path
What to mask.
- openetpath, sequence of paths, or mapping
Daily OpenET rasters; see
index_openet_rasters().- station_lat, station_lonfloat, optional
Tower position in decimal degrees (WGS 84). Required for the xarray targets, which carry grid offsets rather than map coordinates; ignored for GeoTIFFs, which are already georeferenced.
- **kwargs
Forwarded to the dispatched function.
- Returns:
- MaskedFootprint, SummaryResult, or list of pathlib.Path
Matching the dispatched function.
- fluxfootprints.openet_masking.footprint_grid_geometry(x: ndarray | Sequence[float], y: ndarray | Sequence[float], station_lat: float, station_lon: float, crs: str | int | CRS = 'auto') GridGeometry[source]
Georeference a footprint grid given in metres relative to the tower.
Uses the same convention as
export_rasters_geotiff(): the tower is projected into the local WGS 84 UTM zone (or crs), grid offsets are added to it, and the resulting north-up transform is anchored on the outer edge of the corner cells.- Parameters:
- x, yarray_like
Regularly spaced cell-centre offsets in metres, as produced by the footprint models (
model.x,model.y).- station_lat, station_lonfloat
Tower position in decimal degrees (WGS 84).
- crsstr, int, pyproj.CRS, default “auto”
Target CRS;
"auto"picks the local UTM zone.
- Returns:
- GridGeometry
- fluxfootprints.openet_masking.index_openet_rasters(source: str | Path | Sequence[str | Path] | Mapping[Any, str | Path | Sequence[str | Path]], pattern: str = '*.tif', recursive: bool = False, date_regex: str | Pattern | None = None, logger: Logger | None = None) dict[date, list[Path]][source]
Build a
date -> [paths]index of daily OpenET rasters.- Parameters:
- sourcepath, sequence of paths, or mapping
A directory to scan, a single raster, a sequence of rasters, or an already-built mapping of date-like keys to one or more paths.
- patternstr, default “*.tif”
Glob used when source is a directory.
- recursivebool, default False
Search sub-directories as well.
- date_regexstr or re.Pattern, optional
Custom date pattern passed to
parse_raster_date().- loggerlogging.Logger, optional
- Returns:
- dict
datetime.datekeys mapped to sorted lists of paths. Several files sharing a date (adjacent tiles, for instance) are kept together and combined when the mask is built.
Notes
Files whose name only carries a year and month are indexed under the first of that month, which still lets them serve a monthly footprint slice.
- fluxfootprints.openet_masking.mask_footprint_dataarray(da: DataArray, openet: str | Path | Sequence[str | Path] | Mapping[Any, str | Path | Sequence[str | Path]], station_lat: float, station_lon: float, freq: str = 'auto', grid_crs: str | int | CRS = 'auto', band: int | str = 1, nodata: float | None = None, valid_range: tuple[float | None, float | None] | None = None, treat_zero_as_nodata: bool = False, resampling: str | Resampling = 'nearest', coverage_threshold: float = 0.5, combine: str = 'any', on_missing: str = 'skip', max_gap_days: int = 8, fill_value: float = 0.0, renormalize: bool = False, pattern: str = '*.tif', recursive: bool = False, date_regex: str | Pattern | None = None, logger: Logger | None = None) MaskedFootprint[source]
Mask a time-resolved footprint field with daily OpenET valid-data masks.
Each time step is matched to the OpenET raster(s) for that date; monthly steps (
freq="monthly") combine every OpenET day inside the month with combine. Footprint weight on cells with no ET value is replaced by fill_value.- Parameters:
- daxarray.DataArray
Footprint field with
time,xandydims, wherex/yare metres from the tower – e.g.model.f_2dor any field ofSummaryResult.- openetpath, sequence of paths, or mapping
Daily OpenET rasters; see
index_openet_rasters().- station_lat, station_lonfloat
Tower position in decimal degrees (WGS 84).
- freq{“auto”, “daily”, “monthly”}, default “auto”
How to interpret the time coordinate.
"auto"reads a series of more than one stamp, all first-of-month and one stamp per month, as monthly, and anything else as daily – so set it explicitly for a single-slice monthly field.- grid_crsstr, int, pyproj.CRS, default “auto”
CRS the footprint grid is georeferenced into;
"auto"uses the local UTM zone, matching the GeoTIFF export.- band, nodata, valid_range, treat_zero_as_nodata, resampling,
- coverage_threshold, combine
Passed through to
openet_mask_on_grid().- on_missing{“skip”, “mask”, “nearest”, “error”}, default “skip”
What to do for a time step with no OpenET raster: leave it unmasked, mask it out entirely, borrow the nearest date within max_gap_days, or raise.
- max_gap_daysint, default 8
Search radius for
on_missing="nearest".- fill_valuefloat, default 0.0
Value written where ET data are missing. 0.0 keeps the field usable by the contour and GeoTIFF exporters; pass
numpy.nanto make the hole explicit.- renormalizebool, default False
Rescale each masked time step so it integrates to the same total as before masking, i.e. redistribute the dropped weight over the cells that have ET. Off by default so the loss stays visible in retained_fraction.
- pattern, recursive, date_regex
Passed through to
index_openet_rasters().- loggerlogging.Logger, optional
- Returns:
- MaskedFootprint
data(masked field),mask(boolean, per time step), andretained_fraction(share of footprint weight surviving each step), plusmissing_datesfor steps with no OpenET coverage.
Notes
retained_fractionis the share of this field’s weight kept, so it speaks to how much of the source area is observable by OpenET. It is not the same as the domain-coverage columns produced bysummarize_periods(), which measure how much of the footprint fits inside the model domain.
- fluxfootprints.openet_masking.mask_rasters_geotiff(raster_dir: str | Path, openet: str | Path | Sequence[str | Path] | Mapping[Any, str | Path | Sequence[str | Path]], out_dir: str | Path | None = None, pattern: str = '*.tif', fill_value: float | None = None, band: int | str = 1, nodata: float | None = None, valid_range: tuple[float | None, float | None] | None = None, treat_zero_as_nodata: bool = False, resampling: str | Resampling = 'nearest', coverage_threshold: float = 0.5, combine: str = 'any', on_missing: str = 'skip', max_gap_days: int = 8, renormalize: bool = False, openet_pattern: str = '*.tif', recursive: bool = False, date_regex: str | Pattern | None = None, logger: Logger | None = None) list[Path][source]
Mask footprint GeoTIFFs on disk with daily OpenET valid-data masks.
Each footprint raster is matched to OpenET by the date in its file name –
..._20240201.tifuses that day,..._202402.tifcombines every OpenET day in February 2024 – so this works directly on the output ofexport_rasters_geotiff(). Masks are built against each file’s own CRS and transform, so reprojected exports are handled too.- Parameters:
- raster_dirpath
Directory of footprint GeoTIFFs (or a single GeoTIFF).
- openetpath, sequence of paths, or mapping
Daily OpenET rasters; see
index_openet_rasters().- out_dirpath, optional
Where masked copies are written. Defaults to
<raster_dir>/openet_masked. Inputs are never overwritten in place unless out_dir is set to the input directory.- patternstr, default “*.tif”
Glob for the footprint rasters.
- fill_valuefloat, optional
Value written where ET data are missing. Defaults to each file’s own nodata value, or 0.0 when it has none.
- band, nodata, valid_range, treat_zero_as_nodata, resampling,
- coverage_threshold, combine
Passed through to
openet_mask_on_grid()(they describe the OpenET rasters, not the footprint rasters).- on_missing{“skip”, “mask”, “nearest”, “error”}, default “skip”
What to do when no OpenET raster matches a footprint raster’s date: copy it through unmasked, blank it, borrow the nearest date within max_gap_days, or raise.
- max_gap_daysint, default 8
Search radius for
on_missing="nearest".- renormalizebool, default False
Rescale each masked band to its pre-masking sum.
- openet_pattern, recursive, date_regex
Passed through to
index_openet_rasters().- loggerlogging.Logger, optional
- Returns:
- list of pathlib.Path
The masked rasters that were written, in name order.
- fluxfootprints.openet_masking.mask_summaries(summaries: SummaryResult, openet: str | Path | Sequence[str | Path] | Mapping[Any, str | Path | Sequence[str | Path]], station_lat: float, station_lon: float, **kwargs: Any) SummaryResult[source]
Apply OpenET masks to every field of a
SummaryResult.Daily fields are matched day-by-day and monthly fields month-by-month. The surviving fraction of footprint weight is appended to the coverage tables as
openet_retained_frac(andopenet_retained_frac_etwfor the ET-weighted fields).- Parameters:
- summariesSummaryResult
Output of
summarize_periods().- openetpath, sequence of paths, or mapping
Daily OpenET rasters; see
index_openet_rasters().- station_lat, station_lonfloat
Tower position in decimal degrees (WGS 84).
- **kwargs
Any keyword accepted by
mask_footprint_dataarray()except freq, which is set per field.
- Returns:
- SummaryResult
New object; the input is not modified.
- fluxfootprints.openet_masking.openet_mask_on_grid(rasters: str | Path | Sequence[str | Path], dst_crs: CRS | str | int, dst_transform: Affine, dst_shape: tuple[int, int], band: int | str = 1, nodata: float | None = None, valid_range: tuple[float | None, float | None] | None = None, treat_zero_as_nodata: bool = False, resampling: str | Resampling = 'nearest', coverage_threshold: float = 0.5, combine: str = 'any') ndarray[source]
Build a boolean valid-data mask for a target grid from OpenET rasters.
- Parameters:
- rasterspath or sequence of paths
One or more OpenET rasters. Several rasters are reduced with combine, which is how adjacent tiles for the same day are merged.
- dst_crs, dst_transform, dst_shape
Target grid, north-up; dst_shape is
(height, width).- bandint or str, default 1
Band index (1-based) or band description to read.
- nodatafloat, optional
Extra nodata value to honour on top of the dataset’s own.
- valid_rangetuple of (float or None, float or None), optional
Keep only cells inside these inclusive bounds, e.g.
(0.0, None)to drop negative ET.- treat_zero_as_nodatabool, default False
Treat exact zeros as missing. Useful where OpenET writes 0 instead of nodata outside the crop mask; leave off when a real 0 mm/day matters.
- resamplingstr or rasterio.warp.Resampling, default “nearest”
Warping method.
"nearest"gives a hard mask;"average"gives the fraction of the destination cell covered by valid source cells, which is then thresholded with coverage_threshold.- coverage_thresholdfloat, default 0.5
Fraction of valid coverage a destination cell needs to count as valid.
- combine{“any”, “all”, “majority”}, default “any”
How to reduce multiple rasters.
- Returns:
- numpy.ndarray
Boolean array of shape dst_shape; True where ET data exist.
- fluxfootprints.openet_masking.parse_raster_date(name: str | Path, date_regex: str | Pattern | None = None) tuple[date, str] | None[source]
Pull a date out of a raster file name.
Recognizes
YYYYMMDD,YYYY-MM-DD,YYYY_MM_DDandYYYY.MM.DDanywhere in the name, and falls back to the year-month forms (YYYYMM,YYYY-MM) used by the monthly footprint exports. The first token that parses as a real calendar date wins.- Parameters:
- namestr or pathlib.Path
File name or full path.
- date_regexstr or re.Pattern, optional
Override pattern. Must expose either three groups (year, month, day) or two groups (year, month).
- Returns:
- tuple of (datetime.date, str) or None
The parsed date and its kind –
"day"or"month"(month-kind dates are anchored to the first of the month).Noneif nothing in the name parses as a date.
fluxfootprints.by_row_fetch_tools module
Per-row fetch geometry, daily centroids, and density rasters.
by_row_fetch_tools.py
Tools for fetching and processing by-row data, including: * coordinate transformations, * daily aggregation, and * raster generation. This module provides functions to: * convert polar coordinates to Cartesian, * aggregate sub-daily data to daily centroids, * generate weighted kernel density rasters, and * impute missing evapotranspiration data.
- fluxfootprints.by_row_fetch_tools.aggregate_to_daily_centroid(df: DataFrame, date_column: str = 'Timestamp', x_column: str = 'X', y_column: str = 'Y', weighted: bool = True)[source]
Aggregate sub-daily coordinate data to daily centroids.
This function groups coordinate data by calendar date and computes the centroid of X and Y values. If weighted=True, centroids are computed using a weighted average based on the ‘ET’ (evapotranspiration) column.
- Parameters:
- dfpandas.DataFrame
Input DataFrame containing timestamps and coordinate columns.
- date_columnstr, optional
Name of the column with datetime objects, by default “Timestamp”.
- x_columnstr, optional
Name of the column representing X coordinates, by default “X”.
- y_columnstr, optional
Name of the column representing Y coordinates, by default “Y”.
- weightedbool, optional
If True, uses the ‘ET’ column as weights for centroid calculation; otherwise computes an unweighted mean. Default is True.
- Returns:
- pandas.DataFrame
DataFrame with one row per date and columns for: - ‘Date’: datetime.date object - x_column: daily centroid X coordinate - y_column: daily centroid Y coordinate
Notes
Requires a column named ‘ET’ if weighted=True.
Assumes the timestamp column can be parsed with pd.to_datetime.
The input DataFrame is left unchanged.
- fluxfootprints.by_row_fetch_tools.generate_density_raster(gdf: GeoDataFrame, resolution: float = 50, buffer_distance: float = 200, epsg: int = 5070, weight_field: str = 'ET')[source]
Generate a weighted kernel density raster from a point GeoDataFrame.
This function uses a Gaussian kernel density estimation (KDE) to create a raster from spatial point data, where each point is weighted by a specified attribute (e.g., evapotranspiration). The output raster is georeferenced with an affine transform and buffered beyond the point extent.
- Parameters:
- gdfgeopandas.GeoDataFrame
Input GeoDataFrame containing point geometries and a weight field.
- resolutionfloat, optional
Size of each raster cell in projection units (typically meters), by default 50.
- buffer_distancefloat, optional
Distance to extend the raster beyond the extent of the points, in the same units as the CRS (e.g., meters), by default 200.
- epsgint, optional
EPSG code for the output coordinate reference system, by default 5070 (NAD83 / Conus Albers).
- weight_fieldstr, optional
Name of the column in gdf to use as weights in the KDE calculation, by default “ET”.
- Returns:
- densitynumpy.ndarray
2D array of KDE density values (not normalized by default).
- transformaffine.Affine
Affine transformation mapping array coordinates to geographic space.
- boundstuple of float
Bounding box of the raster extent as (xmin, ymin, xmax, ymax).
Notes
This function assumes that the input geometries are point features.
The returned density array is not normalized unless the line density /= np.sum(density) is uncommented.
The output CRS is set using gdf.to_crs(epsg=…).
- fluxfootprints.by_row_fetch_tools.impute_evapotranspiration(df: DataFrame, in_field: str = 'ET', out_field: str = 'ET')[source]
Impute missing data in a half-hourly evapotranspiration time series.
- Parameters:
df (pd.DataFrame) – DataFrame with a datetime index and a column ‘ET’ containing evapotranspiration data.
- Returns:
DataFrame with missing values imputed.
- Return type:
pd.DataFrame
- fluxfootprints.by_row_fetch_tools.polar_to_cartesian_dataframe(df: DataFrame, wd_column: str = 'WD', dist_column: str = 'Dist')[source]
Convert polar coordinates in a DataFrame to Cartesian coordinates.
This function computes Cartesian coordinate columns (X_<dist_column> and Y_<dist_column>) from polar inputs defined by wind direction (in degrees from North) and one or more radial distance columns. Unlike a mutating helper, it does not modify the input DataFrame; it returns only the newly computed coordinate columns.
- Parameters:
- dfpandas.DataFrame
Input DataFrame containing polar coordinate columns.
- wd_columnstr, optional
Name of the column representing wind direction in degrees from North, by default “WD”.
- dist_columnstr or list of str, optional
Name(s) of the column(s) representing distance from origin. A single column name produces one
X_/Y_pair; a list of names produces a pair for each distance column. By default “Dist”.
- Returns:
- pandas.DataFrame
A new DataFrame (indexed like
df) containing only the computed coordinate columns. For each distance columnctwo columns are returned: - ‘X_<c>’: Cartesian X coordinate - ‘Y_<c>’: Cartesian Y coordinate
Notes
The function treats -9999 and NaN as missing values.
Wind direction is converted such that 0° = North, 90° = East, etc.
Cartesian conversion is performed using: X = dist * cos(θ) and Y = dist * sin(θ) where θ = (90 - WD) in degrees.
The input DataFrame is left unchanged.
fluxfootprints.compare module
Side-by-side model comparison and diagnostic metrics.
footprint_compare.py
Utility functions for computing and comparing eddy‑covariance flux‑footprint models.
The module provides a small toolkit to re‑run the side‑by‑side evaluations we discussed earlier:
Run multiple footprint models (the xarray rewrite
ffp_xr, the analytic Kormann‑Meixner (KM) approximation, or any other user‑supplied function).Compute common diagnostic metrics against a chosen reference model (RMSE, peak‑location bias, 80 % source‑area overlap).
Return a tidy
pandas.DataFrameand create optional difference plots.
All heavy lifting (meteorological inputs, domain definition …) is delegated to existing model APIs; this file is purely orchestration + diagnostics.
Example
>>> import pandas as pd, numpy as np
>>> from footprint_compare import (
... run_ffp_xr, run_km, compare_footprints)
>>> met = pd.DataFrame({ # 24 h synthetic
... "wind_dir": 0.0, "ws": 5.0, "sigmav": .5,
... "ustar": .3, "ol": 200.0},
... index=pd.date_range("2025-05-01", periods=24, freq="h"))
>>> dom = [-300, 300, -300, 300]
>>> f_ref, (x, y) = run_ffp_xr(met, domain=dom, dx=2)
>>> f_km, _ = run_km(met, domain=dom, dx=2)
>>> res = compare_footprints(f_ref, (x, y),
... {"KM": (f_km, (x, y))})
>>> print(res)
Dependencies
numpy, pandas, matplotlib, scipy;
footprint models: ffp_xr (must be importable, e.g. placed on
PYTHONPATH).
- fluxfootprints.compare.compare_footprints(ref_f: ndarray, ref_grid: tuple[ndarray, ndarray], others: dict[str, tuple[ndarray, tuple[ndarray, ndarray]]], *, plot: bool = True, cmap='RdBu', vlim: float | None = None) DataFrame[source]
Compute metrics for others against reference; optionally plot diffs.
- Parameters:
- ref_f / ref_grid
Reference footprint density and its (x_2d, y_2d) grids.
- others
Dict mapping name → (f_2d, (x_2d, y_2d)).
- plot
If True, draw a difference map for each entry.
- vlim
Symmetric colour‑bar limit for difference plots. By default the max absolute difference across all models is used.
- Returns
- ——-
- ``pandas.DataFrame`` with rows = model names, columns = metrics.
- fluxfootprints.compare.footprint_metrics(f_ref: ndarray, x_ref: ndarray, y_ref: ndarray, f_other: ndarray, x_other: ndarray, y_other: ndarray) dict[str, float][source]
Compute diagnostic comparison metrics between two footprint distributions.
Compares a test footprint (f_other) against a reference footprint (f_ref) using several spatial metrics: root mean squared error (RMSE), peak location difference, and percentage overlap of the 80% contribution area.
- Parameters:
- f_refnumpy.ndarray
Reference footprint values on a 2D grid.
- x_refnumpy.ndarray
X-coordinates for the reference footprint grid.
- y_refnumpy.ndarray
Y-coordinates for the reference footprint grid.
- f_othernumpy.ndarray
Footprint values from the model being compared.
- x_othernumpy.ndarray
X-coordinates for the comparison footprint grid.
- y_othernumpy.ndarray
Y-coordinates for the comparison footprint grid.
- Returns:
- metricsdict of str to float
Dictionary containing: - ‘RMSE’ : Root mean squared error between footprints. - ‘Peak_ref’ : Distance of the peak location in the reference footprint from the origin [m]. - ‘Peak_other’ : Distance of the peak location in the comparison footprint from the origin [m]. - ‘Peak_diff’ : Absolute difference in peak distances [m]. - ‘Overlap80(%)’ : Percent overlap of 80% contribution regions [%].
- Raises:
- ValueError
If the input grids are not congruent in shape.
- fluxfootprints.compare.run_all_and_compare(met: DataFrame, *, domain: tuple[float, float, float, float] = (-300, 300, -300, 300), dx: float = 2.0, zm: float = 2.0, z0: float = 0.1, include_km: bool = True, **extra_kwargs) DataFrame[source]
Run multiple footprint models and compare their outputs to a reference.
Executes the
ffp_xrmodel as the reference and, optionally, the KM toy model using a common meteorological dataset and spatial domain. Each alternative model’s output is compared to the reference using RMSE, peak distance, and 80% overlap metrics.- Parameters:
- metpandas.DataFrame
DataFrame containing meteorological inputs, expected to include: - ‘ol’ : Obukhov length [m] - ‘sigmav’ : Lateral standard deviation of velocity [m/s] - ‘ustar’ : Friction velocity [m/s] - ‘wind_dir’ : Wind direction [degrees]
- domaintuple of float, optional
Spatial extent of the footprint domain (xmin, xmax, ymin, ymax). Default is (-300, 300, -300, 300).
- dxfloat, optional
Grid resolution in meters. Default is 2.0.
- zmfloat, optional
Measurement height [m]. Default is 2.0.
- z0float, optional
Surface roughness length [m]. Default is 0.1.
- include_kmbool, optional
Whether to include the KM toy model in the comparison. Default is True.
- **extra_kwargs
Additional arguments passed to run_ffp_xr.
- Returns:
- df_metricspandas.DataFrame
DataFrame of comparison metrics (RMSE, peak location, 80% overlap) for each alternative model relative to the ffp_xr reference.
Notes
The function generates a comparison plot using compare_footprints(plot=True).
- fluxfootprints.compare.run_ffp_xr(met: DataFrame, *, domain: list[float] | tuple[float, float, float, float] = (-300, 300, -300, 300), dx: float = 2.0, logger=None, **extra_kwargs) dict[str, ndarray | tuple[ndarray, ndarray]][source]
Compute the footprint climatology using the xarray-based FFP implementation.
This function runs the ffp_climatology_new model from the ffp_xr module on a meteorological input DataFrame, returning the 2D footprint and associated spatial grid coordinates.
- Parameters:
- metpandas.DataFrame
DataFrame containing meteorological inputs for footprint modeling.
- domainlist of float or tuple of float, optional
Spatial extent of the footprint domain in the format (xmin, xmax, ymin, ymax). Default is (-300, 300, -300, 300).
- dxfloat, optional
Grid resolution in both x and y directions. Default is 2.0.
- loggerlogging.Logger or None, optional
Logger instance for model output. If None, a default logger is created.
- **extra_kwargs
Additional keyword arguments passed to ffp_climatology_new.
- Returns:
- ffp_2dnumpy.ndarray
2D array representing the climatological footprint values.
- (x, y)tuple of numpy.ndarray
Tuple of arrays representing the x and y coordinates of the footprint grid.
- fluxfootprints.compare.run_km(met: DataFrame, *, zm: float = 2.0, z0: float = 0.1, domain: list[float] | tuple[float, float, float, float] = (-300, 300, -300, 300), dx: float = 2.0, **extra_kwargs) dict[str, ndarray | tuple[ndarray, ndarray]][source]
Compute a toy 2D footprint based on a Kormann–Meixner-style analytic solution.
This simplified implementation models the footprint as a product of a 1D Gamma distribution in the along-wind (y) direction and a Gaussian in the cross-wind (x) direction. It is intended as a minimal, conceptual reference.
- Parameters:
- metpandas.DataFrame
Meteorological input data (unused in this implementation, but included for API compatibility).
- zmfloat, optional
Measurement height above displacement height [m]. Default is 2.0.
- z0float, optional
Surface roughness length [m]. Default is 0.1.
- domainlist of float or tuple of float, optional
Spatial extent of the footprint domain in the format (xmin, xmax, ymin, ymax). Default is (-300, 300, -300, 300).
- dxfloat, optional
Grid resolution in both x and y directions [m]. Default is 2.0.
- **extra_kwargs
Ignored in this implementation. Included for API consistency.
- Returns:
- f2dnumpy.ndarray
2D array of normalized footprint values.
- (xv, yv)tuple of numpy.ndarray
Tuple of 2D arrays representing the x and y coordinates of the grid [m].
Notes
The analytic form is loosely inspired by Kormann & Meixner (2001) under neutral conditions.
Not intended for production use; serves as a placeholder or toy model.
fluxfootprints.footprint_plotting module
- class fluxfootprints.footprint_plotting.FootprintPlotter[source]
Bases:
objectClass to handle all footprint plotting functionality.
Methods
get_contour_levels(f, dx, dy[, rs])Calculate contour levels at specified percentages of the footprint integral.
get_contour_vertices(x, y, f, lev)Get vertices of contour at specified level.
plot_footprint(x_2d, y_2d, fs[, clevs, ...])Plot footprint function and contours.
- get_contour_levels(f: ndarray, dx: float, dy: float, rs: list[float] | None = None) list[tuple[float, float, float]][source]
Calculate contour levels at specified percentages of the footprint integral.
- get_contour_vertices(x: ndarray, y: ndarray, f: ndarray, lev: float) tuple[list | None, list | None][source]
Get vertices of contour at specified level.
- plot_footprint(x_2d: ndarray, y_2d: ndarray, fs: ndarray, clevs: list[float] | None = None, show_heatmap: bool = True, normalize: str | None = None, colormap=None, line_width: float = 0.5, iso_labels: bool | None = None, title: str | None = None) tuple[Figure, Axes][source]
Plot footprint function and contours.
fluxfootprints.footprint_animation module
Hourly / daily / monthly videos of footprint raster time series, with timestamp overlays and optional georeferenced basemaps.
Animate footprint raster time series as video.
This module turns the time-resolved footprint rasters produced by the models in
fluxfootprints (or the daily/monthly summaries from
fluxfootprints.summarize_periods()) into a video or animated GIF. Each
frame is one time step, annotated with its timestamp, and the raster can be
drawn on top of a georeferenced web-map basemap.
The main entry points are FootprintAnimator (full control, reusable)
and animate_footprint() (one-shot convenience wrapper).
Examples
Animate the native (half-hourly) footprint time series aggregated to hours:
from fluxfootprints import animate_footprint
animate_footprint(model, "ffp_hourly.gif", freq="hourly")
Animate the daily-mean summary on an aerial basemap:
animate_footprint(
summaries.f_daily_mean,
"ffp_daily.mp4",
freq=None, # already daily
station_lat=40.05,
station_lon=-113.55,
basemap=True,
)
Notes
Writing .mp4/.webm needs an ffmpeg binary and basemaps need
contextily; both come with the optional extra:
pip install "fluxfootprints[animation]"
ffmpeg already on the system PATH is used in preference to the copy
bundled by imageio-ffmpeg. .gif output needs neither – it only uses
Pillow, which matplotlib already depends on.
- class fluxfootprints.footprint_animation.FootprintAnimator(data: Any, var: str | None = None, freq: str | None = None, reducer: str = 'mean', normalize_each_frame: bool = False, time_slice: tuple[str, str] | None = None, station_lat: float | None = None, station_lon: float | None = None, crs_out: str | int = 'auto', basemap: bool = False, basemap_source: Any = None, basemap_zoom: int | str = 'auto', cmap: str | Colormap = 'viridis', norm: Literal['linear', 'log'] = 'linear', color_scale: Literal['global', 'per_frame'] = 'global', vmin: float | None = None, vmax: float | None = None, percentiles: tuple[float, float] = (0.0, 99.5), log_decades: float = 4.0, alpha: float | None = None, mask_below: float | None = None, mask_quantile: float | None = None, contour_fractions: Sequence[float] | None = None, contour_color: str = 'white', contour_width: float = 0.8, title: str | None = None, timestamp_format: str | None = None, timestamp_loc: str | tuple[float, float] = 'upper left', timestamp_kwargs: dict[str, Any] | None = None, annotation_fn: Callable[[Timestamp, ndarray], str] | None = None, show_colorbar: bool = True, cbar_label: str = 'Flux contribution (m$^{-2}$)', show_tower: bool = True, figsize: tuple[float, float] = (8.0, 7.0), dpi: int = 120, fps: int = 5, logger: Logger | None = None)[source]
Bases:
objectBuild an animation from a time-resolved footprint raster.
The animator resolves the input to a
(time, y, x)raster stack, optionally aggregates it to hourly/daily/monthly steps, places it in a projected CRS when station coordinates are supplied, and renders one frame per time step with a timestamp label.- Parameters:
- dataxarray.DataArray, xarray.Dataset, or footprint model
Source of the footprint time series. Models are read through
get_footprint_timeseries(); datasets needvarif they hold more than one time-resolved 2-D variable.- varstr, optional
Variable name when
datais axarray.Dataset.- freq{“hourly”, “daily”, “monthly”, “native”} or str or None, optional
Time step of the output animation.
None/"native"keeps the input time steps; any pandas offset alias also works.- reducerstr, optional
Aggregation applied when resampling; see
resample_footprints().- normalize_each_framebool, optional
Rescale every frame to sum to one before plotting.
- time_slicetuple of str, optional
(start, stop)labels used to subset the time axis before animating.- station_lat, station_lonfloat, optional
Tower position in WGS84. Both are required to georeference the raster (and therefore to draw a basemap); without them the axes are plotted in metres relative to the tower.
- crs_outstr or int, optional
Target CRS for georeferenced output.
"auto"picks the local UTM zone. Must be a projected CRS.- basemapbool, optional
Draw a web-map basemap under the raster. Requires
contextilyand station coordinates.- basemap_sourceoptional
A
contextilytile provider (defaults to thecontextilydefault).- basemap_zoomint or str, optional
Tile zoom level passed to
contextily.add_basemap.- cmapstr or matplotlib.colors.Colormap, optional
Colormap for the footprint density.
- norm{“linear”, “log”}, optional
Color scaling of the density values.
- color_scale{“global”, “per_frame”}, optional
Whether the color limits are fixed across the whole animation (comparable frames) or recomputed per frame (maximum contrast).
- vmin, vmaxfloat, optional
Explicit color limits; override
percentiles.- percentilestuple of float, optional
Percentiles of the finite, positive data used to derive color limits.
- log_decadesfloat, optional
Dynamic range of the log color scale, in decades below
vmax. Only used whennorm="log"andvminis not given: footprint fields trail off to arbitrarily small densities, so the lowest percentile is a useless floor for a log scale.- alphafloat, optional
Raster opacity. Defaults to 0.75 when a basemap is drawn, else 1.0.
- mask_belowfloat, optional
Densities at or below this absolute value are drawn transparent.
- mask_quantilefloat, optional
Same as
mask_belowbut expressed as a quantile (0-1) of the finite, positive densities.- contour_fractionssequence of float, optional
Source-area fractions (e.g.
(0.5, 0.8)) contoured on each frame.- contour_color, contour_widthoptional
Styling for those contours.
- titlestr, optional
Static figure title drawn above the axes.
- timestamp_formatstr, optional
strftimepattern for the per-frame label; defaults followfreq.- timestamp_locstr or tuple, optional
One of the
"upper left"-style keys, or an(x, y)pair in axes fractions.- timestamp_kwargsdict, optional
Extra keyword arguments forwarded to
matplotlib.axes.Axes.text().- annotation_fncallable, optional
f(timestamp, frame_array) -> strreturning a second line of text appended under the timestamp.- show_colorbarbool, optional
Draw a colorbar for the density.
- cbar_labelstr, optional
Colorbar label.
- show_towerbool, optional
Mark the tower position.
- figsizetuple of float, optional
Figure size in inches.
- dpiint, optional
Figure and output resolution.
- fpsint, optional
Frames per second of the saved animation.
- loggerlogging.Logger, optional
Logger used for progress messages.
- Attributes:
- framesxarray.DataArray
The prepared
(time, y, x)stack that will be animated.- timespandas.DatetimeIndex
Timestamps of the frames.
- extenttuple of float
(left, right, bottom, top)of the raster in plot coordinates.- crspyproj.CRS or None
CRS of the plot coordinates, or
Nonewhen plotting in local metres.
Methods
build()Create the figure and its
FuncAnimation.from_summary(summaries[, layer])Build an animator from a
SummaryResult.save(path[, writer, frames_dir])Render the animation to a video or GIF.
- build() tuple[Figure, FuncAnimation][source]
Create the figure and its
FuncAnimation.- Returns:
- figmatplotlib.figure.Figure
The figure being animated. Close it when finished.
- animmatplotlib.animation.FuncAnimation
The animation, ready to save or embed (
anim.to_jshtml()).
- classmethod from_summary(summaries: Any, layer: str = 'daily_mean', **kwargs: Any) FootprintAnimator[source]
Build an animator from a
SummaryResult.- Parameters:
- summariesSummaryResult
Output of
fluxfootprints.summarize_periods().- layer{“daily_mean”, “monthly_mean”, “daily_etw”, “monthly_etw”}
Which summary raster to animate. The timestamp format follows the layer’s frequency unless overridden in
kwargs.- **kwargs
Forwarded to
FootprintAnimator.
- Returns:
- FootprintAnimator
- save(path: str | Path, writer: str | Any | None = None, frames_dir: str | Path | None = None, **writer_kwargs: Any) Path[source]
Render the animation to a video or GIF.
- Parameters:
- pathstr or pathlib.Path
Output file. The suffix selects the writer:
.mp4/.mov/.m4v/.webmuse ffmpeg,.gifuses Pillow,.htmlwrites an embedded HTML player.- writerstr or matplotlib writer, optional
Override the writer chosen from the suffix.
- frames_dirstr or pathlib.Path, optional
If given, also write each frame as a numbered PNG into this directory.
- **writer_kwargs
Extra keyword arguments passed to the writer (e.g.
bitrate=2400,codec="libx264").
- Returns:
- pathlib.Path
The path that was written.
- fluxfootprints.footprint_animation.animate_footprint(data: Any, path: str | Path, freq: str | None = None, **kwargs: Any) Path[source]
Render a footprint time series to a video or GIF in one call.
- Parameters:
- dataxarray.DataArray, xarray.Dataset, or footprint model
Source of the time-resolved footprint raster.
- pathstr or pathlib.Path
Output file; the suffix selects the format (
.mp4,.gif, …).- freq{“hourly”, “daily”, “monthly”, “native”} or None, optional
Time step of the animation.
- **kwargs
Any other
FootprintAnimatorargument, pluswriter,frames_dir, and writer options which are forwarded toFootprintAnimator.save().
- Returns:
- pathlib.Path
The path that was written.
Examples
>>> animate_footprint(model, "daily.gif", freq="daily") PosixPath('daily.gif')
- fluxfootprints.footprint_animation.ensure_ffmpeg() str | None[source]
Locate an ffmpeg binary for matplotlib’s video writers.
Prefers whatever matplotlib is already configured to use (normally
ffmpegon the systemPATH) and otherwise falls back to the binary shipped with the optionalimageio-ffmpegpackage, pointing matplotlib at it soFFMpegWriterbecomes usable.- Returns:
- str or None
Path to a working ffmpeg executable, or
Noneif none was found.
- fluxfootprints.footprint_animation.resample_footprints(da: DataArray, freq: str | None = None, reducer: str = 'mean', normalize_each_frame: bool = False) DataArray[source]
Aggregate a footprint time series onto a coarser time step.
- Parameters:
- daxarray.DataArray
Footprint densities with a
timedimension plusxandy.- freqstr or None, optional
Target time step; see
resolve_freq().Nonekeeps the native time steps.- reducer{“mean”, “sum”, “median”, “max”}, optional
Reduction applied within each period.
"mean"is appropriate for normalized footprint densities.- normalize_each_framebool, optional
If
True, rescale each resulting frame so it sums to one. Useful when comparing footprint shape rather than magnitude.
- Returns:
- xarray.DataArray
The aggregated series, with all-NaN periods dropped.
- fluxfootprints.footprint_animation.resolve_freq(freq: str | None) str | None[source]
Translate a friendly time-step name to a pandas offset alias.
- Parameters:
- freqstr or None
"hourly","daily","monthly","native"/None, or any pandas offset alias (e.g."3h","7D") which is passed through.
- Returns:
- str or None
The pandas offset alias, or
Noneto keep the native time steps.
- Raises:
- ValueError
If
freqis not a string orNone.
Benchmark data preparation
fluxfootprints.canonical_schema module
The canonical input contract every prepared benchmark dataset is converted to: field registry with units, allowed ranges and provenance classes, the JSON provenance sidecar, the meteorological wind-direction convention, and a validator that reports contract violations without ever converting a unit or touching the frame.
canonical_schema.py
The canonical benchmark-input contract, and a validator for it.
Every prepared validation dataset under data/validation_data/processed/ is
converted to one table format, so that tests, examples and model-comparison
scripts can consume any site without per-site column editing. This module is
the machine-readable half of that contract; the prose half is
data/validation_data/schemas/canonical_fluxfootprints_input.md, whose field
tables are generated from canonical_field_table() so the two cannot
drift.
The contract has three parts.
Fields. Nine required columns –
timestamp(carried as the frame’sDatetimeIndex),site_id, and the eightFFPModelinputsustar,sigmav,ol,wind_dir,umean,zm,z0,h– plus a fixed vocabulary of optional measurement, QC and provenance columns. Each field declares its units, meaning, allowed range and the provenance classes it may carry (CANONICAL_FIELDS).Provenance. Every value in a processed table is exactly one of
Provenance:observed,network_derived,prep_calculated,fixed_metadataorexternal_forcing. A JSON sidecar beside each processed file records the class, units, source variable and derivation method per column (write_provenance_sidecar()). Nothing may be fabricated: a derived field without a recorded method is a validation error, not a warning.Conventions.
wind_diris meteorological – the direction the wind comes from, degrees clockwise from true north, on[0, 360]. The source area therefore lies at bearingwind_dirfrom the tower, sowind_dir = 90puts it to the east. SeeWIND_DIR_CONVENTIONandcardinal_sector().
Units are checked, never converted. validate_canonical_frame() reports a
declared-unit mismatch as an error and returns; it does not rescale the column,
because a silent conversion is exactly the failure a benchmark cannot tolerate.
The validator never mutates the frame it is given.
Severity is split. Errors are contract violations: a missing column, a
non-finite value, a value outside its physical range, zm >= h, an
undeclared derivation. Warnings are rows the contract permits but
FFPModel will drop or flag – ustar <= 0.1,
h <= 10, zm <= 27.5 * z0, zm >= 0.8 * h, zm / ol < -15.5 – so a
benchmark subset can be chosen to survive the model’s own filtering
(_apply_validity_masks and check_validity_ranges) instead of being
silently thinned by it.
Examples
>>> report = validate_canonical_frame(df)
>>> report.ok
True
>>> report.raise_for_status()
- fluxfootprints.canonical_schema.CANONICAL_FIELDS: dict[str, FieldSpec] = {'air_temperature': FieldSpec(name='air_temperature', units='degC', meaning='Air temperature', kind='numeric', required=False, minimum=-90.0, maximum=60.0, min_inclusive=True, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.NETWORK_DERIVED: 'network_derived'>}), note='degC, not K: a median above 150 is a unit error'), 'canopy_height': FieldSpec(name='canopy_height', units='m', meaning='Canopy height, h_c', kind='numeric', required=False, minimum=0.0, maximum=120.0, min_inclusive=True, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.FIXED_METADATA: 'fixed_metadata'>}), note=''), 'co2_flux': FieldSpec(name='co2_flux', units='umol m-2 s-1', meaning='CO2 flux, FC', kind='numeric', required=False, minimum=-100.0, maximum=100.0, min_inclusive=True, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.NETWORK_DERIVED: 'network_derived'>}), note=''), 'displacement_height': FieldSpec(name='displacement_height', units='m', meaning='Zero-plane displacement height, d', kind='numeric', required=False, minimum=0.0, maximum=100.0, min_inclusive=True, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.FIXED_METADATA: 'fixed_metadata'>}), note=''), 'h': FieldSpec(name='h', units='m', meaning='Boundary-layer height', kind='numeric', required=True, minimum=0.0, maximum=5000.0, min_inclusive=False, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.EXTERNAL_FORCING: 'external_forcing'>, <Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.NETWORK_DERIVED: 'network_derived'>}), note="build_climatology's 2000 m default and NEON's constant 1000 m are placeholders, not observations"), 'h2o_flux': FieldSpec(name='h2o_flux', units='mmol m-2 s-1', meaning='H2O flux, FH2O', kind='numeric', required=False, minimum=-50.0, maximum=50.0, min_inclusive=True, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.NETWORK_DERIVED: 'network_derived'>}), note=''), 'latent_heat': FieldSpec(name='latent_heat', units='W m-2', meaning='Latent heat flux, LE', kind='numeric', required=False, minimum=-500.0, maximum=1500.0, min_inclusive=True, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.NETWORK_DERIVED: 'network_derived'>}), note=''), 'license': FieldSpec(name='license', units='', meaning='License or use policy of the source', kind='string', required=False, minimum=None, maximum=None, min_inclusive=False, max_inclusive=True, provenance=frozenset({<Provenance.FIXED_METADATA: 'fixed_metadata'>}), note=''), 'measurement_height': FieldSpec(name='measurement_height', units='m', meaning='Sensor height above ground, z_EC', kind='numeric', required=False, minimum=0.0, maximum=200.0, min_inclusive=False, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.FIXED_METADATA: 'fixed_metadata'>}), note=''), 'ol': FieldSpec(name='ol', units='m', meaning='Obukhov length; negative unstable, positive stable', kind='numeric', required=True, minimum=-100000.0, maximum=100000.0, min_inclusive=True, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.NETWORK_DERIVED: 'network_derived'>}), note='Must be non-zero; ol = zm / ZL must reuse the same zm the model is given'), 'prep_qc': FieldSpec(name='prep_qc', units='', meaning='QA decisions made during preparation', kind='string', required=False, minimum=None, maximum=None, min_inclusive=False, max_inclusive=True, provenance=frozenset({<Provenance.PREP_CALCULATED: 'prep_calculated'>}), note=''), 'pressure': FieldSpec(name='pressure', units='kPa', meaning='Atmospheric pressure', kind='numeric', required=False, minimum=30.0, maximum=115.0, min_inclusive=True, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.NETWORK_DERIVED: 'network_derived'>}), note='kPa, not Pa or hPa'), 'sensible_heat': FieldSpec(name='sensible_heat', units='W m-2', meaning='Sensible heat flux, H', kind='numeric', required=False, minimum=-500.0, maximum=1500.0, min_inclusive=True, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.NETWORK_DERIVED: 'network_derived'>}), note=''), 'sigmav': FieldSpec(name='sigmav', units='m s-1', meaning='Standard deviation of the lateral wind velocity', kind='numeric', required=True, minimum=0.0, maximum=10.0, min_inclusive=False, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.NETWORK_DERIVED: 'network_derived'>}), note='predict_sigmav output is prep_calculated, never observed'), 'site_id': FieldSpec(name='site_id', units='', meaning='AmeriFlux/NEON site identifier, e.g. US-Var', kind='string', required=True, minimum=None, maximum=None, min_inclusive=False, max_inclusive=True, provenance=frozenset({<Provenance.FIXED_METADATA: 'fixed_metadata'>}), note=''), 'sonic_temperature': FieldSpec(name='sonic_temperature', units='degC', meaning='Sonic temperature', kind='numeric', required=False, minimum=-90.0, maximum=60.0, min_inclusive=True, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.NETWORK_DERIVED: 'network_derived'>}), note=''), 'source_dataset_version': FieldSpec(name='source_dataset_version', units='', meaning='Publisher version string, e.g. AmeriFlux 26-5', kind='string', required=False, minimum=None, maximum=None, min_inclusive=False, max_inclusive=True, provenance=frozenset({<Provenance.FIXED_METADATA: 'fixed_metadata'>}), note=''), 'source_doi': FieldSpec(name='source_doi', units='', meaning='DOI of the source product', kind='string', required=False, minimum=None, maximum=None, min_inclusive=False, max_inclusive=True, provenance=frozenset({<Provenance.FIXED_METADATA: 'fixed_metadata'>}), note=''), 'source_file': FieldSpec(name='source_file', units='', meaning='File under raw/ each row came from', kind='string', required=False, minimum=None, maximum=None, min_inclusive=False, max_inclusive=True, provenance=frozenset({<Provenance.FIXED_METADATA: 'fixed_metadata'>}), note=''), 'source_qc': FieldSpec(name='source_qc', units='', meaning='Source QA/QC flags, unmodified', kind='string', required=False, minimum=None, maximum=None, min_inclusive=False, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.NETWORK_DERIVED: 'network_derived'>}), note=''), 'source_timestamp': FieldSpec(name='source_timestamp', units='', meaning='Timestamp exactly as published, unparsed', kind='string', required=False, minimum=None, maximum=None, min_inclusive=False, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>}), note=''), 'source_url': FieldSpec(name='source_url', units='', meaning='Retrieval URL', kind='string', required=False, minimum=None, maximum=None, min_inclusive=False, max_inclusive=True, provenance=frozenset({<Provenance.FIXED_METADATA: 'fixed_metadata'>}), note=''), 'timestamp': FieldSpec(name='timestamp', units='', meaning="Interval timestamp, carried as the frame's DatetimeIndex, which is what FFPModel turns into the time coordinate", kind='datetime', required=True, minimum=None, maximum=None, min_inclusive=False, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.PREP_CALCULATED: 'prep_calculated'>}), note='Timezone-aware, or the convention documented per site under metadata/'), 'umean': FieldSpec(name='umean', units='m s-1', meaning='Mean horizontal wind speed at the measurement height', kind='numeric', required=True, minimum=0.0, maximum=60.0, min_inclusive=True, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.NETWORK_DERIVED: 'network_derived'>}), note=''), 'ustar': FieldSpec(name='ustar', units='m s-1', meaning='Friction velocity', kind='numeric', required=True, minimum=0.0, maximum=5.0, min_inclusive=False, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.NETWORK_DERIVED: 'network_derived'>}), note='FFPModel NaNs and drops rows with ustar <= 0.1'), 'variable_provenance': FieldSpec(name='variable_provenance', units='', meaning="Per-variable provenance, inline as 'ol=network_derived;z0=prep_calculated', or a pointer to the JSON sidecar", kind='string', required=False, minimum=None, maximum=None, min_inclusive=False, max_inclusive=True, provenance=frozenset({<Provenance.PREP_CALCULATED: 'prep_calculated'>}), note=''), 'wind_dir': FieldSpec(name='wind_dir', units='degrees', meaning='Meteorological wind direction, the direction the wind comes FROM', kind='numeric', required=True, minimum=0.0, maximum=360.0, min_inclusive=True, max_inclusive=True, provenance=frozenset({<Provenance.OBSERVED: 'observed'>, <Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.NETWORK_DERIVED: 'network_derived'>}), note='Wrap before writing; FFPModel NaNs anything outside [0, 360]'), 'z0': FieldSpec(name='z0', units='m', meaning='Aerodynamic roughness length', kind='numeric', required=True, minimum=0.0, maximum=10.0, min_inclusive=False, max_inclusive=True, provenance=frozenset({<Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.NETWORK_DERIVED: 'network_derived'>, <Provenance.FIXED_METADATA: 'fixed_metadata'>}), note='z0 = 0.1 * canopy_height is a labelled fallback approximation, not an observation'), 'zm': FieldSpec(name='zm', units='m', meaning='Measurement height above the displacement height, z_EC - d', kind='numeric', required=True, minimum=0.0, maximum=200.0, min_inclusive=False, max_inclusive=True, provenance=frozenset({<Provenance.PREP_CALCULATED: 'prep_calculated'>, <Provenance.NETWORK_DERIVED: 'network_derived'>, <Provenance.FIXED_METADATA: 'fixed_metadata'>}), note='Never a nominal tower height; must be < h')}
Every canonical field, keyed by name, in contract order.
- fluxfootprints.canonical_schema.CARDINAL_SECTORS: dict[str, float] = {'E': 90.0, 'N': 0.0, 'S': 180.0, 'W': 270.0}
Cardinal sector centres used by the benchmark subsets, in degrees.
- exception fluxfootprints.canonical_schema.CanonicalSchemaError(report: ValidationReport)[source]
Bases:
ValueErrorRaised when a frame or file violates the canonical contract.
- fluxfootprints.canonical_schema.FFP_INPUT_FIELDS: tuple[str, ...] = ('ustar', 'sigmav', 'ol', 'wind_dir', 'umean', 'zm', 'z0', 'h')
The eight FFPModel inputs, in the order the model lists them.
- class fluxfootprints.canonical_schema.FieldSpec(name: str, units: str, meaning: str, kind: str = 'numeric', required: bool = False, minimum: float | None = None, maximum: float | None = None, min_inclusive: bool = False, max_inclusive: bool = True, provenance: frozenset[Provenance] = frozenset({Provenance.EXTERNAL_FORCING, Provenance.FIXED_METADATA, Provenance.NETWORK_DERIVED, Provenance.OBSERVED, Provenance.PREP_CALCULATED}), note: str = '')[source]
Bases:
objectThe contract for one canonical column.
- Parameters:
- name
Canonical column name.
- units
Canonical units in the
"m s-1"spelling;""for unitless or non-numeric fields.- meaning
One-line definition.
- kind
"numeric","string"or"datetime".- required
Whether a processed table must carry the column.
- minimum, maximum
Allowed range;
Noneis unbounded on that side.- min_inclusive, max_inclusive
Whether the bounds themselves are allowed.
- provenance
Provenance classes this field may legitimately carry.
- note
Model behaviour or preparation caveat worth carrying into the docs.
- Attributes:
- maximum
- minimum
provenance_textAllowed provenance classes, comma-separated in declaration order.
range_textThe allowed range in interval notation, e.g.
- kind: str = 'numeric'
- max_inclusive: bool = True
- maximum: float | None = None
- meaning: str
- min_inclusive: bool = False
- minimum: float | None = None
- name: str
- note: str = ''
- provenance: frozenset[Provenance] = frozenset({Provenance.EXTERNAL_FORCING, Provenance.FIXED_METADATA, Provenance.NETWORK_DERIVED, Provenance.OBSERVED, Provenance.PREP_CALCULATED})
- property provenance_text: str
Allowed provenance classes, comma-separated in declaration order.
- property range_text: str
The allowed range in interval notation, e.g.
"(0, 5]".
- required: bool = False
- units: str
- class fluxfootprints.canonical_schema.Issue(severity: str, code: str, field: str | None, message: str, n_rows: int = 0, examples: tuple[str, ...] = ())[source]
Bases:
objectOne validation finding.
- Attributes:
- severity
"error"for a contract violation,"warning"for a row the contract permits butFFPModelwill drop or flag.- code
Stable machine-readable identifier, e.g.
"out_of_range".- field
Canonical field the finding concerns, or
Nonewhen it is frame-level.- message
Human-readable explanation.
- n_rows
Number of offending rows; 0 for structural findings.
- examples
Up to five offending index labels, as strings.
- code: str
- examples: tuple[str, ...] = ()
- field: str | None
- message: str
- n_rows: int = 0
- severity: str
- fluxfootprints.canonical_schema.OPTIONAL_FIELDS: tuple[str, ...] = ('source_timestamp', 'measurement_height', 'displacement_height', 'canopy_height', 'sensible_heat', 'latent_heat', 'air_temperature', 'sonic_temperature', 'pressure', 'co2_flux', 'h2o_flux', 'source_qc', 'prep_qc', 'source_file', 'source_dataset_version', 'source_url', 'source_doi', 'license', 'variable_provenance')
The recommended additional columns.
- fluxfootprints.canonical_schema.PROVENANCE_SCHEMA_VERSION = '1.0'
Version of the provenance-sidecar layout written by
write_provenance_sidecar(). Bump on any breaking key change.
- class fluxfootprints.canonical_schema.Provenance(*values)[source]
Bases:
str,EnumHow a value in a canonical table was obtained.
Exactly one class applies to each column of a processed table.
- EXTERNAL_FORCING = 'external_forcing'
Taken from a different dataset, e.g. a reanalysis boundary-layer height. Requires
methodin the sidecar.
- FIXED_METADATA = 'fixed_metadata'
A site constant from BADM/BIF or site documentation.
- NETWORK_DERIVED = 'network_derived'
Computed by the publisher from its own observations (AmeriFlux
MO_LENGTH,ZL; NEONdistObkv).
- OBSERVED = 'observed'
Measured and published by the source network.
- PREP_CALCULATED = 'prep_calculated'
Computed during preparation, from documented inputs by a documented formula. Requires
methodin the sidecar.
- fluxfootprints.canonical_schema.REQUIRED_FIELDS: tuple[str, ...] = ('timestamp', 'site_id', 'ustar', 'sigmav', 'ol', 'wind_dir', 'umean', 'zm', 'z0', 'h')
The nine columns a processed table must carry.
- class fluxfootprints.canonical_schema.ValidationReport(issues: list[Issue] = <factory>, n_rows: int = 0, fields_present: tuple[str, ...]=())[source]
Bases:
objectThe outcome of validating one frame.
- Attributes:
- issues
Every finding, errors and warnings alike, in the order found.
- n_rows
Rows in the validated frame.
- fields_present
Canonical fields the frame actually carried.
Methods
codes_for(severity)The set of codes present at one severity.
fields_with(code)Fields carrying a given code.
Raise
CanonicalSchemaErrorif there is any error.summary()Multi-line summary: the verdict line, then one line per finding.
to_frame()Findings as a table, one row per issue.
- property codes: set[str]
The set of codes present, for concise assertions in tests.
- fields_present: tuple[str, ...] = ()
- n_rows: int = 0
- property ok: bool
True when there are no errors. Warnings do not affect this.
- raise_for_status() ValidationReport[source]
Raise
CanonicalSchemaErrorif there is any error.- Returns:
- ValidationReport
self, so calls can be chained.
- fluxfootprints.canonical_schema.WIND_DIR_CONVENTION = 'meteorological: wind_dir is the direction the wind comes FROM, in degrees clockwise from true north, on [0, 360]; the footprint source area lies upwind, at bearing wind_dir from the tower'
The wind-direction convention, stated once so scripts can quote it verbatim.
- fluxfootprints.canonical_schema.angular_difference(a: Any, b: Any) Any[source]
Signed smallest angle from
btoa, in degrees on[-180, 180).A circular difference, for comparing a modelled footprint bearing against an expected one without a wrap-around fault near 0/360.
- Parameters:
- a, b
Bearings in degrees.
- Returns:
- Signed difference
a - b, wrapped. An exact reversal maps to -180, - the closed end of the interval; compare magnitudes when only the size of
- the error matters.
- Signed difference
- fluxfootprints.canonical_schema.assert_canonical(df: DataFrame, **kwargs: Any) ValidationReport[source]
Validate a frame and raise on any error.
Convenience wrapper around
validate_canonical_frame(...).raise_for_status()for preparation scripts, which should refuse to write a non-conforming table.- Parameters:
- df
Candidate processed table.
- **kwargs
Passed to
validate_canonical_frame().
- Returns:
- ValidationReport
- Raises:
- CanonicalSchemaError
- fluxfootprints.canonical_schema.canonical_field_table() DataFrame[source]
Return the field registry as a table, one row per canonical field.
The Markdown tables in
schemas/canonical_fluxfootprints_input.mdare generated from this, so the documentation and the validator cannot disagree.- Returns:
- pandas.DataFrame
Columns
field, units, meaning, kind, required, allowed_range, provenance, note.
- fluxfootprints.canonical_schema.cardinal_sector(wind_dir: Any, tolerance: float = 10.0) Any[source]
Label bearings that fall within
toleranceof a cardinal direction.Used to build the N/E/S/W benchmark subsets. Bearings outside every sector give
None.- Parameters:
- wind_dir
Meteorological wind direction, degrees.
- tolerance
Half-width of each sector, degrees. The default 10 gives the 350-10 / 80-100 / 170-190 / 260-280 windows the validation context asks for.
- Returns:
"N","E","S","W"orNone, elementwise.
Examples
>>> cardinal_sector(355.0) 'N' >>> cardinal_sector(45.0) is None True
- fluxfootprints.canonical_schema.format_variable_provenance(mapping: Mapping[str, Any]) str[source]
Inverse of
parse_variable_provenance(), in field-registry order.- Parameters:
- mapping
Field name ->
Provenancemember or its string value.
- Returns:
- str
The inline spelling for a
variable_provenancecolumn.
- fluxfootprints.canonical_schema.load_provenance_sidecar(data_path: str | Path) dict[str, Any][source]
Read the JSON sidecar belonging to a processed file.
- Parameters:
- data_path
The processed data file, not the sidecar itself.
- Raises:
- FileNotFoundError
If no sidecar exists beside
data_path.
- fluxfootprints.canonical_schema.normalize_units(units: str | None) str[source]
Normalize a unit spelling to its canonical form.
Recognizes spelling variants only (
"m/s"->"m s-1"). It never maps between physically different units:"Pa"does not become"kPa".- Parameters:
- units
Declared unit string, or
None.
- Returns:
- str
The canonical spelling, or the input with whitespace collapsed when the spelling is not recognized.
- fluxfootprints.canonical_schema.parse_variable_provenance(text: str) dict[str, str][source]
Parse the inline
variable_provenancespelling into a mapping.- Parameters:
- text
e.g.
"ol=network_derived;z0=prep_calculated;h=external_forcing".
- Returns:
- dict
Field name -> provenance class string. Unparseable items are skipped.
Examples
>>> parse_variable_provenance("ol=network_derived;z0=prep_calculated") {'ol': 'network_derived', 'z0': 'prep_calculated'}
- fluxfootprints.canonical_schema.provenance_sidecar_path(data_path: str | Path) Path[source]
The sidecar path for a processed data file.
processed/US-Var/US-Var_HH.parquet->processed/US-Var/US-Var_HH.provenance.json.
- fluxfootprints.canonical_schema.read_canonical(path: str | Path) DataFrame[source]
Read a processed canonical file into a frame with a
DatetimeIndex.Supports
.parquet/.pqand.csv. Atimestampcolumn, when present, becomes the index; timestamps are parsed but never re-zoned.- Parameters:
- path
Processed file under
processed/.
- Returns:
- pandas.DataFrame
- fluxfootprints.canonical_schema.validate_canonical_file(path: str | Path, *, require_tz: bool = True, require_provenance: bool = True, strict_columns: bool = False) ValidationReport[source]
Read a processed file with its sidecar, and validate both.
- Parameters:
- path
Processed data file. Its sidecar is read from
provenance_sidecar_path()when one exists.- require_tz, require_provenance, strict_columns
Passed through to
validate_canonical_frame(). Provenance is required here by default, since a file inprocessed/is a deliverable rather than an intermediate frame.
- Returns:
- ValidationReport
- fluxfootprints.canonical_schema.validate_canonical_frame(df: DataFrame, *, provenance: Mapping[str, Any] | str | Path | None = None, require_tz: bool = True, require_provenance: bool = False, strict_columns: bool = False) ValidationReport[source]
Validate a DataFrame against the canonical benchmark contract.
The frame is never modified and no value is ever converted: a unit problem is reported, not repaired.
- Parameters:
- df
Candidate processed table, timestamps carried in a
DatetimeIndex.- provenance
A loaded sidecar mapping, a path to a processed data file whose sidecar should be read, or
Noneto skip the provenance checks.- require_tz
Treat a timezone-naive index as an error (the default). Set
Falseonly when the timestamp convention is documented inmetadata/<site>/; the finding is then downgraded to a warning.- require_provenance
Error when no provenance is supplied at all. A file destined for
processed/should be validated with this on.- strict_columns
Warn about columns outside the canonical vocabulary. Off by default, since retaining raw source columns is encouraged.
- Returns:
- ValidationReport
Findings, with
ValidationReport.okFalse if any is an error.
Examples
>>> validate_canonical_frame(df).raise_for_status()
- fluxfootprints.canonical_schema.wrap_degrees(values: Any) Any[source]
Wrap bearings onto
[0, 360).- Parameters:
- values
Scalar, array or Series of degrees, possibly negative or above 360.
- Returns:
- Same type as the input, wrapped. NaN is preserved.
- fluxfootprints.canonical_schema.write_provenance_sidecar(data_path: str | Path, *, site_id: str, fields: Mapping[str, Mapping[str, Any]], timestamp_convention: str, source: Mapping[str, Any] | None = None, notes: str | None = None) Path[source]
Write the JSON provenance sidecar for a processed file.
- Parameters:
- data_path
The processed data file the sidecar describes.
- site_id
AmeriFlux/NEON site ID.
- fields
Per-column records. Each needs
provenance(aProvenancemember or its value) andunits;prep_calculatedandexternal_forcingalso needmethod, andobservedandnetwork_derivedneedsource_variable. Extra keys (source_file,notes) are kept as given.- timestamp_convention
e.g.
"UTC, start-of-interval, 30 min". Never left implicit.- source
Dataset-level provenance:
dataset,url,doi,version,license,retrieved_utc.- notes
Free-text caveats.
- Returns:
- pathlib.Path
The sidecar path written.
Meteorological data retrieval
fluxfootprints.nldas_read_functions module
- fluxfootprints.nldas_read_functions.call_nldas_time_series(lat, lon, time_start, time_end, data, token)[source]
INPUTS: lat - latitude lon - longitude time_start - start of time series in YYYY-MM-DDThh:mm:ss format (UTC) end_time - end of the time series in YYYY-MM-DDThh:mm:ss format (UTC) data - name of the data parameter for the time series token - earthaccess bearer token
OUTPUT: time series csv output string
Function modified from How_to_Access_GiC_Time_Series_Service.ipynb https://github.com/nasa/gesdisc-tutorials/tree/main/notebooks
- fluxfootprints.nldas_read_functions.fetch_nldas_forcing_dataset(lat, lon, time_start, time_end, token)[source]
Fetches all NLDAS forcing variables required for ASCE ET_o calculation
and combines them into a single pandas DataFrame.
- fluxfootprints.nldas_read_functions.parse_nldas_csv(ts)[source]
INPUTS: ts - time series output of the time series service
OUTPUTS: headers,df - the headers from the CSV as a dict and the values in a pandas dataframe Function modified from How_to_Access_GiC_Time_Series_Service.ipynb https://github.com/nasa/gesdisc-tutorials/tree/main/notebooks
Module contents
- class fluxfootprints.BackwardLSModel(cfg: LSFootprintConfig)[source]
Bases:
objectA 3‑D backward Lagrangian stochastic footprint model.
Methods
Return upstream distance array and integrated footprint f(x).
Return (x_centres, y_centres, footprint_density).
run()Run the LS footprint simulation and accumulate 2‑D footprint.
- crosswind_integrated() Tuple[ndarray, ndarray][source]
Return upstream distance array and integrated footprint f(x).
- class fluxfootprints.BaseFootprintModel(df: DataFrame, domain: list | None = None, dx: float = 10.0, dy: float = 10.0, rs: list | None = None, smooth_data: bool = True, verbosity: int = 2, logger: Logger | None = None, **kwargs)[source]
Bases:
ABCAbstract base class for flux footprint models.
All footprint model implementations should inherit from this class and implement the required methods to ensure API consistency.
- Attributes:
- dfpandas.DataFrame
Input meteorological data
- domainlist
Spatial domain [xmin, xmax, ymin, ymax]
- dx, dyfloat
Grid resolution in x and y directions
- rslist
Source area fractions to compute
- loggerlogging.Logger
Logger instance
- x, ynumpy.ndarray
Grid coordinate arrays
- f_2dxarray.DataArray or None
Time-resolved 2D footprint (time, x, y)
- fclim_2dxarray.DataArray or None
Climatological 2D footprint (x, y)
Methods
Return x and y coordinate arrays.
Return the climatological footprint as xarray DataArray.
Return time-resolved footprint if available.
Return complete results as xarray Dataset.
run([return_result])Execute footprint calculation.
to_netcdf(filepath)Save results to netCDF file.
- REQUIRED_COLUMNS = []
- get_coordinates() tuple[ndarray, ndarray][source]
Return x and y coordinate arrays.
- Returns:
- x, ynumpy.ndarray
1D coordinate arrays in meters
- get_footprint_climatology() DataArray[source]
Return the climatological footprint as xarray DataArray.
- Returns:
- xarray.DataArray
2D footprint climatology with dims (x, y)
- get_footprint_timeseries() DataArray | None[source]
Return time-resolved footprint if available.
- Returns:
- xarray.DataArray or None
3D footprint with dims (time, x, y) if available
- get_results() Dataset[source]
Return complete results as xarray Dataset.
- Returns:
- xarray.Dataset
Dataset containing all footprint outputs and metadata
- abstractmethod run(return_result: bool = True) Dataset | None[source]
Execute footprint calculation.
- Parameters:
- return_resultbool
If True, return xarray Dataset with results
- Returns:
- xarray.Dataset or None
Dataset containing footprint_climatology (x, y), optionally footprint_2d (time, x, y), and metadata
- exception fluxfootprints.CanonicalSchemaError(report: ValidationReport)[source]
Bases:
ValueErrorRaised when a frame or file violates the canonical contract.
- class fluxfootprints.CategoricalResult(radius: float, dominant_class: Any, p_footprint: float, p_target: float, chi2: float, p_value: float, dof: int, level: Level, footprint_composition: Mapping[Any, float], target_composition: Mapping[Any, float])[source]
Bases:
objectLand-cover representativeness for one target-area radius (Sect. 2.4).
- Attributes:
- radiusfloat
Target-area radius from the tower [m].
- dominant_classAny
Land-cover code holding the largest footprint-weighted share. Keys are whatever the classification raster carries, typically
intNLCD codes.- p_footprintfloat
Percentage of dominant_class within the footprint, footprint-weighted (P_footprint in the paper) [%].
- p_targetfloat
Percentage of the same class within the target area (P_target) [%].
- chi2float
Chi-square statistic comparing the two compositions.
- p_valuefloat
p-value of the chi-square test. Values >= 0.05 indicate the compositions are not significantly different.
- dofint
Degrees of freedom of the chi-square test.
- levelLevel
Three-level representativeness index from
classify_categorical().- footprint_compositionMapping
Full footprint-weighted composition, class code -> percentage [%].
- target_compositionMapping
Full target-area composition, class code -> percentage [%].
References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Sect. 2.4.
Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> f = np.exp(-((X - 40.0) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) >>> f /= f.sum() * 400.0 >>> fclim = xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> landcover = xr.DataArray( ... np.where(X < 60.0, 42, 81), dims=("x", "y"), coords={"x": x, "y": y} ... ) >>> result = evaluate_landcover(fclim, landcover, radii=(250.0,))[0] >>> result.radius, result.dominant_class, result.dof (250.0, 42, 1) >>> round(result.p_footprint, 2), round(result.p_target, 2) (55.88, 62.59) >>> result.level <Level.LOW: 'low'>
- chi2: float
- dof: int
- dominant_class: Any
- footprint_composition: Mapping[Any, float]
- p_footprint: float
- p_target: float
- p_value: float
- radius: float
- target_composition: Mapping[Any, float]
- class fluxfootprints.ClimatologyMetrics(fraction: float, contour_level: float, fetch: float, area: float, symmetry: float, enclosed_fraction: float, n_cells: int, seasonal_overlap: float | None = None, daynight_overlap: float | None = None)[source]
Bases:
objectSummary metrics of a truncated footprint climatology (Sect. 2.2).
- Attributes:
- fractionfloat
Source-weight fraction at which the climatology was truncated, e.g. 0.8 for the 80 % contour.
- contour_levelfloat
Footprint density [m⁻²] enclosing fraction of the total source weight; usable directly as a
levelsargument toax.contour.nanfor an already-truncated climatology that no longer carries the level it was cut at, such as one sliced out of the stack thatmonthly_climatologies()returns.- fetchfloat
X80, the maximum distance from the tower to the truncation contour [m].
- areafloat
A80, the area enclosed by the truncation contour [m²].
- symmetryfloat
S80 = A80 / (pi * X80²), Eq. 1. Ranges 0-1; 1 is a perfectly circular climatology centred on the tower, and below
ASYMMETRY_THRESHOLD(0.30) the paper calls the climatology relatively asymmetric.- enclosed_fractionfloat
Source weight actually enclosed by the contour, relative to the total mass on the domain. Falls below fraction when footprint mass leaves the domain.
- n_cellsint
Number of grid cells inside the contour.
- seasonal_overlapfloat or None
O80_season, Eq. 2, the geometric-mean overlap of monthly climatologies across a site-year.
Nonewhen a single climatology was supplied.- daynight_overlapfloat or None
O80_daynight, Eq. 3, the mean overlap of paired daytime and nighttime climatologies.
Nonewhen no pairing was supplied.
References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Sect. 2.2.
Examples
>>> metrics = ClimatologyMetrics( ... fraction=0.8, ... contour_level=5.351e-06, ... fetch=181.11, ... area=64400.0, ... symmetry=0.625, ... enclosed_fraction=0.8188, ... n_cells=161, ... ) >>> metrics.fetch, metrics.area (181.11, 64400.0) >>> metrics.seasonal_overlap is None # not supplied True
- area: float
- contour_level: float
- daynight_overlap: float | None = None
- enclosed_fraction: float
- fetch: float
- fraction: float
- n_cells: int
- seasonal_overlap: float | None = None
- symmetry: float
- class fluxfootprints.ContinuousResult(radius: float, intercept: float, slope: float, r_squared: float, p_value: float, rmse: float, mae: float, n: int, level: Level | None, bias: ndarray, within_threshold: float)[source]
Bases:
objectContinuous-field representativeness for one target-area radius (Sect. 2.4).
Holds the site-level model II regression of target-area against footprint-weighted values (Eq. 7), reported as in Table 1 of the paper, together with the per-period sensor location biases (Eq. 6).
- Attributes:
- radiusfloat
Target-area radius from the tower [m].
- interceptfloat
Regression intercept, beta_0.
- slopefloat
Regression slope, beta_1. A slope below 1 means the footprint saw systematically higher values than the target area.
- r_squaredfloat
Coefficient of determination.
- p_valuefloat
Significance of the regression.
- rmsefloat
Root mean square error between footprint-weighted and target-area values.
- maefloat
Mean absolute error between footprint-weighted and target-area values.
- nint
Number of matched footprint / field periods entering the regression.
- levelLevel or None
Three-level representativeness index from
classify_continuous(), or None where fewer than three periods survived at this radius and no regression could be fitted.- biasnumpy.ndarray
Per-period sensor location bias Delta (Eq. 6), as a fraction rather than a percentage. Length n.
- within_thresholdfloat
Fraction of periods with
|Delta| <=the bias threshold [-].
References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Sect. 2.4. Schmid, H. P., and Lloyd, C. R. (1999). Agric. For. Meteorol., 93, 195-209.
Examples
>>> import numpy as np, pandas as pd, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> def clim(cx): ... f = np.exp(-((X - cx) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) ... f /= f.sum() * 400.0 ... return xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> clims = [clim(40.0 + 8 * i) for i in range(8)] >>> scenes = [ ... xr.DataArray( ... 0.30 + 0.0005 * X + 0.01 * i, dims=("x", "y"), coords={"x": x, "y": y} ... ) ... for i in range(8) ... ] >>> result = evaluate_vegetation_index(clims, scenes, radii=(250.0,))[0] >>> result.radius, result.n (250.0, 8) >>> round(result.slope, 4), round(result.intercept, 4), round(result.r_squared, 4) (0.7187, 0.0699, 0.9999) >>> result.level <Level.MEDIUM: 'medium'> >>> result.bias.shape # one sensor location bias per matched period (8,)
- bias: ndarray
- intercept: float
- mae: float
- n: int
- p_value: float
- r_squared: float
- radius: float
- rmse: float
- slope: float
- within_threshold: float
- class fluxfootprints.FFPModel(df: DataFrame, domain: list | None = None, dx: float = 10.0, dy: float = 10.0, nx: int = 1000, ny: int = 1000, rs: list | None = None, rslayer: bool = False, smooth_data: bool = True, crop: bool = False, verbosity: int = 2, logger=None, **kwargs)[source]
Bases:
BaseFootprintModelFlux Footprint Prediction (FFP) model based on Kljun et al. (2015).
This class implements an improved version of the flux footprint model for determining the source area of scalar fluxes measured by eddy covariance towers. It uses atmospheric stability, wind parameters, and terrain configurations to calculate spatial footprints.
- Attributes:
- dfpandas.DataFrame
Copy of the input DataFrame used for modeling.
- domainlist of float
Spatial domain defined as [xmin, xmax, ymin, ymax].
- fclim_2dxarray.DataArray or None
Final footprint climatology after all processing.
- f_2dxarray.DataArray or None
Instantaneous 2D footprint estimate.
- loggerlogging.Logger
Logger instance for model-level debugging.
Methods
Apply 3x3 smoothing from Kljun et al. paper with mass conservation.
Apply roughness sublayer (RSL) corrections to crosswind dispersion.
calc_2d_footprint(f_ci_dummy, sigy_dummy)Calculate the two-dimensional footprint distribution f(x,y).
calc_crosswind_extent(r, x_ru, x_rd)Calculate crosswind extent of source area using parameterized relationships.
Calculate crosswind-integrated footprint extent for relative contribution r.
Calculate the scaled crosswind-integrated footprint F̂y*(X̂*).
Calculate the standard deviation of cross-wind spread \(\sigma_y\).
Calculate upwind and downwind distances from the peak location for fraction r.
Calculate the dimensionless wind-speed group Π4 = u(zm)/(u* k) = ln(zm/z0) - ψM, following Kljun et al. (2015).
calc_real_footprint_peak(zm, h[, u_zm, ...])Calculate the real-scale footprint peak location.
Calculate scaled distance X* with roughness sublayer correction.
Calculate the scaled footprint peak location (X*max).
Calculate the scaled distance X* based on wind and height parameters.
calc_unstable_psi(stability_param)Calculate ψM for unstable atmospheric conditions.
Compute the flux footprint using xarray operations.
Calculate dimensionless Π groups for analysis.
Compute source area contour datasets for all specified relative contributions, derived directly from the fclim_2d climatology grid.
Check if the measurement height is above the roughness sublayer.
Check validity ranges according to equation 27 and other constraints from Kljun et al. (2015).
Create xarray Dataset from input DataFrame.
Define the spatial computational domain and polar coordinate grids.
get_coordinates()Return x and y coordinate arrays.
get_footprint_climatology()Return the climatological footprint as xarray DataArray.
get_footprint_timeseries()Return time-resolved footprint if available.
get_results()Return complete results as xarray Dataset.
get_scalar_value(arr)Convert an array-like object to a scalar float value.
Compute the source area contour threshold for fraction r of the climatological footprint, operating directly on the pre-computed fclim_2d grid.
Classify stability regimes for diagnostics.
Initialize core model parameters (a, b, c, d, ac, bc, cc).
Ensure footprint integrates to 1.0 after calculation.
plot_footprint([config, ax, show_contours, ...])Plot the footprint climatology with optional contours.
Sanitize DataFrame fields, compute RSL parameters, and drop invalid timesteps.
run([return_result])Execute the FFP model calculations.
save_results(filename)Save model results to a netCDF file.
scale_to_real_distance(x_star)Convert scaled distance to real distance using Eq.
to_netcdf(filepath)Save results to netCDF file.
verify_data(data, name)Verify the content and structure of a DataArray.
verify_results(data[, name])Check for NaN or infinite values in the given data.
- apply_paper_smoothing()[source]
Apply 3x3 smoothing from Kljun et al. paper with mass conservation.
- apply_rsl_corrections()[source]
Apply roughness sublayer (RSL) corrections to crosswind dispersion.
- calc_2d_footprint(f_ci_dummy, sigy_dummy)[source]
Calculate the two-dimensional footprint distribution f(x,y).
Implementation of Equation 10 from Kljun et al. (2015): f(x,y) = fy(x) * (1/(sqrt(2π)σy)) * exp(-y^2/(2σy^2))
Where: - fy(x) is the crosswind-integrated footprint - σy is the standard deviation of crosswind spread - y is the crosswind distance from the centerline
- Parameters:
f_ci_dummy – Crosswind-integrated footprint
sigy_dummy – Scaled crosswind dispersion
- Returns:
Two-dimensional footprint [m^-2]
- Return type:
xr.DataArray
- calc_crosswind_extent(r: float, x_ru: DataArray, x_rd: DataArray) DataArray[source]
Calculate crosswind extent of source area using parameterized relationships.
- Parameters:
r – Relative contribution
x_ru – Upwind distance from peak
x_rd – Downwind distance from peak
- Returns:
Crosswind extent at each distance
- Return type:
xr.DataArray
- calc_crosswind_integrated_extent(r: float) DataArray[source]
Calculate crosswind-integrated footprint extent for relative contribution r. Following Equations 23-25 of the paper.
- Parameters:
r – Relative contribution (between 0.1 and 0.9)
- Returns:
Distance from receptor containing fraction r of footprint
- Return type:
xr.DataArray
- calc_crosswind_integrated_footprint(x_star)[source]
Calculate the scaled crosswind-integrated footprint F̂y*(X̂*).
- Parameters:
- x_starxarray.DataArray
Scaled distance from the receptor.
- Returns:
- xarray.DataArray
The scaled crosswind-integrated footprint.
- calc_crosswind_spread(x: float | ndarray | DataArray) float | ndarray | DataArray[source]
Calculate the standard deviation of cross-wind spread \(\sigma_y\).
Implements Eqs. 18-19 from Kljun et al., 2015:
\[ \begin{align}\begin{aligned}\sigma_y^* = a_c \sqrt{\frac{b_c \lvert X^* \rvert^2} {1 + c_c \lvert X^* \rvert}}\\\sigma_y = \frac{\sigma_y^*}{\text{scale\_const}} \; z_m \; \sigma_v / u_*\end{aligned}\end{align} \]where \(a_c = 2.17\), \(b_c = 1.66\), \(c_c = 20.0\), and
scale_constdepends on stability (see paper).The method dispatches on the type of x:
xarray DataArray – x is treated as the pre-scaled dimensionless distance \(X^*\). Per-timestep stability parameters from
self.dsare used, and anxr.DataArrayis returned. Use this path for climatology calculations where time-varying parameters matter.numpy array / float – x is treated as the raw upwind distance [m].
calc_scaled_xconverts it to \(X^*\) using time-mean parameters, and a numpy array / float is returned. Use this path for single-footprint visualisation where a representative spread is needed.
- Parameters:
- xfloat, ndarray, or xr.DataArray
For numpy/float: raw upwind distance [m]. For DataArray: pre-scaled dimensionless distance \(X^*\).
- Returns:
- float, ndarray, or xr.DataArray
Cross-wind spread \(\sigma_y\) [m].
- calc_peak_based_limits(r: float) tuple[DataArray, DataArray][source]
Calculate upwind and downwind distances from the peak location for fraction r. Following Eq. 26 of the paper.
- Parameters:
r – Relative contribution (between 0.1 and 0.9)
- Returns:
Tuple containing upwind and downwind distances from peak
- calc_pi_4()[source]
Calculate the dimensionless wind-speed group Π4 = u(zm)/(u* k) = ln(zm/z0) - ψM, following Kljun et al. (2015).
Π4 is the scaled<->real conversion factor for the along-wind distance (Eq. 6/7) and must be positive. Callers use the return value of this method directly as Π4 (do not subtract it from
ln(zm/z0)again).- Returns:
- xr.DataArray
Π4 = ln(zm/z0) - ψM. In neutral conditions ψM = 0 so Π4 = ln(zm/z0).
- calc_real_footprint_peak(zm: float | DataArray, h: float | DataArray, u_zm: float | DataArray = None, ustar: float | DataArray = None, z0: float | DataArray = None, k: float = 0.4) float | DataArray[source]
Calculate the real-scale footprint peak location. Based on Equations 20-22 of Kljun et al. (2015). Works with both scalar values and xarray DataArrays.
- Parameters:
zm – Measurement height [m]
h – Boundary layer height [m]
u_zm – Mean wind speed at measurement height [m/s], optional
ustar – Friction velocity [m/s], optional
z0 – Roughness length [m], optional
k – von Karman constant (default=0.4)
- Returns:
Real distance to footprint peak [m] Returns DataArray if inputs are DataArrays, float if inputs are scalars
- calc_scaled_distance_rsl()[source]
Calculate scaled distance X* with roughness sublayer correction.
- Returns:
- xarray.DataArray
Corrected scaled distance.
- calc_scaled_footprint_peak() float[source]
Calculate the scaled footprint peak location (X*max). Based on Equation 20 of Kljun et al. (2015).
- Returns:
Scaled distance to footprint peak (constant)
- Return type:
float
- calc_scaled_x(x: float | ndarray) float | ndarray[source]
Calculate the scaled distance X* based on wind and height parameters.
Implements Eq. 7 of Kljun et al. (2015):
\[X^* = \frac{x}{z_m}\left(1 - \frac{z_m}{h}\right) \left[\ln\frac{z_m}{z_0} - \psi_M\right]^{-1}\]i.e.
X* = x/zm * (1 - zm/h) / Pi_4withPi_4 = ln(zm/z0) - psi_M = u(zm)/(u* k)(Eqs. 6/7). This is the same scaled<->real conversion used incalc_xr_footprint(), so the auxiliary source-area contour helpers stay consistent with the main climatology path.- Parameters:
- xfloat or ndarray
Upwind distance from the receptor.
- Returns:
- float or ndarray
Scaled distance X*.
- calc_unstable_psi(stability_param)[source]
Calculate ψM for unstable atmospheric conditions.
- Parameters:
- stability_paramxarray.DataArray
Stability parameter zm/L.
- Returns:
- xarray.DataArray
Stability correction ψM.
- calc_xr_footprint()[source]
Compute the flux footprint using xarray operations.
- Returns:
- xarray.DataArray
Climatological flux footprint.
- calculate_pi_groups()[source]
Calculate dimensionless Π groups for analysis.
- Returns:
- tuple of xarray.DataArray
Π₁, Π₂, Π₃, Π₄ values.
- calculate_source_areas()[source]
Compute source area contour datasets for all specified relative contributions, derived directly from the fclim_2d climatology grid.
Must be called after
run()orcalc_xr_footprint().- Returns:
- dict
Mapping of contribution label (e.g.
'r_80') to the xarray.Dataset returned byget_source_area_contour().
- Raises:
- RuntimeError
If fclim_2d has not been computed yet.
- check_rsl_validity()[source]
Check if the measurement height is above the roughness sublayer.
- Returns:
- pandas.Series
Boolean mask of valid rows.
- check_validity_ranges()[source]
Check validity ranges according to equation 27 and other constraints from Kljun et al. (2015).
Implements the following validity checks: 1. Height validity (Eq. 27): 20z₀ < zm < he
where he ≈ 0.8h is the entrainment height
Stability validity (Eq. 27): -15.5 ≤ zm/L
Turbulence validity: u* > 0.1 m/s, σv > 0
Roughness sublayer validity: zm > z* ≈ n*hrs where hrs ≈ 10z₀ and 2 ≤ n ≤ 5 (Section 2)
- Returns:
- xr.Dataset: Dataset containing per-timestep boolean masks for:
- height_valid: True where 20z₀ < zm < he
- stability_valid: True where -15.5 ≤ zm/L
- turbulence_valid: True where u* > 0.1 and σv > 0
- rsl_valid: True where measurement height above RSL
- Combined per-timestep validity is stored in self.valid_footprint (dims: time).
- get_scalar_value(arr)[source]
Convert an array-like object to a scalar float value.
- Parameters:
- arrarray-like or scalar
The value to be converted.
- Returns:
- float
A scalar representation of the input.
- get_source_area_contour(r)[source]
Compute the source area contour threshold for fraction r of the climatological footprint, operating directly on the pre-computed fclim_2d grid.
- Parameters:
- rfloat
Relative contribution (0 < r < 1), e.g. 0.8 for the 80 % footprint.
- Returns:
- xarray.Dataset
Dataset containing: -
contour_level: the fclim_2d value that encloses fraction rof the total flux (use this as the
levelsargument toax.contour).x,y: the domain coordinate arrays.f: the full fclim_2d climatology grid.
- Raises:
- RuntimeError
If fclim_2d has not been computed yet (call
run()first).- ValueError
If r lies outside (0, 1), raised by the shared threshold kernel.
See also
fluxfootprints.representativeness.truncate_to_contourMask and renormalise a climatology at this contour, after Chu et al. (2021).
- handle_stability_regimes()[source]
Classify stability regimes for diagnostics.
- Returns:
- xarray.Dataset
Dataset with regime-specific masks.
Notes
Kljun et al. (2015) deliberately use a single, stability-independent parameter set (a, b, c, d) for the scaled crosswind-integrated footprint (Appendix A: a=1.4524, b=-1.9914, c=1.4622, d=0.1359). The stability dependence enters only through the scaled distance X* and the crosswind-spread scaling. Therefore this method no longer overrides the universal coefficients; it only classifies regimes and records a diagnostic flag.
- initialize_model_parameters()[source]
Initialize core model parameters (a, b, c, d, ac, bc, cc). Ensures scalar values are assigned.
- plot_footprint(config=None, ax=None, show_contours=True, levels=10)[source]
Plot the footprint climatology with optional contours.
- Parameters:
config – Configuration object containing metadata (optional)
ax – Matplotlib axis to plot on (optional)
show_contours – Whether to show contour lines
levels – Number of contour levels or list of levels
- Returns:
matplotlib.figure.Figure, matplotlib.axes.Axes
- prep_df_fields()[source]
Sanitize DataFrame fields, compute RSL parameters, and drop invalid timesteps.
Assumes df already contains standardized column names from build_climatology.
- run(return_result: bool = True) Dataset | None[source]
Execute the FFP model calculations.
- Parameters:
return_result – If True, returns complete results dataset
- Returns:
Dataset containing footprint results
- Return type:
xr.Dataset | None
- save_results(filename: str)[source]
Save model results to a netCDF file.
- Parameters:
filename – Path where the results should be saved
- scale_to_real_distance(x_star: DataArray) DataArray[source]
Convert scaled distance to real distance using Eq. 6/7.
- Parameters:
x_star – Scaled distance X*
- Returns:
Real-scale distance
- Return type:
xr.DataArray
- class fluxfootprints.FieldSpec(name: str, units: str, meaning: str, kind: str = 'numeric', required: bool = False, minimum: float | None = None, maximum: float | None = None, min_inclusive: bool = False, max_inclusive: bool = True, provenance: frozenset[Provenance] = frozenset({Provenance.EXTERNAL_FORCING, Provenance.FIXED_METADATA, Provenance.NETWORK_DERIVED, Provenance.OBSERVED, Provenance.PREP_CALCULATED}), note: str = '')[source]
Bases:
objectThe contract for one canonical column.
- Parameters:
- name
Canonical column name.
- units
Canonical units in the
"m s-1"spelling;""for unitless or non-numeric fields.- meaning
One-line definition.
- kind
"numeric","string"or"datetime".- required
Whether a processed table must carry the column.
- minimum, maximum
Allowed range;
Noneis unbounded on that side.- min_inclusive, max_inclusive
Whether the bounds themselves are allowed.
- provenance
Provenance classes this field may legitimately carry.
- note
Model behaviour or preparation caveat worth carrying into the docs.
- Attributes:
- maximum
- minimum
provenance_textAllowed provenance classes, comma-separated in declaration order.
range_textThe allowed range in interval notation, e.g.
- kind: str = 'numeric'
- max_inclusive: bool = True
- maximum: float | None = None
- meaning: str
- min_inclusive: bool = False
- minimum: float | None = None
- name: str
- note: str = ''
- provenance: frozenset[Provenance] = frozenset({Provenance.EXTERNAL_FORCING, Provenance.FIXED_METADATA, Provenance.NETWORK_DERIVED, Provenance.OBSERVED, Provenance.PREP_CALCULATED})
- property provenance_text: str
Allowed provenance classes, comma-separated in declaration order.
- property range_text: str
The allowed range in interval notation, e.g.
"(0, 5]".
- required: bool = False
- units: str
- class fluxfootprints.FootprintAnimator(data: Any, var: str | None = None, freq: str | None = None, reducer: str = 'mean', normalize_each_frame: bool = False, time_slice: tuple[str, str] | None = None, station_lat: float | None = None, station_lon: float | None = None, crs_out: str | int = 'auto', basemap: bool = False, basemap_source: Any = None, basemap_zoom: int | str = 'auto', cmap: str | Colormap = 'viridis', norm: Literal['linear', 'log'] = 'linear', color_scale: Literal['global', 'per_frame'] = 'global', vmin: float | None = None, vmax: float | None = None, percentiles: tuple[float, float] = (0.0, 99.5), log_decades: float = 4.0, alpha: float | None = None, mask_below: float | None = None, mask_quantile: float | None = None, contour_fractions: Sequence[float] | None = None, contour_color: str = 'white', contour_width: float = 0.8, title: str | None = None, timestamp_format: str | None = None, timestamp_loc: str | tuple[float, float] = 'upper left', timestamp_kwargs: dict[str, Any] | None = None, annotation_fn: Callable[[Timestamp, ndarray], str] | None = None, show_colorbar: bool = True, cbar_label: str = 'Flux contribution (m$^{-2}$)', show_tower: bool = True, figsize: tuple[float, float] = (8.0, 7.0), dpi: int = 120, fps: int = 5, logger: Logger | None = None)[source]
Bases:
objectBuild an animation from a time-resolved footprint raster.
The animator resolves the input to a
(time, y, x)raster stack, optionally aggregates it to hourly/daily/monthly steps, places it in a projected CRS when station coordinates are supplied, and renders one frame per time step with a timestamp label.- Parameters:
- dataxarray.DataArray, xarray.Dataset, or footprint model
Source of the footprint time series. Models are read through
get_footprint_timeseries(); datasets needvarif they hold more than one time-resolved 2-D variable.- varstr, optional
Variable name when
datais axarray.Dataset.- freq{“hourly”, “daily”, “monthly”, “native”} or str or None, optional
Time step of the output animation.
None/"native"keeps the input time steps; any pandas offset alias also works.- reducerstr, optional
Aggregation applied when resampling; see
resample_footprints().- normalize_each_framebool, optional
Rescale every frame to sum to one before plotting.
- time_slicetuple of str, optional
(start, stop)labels used to subset the time axis before animating.- station_lat, station_lonfloat, optional
Tower position in WGS84. Both are required to georeference the raster (and therefore to draw a basemap); without them the axes are plotted in metres relative to the tower.
- crs_outstr or int, optional
Target CRS for georeferenced output.
"auto"picks the local UTM zone. Must be a projected CRS.- basemapbool, optional
Draw a web-map basemap under the raster. Requires
contextilyand station coordinates.- basemap_sourceoptional
A
contextilytile provider (defaults to thecontextilydefault).- basemap_zoomint or str, optional
Tile zoom level passed to
contextily.add_basemap.- cmapstr or matplotlib.colors.Colormap, optional
Colormap for the footprint density.
- norm{“linear”, “log”}, optional
Color scaling of the density values.
- color_scale{“global”, “per_frame”}, optional
Whether the color limits are fixed across the whole animation (comparable frames) or recomputed per frame (maximum contrast).
- vmin, vmaxfloat, optional
Explicit color limits; override
percentiles.- percentilestuple of float, optional
Percentiles of the finite, positive data used to derive color limits.
- log_decadesfloat, optional
Dynamic range of the log color scale, in decades below
vmax. Only used whennorm="log"andvminis not given: footprint fields trail off to arbitrarily small densities, so the lowest percentile is a useless floor for a log scale.- alphafloat, optional
Raster opacity. Defaults to 0.75 when a basemap is drawn, else 1.0.
- mask_belowfloat, optional
Densities at or below this absolute value are drawn transparent.
- mask_quantilefloat, optional
Same as
mask_belowbut expressed as a quantile (0-1) of the finite, positive densities.- contour_fractionssequence of float, optional
Source-area fractions (e.g.
(0.5, 0.8)) contoured on each frame.- contour_color, contour_widthoptional
Styling for those contours.
- titlestr, optional
Static figure title drawn above the axes.
- timestamp_formatstr, optional
strftimepattern for the per-frame label; defaults followfreq.- timestamp_locstr or tuple, optional
One of the
"upper left"-style keys, or an(x, y)pair in axes fractions.- timestamp_kwargsdict, optional
Extra keyword arguments forwarded to
matplotlib.axes.Axes.text().- annotation_fncallable, optional
f(timestamp, frame_array) -> strreturning a second line of text appended under the timestamp.- show_colorbarbool, optional
Draw a colorbar for the density.
- cbar_labelstr, optional
Colorbar label.
- show_towerbool, optional
Mark the tower position.
- figsizetuple of float, optional
Figure size in inches.
- dpiint, optional
Figure and output resolution.
- fpsint, optional
Frames per second of the saved animation.
- loggerlogging.Logger, optional
Logger used for progress messages.
- Attributes:
- framesxarray.DataArray
The prepared
(time, y, x)stack that will be animated.- timespandas.DatetimeIndex
Timestamps of the frames.
- extenttuple of float
(left, right, bottom, top)of the raster in plot coordinates.- crspyproj.CRS or None
CRS of the plot coordinates, or
Nonewhen plotting in local metres.
Methods
build()Create the figure and its
FuncAnimation.from_summary(summaries[, layer])Build an animator from a
SummaryResult.save(path[, writer, frames_dir])Render the animation to a video or GIF.
- build() tuple[Figure, FuncAnimation][source]
Create the figure and its
FuncAnimation.- Returns:
- figmatplotlib.figure.Figure
The figure being animated. Close it when finished.
- animmatplotlib.animation.FuncAnimation
The animation, ready to save or embed (
anim.to_jshtml()).
- classmethod from_summary(summaries: Any, layer: str = 'daily_mean', **kwargs: Any) FootprintAnimator[source]
Build an animator from a
SummaryResult.- Parameters:
- summariesSummaryResult
Output of
fluxfootprints.summarize_periods().- layer{“daily_mean”, “monthly_mean”, “daily_etw”, “monthly_etw”}
Which summary raster to animate. The timestamp format follows the layer’s frequency unless overridden in
kwargs.- **kwargs
Forwarded to
FootprintAnimator.
- Returns:
- FootprintAnimator
- save(path: str | Path, writer: str | Any | None = None, frames_dir: str | Path | None = None, **writer_kwargs: Any) Path[source]
Render the animation to a video or GIF.
- Parameters:
- pathstr or pathlib.Path
Output file. The suffix selects the writer:
.mp4/.mov/.m4v/.webmuse ffmpeg,.gifuses Pillow,.htmlwrites an embedded HTML player.- writerstr or matplotlib writer, optional
Override the writer chosen from the suffix.
- frames_dirstr or pathlib.Path, optional
If given, also write each frame as a numbered PNG into this directory.
- **writer_kwargs
Extra keyword arguments passed to the writer (e.g.
bitrate=2400,codec="libx264").
- Returns:
- pathlib.Path
The path that was written.
- class fluxfootprints.GridGeometry(crs: CRS, transform: Affine, width: int, height: int, x_origin: float, y_origin: float, y_ascending: bool)[source]
Bases:
objectGeoreferencing for a footprint grid expressed in metres from the tower.
- Attributes:
shape(height, width), i.e. numpy row/column order.
- crs: CRS
- height: int
- property shape: tuple[int, int]
(height, width), i.e. numpy row/column order.
- transform: Affine
- width: int
- x_origin: float
- y_ascending: bool
- y_origin: float
- class fluxfootprints.Issue(severity: str, code: str, field: str | None, message: str, n_rows: int = 0, examples: tuple[str, ...] = ())[source]
Bases:
objectOne validation finding.
- Attributes:
- severity
"error"for a contract violation,"warning"for a row the contract permits butFFPModelwill drop or flag.- code
Stable machine-readable identifier, e.g.
"out_of_range".- field
Canonical field the finding concerns, or
Nonewhen it is frame-level.- message
Human-readable explanation.
- n_rows
Number of offending rows; 0 for structural findings.
- examples
Up to five offending index labels, as strings.
- code: str
- examples: tuple[str, ...] = ()
- field: str | None
- message: str
- n_rows: int = 0
- severity: str
- class fluxfootprints.KormannMeixnerModel(df: DataFrame, domain: list | None = None, dx: float = 10.0, dy: float = 10.0, rs: list | None = None, smooth_data: bool = True, verbosity: int = 2, logger: Logger | None = None, **kwargs)[source]
Bases:
BaseFootprintModelKormann & Meixner (2001) analytical footprint model adapter.
Wraps the analytical functions in
fluxfootprints.kormannmeixnerin the standardBaseFootprintModelinterface. Intended to be driven throughfluxfootprints.ffp_daily_monthly_helper.build_climatology(), which resolves per-timestep inputs into a DataFrame with the standardized column names below; the model reads those columns directly.- Parameters:
- dfpandas.DataFrame
Input data. Must contain the following required columns:
- zmfloat or Series
Measurement height above displacement height (i.e. z - d) [m].
- z0float or Series
Aerodynamic roughness length [m].
- ustarfloat or Series
Friction velocity [m s⁻¹].
- olfloat or Series
Obukhov length [m].
- sigmavfloat or Series
Standard deviation of lateral velocity fluctuations [m s⁻¹].
- umeanfloat or Series
Mean wind speed at zm [m s⁻¹].
- wind_dirfloat or Series, optional
Mean wind direction the wind blows from [deg]. When present, the wind-aligned footprint is rotated into the (x, y) output domain for that timestep; when absent, it is placed directly using the standard negative-upwind x convention (no rotation).
Methods
get_coordinates()Return x and y coordinate arrays.
get_footprint_climatology()Return the climatological footprint as xarray DataArray.
get_footprint_timeseries()Return time-resolved footprint if available.
get_results()Return complete results as xarray Dataset.
run([return_result])Execute Kormann-Meixner footprint calculation.
to_netcdf(filepath)Save results to netCDF file.
- REQUIRED_COLUMNS = ['zm', 'z0', 'ustar', 'ol', 'sigmav', 'umean']
- class fluxfootprints.LSFootprintConfig(zm: float, ustar: float, L: float, h: float, wind_dir_deg: float, z0: float = 0.1, n_particles: int = 20000, dt: float = 0.25, t_max: float = 600.0, domain: Tuple[float, float] = (2000.0, 2000.0), dx: float = 10.0, dy: float = 10.0, seed: int | None = None)[source]
Bases:
objectContainer for model parameters fixed for one simulation.
- Attributes:
- seed
Methods
rng
- L: float
- domain: Tuple[float, float] = (2000.0, 2000.0)
- dt: float = 0.25
- dx: float = 10.0
- dy: float = 10.0
- h: float
- n_particles: int = 20000
- seed: int | None = None
- t_max: float = 600.0
- ustar: float
- wind_dir_deg: float
- z0: float = 0.1
- zm: float
- class fluxfootprints.LSFootprintModelAdapter(*args, n_particles: int = 20000, **kwargs)[source]
Bases:
BaseFootprintModelLagrangian Stochastic footprint model adapter.
Wraps
BackwardLSModelin the standardBaseFootprintModelinterface. Intended to be driven throughfluxfootprints.ffp_daily_monthly_helper.build_climatology(), which resolves per-timestep inputs into a DataFrame with the standardized column names below; the model reads those columns directly.- Parameters:
- dfpandas.DataFrame
Input data. Must contain the following required columns:
- zmfloat or Series
Measurement height above displacement height (i.e. z - d) [m].
- z0float or Series
Aerodynamic roughness length [m].
- ustarfloat or Series
Friction velocity [m s⁻¹].
- olfloat or Series
Obukhov length [m].
- wind_dirfloat or Series
Mean wind direction the wind blows from [deg].
- hfloat or Series
Boundary-layer height [m].
- n_particlesint, default 20000
Number of stochastic particles released per timestep.
Methods
get_coordinates()Return x and y coordinate arrays.
get_footprint_climatology()Return the climatological footprint as xarray DataArray.
get_footprint_timeseries()Return time-resolved footprint if available.
get_results()Return complete results as xarray Dataset.
run([return_result])Execute Lagrangian footprint calculation.
to_netcdf(filepath)Save results to netCDF file.
- REQUIRED_COLUMNS = ['zm', 'z0', 'ustar', 'ol', 'wind_dir', 'h']
- class fluxfootprints.LandsatBands(collection: str, blue: str, red: str, nir: str, qa: str, label: str)[source]
Bases:
objectBand names of one Landsat sensor’s Collection 2 surface-reflectance product.
- Attributes:
- collectionstr
Earth Engine ImageCollection id.
- blue, red, nirstr
Bands entering the EVI of Eq. 4, in that role.
- qastr
Band holding the CFMask quality bits,
QA_PIXELthroughout Collection 2.- labelstr
Human-readable sensor name, recorded on the returned array.
- blue: str
- collection: str
- label: str
- nir: str
- qa: str
- red: str
- class fluxfootprints.Level(*values)[source]
Bases:
str,EnumThree-level footprint-to-target-area representativeness index.
The members are
strsubclasses, soLevel.HIGH == "high"is True and the values serialise directly into DataFrames, CSVs, and GeoPackage fields.- Attributes:
- HIGHLevel
Footprint and target area agree closely. For land cover: a single class holds >= 80 % of both the footprint-weighted and the target-area composition, and the chi-square test is not significant. For a continuous field: R² >= 0.8 with slope 1.0 +/- 0.1 and intercept 0.0 +/- 0.1.
- MEDIUMLevel
Partial agreement. For land cover: a single class holds >= 50 % of both compositions and the chi-square test is not significant, but the HIGH criteria are not met. For a continuous field: R² >= 0.6 and p < 0.05, but the HIGH criteria are not met.
- LOWLevel
Poor agreement. For land cover: no class reaches 50 % in the footprint or the target area, or the compositions differ significantly. For a continuous field: R² < 0.6 or p >= 0.05.
References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Sect. 2.4.
Examples
>>> Level.HIGH <Level.HIGH: 'high'> >>> Level.HIGH == "high" # a str subclass, so it compares and serialises as one True >>> Level("medium") <Level.MEDIUM: 'medium'> >>> str(Level.HIGH) # the value, not "Level.HIGH" 'high'
- HIGH = 'high'
- LOW = 'low'
- MEDIUM = 'medium'
- class fluxfootprints.MaskedFootprint(data: DataArray, mask: DataArray, retained_fraction: DataArray, missing_dates: list[Timestamp])[source]
Bases:
objectResult of masking a footprint field with OpenET valid-data masks.
- data: DataArray
- mask: DataArray
- missing_dates: list[Timestamp]
- retained_fraction: DataArray
- class fluxfootprints.Provenance(*values)[source]
Bases:
str,EnumHow a value in a canonical table was obtained.
Exactly one class applies to each column of a processed table.
- EXTERNAL_FORCING = 'external_forcing'
Taken from a different dataset, e.g. a reanalysis boundary-layer height. Requires
methodin the sidecar.
- FIXED_METADATA = 'fixed_metadata'
A site constant from BADM/BIF or site documentation.
- NETWORK_DERIVED = 'network_derived'
Computed by the publisher from its own observations (AmeriFlux
MO_LENGTH,ZL; NEONdistObkv).
- OBSERVED = 'observed'
Measured and published by the source network.
- PREP_CALCULATED = 'prep_calculated'
Computed during preparation, from documented inputs by a documented formula. Requires
methodin the sidecar.
- class fluxfootprints.RMAFit(slope: float, intercept: float, r_squared: float, p_value: float, n: int, slope_ci: tuple[float, float], intercept_ci: tuple[float, float], ci_level: float, ci_method: str)[source]
Bases:
objectA reduced major axis fit and its confidence intervals.
What
rma_regression()returns: the model II regression of Eq. 7, with the interval estimates the paper’slmodel2reports alongside the point estimates of Table 1.- Attributes:
- slopefloat
beta_1, the RMA slope.
nanwhen either variable is constant.- interceptfloat
beta_0,
mean(y) - slope * mean(x).- r_squaredfloat
Squared Pearson correlation of the two inputs. Note that this is a property of the correlation alone: it does not describe the scatter about the RMA line, and it is the same whichever variable is called the predictor.
- p_valuefloat
Two-sided significance of that correlation, testing rho = 0. The RMA slope has no null of its own – it cannot be zero – so this is the test the paper’s MEDIUM criterion uses.
- nint
Number of finite pairs the fit rests on.
- slope_cituple of float
Lower and upper confidence limits on slope, in that order.
- intercept_cituple of float
Lower and upper confidence limits on intercept, in that order.
- ci_levelfloat
Confidence level the two intervals were computed at, e.g. 0.95.
- ci_methodstr
"analytical"or"bootstrap", the estimator used for them.
See also
rma_regressionProduces this fit.
classify_continuousTurns it into the three-level index of Sect. 2.4.
References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Eq. 7.
Examples
>>> import numpy as np >>> rng = np.random.default_rng(3) >>> footprint = rng.uniform(0.2, 0.8, 40) >>> target = 0.95 * footprint + 0.02 + rng.normal(0.0, 0.03, 40) >>> fit = rma_regression(footprint, target) >>> round(fit.slope, 4), round(fit.intercept, 4), round(fit.r_squared, 4), fit.n (0.9739, 0.0039, 0.9647, 40) >>> fit.ci_level, fit.ci_method (0.95, 'analytical')
- ci_level: float
- ci_method: str
- intercept: float
- intercept_ci: tuple[float, float]
- n: int
- p_value: float
- r_squared: float
- slope: float
- slope_ci: tuple[float, float]
- class fluxfootprints.ValidationReport(issues: list[Issue] = <factory>, n_rows: int = 0, fields_present: tuple[str, ...]=())[source]
Bases:
objectThe outcome of validating one frame.
- Attributes:
- issues
Every finding, errors and warnings alike, in the order found.
- n_rows
Rows in the validated frame.
- fields_present
Canonical fields the frame actually carried.
Methods
codes_for(severity)The set of codes present at one severity.
fields_with(code)Fields carrying a given code.
Raise
CanonicalSchemaErrorif there is any error.summary()Multi-line summary: the verdict line, then one line per finding.
to_frame()Findings as a table, one row per issue.
- property codes: set[str]
The set of codes present, for concise assertions in tests.
- fields_present: tuple[str, ...] = ()
- n_rows: int = 0
- property ok: bool
True when there are no errors. Warnings do not affect this.
- raise_for_status() ValidationReport[source]
Raise
CanonicalSchemaErrorif there is any error.- Returns:
- ValidationReport
self, so calls can be chained.
- class fluxfootprints.WangFootprintModel(df: DataFrame, domain: list | None = None, dx: float = 10.0, dy: float = 10.0, rs: list | None = None, smooth_data: bool = True, verbosity: int = 2, logger: Logger | None = None, **kwargs)[source]
Bases:
BaseFootprintModelWang et al. (2006) convective footprint model adapter.
Wraps
wang2006_fy()andreconstruct_gaussian_2d()in the standardBaseFootprintModelinterface. Intended to be driven throughfluxfootprints.ffp_daily_monthly_helper.build_climatology(), which resolves per-timestep inputs into a DataFrame with the standardized column names below; the model reads those columns directly.Note: Valid only for daytime convective conditions (Obukhov length < 0).
- Parameters:
- dfpandas.DataFrame
Input data. Must contain the following required columns:
- zmfloat or Series
Measurement height above displacement height (i.e. z - d) [m].
- hfloat or Series
Boundary-layer height [m].
- olfloat or Series
Obukhov length [m]. Must be negative (convective) per timestep.
- sigmavfloat or Series
Standard deviation of lateral velocity fluctuations [m s⁻¹].
- umeanfloat or Series
Mean wind speed at zm [m s⁻¹].
Methods
get_coordinates()Return x and y coordinate arrays.
get_footprint_climatology()Return the climatological footprint as xarray DataArray.
get_footprint_timeseries()Return time-resolved footprint if available.
get_results()Return complete results as xarray Dataset.
run([return_result])Execute Wang footprint calculation.
to_netcdf(filepath)Save results to netCDF file.
- REQUIRED_COLUMNS = ['zm', 'h', 'ol', 'sigmav', 'umean']
- class fluxfootprints.WeightedValue(value: float, retained_weight: float, n_cells: int)[source]
Bases:
objectA field value averaged over a footprint or a target area (Sect. 2.4).
Returned by
footprint_weighted_value()andtarget_area_value(), which differ only in the weights they average under: the footprint’s own source weights, or the uniform weights of a target-area disc.- Attributes:
- valuefloat
The averaged field value – EVI_footprint of Eq. 5, or EVI_target.
nanwhen no cell carried both weight and data.- retained_weightfloat
Fraction of the weight that fell on cells holding data, in [0, 1], before the renormalisation that produced value. It is 1 for a raster covering the whole source area, and below 1 where the raster is nodata or does not reach – the cue that value describes only part of the intended area. Over a target area, where every cell of the disc weighs the same, it is the fraction of the disc’s cells that held data.
- n_cellsint
Number of cells that contributed to value.
References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Sect. 2.4.
Examples
>>> result = WeightedValue(value=0.42, retained_weight=1.0, n_cells=161) >>> result WeightedValue(value=0.42, retained_weight=1.0, n_cells=161) >>> result.value 0.42
- n_cells: int
- retained_weight: float
- value: float
- fluxfootprints.aggregate_to_daily_centroid(df: DataFrame, date_column: str = 'Timestamp', x_column: str = 'X', y_column: str = 'Y', weighted: bool = True)[source]
Aggregate sub-daily coordinate data to daily centroids.
This function groups coordinate data by calendar date and computes the centroid of X and Y values. If weighted=True, centroids are computed using a weighted average based on the ‘ET’ (evapotranspiration) column.
- Parameters:
- dfpandas.DataFrame
Input DataFrame containing timestamps and coordinate columns.
- date_columnstr, optional
Name of the column with datetime objects, by default “Timestamp”.
- x_columnstr, optional
Name of the column representing X coordinates, by default “X”.
- y_columnstr, optional
Name of the column representing Y coordinates, by default “Y”.
- weightedbool, optional
If True, uses the ‘ET’ column as weights for centroid calculation; otherwise computes an unweighted mean. Default is True.
- Returns:
- pandas.DataFrame
DataFrame with one row per date and columns for: - ‘Date’: datetime.date object - x_column: daily centroid X coordinate - y_column: daily centroid Y coordinate
Notes
Requires a column named ‘ET’ if weighted=True.
Assumes the timestamp column can be parsed with pd.to_datetime.
The input DataFrame is left unchanged.
- fluxfootprints.analytical_power_law_parameters(z_m: float | ndarray, z_0: float | ndarray, L: float | ndarray, u_star: float | ndarray, u_zm: float | ndarray) Tuple[float | ndarray, float | ndarray, float | ndarray, float | ndarray][source]
Return m, n, U, κ using the analytical matching approach.
Accepts scalars or same-shaped arrays for every argument, so a whole column of per-timestep inputs can be resolved in one vectorized call instead of looping row by row.
- Parameters:
- z_m
Eddy-covariance measurement height (m).
- z_0
Aerodynamic roughness length (m).
- L
Obukhov length (m) (negative ⇒ unstable).
- u_star
Friction velocity (m sâ»Â¹).
- u_zm
Mean wind speed at z_m (m sâ»Â¹).
- Returns:
- m, n, U, kappa
Power-law exponents and proportionality constants for
u(z) = U z**mandK(z) = kappa z**n.
- fluxfootprints.angular_difference(a: Any, b: Any) Any[source]
Signed smallest angle from
btoa, in degrees on[-180, 180).A circular difference, for comparing a modelled footprint bearing against an expected one without a wrap-around fault near 0/360.
- Parameters:
- a, b
Bearings in degrees.
- Returns:
- Signed difference
a - b, wrapped. An exact reversal maps to -180, - the closed end of the interval; compare magnitudes when only the size of
- the error matters.
- Signed difference
- fluxfootprints.animate_footprint(data: Any, path: str | Path, freq: str | None = None, **kwargs: Any) Path[source]
Render a footprint time series to a video or GIF in one call.
- Parameters:
- dataxarray.DataArray, xarray.Dataset, or footprint model
Source of the time-resolved footprint raster.
- pathstr or pathlib.Path
Output file; the suffix selects the format (
.mp4,.gif, …).- freq{“hourly”, “daily”, “monthly”, “native”} or None, optional
Time step of the animation.
- **kwargs
Any other
FootprintAnimatorargument, pluswriter,frames_dir, and writer options which are forwarded toFootprintAnimator.save().
- Returns:
- pathlib.Path
The path that was written.
Examples
>>> animate_footprint(model, "daily.gif", freq="daily") PosixPath('daily.gif')
- fluxfootprints.apply_openet_mask(target: DataArray | SummaryResult | str | Path, openet: str | Path | Sequence[str | Path] | Mapping[Any, str | Path | Sequence[str | Path]], station_lat: float | None = None, station_lon: float | None = None, **kwargs: Any) MaskedFootprint | SummaryResult | list[Path][source]
Mask footprint output with daily OpenET valid-data masks.
Convenience entry point that dispatches on target:
target
behaviour
xarray.DataArraySummaryResultpath to a directory or GeoTIFF
- Parameters:
- targetxarray.DataArray, SummaryResult, or path
What to mask.
- openetpath, sequence of paths, or mapping
Daily OpenET rasters; see
index_openet_rasters().- station_lat, station_lonfloat, optional
Tower position in decimal degrees (WGS 84). Required for the xarray targets, which carry grid offsets rather than map coordinates; ignored for GeoTIFFs, which are already georeferenced.
- **kwargs
Forwarded to the dispatched function.
- Returns:
- MaskedFootprint, SummaryResult, or list of pathlib.Path
Matching the dispatched function.
- fluxfootprints.assert_canonical(df: DataFrame, **kwargs: Any) ValidationReport[source]
Validate a frame and raise on any error.
Convenience wrapper around
validate_canonical_frame(...).raise_for_status()for preparation scripts, which should refuse to write a non-conforming table.- Parameters:
- df
Candidate processed table.
- **kwargs
Passed to
validate_canonical_frame().
- Returns:
- ValidationReport
- Raises:
- CanonicalSchemaError
- fluxfootprints.assess_representativeness(model_or_climatology: Any, station_lat: float | None = None, station_lon: float | None = None, site_id: str | None = None, landcover: Any = None, continuous: Any = None, radii: Sequence[float] = (250, 500, 1000, 1500, 2000, 3000), landcover_name: str = 'land_cover', continuous_name: str = 'EVI', crs: str | int | None = 'auto', fraction: float = 0.8, alpha: float = 0.05, bias_threshold: float = 0.1, min_matches: int = 6, partition: str = 'month+daynight', tz: str | float | tzinfo | None = None, daytime: Series | ndarray | DataArray | None = None, min_times: int = 1, sw_in_pot: str | None = 'SW_IN_POT', ci_level: float = 0.95, ci_method: str = 'analytical') DataFrame[source]
Run the whole footprint-to-target-area analysis and return one tidy table.
The driver over the rest of this module: it aggregates monthly footprint climatologies (Sect. 2.2), summarises each with the metrics of Eqs. 1-3, compares them against a series of target-area discs for a categorical and a continuous land-surface field (Sect. 2.4), and classifies every comparison HIGH, MEDIUM, or LOW. Either field may be omitted, so the same call serves a land-cover-only, a vegetation-index-only, or a full analysis.
- Parameters:
- model_or_climatologyBaseFootprintModel, xarray.Dataset, or xarray.DataArray
Either a run footprint model or its time-resolved footprints, which are aggregated here with
monthly_climatologies(), or a Dataset that function has already produced, which is used as it stands. The second form is the one to reach for when several analyses share one set of climatologies, since the aggregation is by far the slowest step.- station_lat, station_lonfloat, optional
Tower position in decimal degrees (WGS 84). Needed to split day from night when the climatologies have to be aggregated here without a precomputed daytime or potential-radiation column, and to georeference any raster passed as a path.
- site_idstr, optional
Site identifier, e.g.
"US-MOz", written to thesiteindex level so that frames from several sites concatenate. Missing when omitted.- landcoverxarray.DataArray, xarray.Dataset, str, or pathlib.Path, optional
Categorical land-cover raster – the consolidated NLCD / Land Cover of Canada codes of Table S6 in the paper. A path is reprojected onto the footprint grid with
sample_raster_on_grid()using nearest neighbour; a DataArray must already be on that grid. Omit to skip the categorical analysis.- continuousmapping, pandas.Series, xarray.DataArray, xarray.Dataset, or sequence, optional
Time series of continuous rasters – the Landsat EVI scenes of the paper – in any of
a mapping of retrieval time -> raster (a DataArray or a path);
a
pandas.Seriesof rasters indexed by retrieval time;a
DataArraywith dims(time, x, y), or a Dataset holding exactly one such variable;an iterable of
(time, raster)pairs.
Each scene is matched to the climatologies of the calendar month it was retrieved in, as in Sect. 2.4. Omit to skip the continuous analysis.
- radiisequence of float, default TARGET_RADII
Target-area radii [m], evaluated and reported in the order given.
- landcover_name, continuous_namestr, default “land_cover”, “EVI”
Labels written to the
variableindex level, so that a frame can carry more than one field.- crsstr, int, or None, default “auto”
Target CRS used when reprojecting a raster given as a path;
"auto"picks the local WGS 84 UTM zone.- fractionfloat, default 0.8
Source-weight fraction each climatology is truncated at. Ignored when model_or_climatology is an already-aggregated Dataset, whose own fraction is used.
- alphafloat, default 0.05
Significance level for the chi-square test and the regression.
- bias_thresholdfloat, default 0.10
|Delta|at or below which a period counts as representative, reported aswithin_threshold.- min_matchesint, default 6
Fewest matched scenes a site-level regression may be fitted on.
- partition{“month+daynight”, “month”}, default “month+daynight”
How to group timesteps when aggregating, passed to
monthly_climatologies().- tzstr, float, datetime.tzinfo, or None, optional
Time zone of the footprint timestamps, for the day-night split.
- daytimepandas.Series, numpy.ndarray, or xarray.DataArray, optional
A precomputed day-night flag, used instead of computing one.
- min_timesint, default 1
Fewest contributing timesteps a month-period group needs before it is aggregated and evaluated.
- sw_in_potstr or None, default “SW_IN_POT”
Precomputed potential-radiation column to prefer over solar geometry.
- ci_levelfloat, default 0.95
Confidence level of the regression interval limits.
- ci_method{‘analytical’, ‘bootstrap’}, default ‘analytical’
Interval estimator, passed through to
rma_regression().
- Returns:
- pandas.DataFrame
A tidy frame indexed by
RESULT_INDEX–(site, year, month, period, radius, variable)– carryingRESULT_COLUMNS. Rows are of three scopes, told apart by thescopecolumn, and of three kinds, told apart bykind:scope="period",kind="climatology"One row per aggregated month and period, carrying fetch, area, symmetry, contour_level, n_cells, and n_times.
radiusis missing andvariableis"footprint".scope="period",kind="categorical"One row per month, period, and radius: dominant_class, value_footprint and value_target (P_footprint and P_target as fractions), chi2, dof, p_value, and level.
scope="period",kind="continuous"One row per matched scene, period, and radius: time, value_footprint (Eq. 5), value_target, bias (Delta, Eq. 6, as a fraction), and within_threshold. These rows are the paper’s Dataset S5.
scope="site_year",kind="climatology"One row per year and period, with fetch, area, and symmetry averaged over that year’s months as in Fig. 3, and the overlap indices seasonal_overlap (Eq. 2) and daynight_overlap (Eq. 3).
monthis missing.scope="site",kind="categorical"One row per period and radius, from the climatologies of every month pooled – the paper’s Dataset S4, and the level reported in its Fig. 5.
yearandmonthare missing.scope="site",kind="continuous"One row per period and radius, holding the site-level model II regression of Eq. 7: slope, intercept, their confidence limits, r_squared, p_value, rmse, mae, n, sufficient, and level. The paper’s Dataset S6 and Table 1.
Columns an analysis does not produce are missing, so the frame is sparse by construction; select a slice with the
scopeandkindcolumns before reading values off it.DataFrame.attrsrecords the settings the run used, includingunmatched_fields, the retrieval times of continuous rasters that fell in no aggregated month.
- Raises:
- TypeError
If model_or_climatology, landcover, or continuous is of an unusable type.
- ValueError
If radii is empty or holds a radius that is not positive and finite; if a raster is not on the footprint grid, or was given as a path without a tower position; if the climatologies carry no aggregated month at all; or for any of the reasons the underlying evaluations raise, with the offending month and period prepended.
- ImportError
If a raster given as a path needs
rioxarrayand it is not installed.
See also
monthly_climatologiesThe aggregation of Sect. 2.2 this drives.
categorical_representativenessThe per-climatology land-cover step.
sensor_location_biasThe per-period continuous step, Eq. 6.
continuous_representativenessThe site-level regression, Eq. 7.
export_representativeness_tablesWrites this frame in the published column schema of Datasets S4-S6.
Notes
The index is unique as long as at most one continuous raster is matched to each month, which is the paper’s case – it kept the cloud-free Landsat scenes overlapping each site’s measurement periods, a median of 13 matches per site over several years. Two scenes in one month repeat the index and are told apart by the
timecolumn, so prefer.queryor boolean masks over.locon a frame that may hold them.The site-level rows pool every year, following the paper, whose Datasets S4 and S6 are site properties; only the overlap indices and the averaged fetch and area are site-year properties, and those are the
scope="site_year"rows. Restrict the input to one year to get single-year site rows.The percentages of Sect. 3.3 come out of the per-period continuous rows directly:
period = results.query("scope == 'period' and kind == 'continuous'") period.groupby("radius")["within_threshold"].mean()
bias is filled for the categorical rows too, as the relative difference between the dominant class’s footprint and target-area shares. Chu et al. (2021) apply the +/-10 % threshold only to the continuous field, so
within_thresholdis left missing there.The level column holds the value of a
Level–"high","medium", or"low"– rather than the member itself, because pandas stores a string column as strings. SinceLevelis astrenum,results["level"] == Level.HIGHand== "high"both select the same rows; onlyis Level.HIGHdoes not.References
Chu, H., et al. (2021). Representativeness of Eddy-Covariance flux footprints for areas surrounding AmeriFlux sites. Agric. For. Meteorol., 301-302, 108350. https://doi.org/10.1016/j.agrformet.2021.108350
Examples
>>> import numpy as np, pandas as pd, xarray as xr >>> times = pd.date_range("2020-01-01", "2020-06-30 21:00", freq="3h") >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> rng = np.random.default_rng(0) >>> centres = rng.normal(30.0, 25.0, times.size) # a wandering wind direction >>> f = np.stack( ... [np.exp(-((X - c) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) for c in centres] ... ) >>> f /= f.sum(axis=(1, 2), keepdims=True) * 400.0 >>> f_2d = xr.DataArray( ... f, coords={"time": times, "x": x, "y": y}, dims=("time", "x", "y") ... ) >>> landcover = xr.DataArray( ... np.where(X < 60.0, 42, 81), dims=("x", "y"), coords={"x": x, "y": y} ... ) >>> scenes = { ... pd.Timestamp(f"2020-{m:02d}-15"): xr.DataArray( ... 0.30 + 0.0005 * X + 0.02 * m, dims=("x", "y"), coords={"x": x, "y": y} ... ) ... for m in range(1, 7) ... } >>> results = assess_representativeness( ... f_2d, ... 40.0, ... -111.9, ... site_id="US-Xyz", ... tz=-7, ... landcover=landcover, ... continuous=scenes, ... radii=(250.0, 1000.0), ... ) >>> results.index.names FrozenList(['site', 'year', 'month', 'period', 'radius', 'variable']) >>> sorted(results["scope"].unique()) ['period', 'site', 'site_year']
The site-scope rows carry the Sect. 2.4 regression and its level:
>>> site = results.reset_index() >>> site = site[(site["scope"] == "site") & (site["variable"] == "EVI")] >>> print( ... site[["period", "radius", "n", "slope", "r_squared", "level"]] ... .round(4) ... .to_string(index=False) ... ) period radius n slope r_squared level daytime 250.0 6 0.9942 0.9997 high daytime 1000.0 6 0.9942 0.9997 high nighttime 250.0 6 0.9840 0.9988 high nighttime 1000.0 6 0.9840 0.9988 high
- fluxfootprints.build_climatology(df: DataFrame, model_type: str = 'ffp', ustar: str | float | int | Series | None = 'ustar_1_1_1', ol: str | float | int | Series | None = 'mo_length_1_1_1', umean: str | float | int | Series | None = 'ws_1_1_1', sigmav: str | float | int | Series | None = 'v_sigma_1_1_1', wind_dir: str | float | int | Series | None = 'wd_1_1_1', zm: str | float | int | Series | None = 'zm', z0: str | float | int | Series | None = 'z0', h: str | float | int | Series | None = 2000.0, dx: float = 10.0, dy: float = 10.0, domain: tuple[float, float, float, float] | None = None, rs: list[float] | None = None, smooth_data: bool = True, verbosity: int = 2, logger: Logger | None = None, **model_kwargs) BaseFootprintModel[source]
Prepare inputs, validate per-model requirements, and run the footprint model.
- fluxfootprints.call_nldas_time_series(lat, lon, time_start, time_end, data, token)[source]
INPUTS: lat - latitude lon - longitude time_start - start of time series in YYYY-MM-DDThh:mm:ss format (UTC) end_time - end of the time series in YYYY-MM-DDThh:mm:ss format (UTC) data - name of the data parameter for the time series token - earthaccess bearer token
OUTPUT: time series csv output string
Function modified from How_to_Access_GiC_Time_Series_Service.ipynb https://github.com/nasa/gesdisc-tutorials/tree/main/notebooks
- fluxfootprints.canonical_field_table() DataFrame[source]
Return the field registry as a table, one row per canonical field.
The Markdown tables in
schemas/canonical_fluxfootprints_input.mdare generated from this, so the documentation and the validator cannot disagree.- Returns:
- pandas.DataFrame
Columns
field, units, meaning, kind, required, allowed_range, provenance, note.
- fluxfootprints.cardinal_sector(wind_dir: Any, tolerance: float = 10.0) Any[source]
Label bearings that fall within
toleranceof a cardinal direction.Used to build the N/E/S/W benchmark subsets. Bearings outside every sector give
None.- Parameters:
- wind_dir
Meteorological wind direction, degrees.
- tolerance
Half-width of each sector, degrees. The default 10 gives the 350-10 / 80-100 / 170-190 / 260-280 windows the validation context asks for.
- Returns:
"N","E","S","W"orNone, elementwise.
Examples
>>> cardinal_sector(355.0) 'N' >>> cardinal_sector(45.0) is None True
- fluxfootprints.categorical_representativeness(w: DataArray | ndarray, landcover: DataArray | ndarray, x: ndarray | DataArray, y: ndarray | DataArray, radii: Sequence[float] = (250, 500, 1000, 1500, 2000, 3000), alpha: float = 0.05) DataFrame[source]
Evaluate land-cover representativeness against each target area, Sect. 2.4.
The categorical counterpart of
sensor_location_bias(). The dominant land-cover type – the class holding the largest footprint-weighted share – is identified once, since it does not depend on the target area; its share within each disc is then read off, the full compositions are compared with a chi-square test, and each radius is classified HIGH, MEDIUM, or LOW byclassify_categorical().- Parameters:
- wxarray.DataArray or numpy.ndarray
Footprint source weights with dims
(x, y), normally a truncated, renormalised climatology fromtruncate_to_contour()or a slice ofmonthly_climatologies(). Weights summing to something other than 1 are renormalised, as infootprint_weighted_composition().- landcoverxarray.DataArray or numpy.ndarray
Categorical raster on the same grid, typically the integer codes of the consolidated NLCD / Land Cover of Canada groups of Table S6, NaN where it holds no data. Use
sample_raster_on_grid()withcategorical=Trueto bring an external product onto the grid.- x, ynumpy.ndarray or xarray.DataArray
Cell-centre offsets from the tower [m] (
model.x,model.y), which is the grid both w and landcover must be on.- radiisequence of float, default TARGET_RADII
Target-area radii [m], evaluated and reported in the order given.
- alphafloat, default 0.05
Significance level; compositions with
p_value >= alphacount as not significantly different.
- Returns:
- pandas.DataFrame
One row per radius, with columns
radiusTarget-area radius [m].
dominant_classClass code holding the largest footprint-weighted share. Constant down the frame, since the footprint does not depend on the radius.
p_footprintThat class’s footprint-weighted share, P_footprint, as a fraction in [0, 1] rather than the percentage of the paper; multiply by 100 to match its figures. Constant down the frame.
p_targetThe same class’s share of the disc, P_target, likewise a fraction.
0.0when the disc holds none of it.chi2Chi-square statistic comparing the two full compositions.
p_valuep-value of that test.
dofDegrees of freedom, one less than the number of classes tested.
levelLevelmember:HIGHwhen both shares reach 0.80 and the compositions do not differ significantly,MEDIUMwhen both reach 0.50 and they do not differ significantly,LOWotherwise. Being astrenum it compares equal to"high"and serialises as it.
DataFrame.attrsholdsalphaandfootprint_composition, the full footprint-weighted composition the dominant class was taken from.
- Raises:
- ValueError
If radii is empty; if any radius is not positive and finite, or is smaller than half the grid spacing so that its disc holds no cell centre; if w, landcover, and the grid x and y describe are not one and the same; if w holds a non-finite or negative weight, or no weight at all; or if no cell carries both weight and a class code, leaving no composition to take a dominant class from.
See also
footprint_weighted_compositionP_footprint over the whole composition.
target_area_compositionP_target over the whole composition.
classify_categoricalThe three-level index applied here.
sensor_location_biasThe continuous-field counterpart, Eq. 6.
Notes
The chi-square test rests on pseudo-counts: both compositions are scaled by the number of classified cells inside the target area, because a footprint-weighted composition is a share of source weight and carries no sample size of its own. Classes absent from both compositions are dropped rather than entered as matching zeros, so the degrees of freedom count only the classes the analysis actually saw. Both choices are set out in
_composition_chi_square(); the p-value is best read as a comparison on the paper’s 30 m grid rather than as an absolute significance.The dominant class comes from the footprint, never from the target area, so
p_targetcan fall to zero while a different class dominates the disc – the mismatch the index is there to report. Ties for the largest footprint share go to the lowest class code.A raster covering only part of the footprint or the disc lowers the retained weight of the underlying compositions rather than making them NaN, so a row here can rest on partial coverage; read
attrs["footprint_composition"].attrs["retained_weight"]when that matters.Chu et al. (2021) found 34 of 214 sites where the land-cover product disagreed with the site’s own IGBP metadata, so a result that contradicts site metadata is worth checking against the product before trusting it.
References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Sect. 2.4.
Goeckede, M., et al. (2008). Quality control of CarboEurope flux data – Part 1. Biogeosciences, 5, 433-450. Source of the 50 % and 80 % criteria.
Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> f = np.exp(-((X - 40.0) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) >>> f /= f.sum() * 400.0 >>> fclim = xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> weights = truncate_to_contour(fclim) >>> landcover = xr.DataArray( ... np.where(X < 60.0, 42, 81), dims=("x", "y"), coords={"x": x, "y": y} ... ) >>> result = categorical_representativeness( ... weights, landcover, x, y, radii=(250.0, 1000.0) ... ) >>> list(result.columns) ['radius', 'dominant_class', 'p_footprint', 'p_target', 'chi2', 'p_value', 'dof', 'level'] >>> print(result[["radius", "dominant_class", "level"]].to_string(index=False)) radius dominant_class level 250.0 42 low 1000.0 42 low
p_footprintandp_targetare reported here as fractions, whileclassify_categorical()andevaluate_landcover()work in percentages:>>> round(float(result["p_footprint"].iloc[0]), 4) 0.5588
- fluxfootprints.classify_categorical(p_footprint: float, p_target: float, p_value: float, alpha: float = 0.05, high_percent: float = 80.0, medium_percent: float = 50.0) Level[source]
Assign the land-cover representativeness level, Sect. 2.4.
The 50 % and 80 % criteria follow Goeckede et al. (2008).
- Parameters:
- p_footprintfloat
Percentage of the dominant land-cover type within the footprint [%].
- p_targetfloat
Percentage of the same type within the target area [%].
- p_valuefloat
Chi-square p-value comparing the two compositions.
- alphafloat, default
DEFAULT_ALPHA Significance level; compositions with
p_value >= alphaare treated as not significantly different.- high_percentfloat, default
HIGH_COVER_PERCENT Percentage both compositions must reach for
HIGH[%].- medium_percentfloat, default
MEDIUM_COVER_PERCENT Percentage both compositions must reach for
MEDIUM[%].
- Returns:
- Level
HIGHwhen both percentages are >= high_percent and the compositions do not differ significantly;MEDIUMwhen both are >= medium_percent and the compositions do not differ significantly;LOWotherwise.
Examples
>>> classify_categorical(92.0, 88.0, 0.42) # both >= 80 %, compositions agree <Level.HIGH: 'high'> >>> classify_categorical(92.0, 61.0, 0.31) # both >= 50 % <Level.MEDIUM: 'medium'> >>> classify_categorical(92.0, 20.0, 0.001) # target area is something else <Level.LOW: 'low'>
The thresholds are arguments, so a stricter rule needs no rewriting:
>>> classify_categorical(92.0, 88.0, 0.42, high_percent=90.0) <Level.MEDIUM: 'medium'>
- fluxfootprints.classify_continuous(r_squared: float, slope: float, intercept: float, p_value: float, alpha: float = 0.05, high_r_squared: float = 0.8, medium_r_squared: float = 0.6, slope_tolerance: float = 0.1, intercept_tolerance: float = 0.1) Level[source]
Assign the continuous-field representativeness level, Sect. 2.4.
- Parameters:
- r_squaredfloat
Coefficient of determination of the model II regression.
- slopefloat
Regression slope beta_1.
- interceptfloat
Regression intercept beta_0.
- p_valuefloat
Significance of the regression.
- alphafloat, default
DEFAULT_ALPHA Significance level for the MEDIUM criterion.
- high_r_squaredfloat, default
HIGH_R_SQUARED R^2 required for
HIGH.- medium_r_squaredfloat, default
MEDIUM_R_SQUARED R^2 required for
MEDIUM.- slope_tolerancefloat, default
SLOPE_TOLERANCE Half-width of the slope window about 1.0 required for
HIGH.- intercept_tolerancefloat, default
INTERCEPT_TOLERANCE Half-width of the intercept window about 0.0 required for
HIGH.
- Returns:
- Level
HIGHwhenr_squared >= high_r_squaredwith the slope within slope_tolerance of 1.0 and the intercept within intercept_tolerance of 0.0;MEDIUMwhenr_squared >= medium_r_squaredandp_value < alpha;LOWotherwise.
Notes
The intercept tolerance is absolute and calibrated to EVI’s 0-1 range. A field on a different scale, e.g. land surface temperature in kelvin, needs a rescaled intercept_tolerance before this classification is meaningful.
A degenerate fit – too few distinct values to orient a slope, say – arrives here as non-finite statistics, which fail every comparison below and so fall through to LOW.
Examples
>>> classify_continuous(0.94, 0.96, 0.02, 1e-6) <Level.HIGH: 'high'> >>> classify_continuous(0.71, 0.83, 0.05, 1e-4) # fits, but not one-to-one <Level.MEDIUM: 'medium'> >>> classify_continuous(0.31, 0.62, 0.18, 0.2) <Level.LOW: 'low'>
A field on another scale needs a rescaled intercept tolerance:
>>> classify_continuous(0.94, 0.96, 0.02, 1e-6, intercept_tolerance=0.01) <Level.MEDIUM: 'medium'>
- fluxfootprints.climatology_metrics(fclim: DataArray, dx: float | None = None, dy: float | None = None, fraction: float = 0.8, seasonal_overlap: float | None = None, daynight_overlap: float | None = None) ClimatologyMetrics[source]
Summarise a footprint climatology with the metrics of Sect. 2.2.
- Parameters:
- fclimxarray.DataArray
Footprint climatology with dims
(x, y)[m⁻²]. An array already truncated bytruncate_to_contour()is also accepted and is summarised on the contour it was cut at, rather than being truncated a second time; see Notes.- dx, dyfloat, optional
Grid spacing [m]. Inferred from coordinates when omitted.
- fractionfloat, default 0.8
Source-weight fraction defining the truncation contour. Ignored for an already-truncated fclim, whose own fraction is used instead.
- seasonal_overlap, daynight_overlapfloat, optional
Precomputed overlap indices to carry into the result, from
seasonal_overlap()anddaynight_overlap(). Both are site-year properties spanning several climatologies and so cannot be derived from fclim alone.
- Returns:
- ClimatologyMetrics
Fetch, area, symmetry, cell count, contour level, and enclosed mass. Symmetry below
ASYMMETRY_THRESHOLD(0.30) is what the paper calls a relatively asymmetric climatology.
Notes
Truncated input is recognised by the
contour_fractionattribute thattruncate_to_contour()writes, and its fraction and contour level are read back from there.enclosed_fractionis thennan: the mass outside the contour has been zeroed, so what share of the domain’s source weight was retained is no longer recoverable from the array. Pass the raw climatology to get it.contour_levelisnantoo where a truncated input has lost that attribute, as a climatology sliced out of the stack frommonthly_climatologies()has: the level varies from group to group, so it is a data variable of that Dataset rather than an attribute of the stacked array. Read it fromds.contour_levelalongside.Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> f = np.exp(-((X - 40.0) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) >>> f /= f.sum() * 400.0 >>> fclim = xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> metrics = climatology_metrics(fclim) >>> round(metrics.fetch, 2), round(metrics.area, 1), metrics.n_cells (181.11, 64400.0, 161) >>> round(metrics.symmetry, 3), round(metrics.enclosed_fraction, 4) (0.625, 0.8188)
The overlap indices are optional, and stay None unless supplied:
>>> metrics.seasonal_overlap is None True
- fluxfootprints.compute_aerodynamic_params(df: DataFrame, inst_height: float | str | Series | ndarray, crop_height: float | str | Series | ndarray, veg_type: str | None = None, z0_ratio: float | str | Series | ndarray | None = None, d_h_ratio: float | str | Series | ndarray | None = None) DataFrame[source]
Calculate effective measurement height (zm) and roughness length (z0).
Requires inst_height and crop_height, plus EITHER a veg_type preset OR custom ratio parameters (z0_ratio / d_h_ratio).
If both veg_type and z0_ratio/d_h_ratio are provided, the user-provided ratios will be used instead of the VEG_PRESETS parameters.
- fluxfootprints.continuous_representativeness(pairs: DataFrame, radii: Sequence[float] = (250, 500, 1000, 1500, 2000, 3000), min_matches: int = 6, alpha: float = 0.05, ci_level: float = 0.95, ci_method: str = 'analytical', n_boot: int = 2000, random_state: int | Generator | None = None) DataFrame[source]
Evaluate continuous-field representativeness per target area, Sect. 2.4.
The continuous counterpart of
categorical_representativeness(), and the site-level step that followssensor_location_bias_series(): the matched periods of one site are regressed against each target radius by Eq. 7 and classified HIGH, MEDIUM, or LOW byclassify_continuous().\[EVI_{target} \sim \beta_0 + \beta_1 EVI_{footprint}\]- Parameters:
- pairspandas.DataFrame
Tidy frame of matched periods, one row per period and radius, with columns
timePeriod label, carried only to identify the matches; the fit does not use it and does not sort by it.
radiusTarget-area radius [m].
value_footprintFootprint-weighted value, EVI_footprint of Eq. 5.
value_targetTarget-area mean over the disc of that radius, EVI_target.
Exactly the frame
sensor_location_bias_series()returns; any further columns,deltaandwithin_thresholdamong them, are ignored.- radiisequence of float, default TARGET_RADII
Target-area radii [m] to report, in the order given. Radii of pairs not listed here are ignored.
- min_matchesint, default 6
Fewest matched periods a radius may be fitted on. Below it the row is reported with
sufficient = Falseand a missing level rather than a fitted one, following the paper, which regressed only the 166 of 214 sites holding at least six matches. Must be at least 3.- alphafloat, default 0.05
Significance level for the MEDIUM criterion.
- ci_levelfloat, default 0.95
Confidence level of the reported interval limits.
- ci_method{‘analytical’, ‘bootstrap’}, default ‘analytical’
Interval estimator, passed to
rma_regression().- n_bootint, default 2000
Number of resamples, used only when
ci_method='bootstrap'.- random_stateint, numpy.random.Generator, or None, optional
Seed or generator for the bootstrap.
- Returns:
- pandas.DataFrame
One row per requested radius, in the order given, with columns
radiusTarget-area radius [m].
nMatched periods entering the row: rows of pairs at that radius holding a finite value on both sides.
intercept,slopebeta_0 and beta_1 of Eq. 7. A slope below 1 means the footprint saw systematically higher values than the target area, which held at every radius in the paper.
r_squared,p_valueSquared Pearson correlation and its two-sided significance.
rmse,maeRoot mean square and mean absolute difference between the footprint-weighted and target-area values, as in Table 1. These describe the two series against the 1:1 line, not the residuals about the fitted line, and so are reported whenever the radius holds a match at all.
intercept_lower,intercept_upper,slope_lower,slope_upperConfidence limits at ci_level, the square-bracketed columns of Table 1.
sufficientWhether
n >= min_matches, i.e. whether the row was fitted.levelLevelmember, orpd.NAwhere sufficient is False.HIGHwhenr_squared >= 0.8with slope in [0.9, 1.1] and intercept in [-0.1, 0.1];MEDIUMwhenr_squared >= 0.6andp_value < alpha;LOWotherwise.
DataFrame.attrsrecordsmin_matches,alpha,ci_level, andci_method.
- Raises:
- KeyError
If pairs is missing any of the four required columns.
- ValueError
If pairs is not a DataFrame; if radii is empty or holds a radius that is not positive and finite; or if min_matches is below 3, which is the fewest pairs a regression can be fitted on at all.
See also
sensor_location_bias_seriesProduces the frame this consumes.
rma_regressionThe model II fit applied per radius.
classify_continuousThe three-level index applied here.
categorical_representativenessThe land-cover counterpart.
Notes
A radius listed in radii but absent from pairs comes back with
n = 0andsufficient = Falserather than raising, so a frame built over a shorter set of radii still reports against the full series. Radii are matched to within floating-point tolerance, so an integer 250 finds a 250.0 in the frame.The insufficient rows are the point of min_matches: a site-radius with three or four matches can be fitted, and will be classified LOW or HIGH by the same criteria, but on evidence too thin to mean what the index says. Chu et al. (2021) set the bar at six matched months, with a median of 13 per site. Filter on
sufficientrather than onlevel.notna()if the distinction matters downstream.Rows are fitted independently per radius. That is deliberate: the footprint-weighted value is the same across radii while the target-area mean is not, so the systematic bias grows with the disc – from a slope of 0.96 at 250 m to 0.80 at 3000 m in Table 1 – and pooling radii would average that trend away.
References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Sect. 2.4, Table 1.
Examples
>>> import numpy as np, pandas as pd, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> def clim(cx): ... f = np.exp(-((X - cx) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) ... f /= f.sum() * 400.0 ... return xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> clims = [clim(40.0 + 8 * i) for i in range(8)] >>> scenes = [ ... xr.DataArray( ... 0.30 + 0.0005 * X + 0.01 * i, dims=("x", "y"), coords={"x": x, "y": y} ... ) ... for i in range(8) ... ] >>> pairs = sensor_location_bias_series( ... { ... pd.Timestamp(f"2020-{m:02d}-01"): (truncate_to_contour(c), s) ... for m, (c, s) in enumerate(zip(clims, scenes), 1) ... }, ... x, ... y, ... radii=(250.0, 1000.0), ... ) >>> site = continuous_representativeness(pairs, radii=(250.0, 1000.0)) >>> print( ... site[["radius", "n", "slope", "intercept", "r_squared", "level"]] ... .round(4) ... .to_string(index=False) ... ) radius n slope intercept r_squared level 250.0 8 0.7187 0.0699 0.9999 medium 1000.0 8 0.7187 0.0699 0.9999 medium
- fluxfootprints.contour_level_for_fraction(fclim: DataArray, dx: float | None = None, dy: float | None = None, fraction: float = 0.8) float[source]
Find the footprint density enclosing a given fraction of the source weight.
Sorts the climatology in descending order and accumulates
value * dx * dyuntil fraction is reached, following the same construction asget_source_area_contour().- Parameters:
- fclimxarray.DataArray
Footprint climatology with dims
(x, y)and density units [m⁻²].- dx, dyfloat, optional
Grid spacing [m]. Inferred from the
x/ycoordinates when omitted.- fractionfloat, default 0.8
Source-weight fraction to enclose, in (0, 1).
- Returns:
- float
Footprint density [m⁻²] at the contour. Pass as
levelstomatplotlib.axes.Axes.contour.
- Raises:
- ValueError
If fraction is outside (0, 1), or fclim carries no positive mass.
Notes
Because the package’s climatologies integrate to the mean captured fraction rather than to 1, a fraction larger than the captured mass saturates at the smallest positive density on the grid rather than raising.
Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> f = np.exp(-((X - 40.0) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) >>> f /= f.sum() * 400.0 # a density: integrates to 1 over dx * dy >>> fclim = xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> round(contour_level_for_fraction(fclim), 9) 5.351e-06
- fluxfootprints.crosswind_integrated_footprint(x: ndarray | float, xi: float, m: float, n: float) ndarray | float[source]
Compute the cross-wind-integrated footprint f(x, z).
This function implements Eq. (21) from Kormann & Meixner (2001), which describes the probability density function of source area contributions in the along-wind direction, integrated over the cross-wind direction.
- Parameters:
- xfloat or np.ndarray
Downwind distance(s) from the tower (m).
- xifloat
Footprint length-scale ξ(z) computed using length_scale_xi (m).
- mfloat
Power law exponent for wind speed profile.
- nfloat
Power law exponent for eddy diffusivity profile.
- Returns:
- float or np.ndarray
Cross-wind-integrated footprint value(s) at distance x.
- fluxfootprints.cumulative_fetch(x_p: float, xi: float, m: float, n: float) float[source]
Calculate the cumulative fetch P(x_p), the fraction of flux originating upwind of x_p.
Implements Eq. (29) from Kormann & Meixner (2001), returning the cumulative contribution to the flux footprint up to a specified downwind distance.
- Parameters:
- x_pfloat
Downwind distance from the tower (m) at which the cumulative flux contribution is evaluated.
- xifloat
Characteristic length-scale ξ(z) computed using length_scale_xi (m).
- mfloat
Power law exponent for wind speed profile.
- nfloat
Power law exponent for eddy diffusivity profile.
- Returns:
- float
Fraction of total flux (between 0 and 1) originating upwind of x_p.
- fluxfootprints.daynight_overlap(day: DataArray, night: DataArray, dim: str = 'month') float[source]
Compute the daytime-nighttime footprint overlap index O80_daynight, Eq. 3.
\[O_{80,daynight} = \frac{1}{K} \sum_{k=1}^{K} \sum_{i=1}^{I} \left( \varphi^{day}_{ik} \varphi^{night}_{ik} \right)^{1/2}\]The
overlap()of the paired daytime and nighttime climatologies of each month, averaged over the K months of a site-year.- Parameters:
- day, nightxarray.DataArray
Paired monthly daytime and nighttime climatologies stacked over dim, on a common grid and each month already rescaled to sum to 1 – the output of
truncate_to_contour().- dimstr, default “month”
Name of the dimension the months are stacked over. The two arrays are paired by position along it; where both carry a coordinate for it, the coordinates must agree.
- Returns:
- float
Overlap index in [0, 1]. One indicates perfectly overlapping day and night climatologies; around 93 % of the site-years in the paper exceeded 0.8, because a climatology aggregates many half-hours from many wind directions and the day and night source areas share their high-weight core even where the nighttime one reaches farther.
- Raises:
- TypeError
If day or night is not a DataArray.
- ValueError
If either array carries no dim dimension or holds no months along it; if the two hold different numbers of months or disagree over the dim coordinate; if their grids differ; if any weight is non-finite or negative; or if any month does not sum to 1.
See also
overlapThe per-month kernel this averages.
daynight_overlap_indexThe same index from sequences of climatologies.
seasonal_overlapThe companion index across the months of a site-year.
Notes
Unlike Eq. 2, this index pairs the two climatologies month by month before averaging, so a month whose day and night footprints diverge lowers the result without zeroing it. Nighttime climatologies reached about 45 % farther and covered about 90 % more area than daytime ones at more than 95 % of the site-years in the paper.
Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> def weights(cx, spread=80.0): ... f = np.exp(-((X - cx) ** 2 + Y ** 2) / (2 * spread ** 2)) ... f /= f.sum() # the overlap indices take unit-sum weights ... return xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> day = xr.concat( ... [weights(0.0), weights(20.0), weights(40.0)], dim="month" ... ).assign_coords(month=[1, 2, 3]) >>> night = xr.concat( ... [weights(c, 110.0) for c in (0.0, 20.0, 40.0)], dim="month" ... ).assign_coords(month=[1, 2, 3]) >>> round(daynight_overlap(day, night), 4) # nighttime reaches farther 0.9731
- fluxfootprints.daynight_overlap_index(daytime: Sequence[DataArray], nighttime: Sequence[DataArray]) float[source]
Compute the daytime-nighttime footprint overlap index O80_daynight, Eq. 3.
Adapter over
daynight_overlap()for monthly climatologies held as sequences rather than stacked over a dimension. See that function for the equation.- Parameters:
- daytime, nighttimesequence of xarray.DataArray
Paired monthly daytime and nighttime climatologies for one site-year, each with dims
(x, y)on a common grid and each summing to 1.
- Returns:
- float
Overlap index in [0, 1]. One indicates perfectly overlapping day and night climatologies; around 93 % of the site-years in the paper exceeded 0.8.
- Raises:
- ValueError
If the two sequences differ in length, or their grids differ.
Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> def weights(cx, spread=80.0): ... f = np.exp(-((X - cx) ** 2 + Y ** 2) / (2 * spread ** 2)) ... f /= f.sum() # the overlap indices take unit-sum weights ... return xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> day = [weights(c) for c in (0.0, 20.0)] >>> night = [weights(c, 110.0) for c in (0.0, 20.0)] >>> round(daynight_overlap_index(day, night), 4) 0.9732
- fluxfootprints.effective_fetch(fraction: float, xi: float, m: float, n: float) float[source]
Invert the cumulative fetch function to determine the fetch distance x_p for a given flux fraction.
Solves for x_p such that cumulative_fetch(x_p) = fraction, which identifies the distance upwind from which a specified fraction of the total flux originates.
- Parameters:
- fractionfloat
Desired cumulative flux contribution (must be in the open interval (0, 1)).
- xifloat
Characteristic length-scale ξ(z) computed using length_scale_xi (m).
- mfloat
Power law exponent for wind speed profile.
- nfloat
Power law exponent for eddy diffusivity profile.
- Returns:
- float
Effective fetch distance x_p (m) upwind of the sensor that contributes the given flux fraction.
- Raises:
- ValueError
If fraction is not in the open interval (0, 1).
- fluxfootprints.evaluate_landcover(fclim: DataArray, classes: DataArray, radii: Sequence[float] = (250, 500, 1000, 1500, 2000, 3000), dx: float | None = None, dy: float | None = None, fraction: float = 0.8, alpha: float = 0.05) list[CategoricalResult][source]
Evaluate land-cover representativeness across a series of target areas.
Identifies the dominant land-cover type within the footprint, compares its share and the full composition against each target area with a chi-square test, and classifies the result (Sect. 2.4).
- Parameters:
- fclimxarray.DataArray
Footprint climatology with dims
(x, y)[m⁻²].- classesxarray.DataArray
Categorical land-cover raster on the same grid. Use
sample_raster_on_grid()to bring an external product onto it.- radiisequence of float, default TARGET_RADII
Target-area radii [m].
- dx, dyfloat, optional
Grid spacing [m]. Inferred from coordinates when omitted.
- fractionfloat, default 0.8
Source-weight fraction defining the truncation contour.
- alphafloat, default 0.05
Significance level for the chi-square test.
- Returns:
- list of CategoricalResult
One entry per radius, in the order given. The shares and both compositions are percentages, as the paper reports them, not the fractions
categorical_representativeness()returns.
- Raises:
- ValueError
If fclim and classes are not on the same grid; if fclim carries no
xorycoordinate to lay the discs out on; if radii is empty or holds an unusable radius; or if no cell carries both source weight and a class code.
See also
categorical_representativenessThe per-radius comparison this wraps.
assess_representativenessThe whole-record driver, which reports the same comparison as tidy rows for every month and period.
evaluate_vegetation_indexThe continuous-field counterpart.
representativeness_summaryFlattens these results into a table.
Notes
The chi-square test compares footprint-weighted counts against target-area counts; classes absent from both are dropped. The paper found 34 of 214 sites where the land-cover product disagreed with the site’s own IGBP metadata, so a disagreement between this result and site metadata is worth checking against the product before trusting it.
A raw climatology is truncated here at fraction and rescaled to unit sum; one already cut by
truncate_to_contour()is rescaled but not cut again, so a slice ofmonthly_climatologies()can be passed straight in.References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Sect. 2.4.
Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> f = np.exp(-((X - 40.0) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) >>> f /= f.sum() * 400.0 >>> fclim = xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> landcover = xr.DataArray( ... np.where(X < 60.0, 42, 81), dims=("x", "y"), coords={"x": x, "y": y} ... ) >>> results = evaluate_landcover(fclim, landcover, radii=(250.0, 1000.0)) >>> [(entry.radius, entry.dominant_class, entry.level) for entry in results] [(250.0, 42, <Level.LOW: 'low'>), (1000.0, 42, <Level.LOW: 'low'>)]
Unlike
categorical_representativeness(), the percentages here are in percent:>>> round(results[0].p_footprint, 2) 55.88
- fluxfootprints.evaluate_vegetation_index(climatologies: Sequence[DataArray], fields: Sequence[DataArray], radii: Sequence[float] = (250, 500, 1000, 1500, 2000, 3000), dx: float | None = None, dy: float | None = None, fraction: float = 0.8, alpha: float = 0.05, bias_threshold: float = 0.1) list[ContinuousResult][source]
Evaluate continuous-field representativeness across a series of target areas.
For each matched climatology / field pair the footprint-weighted value (Eq. 5), target-area mean, and sensor location bias (Eq. 6) are computed; the pairs are then regressed per radius (Eq. 7) and classified.
- Parameters:
- climatologiessequence of xarray.DataArray
Footprint climatologies with dims
(x, y)[m⁻²], one per period. In the paper these are monthly climatologies matched to the retrieval date of the underlying Landsat scene.- fieldssequence of xarray.DataArray
Continuous fields on the same grid, matched element-wise to climatologies, e.g. Landsat EVI.
- radiisequence of float, default TARGET_RADII
Target-area radii [m].
- dx, dyfloat, optional
Grid spacing [m]. Inferred from coordinates when omitted.
- fractionfloat, default 0.8
Source-weight fraction defining the truncation contour.
- alphafloat, default 0.05
Significance level for the regression.
- bias_thresholdfloat, default 0.10
|Delta|below which a period counts as representative, reported asContinuousResult.within_threshold.
- Returns:
- list of ContinuousResult
One entry per radius, in the order given.
biasholds the biases of the periods that entered that radius’s regression, andlevelis None where fewer than three of them survived – a raster covering only part of the larger discs can leave a radius unfittable while the smaller ones are fine.
- Raises:
- ValueError
If the two sequences differ in length; if fewer than three pairs are supplied; if the first climatology carries no
xorycoordinate; or if a climatology and its field are not on one grid.
See also
sensor_location_biasEq. 6, computed once per pair here.
continuous_representativenessThe per-radius regression this wraps.
assess_representativenessThe whole-record driver, which matches scenes to months itself and reports the per-period biases alongside the fit.
evaluate_landcoverThe categorical counterpart.
representativeness_summaryFlattens these results into a table.
Notes
The paper required at least six matched pairs for a site-level regression, with a median of 13 per site. Fewer pairs will fit but the resulting level should not be trusted.
Pairing is the caller’s job: the two sequences are matched element by element, in the order given. The paper pairs each Landsat scene with the climatology of the month it was retrieved in.
within_thresholdaverages the flags of the periods whose bias is defined, so a scene that did not reach the disc lowers the count of periods rather than counting as a failure – the convention behind the percentages of Sect. 3.3 and Fig. 7.References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Sect. 2.4, Table 1.
Examples
>>> import numpy as np, pandas as pd, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> def clim(cx): ... f = np.exp(-((X - cx) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) ... f /= f.sum() * 400.0 ... return xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> clims = [clim(40.0 + 8 * i) for i in range(8)] >>> scenes = [ ... xr.DataArray( ... 0.30 + 0.0005 * X + 0.01 * i, dims=("x", "y"), coords={"x": x, "y": y} ... ) ... for i in range(8) ... ] >>> results = evaluate_vegetation_index(clims, scenes, radii=(250.0, 1000.0)) >>> [(entry.radius, round(entry.slope, 4)) for entry in results] [(250.0, 0.7187), (1000.0, 0.7187)] >>> [(entry.radius, entry.n, entry.level) for entry in results] [(250.0, 8, <Level.MEDIUM: 'medium'>), (1000.0, 8, <Level.MEDIUM: 'medium'>)]
- fluxfootprints.export_contour_stats_csv(df: DataFrame, model: FFPModel, summaries: SummaryResult, station_lat: float, station_lon: float, csv_path: str | Path, rn_col: str | None = None, h_col: str | None = None, le_col: str | None = None, g_col: str | None = None, method: str = 'auto', crs_out: str | int = 'auto', levels=(0.8,)) Path[source]
Write the area and centroid of footprint source-area contours to a CSV.
For each layer, contour level and time slice, the smallest region holding fraction
rof the normalised footprint is traced as a polygon. The polygon is shifted from tower-relative metres intocrs_out, and its area and centroid are written as one row.- Parameters:
- dfpd.DataFrame
Not used. Kept for signature compatibility with the other export helpers.
- modelFFPModel
A footprint model that has been run. Its
xandyattributes are 1-D coordinate arrays in metres relative to the tower.- summariesSummaryResult
Output of the daily/monthly summary step. The attributes
f_daily_mean,f_monthly_mean,f_daily_et_weightedandf_monthly_et_weightedare read asxr.DataArrayobjects with atimedimension.Nonelayers are skipped.- station_lat, station_lonfloat
Tower latitude and longitude in decimal degrees (WGS 84).
- csv_pathstr or Path
Path of the CSV file to write. Overwritten if it exists.
- rn_col, h_col, le_col, g_colstr, optional
Not used. Kept for signature compatibility.
- method{“auto”, “skimage”, “rasterio”}, default “auto”
Contour extraction backend passed to
_make_contour_polygon_from_field_alt."auto"tries scikit-image first and falls back to rasterio.- crs_outstr or int, default “auto”
CRS the polygons are placed in before area and centroid are computed.
"auto"uses the local WGS 84 UTM zone. Any other value must be a projected CRS in metres, because the grid offsets are added directly to the tower’s projected coordinates and area is converted from m^2.- levelsiterable of float, default (0.8,)
Cumulative source-area fractions (0-1) to contour.
- Returns:
- Path
Path of the written CSV.
Notes
The CSV has the columns
layer,time(ISO 8601),r(contour level),area_ha(polygon area in hectares),centroid_lonandcentroid_lat(WGS 84 degrees). Only the largest polygon is kept for each slice, so a footprint split into several patches is under-reported. Slices where no contour can be traced, such as an all-zero or all-NaN field, are skipped without a row.
- fluxfootprints.export_contours_gpkg(model: FFPModel, summaries: SummaryResult, df: DataFrame, station_lat: float, station_lon: float, gpkg_path: str | Path, crs_out: str | int = 'auto', levels: tuple = (0.8,), stats_csv: str | Path | None = None, centroid_out: str | int = 4326, rn_col: str | None = None, h_col: str | None = None, le_col: str | None = None, g_col: str | None = None, contour_method: str = 'auto') Path[source]
Export source-area contours to GeoPackage with energy balance stats.
Adapted to work with FFPModel output structure.
- fluxfootprints.export_rasters_geotiff(model: FFPModel, summaries: SummaryResult, station_lat: float, station_lon: float, out_dir: str | Path, crs_out: str | int = 'auto', which=('daily_mean', 'monthly_mean', 'daily_etw', 'monthly_etw'), prefix='ffp', dtype='float32', nodata=0.0) Path[source]
Export each time slice of the footprint summaries as a single-band GeoTIFF.
The footprint grid (
model.x,model.y) is given in metres relative to the tower. It is first placed in the local WGS 84 UTM zone by offsetting it with the tower’s projected coordinates. Ifcrs_outnames a different CRS, each slice is then reprojected onto that CRS with bilinear resampling.- Parameters:
- modelFFPModel
A footprint model that has been run. Its
xandyattributes are 1-D, evenly spaced coordinate arrays (metres from the tower) with at least two points each.- summariesSummaryResult
Output of the daily/monthly summary step. The attributes
f_daily_mean,f_monthly_mean,f_daily_et_weightedandf_monthly_et_weightedare read asxr.DataArrayobjects with dims("time", "x", "y"). Missing orNoneattributes are skipped.- station_lat, station_lonfloat
Tower latitude and longitude in decimal degrees (WGS 84). Used to pick the UTM zone and to anchor the grid.
- out_dirstr or Path
Output directory. Created if it does not exist.
- crs_outstr or int, default “auto”
Target CRS for the output rasters.
"auto"writes in the local UTM zone without resampling. Any other value accepted bypyproj.CRS.from_user_input(e.g.4326,"EPSG:5070") triggers reprojection.- whichiterable of str, default (“daily_mean”, “monthly_mean”, “daily_etw”, “monthly_etw”)
Layers to export. Valid names are
"daily_mean","monthly_mean","daily_etw"and"monthly_etw".- prefixstr, default “ffp”
Filename prefix.
- dtypestr, default “float32”
Output raster data type. Also selects the deflate predictor: 3 for floating-point types, 2 for integer types.
- nodatafloat, default 0.0
Nodata value written to the raster metadata. Reprojected cells outside the source extent are also filled with this value.
- Returns:
- Path
The output directory.
- Raises:
- ValueError
If
model.xormodel.yhas fewer than two points, or ifwhichcontains an unknown layer name.
Notes
Files are named
{prefix}_{layer}_{YYYYMMDD}.tiffor daily layers and{prefix}_{layer}_{YYYYMM}.tiffor monthly layers. Rasters at least 256 x 256 pixels are written tiled with 256-pixel blocks; smaller rasters are written striped.
- fluxfootprints.export_representativeness_gpkg(results: DataFrame, geometry: GridGeometry, gpkg_path: str | Path, layer: str = 'representativeness') Path[source]
Write representativeness results to a GeoPackage as target-area discs.
Warning
Not implemented. This function is a placeholder and raises
NotImplementedErroron every call. The parameters and return value below describe the intended interface, not current behaviour.- Parameters:
- resultspandas.DataFrame
Tidy results carrying a
radiuscolumn, fromrepresentativeness_summary()or fromassess_representativeness()with its index reset.- geometryGridGeometry
Georeferencing for the footprint grid, from
footprint_grid_geometry().- gpkg_pathstr or pathlib.Path
Output GeoPackage path.
- layerstr, default “representativeness”
Layer name to write.
- Returns:
- pathlib.Path
The written path.
- Raises:
- ImportError
If
geopandasis not installed.- ValueError
If results carries no
radiuscolumn.
Notes
Each row becomes a buffered point centred on the tower with the row’s radius, so the layer draws as nested discs attributed with their representativeness levels.
Examples
>>> import pandas as pd >>> export_representativeness_gpkg( ... pd.DataFrame({"radius": [250.0]}), None, "out.gpkg" ... ) Traceback (most recent call last): ... NotImplementedError
- fluxfootprints.export_representativeness_tables(results: DataFrame, directory: str | Path, datasets: Sequence[str] = ('S4', 'S5', 'S6'), fmt: str = 'csv', prefix: str = 'representativeness', write_empty: bool = False, **kwargs: Any) dict[str, Path][source]
Write the published-schema tables of a result frame to CSV or Parquet.
A thin writer over
representativeness_table(): it builds each requested table and writes it as<prefix>_<dataset>.<ext>, so that the outputs sit next to Chu et al.’s Datasets S4-S6 (Zenodo, doi:10.5281/zenodo.4015350) with matching columns and units.- Parameters:
- resultspandas.DataFrame
Output of
assess_representativeness(). Frames from several sites may be concatenated first; theSITE_IDcolumn keeps them apart.- directorystr or pathlib.Path
Directory to write into. Created, parents included, if it does not exist.
- datasetssequence of str, default (“S4”, “S5”, “S6”)
Which tables to write, from
"S4","S5", and"S6".- fmt{“csv”, “parquet”}, default “csv”
Output format. Parquet needs
pyarroworfastparquet, and preserves the nullable integer and boolean dtypes that CSV flattens to text.- prefixstr, default “representativeness”
Leading part of each file name.
- write_emptybool, default False
Whether to write a header-only file for a table with no rows – an S4 from a run without a land-cover raster, say. When False such a table is skipped and simply does not appear in the returned mapping.
- **kwargs
Passed through to
pandas.DataFrame.to_csv()orto_parquet().index=Falseis set for CSV unless overridden.
- Returns:
- dict of str to pathlib.Path
Dataset number -> the path written, in the order requested. Datasets skipped as empty are absent.
- Raises:
- KeyError
If datasets names a table that is not one of the three.
- ValueError
If fmt is neither
"csv"nor"parquet", or if results is not a tidy result frame.- ImportError
If Parquet was asked for and no Parquet engine is installed.
See also
representativeness_tableBuilds one table without writing it.
assess_representativenessProduces the frame this writes.
export_representativeness_gpkgWrites the same results as target-area discs for mapping.
Notes
The tidy frame itself is the thing to keep for further analysis – these tables drop the climatology metrics and the per-month land-cover rows, which have no counterpart in the published supplement. Write it alongside with
results.to_parquet(...)when the full record matters.Examples
>>> import numpy as np, pandas as pd, xarray as xr >>> times = pd.date_range("2020-01-01", "2020-06-30 21:00", freq="3h") >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> rng = np.random.default_rng(0) >>> centres = rng.normal(30.0, 25.0, times.size) # a wandering wind direction >>> f = np.stack( ... [np.exp(-((X - c) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) for c in centres] ... ) >>> f /= f.sum(axis=(1, 2), keepdims=True) * 400.0 >>> f_2d = xr.DataArray( ... f, coords={"time": times, "x": x, "y": y}, dims=("time", "x", "y") ... ) >>> landcover = xr.DataArray( ... np.where(X < 60.0, 42, 81), dims=("x", "y"), coords={"x": x, "y": y} ... ) >>> scenes = { ... pd.Timestamp(f"2020-{m:02d}-15"): xr.DataArray( ... 0.30 + 0.0005 * X + 0.02 * m, dims=("x", "y"), coords={"x": x, "y": y} ... ) ... for m in range(1, 7) ... } >>> results = assess_representativeness( ... f_2d, ... 40.0, ... -111.9, ... site_id="US-Xyz", ... tz=-7, ... landcover=landcover, ... continuous=scenes, ... radii=(250.0, 1000.0), ... ) >>> import tempfile, pathlib >>> directory = pathlib.Path(tempfile.mkdtemp()) >>> written = export_representativeness_tables(results, directory) >>> sorted(written) ['S4', 'S5', 'S6'] >>> sorted(p.name for p in written.values()) ['representativeness_S4.csv', 'representativeness_S5.csv', 'representativeness_S6.csv'] >>> written["S6"].exists() True
- fluxfootprints.fetch_landsat_evi(x: float, y: float, crs: str | int | CRS, start: str | date | datetime | Timestamp, end: str | date | datetime | Timestamp, radius: float = 3000.0, max_cloud: float = 0.01, scale: float = 30.0, max_scenes: int | None = 200, project: str | None = None) list[DataArray][source]
Fetch the clear Landsat EVI scenes over a tower, one array per scene.
Merges Landsat 5 / TM and Landsat 8 / OLI Collection 2 Level-2 surface reflectance over
[start, end), computes EVI per Chu et al. (2021), Eq. 4, keeps only the scenes that see the target disc essentially clear, and returns each on a tower-centred grid – the input series of the continuous evaluation of Sect. 2.4.- Parameters:
- x, yfloat
Tower position in crs [m].
- crsstr, int, or pyproj.CRS
Projected CRS the coordinates are in, and the scenes are returned in. Geographic CRSs are refused.
- start, endstr, datetime.date, datetime.datetime, or pandas.Timestamp
Retrieval window. start is inclusive and end exclusive, as
ee.ImageCollection.filterDatereads them.- radiusfloat, default 3000.0
Half-width of the square fetched [m], and the radius of the disc the cloud screening is applied over.
- max_cloudfloat, default 0.01
Fraction of that disc allowed to lack a clear observation, i.e. to be cloud, cloud shadow, cirrus, fill, or off-swath. The default is the paper’s < 1 % criterion.
- scalefloat, default 30.0
Cell size [m]; 30 m is Landsat’s native resolution.
- max_scenesint or None, default 200
Refuse rather than issue more than this many pixel requests, since each surviving scene costs one. None lifts the cap.
- projectstr, optional
Cloud project, forwarded to
initialize()on first use.
- Returns:
- list of xarray.DataArray
One array per surviving scene, oldest first, each on dims
("y", "x")in crs, float64 withnanwhere the pixel was cloudy or off-swath, named"evi"and georeferenced for.rio. Each carries a scalartimecoordinate, and its scene id, sensor, and obscured fraction in.attrs. Empty if no scene clears max_cloud.
- Raises:
- ImportError
If
earthengine-apiorrioxarrayis not installed.- ValueError
If crs is geographic or unparseable; if radius, scale, or the dates are invalid; if max_cloud is outside
[0, 1]; if the grid would exceed the Earth Engine response limit; or if more than max_scenes scenes survive the screening.- RuntimeError
If Earth Engine cannot be initialised, or refuses a request.
See also
fetch_nlcdThe categorical field of the same analysis.
fluxfootprints.evaluate_vegetation_indexConsumes these, once warped onto the footprint grid and paired with matching climatologies.
fluxfootprints.sensor_location_biasEq. 6, computed per matched scene.
Notes
Landsat 7 / ETM+ is skipped, as Chu et al. (2021) skip it: the scan-line corrector failed in 2003, and the resulting gaps would bias a footprint-weighted mean by however much of the footprint they fell on. Landsat 9 is likewise absent, since the paper’s record ends in 2019 and this function reproduces its sensor set; a window after 2013 therefore returns Landsat 8 scenes alone.
Pairing scenes to climatologies is the caller’s job, as it is for
evaluate_vegetation_index(). The paper pairs each scene with the monthly climatology of the month it was retrieved in, which thetimecoordinate on each array supports directly.Each surviving scene is one
computePixelsrequest, so a wide window over a large radius is a slow call; narrowing the window is far cheaper than raising max_scenes.References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Sect. 2.4, Eq. 4.
Examples
>>> geom = footprint_grid_geometry(model.x, model.y, 40.0, -111.9) >>> scenes = fetch_landsat_evi( ... geom.x_origin, geom.y_origin, geom.crs, "2015-01-01", "2020-01-01" ... ) >>> [scene.attrs["date"] for scene in scenes[:2]] ['2015-04-12', '2015-06-15']
- fluxfootprints.fetch_nlcd(x: float, y: float, crs: str | int | CRS, year: int, radius: float = 3000.0, scale: float = 30.0, collection: str = 'USGS/NLCD_RELEASES/2019_REL/NLCD', band: str = 'landcover', project: str | None = None) DataArray[source]
Fetch an NLCD land-cover tile centred on a tower.
Reads one epoch of the National Land Cover Database from Earth Engine onto a square of half-width radius about
(x, y), the input of the categorical evaluation of Chu et al. (2021), Sect. 2.4.- Parameters:
- x, yfloat
Tower position in crs [m], e.g. the
x_originandy_originof aGridGeometry.- crsstr, int, or pyproj.CRS
Projected CRS the coordinates are in, and the tile is returned in. Geographic CRSs are refused: radius and scale are metres.
- yearint
NLCD release year to read, e.g. 2016 as in the paper. Must be an epoch collection publishes; the error message lists them.
- radiusfloat, default 3000.0
Half-width of the square fetched [m]. The default circumscribes the largest target area of
TARGET_RADII.- scalefloat, default 30.0
Cell size [m]; 30 m is NLCD’s native resolution.
- collectionstr, default NLCD_COLLECTION
Earth Engine ImageCollection to read the epoch from, should a newer release be wanted.
- bandstr, default “landcover”
Band of collection holding the class codes.
- projectstr, optional
Cloud project, forwarded to
initialize()on first use.
- Returns:
- xarray.DataArray
Class codes on dims
("y", "x")in crs, float64 withnanwhere NLCD is unclassified or absent, named"nlcd"and georeferenced for.rio. Ready for_align_raster(..., categorical=True), which is whatsample_raster_on_grid()andevaluate_landcover()build on.
- Raises:
- ImportError
If
earthengine-apiorrioxarrayis not installed.- ValueError
If crs is geographic or unparseable, if radius or scale is not positive, if the grid would exceed the Earth Engine response limit, or if year is not an epoch of collection.
- RuntimeError
If Earth Engine cannot be initialised, or refuses the request.
See also
fetch_landsat_eviThe continuous field of the same analysis.
fluxfootprints.evaluate_landcoverConsumes this, once warped onto the footprint grid.
fluxfootprints.sample_raster_on_gridDoes that warp for a raster on disc.
Notes
Class codes must be resampled with nearest neighbour, so pass
categorical=Truewhen aligning: bilinear resampling of Anderson codes invents classes that are not in the legend. The codes themselves survive the float64 representation exactly.Chu et al. (2021) used NLCD 2016 over the conterminous United States, and the Canadian Land Cover map for their Canadian sites; only the former is fetched here, so a site outside NLCD’s extent comes back all-
nanrather than wrong.References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Sect. 2.4.
Examples
>>> geom = footprint_grid_geometry(model.x, model.y, 40.0, -111.9) >>> nlcd = fetch_nlcd(geom.x_origin, geom.y_origin, geom.crs, 2016) >>> aligned, valid = _align_raster(nlcd, grid, categorical=True)
- fluxfootprints.fetch_nldas_forcing_dataset(lat, lon, time_start, time_end, token)[source]
Fetches all NLDAS forcing variables required for ASCE ET_o calculation
and combines them into a single pandas DataFrame.
- class fluxfootprints.ffp_climatology_new(df: DataFrame, domain: list | ndarray | tuple | None = None, dx: int = 10.0, dy: int = 10.0, nx: int = 1000, ny: int = 1000, rs: list | ndarray | None = None, rslayer: bool = False, smooth_data: bool = True, crop: bool = False, verbosity: int = 2, fig: bool = False, logger=None, time=None, **kwargs)[source]
Bases:
BaseFootprintModelCreate a footprint‑climatology from a series of individual flux‑footprint estimates using the simple 2‑D parameterisation of Kljun et al. (2015).
The routine
computes a footprint for every time step,
rotates each footprint into its observed wind direction,
aggregates all footprints onto a common grid, and
derives user‑requested source‑area contours (
rs).
The implementation is based on:
Kljun, N., Calanca, P., Rotach, M. W., & Schmid, H. P. (2015). A simple two‑dimensional parameterisation for flux‑footprint predictions (FFP). Geoscientific Model Development, 8, 3695‑3713. https://doi.org/10.5194/gmd‑8‑3695‑2015
- Parameters:
- dfpandas.DataFrame
DataFrame containing time series of micrometeorological inputs. Must contain the following required columns:
- zmfloat or Series
Measurement height above displacement height (i.e. z – d) [m].
- z0float or Series or None
Surface roughness length [m]. Either z0 or umean must be supplied per timestep; if z0 is missing it is derived from the log-wind profile using umean (see
calc_xr_footprint()).- umeanfloat or Series or None
Mean wind speed at zm [m s⁻¹]. Either umean or z0 must be supplied per timestep.
- hfloat or Series
Boundary‑layer height [m].
- olfloat or Series
Obukhov length [m].
- sigmavfloat or Series
Standard deviation of lateral velocity fluctuations [m s⁻¹].
- ustarfloat or Series
Friction velocity [m s⁻¹].
- wind_dirfloat or Series
Wind direction in degrees (0–360°, meteorological convention).
- domainarray_like of float, default=[-1000.0, 1000.0, -1000.0, 1000.0]
Domain limits
[xmin, xmax, ymin, ymax][m].- dx, dyfloat, default=10.0
Grid‑cell size in x and y directions [m].
- nx, nyint, default=1000
Number of grid cells in x and y. Ignored if dx/dy are supplied.
- rslist or ndarray, default=[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]
Source‑area contour fractions (e.g., 0.8 for 80%).
- rslayerbool, default=False
If True, allow calculations when zm lies in the roughness sub‑layer (RS). Warning: Model is formally invalid within the RS.
- smooth_databool, default=True
Apply a Gaussian convolution filter to smooth the footprint climatology.
- cropbool, default=False
Crop the output grid to the extent of the largest requested contour.
- verbosity{0, 1, 2}, default=2
Verbosity level: 0 = silent, 1 = fatal only, 2 = warning/info.
- figbool, default=False
Plot an example footprint on screen.
- Returns
- ——-
- resultsxarray.Dataset
Dataset containing: - footprint_climatology : DataArray (x, y)
Normalised 2D footprint climatology density values [m⁻²].
- domain_x1D coordinate array
x-axis grid coordinates [m].
- domain_y1D coordinate array
y-axis grid coordinates [m].
Methods
get_coordinates()Return x and y coordinate arrays.
get_footprint_climatology()Return the climatological footprint as xarray DataArray.
get_footprint_timeseries()Return time-resolved footprint if available.
get_results()Return complete results as xarray Dataset.
Sanitize DataFrame input range bounds and drop invalid timesteps.
run([return_result])Execute footprint calculation.
to_netcdf(filepath)Save results to netCDF file.
calc_xr_footprint
create_xr_dataset
define_domain
raise_ffp_exception
Notes
Implemented by N. Kljun & G. Fratini, originally in MATLAB, ported to Python 3.x (v 1.4, 11 Dec 2019).
Examples
>>> import pandas as pd >>> from fluxfootprints import ffp_climatology_new >>> # Prepare DataFrame with required columns >>> df = pd.DataFrame({ ... "zm": [2.5], "z0": [0.1], "umean": [3.2], "h": [800.0], ... "ol": [-50.0], "sigmav": [0.4], "ustar": [0.25], "wind_dir": [180.0] ... }) >>> model = ffp_climatology_new(df=df, dx=5.0, dy=5.0) >>> results = model.run()
- prep_df_fields()[source]
Sanitize DataFrame input range bounds and drop invalid timesteps.
Assumes df already contains standardized column names from build_climatology. z0 and umean are not both required: at least one must be available per timestep, since a missing z0 can be derived from the log-wind profile using umean (see
calc_xr_footprint()). Kljun et al. (2015) Eq. 27 validity bounds (stability, height, roughness sublayer) are evaluated per-timestep incalc_xr_footprint(), not here.
- fluxfootprints.footprint_2d(x: ndarray, y: ndarray, xi: float, m: float, n: float, u_zm: float, sigma_v: float) Tuple[ndarray, ndarray, ndarray][source]
Return 2-D footprint density φ(x, y, z_m).
- Parameters:
- x, y
1-D arrays of upstream and cross-stream distances (m). Positive x is up-wind.
- xi, m, n
Parameters returned by
length_scale_xi()andanalytical_power_law_parameters().- u_zm
Mean wind speed at measurement height (m sâ»Â¹).
- sigma_v
Standard deviation of cross-wind velocity fluctuations (m sâ»Â¹).
- Returns:
- X, Y, phi
Meshgrids of x, y and the footprint density φ (mâ»Â²).
- fluxfootprints.footprint_area(mask: DataArray, dx: float | None = None, dy: float | None = None) float[source]
Compute the footprint area A80, Sect. 2.2.
- Parameters:
- maskxarray.DataArray
Boolean contour mask with dims
(x, y). A climatology truncated bytruncate_to_contour()is equally accepted; seefootprint_fetch().- dx, dyfloat, optional
Grid spacing [m]. Inferred from the mask coordinates when omitted.
- Returns:
- float
Area enclosed by the contour [m²], as the cell count times
dx * dy.0.0if no cell is inside.
Notes
Counting whole cells makes the area a step function of the grid spacing: a contour is resolved to within one cell of its true extent, so coarse grids and small source areas do not mix.
Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> f = np.exp(-((X - 40.0) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) >>> f /= f.sum() * 400.0 # a density: integrates to 1 over dx * dy >>> fclim = xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> mask = footprint_contour_mask(fclim) >>> round(footprint_area(mask), 1) # 161 cells of 20 m x 20 m 64400.0
- fluxfootprints.footprint_at_points(x: ndarray, y: ndarray, xi: float, m: float, n: float, u_zm: float, sigma_v: float) ndarray[source]
Evaluate the closed-form 2-D footprint density directly at given points.
Implements the same Eq. (19)/(21) formulation as
crosswind_integrated_footprint()/footprint_2d(), but takesx/yas same-shaped arrays of arbitrary points (e.g. an already-rotated output grid) rather than building the density on the outer product of separate 1-D wind-aligned axes. This lets a footprint grid in a rotated/translated frame be filled by direct evaluation instead of computing it on a wind-aligned grid and then interpolating (e.g. viascipy.interpolate.griddata()) onto the target frame.- Parameters:
- x, y
Arrays of downwind distance and crosswind offset (m) in the wind-aligned frame, same shape. Points with
x <= 0are upwind of the sensor, where the model is undefined, and are assigned zero density.- xi, m, n
Parameters returned by
length_scale_xi()andanalytical_power_law_parameters().- u_zm
Mean wind speed at measurement height (m sâ»Â¹).
- sigma_v
Standard deviation of cross-wind velocity fluctuations (m sâ»Â¹).
- Returns:
- np.ndarray
Footprint density φ (mâ»Â²), same shape as
x/y.
- fluxfootprints.footprint_contour_mask(fclim: DataArray, dx: float | None = None, dy: float | None = None, fraction: float = 0.8) DataArray[source]
Build a boolean mask of the cells inside a source-weight contour.
- Parameters:
- fclimxarray.DataArray
Footprint climatology with dims
(x, y)[m⁻²].- dx, dyfloat, optional
Grid spacing [m]. Inferred from coordinates when omitted.
- fractionfloat, default 0.8
Source-weight fraction to enclose.
- Returns:
- xarray.DataArray
Boolean array with the dims and coords of fclim, True inside the contour.
See also
contour_level_for_fractionThe threshold this mask is built from.
truncate_to_contourMask and renormalise in one step.
Notes
Non-finite cells never enter the mask, so a climatology padded with NaN outside its domain is handled like one padded with zeros.
Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> f = np.exp(-((X - 40.0) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) >>> f /= f.sum() * 400.0 # a density: integrates to 1 over dx * dy >>> fclim = xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> mask = footprint_contour_mask(fclim) >>> mask.dtype dtype('bool') >>> int(mask.sum()) 161
- fluxfootprints.footprint_fetch(mask: DataArray, origin: tuple[float, float] = (0.0, 0.0)) float[source]
Compute the footprint fetch X80, Sect. 2.2.
- Parameters:
- maskxarray.DataArray
Boolean contour mask with dims
(x, y), as returned byfootprint_contour_mask(). A climatology truncated bytruncate_to_contour()is equally accepted, renormalised or not: its positive cells are exactly the cells inside the contour.- origintuple of float, default (0.0, 0.0)
Tower position
(x, y)in the coordinates of mask [m]. The package’s grids are tower-centred, so the default holds unless the array carries coordinates that put the tower somewhere else.
- Returns:
- float
Maximum distance from the tower to a cell inside the contour [m].
nanif no cell is inside.
- Raises:
- ValueError
If mask carries no
xorycoordinate to measure distance from.
Notes
Distances are measured to cell centres, as in
target_area_mask(), so the fetch understates the true reach of the contour by up to half a cell diagonal.Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> f = np.exp(-((X - 40.0) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) >>> f /= f.sum() * 400.0 # a density: integrates to 1 over dx * dy >>> fclim = xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> mask = footprint_contour_mask(fclim) >>> round(footprint_fetch(mask), 4) 181.1077
- fluxfootprints.footprint_grid_geometry(x: ndarray | Sequence[float], y: ndarray | Sequence[float], station_lat: float, station_lon: float, crs: str | int | CRS = 'auto') GridGeometry[source]
Georeference a footprint grid given in metres relative to the tower.
Uses the same convention as
export_rasters_geotiff(): the tower is projected into the local WGS 84 UTM zone (or crs), grid offsets are added to it, and the resulting north-up transform is anchored on the outer edge of the corner cells.- Parameters:
- x, yarray_like
Regularly spaced cell-centre offsets in metres, as produced by the footprint models (
model.x,model.y).- station_lat, station_lonfloat
Tower position in decimal degrees (WGS 84).
- crsstr, int, pyproj.CRS, default “auto”
Target CRS;
"auto"picks the local UTM zone.
- Returns:
- GridGeometry
- fluxfootprints.footprint_symmetry(w: DataArray, dx: float | None = None, dy: float | None = None, origin: tuple[float, float] = (0.0, 0.0)) float[source]
Compute the footprint symmetry index S80 of a truncated climatology, Eq. 1.
Convenience wrapper that takes the fetch and area off one array and feeds them to
symmetry_index().- Parameters:
- wxarray.DataArray
Truncated climatology from
truncate_to_contour(), or the boolean mask fromfootprint_contour_mask().- dx, dyfloat, optional
Grid spacing [m]. Inferred from coordinates when omitted.
- origintuple of float, default (0.0, 0.0)
Tower position
(x, y)in the coordinates of w [m].
- Returns:
- float
Symmetry index in [0, 1]; values below
ASYMMETRY_THRESHOLD(0.30) are the paper’s relatively asymmetric climatologies.nanif w holds no cell inside the contour.
Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> f = np.exp(-((X - 40.0) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) >>> f /= f.sum() * 400.0 # a density: integrates to 1 over dx * dy >>> fclim = xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> round(footprint_symmetry(truncate_to_contour(fclim)), 4) 0.625
- fluxfootprints.footprint_weighted_composition(weights: DataArray | ndarray, landcover: DataArray | ndarray) Series[source]
Compute the footprint-weighted composition of a categorical field.
The categorical counterpart of
footprint_weighted_value(): every class takes the share of the footprint weight that falls on it, which is P_footprint of Sect. 2.4 once multiplied by 100.- Parameters:
- weightsxarray.DataArray or numpy.ndarray
Footprint source weights with dims
(x, y), as forfootprint_weighted_value().- landcoverxarray.DataArray or numpy.ndarray
Categorical raster on the same grid, typically the integer codes of the consolidated NLCD / Land Cover of Canada groups of Table S6, NaN where it holds no data.
- Returns:
- pandas.Series
Weight fraction per class, indexed by class code in ascending order (index name
class) and summing to 1. Empty when no cell carries both weight and a class code.Series.attrsholdsretained_weightandn_cells, as onWeightedValue.
- Raises:
- ValueError
As for
footprint_weighted_value().
See also
target_area_compositionThe unweighted counterpart over a target disc.
Notes
Integral class codes come back as integers whatever the raster’s dtype, since the alignment path returns float64. The fractions are shares of the retained weight, so this composition and a target-area one over the same product are directly comparable class by class – the comparison
classify_categorical()rests on.Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> f = np.exp(-((X - 40.0) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) >>> f /= f.sum() * 400.0 >>> fclim = xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> weights = truncate_to_contour(fclim) >>> landcover = xr.DataArray( ... np.where(X < 60.0, 42, 81), dims=("x", "y"), coords={"x": x, "y": y} ... ) >>> composition = footprint_weighted_composition(weights, landcover) >>> {int(k): round(float(v), 4) for k, v in composition.items()} {42: 0.5588, 81: 0.4412}
- fluxfootprints.footprint_weighted_value(weights: DataArray | ndarray, raster: DataArray | ndarray) WeightedValue[source]
Compute the footprint-weighted value of a continuous field, Eq. 5.
\[EVI_{footprint} = \sum_{j=1}^{J} \varphi_j EVI_j\]- Parameters:
- weightsxarray.DataArray or numpy.ndarray
Footprint source weights with dims
(x, y), normally the truncated, renormalised climatology fromtruncate_to_contour(). Weights that sum to something other than 1 are accepted and renormalised; the retained fraction is then reported relative to their own total.- rasterxarray.DataArray or numpy.ndarray
Continuous field on the same grid, e.g. Landsat EVI, NaN where it holds no data.
sample_raster_on_grid()delivers an external product in exactly that form. DataArrays are compared cell for cell rather than aligned on their coordinates, so a mismatched grid raises instead of silently joining.
- Returns:
- WeightedValue
The weighted value, the fraction of the footprint weight that fell on cells holding data, and the number of contributing cells.
valueisnanandretained_weight0 when the raster covers no weighted cell.
- Raises:
- ValueError
If the two arrays are not on the same grid; if weights is empty or holds a non-finite or negative weight; or if it carries no weight at all.
See also
target_area_valueThe unweighted counterpart over a target disc.
sensor_location_biasCompares the two, Eq. 6.
footprint_weighted_compositionThe categorical counterpart.
Notes
Cells where raster is NaN are dropped and the surviving weights renormalised, so partial raster coverage biases the result toward the covered part of the source area rather than returning NaN outright.
retained_weightis what says how far that went: a value carrying 0.6 of the footprint weight is a different measurement from one carrying all of it, and Chu et al. (2021) kept only scenes covering the source area.Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> f = np.exp(-((X - 40.0) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) >>> f /= f.sum() * 400.0 >>> fclim = xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> weights = truncate_to_contour(fclim) >>> evi = xr.DataArray( ... 0.30 + 0.0005 * X, dims=("x", "y"), coords={"x": x, "y": y} ... ) >>> result = footprint_weighted_value(weights, evi) >>> round(result.value, 4), round(result.retained_weight, 4), result.n_cells (0.32, 1.0, 161)
- fluxfootprints.format_variable_provenance(mapping: Mapping[str, Any]) str[source]
Inverse of
parse_variable_provenance(), in field-registry order.- Parameters:
- mapping
Field name ->
Provenancemember or its string value.
- Returns:
- str
The inline spelling for a
variable_provenancecolumn.
- fluxfootprints.generate_density_raster(gdf: GeoDataFrame, resolution: float = 50, buffer_distance: float = 200, epsg: int = 5070, weight_field: str = 'ET')[source]
Generate a weighted kernel density raster from a point GeoDataFrame.
This function uses a Gaussian kernel density estimation (KDE) to create a raster from spatial point data, where each point is weighted by a specified attribute (e.g., evapotranspiration). The output raster is georeferenced with an affine transform and buffered beyond the point extent.
- Parameters:
- gdfgeopandas.GeoDataFrame
Input GeoDataFrame containing point geometries and a weight field.
- resolutionfloat, optional
Size of each raster cell in projection units (typically meters), by default 50.
- buffer_distancefloat, optional
Distance to extend the raster beyond the extent of the points, in the same units as the CRS (e.g., meters), by default 200.
- epsgint, optional
EPSG code for the output coordinate reference system, by default 5070 (NAD83 / Conus Albers).
- weight_fieldstr, optional
Name of the column in gdf to use as weights in the KDE calculation, by default “ET”.
- Returns:
- densitynumpy.ndarray
2D array of KDE density values (not normalized by default).
- transformaffine.Affine
Affine transformation mapping array coordinates to geographic space.
- boundstuple of float
Bounding box of the raster extent as (xmin, ymin, xmax, ymax).
Notes
This function assumes that the input geometries are point features.
The returned density array is not normalized unless the line density /= np.sum(density) is uncommented.
The output CRS is set using gdf.to_crs(epsg=…).
- fluxfootprints.index_openet_rasters(source: str | Path | Sequence[str | Path] | Mapping[Any, str | Path | Sequence[str | Path]], pattern: str = '*.tif', recursive: bool = False, date_regex: str | Pattern | None = None, logger: Logger | None = None) dict[date, list[Path]][source]
Build a
date -> [paths]index of daily OpenET rasters.- Parameters:
- sourcepath, sequence of paths, or mapping
A directory to scan, a single raster, a sequence of rasters, or an already-built mapping of date-like keys to one or more paths.
- patternstr, default “*.tif”
Glob used when source is a directory.
- recursivebool, default False
Search sub-directories as well.
- date_regexstr or re.Pattern, optional
Custom date pattern passed to
parse_raster_date().- loggerlogging.Logger, optional
- Returns:
- dict
datetime.datekeys mapped to sorted lists of paths. Several files sharing a date (adjacent tiles, for instance) are kept together and combined when the mask is built.
Notes
Files whose name only carries a year and month are indexed under the first of that month, which still lets them serve a monthly footprint slice.
- fluxfootprints.lagrangian_timescale(z: float, zm: float, ustar: float) float[source]
Simple height‑dependent Lagrangian timescale (Hunt, 1984 style).
T_L = 0.15 z / σ_w + 0.3 z_m / σ_w(z_m) (heuristic)
- fluxfootprints.length_scale_xi(z: float, U: float, kappa: float, m: float, n: float) float[source]
Calculate the characteristic footprint length-scale ξ(z).
This function implements Eq. (19) from Kormann & Meixner (2001) to compute the length scale based on measurement height and atmospheric parameters.
- Parameters:
- zfloat
Measurement height above displacement height (m).
- Ufloat
Mean horizontal wind speed at height z (m/s).
- kappafloat
von Kármán constant (typically ~0.4).
- mfloat
Power law exponent for wind speed profile.
- nfloat
Power law exponent for eddy diffusivity profile.
- Returns:
- float
Characteristic length-scale ξ(z) (m).
- fluxfootprints.level_colors(cmap: str = 'Blues') dict[Level, tuple[float, float, float, float]][source]
Map the three-level index onto its ramp, HIGH darkest.
- Parameters:
- cmapstr, default LEVEL_CMAP
Name of a Matplotlib sequential colormap.
- Returns:
- dict
Level-> RGBA.
See also
radius_colorsThe same for target-area radii.
Examples
>>> list(level_colors()) [<Level.HIGH: 'high'>, <Level.MEDIUM: 'medium'>, <Level.LOW: 'low'>]
- fluxfootprints.load_amf_df(csv_path: str | Path, cfg: dict[str, Any]) DataFrame[source]
Load AMF half-hourly CSV and return a tidy DataFrame indexed by time.
- fluxfootprints.load_config(ini_path: str | Path) dict[str, Any][source]
Read a minimal INI for site metadata and column names.
- fluxfootprints.load_provenance_sidecar(data_path: str | Path) dict[str, Any][source]
Read the JSON sidecar belonging to a processed file.
- Parameters:
- data_path
The processed data file, not the sidecar itself.
- Raises:
- FileNotFoundError
If no sidecar exists beside
data_path.
- fluxfootprints.log_wind_profile(z: float, ustar: float, z0: float) float[source]
Neutral logarithmic mean‑wind profile.
- Parameters:
- zfloat
Height above displacement (m).
- ustarfloat
Friction velocity (m s⁻¹).
- z0float
Aerodynamic roughness length (m).
- Returns:
- float
Mean wind speed at z (m s⁻¹).
- fluxfootprints.mask_footprint_dataarray(da: DataArray, openet: str | Path | Sequence[str | Path] | Mapping[Any, str | Path | Sequence[str | Path]], station_lat: float, station_lon: float, freq: str = 'auto', grid_crs: str | int | CRS = 'auto', band: int | str = 1, nodata: float | None = None, valid_range: tuple[float | None, float | None] | None = None, treat_zero_as_nodata: bool = False, resampling: str | Resampling = 'nearest', coverage_threshold: float = 0.5, combine: str = 'any', on_missing: str = 'skip', max_gap_days: int = 8, fill_value: float = 0.0, renormalize: bool = False, pattern: str = '*.tif', recursive: bool = False, date_regex: str | Pattern | None = None, logger: Logger | None = None) MaskedFootprint[source]
Mask a time-resolved footprint field with daily OpenET valid-data masks.
Each time step is matched to the OpenET raster(s) for that date; monthly steps (
freq="monthly") combine every OpenET day inside the month with combine. Footprint weight on cells with no ET value is replaced by fill_value.- Parameters:
- daxarray.DataArray
Footprint field with
time,xandydims, wherex/yare metres from the tower – e.g.model.f_2dor any field ofSummaryResult.- openetpath, sequence of paths, or mapping
Daily OpenET rasters; see
index_openet_rasters().- station_lat, station_lonfloat
Tower position in decimal degrees (WGS 84).
- freq{“auto”, “daily”, “monthly”}, default “auto”
How to interpret the time coordinate.
"auto"reads a series of more than one stamp, all first-of-month and one stamp per month, as monthly, and anything else as daily – so set it explicitly for a single-slice monthly field.- grid_crsstr, int, pyproj.CRS, default “auto”
CRS the footprint grid is georeferenced into;
"auto"uses the local UTM zone, matching the GeoTIFF export.- band, nodata, valid_range, treat_zero_as_nodata, resampling,
- coverage_threshold, combine
Passed through to
openet_mask_on_grid().- on_missing{“skip”, “mask”, “nearest”, “error”}, default “skip”
What to do for a time step with no OpenET raster: leave it unmasked, mask it out entirely, borrow the nearest date within max_gap_days, or raise.
- max_gap_daysint, default 8
Search radius for
on_missing="nearest".- fill_valuefloat, default 0.0
Value written where ET data are missing. 0.0 keeps the field usable by the contour and GeoTIFF exporters; pass
numpy.nanto make the hole explicit.- renormalizebool, default False
Rescale each masked time step so it integrates to the same total as before masking, i.e. redistribute the dropped weight over the cells that have ET. Off by default so the loss stays visible in retained_fraction.
- pattern, recursive, date_regex
Passed through to
index_openet_rasters().- loggerlogging.Logger, optional
- Returns:
- MaskedFootprint
data(masked field),mask(boolean, per time step), andretained_fraction(share of footprint weight surviving each step), plusmissing_datesfor steps with no OpenET coverage.
Notes
retained_fractionis the share of this field’s weight kept, so it speaks to how much of the source area is observable by OpenET. It is not the same as the domain-coverage columns produced bysummarize_periods(), which measure how much of the footprint fits inside the model domain.
- fluxfootprints.mask_rasters_geotiff(raster_dir: str | Path, openet: str | Path | Sequence[str | Path] | Mapping[Any, str | Path | Sequence[str | Path]], out_dir: str | Path | None = None, pattern: str = '*.tif', fill_value: float | None = None, band: int | str = 1, nodata: float | None = None, valid_range: tuple[float | None, float | None] | None = None, treat_zero_as_nodata: bool = False, resampling: str | Resampling = 'nearest', coverage_threshold: float = 0.5, combine: str = 'any', on_missing: str = 'skip', max_gap_days: int = 8, renormalize: bool = False, openet_pattern: str = '*.tif', recursive: bool = False, date_regex: str | Pattern | None = None, logger: Logger | None = None) list[Path][source]
Mask footprint GeoTIFFs on disk with daily OpenET valid-data masks.
Each footprint raster is matched to OpenET by the date in its file name –
..._20240201.tifuses that day,..._202402.tifcombines every OpenET day in February 2024 – so this works directly on the output ofexport_rasters_geotiff(). Masks are built against each file’s own CRS and transform, so reprojected exports are handled too.- Parameters:
- raster_dirpath
Directory of footprint GeoTIFFs (or a single GeoTIFF).
- openetpath, sequence of paths, or mapping
Daily OpenET rasters; see
index_openet_rasters().- out_dirpath, optional
Where masked copies are written. Defaults to
<raster_dir>/openet_masked. Inputs are never overwritten in place unless out_dir is set to the input directory.- patternstr, default “*.tif”
Glob for the footprint rasters.
- fill_valuefloat, optional
Value written where ET data are missing. Defaults to each file’s own nodata value, or 0.0 when it has none.
- band, nodata, valid_range, treat_zero_as_nodata, resampling,
- coverage_threshold, combine
Passed through to
openet_mask_on_grid()(they describe the OpenET rasters, not the footprint rasters).- on_missing{“skip”, “mask”, “nearest”, “error”}, default “skip”
What to do when no OpenET raster matches a footprint raster’s date: copy it through unmasked, blank it, borrow the nearest date within max_gap_days, or raise.
- max_gap_daysint, default 8
Search radius for
on_missing="nearest".- renormalizebool, default False
Rescale each masked band to its pre-masking sum.
- openet_pattern, recursive, date_regex
Passed through to
index_openet_rasters().- loggerlogging.Logger, optional
- Returns:
- list of pathlib.Path
The masked rasters that were written, in name order.
- fluxfootprints.mask_rasters_geotiff_with_diagnostics(raster_dir: str | Path, openet: str | Path | Sequence[str | Path] | Mapping[Any, str | Path | Sequence[str | Path]], out_dir: str | Path | None = None, pattern: str = '*.tif', fill_value: float | None = None, band: int | str = 1, nodata: float | None = None, valid_range: tuple[float | None, float | None] | None = None, treat_zero_as_nodata: bool = False, resampling: str | Resampling = 'nearest', coverage_threshold: float = 0.5, combine: str = 'any', on_missing: str = 'skip', max_gap_days: int = 8, renormalize: bool = True, openet_pattern: str = '*.tif', recursive: bool = False, date_regex: str | Any | None = None, diagnostics_csv: str | Path | None = None, min_retained_fraction: float | None = None, logger: Logger | None = None) DataFrame[source]
Mask footprint GeoTIFFs with OpenET validity and return diagnostics.
Footprint cells that do not overlap valid OpenET data are replaced with
fill_value. Whenrenormalize=True(the default for this OpenET comparison helper), surviving footprint weights are multiplied by a scalar so each raster band retains the same total mass it had before masking.Unlike
fluxfootprints.openet_masking.mask_rasters_geotiff(), this function returns apandas.DataFramewith one record per raster band and optionally writes that table to CSV.- Parameters:
- raster_dirpath
Directory containing footprint GeoTIFFs, or one footprint GeoTIFF.
- openetpath, sequence, or mapping
Daily OpenET rasters accepted by
index_openet_rasters.- out_dirpath, optional
Output directory. Defaults to
<raster_dir>/openet_masked.- patternstr, default “*.tif”
Glob for footprint rasters when
raster_diris a directory.- fill_valuefloat, optional
Value written where OpenET is missing. Defaults to footprint nodata, or 0.0 when the footprint has no nodata value.
- band, nodata, valid_range, treat_zero_as_nodata, resampling,
- coverage_threshold, combine
Options describing the OpenET valid-data mask.
- on_missing{“skip”, “mask”, “nearest”, “error”}, default “skip”
Behavior when no OpenET raster matches the footprint date.
- max_gap_daysint, default 8
Maximum nearest-date search distance when
on_missing="nearest".- renormalizebool, default True
Preserve pre-mask footprint mass by rescaling surviving weights.
- diagnostics_csvpath, optional
Write the returned diagnostics table to this CSV.
- min_retained_fractionfloat, optional
If provided, emit a warning whenever the fraction of footprint mass overlapping valid OpenET falls below this value.
- Returns:
- pandas.DataFrame
One row per raster band with columns including
date,frequency,source_path,output_path,openet_dates,missing_openet,band,original_sum,retained_sum,retained_fraction,masked_fraction,valid_pixel_fraction,renormalized_sum, andscale_factor.
Notes
retained_fractionis footprint-weighted coverage, not simple pixel coverage. For footprint-weighted OpenET extraction this is generally the more useful quality-control metric because missing pixels near the footprint peak matter more than missing pixels in the low-weight tail.
- fluxfootprints.mask_summaries(summaries: SummaryResult, openet: str | Path | Sequence[str | Path] | Mapping[Any, str | Path | Sequence[str | Path]], station_lat: float, station_lon: float, **kwargs: Any) SummaryResult[source]
Apply OpenET masks to every field of a
SummaryResult.Daily fields are matched day-by-day and monthly fields month-by-month. The surviving fraction of footprint weight is appended to the coverage tables as
openet_retained_frac(andopenet_retained_frac_etwfor the ET-weighted fields).- Parameters:
- summariesSummaryResult
Output of
summarize_periods().- openetpath, sequence of paths, or mapping
Daily OpenET rasters; see
index_openet_rasters().- station_lat, station_lonfloat
Tower position in decimal degrees (WGS 84).
- **kwargs
Any keyword accepted by
mask_footprint_dataarray()except freq, which is set per field.
- Returns:
- SummaryResult
New object; the input is not modified.
- fluxfootprints.model2_regression(footprint_values: ndarray | Sequence[float], target_values: ndarray | Sequence[float]) tuple[float, float, float, float][source]
Fit the site-level model II regression of Eq. 7.
\[EVI_{target} \sim \beta_0 + \beta_1 EVI_{footprint}\]The paper uses R’s
lmodel2, appropriate here because both variables carry error. This implements the reduced major axis (standard major axis) solution, whose slope is the ordinary least-squares slope divided by the Pearson correlation coefficient.- Parameters:
- footprint_valuesarray_like
Footprint-weighted values, the predictor.
- target_valuesarray_like
Target-area means, the response. Must match footprint_values in length.
- Returns:
- interceptfloat
beta_0.
- slopefloat
beta_1.
- r_squaredfloat
Squared Pearson correlation of the two inputs.
- p_valuefloat
Two-sided significance of the correlation.
- Raises:
- ValueError
If the inputs differ in length, or fewer than three finite pairs remain.
Notes
Pairs where either value is non-finite are dropped before fitting. The RMA slope is undefined in sign when the correlation is zero; the sign is taken from the correlation, as
lmodel2does.This is
rma_regression()reduced to four values; call that directly for the confidence intervals of Table 1, and read its note on why ordinary least squares cannot stand in for the fit.Examples
>>> import numpy as np >>> rng = np.random.default_rng(3) >>> footprint = rng.uniform(0.2, 0.8, 40) >>> target = 0.95 * footprint + 0.02 + rng.normal(0.0, 0.03, 40) >>> intercept, slope, r_squared, p_value = model2_regression(footprint, target) >>> round(intercept, 4), round(slope, 4), round(r_squared, 4) (0.0039, 0.9739, 0.9647) >>> bool(p_value < 1e-6) True
- fluxfootprints.monthly_climatologies(model_or_ds: Any, partition: str = 'month+daynight', latitude: float | None = None, longitude: float | None = None, tz: str | float | tzinfo | None = None, daytime: Series | ndarray | DataArray | None = None, fraction: float = 0.8, dx: float | None = None, dy: float | None = None, min_times: int = 1, sw_in_pot: str | None = 'SW_IN_POT') Dataset[source]
Aggregate time-resolved footprints into monthly climatologies, Sect. 2.2.
Chu et al. (2021) aggregate every half-hourly footprint of a month into a daytime and a nighttime climatology, truncate each at the 80 % contour of source weights, and rescale the retained cells to sum to one. The result is the unit of analysis for everything downstream: the metrics of Eqs. 1-3 and the footprint-weighted statistics of Eqs. 5-6.
- Parameters:
- model_or_dsBaseFootprintModel, xarray.Dataset, or xarray.DataArray
A run footprint model, its results Dataset, or the time-resolved footprints themselves with dims
(time, x, y). A model is also searched for a sw_in_pot column in its input frame.- partition{“month+daynight”, “month”}, default “month+daynight”
How to group the timesteps.
"month+daynight"splits each month into daytime and nighttime as in the paper;"month"aggregates every hour of the month into one climatology, labelled"all".- latitudefloat, optional
Site latitude, positive north [degrees].
- longitudefloat, optional
Site longitude, positive east [degrees]; western longitudes are negative.
- tzstr, float, datetime.tzinfo, or None, optional
Time zone the footprint timestamps are in, as accepted by
partition_daynight().- daytimepandas.Series, numpy.ndarray, or xarray.DataArray, optional
A precomputed day-night flag, used instead of computing one. A Series is aligned on its index and must cover every timestamp; an array is taken in order.
- fractionfloat, default 0.8
Source-weight fraction each climatology is truncated at.
- dx, dyfloat, optional
Grid spacing [m]. Inferred from the
xandycoordinates when omitted.- min_timesint, default 1
Fewest contributing timesteps a group needs before it is aggregated. Thinner groups come back as all-NaN weights with their
n_timesrecorded, rather than as a climatology built from a handful of half-hours.- sw_in_potstr or None, default “SW_IN_POT”
Precomputed potential-radiation column to prefer over the solar geometry, matched case-insensitively.
- Returns:
- xarray.Dataset
Dims
(month, period, x, y), carryingfootprint_climatologyTruncated, renormalised weights [-] summing to one over
(x, y)in every aggregated group, and all-NaN in the groups left empty or held back by min_times.n_timesTimesteps that contributed positive weight to each group.
contour_levelSource-weight density [m⁻²] at the truncation contour. It varies from group to group, so it lives here rather than in the stacked array’s attributes, and
climatology_metrics()reports it asnanfor a slice; read it from here alongside.contour_n_cellsCells retained inside the contour.
The
monthcoordinate is the first instant of each calendar month present, andperiodis["daytime", "nighttime"]or["all"].
- Raises:
- TypeError
If model_or_ds is of an unusable type.
- ValueError
If partition is not one of the two accepted values; if min_times is below one; if the footprints carry no usable
timecoordinate or their dims are not(time, x, y); or if the day-night split cannot be resolved.
See also
truncate_to_contourThe per-group truncation and renormalisation.
seasonal_overlapEq. 2, taken over this Dataset’s
monthdimension.daynight_overlapEq. 3, over its two
periodslices.climatology_metricsFetch, area, and symmetry of one group.
Notes
Months are calendar months of a specific year, not months of the year, so a multi-year record yields a separate January per year rather than one blended January. The paper’s indices are site-year properties; select a year before passing the
monthdimension toseasonal_overlap().Each group is summed over its contributing timesteps and divided by their count, matching the climatology convention of the footprint models. The divisor cancels in the renormalisation, so it affects only the recorded
contour_level, which stays comparable across groups because of it.A group with no data – polar night, or a month the tower was down – is all-NaN rather than all-zero, so that it cannot be mistaken for a footprint that legitimately puts no weight anywhere. Drop those before the overlap indices, which require every month to sum to one:
ds.where(ds.n_times > 0, drop=True).Examples
>>> import numpy as np, pandas as pd, xarray as xr >>> times = pd.date_range("2020-01-01", "2020-06-30 21:00", freq="3h") >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> rng = np.random.default_rng(0) >>> centres = rng.normal(30.0, 25.0, times.size) # a wandering wind direction >>> f = np.stack( ... [np.exp(-((X - c) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) for c in centres] ... ) >>> f /= f.sum(axis=(1, 2), keepdims=True) * 400.0 >>> f_2d = xr.DataArray( ... f, coords={"time": times, "x": x, "y": y}, dims=("time", "x", "y") ... ) >>> clim = monthly_climatologies(f_2d, latitude=40.0, longitude=-111.9, tz=-7) >>> dict(clim.sizes) {'month': 6, 'period': 2, 'x': 21, 'y': 21} >>> list(clim.data_vars) ['footprint_climatology', 'n_times', 'contour_level', 'contour_n_cells'] >>> [str(p) for p in clim["period"].values] ['daytime', 'nighttime']
Every month-period slice arrives truncated at fraction and rescaled to unit sum, ready for the weighted statistics:
>>> round(float(clim["footprint_climatology"].isel(month=0, period=0).sum()), 10) 1.0
- fluxfootprints.normalize_units(units: str | None) str[source]
Normalize a unit spelling to its canonical form.
Recognizes spelling variants only (
"m/s"->"m s-1"). It never maps between physically different units:"Pa"does not become"kPa".- Parameters:
- units
Declared unit string, or
None.
- Returns:
- str
The canonical spelling, or the input with whitespace collapsed when the spelling is not recognized.
- fluxfootprints.openet_mask_on_grid(rasters: str | Path | Sequence[str | Path], dst_crs: CRS | str | int, dst_transform: Affine, dst_shape: tuple[int, int], band: int | str = 1, nodata: float | None = None, valid_range: tuple[float | None, float | None] | None = None, treat_zero_as_nodata: bool = False, resampling: str | Resampling = 'nearest', coverage_threshold: float = 0.5, combine: str = 'any') ndarray[source]
Build a boolean valid-data mask for a target grid from OpenET rasters.
- Parameters:
- rasterspath or sequence of paths
One or more OpenET rasters. Several rasters are reduced with combine, which is how adjacent tiles for the same day are merged.
- dst_crs, dst_transform, dst_shape
Target grid, north-up; dst_shape is
(height, width).- bandint or str, default 1
Band index (1-based) or band description to read.
- nodatafloat, optional
Extra nodata value to honour on top of the dataset’s own.
- valid_rangetuple of (float or None, float or None), optional
Keep only cells inside these inclusive bounds, e.g.
(0.0, None)to drop negative ET.- treat_zero_as_nodatabool, default False
Treat exact zeros as missing. Useful where OpenET writes 0 instead of nodata outside the crop mask; leave off when a real 0 mm/day matters.
- resamplingstr or rasterio.warp.Resampling, default “nearest”
Warping method.
"nearest"gives a hard mask;"average"gives the fraction of the destination cell covered by valid source cells, which is then thresholded with coverage_threshold.- coverage_thresholdfloat, default 0.5
Fraction of valid coverage a destination cell needs to count as valid.
- combine{“any”, “all”, “majority”}, default “any”
How to reduce multiple rasters.
- Returns:
- numpy.ndarray
Boolean array of shape dst_shape; True where ET data exist.
- fluxfootprints.overlap(w1: DataArray | ndarray, w2: DataArray | ndarray) float[source]
Compute the overlap of two footprints, the kernel of Eqs. 2-3.
\[\sum_{i=1}^{I} \sqrt{\varphi^{1}_{i} \varphi^{2}_{i}}\]The cell-wise geometric mean of two footprints, summed over cells: the Bhattacharyya coefficient of the two weight fields read as discrete distributions. It is Eq. 2 for the special case of two months, and the per-month term of Eq. 3.
- Parameters:
- w1, w2xarray.DataArray or numpy.ndarray
Footprint weights on a common grid, each summing to 1 – the output of
truncate_to_contour(). DataArrays are compared cell for cell rather than aligned on their coordinates, so a mismatched grid raises instead of silently joining.
- Returns:
- float
Overlap in [0, 1] for weights that sum to 1: one when the two footprints are identical, zero when their supports are disjoint.
- Raises:
- ValueError
If the two grids differ in dims, shape, or coordinates; or if either array is empty or holds a non-finite or negative weight.
See also
seasonal_overlapEq. 2, over the K months of a site-year.
daynight_overlapEq. 3, over paired daytime and nighttime months.
Notes
Normalisation is left to the callers rather than checked here, so that the kernel stays usable on any pair of non-negative fields; the upper bound of 1 holds only for weights that sum to 1.
seasonal_overlap()anddaynight_overlap()do check it.Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> def weights(cx, spread=80.0): ... f = np.exp(-((X - cx) ** 2 + Y ** 2) / (2 * spread ** 2)) ... f /= f.sum() # the overlap indices take unit-sum weights ... return xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> round(overlap(weights(0.0), weights(0.0)), 6) # identical months 1.0 >>> round(overlap(weights(0.0), weights(40.0)), 4) # shifted downwind 0.9716
- fluxfootprints.parse_nldas_csv(ts)[source]
INPUTS: ts - time series output of the time series service
OUTPUTS: headers,df - the headers from the CSV as a dict and the values in a pandas dataframe Function modified from How_to_Access_GiC_Time_Series_Service.ipynb https://github.com/nasa/gesdisc-tutorials/tree/main/notebooks
- fluxfootprints.parse_raster_date(name: str | Path, date_regex: str | Pattern | None = None) tuple[date, str] | None[source]
Pull a date out of a raster file name.
Recognizes
YYYYMMDD,YYYY-MM-DD,YYYY_MM_DDandYYYY.MM.DDanywhere in the name, and falls back to the year-month forms (YYYYMM,YYYY-MM) used by the monthly footprint exports. The first token that parses as a real calendar date wins.- Parameters:
- namestr or pathlib.Path
File name or full path.
- date_regexstr or re.Pattern, optional
Override pattern. Must expose either three groups (year, month, day) or two groups (year, month).
- Returns:
- tuple of (datetime.date, str) or None
The parsed date and its kind –
"day"or"month"(month-kind dates are anchored to the first of the month).Noneif nothing in the name parses as a date.
- fluxfootprints.parse_variable_provenance(text: str) dict[str, str][source]
Parse the inline
variable_provenancespelling into a mapping.- Parameters:
- text
e.g.
"ol=network_derived;z0=prep_calculated;h=external_forcing".
- Returns:
- dict
Field name -> provenance class string. Unparseable items are skipped.
Examples
>>> parse_variable_provenance("ol=network_derived;z0=prep_calculated") {'ol': 'network_derived', 'z0': 'prep_calculated'}
- fluxfootprints.partition_daynight(df: DataFrame | DatetimeIndex, latitude: float | None = None, longitude: float | None = None, tz: str | float | tzinfo | None = None, sw_in_pot: str | None = 'SW_IN_POT') Series[source]
Split records into daytime and nighttime, after Sect. 2.2.
Chu et al. (2021) separate daytime from nighttime by the potential incoming radiation computed from the site’s geolocation and time zone, calling a record daytime where that radiation exceeds 0 W m⁻² – i.e. wherever the Sun is above the horizon. Daytime and nighttime footprints are then aggregated into separate climatologies, because the nighttime ones reach about 45 % farther and cover about 90 % more area.
- Parameters:
- dfpandas.DataFrame or pandas.DatetimeIndex
Measurement records indexed by timestamp, or the timestamps alone. A frame carrying a potential-radiation column is used as-is; see sw_in_pot.
- latitudefloat, optional
Site latitude, positive north [degrees]. Required unless the radiation is supplied by column.
- longitudefloat, optional
Site longitude, positive east [degrees]; western longitudes are negative. Required unless the radiation is supplied by column.
- tzstr, float, datetime.tzinfo, or None, optional
Time zone the timestamps are in: a fixed UTC offset in hours (e.g.
-7), an IANA zone name (e.g."America/Denver"), or atzinfo. Omit it when the index is already time-zone aware.- sw_in_potstr or None, default “SW_IN_POT”
Column of precomputed potential radiation [W m⁻²] to use instead of the solar geometry, matched case-insensitively; the AmeriFlux BASE name by default. Pass None to always compute the geometry.
- Returns:
- pandas.Series
Boolean Series named
"daytime"on the index of df, True where the Sun is above the horizon.
- Raises:
- TypeError
If df is not a DataFrame or DatetimeIndex, or tz is of an unusable type.
- ValueError
If the geolocation is needed but missing or out of range; if the index holds a NaT, or is naive with no tz (or aware with one); or if the sw_in_pot column has gaps that cannot be filled because no geolocation was given.
See also
potential_radiationThe radiation the threshold is applied to.
monthly_climatologiesUses this split to build the paired climatologies.
Notes
A precomputed column is preferred where present, so that the split matches whatever the data provider used; gaps in it are filled from the geometry when a geolocation is available, since a boolean flag has no room for a missing value.
AmeriFlux BASE timestamps are in local standard time throughout the year. Pass the site’s standard-time offset as a number, not an IANA zone name, or every summer sunrise moves an hour.
Examples
>>> import pandas as pd >>> times = pd.date_range("2020-12-21", periods=4, freq="6h") >>> partition_daynight(times, 40.0, -111.0, tz=-7).tolist() [False, False, True, False]
A precomputed column wins over the geometry, and needs no geolocation:
>>> df = pd.DataFrame({"SW_IN_POT": [0.0, 0.0, 550.0, 0.0]}, index=times) >>> partition_daynight(df).tolist() [False, False, True, False]
- fluxfootprints.plot_bias_density(data: DataFrame, *, radii: Sequence[float] | None = None, bias_column: str | None = None, radius_column: str = 'radius', percent: bool = True, clip: tuple[float, float] = (-100.0, 100.0), bandwidth: float | str | None = None, gridsize: int = 512, threshold: float | None = None, ax: Axes | None = None, cmap: str = 'Oranges', figsize: tuple[float, float] = (6.0, 4.0)) tuple[Figure, Axes][source]
Plot kernel densities of the sensor location bias by radius, Fig. 7.
One Gaussian kernel density per target area over the per-period biases of Eq. 6, drawn on the radius ramp. The peak sharpens and rises towards the smallest radius, which is the paper’s Fig. 7 result: the wider the disc, the further a month’s footprint-weighted value strays from it.
- Parameters:
- datapandas.DataFrame
Per-period biases, either the frame of
sensor_location_bias_series()(columndelta) or the tidy frame ofassess_representativeness()(columnbias), whoseradiusindex level is reset here. Subset the latter to onevariableandperiodfirst.- radiisequence of float, optional
Draw only these radii. Every radius present is drawn when omitted.
- bias_columnstr, optional
Column holding the bias as a fraction. Detected among
"delta"and"bias"when omitted.- radius_columnstr, default ‘radius’
Column keying the densities.
- percentbool, default True
Draw the bias as a percentage, as the paper’s axis does. Set False to keep the fractions the analysis returns.
- cliptuple of float, default (-100.0, 100.0)
Range the densities are drawn over, in the units of the axis. Every finite bias enters its kernel, including those outside this window; only the drawing is clipped, as the paper’s +/-100 % axis is.
- bandwidthfloat or str, optional
Passed to
scipy.stats.gaussian_kdeasbw_method. Scott’s rule when omitted.- gridsizeint, default 512
Number of points each density is evaluated on.
- thresholdfloat, optional
Draw guides at
+/- threshold, as a fraction – passBIAS_THRESHOLDfor the +/-10 % of Sect. 2.4. No guides when omitted, as in the published figure.- axmatplotlib.axes.Axes, optional
Axes to draw on. A new figure is opened when omitted.
- cmapstr, default RADIUS_CMAP
Sequential colormap for the radius series.
- figsizetuple of float, default (6.0, 4.0)
Size of a new figure [in]. Ignored when ax is given.
- Returns:
- tuple of (matplotlib.figure.Figure, matplotlib.axes.Axes)
The figure and the axes drawn on.
- Raises:
- TypeError
If data is not a DataFrame.
- ValueError
If no bias column is found, if radii names an absent radius, if clip does not increase, if gridsize is below two, or if no radius holds the spread a kernel density needs.
See also
fluxfootprints.sensor_location_bias_seriesProduces data.
fluxfootprints.BIAS_THRESHOLDThe +/-10 % the paper judges against.
plot_footprint_target_scatterThe same mismatch pair by pair.
Notes
A radius whose biases are all but identical – a single period, or a perfectly homogeneous site – offers no bandwidth to estimate and is skipped rather than drawn as a spike. Its absence from the legend is the signal that it was.
References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Sect. 3.3, Fig. 7.
Examples
>>> bias = sensor_location_bias_series(pairs, model.x, model.y) >>> fig, ax = plot_bias_density(bias, threshold=BIAS_THRESHOLD) >>> ax.set_title("Daytime footprints")
- fluxfootprints.plot_footprint_target_scatter(data: DataFrame, *, radii: Sequence[float] | None = None, x_column: str = 'value_footprint', y_column: str = 'value_target', radius_column: str | None = 'radius', fit: bool = True, annotate: bool | None = None, variable: str = 'EVI', ax: Axes | None = None, cmap: str = 'Oranges', figsize: tuple[float, float] = (5.5, 5.5)) tuple[Figure, Axes][source]
Plot target-area against footprint-weighted values, Fig. 1f and Fig. 6.
Scatters the matched pairs of Eq. 5, one series per target-area radius, under the reduced major axis fit of Eq. 7 and the 1:1 line. Points settling below 1:1 are the paper’s central result: the footprint saw a higher value than the disc drawn around it.
- Parameters:
- datapandas.DataFrame
Matched values, one row per period and radius: either the frame of
sensor_location_bias_series()or the tidy frame ofassess_representativeness(), whoseradiusindex level is reset here. Subset the latter to onevariableandperiodfirst – every row given is drawn.- radiisequence of float, optional
Draw only these radii. Every radius present is drawn when omitted.
- x_column, y_columnstr
Columns holding the footprint-weighted and target-area values, defaulting to the names both frames use.
- radius_columnstr or None, default ‘radius’
Column keying the series. Pass None to draw the frame as a single series, the one-panel form of Fig. 6.
- fitbool, default True
Draw the RMA line of
rma_regression()through each series. A series of fewer than three finite pairs is scattered without one.- annotatebool, optional
Write the fit statistics into the corner. Defaults to True for a single series and False for several, where the box would crowd the panel and the legend already carries the identities.
- variablestr, default ‘EVI’
Name of the field, used in the axis labels.
- axmatplotlib.axes.Axes, optional
Axes to draw on. A new figure is opened when omitted.
- cmapstr, default RADIUS_CMAP
Sequential colormap for the radius series.
- figsizetuple of float, default (5.5, 5.5)
Size of a new figure [in]. Ignored when ax is given.
- Returns:
- tuple of (matplotlib.figure.Figure, matplotlib.axes.Axes)
The figure and the axes drawn on, at equal aspect and with one range on both axes, so that the 1:1 line runs corner to corner.
- Raises:
- TypeError
If data is not a DataFrame.
- ValueError
If a named column is neither a column nor an index level of data, if radii names an absent radius, or if no finite pair survives.
See also
fluxfootprints.rma_regressionThe model II fit drawn here.
fluxfootprints.evaluate_vegetation_indexReduces the same pairs to a three-level index.
plot_bias_densityThe same mismatch as a distribution of Eq. 6.
Notes
The line is the reduced major axis rather than ordinary least squares: both axes are spatial averages of one noisy raster and both carry error, so an OLS slope would sit systematically shallower and the slopes of Table 1 are reproducible only against the RMA. Each line is drawn across the range of its own series, never extrapolated past it.
References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Fig. 1f, Fig. 6, Table 1.
Examples
>>> bias = sensor_location_bias_series(pairs, model.x, model.y) >>> fig, ax = plot_footprint_target_scatter(bias) >>> fig, ax = plot_footprint_target_scatter( ... bias[bias["radius"] == 250], radius_column=None ... )
- fluxfootprints.plot_landcover_composition(results: Sequence[CategoricalResult], *, class_labels: Mapping[Any, str] | None = None, max_classes: int | None = None, radii: Sequence[float] | None = None, ax: Axes | None = None, cmap: str = 'Oranges', footprint_color: str = '#1b4f9c', figsize: tuple[float, float] = (7.0, 4.5)) tuple[Figure, Axes][source]
Plot footprint-weighted against target-area land-cover shares, Fig. 1e.
One row per land-cover class, ordered by footprint-weighted share. The footprint-weighted percentage is a filled circle in the accent colour, and each target area a triangle on the radius ramp, so the drift of a class’s share as the disc widens reads as a trail away from the circle.
- Parameters:
- resultssequence of CategoricalResult
The output of
evaluate_landcover(), one entry per radius. Every entry carries the same footprint composition – the footprint does not depend on the disc – so the first entry’s is drawn.- class_labelsmapping, optional
Class code -> display name, e.g.
{41: "Deciduous forest"}. A code with no entry is labelled by the code itself.- max_classesint, optional
Keep only this many classes, those with the largest share in either the footprint or any target area. Every class present is drawn when omitted.
- radiisequence of float, optional
Draw only these radii. The ramp is still sampled over every radius in results, so a subset keeps the colours of the full series.
- axmatplotlib.axes.Axes, optional
Axes to draw on. A new figure is opened when omitted.
- cmapstr, default RADIUS_CMAP
Sequential colormap for the target-area series.
- footprint_colorstr, default FOOTPRINT_COLOR
Colour of the footprint-weighted series.
- figsizetuple of float, default (7.0, 4.5)
Size of a new figure [in]. Ignored when ax is given.
- Returns:
- tuple of (matplotlib.figure.Figure, matplotlib.axes.Axes)
The figure and the axes drawn on. Nothing is shown or saved.
- Raises:
- ValueError
If results is empty, if radii names a radius results does not hold, if max_classes is not positive, or if no class carries a share in either the footprint or a target area.
See also
fluxfootprints.evaluate_landcoverProduces results.
plot_level_barsThe same evaluation reduced to its three-level index and pooled across sites.
Notes
Shares are the percentages
CategoricalResultcarries, so a column of circles sums to 100 % only when max_classes dropped nothing.References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Fig. 1e.
Examples
>>> results = evaluate_landcover(model.fclim_2d, nlcd) >>> fig, ax = plot_landcover_composition( ... results, class_labels={41: "Deciduous forest", 82: "Cropland"} ... ) >>> fig.savefig("landcover.png", dpi=200)
- fluxfootprints.plot_level_bars(data: DataFrame, *, level_column: str | None = None, radius_column: str = 'radius', radii: Sequence[float] | None = None, percent: bool = True, ax: Axes | None = None, cmap: str = 'Blues', figsize: tuple[float, float] = (6.0, 4.0), width: float = 0.72) tuple[Figure, Axes][source]
Plot the three-level index stacked across target areas, Fig. 5 and Fig. 8.
One stacked bar per radius, HIGH at the bottom in the darkest step through LOW at the top in the lightest. Over a table of many sites this is the paper’s Fig. 5 (land cover) and Fig. 8 (vegetation index): the share of sites whose footprint still represents the disc, falling as the disc widens.
- Parameters:
- datapandas.DataFrame
One row per site and radius – the frames of
representativeness_summary()concatenated across sites, or the site-scope rows ofassess_representativeness(), whoseradiusindex level is reset here. Rows are counted as given, so subset to one ecosystem type, ecoregion, or period first to draw a panel of Fig. 5.- level_columnstr, optional
Column holding the
Level. Detected among"level","landcover_level", and"continuous_level"when omitted – ambiguous on a summary table carrying both halves, so name the one to draw.- radius_columnstr, default ‘radius’
Column keying the bars.
- radiisequence of float, optional
Draw only these radii. Every radius present is drawn when omitted.
- percentbool, default True
Scale each bar to 100 %, as the paper’s axis does. Set False to stack raw site counts, which also shows how many sites each bar rests on.
- axmatplotlib.axes.Axes, optional
Axes to draw on. A new figure is opened when omitted.
- cmapstr, default LEVEL_CMAP
Sequential colormap for the three levels, darkest for HIGH.
- figsizetuple of float, default (6.0, 4.0)
Size of a new figure [in]. Ignored when ax is given.
- widthfloat, default 0.72
Bar width as a fraction of the spacing between radii.
- Returns:
- tuple of (matplotlib.figure.Figure, matplotlib.axes.Axes)
The figure and the axes drawn on. Radii are laid out at even spacing and labelled by value, not to scale, as the paper’s panels are.
- Raises:
- TypeError
If data is not a DataFrame.
- ValueError
If no level column is found, if radii names an absent radius, if the level column holds a value that is not one of
"high","medium", and"low", or if every row is missing a level.
See also
fluxfootprints.representativeness_summaryProduces one site’s rows.
fluxfootprints.LevelThe index being counted.
plot_landcover_compositionThe composition behind one site’s verdict.
Notes
Rows missing a level are dropped before counting, which is the paper’s own treatment: 34 of its 214 sites carried a land-cover product that disagreed with the site metadata and were left out of the land-cover panels, and a site with fewer than
MIN_MATCHESmatched scenes has no regression to classify. Withpercent=Trueeach bar is therefore a share of the sites classified at that radius, which need not be the same count from bar to bar.References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Fig. 5, Fig. 8.
Examples
>>> table = pd.concat(summaries) >>> fig, ax = plot_level_bars(table, level_column="landcover_level") ... >>> ax.set_title("Land-cover representativeness")
- fluxfootprints.polar_to_cartesian_dataframe(df: DataFrame, wd_column: str = 'WD', dist_column: str = 'Dist')[source]
Convert polar coordinates in a DataFrame to Cartesian coordinates.
This function computes Cartesian coordinate columns (X_<dist_column> and Y_<dist_column>) from polar inputs defined by wind direction (in degrees from North) and one or more radial distance columns. Unlike a mutating helper, it does not modify the input DataFrame; it returns only the newly computed coordinate columns.
- Parameters:
- dfpandas.DataFrame
Input DataFrame containing polar coordinate columns.
- wd_columnstr, optional
Name of the column representing wind direction in degrees from North, by default “WD”.
- dist_columnstr or list of str, optional
Name(s) of the column(s) representing distance from origin. A single column name produces one
X_/Y_pair; a list of names produces a pair for each distance column. By default “Dist”.
- Returns:
- pandas.DataFrame
A new DataFrame (indexed like
df) containing only the computed coordinate columns. For each distance columnctwo columns are returned: - ‘X_<c>’: Cartesian X coordinate - ‘Y_<c>’: Cartesian Y coordinate
Notes
The function treats -9999 and NaN as missing values.
Wind direction is converted such that 0° = North, 90° = East, etc.
Cartesian conversion is performed using: X = dist * cos(θ) and Y = dist * sin(θ) where θ = (90 - WD) in degrees.
The input DataFrame is left unchanged.
- fluxfootprints.potential_radiation(df: DataFrame | DatetimeIndex, latitude: float, longitude: float, tz: str | float | tzinfo | None = None) Series[source]
Compute potential (top-of-atmosphere) incoming shortwave radiation.
\[SW_{IN,POT} = \frac{S_0}{R^2} \max(\cos\theta_z, 0)\]The flux a horizontal surface would receive with no atmosphere: the solar constant, scaled by the inverse square of the Earth-Sun distance R and projected onto the horizontal by the solar zenith angle. This is the quantity Chu et al. (2021), Sect. 2.2, threshold at 0 W m⁻² to separate daytime from nighttime records.
- Parameters:
- dfpandas.DataFrame or pandas.DatetimeIndex
Measurement records indexed by timestamp, or the timestamps alone.
- latitudefloat
Site latitude, positive north [degrees].
- longitudefloat
Site longitude, positive east [degrees]; western longitudes are negative.
- tzstr, float, datetime.tzinfo, or None, optional
Time zone the timestamps are in: a fixed UTC offset in hours (e.g.
-7), an IANA zone name (e.g."America/Denver"), or atzinfo. Omit it when the index is already time-zone aware.
- Returns:
- pandas.Series
Potential radiation [W m⁻²] named
"SW_IN_POT", on the index of df. Zero whenever the Sun is at or below the horizon.
- Raises:
- TypeError
If df is not a DataFrame or DatetimeIndex, or tz is of an unusable type.
- ValueError
If the index holds a NaT; if it is naive and tz is None, or aware and tz was given; or if the geolocation is out of range.
See also
partition_daynightThe day-night flag this radiation defines.
Notes
Timestamps are read as instants, so a period-averaged record is placed at whatever its label says – the start of the averaging period for AmeriFlux
TIMESTAMP_START, the end forTIMESTAMP_END. Only the one half-hour that straddles each horizon crossing can be classified either way by that choice.The solar constant used is 1361 W m⁻², the modern total-solar-irradiance value; the flux therefore peaks near 1413 W m⁻² at perihelion in early January and near 1321 W m⁻² at aphelion in early July.
Examples
Six-hourly instants over the winter solstice at a mountain-west site, in local standard time: only local noon has the Sun up.
>>> import pandas as pd >>> times = pd.date_range("2020-12-21", periods=4, freq="6h") >>> potential_radiation(times, 40.0, -111.0, tz=-7).round(1).tolist() [0.0, 0.0, 624.2, 0.0]
- fluxfootprints.predict_sigmav(df: DataFrame, estimator: Any | None = None, predictors: Sequence[str] | None = None) Series[source]
Gap-fill the crosswind velocity standard deviation, Sect. 2.2 and Text S1.
Warning
Not implemented. This function is a placeholder for the paper’s random-forest gap-fill and raises
NotImplementedErroron every call. The parameters and return value below describe the intended interface, not current behaviour. Supply V_SIGMA yourself, or predict it with your own estimator, until this lands.V_SIGMA is missing at many towers but is required by the FFP model. The paper trains a random forest on the 106 sites that do report it, using friction velocity, boundary-layer height, wind speed, incoming shortwave radiation, the Obukhov stability parameter, and IGBP class, and reports R² = 0.79 with MAE = 0.15 m s⁻¹ against a withheld test set.
- Parameters:
- dfpandas.DataFrame
Input data containing the predictor columns and, for rows used in training, an observed
sigmavcolumn [m s⁻¹].- estimatorobject, optional
A fitted scikit-learn regressor to apply. When omitted, a
RandomForestRegressoris fitted on the rows of df that have an observedsigmav.- predictorssequence of str, optional
Predictor column names. Defaults to the paper’s six:
["ustar", "h", "umean", "sw_in", "zm_over_ol", "igbp"].
- Returns:
- pandas.Series
Sigma_v [m s⁻¹] indexed like df, with observed values retained where present and predictions filled in elsewhere.
- Raises:
- NotImplementedError
Always, at present – see the warning above. The exceptions below are the intended behaviour once this is implemented.
- ImportError
If
scikit-learnis not installed and no estimator is supplied.- ValueError
If required predictor columns are missing, or no rows carry an observed
sigmavto train on.
Notes
Predicted sigma_v adds uncertainty mainly in the crosswind dimension of the footprint. The paper argues this largely cancels when aggregating many timesteps into a monthly climatology, but advises against relying on it for half-hourly footprint analysis.
Examples
>>> import pandas as pd >>> predict_sigmav(pd.DataFrame({"ustar": [0.3]})) Traceback (most recent call last): ... NotImplementedError
- fluxfootprints.provenance_sidecar_path(data_path: str | Path) Path[source]
The sidecar path for a processed data file.
processed/US-Var/US-Var_HH.parquet->processed/US-Var/US-Var_HH.provenance.json.
- fluxfootprints.radius_colors(radii: Sequence[float], cmap: str = 'Oranges') dict[float, tuple[float, float, float, float]][source]
Map target-area radii onto the sequential ramp, dark to light.
The step a radius gets depends on its rank within radii rather than on its value, so the ramp spans the series actually drawn and two figures over the same series of radii agree.
- Parameters:
- radiisequence of float
Target-area radii [m]. Sorted ascending here; duplicates collapse.
- cmapstr, default RADIUS_CMAP
Name of a Matplotlib sequential colormap.
- Returns:
- dict
Radius -> RGBA, the smallest radius darkest.
See also
level_colorsThe same for the three-level index.
Examples
>>> sorted(radius_colors([3000, 250])) [250.0, 3000.0]
- fluxfootprints.read_canonical(path: str | Path) DataFrame[source]
Read a processed canonical file into a frame with a
DatetimeIndex.Supports
.parquet/.pqand.csv. Atimestampcolumn, when present, becomes the index; timestamps are parsed but never re-zoned.- Parameters:
- path
Processed file under
processed/.
- Returns:
- pandas.DataFrame
- fluxfootprints.reconstruct_gaussian_2d(x: ndarray, fy: ndarray, sigma_v: float | None = None, U: float | None = None, y_max: float | None = None, ny: int = 201, alpha: float = 0.3) Tuple[ndarray, ndarray, ndarray][source]
Reconstruct a 2‑D footprint
f(x,y)from a 1‑Df_y(x).Assumes the lateral distribution at each stream‑wise distance
xis Gaussian with standard deviation σ_y(x).The default dispersion law is σ_y = α·x (α≈0.3). If turbulence statistics are available, pass
sigma_v(lateral velocity std, m s⁻¹) andU(mean wind speed, m s⁻¹) to use σ_y = (σ_v/U)·x.- Parameters:
- xndarray
Stream‑wise distances (m), same grid used for fy.
- fyndarray
Cross‑wind‑integrated footprint (m⁻¹), e.g. output of wang2006_fy.
- sigma_vfloat, optional
Lateral velocity standard deviation (m s⁻¹). If given with U overrides the default α‑law.
- Ufloat, optional
Mean wind speed at sensor height (m s⁻¹). Required with sigma_v.
- y_maxfloat, optional
Half‑width of y domain (m). Default = 3·max(σ_y).
- nyint, default 201
Number of grid points in y direction (symmetric about 0).
- alphafloat, default 0.3
Proportionality constant for σ_y = α·x when sigma_v/U unavailable.
- Returns:
- (X, Y, F)Tuple[np.ndarray, np.ndarray, np.ndarray]
X, Y : 2‑D coordinate grids (shape len(x) × ny).
- F : 2‑D footprint array f(x_i, y_j) (m⁻²) normalised so that
∬ F dx dy = 1.
- fluxfootprints.representativeness_summary(categorical: Sequence[CategoricalResult] | None = None, continuous: Sequence[ContinuousResult] | None = None, metrics: ClimatologyMetrics | None = None, site_id: str | None = None) DataFrame[source]
Flatten representativeness results into a tidy table.
Joins the two halves of the analysis on their target-area radius, so that a site’s land-cover and vegetation-index verdicts can be read on one line per radius, and written or concatenated across sites.
- Parameters:
- categoricalsequence of CategoricalResult, optional
Land-cover results from
evaluate_landcover().- continuoussequence of ContinuousResult, optional
Continuous-field results from
evaluate_vegetation_index().- metricsClimatologyMetrics, optional
Climatology metrics, repeated across every row when supplied.
- site_idstr, optional
Site identifier written to a
site_idcolumn, e.g."US-MOz". The column is always present, and missing when no identifier is given.
- Returns:
- pandas.DataFrame
One row per target radius over a fresh
RangeIndex, carryingSUMMARY_COLUMNSin that order:site_id,radiusRow identity. Radii come in the order categorical presents them, followed by any radius only continuous holds.
dominant_class,p_footprint,p_target,chi2,chi2_p_value,dof,landcover_levelThe land-cover half, with the two shares as percentages.
n,slope,intercept,r_squared,regression_p_value,rmse,mae,mean_bias,median_bias,bias_within_threshold,continuous_levelThe continuous half. The three bias columns summarise the per-period Deltas of Eq. 6 as fractions; the rest is the model II regression of Eq. 7.
contour_fractionthroughdaynight_overlapThe climatology metrics of Sect. 2.2, repeated down the frame because they describe the footprint rather than any one disc.
Missing halves are filled with NaN, so the frame can be concatenated across sites and written with
export_contour_stats_csv()conventions.
- Raises:
- ValueError
If both categorical and continuous are omitted or empty, leaving no radius to build rows from, or if either holds two results for the same radius.
See also
evaluate_landcoverProduces the land-cover half.
evaluate_vegetation_indexProduces the continuous half.
assess_representativenessThe whole-record driver, whose tidy frame carries the same statistics per month and period rather than pooled.
export_representativeness_gpkgDraws this table as nested discs.
Notes
The two
*_levelcolumns hold aLevel’s value –"high","medium", or"low"– rather than the member, becausestr()of a member renders as"Level.HIGH"and would reach a CSV that way. They still compare equal to the members.A radius present in one half and not the other still gets a row, with the other half’s columns missing. That is the case worth seeing rather than hiding: the land cover and the vegetation index need not have been evaluated over the same series of discs.
References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Sect. 2.4.
Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> def clim(cx): ... f = np.exp(-((X - cx) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) ... f /= f.sum() * 400.0 ... return xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> landcover = xr.DataArray( ... np.where(X < 60.0, 42, 81), dims=("x", "y"), coords={"x": x, "y": y} ... ) >>> scenes = [ ... xr.DataArray( ... 0.30 + 0.0005 * X + 0.01 * i, dims=("x", "y"), coords={"x": x, "y": y} ... ) ... for i in range(8) ... ] >>> categorical = evaluate_landcover(clim(40.0), landcover, radii=(250.0, 1000.0)) >>> continuous = evaluate_vegetation_index( ... [clim(40.0 + 8 * i) for i in range(8)], scenes, radii=(250.0, 1000.0) ... ) >>> summary = representativeness_summary( ... categorical=categorical, continuous=continuous, site_id="US-Xyz" ... ) >>> summary.shape # one row per radius (2, 29) >>> print( ... summary[ ... ["site_id", "radius", "dominant_class", ... "landcover_level", "continuous_level"] ... ].to_string(index=False) ... ) site_id radius dominant_class landcover_level continuous_level US-Xyz 250.0 42 low medium US-Xyz 1000.0 42 low medium
- fluxfootprints.representativeness_table(results: DataFrame, dataset: str) DataFrame[source]
Reshape a tidy result frame into one of the published table layouts.
Chu et al. (2021) release their per-site statistics as Datasets S4-S6 on Zenodo (doi:10.5281/zenodo.4015350). This puts the corresponding rows of
assess_representativeness()into the same shape and units, so that a local analysis can be read next to, or concatenated with, the published tables.- Parameters:
- resultspandas.DataFrame
Output of
assess_representativeness(), indexed or with the index already reset.- dataset{“S4”, “S5”, “S6”}
Which table to build, case-insensitively:
"S4"Land-cover statistics: the dominant class, P_footprint, P_target, their difference, and the chi-square test, per period and target area. Built from the
scope="site"categorical rows."S5"Site-month vegetation-index statistics: the footprint-weighted and target-area values and the sensor location bias Delta, per matched scene, period, and target area. Built from the
scope="period"continuous rows."S6"Site-level model II regressions in the layout of Table 1: intercept and slope with their confidence limits, R², RMSE, and MAE. Built from the
scope="site"continuous rows.
- Returns:
- pandas.DataFrame
The requested table over a fresh
RangeIndex, with the columns ofCHU_TABLE_COLUMNS[dataset]in that order. Empty, but still carrying every column, when results holds no rows of that kind – an analysis run without a land-cover raster has an empty S4.
- Raises:
- KeyError
If dataset is not one of the three.
- ValueError
If results is missing the columns of a tidy result frame.
See also
assess_representativenessProduces the frame this reshapes.
export_representativeness_tablesWrites these tables to disk.
Notes
The shares of S4 and the bias of S5 are converted to the percentages the paper reports, while the tidy frame keeps them as fractions like the rest of this module. Units are the only transformation: no row is dropped, reordered, or recomputed.
The column names are reconstructed from the paper’s text – Sects. 2.4 and 3.3, and Table 1 – rather than copied from the Zenodo archive, which this package does not ship or read. Treat them as a faithful layout rather than a byte-for-byte match, and check against the archive before merging on column names. Two columns have no counterpart in the paper’s own tables and are added because a local analysis needs them:
PRODUCTandVARIABLE, naming the raster each row came from, andTIMESTAMP, the retrieval time of the matched scene, which the paper resolves only to the month.Examples
>>> import numpy as np, pandas as pd, xarray as xr >>> times = pd.date_range("2020-01-01", "2020-06-30 21:00", freq="3h") >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> rng = np.random.default_rng(0) >>> centres = rng.normal(30.0, 25.0, times.size) # a wandering wind direction >>> f = np.stack( ... [np.exp(-((X - c) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) for c in centres] ... ) >>> f /= f.sum(axis=(1, 2), keepdims=True) * 400.0 >>> f_2d = xr.DataArray( ... f, coords={"time": times, "x": x, "y": y}, dims=("time", "x", "y") ... ) >>> landcover = xr.DataArray( ... np.where(X < 60.0, 42, 81), dims=("x", "y"), coords={"x": x, "y": y} ... ) >>> scenes = { ... pd.Timestamp(f"2020-{m:02d}-15"): xr.DataArray( ... 0.30 + 0.0005 * X + 0.02 * m, dims=("x", "y"), coords={"x": x, "y": y} ... ) ... for m in range(1, 7) ... } >>> results = assess_representativeness( ... f_2d, ... 40.0, ... -111.9, ... site_id="US-Xyz", ... tz=-7, ... landcover=landcover, ... continuous=scenes, ... radii=(250.0, 1000.0), ... ) >>> s6 = representativeness_table(results, "S6") >>> list(s6.columns) == list(CHU_TABLE_COLUMNS["S6"]) True >>> print( ... s6[["SITE_ID", "PERIOD", "TARGET_AREA", "N", "SLOPE", "R2", "REP_EVI"]] ... .head(2) ... .round(4) ... .to_string(index=False) ... ) SITE_ID PERIOD TARGET_AREA N SLOPE R2 REP_EVI US-Xyz daytime 250.0 6 0.9942 0.9997 high US-Xyz daytime 1000.0 6 0.9942 0.9997 high
The land-cover and per-period tables come off the same frame:
>>> representativeness_table(results, "S4").shape (4, 12) >>> representativeness_table(results, "S5").shape (24, 11)
- fluxfootprints.resample_footprints(da: DataArray, freq: str | None = None, reducer: str = 'mean', normalize_each_frame: bool = False) DataArray[source]
Aggregate a footprint time series onto a coarser time step.
- Parameters:
- daxarray.DataArray
Footprint densities with a
timedimension plusxandy.- freqstr or None, optional
Target time step; see
resolve_freq().Nonekeeps the native time steps.- reducer{“mean”, “sum”, “median”, “max”}, optional
Reduction applied within each period.
"mean"is appropriate for normalized footprint densities.- normalize_each_framebool, optional
If
True, rescale each resulting frame so it sums to one. Useful when comparing footprint shape rather than magnitude.
- Returns:
- xarray.DataArray
The aggregated series, with all-NaN periods dropped.
- fluxfootprints.resolve_freq(freq: str | None) str | None[source]
Translate a friendly time-step name to a pandas offset alias.
- Parameters:
- freqstr or None
"hourly","daily","monthly","native"/None, or any pandas offset alias (e.g."3h","7D") which is passed through.
- Returns:
- str or None
The pandas offset alias, or
Noneto keep the native time steps.
- Raises:
- ValueError
If
freqis not a string orNone.
- fluxfootprints.rma_regression(x: ndarray | Sequence[float], y: ndarray | Sequence[float], ci_level: float = 0.95, ci_method: str = 'analytical', n_boot: int = 2000, random_state: int | Generator | None = None) RMAFit[source]
Fit the reduced major axis (model II) regression of Eq. 7.
\[y \sim \beta_0 + \beta_1 x\]The reduced major axis – equivalently the standard major axis, the
"SMA"row of R’slmodel2– minimises the triangular areas between the points and the line rather than the vertical residuals alone. Its slope is the ordinary least squares slope divided by the Pearson correlation coefficient, i.e.sign(r) * sd(y) / sd(x).- Parameters:
- xarray_like
Predictor. In Eq. 7 the footprint-weighted values of Eq. 5.
- yarray_like
Response, of the same length as x. In Eq. 7 the target-area means.
- ci_levelfloat, default 0.95
Confidence level of the reported intervals, strictly between 0 and 1.
- ci_method{‘analytical’, ‘bootstrap’}, default ‘analytical’
How to bracket the slope.
'analytical'uses the closed form of McArdle (1988) thatlmodel2reports;'bootstrap'takes percentile limits over n_boot paired resamples, which does not assume bivariate normality but needs a decent n.- n_bootint, default 2000
Number of resamples, used only when
ci_method='bootstrap'.- random_stateint, numpy.random.Generator, or None, optional
Seed or generator for the bootstrap, for reproducible limits.
- Returns:
- RMAFit
The slope, intercept, R-squared, p-value, and the two confidence intervals.
- Raises:
- ValueError
If the inputs differ in length; if fewer than three finite pairs remain, leaving no residual degrees of freedom; if ci_level is not strictly between 0 and 1; if ci_method is neither of the two accepted values; or if n_boot is not positive.
See also
model2_regressionThe four-value form of this fit.
continuous_representativenessApplies it per target radius.
classify_continuousTurns slope, intercept, and R-squared into a Level.
Notes
Ordinary least squares must not be substituted here. OLS assumes the predictor is measured without error and minimises only the vertical residuals, so its slope is
rtimes this one: with the R-squared of 0.94 down to 0.71 that Chu et al. (2021) report across target radii, an OLS slope would sit roughly 3 % to 16 % below the RMA slope on the same data – always shallower, never steeper. Both variables here are spatial averages of the same noisy raster and both carry error, so that attenuation is a bias, not a difference of convention. It runs the wrong way for this analysis twice over: it deepens the apparent footprint-to-target-area bias of Sect. 3.3, and it pushes sites out of the0.9 <= slope <= 1.1HIGH criterion of Sect. 2.4 for no reason but the scatter. Table 1 is reproducible only againstlmodel2’s RMA.R-squared and the p-value are those of the Pearson correlation. They are symmetric in x and y and say nothing about the RMA line’s own residuals; the RMA slope cannot be zero, so the correlation’s null is the only meaningful one to test, which is what
lmodel2reports.Pairs where either value is non-finite are dropped before fitting. The slope is returned as
nanwhen either variable is constant, or when the correlation is exactly zero and its sign cannot be taken –lmodel2likewise refuses an SMA slope it cannot orient. R-squared and the p-value are still reported in the second case.References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Eq. 7.
Legendre, P. (2014). lmodel2: Model II Regression. R package version 1.7-2.
McArdle, B. H. (1988). The structural relationship: regression in biology. Canadian Journal of Zoology, 66, 2329-2339. Source of the analytical interval.
Examples
>>> footprint = [0.21, 0.35, 0.52, 0.66, 0.71, 0.44] >>> target = [0.19, 0.33, 0.49, 0.62, 0.70, 0.41] >>> fit = rma_regression(footprint, target) >>> fit.slope_ci[0] < fit.slope < fit.slope_ci[1] True
- fluxfootprints.sample_raster_on_grid(raster_path: str | Path, x: ndarray, y: ndarray, station_lat: float, station_lon: float, crs: str | int | None = 'auto', band: int = 1, categorical: bool = False) DataArray[source]
Read an external raster onto the tower-centred footprint grid.
Georeferences the grid with
footprint_grid_geometry(), then reprojects the raster onto it, so that land-cover and vegetation-index products can be compared cell-for-cell against a climatology.- Parameters:
- raster_pathstr or pathlib.Path
Path to a raster readable by rasterio / rioxarray, e.g. an NLCD tile or a Landsat EVI scene.
- x, ynumpy.ndarray
Cell-centre offsets from the tower [m] (
model.x,model.y).- station_lat, station_lonfloat
Tower position in decimal degrees (WGS 84).
- crsstr, int, or None, default “auto”
Target CRS for the grid;
"auto"selects the local WGS 84 UTM zone, matchingexport_rasters_geotiff().- bandint, default 1
One-based band index to read.
- categoricalbool, default False
If True, resample with nearest neighbour to preserve class codes; if False, resample bilinearly for a continuous field.
- Returns:
- xarray.DataArray
Raster values with dims
(x, y)and the coords of the footprint grid, NaN where the source raster does not cover the grid.
- Raises:
- ImportError
If
rioxarrayis not installed.- ValueError
If the raster has no CRS, so it cannot be reprojected.
See also
fluxfootprints.footprint_grid_geometryBuilds the target
GridGeometry.fluxfootprints.openet_mask_on_gridThe same reprojection for data-availability masks.
_align_rasterThe reprojection this wraps.
Notes
The warp itself happens on the georeferenced grid, whose coordinates are projected metres; the result is handed back on the tower-centred
(x, y)grid the rest of this module works in, so that it lines up cell for cell with a climatology and withtarget_area_mask().Examples
>>> import numpy as np, tempfile, pathlib >>> import rasterio >>> from rasterio.transform import from_origin >>> from fluxfootprints.openet_masking import footprint_grid_geometry >>> lat, lon = 40.0, -111.9 >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> geom = footprint_grid_geometry(x, y, lat, lon, crs="auto")
Write a small west-to-east EVI ramp in the tower’s own UTM zone:
>>> directory = pathlib.Path(tempfile.mkdtemp()) >>> path = directory / "evi.tif" >>> n = 60 >>> values = 0.20 + 0.005 * np.tile(np.arange(n, dtype="float32"), (n, 1)) >>> with rasterio.open( ... path, ... "w", ... driver="GTiff", ... height=n, ... width=n, ... count=1, ... dtype="float32", ... crs=geom.crs, ... transform=from_origin( ... geom.x_origin - 600.0, geom.y_origin + 600.0, 20.0, 20.0 ... ), ... ) as dst: ... dst.write(values, 1) >>> evi = sample_raster_on_grid(path, x, y, lat, lon) >>> evi.dims, evi.shape (('x', 'y'), (21, 21)) >>> round(float(evi.sel(x=0.0, y=0.0)), 4) 0.3475 >>> round(float(evi.min()), 4), round(float(evi.max()), 4) (0.2975, 0.3975)
- fluxfootprints.seasonal_overlap(weights: DataArray, dim: str = 'month') float[source]
Compute the seasonal footprint overlap index O80_season, Eq. 2.
\[O_{80,season} = \sum_{i=1}^{I} \left( \prod_{k=1}^{K} \varphi_{ik} \right)^{1/K}\]The cell-wise geometric mean of the K monthly climatologies in a site-year, summed over the I grid cells.
- Parameters:
- weightsxarray.DataArray
Monthly climatologies stacked over dim, on a common grid and each month already rescaled to sum to 1 – the output of
truncate_to_contour(). Computed separately for daytime and nighttime in the paper.- dimstr, default “month”
Name of the dimension the months are stacked over.
- Returns:
- float
Overlap index in [0, 1]. One indicates perfectly overlapping monthly climatologies; the paper treats values below 0.8 as showing “noticeable monthly variability”, which it found at 32-44 % of the studied site-years, concentrated in the cropland, grassland, and wetland sites whose canopy height swings through the growing season.
- Raises:
- TypeError
If weights is not a DataArray.
- ValueError
If weights carries no dim dimension or holds fewer than two months along it; if any weight is non-finite or negative; or if any month does not sum to 1.
See also
overlapThe two-footprint kernel, which Eq. 2 reduces to at K = 2.
seasonal_overlap_indexThe same index from a sequence of climatologies.
daynight_overlapThe companion index across daytime and nighttime.
Notes
The exponent used here is 1/K, a true geometric mean over all K months. Equation 2 as printed in Chu et al. (2021) carries 1/k inside a product running over k = 1..K, which is a typo: the index k has no value outside the product it is bound to, and only the 1/K reading returns 1.0 when every month is identical – the defining property of an overlap index.
The geometric mean is computed as
exp(mean(log(w)))over the cells every month covers, and is taken as zero elsewhere, so a zero in any one month propagates to a zero cell rather than a NaN. The index therefore measures the source area common to all months, and a single month pointing elsewhere is enough to drive it to zero.Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> def weights(cx, spread=80.0): ... f = np.exp(-((X - cx) ** 2 + Y ** 2) / (2 * spread ** 2)) ... f /= f.sum() # the overlap indices take unit-sum weights ... return xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> months = xr.concat( ... [weights(0.0), weights(20.0), weights(40.0)], dim="month" ... ).assign_coords(month=[1, 2, 3]) >>> round(seasonal_overlap(months), 4) 0.9809
- fluxfootprints.seasonal_overlap_index(climatologies: Sequence[DataArray]) float[source]
Compute the seasonal footprint overlap index O80_season, Eq. 2.
Adapter over
seasonal_overlap()for monthly climatologies held as a sequence rather than stacked over a dimension. See that function for the equation, the 1/K exponent, and the handling of zeros.- Parameters:
- climatologiessequence of xarray.DataArray
Monthly climatologies for one site-year, each with dims
(x, y)on a common grid and each summing to 1. Computed separately for daytime and nighttime in the paper.
- Returns:
- float
Overlap index in [0, 1]. One indicates perfectly overlapping monthly climatologies; the paper treats values below 0.8 as showing “noticeable monthly variability”.
- Raises:
- ValueError
If fewer than two climatologies are supplied, or their grids differ.
Notes
The geometric mean is zero wherever any single month has zero weight, so the index is dominated by the area common to all months.
Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> def weights(cx, spread=80.0): ... f = np.exp(-((X - cx) ** 2 + Y ** 2) / (2 * spread ** 2)) ... f /= f.sum() # the overlap indices take unit-sum weights ... return xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> round(seasonal_overlap_index([weights(c) for c in (0.0, 20.0, 40.0)]), 4) 0.9809
- fluxfootprints.sensor_location_bias(w: DataArray | ndarray, raster: DataArray | ndarray, x: ndarray | DataArray, y: ndarray | DataArray, radii: Sequence[float] = (250, 500, 1000, 1500, 2000, 3000)) DataFrame[source]
Compute the sensor location bias Delta against each target area, Eq. 6.
\[\Delta = \frac{EVI_{footprint} - EVI_{target}}{EVI_{target}}\]After Schmid and Lloyd (1999); the time-explicit footprint-to-target-area bias for one period, evaluated against the series of target radii of Sect. 2.1. The footprint-weighted value of Eq. 5 does not depend on the radius, so it is computed once and repeated down the frame; only the target-area mean moves.
- Parameters:
- wxarray.DataArray or numpy.ndarray
Footprint source weights with dims
(x, y), normally one period’s truncated, renormalised climatology fromtruncate_to_contour()or a slice ofmonthly_climatologies(). Weights summing to something other than 1 are renormalised, as infootprint_weighted_value().- rasterxarray.DataArray or numpy.ndarray
Continuous field on the same grid, e.g. the Landsat EVI scene matched to this period, NaN where it holds no data.
- x, ynumpy.ndarray or xarray.DataArray
Cell-centre offsets from the tower [m] (
model.x,model.y), which is the grid both w and raster must be on.- radiisequence of float, default TARGET_RADII
Target-area radii [m], evaluated and reported in the order given.
- Returns:
- pandas.DataFrame
One row per radius, with columns
radiusTarget-area radius [m].
value_footprintFootprint-weighted value, Eq. 5. Constant down the frame.
value_targetTarget-area mean over the disc of that radius.
deltaSensor location bias as a fraction, not a percentage; multiply by 100 to match the paper’s figures. Positive values mean the footprint covered higher values than its surroundings, which held at every target radius in the paper.
nanwhere either value is non-finite or the target-area mean is zero.within_thresholdWhether
|delta| <=BIAS_THRESHOLD, in pandas’ nullable boolean dtype, andpd.NAwhere delta is undefined.
DataFrame.attrs["bias_threshold"]records the threshold applied.
- Raises:
- ValueError
If radii is empty; if any radius is not positive and finite, or is smaller than half the grid spacing so that its disc holds no cell centre; if w, raster, and the grid x and y describe are not one and the same; or if w holds a non-finite or negative weight, or no weight at all.
See also
sensor_location_bias_seriesMaps this over a time-indexed collection.
footprint_weighted_valueEVI_footprint, Eq. 5.
target_area_valueEVI_target.
model2_regressionThe site-level counterpart, Eq. 7.
Notes
A raster covering only part of the footprint or the disc lowers the retained weight of the underlying averages rather than making them NaN, so a delta here can rest on partial coverage. Call
footprint_weighted_value()andtarget_area_value()directly when that fraction matters; Chu et al. (2021) kept only scenes that were cloud-free within 3000 m of the tower.References
Schmid, H. P., and Lloyd, C. R. (1999). Spatial representativeness and the location bias of flux footprints over inhomogeneous areas. Agric. For. Meteorol., 93, 195-209.
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Sect. 2.4.
Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> f = np.exp(-((X - 40.0) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) >>> f /= f.sum() * 400.0 >>> fclim = xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> weights = truncate_to_contour(fclim) >>> evi = xr.DataArray( ... 0.30 + 0.0005 * X, dims=("x", "y"), coords={"x": x, "y": y} ... ) >>> bias = sensor_location_bias(weights, evi, x, y, radii=(250.0, 1000.0)) >>> print(bias.round(4).to_string(index=False)) radius value_footprint value_target delta within_threshold 250.0 0.32 0.3 0.0667 True 1000.0 0.32 0.3 0.0667 True >>> bias.attrs["bias_threshold"] 0.1
- fluxfootprints.sensor_location_bias_series(pairs: Mapping[Any, Any] | Series | Sequence[Any], x: ndarray | DataArray, y: ndarray | DataArray, radii: Sequence[float] = (250, 500, 1000, 1500, 2000, 3000)) DataFrame[source]
Compute the sensor location bias of every period of a record, Eq. 6.
sensor_location_bias()mapped over a time-indexed collection of matched climatology / field pairs and concatenated: the site-months of Chu et al. (2021), each a monthly footprint climatology paired with the Landsat scene retrieved within it. Grouping the result byradiusand averagingwithin_thresholdreproduces the percentages within the +/-10 % threshold of Sect. 3.3 and Fig. 7.- Parameters:
- pairsmapping, pandas.Series, or sequence
The periods, in any of three forms:
a mapping of time label ->
(climatology, raster);a
pandas.Seriesindexed by time holding such pairs;an iterable of
(time, climatology, raster)triples.
Each climatology and raster is what
sensor_location_bias()takes as w and raster: a truncated, renormalised climatology and the field matched to that period, both on the grid x and y describe. Periods are evaluated in the order presented, not sorted.- x, ynumpy.ndarray or xarray.DataArray
Cell-centre offsets from the tower [m], shared by every period.
- radiisequence of float, default TARGET_RADII
Target-area radii [m].
- Returns:
- pandas.DataFrame
The frames of
sensor_location_bias()stacked under a leadingtimecolumn, over a freshRangeIndex: columnstime,radius,value_footprint,value_target,delta, andwithin_threshold, one row per period and radius.DataFrame.attrs["bias_threshold"]records the threshold applied.
- Raises:
- TypeError
If pairs is not one of the three accepted forms.
- ValueError
If pairs holds no periods, or for any of the reasons
sensor_location_bias()raises, with the offending time label prepended to the message.
See also
sensor_location_biasThe single-period computation this maps.
monthly_climatologiesProduces the per-period climatologies.
evaluate_vegetation_indexRegresses the same pairs, Eq. 7.
Notes
The paper matched a scene to the climatology of the month it was retrieved in, and its Fig. 7 pools 3307 such site-months across 214 sites. Pairing is the caller’s job here: this function evaluates the pairs it is given, in the order it is given them.
References
Chu, H., et al. (2021). Agric. For. Meteorol., 301-302, 108350, Sect. 3.3.
Examples
>>> import numpy as np, pandas as pd, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> def clim(cx): ... f = np.exp(-((X - cx) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) ... f /= f.sum() * 400.0 ... return xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> clims = [clim(40.0 + 8 * i) for i in range(8)] >>> scenes = [ ... xr.DataArray( ... 0.30 + 0.0005 * X + 0.01 * i, dims=("x", "y"), coords={"x": x, "y": y} ... ) ... for i in range(8) ... ] >>> pairs = sensor_location_bias_series( ... { ... pd.Timestamp(f"2020-{m:02d}-01"): (truncate_to_contour(c), s) ... for m, (c, s) in enumerate(zip(clims, scenes), 1) ... }, ... x, ... y, ... radii=(250.0, 1000.0), ... ) >>> pairs.shape (16, 6) >>> print( ... pairs.head(4).to_string( ... index=False, float_format=lambda v: format(v, ".4f") ... ) ... ) time radius value_footprint value_target delta within_threshold 2020-01-01 250.0000 0.3200 0.3000 0.0667 True 2020-01-01 1000.0000 0.3200 0.3000 0.0667 True 2020-02-01 250.0000 0.3342 0.3100 0.0781 True 2020-02-01 1000.0000 0.3342 0.3100 0.0781 True
- fluxfootprints.sigma_v(ustar: float) float[source]
Cross‑wind velocity std dev (surface‑layer neutral approx.).
- fluxfootprints.sigma_w(ustar: float) float[source]
Vertical velocity standard deviation (surface‑layer neutral approx.).
- fluxfootprints.summarize_periods(model: BaseFootprintModel, df: DataFrame, et_source: str | None = 'LE', is_le: bool = True, calc_et_weighted: bool = True, daily: bool = True, monthly: bool = True, normalize_each_time: bool = False) SummaryResult[source]
Build daily/monthly summaries matching Kljun canonical denominator logic.
Aggregates a model’s time-resolved 2-D footprint into daily and/or monthly mean footprints, ET-weighted footprints (optional), and domain-coverage statistics. Timesteps whose footprint integrates to zero (e.g. model failures or filtered records) are treated as invalid and excluded from every average and from the ET-weighting denominator.
- Parameters:
- modelBaseFootprintModel
A footprint model that has already been run, so that
model.f_2dis populated andmodel.get_footprint_timeseries()returns anxr.DataArraywith dims("time", "x", "y")holding footprint density (m^-2). If the model exposesdxanddythey are used as the grid cell size; otherwise cell size is inferred from thex/ycoordinate spacing.- dfpd.DataFrame
Time-indexed input data. Only used to supply the ET/LE column; it is reindexed onto the footprint
timecoordinate using exact matches, so timestamps absent fromdfget NaN weights.- et_sourcestr or None, default “LE”
Name of the column in
dfused for ET weighting. If None or not present indf, ET-weighted outputs are silently skipped.- is_lebool, default True
If True,
et_sourceis latent energy (W m^-2) and is converted to ET (mm hr^-1) by dividing by 680.6. Set False whenet_sourcealready holds ET in mm hr^-1 (e.g. NLDAS).- calc_et_weightedbool, default True
Toggle ET-weighted footprints and ET-weighted coverage on/off.
- dailybool, default True
Compute daily (
"1D") aggregates.- monthlybool, default True
Compute monthly (
"MS", month-start labelled) aggregates.- normalize_each_timebool, default False
If True, rescale each valid time slice so it sums to 1 over x, y before averaging. Note this normalizes the raw sum (not the area-integral), so the resulting values are per-cell weights rather than densities. Coverage statistics are always computed on the un-normalized footprint.
- Returns:
- SummaryResult
Fields are populated according to the
daily,monthlyand ET flags; unrequested outputs remain None.f_daily_mean/f_monthly_meanxr.DataArrayUnweighted mean of valid footprint slices per period.
f_daily_et_weighted/f_monthly_et_weightedxr.DataArraysum(f * ET) / sum(ET)over valid slices per period. NaN for periods whose ET sum is <= 0.
daily_domain_coverage/monthly_domain_coveragepd.DataFrameIndexed by period, with columns
mean_coverage_pct,et_weighted_coverage_pct(only when ET is used),min_coverage_pct,max_coverage_pctandvalid_timesteps. Coverage is the percentage of the footprint captured inside the model domain (sum(f) * dx * dy * 100).
- Raises:
- ValueError
If the model has no computed
f_2dor does not provide a time-resolved footprint.
Notes
Coverage values can exceed 100% close to the tower, where a narrow crosswind Gaussian is under-resolved by the grid. This discretization artifact is intentionally left unclamped.
Negative ET values are not clipped and therefore enter the weighting as-is; filter or clip
et_sourcebeforehand if that is undesirable.
- fluxfootprints.symmetry_index(area: float, fetch: float) float[source]
Compute the footprint symmetry index S80, Eq. 1.
\[S_{80} = \frac{A_{80}}{\pi X_{80}^2}\]- Parameters:
- areafloat
Footprint area A80 [m²].
- fetchfloat
Footprint fetch X80 [m].
- Returns:
- float
Symmetry index in [0, 1]. One indicates a perfectly circular climatology centred on the tower; the paper flags values below
ASYMMETRY_THRESHOLD(0.30) as relatively asymmetric, arising from uni- or bimodal prevailing winds.nanif fetch is zero or non-finite, or if area is non-finite.
See also
footprint_symmetryThe same index straight from a truncated climatology.
Notes
The disc of radius X80 the index compares against is the smallest one centred on the tower that contains the contour, so the ratio is bounded above by 1 in the continuum. On a grid it can creep past 1, because the area counts whole cells while the fetch reaches only to cell centres; the result is clipped to [0, 1] rather than reported above one.
Examples
>>> round(symmetry_index(40000.0, 200.0), 6) # a square inscribed in its disc 0.31831 >>> symmetry_index(1.0, 0.0) # an undefined fetch nan
- fluxfootprints.target_area_composition(landcover: DataArray | ndarray, x: ndarray | DataArray, y: ndarray | DataArray, radius: float) Series[source]
Compute the composition of a categorical field over a target area.
The categorical counterpart of
target_area_value(): every class takes the share of the disc’s cells it covers, which is P_target of Sect. 2.4 once multiplied by 100.- Parameters:
- landcoverxarray.DataArray or numpy.ndarray
Categorical raster with dims
(x, y)on the tower-centred grid, NaN where it holds no data.- x, ynumpy.ndarray or xarray.DataArray
Cell-centre offsets from the tower [m] (
model.x,model.y).- radiusfloat
Target-area radius [m].
- Returns:
- pandas.Series
Area fraction per class, indexed by class code in ascending order (index name
class) and summing to 1. Empty when no cell inside the disc holds a class code.Series.attrsholdsretained_weight, the fraction of the disc’s cells that held one, andn_cells.
- Raises:
- ValueError
As for
target_area_value().
See also
footprint_weighted_compositionThe footprint-weighted counterpart.
Notes
Cells without a class code are dropped from numerator and denominator alike, so the fractions describe the classified part of the disc; how much of it that was is
attrs["retained_weight"].Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, _ = np.meshgrid(x, y, indexing="ij") >>> landcover = xr.DataArray( ... np.where(X < 60.0, 42, 81), dims=("x", "y"), coords={"x": x, "y": y} ... ) >>> composition = target_area_composition(landcover, x, y, 100.0) >>> {int(k): round(float(v), 4) for k, v in composition.items()} {42: 0.7901, 81: 0.2099}
- fluxfootprints.target_area_mask(x: ndarray | DataArray, y: ndarray | DataArray, radius: float) DataArray[source]
Build a boolean mask of the disc of a given radius around the tower.
- Parameters:
- x, ynumpy.ndarray or xarray.DataArray
Cell-centre offsets from the tower [m], as produced by the footprint models (
model.x,model.y).- radiusfloat
Target-area radius [m], e.g. one of
TARGET_RADII.
- Returns:
- xarray.DataArray
Boolean array with dims
(x, y), True where the cell centre lies within radius of the origin.
- Raises:
- ValueError
If radius is not positive and finite, or if x or y is not a non-empty 1-D array of cell-centre offsets.
Notes
Membership is decided on cell centres, so a target area larger than the model domain is silently clipped to the domain. Compare the mask’s cell count against
pi * radius**2 / (dx * dy)to detect that case.Examples
>>> import numpy as np >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> mask = target_area_mask(x, y, 100.0) >>> mask.dims ('x', 'y') >>> int(mask.sum()) # cell centres within 100 m of the tower 81
- fluxfootprints.target_area_value(raster: DataArray | ndarray, x: ndarray | DataArray, y: ndarray | DataArray, radius: float) WeightedValue[source]
Compute the unweighted mean of a continuous field over a target area.
EVI_target of Sect. 2.4: the field averaged over the disc of radius radius around the tower, every cell counting the same, against which the footprint-weighted value of Eq. 5 is compared.
- Parameters:
- rasterxarray.DataArray or numpy.ndarray
Continuous field with dims
(x, y)on the tower-centred grid, NaN where it holds no data, assample_raster_on_grid()returns it.- x, ynumpy.ndarray or xarray.DataArray
Cell-centre offsets from the tower [m] (
model.x,model.y), which is the grid raster must be on.- radiusfloat
Target-area radius [m], e.g. one of
TARGET_RADII.
- Returns:
- WeightedValue
The arithmetic mean over the disc, the fraction of its cells that held data, and the number of those cells.
valueisnanandretained_weight0 when no cell inside the disc holds data.
- Raises:
- ValueError
If radius is not positive and finite; if raster is not on the grid x and y describe; or if the disc is smaller than one grid cell, so that no cell centre falls inside it.
See also
footprint_weighted_valueThe footprint-weighted counterpart, Eq. 5.
target_area_maskThe disc this averages over.
Notes
A disc reaching past the edge of the domain is silently clipped to it, as
target_area_mask()documents, and a raster not covering the whole disc lowersretained_weightrather than the value going NaN. The two together are what make a target-area value at 3000 m trustworthy or not, and the paper’s larger radii are exactly where domains and scenes run out.Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, _ = np.meshgrid(x, y, indexing="ij") >>> evi = xr.DataArray(0.30 + 0.0005 * X, dims=("x", "y"), coords={"x": x, "y": y}) >>> result = target_area_value(evi, x, y, 100.0) >>> round(result.value, 4), round(result.retained_weight, 4), result.n_cells (0.3, 1.0, 81)
- fluxfootprints.truncate_to_contour(fclim: DataArray, dx: float | None = None, dy: float | None = None, fraction: float = 0.8, renormalize: bool = True) DataArray[source]
Truncate a climatology at a source-weight contour and rescale its weights.
Chu et al. (2021), Sect. 2.2, truncate every climatology at the 80 % contour and rescale the retained weights to sum to unity, so that all downstream weighted statistics are taken over the same source area.
- Parameters:
- fclimxarray.DataArray
Footprint climatology with dims
(x, y)[m⁻²].- dx, dyfloat, optional
Grid spacing [m]. Inferred from coordinates when omitted.
- fractionfloat, default 0.8
Source-weight fraction to retain.
- renormalizebool, default True
If True, rescale the retained cells so they sum to 1, giving unitless weights suitable for
footprint_weighted_value(). If False, retain the original densities [m⁻²].
- Returns:
- xarray.DataArray
Climatology with cells outside the contour set to zero, carrying the dims and coords of fclim. Unitless when renormalize is True.
Notes
The rescaling is a sum over cells, not an area integral, matching the paper’s
sum(phi_ik) = 1convention. The retained cells therefore sum to 1 regardless of fraction: the fraction selects the source area, it does not survive as the mass of the result.The mask is applied with
where(..., 0.0), so cells outside the contour – including any non-finite ones – come back as zeros rather than NaN, and the weights stay safe to sum.Examples
>>> import numpy as np, xarray as xr >>> x = np.arange(-200.0, 201.0, 20.0) >>> y = x.copy() >>> X, Y = np.meshgrid(x, y, indexing="ij") >>> f = np.exp(-((X - 40.0) ** 2 + Y ** 2) / (2 * 80.0 ** 2)) >>> f /= f.sum() * 400.0 # a density: integrates to 1 over dx * dy >>> fclim = xr.DataArray(f, dims=("x", "y"), coords={"x": x, "y": y}) >>> truncated = truncate_to_contour(fclim) >>> float(round(float(truncated.sum()), 10)) # renormalised to unit sum 1.0 >>> truncated.attrs["contour_n_cells"] 161
Keeping the original densities instead:
>>> raw = truncate_to_contour(fclim, renormalize=False) >>> bool(float(raw.sum()) < 1.0) True
- fluxfootprints.validate_canonical_file(path: str | Path, *, require_tz: bool = True, require_provenance: bool = True, strict_columns: bool = False) ValidationReport[source]
Read a processed file with its sidecar, and validate both.
- Parameters:
- path
Processed data file. Its sidecar is read from
provenance_sidecar_path()when one exists.- require_tz, require_provenance, strict_columns
Passed through to
validate_canonical_frame(). Provenance is required here by default, since a file inprocessed/is a deliverable rather than an intermediate frame.
- Returns:
- ValidationReport
- fluxfootprints.validate_canonical_frame(df: DataFrame, *, provenance: Mapping[str, Any] | str | Path | None = None, require_tz: bool = True, require_provenance: bool = False, strict_columns: bool = False) ValidationReport[source]
Validate a DataFrame against the canonical benchmark contract.
The frame is never modified and no value is ever converted: a unit problem is reported, not repaired.
- Parameters:
- df
Candidate processed table, timestamps carried in a
DatetimeIndex.- provenance
A loaded sidecar mapping, a path to a processed data file whose sidecar should be read, or
Noneto skip the provenance checks.- require_tz
Treat a timezone-naive index as an error (the default). Set
Falseonly when the timestamp convention is documented inmetadata/<site>/; the finding is then downgraded to a warning.- require_provenance
Error when no provenance is supplied at all. A file destined for
processed/should be validated with this on.- strict_columns
Warn about columns outside the canonical vocabulary. Off by default, since retaining raw source columns is encouraged.
- Returns:
- ValidationReport
Findings, with
ValidationReport.okFalse if any is an error.
Examples
>>> validate_canonical_frame(df).raise_for_status()
- fluxfootprints.wang2006_fy(x: ndarray, zm: float, h: float, L: float) ndarray[source]
Cross‑wind integrated footprint
f_y(x)(m⁻¹).Implements Wang et al. (2006) adjusted CWI footprint.
- Parameters:
- xndarray
1‑D array of stream‑wise distances (m, positive upwind).
- zmfloat
Measurement height (m).
- hfloat
Boundary‑layer height (m).
- Lfloat
Obukhov length (m). Must be negative for convective cases.
- Returns:
- ndarray
Footprint values f_y(x) (same shape as x) with ∫ f_y dx = 1.
- fluxfootprints.wrap_degrees(values: Any) Any[source]
Wrap bearings onto
[0, 360).- Parameters:
- values
Scalar, array or Series of degrees, possibly negative or above 360.
- Returns:
- Same type as the input, wrapped. NaN is preserved.
- fluxfootprints.write_provenance_sidecar(data_path: str | Path, *, site_id: str, fields: Mapping[str, Mapping[str, Any]], timestamp_convention: str, source: Mapping[str, Any] | None = None, notes: str | None = None) Path[source]
Write the JSON provenance sidecar for a processed file.
- Parameters:
- data_path
The processed data file the sidecar describes.
- site_id
AmeriFlux/NEON site ID.
- fields
Per-column records. Each needs
provenance(aProvenancemember or its value) andunits;prep_calculatedandexternal_forcingalso needmethod, andobservedandnetwork_derivedneedsource_variable. Extra keys (source_file,notes) are kept as given.- timestamp_convention
e.g.
"UTC, start-of-interval, 30 min". Never left implicit.- source
Dataset-level provenance:
dataset,url,doi,version,license,retrieved_utc.- notes
Free-text caveats.
- Returns:
- pathlib.Path
The sidecar path written.