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: ABC

Abstract 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

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 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

to_netcdf(filepath: str) → None[source]

Save results to netCDF file.

Parameters:
filepathstr

Output file path

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: BaseFootprintModel

Flux 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_paper_smoothing()

Apply 3x3 smoothing from Kljun et al. paper with mass conservation.

apply_rsl_corrections()

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.

calc_crosswind_integrated_extent(r)

Calculate crosswind-integrated footprint extent for relative contribution r.

calc_crosswind_integrated_footprint(x_star)

Calculate the scaled crosswind-integrated footprint F̂y*(X̂*).

calc_crosswind_spread(x)

Calculate the standard deviation of cross-wind spread \(\sigma_y\).

calc_peak_based_limits(r)

Calculate upwind and downwind distances from the peak location for fraction r.

calc_pi_4()

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.

calc_scaled_distance_rsl()

Calculate scaled distance X* with roughness sublayer correction.

calc_scaled_footprint_peak()

Calculate the scaled footprint peak location (X*max).

calc_scaled_x(x)

Calculate the scaled distance X* based on wind and height parameters.

calc_unstable_psi(stability_param)

Calculate ψM for unstable atmospheric conditions.

calc_xr_footprint()

Compute the flux footprint using xarray operations.

calculate_pi_groups()

Calculate dimensionless Π groups for analysis.

calculate_source_areas()

Compute source area contour datasets for all specified relative contributions, derived directly from the fclim_2d climatology grid.

check_rsl_validity()

Check if the measurement height is above the roughness sublayer.

check_validity_ranges()

Check validity ranges according to equation 27 and other constraints from Kljun et al. (2015).

create_xr_dataset()

Create xarray Dataset from input DataFrame.

define_domain()

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.

get_source_area_contour(r)

Compute the source area contour threshold for fraction r of the climatological footprint, operating directly on the pre-computed fclim_2d grid.

handle_stability_regimes()

Classify stability regimes for diagnostics.

initialize_model_parameters()

Initialize core model parameters (a, b, c, d, ac, bc, cc).

normalize_footprint()

Ensure footprint integrates to 1.0 after calculation.

plot_footprint([config, ax, show_contours, ...])

Plot the footprint climatology with optional contours.

prep_df_fields()

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_const depends 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.ds are used, and an xr.DataArray is 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_x converts 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_4 with Pi_4 = ln(zm/z0) - psi_M = u(zm)/(u* k) (Eqs. 6/7). This is the same scaled<->real conversion used in calc_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() or calc_xr_footprint().

Returns:
dict

Mapping of contribution label (e.g. 'r_80') to the xarray.Dataset returned by get_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

  1. Stability validity (Eq. 27): -15.5 ≤ zm/L

  2. Turbulence validity: u* > 0.1 m/s, σv > 0

  3. 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).
create_xr_dataset()[source]

Create xarray Dataset from input DataFrame.

define_domain()[source]

Define the spatial computational domain and polar coordinate grids.

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 r

