Python API¶
This guide covers the Python API for pyCropWat.
EffectivePrecipitation Class¶
The main interface for calculating effective precipitation.
Basic Usage¶
from pycropwat import EffectivePrecipitation
# Initialize with geometry file
ep = EffectivePrecipitation(
asset_id='ECMWF/ERA5_LAND/MONTHLY_AGGR',
precip_band='total_precipitation_sum',
geometry_path='study_area.geojson',
start_year=2020,
end_year=2023,
precip_scale_factor=1000
)
# Process all months in parallel
results = ep.process(output_dir='./outputs', n_workers=4)
Common Precipitation Datasets¶
# ERA5-Land Monthly (~11 km global)
ep_era5 = EffectivePrecipitation(
asset_id='ECMWF/ERA5_LAND/MONTHLY_AGGR',
precip_band='total_precipitation_sum',
geometry_path='study_area.geojson',
start_year=2020, end_year=2023,
precip_scale_factor=1000 # meters to mm
)
# TerraClimate (~4 km global)
ep_terra = EffectivePrecipitation(
asset_id='IDAHO_EPSCOR/TERRACLIMATE',
precip_band='pr',
geometry_path='study_area.geojson',
start_year=2020, end_year=2023,
precip_scale_factor=1 # already in mm
)
# GridMET (~4 km U.S. only)
ep_gridmet = EffectivePrecipitation(
asset_id='IDAHO_EPSCOR/GRIDMET',
precip_band='pr',
geometry_path='study_area.geojson',
start_year=2020, end_year=2023,
precip_scale_factor=1
)
# PRISM (~4 km U.S. only)
ep_prism = EffectivePrecipitation(
asset_id='OREGONSTATE/PRISM/AN81m',
precip_band='ppt',
geometry_path='study_area.geojson',
start_year=2020, end_year=2023,
precip_scale_factor=1
)
# CHIRPS (~5.5 km global, 50°S-50°N)
ep_chirps = EffectivePrecipitation(
asset_id='UCSB-CHG/CHIRPS/PENTAD',
precip_band='precipitation',
geometry_path='study_area.geojson',
start_year=2020, end_year=2023,
precip_scale_factor=1
)
Using GEE Vector 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=2020,
end_year=2023,
precip_scale_factor=1000
)
Local Precipitation Input¶
Pass local_precip to read monthly precipitation from disk instead of downloading it from Earth
Engine. asset_id and precip_band are then unused (they are Optional[str] = None since v1.3.0),
and start_year/end_year are inferred from the files when omitted:
from pycropwat import EffectivePrecipitation
ep = EffectivePrecipitation(
local_precip='../pyCropWat_Data/Precip', # directory, NetCDF file, or glob
local_precip_pattern='Precip_*.tif',
local_precip_nodata=-9999,
method='cropwat'
)
# No Earth Engine at all for precipitation-only methods
results = ep.process(output_dir='./outputs', n_workers=4)
New Constructor Arguments¶
All local-precipitation arguments are appended after the existing ones, so existing positional and keyword calls are unchanged.
| Parameter | Type | Default | Description |
|---|---|---|---|
local_precip |
str or Path |
None |
Local precipitation to use instead of GEE: a directory of monthly rasters, a single NetCDF file (or a directory of them), or a glob string such as './Precip/Precip_*.tif'. Not compatible with method='pcml' |
local_precip_pattern |
str |
'*.tif' |
Glob used when local_precip is a directory |
local_precip_variable |
str |
None |
NetCDF variable holding precipitation. Auto-detected when None |
local_precip_nodata |
float |
None |
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 |
local_precip_crs |
str |
None |
CRS override, e.g. 'EPSG:4326'. Replaces whatever CRS the files declare (logged at INFO) and supplies one when they declare none. It relabels the grid in place - values and transform are untouched, nothing is reprojected |
local_precip_date_regex |
str |
None |
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})' |
clip_to_geometry |
bool |
True |
Clip local precipitation rasters to geometry_path when it points at a local vector file. No effect on GEE precipitation, which is always clipped server-side. Setting it to False keeps the full grid, but pixels outside the AWC/ETo download region then stay NaN in usda_scs/ensemble/suet outputs |
precip_scale_factor applies to local files too, so use it to convert your units to millimetres per
month (e.g. 1000 for metres). It is a single multiplier, so it cannot turn a rate such as
kg m⁻² s⁻¹ into a monthly total - accumulate those to monthly totals before pyCropWat reads them.
Related changes to existing arguments:
| Parameter | Change |
|---|---|
asset_id |
Now Optional[str] = None. Required only when local_precip is None and method != 'pcml' |
precip_band |
Now Optional[str] = None. Same rule as asset_id |
geometry_path / gee_geometry_asset |
Optional with local_precip. Without either, the extent of the local files is used |
Omitting a precipitation source altogether raises ValueError("No precipitation source provided...").
What Still Comes From Earth Engine¶
| Method | Earth Engine required with local_precip? |
|---|---|
cropwat, fao_aglw, fixed_percentage, dependable_rainfall, farmwest |
No - ee.Initialize() is never called; runs offline |
usda_scs, ensemble, suet |
Yes - AWC and/or ETo are still downloaded from GEE |
pcml |
Not supported - raises ValueError (pre-computed GEE product) |
A GEE FeatureCollection geometry (gee_geometry_asset, or a geometry_path that is a GEE asset)
also requires Earth Engine, regardless of method.
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', # clips the local rasters
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='./outputs', n_workers=4)
Local NetCDF input with an explicit variable and CRS:
ep = EffectivePrecipitation(
local_precip='./wrf_precip.nc',
local_precip_variable='RAINNC',
local_precip_crs='EPSG:4326', # override: relabels the grid, never reprojects
method='fao_aglw'
)
AWC/ETo coverage
usda_scs, ensemble and suet still download AWC and ETo from Earth Engine for the
geometry you give them. Any part of the local precipitation grid the download did not cover
stays NaN in the outputs - pyCropWat does not backfill it - and the run logs a WARNING
naming the uncovered pixel count, once per field per process (twice for usda_scs and
ensemble: once for AWC, once for ETo):
AWC covers only 3.0% of the precipitation grid; 534255 pixel(s) will be NaN in the output. Clip the precipitation to the geometry (clip_to_geometry=True) or widen the geometry.
Clipping to the geometry (clip_to_geometry=True, the default) or widening the geometry gives
full coverage.
See Local Precipitation Data for the complete guide.
Custom Resolution¶
ep = EffectivePrecipitation(
asset_id='IDAHO_EPSCOR/TERRACLIMATE',
precip_band='pr',
geometry_path='study_area.geojson',
start_year=2020,
end_year=2023,
scale=5000 # 5 km resolution in meters
)
Processing Options¶
# Process all months with parallel workers
results = ep.process(output_dir='./outputs', n_workers=4)
# Process specific months only
results = ep.process(
output_dir='./outputs',
n_workers=4,
months=[6, 7, 8, 9] # June through September
)
# Sequential processing (useful for debugging)
results = ep.process_sequential(output_dir='./outputs')
LocalPrecipitationSource Class¶
pycropwat.local.LocalPrecipitationSource is the reader behind local_precip. Use it directly when
you want to inspect a dataset, or to pull monthly grids without running the full workflow.
Constructor¶
LocalPrecipitationSource(
path: Union[str, Path], # directory of rasters, a raster/NetCDF file, or a glob string
pattern: str = '*.tif', # glob applied when `path` is a directory
variable: Optional[str] = None, # NetCDF variable; auto-detected when None
scale_factor: float = 1.0, # multiplier applied to every returned array (source units -> mm)
nodata: Optional[Union[float, Sequence[float]]] = None, # extra nodata sentinel(s), in addition to the file metadata
crs: Optional[Union[str, CRS]] = None, # CRS override: relabels the grid, replacing any declared CRS
date_regex: Optional[str] = None, # custom regex with named groups 'year' and 'month'
time_dim: str = 'time', # NetCDF time coordinate name
x_dim: Optional[str] = None, # NetCDF X dimension; auto-detected when None
y_dim: Optional[str] = None # NetCDF Y dimension; auto-detected when None
)
Files are indexed up front (cheap); pixel data is read only when get_month() is called.
Properties¶
| Property | Type | Description |
|---|---|---|
kind |
str |
'raster' for one-file-per-month rasters, 'netcdf' otherwise |
files |
List[Path] |
The data files backing this source, sorted |
crs |
CRS |
Coordinate reference system of the source data |
bounds |
Tuple[float, float, float, float] |
(minx, miny, maxx, maxy) in EPSG:4326 lon/lat degrees |
native_bounds |
Tuple[float, float, float, float] |
(minx, miny, maxx, maxy) in the source CRS |
year_range |
Tuple[int, int] |
(first_year, last_year) present in the source |
resolution |
Tuple[float, float] |
(x_resolution, y_resolution) as positive values in CRS units |
shape |
Tuple[int, int] |
(n_rows, n_cols) of every monthly grid |
Methods¶
| Method | Returns | Description |
|---|---|---|
available_months() |
List[Tuple[int, int]] |
Sorted (year, month) pairs present in the source |
has_month(year, month) |
bool |
Whether that month can be read |
get_month(year, month) |
Optional[xr.DataArray] |
The monthly grid, or None when the month is missing |
close() |
None |
Release open NetCDF handles (a no-op in raster mode); safe to call twice |
len(src) gives the number of months available, and repr(src) summarises kind, files, months,
year range, CRS and shape.
Every array returned by get_month() has dims exactly ('y', 'x'), dtype float32, values in
millimetres (scale_factor applied), nodata as NaN, a descending (north-up) y coordinate, the
source CRS attached via rioxarray, and attrs {'units', 'long_name', 'year', 'month', 'source'}.
Usage¶
from pycropwat import LocalPrecipitationSource
src = LocalPrecipitationSource(
'../pyCropWat_Data/Precip',
pattern='Precip_*.tif',
nodata=-9999
)
print(src.kind, len(src), src.year_range) # raster 264 (2000, 2021)
print(src.crs, src.shape, src.resolution) # EPSG:4326 (689, 799) (0.0397..., 0.0397...)
print(src.bounds) # (-71.3018, -40.3510, -39.5434, -12.9648)
da = src.get_month(2005, 7) # xr.DataArray, dims ('y', 'x'), mm
print(da.dims, da.shape, float(da.mean()))
src.close()
The source is also a context manager, which is the safest way to use NetCDF inputs:
from pycropwat import open_local_precipitation
# open_local_precipitation() is a thin factory; kwargs are forwarded unchanged
with open_local_precipitation('precip_2000_2020.nc',
variable='tp',
scale_factor=1000.0) as src: # metres -> mm
for year, month in src.available_months():
da = src.get_month(year, month)
parse_year_month()¶
parse_year_month(name: Union[str, Path], date_regex: Optional[str] = None) -> Optional[Tuple[int, int]]
Extracts (year, month) from a file name, stem, path or arbitrary string. Built-in layouts are
YYYY_MM, YYYY-MM, YYYYMM and YYYY.MM; the last plausible match in the string wins, so
prefixes such as Precip_ never confuse it. Returns None when nothing matched.
from pycropwat import parse_year_month
parse_year_month('Precip_2005_07.tif') # (2005, 7)
parse_year_month('pr200507') # (2005, 7)
parse_year_month('/data/2019/x-2005-07.nc') # (2005, 7)
parse_year_month('no_date_here.tif') # None
# Custom layout: MM_YYYY
parse_year_month('rain_07_2005.tif',
date_regex=r'(?P<month>\d{2})_(?P<year>\d{4})') # (2005, 7)
date_regex must define both named groups, otherwise a ValueError is raised.
Full generated reference: Local Data API. Task-oriented guide: Local Precipitation Data.
Effective Precipitation Methods¶
pyCropWat supports multiple methods for calculating effective precipitation:
from pycropwat.methods import (
cropwat_effective_precip,
fao_aglw_effective_precip,
fixed_percentage_effective_precip,
dependable_rainfall_effective_precip,
farmwest_effective_precip,
usda_scs_effective_precip,
list_available_methods,
get_method_function
)
import numpy as np
# List all available methods with descriptions
for method, description in list_available_methods().items():
print(f"{method}: {description}")
# Get a method function by name
peff_func = get_method_function('cropwat')
precip = np.array([50, 100, 200, 300, 400])
# CROPWAT method
peff_cropwat = cropwat_effective_precip(precip)
# [46. 72. 136. 155. 165.]
# FAO/AGLW method
peff_fao = fao_aglw_effective_precip(precip)
# Fixed percentage (e.g., 80%)
peff_fixed = fixed_percentage_effective_precip(precip, percentage=0.8)
# [40. 80. 160. 240. 320.]
# Dependable rainfall at 75% probability
peff_depend = dependable_rainfall_effective_precip(precip, probability=0.75)
# FarmWest method
peff_farmwest = farmwest_effective_precip(precip)
# [33.75 71.25 146.25 221.25 296.25]
# USDA-SCS method (requires AWC and ETo arrays)
# AWC is in volumetric fraction (cm/cm or inch/inch)
# ETo is reference evapotranspiration in mm
eto = np.array([80, 120, 180, 220, 260]) # mm
awc = np.array([0.15, 0.15, 0.15, 0.15, 0.15]) # volumetric fraction
peff_usda = usda_scs_effective_precip(precip, eto, awc, rooting_depth=1.0, mad_factor=0.5)
# SuET method (requires ETo array)
from pycropwat.methods import suet_effective_precip
peff_suet = suet_effective_precip(precip, eto)
Using USDA-SCS Method with GEE Assets¶
The USDA-SCS method can be used with GEE assets for AWC and ETo:
# For U.S. regions (SSURGO AWC + GridMET ETo)
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/openet/soil/ssurgo_AWC_WTA_0to152cm_composite',
'awc_band': 'AWC',
'eto_asset': 'projects/openet/assets/reference_et/conus/gridmet/monthly/v1',
'eto_band': 'eto',
'rooting_depth': 1.0,
'mad_factor': 0.5
}
)
# For global regions (FAO HWSD AWC + AgERA5 ETo)
ep_global = 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='usda_scs',
method_params={
'awc_asset': 'projects/sat-io/open-datasets/FAO/HWSD_V2_SMU',
'awc_band': 'AWC',
'eto_asset': 'projects/climate-engine-pro/assets/ce-ag-era5-v2/daily',
'eto_band': 'ReferenceET_PenmanMonteith_FAO56',
'eto_is_daily': True, # Will aggregate daily to monthly
'rooting_depth': 1.0,
'mad_factor': 0.5
}
)
Temporal Aggregation¶
The TemporalAggregator class provides functions for aggregating monthly data:
from pycropwat.analysis import TemporalAggregator
agg = TemporalAggregator('./outputs')
# Annual totals
annual = agg.annual_aggregate(
year=2020,
method='sum',
output_path='./annual_2020.tif'
)
# Seasonal aggregation (DJF, MAM, JJA, SON)
summer = agg.seasonal_aggregate(
year=2020,
season='JJA',
method='sum',
output_path='./summer_2020.tif'
)
# Growing season (customizable months)
# Northern Hemisphere (April-October, same year)
growing_nh = agg.growing_season_aggregate(
year=2020,
start_month=4,
end_month=10,
method='sum',
output_path='./growing_2020.tif'
)
# Southern Hemisphere (October-March, cross-year)
# When start_month > end_month, automatically loads from two years
# This aggregates Oct 2020 - Mar 2021
growing_sh = agg.growing_season_aggregate(
year=2020,
start_month=10,
end_month=3,
method='sum',
output_path='./growing_2020_2021.tif'
)
# Custom month range
custom = agg.custom_aggregate(
year=2020,
months=[5, 6, 7, 8],
method='mean',
output_path='./may_aug_mean_2020.tif'
)
# Multi-year climatology (per-month long-term mean)
climatology = agg.multi_year_climatology(
start_year=1990,
end_year=2020,
months=None, # All months; or specify list [6, 7, 8]
output_dir='./climatology/'
)
Statistical Analysis¶
The StatisticalAnalyzer class provides anomaly detection, trend analysis, and zonal statistics:
from pycropwat.analysis import StatisticalAnalyzer
stats = StatisticalAnalyzer('./outputs')
# Calculate anomaly relative to climatology
anomaly = stats.calculate_anomaly(
year=2023,
month=6,
clim_start=1990,
clim_end=2020,
anomaly_type='percent', # 'absolute', 'percent', or 'standardized'
output_path='./anomaly_2023_06.tif'
)
# Trend analysis with linear regression
trend_linear = stats.calculate_trend(
start_year=2000,
end_year=2020,
month=None, # Annual trends; specify 1-12 for specific month
method='linear',
output_dir='./trend_linear/'
)
# Returns a (slope, pvalue) tuple of DataArrays; output_dir also writes
# trend_slope_*.tif and trend_pvalue_*.tif
# Trend analysis with Theil-Sen slope and Mann-Kendall test
trend_sen = stats.calculate_trend(
start_year=2000,
end_year=2020,
method='sen',
output_dir='./trend_sen/'
)
# Returns the same (slope, pvalue) tuple as 'linear'
# Zonal statistics by polygon
zonal_df = stats.zonal_statistics(
geometry_path='./regions.shp',
start_year=2000,
end_year=2020,
months=None, # All months; or specify list [6, 7, 8]
stats=['mean', 'sum', 'min', 'max', 'std'],
output_path='./zonal_stats.csv'
)
Export Functions¶
Export data to different formats:
from pycropwat.analysis import export_to_netcdf, export_to_cog
# Export time series to NetCDF with time dimension
export_to_netcdf(
input_dir='./outputs',
output_path='./data.nc',
variable_name='effective_precip',
pattern='effective_precip_[0-9]*.tif', # Excludes fraction files
compression=True
)
# Convert to Cloud-Optimized GeoTIFF
export_to_cog(
input_path='./effective_precip_2020_06.tif',
output_path='./cog_2020_06.tif'
)
# Batch convert to COGs (excludes fraction files)
from pathlib import Path
for tif in Path('./outputs').glob('effective_precip_[0-9]*.tif'):
export_to_cog(str(tif), f'./cogs/{tif.name}')
Visualization¶
The Visualizer class provides plotting functions:
from pycropwat.analysis import Visualizer
viz = Visualizer('./outputs')
# Time series plot
fig = viz.plot_time_series(
start_year=2000,
end_year=2020,
stat='mean', # Spatial statistic: 'mean', 'sum', 'min', 'max'
title='Effective Precipitation Time Series',
figsize=(12, 6),
output_path='./timeseries.png'
)
# Monthly climatology bar chart
fig = viz.plot_monthly_climatology(
start_year=2000,
end_year=2020,
stat='mean',
title='Monthly Climatology',
output_path='./climatology.png'
)
# Single raster map
fig = viz.plot_raster(
year=2020,
month=6,
cmap='YlGnBu',
vmin=0,
vmax=150,
title='Effective Precipitation - June 2020',
output_path='./map_2020_06.png'
)
# Interactive HTML map (requires leafmap or folium)
viz.plot_interactive_map(
year=2020,
month=6,
cmap='YlGnBu',
opacity=0.7,
zoom_start=6,
output_path='./map.html'
)
# Compare two datasets side-by-side with difference map
fig = viz.plot_comparison(
other_dir='./terraclimate_outputs',
year=2020,
month=6,
labels=('ERA5', 'TerraClimate'),
cmap='YlGnBu',
diff_cmap='RdBu',
output_path='./comparison.png'
)
# Scatter plot comparison with statistics (R², RMSE, bias)
fig = viz.plot_scatter_comparison(
other_dir='./terraclimate_outputs',
start_year=2000,
end_year=2020,
months=None, # All months
sample_size=10000, # Max points to plot
labels=('ERA5 (mm)', 'TerraClimate (mm)'),
output_path='./scatter.png'
)
# Annual totals comparison bar chart
fig = viz.plot_annual_comparison(
other_dir='./terraclimate_outputs',
start_year=2000,
end_year=2020,
labels=('ERA5', 'TerraClimate'),
output_path='./annual_compare.png'
)
Working with Results¶
import rioxarray
# Process and get file paths
results = ep.process(output_dir='./outputs', n_workers=4)
# Results is a list of tuples: [(ep_path, epf_path), ...]
for ep_path, epf_path in results:
if ep_path is not None:
# Read effective precipitation
da = rioxarray.open_rasterio(ep_path)
print(f"Mean effective precip: {da.mean().values:.2f} mm")
Advanced: Accessing Internal Methods¶
# Get native scale of the dataset
native_scale = ep._get_native_scale()
print(f"Native scale: {native_scale} meters")
# Get a monthly image directly
monthly_img = ep._get_monthly_image(year=2020, month=6)
# Download monthly precipitation as xarray
pr_data = ep._download_monthly_precip(year=2020, month=6)
print(pr_data)
Error Handling¶
from pycropwat import EffectivePrecipitation
try:
ep = EffectivePrecipitation(
asset_id='ECMWF/ERA5_LAND/MONTHLY_AGGR',
precip_band='total_precipitation_sum',
geometry_path='nonexistent.geojson',
start_year=2020,
end_year=2023
)
except FileNotFoundError:
print("Geometry file not found!")
except ValueError as e:
print(f"Invalid parameters: {e}")
Logging¶
pyCropWat uses Python's logging module:
import logging
# Enable debug logging
logging.basicConfig(level=logging.DEBUG)
# Or just for pycropwat
logger = logging.getLogger('pycropwat')
logger.setLevel(logging.DEBUG)
Integration with Dask¶
pyCropWat uses Dask for parallel processing. You can customize the scheduler: