Skip to content

Core Module

core

Core module for effective precipitation calculations.

This module provides the main :class:EffectivePrecipitation class for calculating effective precipitation from climate datasets available on Google Earth Engine (GEE) or from precipitation rasters/NetCDF files that already exist on disk.

Two precipitation sources are supported:

  • Google Earth Engine (default) - pass asset_id and precip_band.
  • Local files - pass local_precip pointing at a directory of monthly rasters, a NetCDF file, or a glob string. See :mod:pycropwat.local.

With local precipitation, available water capacity (AWC), reference evapotranspiration (ETo) and GEE FeatureCollection geometries are still downloaded from Earth Engine, so the usda_scs, ensemble and suet methods continue to require GEE access. The precipitation-only methods (cropwat, fao_aglw, fixed_percentage, dependable_rainfall, farmwest) need no Earth Engine at all - Earth Engine is never initialised for them, so those runs work completely offline. The pcml method is a pre-computed GEE product and is therefore incompatible with local precipitation.

The module supports multiple effective precipitation methods:

  • Ensemble: Mean of 6 methods (default)
  • CROPWAT: FAO CROPWAT method
  • FAO/AGLW: FAO Dependable Rainfall (80% exceedance)
  • Fixed Percentage: Simple fixed percentage method
  • Dependable Rainfall: FAO Dependable Rainfall method
  • FarmWest: FarmWest method
  • USDA-SCS: Soil moisture depletion method (requires AWC and ETo)
  • TAGEM-SuET: Turkish irrigation method (requires ETo)
  • PCML: Physics-Constrained ML for Western U.S. (pre-computed GEE asset)
Example
from pycropwat import EffectivePrecipitation
ep = EffectivePrecipitation(
    asset_id='ECMWF/ERA5_LAND/MONTHLY_AGGR',
    precip_band='total_precipitation_sum',
    geometry_path='study_area.geojson',
    start_year=2015,
    end_year=2020,
    precip_scale_factor=1000,
    method='ensemble'
)
results = ep.process(output_dir='./output', n_workers=4)

Local precipitation input (no Earth Engine required):

from pycropwat import EffectivePrecipitation
ep = EffectivePrecipitation(
    local_precip='../pyCropWat_Data/Precip',
    local_precip_pattern='Precip_*.tif',
    local_precip_nodata=-9999,
    method='cropwat',
    start_year=2005,
    end_year=2005
)
results = ep.process_sequential(output_dir='./output', months=[7, 8])
See Also

pycropwat.local : Readers for local precipitation rasters and NetCDF files. pycropwat.methods : Individual effective precipitation calculation functions. pycropwat.analysis : Post-processing and analysis tools. pycropwat.utils : Utility functions for GEE and file operations.

EffectivePrecipitation

EffectivePrecipitation(asset_id: Optional[str] = None, precip_band: Optional[str] = None, geometry_path: Optional[Union[str, Path]] = None, start_year: int = None, end_year: int = None, scale: Optional[float] = None, precip_scale_factor: float = 1.0, gee_project: Optional[str] = None, gee_geometry_asset: Optional[str] = None, method: PeffMethod = 'ensemble', method_params: Optional[dict] = None, local_precip: Optional[Union[str, Path]] = None, local_precip_pattern: str = '*.tif', local_precip_variable: Optional[str] = None, local_precip_nodata: Optional[float] = None, local_precip_crs: Optional[str] = None, local_precip_date_regex: Optional[str] = None, clip_to_geometry: bool = True)

Calculate effective precipitation from GEE or local climate data.

Supports multiple effective precipitation calculation methods including CROPWAT, FAO/AGLW, Fixed Percentage, Dependable Rainfall, FarmWest, and USDA-SCS (which requires AWC and ETo data).

Precipitation is downloaded from Google Earth Engine by default. Pass local_precip to read monthly precipitation from GeoTIFFs or NetCDF files on disk instead. AWC, ETo and GEE FeatureCollection geometries are still read from Earth Engine in that case, so usda_scs, ensemble and suet still need GEE credentials. Precipitation-only methods (cropwat, fao_aglw, fixed_percentage, dependable_rainfall, farmwest) combined with local files need no Earth Engine at all - Earth Engine is never initialised and the run works offline.

Parameters:

Name Type Description Default
asset_id str

GEE ImageCollection asset ID for precipitation data. Required unless local_precip is given, or method='pcml' (which supplies its own asset). Common options:

  • ECMWF/ERA5_LAND/MONTHLY_AGGR (ERA5-Land, global, ~11km),
  • IDAHO_EPSCOR/TERRACLIMATE (TerraClimate, global, ~4km),
  • IDAHO_EPSCOR/GRIDMET (GridMET, CONUS, ~4km),
  • OREGONSTATE/PRISM/AN81m (PRISM, CONUS, ~4km),
  • UCSB-CHG/CHIRPS/DAILY (CHIRPS, 50°S-50°N, ~5km),
  • NASA/GPM_L3/IMERG_MONTHLY_V06 (GPM IMERG, global, ~11km).