of the total flux (use this as the levels argument to ax.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_contour

Mask 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.

normalize_footprint()[source]

Ensure footprint integrates to 1.0 after calculation.

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

verify_data(data: DataArray, name: str) → bool[source]

Verify the content and structure of a DataArray.

Parameters:
dataxarray.DataArray

Data array to check.

namestr

Variable name for logging.

Returns:
bool

True if valid, False otherwise.

verify_results(data, name='data')[source]

Check for NaN or infinite values in the given data.

Parameters:
dataarray-like or xarray.DataArray

The data to verify.

namestr, optional

Descriptive name for logging.

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: BaseFootprintModel

Create a footprint‑climatology from a series of individual flux‑footprint estimates using the simple 2‑D parameterisation of Kljun et al. (2015).

The routine

  1. computes a footprint for every time step,

  2. rotates each footprint into its observed wind direction,

  3. aggregates all footprints onto a common grid, and

  4. 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.

prep_df_fields()

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()
calc_xr_footprint()[source]
create_xr_dataset()[source]
define_domain()[source]
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 in calc_xr_footprint(), not here.

raise_ffp_exception(code)[source]
run(return_result: bool = True)[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

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**m and K(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() and analytical_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 takes x/y as 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. via scipy.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 <= 0 are upwind of the sensor, where the model is undefined, and are assigned zero density.

xi, m, n

Parameters returned by length_scale_xi() and analytical_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: BaseFootprintModel

Kormann & Meixner (2001) analytical footprint model adapter.

Wraps the analytical functions in fluxfootprints.kormannmeixner in the standard BaseFootprintModel interface. Intended to be driven through fluxfootprints.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']
run(return_result: bool = True) → Dataset | None[source]

Execute Kormann-Meixner footprint calculation.

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‑D f_y(x).

Assumes the lateral distribution at each stream‑wise distance x is 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⁻¹) and U (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: BaseFootprintModel

Wang et al. (2006) convective footprint model adapter.

Wraps wang2006_fy() and reconstruct_gaussian_2d() in the standard BaseFootprintModel interface. Intended to be driven through fluxfootprints.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']
run(return_result: bool = True) → Dataset | None[source]

Execute Wang footprint calculation.

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: object

A 3‑D backward Lagrangian stochastic footprint model.

Methods

crosswind_integrated()

Return upstream distance array and integrated footprint f(x).

footprint()

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).

footprint() → Tuple[ndarray, ndarray, ndarray][source]

Return (x_centres, y_centres, footprint_density).

run() → None[source]

Run the LS footprint simulation and accumulate 2‑D footprint.

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: object

Container 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
rng()[source]
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_model.sigma_v(ustar: float) → float[source]

Cross‑wind velocity std dev (surface‑layer neutral approx.).

fluxfootprints.ls_footprint_model.sigma_w(ustar: float) → float[source]

Vertical velocity standard deviation (surface‑layer neutral approx.).

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: BaseFootprintModel

Lagrangian Stochastic footprint model adapter.

Wraps BackwardLSModel in the standard BaseFootprintModel interface. Intended to be driven through fluxfootprints.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']
run(return_result: bool = True) → Dataset | None[source]

Execute Lagrangian footprint calculation.

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: object

Class to store footprint calculation results

Methods

error()

Create an error footprint with all values set to -9999

classmethod error()[source]

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 r of the normalised footprint is traced as a polygon. The polygon is shifted from tower-relative metres into crs_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 x and y attributes 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_weighted and f_monthly_et_weighted are read as xr.DataArray objects with a time dimension. None layers 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_lon and centroid_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. If crs_out names a different CRS, each slice is then reprojected onto that CRS with bilinear resampling.

Parameters:
modelFFPModel

A footprint model that has been run. Its x and y attributes 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_weighted and f_monthly_et_weighted are read as xr.DataArray objects with dims ("time", "x", "y"). Missing or None attributes 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 by pyproj.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.x or model.y has fewer than two points, or if which contains an unknown layer name.

Notes

Files are named {prefix}_{layer}_{YYYYMMDD}.tif for daily layers and {prefix}_{layer}_{YYYYMM}.tif for 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_2d is populated and model.get_footprint_timeseries() returns an xr.DataArray with dims ("time", "x", "y") holding footprint density (m^-2). If the model exposes dx and dy they are used as the grid cell size; otherwise cell size is inferred from the x/y coordinate spacing.

dfpd.DataFrame

Time-indexed input data. Only used to supply the ET/LE column; it is reindexed onto the footprint time coordinate using exact matches, so timestamps absent from df get NaN weights.

et_sourcestr or None, default “LE”

Name of the column in df used for ET weighting. If None or not present in df, ET-weighted outputs are silently skipped.

is_lebool, default True

If True, et_source is latent energy (W m^-2) and is converted to ET (mm hr^-1) by dividing by 680.6. Set False when et_source already 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, monthly and ET flags; unrequested outputs remain None.

  • f_daily_mean / f_monthly_meanxr.DataArray

    Unweighted mean of valid footprint slices per period.

  • f_daily_et_weighted / f_monthly_et_weightedxr.DataArray

    sum(f * ET) / sum(ET) over valid slices per period. NaN for periods whose ET sum is <= 0.

  • daily_domain_coverage / monthly_domain_coveragepd.DataFrame

    Indexed by period, with columns mean_coverage_pct, et_weighted_coverage_pct (only when ET is used), min_coverage_pct, max_coverage_pct and valid_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_2d or 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_source beforehand 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: object

Georeferencing 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: object

Result 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.DataArray

mask_footprint_dataarray()

SummaryResult

mask_summaries()

path to a directory or GeoTIFF

mask_rasters_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.date keys 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, x and y dims, where x/y are metres from the tower – e.g. model.f_2d or any field of SummaryResult.

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.nan to 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), and retained_fraction (share of footprint weight surviving each step), plus missing_dates for steps with no OpenET coverage.

Notes

retained_fraction is 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 by summarize_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.tif uses that day, ..._202402.tif combines every OpenET day in February 2024 – so this works directly on the output of export_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 (and openet_retained_frac_etw for 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_DD and YYYY.MM.DD anywhere 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). None if 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.concat_fetch_gdf(data, epsg=5070)[source]
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 column c two 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:

  1. Run multiple footprint models (the xarray rewrite ffp_xr, the analytic Kormann‑Meixner (KM) approximation, or any other user‑supplied function).

  2. Compute common diagnostic metrics against a chosen reference model (RMSE, peak‑location bias, 80 % source‑area overlap).

  3. Return a tidy pandas.DataFrame and 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_xr model 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: object

Class 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_plotting.add_plotting_to_footprint(FluxFootprint)[source]

Add plotting methods to FluxFootprint class.

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: object

Build 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 need var if they hold more than one time-resolved 2-D variable.

varstr, optional

Variable name when data is a xarray.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 contextily and station coordinates.

basemap_sourceoptional

A contextily tile provider (defaults to the contextily default).

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 when norm="log" and vmin is 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_below but 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

strftime pattern for the per-frame label; defaults follow freq.

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) -> str returning 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 None when 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/.webm use ffmpeg, .gif uses Pillow, .html writes 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 FootprintAnimator argument, plus writer, frames_dir, and writer options which are forwarded to FootprintAnimator.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 ffmpeg on the system PATH) and otherwise falls back to the binary shipped with the optional imageio-ffmpeg package, pointing matplotlib at it so FFMpegWriter becomes usable.

