Source code for fluxfootprints.base_footprint_model
# src/fluxfootprints/base_footprint_model.py
"""
base_footprint_model.py
========================
Base class defining standard interface for all footprint models.
"""
from abc import ABC, abstractmethod
from typing import Optional, Dict, Any, Tuple
import numpy as np
import pandas as pd
import xarray as xr
import logging
[docs]
class BaseFootprintModel(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
----------
df : pandas.DataFrame
Input meteorological data
domain : list
Spatial domain [xmin, xmax, ymin, ymax]
dx, dy : float
Grid resolution in x and y directions
rs : list
Source area fractions to compute
logger : logging.Logger
Logger instance
x, y : numpy.ndarray
Grid coordinate arrays
f_2d : xarray.DataArray or None
Time-resolved 2D footprint (time, x, y)
fclim_2d : xarray.DataArray or None
Climatological 2D footprint (x, y)
"""
REQUIRED_COLUMNS = [] # Override in subclass
def __init__(
self,
df: pd.DataFrame,
domain: list = [-1000.0, 1000.0, -1000.0, 1000.0],
dx: float = 10.0,
dy: float = 10.0,
rs: list = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8],
crop_height: float = 0.2,
atm_bound_height: float = 2000.0,
inst_height: float = 2.0,
smooth_data: bool = True,
verbosity: int = 2,
logger: Optional[logging.Logger] = None,
**kwargs
):
"""
Initialize base footprint model.
Parameters
----------
df : pandas.DataFrame
Input data with required meteorological columns
domain : list
[xmin, xmax, ymin, ymax] in meters
dx, dy : float
Grid spacing in meters
rs : list
Source area fractions (0-1) to compute
crop_height : float
Vegetation/canopy height (m)
atm_bound_height : float
Atmospheric boundary layer height (m)
inst_height : float
Instrument measurement height (m)
smooth_data : bool
Apply smoothing to output
verbosity : int
Logging level (0=silent, 2=debug)
logger : logging.Logger
Custom logger instance
"""
self.df = df.copy()
self.domain = domain
self.dx = float(dx)
self.dy = float(dy)
self.rs = rs
self.crop_height = crop_height
self.atm_bound_height = atm_bound_height
self.inst_height = inst_height
self.smooth_data = smooth_data
self.verbosity = int(verbosity)
# Initialize logger
self.logger = logger or self._setup_logger()
# Initialize coordinate arrays
self.x = None
self.y = None
# Initialize output arrays
self.f_2d = None
self.fclim_2d = None
self.results = None
def _setup_logger(self) -> logging.Logger:
"""Set up default logger."""
logger = logging.getLogger(self.__class__.__name__)
if not logger.handlers:
handler = logging.StreamHandler()
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG if self.verbosity > 1 else logging.WARNING)
return logger
@abstractmethod
def _validate_input_df(self, df: pd.DataFrame) -> pd.DataFrame:
"""
Validate and prepare input DataFrame.
Must check for required columns and handle missing values.
Should return cleaned DataFrame.
"""
pass
[docs]
@abstractmethod
def run(self, return_result: bool = True) -> Optional[xr.Dataset]:
"""
Execute footprint calculation.
Parameters
----------
return_result : bool
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
"""
pass
[docs]
def get_footprint_climatology(self) -> xr.DataArray:
"""
Return the climatological footprint as xarray DataArray.
Returns
-------
xarray.DataArray
2D footprint climatology with dims (x, y)
"""
if self.fclim_2d is None:
raise RuntimeError("Model has not been run. Call run() first.")
return self.fclim_2d
[docs]
def get_footprint_timeseries(self) -> Optional[xr.DataArray]:
"""
Return time-resolved footprint if available.
Returns
-------
xarray.DataArray or None
3D footprint with dims (time, x, y) if available
"""
return self.f_2d
[docs]
def get_coordinates(self) -> Tuple[np.ndarray, np.ndarray]:
"""
Return x and y coordinate arrays.
Returns
-------
x, y : numpy.ndarray
1D coordinate arrays in meters
"""
if self.x is None or self.y is None:
raise RuntimeError("Coordinates not initialized. Call run() first.")
return self.x, self.y
[docs]
def get_results(self) -> xr.Dataset:
"""
Return complete results as xarray Dataset.
Returns
-------
xarray.Dataset
Dataset containing all footprint outputs and metadata
"""
if self.results is None:
raise RuntimeError("No results available. Call run() first.")
return self.results
[docs]
def to_netcdf(self, filepath: str) -> None:
"""
Save results to netCDF file.
Parameters
----------
filepath : str
Output file path
"""
results = self.get_results()
results.to_netcdf(filepath)
self.logger.info(f"Results saved to {filepath}")