None
precip_band str

Name of the precipitation band in the asset. Required unless local_precip is given, or method='pcml'. Examples:

  • ERA5-Land: total_precipitation_sum
  • TerraClimate: pr
  • GridMET: pr
  • PRISM: ppt
  • CHIRPS: precipitation
  • GPM IMERG: precipitation
None
geometry_path str, Path, or None

Path to shapefile or GeoJSON file defining the region of interest. Can also be a GEE FeatureCollection asset ID. Set to None if using gee_geometry_asset instead. With local_precip the geometry is optional (the extent of the local files is used when it is omitted) and a local vector file is additionally used to clip the local rasters, see clip_to_geometry.

None
start_year int

Start year for processing (inclusive). Optional with local_precip; inferred from the available files when omitted.

None
end_year int

End year for processing (inclusive). Optional with local_precip; inferred from the available files when omitted.

None
scale float

Output resolution in meters. If None (default), uses native resolution of the dataset.

None
precip_scale_factor float

Factor to convert precipitation to mm. Default is 1.0. Common values: ERA5-Land (m to mm) = 1000, TerraClimate = 1.0, GridMET = 1.0. Also applied to local precipitation files.

1.0
gee_project str

GEE project ID for authentication. Required for cloud-based GEE access.

None
gee_geometry_asset str

GEE FeatureCollection asset ID for the region of interest. Takes precedence over geometry_path if both are provided.

None
method str

Effective precipitation calculation method. Default is 'ensemble'. Options:

  • 'ensemble' - Mean of 6 methods (default, requires AWC and ETo)
  • 'cropwat' - CROPWAT method (FAO standard)
  • 'fao_aglw' - FAO Dependable Rainfall (80% exceedance)
  • 'fixed_percentage' - Simple fixed percentage method
  • 'dependable_rainfall' - FAO Dependable Rainfall method
  • 'farmwest' - FarmWest method
  • 'usda_scs' - USDA-SCS soil moisture depletion method (requires AWC and ETo data via method_params)
  • 'suet' - TAGEM-SuET method (Turkish Irrigation Management System) (requires ETo data via method_params)
  • 'pcml' - Physics-Constrained ML (Western U.S. only, Jan 2000 - Sep 2024) Uses default GEE asset: projects/ee-peff-westus-unmasked/assets/effective_precip_monthly_unmasked
'ensemble'
method_params dict

Additional parameters for the selected method:

For 'fixed_percentage': - percentage (float): Fraction 0-1. Default 0.7.

For 'dependable_rainfall': - probability (float): Probability level 0.5-0.9. Default 0.75.

For 'usda_scs': - awc_asset (str): GEE Image asset ID for AWC data. Required. U.S.: projects/openet/soil/ssurgo_AWC_WTA_0to152cm_composite Global: projects/sat-io/open-datasets/FAO/HWSD_V2_SMU - awc_band (str): Band name for AWC. Default 'AWC'. - eto_asset (str): GEE ImageCollection asset ID for ETo. Required. U.S.: projects/openet/assets/reference_et/conus/gridmet/monthly/v1 Global: projects/climate-engine-pro/assets/ce-ag-era5-v2/daily - eto_band (str): Band name for ETo. Default 'eto'. U.S. (GridMET): 'eto', Global (AgERA5): 'ReferenceET_PenmanMonteith_FAO56' - eto_is_daily (bool): Whether ETo is daily. Default False. Set True for AgERA5 daily data. - awc_scale_factor (float): Scale factor for AWC. Default 1.0. FAO HWSD AWC is in mm/m; set to 0.001 to convert to volumetric fraction (required for correct USDA-SCS calculation). SSURGO AWC is already in inches/inch (volumetric fraction), so use 1.0 (default). - eto_scale_factor (float): Scale factor for ETo. Default 1.0. - rooting_depth (float): Rooting depth in meters. Default 1.0. - mad_factor (float): Management Allowed Depletion factor (0-1). Controls what fraction of soil water storage is available. Default 0.5.

None
local_precip str or Path

Local precipitation data to use instead of downloading precipitation from GEE. Accepts:

  • a directory of monthly rasters (globbed with local_precip_pattern),
  • a single NetCDF file, or a directory of NetCDF files,
  • a glob string such as './Precip/Precip_*.tif'.