Returns:
str or None

Path to a working ffmpeg executable, or None if 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 time dimension plus x and y.

freqstr or None, optional

Target time step; see resolve_freq(). None keeps 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 None to keep the native time steps.

Raises:
ValueError

If freq is not a string or None.

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.

  1. Fields. Nine required columns – timestamp (carried as the frame’s DatetimeIndex), site_id, and the eight FFPModel inputs ustar, 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).

  2. Provenance. Every value in a processed table is exactly one of Provenance: observed, network_derived, prep_calculated, fixed_metadata or external_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.

  3. Conventions. wind_dir is meteorological – the direction the wind comes from, degrees clockwise from true north, on [0, 360]. The source area therefore lies at bearing wind_dir from the tower, so wind_dir = 90 puts it to the east. See WIND_DIR_CONVENTION and cardinal_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: ValueError

Raised 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: object

The 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; None is 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_text

Allowed provenance classes, comma-separated in declaration order.

range_text

The 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: object

One validation finding.

Attributes:
severity

"error" for a contract violation, "warning" for a row the contract permits but FFPModel will drop or flag.

code

Stable machine-readable identifier, e.g. "out_of_range".

field

Canonical field the finding concerns, or None when 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, Enum

How 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 method in 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; NEON distObkv).

OBSERVED = 'observed'

Measured and published by the source network.

PREP_CALCULATED = 'prep_calculated'

Computed during preparation, from documented inputs by a documented formula. Requires method in 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: object

The 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_for_status()

Raise CanonicalSchemaError if 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.

codes_for(severity: str) → set[str][source]

The set of codes present at one severity.

property errors: list[Issue]

Findings of severity "error".

fields_present: tuple[str, ...] = ()
fields_with(code: str) → set[str | None][source]

Fields carrying a given code.

issues: list[Issue]
n_rows: int = 0
property ok: bool

True when there are no errors. Warnings do not affect this.

raise_for_status() → ValidationReport[source]

Raise CanonicalSchemaError if there is any error.

Returns:
ValidationReport

self, so calls can be chained.

summary() → str[source]

Multi-line summary: the verdict line, then one line per finding.

to_frame() → DataFrame[source]

Findings as a table, one row per issue.

property warnings: list[Issue]

Findings of severity "warning".

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 b to a, 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.
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.md are 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 tolerance of 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" or None, 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 -> Provenance member or its string value.

Returns:
str

The inline spelling for a variable_provenance column.

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_provenance spelling 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/.pq and .csv. A timestamp column, 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 in processed/ 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 None to skip the provenance checks.

require_tz

Treat a timezone-naive index as an error (the default). Set False only when the timestamp convention is documented in metadata/<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.ok False 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 (a Provenance member or its value) and units; prep_calculated and external_forcing also need method, and observed and network_derived need source_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: object

A 3‑D backward Lagrangian stochastic footprint model.

Methods

crosswind_integrated()

Return upstream distance array and integrated footprint f(x).

footprint()

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).

footprint() → Tuple[ndarray, ndarray, ndarray][source]

Return (x_centres, y_centres, footprint_density).

run() → None[source]

Run the LS footprint simulation and accumulate 2‑D footprint.

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: ABC

Abstract 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

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 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

to_netcdf(filepath: str) → None[source]

Save results to netCDF file.

Parameters:
filepathstr

Output file path

exception fluxfootprints.CanonicalSchemaError(report: ValidationReport)[source]

Bases: ValueError

Raised 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: object

Land-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 int NLCD 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]
level: Level
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: object

Summary 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 levels argument to ax.contour. nan for an already-truncated climatology that no longer carries the level it was cut at, such as one sliced out of the stack that monthly_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. None when a single climatology was supplied.

daynight_overlapfloat or None

O80_daynight, Eq. 3, the mean overlap of paired daytime and nighttime climatologies. None when 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: object

Continuous-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
level: Level | None
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: BaseFootprintModel

Flux 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_paper_smoothing()

Apply 3x3 smoothing from Kljun et al. paper with mass conservation.

apply_rsl_corrections()

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.

calc_crosswind_integrated_extent(r)

Calculate crosswind-integrated footprint extent for relative contribution r.

calc_crosswind_integrated_footprint(x_star)

Calculate the scaled crosswind-integrated footprint F̂y*(X̂*).

calc_crosswind_spread(x)

Calculate the standard deviation of cross-wind spread \(\sigma_y\).

calc_peak_based_limits(r)

Calculate upwind and downwind distances from the peak location for fraction r.

calc_pi_4()

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.

calc_scaled_distance_rsl()

Calculate scaled distance X* with roughness sublayer correction.

calc_scaled_footprint_peak()

Calculate the scaled footprint peak location (X*max).

calc_scaled_x(x)

