An interactive guide to irrigation_performance.py — a script that measures irrigation fairness using satellites, weather data, and math.
Sentinel-2, Landsat, and ERA5-Land collect daily images and weather data over Java, Indonesia.
749 tertiary irrigation blocks in DI Klambu, Central Java — each needs water at the right time.
Three indices tell us: Is each block getting enough? Is water distributed fairly? Is delivery consistent?
Indonesia manages thousands of irrigation districts feeding millions of hectares of rice. Traditionally, performance was measured by sending field inspectors. This script replaces months of fieldwork with satellite data processed in minutes.
Get the source code, Jupyter notebook, and study area data to follow along:
irrigation_performance.py — main analysis scriptPUPR_IrrigationPerformance_5Oct25.ipynb — Jupyter notebookdata/klambu.gpkg — Klambu Irrigation Area boundaries (749 blocks)What "irrigation performance" actually means and why it's hard to measure.
Imagine you're the manager of an irrigation district with 749 rice paddies. You need to answer three questions every season:
Compares actual water received (ETa) against what the crop needs (CWR). An SI of 0.83 means 83% of water needs are met.
Measures how evenly SI varies across blocks. CU > 0.85 means good distribution — no block is being starved while others get excess.
Counts how many weeks each block had SI above a threshold. High RI means farmers can depend on the system.
The script avoids physical measurement by using a clever proxy: if a rice plant is getting enough water, it grows well, which changes how it reflects sunlight. Satellites can see this from space.
A doctor doesn't measure every cell in your body. They check a few indicators (blood pressure, temperature) that correlate with overall health. This script checks satellite "vital signs" of rice paddies to assess irrigation health.
The data sources, tools, and concepts that make this pipeline work.
Optical satellites that photograph Earth every few days. Used to detect when rice is transplanted via NDVI and NDWI peaks.
A global weather reanalysis dataset from ECMWF. Provides daily temperature, wind, radiation, and humidity to calculate ET0.
A satellite product that estimates actual evapotranspiration (ETa) — how much water crops actually used, not just how much they needed.
GEE is the cloud engine that processes all the satellite data without downloading terabytes to your laptop.
749 polygon boundaries representing individual irrigation units. Each block's performance is measured independently.
How raw satellite data becomes irrigation performance scores.
def main():
parser = argparse.ArgumentParser(...)
parser.add_argument('--study-area', ...)
parser.add_argument('--year', type=int)
parser.add_argument('--output-dir', required=True)
parser.add_argument('--local-only',
action='store_true')
Define the main function — the starting point of the whole script.
Set up a command-line interface so the user can pass options when running the script.
Accept a GEE asset path for the irrigation district boundaries.
Accept a year number (like 2023).
Require an output folder path — where all results will be saved.
Add a flag: if present, skip all satellite downloading and only do local math on existing CSV files.
How the script turns temperature and sunlight into crop water requirements.
The script calculates ET0 using the Penman-Monteith equation — the FAO standard. Think of it as an "atmospheric thirst meter."
et0 = tmean.expression(
'(0.408 * slope * (Rnet - 0) +'
'(psy * (900/(tmean+273)))'
'* ws * (es - ea)) /'
'(slope + psy * (0.34*ws + 1))',
{'slope': slope, 'Rnet': Rnet,
'psy': psy, 'tmean': tmean,
'ws': ws, 'es': es,
'ea': ea}).rename('ET0')
Calculate ET0 using the FAO Penman-Monteith formula.
The top of the fraction: energy from sunlight (Rnet) combined with...
...the drying power of wind. es - ea is the "vapor pressure deficit" — how much drier the air is compared to saturation.
The bottom of the fraction adjusts for temperature response and wind effects.
Feed in all the weather variables from ERA5-Land.
Name the result "ET0" (in mm/day).
The chain from weather to performance score has four links:
The 1.2 is an empirical correction — satellite ETa tends to be low in humid regions. Values above 1.0 are capped at 1.0 (you can't be more than 100% satisfied).
Kc changes as rice grows. The script models a 110-day growth cycle:
for day in days:
if day <= 10:
kc = 1.05 # Initial stage
elif day <= 40:
kc = 1.05 + (1.15-1.05)*(day-10)/30
elif day <= 80:
kc = 1.15 + (1.2-1.15)*(day-40)/40
else:
kc = 1.2 + (0.95-1.2)*(day-80)/30
Go through each day of the 110-day rice cycle...
Days 1–10 (Initial): Seedlings just transplanted. Low water use. Kc = 1.05
Days 11–40 (Development): Plants growing fast. Kc rises from 1.05 to 1.15
Days 41–80 (Mid-season): Full canopy, heading & flowering. Peak water use. Kc = 1.15 to 1.20
Days 81–110 (Late season): Grain filling, drying down. Kc drops from 1.20 to 0.95
Turning per-block SI into district-wide performance grades.
Imagine 749 students taking an exam. The average score might be 80%, but if half scored 100% and half scored 60%, that's not uniform. CU captures this spread.
weekly_uniformity['uniformity'] = (
1 - weekly_uniformity['std_SI']
/ weekly_uniformity['mean_SI']
).fillna(1).clip(0, 1)
Uniformity = 1 minus the coefficient of variation (std / mean).
If all blocks have identical SI, std = 0, so uniformity = 1 (perfect).
If any value is missing (NaN), treat it as perfect (1). Clamp between 0 and 1.
A water supply that works 9 out of 10 weeks is more reliable than one that works 5 out of 10, even if both deliver the same total water. Farmers need predictability.
reliability['reliability'] = (
reliability['weeks_above']
/ reliability['total_weeks']
).clip(0, 1)
For each block: count weeks where SI was above the threshold (default: 50%).
Divide by total weeks in the season.
Result: fraction of weeks the block received "adequate" water. 1.0 = never missed a week.
The script found: Mean SI = 0.83 (83% of water needs met), CU = 0.93 (very fair distribution), RI = 0.98 (98% of weeks adequate). All 26 weeks had CU above the 0.85 target. Klambu is a well-performing district!
Three modes, real commands, and what to expect.
Downloads everything from GEE + processes locally. Takes 30–60 minutes. Use for first run.
python irrigation_performance.py --output-dir results/klambu_2023 --year 2023
--skip-gee)
Initializes GEE but uses existing CSVs for processing. Use when you've already downloaded but want to recompute.
--local-only)
No GEE at all. Pure pandas processing. Takes seconds. Use for re-analysis with different parameters.
python irrigation_performance.py --output-dir results/klambu_2023 --local-only
uniformity_block_results.csv, reliability_block_results.csv — one row per tertiary block with performance scores.
SI time series, uniformity trends, reliability histograms — ready for reports and presentations.
performance_summary.txt — overall metrics, targets met, and block counts at a glance.
You now understand how irrigation_performance.py turns satellite data into irrigation performance scores. The same approach can be applied to any irrigation district in Indonesia — just change the --study-area GEE asset path.