Files must contain monthly totals (converted to mm with precip_scale_factor if needed) and must be datable, either from the file name (Precip_2005_07.tif) or from a NetCDF time coordinate. AWC, ETo and GEE FeatureCollection geometries are still read from Earth Engine; precipitation-only methods need no Earth Engine at all. Not compatible with method='pcml', which is a pre-computed GEE product.

None
local_precip_pattern str

Glob used when local_precip is a directory. Default '*.tif'.

'*.tif'
local_precip_variable str

NetCDF variable holding precipitation. Auto-detected when None (default).

None
local_precip_nodata float

Additional nodata sentinel, applied on top of the value stored in the file metadata (e.g. -9999). Matching pixels become NaN and propagate as NaN to both output rasters.

None
local_precip_crs str

CRS to assign when the local files carry none, or to override the stored one, e.g. 'EPSG:4326'.

None
local_precip_date_regex str

Regular expression with named groups year and month, used to date files the built-in parser cannot read, e.g. r'(?P<month>\d{2})_(?P<year>\d{4})'.

None
clip_to_geometry bool

Clip local precipitation rasters to geometry_path when it points at a local vector file. Default True. Has no effect on GEE precipitation, which is always clipped to the geometry server-side.

True

Attributes:

Name Type Description
geometry Geometry or None

The loaded geometry for the region of interest. None when local precipitation is used and Earth Engine is not required.

collection ImageCollection or None

The filtered and scaled precipitation image collection. None for the PCML method and when local precipitation files are used.

bounds list

Bounding box coordinates of the geometry (lon/lat ring).

Examples:

Basic usage with Ensemble method (default):

from pycropwat import EffectivePrecipitation
ep = EffectivePrecipitation(
    asset_id='ECMWF/ERA5_LAND/MONTHLY_AGGR',
    precip_band='total_precipitation_sum',
    geometry_path='roi.geojson',
    start_year=2015,
    end_year=2020,
    precip_scale_factor=1000
)
ep.process(output_dir='./output', n_workers=4)

Using GEE FeatureCollection asset:

ep = EffectivePrecipitation(
    asset_id='ECMWF/ERA5_LAND/MONTHLY_AGGR',
    precip_band='total_precipitation_sum',
    gee_geometry_asset='projects/my-project/assets/study_area',
    start_year=2015,
    end_year=2020,
    precip_scale_factor=1000,
    gee_project='my-gee-project'
)

Using FAO/AGLW method:

ep = EffectivePrecipitation(
    asset_id='IDAHO_EPSCOR/TERRACLIMATE',
    precip_band='pr',
    geometry_path='study_area.geojson',
    start_year=2000,
    end_year=2020,
    method='fao_aglw'
)

Using fixed percentage method (80%):

ep = EffectivePrecipitation(
    asset_id='IDAHO_EPSCOR/GRIDMET',
    precip_band='pr',
    geometry_path='farm.geojson',
    start_year=2010,
    end_year=2020,
    method='fixed_percentage',
    method_params={'percentage': 0.8}
)

Using USDA-SCS method with AWC and ETo data:

ep = EffectivePrecipitation(
    asset_id='ECMWF/ERA5_LAND/MONTHLY_AGGR',
    precip_band='total_precipitation_sum',
    geometry_path='arizona.geojson',
    start_year=2015,
    end_year=2020,
    precip_scale_factor=1000,
    method='usda_scs',
    method_params={
        'awc_asset': 'projects/my-project/assets/soil_awc',
        'awc_band': 'AWC',
        'eto_asset': 'IDAHO_EPSCOR/GRIDMET',
        'eto_band': 'eto',
        'eto_is_daily': True,
        'rooting_depth': 1.0
    }
)

Local precipitation input (no Earth Engine required):

ep = EffectivePrecipitation(
    local_precip='../pyCropWat_Data/Precip',
    local_precip_pattern='Precip_*.tif',
    local_precip_nodata=-9999,
    method='cropwat'
)
# start_year/end_year inferred from the files when omitted
ep.process_sequential(output_dir='./output', months=[7, 8])

Local precipitation with AWC and ETo still coming from Earth Engine:

ep = EffectivePrecipitation(
    local_precip='../pyCropWat_Data/Precip',
    local_precip_pattern='Precip_*.tif',
    local_precip_nodata=-9999,
    geometry_path='basin.geojson',
    start_year=2005,
    end_year=2010,
    method='ensemble',
    gee_project='my-gee-project',
    method_params={
        'awc_asset': 'projects/sat-io/open-datasets/FAO/HWSD_V2_SMU',
        'awc_band': 'AWC',
        'awc_scale_factor': 0.001,
        'eto_asset': 'IDAHO_EPSCOR/TERRACLIMATE',
        'eto_band': 'pet',
        'eto_scale_factor': 0.1,
        'eto_is_daily': False,
        'rooting_depth': 2.0,
        'mad_factor': 1.0
    }
)
ep.process(output_dir='./output', n_workers=4)

Local NetCDF input with an explicit variable and date pattern:

ep = EffectivePrecipitation(
    local_precip='./wrf_precip.nc',
    local_precip_variable='RAINNC',
    local_precip_crs='EPSG:4326',
    method='fao_aglw'
)
See Also

pycropwat.local : Readers for local precipitation rasters and NetCDF files. pycropwat.methods : Individual effective precipitation calculation functions. pycropwat.analysis : Post-processing and analysis tools.

Source code in pycropwat/core.py
def __init__(
    self,
    asset_id: Optional[str] = None,
    precip_band: Optional[str] = None,
    geometry_path: Optional[Union[str, Path]] = None,
    start_year: int = None,
    end_year: int = None,
    scale: Optional[float] = None,
    precip_scale_factor: float = 1.0,
    gee_project: Optional[str] = None,
    gee_geometry_asset: Optional[str] = None,
    method: PeffMethod = 'ensemble',
    method_params: Optional[dict] = None,
    local_precip: Optional[Union[str, Path]] = None,
    local_precip_pattern: str = '*.tif',
    local_precip_variable: Optional[str] = None,
    local_precip_nodata: Optional[float] = None,
    local_precip_crs: Optional[str] = None,
    local_precip_date_regex: Optional[str] = None,
    clip_to_geometry: bool = True,
):
    self.asset_id = asset_id
    self.precip_band = precip_band
    self.geometry_path = geometry_path
    self.gee_geometry_asset = gee_geometry_asset
    self.start_year = start_year
    self.end_year = end_year
    self.scale = scale  # None means use native resolution
    self.precip_scale_factor = precip_scale_factor
    self.gee_project = gee_project
    self.method = method
    self.method_params = method_params or {}

    # Local precipitation configuration
    self.local_precip = local_precip
    self.local_precip_pattern = local_precip_pattern
    self.local_precip_variable = local_precip_variable
    self.local_precip_nodata = local_precip_nodata
    self.local_precip_crs = local_precip_crs
    self.local_precip_date_regex = local_precip_date_regex
    self.clip_to_geometry = clip_to_geometry
    self._is_local = local_precip is not None
    self._local_source = None
    self._local_scale_meters = None
    self._clip_gdf = None
    # get_month() is called from Dask worker threads in process(); NetCDF handles
    # are shared and are not thread-safe, so reads are serialised with a lock.
    # The same lock makes the coverage-warning check atomic (_warn_partial_coverage).
    # It is created on first use (_get_local_lock) because a threading.Lock
    # cannot be pickled and a GEE-only instance never needs one.
    self._local_lock = None
    # Ancillary fields already reported as only partly covering the local grid.
    # Process-local: it is pickled with the instance, so each Dask worker
    # process warns at most once per field (see _warn_partial_coverage).
    self._regrid_coverage_warned = set()
    # Whether written rasters declare NaN as their nodata value. Decided once per
    # run from the configuration instead of per month from the data, so a run
    # never emits a time series with mixed nodata metadata. Only local
    # precipitation carries nodata; GEE downloads use defaultValue and never do.
    self._declare_nodata = self._is_local

    # Get the effective precipitation function
    self._peff_function = get_method_function(method)

    # USDA-SCS specific: cache for AWC data (loaded once)
    self._awc_cache = None

    # Input directory for saving downloaded data (set during process())
    self._input_dir = None

    # Check if this is PCML method (uses single multi-band Image instead of ImageCollection)
    self._is_pcml = (method == 'pcml' or self.precip_band == PCML_DEFAULT_BAND)

    # PCML is a pre-computed GEE product; it cannot be derived from local rasters
    if self._is_local and self._is_pcml:
        raise ValueError(
            "local_precip cannot be combined with method='pcml'. PCML effective "
            "precipitation is a pre-computed Google Earth Engine product for the "
            "Western U.S. and is not calculated from precipitation. Use a "
            "precipitation-based method (e.g. 'cropwat', 'ensemble') with local files."
        )

    # A precipitation source is mandatory: either GEE or local files.
    # Each way of getting it wrong reports what is actually missing, so a
    # half-specified GEE source is not mistaken for no source at all.
    if not self._is_local and not self._is_pcml:
        if asset_id is None and precip_band is None:
            raise ValueError(
                "No precipitation source provided. Either pass asset_id and "
                "precip_band to download precipitation from Google Earth Engine, "
                "or pass local_precip pointing at a directory of monthly rasters, "
                "a NetCDF file, or a glob to read precipitation from disk."
            )
        if precip_band is None:
            band_example = _example_band_for_asset(asset_id)
            if band_example is not None:
                hint = f", e.g. precip_band='{band_example}' for '{asset_id}'."
            else:
                hint = (
                    f"; its band list is in the Earth Engine Data Catalog "
                    f"(e.g. precip_band='{_EXAMPLE_PRECIP_BAND}' for "
                    f"'{_EXAMPLE_PRECIP_ASSET}')."
                )
            raise ValueError(
                f"precip_band is required when asset_id is given: asset "
                f"'{asset_id}' was provided but no precipitation band was named. "
                f"Pass the band holding precipitation in that asset{hint} "
                f"To read precipitation from disk instead, pass local_precip."
            )
        if asset_id is None:
            asset_example = _example_asset_for_band(precip_band) or _EXAMPLE_PRECIP_ASSET
            raise ValueError(
                f"asset_id is required when precip_band is given: band "
                f"'{precip_band}' was provided but no Google Earth Engine asset "
                f"to read it from. Pass the ImageCollection holding that band, "
                f"e.g. asset_id='{asset_example}'. To read precipitation from "
                f"disk instead, pass local_precip."
            )

    # For PCML, use default asset if placeholder provided
    if self._is_pcml:
        if self.asset_id == 'PLACEHOLDER' or self.asset_id is None:
            self.asset_id = PCML_DEFAULT_ASSET
            logger.info(f"Using default PCML asset: {self.asset_id}")
        self.precip_band = PCML_DEFAULT_BAND

    # Validate that at least one geometry source is provided
    # (not required for PCML, and optional when reading local precipitation)
    if (geometry_path is None and gee_geometry_asset is None
            and not self._is_pcml and not self._is_local):
        raise ValueError("Either geometry_path or gee_geometry_asset must be provided")

    # Open the local precipitation source up front so a bad path fails immediately
    if self._is_local:
        self._local_source = self._build_local_source()
        self._local_scale_meters = self._local_native_scale()

    # Earth Engine is only needed for GEE precipitation, GEE geometries, or the
    # AWC/ETo driven methods. Precipitation-only methods on local files run offline.
    self._needs_gee = (
        (not self._is_local)
        or gee_geometry_asset is not None
        or (geometry_path is not None and is_gee_asset(str(geometry_path)))
        or method in ('usda_scs', 'ensemble', 'suet')
    )

    # Initialize GEE (only when something actually needs it)
    if self._needs_gee:
        initialize_gee(self.gee_project)
        # Remember it for this process, so _ensure_gee() is a no-op here and
        # only a worker process that unpickled the instance pays for a real
        # ee.Initialize().
        _mark_gee_initialized(self.gee_project)
    else:
        logger.info(
            f"Earth Engine not required: local precipitation with method "
            f"'{self.method}' needs no GEE assets. Skipping initialization."
        )

    # Load a local vector geometry for clipping the local rasters
    if self._is_local and geometry_path is not None and not is_gee_asset(str(geometry_path)):
        self._clip_gdf = self._read_clip_geometry(geometry_path)
        logger.info(f"Loaded clip geometry from {geometry_path}")

    # Resolve the region of interest
    if self._is_pcml and geometry_path is None and gee_geometry_asset is None:
        # Load PCML image first
        self._pcml_image = ee.Image(self.asset_id)
        # Use predefined Western U.S. bounding box since PCML image geometry is unbounded
        self.geometry = ee.Geometry.Polygon([PCML_WESTERN_US_BOUNDS])
        self.bounds = PCML_WESTERN_US_BOUNDS
        logger.info("Using predefined Western U.S. bounding box for PCML")
    elif self._is_local and geometry_path is None and gee_geometry_asset is None:
        # No geometry given: use the extent of the local precipitation files
        min_lon, min_lat, max_lon, max_lat = self._local_source.bounds
        self.bounds = [
            [min_lon, min_lat],
            [max_lon, min_lat],
            [max_lon, max_lat],
            [min_lon, max_lat],
            [min_lon, min_lat]
        ]
        self.geometry = (
            ee.Geometry.Rectangle([min_lon, min_lat, max_lon, max_lat])
            if self._needs_gee else None
        )
        logger.info(
            f"No geometry provided; using local precipitation extent "
            f"({min_lon:.4f}, {min_lat:.4f}, {max_lon:.4f}, {max_lat:.4f})"
        )
    elif self._needs_gee:
        # Load geometry from GEE asset or local file
        self.geometry = load_geometry(geometry_path, gee_asset=gee_geometry_asset)
        self.bounds = self.geometry.bounds().getInfo()['coordinates'][0]
    else:
        # Local vector geometry, no Earth Engine needed
        self.geometry = None
        min_lon, min_lat, max_lon, max_lat = self._clip_gdf.to_crs('EPSG:4326').total_bounds
        self.bounds = [
            [min_lon, min_lat],
            [max_lon, min_lat],
            [max_lon, max_lat],
            [min_lon, max_lat],
            [min_lon, min_lat]
        ]

    # Infer / clamp the year range against the local files
    if self._is_local:
        self._resolve_local_years()

    # Get date range
    self.start_date, self.end_date = get_date_range(self.start_year, self.end_year)

    # Load and filter image collection (or load PCML image)
    if self._is_local:
        self.collection = None
        self._pcml_image = None
    else:
        self._load_collection()