Calculate the scaled distance X* based on wind and height parameters.

calc_unstable_psi(stability_param)

Calculate ψM for unstable atmospheric conditions.

calc_xr_footprint()

Compute the flux footprint using xarray operations.

calculate_pi_groups()

Calculate dimensionless Π groups for analysis.

calculate_source_areas()

Compute source area contour datasets for all specified relative contributions, derived directly from the fclim_2d climatology grid.

check_rsl_validity()

Check if the measurement height is above the roughness sublayer.

check_validity_ranges()

Check validity ranges according to equation 27 and other constraints from Kljun et al. (2015).

create_xr_dataset()

Create xarray Dataset from input DataFrame.

define_domain()

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.

get_source_area_contour(r)

Compute the source area contour threshold for fraction r of the climatological footprint, operating directly on the pre-computed fclim_2d grid.

handle_stability_regimes()

Classify stability regimes for diagnostics.

initialize_model_parameters()

Initialize core model parameters (a, b, c, d, ac, bc, cc).

normalize_footprint()

Ensure footprint integrates to 1.0 after calculation.

plot_footprint([config, ax, show_contours, ...])

Plot the footprint climatology with optional contours.

prep_df_fields()

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_const depends 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.ds are used, and an xr.DataArray is 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_x converts 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_4 with Pi_4 = ln(zm/z0) - psi_M = u(zm)/(u* k) (Eqs. 6/7). This is the same scaled<->real conversion used in calc_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() or calc_xr_footprint().

Returns:
dict

Mapping of contribution label (e.g. 'r_80') to the xarray.Dataset returned by get_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

  1. Stability validity (Eq. 27): -15.5 ≤ zm/L

  2. Turbulence validity: u* > 0.1 m/s, σv > 0

  3. 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).
create_xr_dataset()[source]

Create xarray Dataset from input DataFrame.

define_domain()[source]

Define the spatial computational domain and polar coordinate grids.

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 r

of the total flux (use this as the levels argument to ax.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_contour

Mask 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.

normalize_footprint()[source]

Ensure footprint integrates to 1.0 after calculation.

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

verify_data(data: DataArray, name: str) → bool[source]

Verify the content and structure of a DataArray.

Parameters:
dataxarray.DataArray

Data array to check.

namestr

Variable name for logging.

Returns:
bool

True if valid, False otherwise.

verify_results(data, name='data')[source]

Check for NaN or infinite values in the given data.

Parameters:
dataarray-like or xarray.DataArray

The data to verify.

namestr, optional

Descriptive name for logging.

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: object

The 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; None is 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_text

Allowed provenance classes, comma-separated in declaration order.

range_text

The 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: object

Build 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 need var if they hold more than one time-resolved 2-D variable.

varstr, optional

Variable name when data is a xarray.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 contextily and station coordinates.

basemap_sourceoptional

A contextily tile provider (defaults to the contextily default).

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 when norm="log" and vmin is 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_below but 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

strftime pattern for the per-frame label; defaults follow freq.

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) -> str returning 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 None when 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/.webm use ffmpeg, .gif uses Pillow, .html writes 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: object

Georeferencing 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: object

One validation finding.

Attributes:
severity

"error" for a contract violation, "warning" for a row the contract permits but FFPModel will drop or flag.

code

Stable machine-readable identifier, e.g. "out_of_range".

field

Canonical field the finding concerns, or None when 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: BaseFootprintModel

Kormann & Meixner (2001) analytical footprint model adapter.

Wraps the analytical functions in fluxfootprints.kormannmeixner in the standard BaseFootprintModel interface. Intended to be driven through fluxfootprints.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']
run(return_result: bool = True) → Dataset | None[source]

Execute Kormann-Meixner footprint calculation.

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: object

Container 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
rng()[source]
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: BaseFootprintModel

Lagrangian Stochastic footprint model adapter.

Wraps BackwardLSModel in the standard BaseFootprintModel interface. Intended to be driven through fluxfootprints.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']
run(return_result: bool = True) → Dataset | None[source]

Execute Lagrangian footprint calculation.

class fluxfootprints.LandsatBands(collection: str, blue: str, red: str, nir: str, qa: str, label: str)[source]

Bases: object

Band 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_PIXEL throughout 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, Enum

Three-level footprint-to-target-area representativeness index.

The members are str subclasses, so Level.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: object

Result 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, Enum

How 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 method in 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; NEON distObkv).

OBSERVED = 'observed'

Measured and published by the source network.

PREP_CALCULATED = 'prep_calculated'

Computed during preparation, from documented inputs by a documented formula. Requires method in 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: object

A 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’s lmodel2 reports alongside the point estimates of Table 1.

Attributes:
slopefloat

beta_1, the RMA slope. nan when 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_regression

Produces this fit.

classify_continuous

Turns 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: object

The 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_for_status()

Raise CanonicalSchemaError if 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.

codes_for(severity: str) → set[str][source]

The set of codes present at one severity.

property errors: list[Issue]

Findings of severity "error".

fields_present: tuple[str, ...] = ()
fields_with(code: str) → set[str | None][source]