__getstate__

__getstate__() -> dict

Return picklable state, so instances survive Dask/distributed.

The NetCDF read lock and the open local precipitation source are dropped: a lock and an open file handle cannot be pickled. Both are recreated on demand by :meth:_get_local_lock and :attr:_local_source.

Returns:

Type Description
dict

Instance state without the unpicklable entries.

Source code in pycropwat/core.py
def __getstate__(self) -> dict:
    """
    Return picklable state, so instances survive Dask/``distributed``.

    The NetCDF read lock and the open local precipitation source are dropped:
    a lock and an open file handle cannot be pickled. Both are recreated on
    demand by :meth:`_get_local_lock` and :attr:`_local_source`.

    Returns
    -------
    dict
        Instance state without the unpicklable entries.
    """
    state = self.__dict__.copy()
    state.pop('_local_lock', None)
    state.pop('_local_source_obj', None)
    return state

__setstate__

__setstate__(state: dict) -> None

Restore state, leaving the lock and local source to be rebuilt lazily.

Earth Engine is re-initialised here when the restored instance needs it (:meth:_ensure_gee), because ee state is process-local and does not travel with the pickle: without this a Dask worker process would fail every ee call with "Earth Engine client library not initialized". A run that needs no Earth Engine initialises nothing, and a process that is already initialised pays only a set lookup.