Fields carrying a given code.

issues: list[Issue]
n_rows: int = 0
property ok: bool

True when there are no errors. Warnings do not affect this.

raise_for_status() → ValidationReport[source]

Raise CanonicalSchemaError if there is any error.

Returns:
ValidationReport

self, so calls can be chained.

summary() → str[source]

Multi-line summary: the verdict line, then one line per finding.

to_frame() → DataFrame[source]

Findings as a table, one row per issue.

property warnings: list[Issue]

Findings of severity "warning".

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: BaseFootprintModel

Wang et al. (2006) convective footprint model adapter.

Wraps wang2006_fy() and reconstruct_gaussian_2d() in the standard BaseFootprintModel interface. Intended to be driven through fluxfootprints.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']
run(return_result: bool = True) → Dataset | None[source]

Execute Wang footprint calculation.

class fluxfootprints.WeightedValue(value: float, retained_weight: float, n_cells: int)[source]

Bases: object

A field value averaged over a footprint or a target area (Sect. 2.4).

Returned by footprint_weighted_value() and target_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. nan when 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**m and K(z) = kappa z**n.

fluxfootprints.angular_difference(a: Any, b: Any) → Any[source]

Signed smallest angle from b to a, 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.
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 FootprintAnimator argument, plus writer, frames_dir, and writer options which are forwarded to FootprintAnimator.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.DataArray

mask_footprint_dataarray()

SummaryResult

mask_summaries()

path to a directory or GeoTIFF

mask_rasters_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 the site index 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.Series of rasters indexed by retrieval time;

  • a DataArray with 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 variable index 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 as within_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) – carrying RESULT_COLUMNS. Rows are of three scopes, told apart by the scope column, and of three kinds, told apart by kind:

scope="period", kind="climatology"

One row per aggregated month and period, carrying fetch, area, symmetry, contour_level, n_cells, and n_times. radius is missing and variable is "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). month is 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. year and month are 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 scope and kind columns before reading values off it. DataFrame.attrs records the settings the run used, including unmatched_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 rioxarray and it is not installed.

See also

monthly_climatologies

The aggregation of Sect. 2.2 this drives.

categorical_representativeness

The per-climatology land-cover step.

sensor_location_bias

The per-period continuous step, Eq. 6.

continuous_representativeness

The site-level regression, Eq. 7.

export_representativeness_tables

Writes 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 time column, so prefer .query or boolean masks over .loc on 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_threshold is 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. Since Level is a str enum, results["level"] == Level.HIGH and == "high" both select the same rows; only is Level.HIGH does 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.md are 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 tolerance of 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" or None, 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 by classify_categorical().

Parameters:
wxarray.DataArray or numpy.ndarray

Footprint source weights with dims (x, y), normally a truncated, renormalised climatology from truncate_to_contour() or a slice of monthly_climatologies(). Weights summing to something other than 1 are renormalised, as in footprint_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() with categorical=True to 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 >= alpha count as not significantly different.

Returns:
pandas.DataFrame

One row per radius, with columns

radius

Target-area radius [m].

dominant_class

Class code holding the largest footprint-weighted share. Constant down the frame, since the footprint does not depend on the radius.

p_footprint

That 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_target

The same class’s share of the disc, P_target, likewise a fraction. 0.0 when the disc holds none of it.

chi2

Chi-square statistic comparing the two full compositions.

p_value

p-value of that test.

dof

Degrees of freedom, one less than the number of classes tested.

level

Level member: HIGH when both shares reach 0.80 and the compositions do not differ significantly, MEDIUM when both reach 0.50 and they do not differ significantly, LOW otherwise. Being a str enum it compares equal to "high" and serialises as it.

DataFrame.attrs holds alpha and footprint_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_composition

P_footprint over the whole composition.

target_area_composition

P_target over the whole composition.

classify_categorical

The three-level index applied here.

sensor_location_bias

The 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_target can 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_footprint and p_target are reported here as fractions, while classify_categorical() and evaluate_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 >= alpha are 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

HIGH when both percentages are >= high_percent and the compositions do not differ significantly; MEDIUM when both are >= medium_percent and the compositions do not differ significantly; LOW otherwise.

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

HIGH when r_squared >= high_r_squared with the slope within slope_tolerance of 1.0 and the intercept within intercept_tolerance of 0.0; MEDIUM when r_squared >= medium_r_squared and p_value < alpha; LOW otherwise.

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 by truncate_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() and daynight_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_fraction attribute that truncate_to_contour() writes, and its fraction and contour level are read back from there. enclosed_fraction is then nan: 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_level is nan too where a truncated input has lost that attribute, as a climatology sliced out of the stack from monthly_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 from ds.contour_level alongside.

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.concat_fetch_gdf(data, epsg=5070)[source]
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 follows sensor_location_bias_series(): the matched periods of one site are regressed against each target radius by Eq. 7 and classified HIGH, MEDIUM, or LOW by classify_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

time

Period label, carried only to identify the matches; the fit does not use it and does not sort by it.

radius

Target-area radius [m].