A failure here is logged rather than raised: unpickling must not fail, and the first real Earth Engine call retries and raises :class:_GeeUnavailableError so the failure is reported against the task that needed it.

Parameters:

Name Type Description Default
state dict

State produced by :meth:__getstate__.

required
Source code in pycropwat/core.py
def __setstate__(self, state: dict) -> None:
    """
    Restore state, leaving the lock and local source to be rebuilt lazily.

    Earth Engine is re-initialised here when the restored instance needs it
    (:meth:`_ensure_gee`), because ``ee`` state is process-local and does not
    travel with the pickle: without this a Dask worker process would fail
    every ``ee`` call with "Earth Engine client library not initialized". A
    run that needs no Earth Engine initialises nothing, and a process that is
    already initialised pays only a set lookup.

    A failure here is logged rather than raised: unpickling must not fail, and
    the first real Earth Engine call retries and raises
    :class:`_GeeUnavailableError` so the failure is reported against the task
    that needed it.

    Parameters
    ----------

    state : dict
        State produced by :meth:`__getstate__`.
    """
    self.__dict__.update(state)
    self.__dict__['_local_lock'] = None
    if state.get('_needs_gee'):
        try:
            self._ensure_gee()
        except Exception as e:
            logger.error(
                f"Earth Engine could not be initialized after unpickling: {e}"
            )

cropwat_effective_precip staticmethod

cropwat_effective_precip(pr: ndarray) -> np.ndarray

Calculate CROPWAT effective precipitation.

Parameters:

Name Type Description Default
pr ndarray

Precipitation in mm.

required

Returns:

Type Description
ndarray

Effective precipitation in mm.

Source code in pycropwat/core.py
@staticmethod
def cropwat_effective_precip(pr: np.ndarray) -> np.ndarray:
    """
    Calculate CROPWAT effective precipitation.

    Parameters
    ----------

    pr : np.ndarray
        Precipitation in mm.

    Returns
    -------
    np.ndarray
        Effective precipitation in mm.
    """
    ep = np.where(
        pr <= 250,
        pr * (125 - 0.2 * pr) / 125,
        0.1 * pr + 125
    )
    return ep

process

process(output_dir: Union[str, Path], n_workers: int = 4, months: Optional[List[int]] = None, input_dir: Optional[Union[str, Path]] = None, save_inputs: bool = False) -> List[Tuple[Optional[Path], Optional[Path]]]

Process all months and save effective precipitation rasters.

Downloads precipitation data from Google Earth Engine - or reads it from the local files given by local_precip - calculates effective precipitation using the configured method, and saves results as GeoTIFF files. Uses Dask for parallel processing of multiple months.

Parameters:

Name Type Description Default
output_dir str or Path