value_footprint

Footprint-weighted value, EVI_footprint of Eq. 5.

value_target

Target-area mean over the disc of that radius, EVI_target.

Exactly the frame sensor_location_bias_series() returns; any further columns, delta and within_threshold among 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 = False and 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

radius

Target-area radius [m].

n

Matched periods entering the row: rows of pairs at that radius holding a finite value on both sides.

intercept, slope

beta_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_value

Squared Pearson correlation and its two-sided significance.

rmse, mae

Root 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_upper

Confidence limits at ci_level, the square-bracketed columns of Table 1.

sufficient

Whether n >= min_matches, i.e. whether the row was fitted.

level

Level member, or pd.NA where sufficient is False. HIGH when r_squared >= 0.8 with slope in [0.9, 1.1] and intercept in [-0.1, 0.1]; MEDIUM when r_squared >= 0.6 and p_value < alpha; LOW otherwise.

DataFrame.attrs records min_matches, alpha, ci_level, and ci_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_series

Produces the frame this consumes.

rma_regression

The model II fit applied per radius.

classify_continuous

The three-level index applied here.

categorical_representativeness

The land-cover counterpart.

Notes

A radius listed in radii but absent from pairs comes back with n = 0 and sufficient = False rather 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 sufficient rather than on level.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 * dy until fraction is reached, following the same construction as get_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/y coordinates when omitted.

fractionfloat, default 0.8

Source-weight fraction to enclose, in (0, 1).

Returns:
float

Footprint density [m⁻²] at the contour. Pass as levels to matplotlib.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

overlap

The per-month kernel this averages.

daynight_overlap_index

The same index from sequences of climatologies.

seasonal_overlap

The 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 x or y coordinate 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_representativeness

The per-radius comparison this wraps.

assess_representativeness

The whole-record driver, which reports the same comparison as tidy rows for every month and period.

evaluate_vegetation_index

The continuous-field counterpart.

representativeness_summary

Flattens 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 of monthly_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 as ContinuousResult.within_threshold.

Returns:
list of ContinuousResult

One entry per radius, in the order given. bias holds the biases of the periods that entered that radius’s regression, and level is 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 x or y coordinate; or if a climatology and its field are not on one grid.

See also

sensor_location_bias

Eq. 6, computed once per pair here.

continuous_representativeness

The per-radius regression this wraps.

assess_representativeness

The whole-record driver, which matches scenes to months itself and reports the per-period biases alongside the fit.

evaluate_landcover

The categorical counterpart.

representativeness_summary

Flattens 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_threshold averages 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 r of the normalised footprint is traced as a polygon. The polygon is shifted from tower-relative metres into crs_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 x and y attributes 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_weighted and f_monthly_et_weighted are read as xr.DataArray objects with a time dimension. None layers 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_lon and centroid_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. If crs_out names a different CRS, each slice is then reprojected onto that CRS with bilinear resampling.

Parameters:
modelFFPModel

A footprint model that has been run. Its x and y attributes 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_weighted and f_monthly_et_weighted are read as xr.DataArray objects with dims ("time", "x", "y"). Missing or None attributes 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 by pyproj.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.x or model.y has fewer than two points, or if which contains an unknown layer name.

Notes

Files are named {prefix}_{layer}_{YYYYMMDD}.tif for daily layers and {prefix}_{layer}_{YYYYMM}.tif for 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 NotImplementedError on every call. The parameters and return value below describe the intended interface, not current behaviour.

Parameters:
resultspandas.DataFrame

Tidy results carrying a radius column, from representativeness_summary() or from assess_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 geopandas is not installed.

ValueError

If results carries no radius column.

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; the SITE_ID column 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 pyarrow or fastparquet, 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() or to_parquet(). index=False is 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_table

Builds one table without writing it.

assess_representativeness

Produces the frame this writes.

export_representativeness_gpkg

Writes 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.filterDate reads 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 with nan where the pixel was cloudy or off-swath, named "evi" and georeferenced for .rio. Each carries a scalar time coordinate, and its scene id, sensor, and obscured fraction in .attrs. Empty if no scene clears max_cloud.

Raises:
ImportError

If earthengine-api or rioxarray is 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_nlcd

The categorical field of the same analysis.

fluxfootprints.evaluate_vegetation_index

Consumes these, once warped onto the footprint grid and paired with matching climatologies.

fluxfootprints.sensor_location_bias

Eq. 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 the time coordinate on each array supports directly.

Each surviving scene is one computePixels request, 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_origin and y_origin of a GridGeometry.

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 with nan where NLCD is unclassified or absent, named "nlcd" and georeferenced for .rio. Ready for _align_raster(..., categorical=True), which is what sample_raster_on_grid() and evaluate_landcover() build on.

Raises:
ImportError

If earthengine-api or rioxarray is 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_evi

The continuous field of the same analysis.

fluxfootprints.evaluate_landcover

Consumes this, once warped onto the footprint grid.

fluxfootprints.sample_raster_on_grid

Does that warp for a raster on disc.

Notes

Class codes must be resampled with nearest neighbour, so pass categorical=True when 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-nan rather 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: BaseFootprintModel

Create a footprint‑climatology from a series of individual flux‑footprint estimates using the simple 2‑D parameterisation of Kljun et al. (2015).

The routine

  1. computes a footprint for every time step,

  2. rotates each footprint into its observed wind direction,

  3. aggregates all footprints onto a common grid, and

  4. 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.

prep_df_fields()

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()
calc_xr_footprint()[source]
create_xr_dataset()[source]
define_domain()[source]
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 in calc_xr_footprint(), not here.

raise_ffp_exception(code)[source]
run(return_result: bool = True)[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

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() and analytical_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 by truncate_to_contour() is equally accepted; see footprint_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.0 if 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 takes x/y as 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. via scipy.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 <= 0 are upwind of the sensor, where the model is undefined, and are assigned zero density.

xi, m, n

Parameters returned by length_scale_xi() and analytical_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_fraction

The threshold this mask is built from.

truncate_to_contour

Mask 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 by footprint_contour_mask(). A climatology truncated by truncate_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]. nan if no cell is inside.

Raises:
ValueError

If mask carries no x or y coordinate 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 from footprint_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. nan if 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 for footprint_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.attrs holds retained_weight and n_cells, as on WeightedValue.

Raises:
ValueError

As for footprint_weighted_value().

See also

target_area_composition

The 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 from truncate_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. value is nan and retained_weight 0 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_value

The unweighted counterpart over a target disc.

sensor_location_bias

Compares the two, Eq. 6.

footprint_weighted_composition

The 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_weight is 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 -> Provenance member or its string value.

Returns:
str

The inline spelling for a variable_provenance column.

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.date keys 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_colors

The 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, x and y dims, where x/y are metres from the tower – e.g. model.f_2d or any field of SummaryResult.

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.nan to 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), and retained_fraction (share of footprint weight surviving each step), plus missing_dates for steps with no OpenET coverage.

Notes

retained_fraction is 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 by summarize_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.tif uses that day, ..._202402.tif combines every OpenET day in February 2024 – so this works directly on the output of export_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. When renormalize=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 a pandas.DataFrame with 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_dir is 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, and scale_factor.

Notes

retained_fraction is 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 (and openet_retained_frac_etw for 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 lmodel2 does.

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 x and y coordinates 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_times recorded, 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), carrying

footprint_climatology

Truncated, 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_times

Timesteps that contributed positive weight to each group.

contour_level

Source-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 as nan for a slice; read it from here alongside.

contour_n_cells

Cells retained inside the contour.

The month coordinate is the first instant of each calendar month present, and period is ["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 time coordinate or their dims are not (time, x, y); or if the day-night split cannot be resolved.

See also

truncate_to_contour

The per-group truncation and renormalisation.

seasonal_overlap

Eq. 2, taken over this Dataset’s month dimension.

daynight_overlap

Eq. 3, over its two period slices.

climatology_metrics

Fetch, 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 month dimension to seasonal_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_overlap

Eq. 2, over the K months of a site-year.

daynight_overlap

Eq. 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() and daynight_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_DD and YYYY.MM.DD anywhere 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). None if nothing in the name parses as a date.

fluxfootprints.parse_variable_provenance(text: str) → dict[str, str][source]

Parse the inline variable_provenance spelling 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 a tzinfo. 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_radiation

The radiation the threshold is applied to.

monthly_climatologies

Uses 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() (column delta) or the tidy frame of assess_representativeness() (column bias), whose radius index level is reset here. Subset the latter to one variable and period first.

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_kde as bw_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 – pass BIAS_THRESHOLD for 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_series

Produces data.

fluxfootprints.BIAS_THRESHOLD

The +/-10 % the paper judges against.

plot_footprint_target_scatter

The 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 of assess_representativeness(), whose radius index level is reset here. Subset the latter to one variable and period first – 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_regression

The model II fit drawn here.

fluxfootprints.evaluate_vegetation_index

Reduces the same pairs to a three-level index.

plot_bias_density

The 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_landcover

Produces results.

plot_level_bars

The same evaluation reduced to its three-level index and pooled across sites.

Notes

Shares are the percentages CategoricalResult carries, 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 of assess_representativeness(), whose radius index 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_summary

Produces one site’s rows.

fluxfootprints.Level

The index being counted.

plot_landcover_composition

The 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_MATCHES matched scenes has no regression to classify. With percent=True each 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 column c two 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 a tzinfo. 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_daynight

The 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 for TIMESTAMP_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 NotImplementedError on 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 sigmav column [m s⁻¹].

estimatorobject, optional

A fitted scikit-learn regressor to apply. When omitted, a RandomForestRegressor is fitted on the rows of df that have an observed sigmav.

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-learn is not installed and no estimator is supplied.

ValueError

If required predictor columns are missing, or no rows carry an observed sigmav to 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_colors

The 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/.pq and .csv. A timestamp column, 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‑D f_y(x).