Directory to save output rasters. Will be created if it doesn't exist.

required
n_workers int

Number of parallel workers for Dask. Default is 4. Set to 1 for sequential processing.

4
months list of int

List of months to process (1-12). If None, processes all months in the date range. Useful for seasonal analyses.

None
input_dir str or Path

Directory to save downloaded input data (precipitation, AWC, ETo). If None and save_inputs is True, uses output_dir/../analysis_inputs.

None
save_inputs bool

Whether to save downloaded input data as GeoTIFF files. Default is False. Useful for debugging or further analysis.

False

Returns:

Type Description
list of tuple

List of tuples containing paths to saved files: (effective_precip_path, effective_precip_fraction_path). Returns (None, None) for months that failed to process.

Notes

Output files are named:

  • effective_precip_YYYY_MM.tif - Effective precipitation in mm
  • effective_precip_fraction_YYYY_MM.tif - Effective/total ratio (non-PCML methods)
  • effective_precip_fraction_YYYY.tif - Annual (water year) fraction (PCML method only)

For the USDA-SCS method, AWC and ETo data are automatically downloaded and cached for efficiency. This is true for local precipitation too: AWC and ETo always come from Earth Engine.

With local precipitation, months that have no file are skipped with a warning instead of being reported as failures, and nodata pixels are written as NaN in both outputs.

Examples:

Process all months in parallel:

ep = EffectivePrecipitation(...)
results = ep.process(output_dir='./output', n_workers=8)

Process only summer months:

results = ep.process(
    output_dir='./output',
    months=[6, 7, 8]  # June, July, August
)

Save input data for debugging:

results = ep.process(
    output_dir='./output',
    save_inputs=True,
    input_dir='./inputs'
)
See Also
process_sequential: Sequential processing for debugging.
Source code in pycropwat/core.py
def process(
    self,
    output_dir: Union[str, Path],
    n_workers: int = 4,
    months: Optional[List[int]] = None,
    input_dir: Optional[Union[str, Path]] = None,
    save_inputs: bool = False
) -> List[Tuple[Optional[Path], Optional[Path]]]:
    """
    Process all months and save effective precipitation rasters.

    Downloads precipitation data from Google Earth Engine - or reads it from
    the local files given by ``local_precip`` - calculates effective
    precipitation using the configured method, and saves results as GeoTIFF
    files. Uses Dask for parallel processing of multiple months.

    Parameters
    ----------

    output_dir : str or Path
        Directory to save output rasters. Will be created if it
        doesn't exist.

    n_workers : int, optional
        Number of parallel workers for Dask. Default is 4.
        Set to 1 for sequential processing.

    months : list of int, optional
        List of months to process (1-12). If None, processes all months
        in the date range. Useful for seasonal analyses.

    input_dir : str or Path, optional
        Directory to save downloaded input data (precipitation, AWC, ETo).
        If None and save_inputs is True, uses ``output_dir/../analysis_inputs``.

    save_inputs : bool, optional
        Whether to save downloaded input data as GeoTIFF files.
        Default is False. Useful for debugging or further analysis.

    Returns
    -------
    list of tuple
        List of tuples containing paths to saved files:
        ``(effective_precip_path, effective_precip_fraction_path)``.
        Returns ``(None, None)`` for months that failed to process.

    Notes
    -----
    Output files are named:

    - ``effective_precip_YYYY_MM.tif`` - Effective precipitation in mm
    - ``effective_precip_fraction_YYYY_MM.tif`` - Effective/total ratio (non-PCML methods)
    - ``effective_precip_fraction_YYYY.tif`` - Annual (water year) fraction (PCML method only)

    For the USDA-SCS method, AWC and ETo data are automatically downloaded
    and cached for efficiency. This is true for local precipitation too:
    AWC and ETo always come from Earth Engine.

    With local precipitation, months that have no file are skipped with a
    warning instead of being reported as failures, and nodata pixels are
    written as NaN in both outputs.

    Examples
    --------
    Process all months in parallel:

    ```python
    ep = EffectivePrecipitation(...)
    results = ep.process(output_dir='./output', n_workers=8)
    ```

    Process only summer months:

    ```python
    results = ep.process(
        output_dir='./output',
        months=[6, 7, 8]  # June, July, August
    )
    ```

    Save input data for debugging:

    ```python
    results = ep.process(
        output_dir='./output',
        save_inputs=True,
        input_dir='./inputs'
    )
    ```

    See Also
    --------
        process_sequential: Sequential processing for debugging.
    """
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    # Set up input directory for saving downloaded data
    if save_inputs:
        if input_dir is not None:
            self._input_dir = Path(input_dir)
        else:
            # Default: parallel to output_dir in analysis_inputs
            self._input_dir = output_dir.parent / 'analysis_inputs' / output_dir.name
        self._input_dir.mkdir(parents=True, exist_ok=True)
        logger.info(f"Input data will be saved to: {self._input_dir}")
    else:
        self._input_dir = None

    # Generate list of (year, month) to process
    all_dates = get_monthly_dates(self.start_year, self.end_year)

    if months is not None:
        all_dates = [(y, m) for y, m in all_dates if m in months]

    # Drop months that have no local precipitation file
    all_dates = self._filter_local_dates(all_dates)

    logger.info(f"Processing {len(all_dates)} months with {n_workers} workers")

    # Create delayed tasks
    tasks = [
        delayed(self._process_single_month)(year, month, output_dir)
        for year, month in all_dates
    ]

    # Execute in parallel with progress bar
    with ProgressBar():
        results = compute(*tasks, num_workers=n_workers)

    return list(results)