Assumes the lateral distribution at each stream‑wise distance x is 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⁻¹) and U (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_id column, 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, carrying SUMMARY_COLUMNS in that order:

site_id, radius

Row 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_level

The 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_level

The 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_fraction through daynight_overlap

The 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_landcover

Produces the land-cover half.

evaluate_vegetation_index

Produces the continuous half.

assess_representativeness

The whole-record driver, whose tidy frame carries the same statistics per month and period rather than pooled.

export_representativeness_gpkg

Draws this table as nested discs.

Notes

The two *_level columns hold a Level’s value – "high", "medium", or "low" – rather than the member, because str() 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 of CHU_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_representativeness

Produces the frame this reshapes.

export_representativeness_tables

Writes 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: PRODUCT and VARIABLE, naming the raster each row came from, and TIMESTAMP, 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 time dimension plus x and y.

freqstr or None, optional

Target time step; see resolve_freq(). None keeps 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 None to keep the native time steps.

Raises:
ValueError

If freq is not a string or None.

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’s lmodel2 – 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) that lmodel2 reports; '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_regression

The four-value form of this fit.

continuous_representativeness

Applies it per target radius.

classify_continuous

Turns 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 r times 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 the 0.9 <= slope <= 1.1 HIGH criterion of Sect. 2.4 for no reason but the scatter. Table 1 is reproducible only against lmodel2’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 lmodel2 reports.

Pairs where either value is non-finite are dropped before fitting. The slope is returned as nan when either variable is constant, or when the correlation is exactly zero and its sign cannot be taken – lmodel2 likewise 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, matching export_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 rioxarray is not installed.

ValueError

If the raster has no CRS, so it cannot be reprojected.

See also

fluxfootprints.footprint_grid_geometry

Builds the target GridGeometry.

fluxfootprints.openet_mask_on_grid

The same reprojection for data-availability masks.

_align_raster

The 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 with target_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

overlap

The two-footprint kernel, which Eq. 2 reduces to at K = 2.

seasonal_overlap_index

The same index from a sequence of climatologies.

daynight_overlap

The 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 from truncate_to_contour() or a slice of monthly_climatologies(). Weights summing to something other than 1 are renormalised, as in footprint_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

radius

Target-area radius [m].

value_footprint

Footprint-weighted value, Eq. 5. Constant down the frame.

value_target

Target-area mean over the disc of that radius.

delta

Sensor 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. nan where either value is non-finite or the target-area mean is zero.

within_threshold

Whether |delta| <= BIAS_THRESHOLD, in pandas’ nullable boolean dtype, and pd.NA where 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_series

Maps this over a time-indexed collection.

footprint_weighted_value

EVI_footprint, Eq. 5.

target_area_value

EVI_target.

model2_regression

The 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() and target_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 by radius and averaging within_threshold reproduces 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.Series indexed 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 leading time column, over a fresh RangeIndex: columns time, radius, value_footprint, value_target, delta, and within_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_bias

The single-period computation this maps.

monthly_climatologies

Produces the per-period climatologies.

evaluate_vegetation_index

Regresses 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_2d is populated and model.get_footprint_timeseries() returns an xr.DataArray with dims ("time", "x", "y") holding footprint density (m^-2). If the model exposes dx and dy they are used as the grid cell size; otherwise cell size is inferred from the x/y coordinate spacing.

dfpd.DataFrame

Time-indexed input data. Only used to supply the ET/LE column; it is reindexed onto the footprint time coordinate using exact matches, so timestamps absent from df get NaN weights.

et_sourcestr or None, default “LE”

Name of the column in df used for ET weighting. If None or not present in df, ET-weighted outputs are silently skipped.

is_lebool, default True

If True, et_source is latent energy (W m^-2) and is converted to ET (mm hr^-1) by dividing by 680.6. Set False when et_source already 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, monthly and ET flags; unrequested outputs remain None.

  • f_daily_mean / f_monthly_meanxr.DataArray

    Unweighted mean of valid footprint slices per period.

  • f_daily_et_weighted / f_monthly_et_weightedxr.DataArray

    sum(f * ET) / sum(ET) over valid slices per period. NaN for periods whose ET sum is <= 0.

  • daily_domain_coverage / monthly_domain_coveragepd.DataFrame

    Indexed by period, with columns mean_coverage_pct, et_weighted_coverage_pct (only when ET is used), min_coverage_pct, max_coverage_pct and valid_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_2d or 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_source beforehand 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. nan if fetch is zero or non-finite, or if area is non-finite.

See also

footprint_symmetry

The 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.attrs holds retained_weight, the fraction of the disc’s cells that held one, and n_cells.

Raises:
ValueError

As for target_area_value().

See also

footprint_weighted_composition

The 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, as sample_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. value is nan and retained_weight 0 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_value

The footprint-weighted counterpart, Eq. 5.

target_area_mask

The 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 lowers retained_weight rather 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) = 1 convention. 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 in processed/ 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 None to skip the provenance checks.

require_tz

Treat a timezone-naive index as an error (the default). Set False only when the timestamp convention is documented in metadata/<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.ok False 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 (a Provenance member or its value) and units; prep_calculated and external_forcing also need method, and observed and network_derived need source_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.