process_sequential

process_sequential(output_dir: Union[str, Path], months: Optional[List[int]] = None, input_dir: Optional[Union[str, Path]] = None, save_inputs: bool = False) -> List[Tuple[Optional[Path], Optional[Path]]]

Process all months sequentially (useful for debugging).

Same as :meth:process but without parallel processing. Useful for debugging issues, testing on small datasets, or when GEE rate limits are a concern. Also the simplest way to run a local precipitation workflow with no Earth Engine involvement.

Parameters:

Name Type Description Default
output_dir str or Path

Directory to save output rasters. Will be created if it doesn't exist.

required
months list of int

List of months to process (1-12). If None, processes all months in the date range.

None
input_dir str or Path

Directory to save downloaded input data (precipitation, AWC, ETo). If None and save_inputs is True, uses output_dir/../analysis_inputs.

None
save_inputs bool

Whether to save downloaded input data. Default is False.

False

Returns:

Type Description
list of tuple

List of tuples containing paths to saved files: (effective_precip_path, effective_precip_fraction_path). Returns (None, None) for months that failed to process.

Examples:

Debug a single month:

ep = EffectivePrecipitation(...)
results = ep.process_sequential(
    output_dir='./output',
    months=[1]  # Process only January
)
See Also
process: Parallel processing method (recommended for production).
Source code in pycropwat/core.py
def process_sequential(
    self,
    output_dir: Union[str, Path],
    months: Optional[List[int]] = None,
    input_dir: Optional[Union[str, Path]] = None,
    save_inputs: bool = False
) -> List[Tuple[Optional[Path], Optional[Path]]]:
    """
    Process all months sequentially (useful for debugging).

    Same as :meth:`process` but without parallel processing. Useful for
    debugging issues, testing on small datasets, or when GEE rate limits
    are a concern. Also the simplest way to run a local precipitation
    workflow with no Earth Engine involvement.

    Parameters
    ----------

    output_dir : str or Path
        Directory to save output rasters. Will be created if it
        doesn't exist.

    months : list of int, optional
        List of months to process (1-12). If None, processes all months
        in the date range.

    input_dir : str or Path, optional
        Directory to save downloaded input data (precipitation, AWC, ETo).
        If None and save_inputs is True, uses ``output_dir/../analysis_inputs``.

    save_inputs : bool, optional
        Whether to save downloaded input data. Default is False.

    Returns
    -------
    list of tuple
        List of tuples containing paths to saved files:
        ``(effective_precip_path, effective_precip_fraction_path)``.
        Returns ``(None, None)`` for months that failed to process.

    Examples
    --------
    Debug a single month:

    ```python
    ep = EffectivePrecipitation(...)
    results = ep.process_sequential(
        output_dir='./output',
        months=[1]  # Process only January
    )
    ```

    See Also
    --------
        process: Parallel processing method (recommended for production).
    """
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    # Set up input directory for saving downloaded data
    if save_inputs:
        if input_dir is not None:
            self._input_dir = Path(input_dir)
        else:
            # Default: parallel to output_dir in analysis_inputs
            self._input_dir = output_dir.parent / 'analysis_inputs' / output_dir.name
        self._input_dir.mkdir(parents=True, exist_ok=True)
        logger.info(f"Input data will be saved to: {self._input_dir}")
    else:
        self._input_dir = None

    all_dates = get_monthly_dates(self.start_year, self.end_year)

    if months is not None:
        all_dates = [(y, m) for y, m in all_dates if m in months]

    # Drop months that have no local precipitation file
    all_dates = self._filter_local_dates(all_dates)

    logger.info(f"Processing {len(all_dates)} months sequentially")

    results = []
    for year, month in all_dates:
        result = self._process_single_month(year, month, output_dir)
        results.append(result)

    return results