Skip to content

Calibration and Validation Tutorial

This tutorial walks through automated parameter calibration against a USGS gauge record and spatial flood validation against FEMA NFHL data.


Overview

PyWMP's calibration module (pywmp.calibration) implements three optimisation algorithms. The default is differential evolution (Storn & Price, 1997), which is recommended for problems with 3–15 parameters because it is gradient-free and robust to multi-modal response surfaces (Duan et al., 1992).

Performance is evaluated using Nash-Sutcliffe Efficiency (NSE; Nash & Sutcliffe, 1970) or Kling-Gupta Efficiency (KGE; Gupta et al., 2009). Ratings follow the guidelines of Moriasi et al. (2007).


Part 1 — Streamflow calibration

Step 1 — Download observed gauge data

from pywmp.validation import download_streamflow, GaugeStation

obs = download_streamflow("02290000", start="2019-01-01", end="2020-12-31")
# Returns TimeSeries via USGS NWIS REST API

Step 2 — Define parameter search space

from pywmp.calibration import ParameterBound

params = [
    ParameterBound("cn",         lo=70,   hi=98,   initial=85),
    ParameterBound("initial_abs",lo=0.10, hi=0.60, initial=0.30),
    ParameterBound("lag_hr",     lo=1.0,  hi=8.0,  initial=4.0),
    ParameterBound("k_mann",     lo=0.02, hi=0.15, initial=0.06),
]

Log-scale search (log_scale=True) is recommended for parameters that span more than one order of magnitude, such as saturated hydraulic conductivity Ks (Green & Ampt, 1911).

Step 3 — Run differential evolution

from pywmp.calibration import CalibrationEngine

def run_model(cn, initial_abs, lag_hr, k_mann):
    """Thin wrapper that runs PyWMP and returns a TimeSeries."""
    ...
    return sim_q

engine = CalibrationEngine(
    model_fn=run_model,
    observed=obs,
    params=params,
    objective="KGE",                     # Gupta et al. (2009)
    method="differential_evolution",     # Storn & Price (1997)
)
results = engine.run(max_iter=400, popsize=15, tol=1e-4)

Differential evolution (Storn & Price, 1997) runs a population of candidate parameter vectors and evolves them via mutation and crossover. The population size (popsize) should be at least 5 × number of parameters. For 4 parameters, popsize=15 provides a good balance between exploration and convergence speed.

Step 4 — Evaluate and rate performance

from pywmp.validation import HydroMetrics

best_sim = run_model(**results.best.values)
m = HydroMetrics(obs=obs.values, sim=best_sim.values)

print(f"NSE  = {m.nse():.3f}")   # Nash & Sutcliffe (1970)
print(f"KGE  = {m.kge():.3f}")   # Gupta et al. (2009); Kling et al. (2012)
print(f"PBIAS= {m.pbias():.1f}%")
print(f"Rating: {m.rating()}")   # Moriasi et al. (2007) rating thresholds

# Performance criteria (Moriasi et al., 2007):
# NSE > 0.75 and |PBIAS| < 10% → Very Good
# NSE > 0.65 and |PBIAS| < 15% → Good
# NSE > 0.50 and |PBIAS| < 25% → Satisfactory

Step 5 — Convergence plot

import matplotlib.pyplot as plt

plt.figure(figsize=(8, 3))
plt.plot(results.convergence_curve)
plt.xlabel("Iteration")
plt.ylabel("KGE")
plt.title("Differential evolution convergence")
plt.tight_layout()
plt.savefig("outputs/figures/calibration_convergence.png", dpi=150)

Part 2 — Local refinement with Nelder-Mead

After a global search, Nelder-Mead simplex (Nelder & Mead, 1965) can refine the solution near the differential evolution optimum:

from pywmp.calibration import ParameterBound

# Tighten bounds around the DE optimum
refined_params = [
    ParameterBound("cn",     lo=results.best.values["cn"] - 3,
                              hi=results.best.values["cn"] + 3,
                              initial=results.best.values["cn"]),
    ...
]

engine_nm = CalibrationEngine(
    model_fn=run_model,
    observed=obs,
    params=refined_params,
    objective="KGE",
    method="nelder_mead",       # Nelder & Mead (1965)
)
refined = engine_nm.run(max_iter=200)
print(f"Refined KGE: {refined.best.objective_value:.4f}")

Part 3 — Spatial flood validation

Spatial validation compares the simulated flood extent against a reference binary flood mask, using the Critical Success Index (CSI; Schaefer, 1990).

from pywmp.validation import SpatialFloodValidation
import rasterio
import numpy as np
import geopandas as gpd

# Load simulated depth raster
with rasterio.open("outputs/inundation/rain_100yr_depth.tif") as src:
    depth = src.read(1)
    transform = src.transform
    crs = src.crs

# Load FEMA AE zone (FEMA, 2023) as binary reference
fema_ae = gpd.read_file("data/fema/nfhl.gpkg").query("FLD_ZONE == 'AE'")
fema_mask = rasterize_to_array(fema_ae, transform, depth.shape, crs)

# Validate
val = SpatialFloodValidation(
    sim_depth=depth,
    ref_mask=fema_mask.astype(bool),
    depth_threshold=0.1,    # ft — cells shallower than this are treated as dry
)
m = val.metrics()

print(f"CSI = {m['CSI']:.3f}")   # Schaefer (1990)
print(f"HR  = {m['HR']:.3f}")   # Wing et al. (2017)
print(f"FAR = {m['FAR']:.3f}")  # Wing et al. (2017)
print(f"F1  = {m['F1']:.3f}")

CSI (Critical Success Index; Schaefer, 1990) is preferred over accuracy for flood validation because it ignores true negatives (the large area of dry land that inflates accuracy). Target CSI thresholds: ≥ 0.50 for HLL simulations at 10m resolution, consistent with Bates et al. (2010) and Wing et al. (2017).

Export contingency raster

val.export_geotiff("outputs/qaqc/spatial_validation_100yr.tif")
# Pixel values: 1=TP, 2=FP, 3=FN, 4=TN

Part 4 — Paired gauge + spatial report

from pywmp.validation import ModelVsObserved

mv = ModelVsObserved(
    station=GaugeStation("02290000", start="2019-01-01", end="2020-12-31"),
    sim_series=best_sim,
)
report = mv.report()   # dict with metrics + rating + recommendation text
print(report["summary"])

References

Bates, P. D., Horritt, M. S., & Fewtrell, T. J. (2010). A simple inertial formulation of the shallow water equations for efficient two-dimensional flood inundation modelling. Journal of Hydrology, 387(1–2), 33–45. DOI ↗

Duan, Q., Sorooshian, S., & Gupta, V. K. (1992). Effective and efficient global optimization for conceptual rainfall-runoff models. Water Resources Research, 28(4), 1015–1031. DOI ↗

Federal Emergency Management Agency. (2023). National Flood Hazard Layer (NFHL). FEMA MSC ↗

Green, W. H., & Ampt, G. A. (1911). Studies on soil physics. Journal of Agricultural Science, 4(1), 1–24. DOI ↗

Gupta, H. V., Kling, H., Yilmaz, K. K., & Martinez, G. F. (2009). Decomposition of the mean squared error and NSE performance criteria: Implications for improving hydrological modelling. Journal of Hydrology, 377(1–2), 80–91. DOI ↗

Kling, H., Fuchs, M., & Paulin, M. (2012). Runoff conditions in the upper Danube basin under an ensemble of climate change scenarios. Journal of Hydrology, 424–425, 264–277. DOI ↗

Moriasi, D. N., Arnold, J. G., Van Liew, M. W., Bingner, R. L., Harmel, R. D., & Veith, T. L. (2007). Model evaluation guidelines for systematic quantification of accuracy in watershed simulations. Transactions of the ASABE, 50(3), 885–900. DOI ↗

Nash, J. E., & Sutcliffe, J. V. (1970). River flow forecasting through conceptual models. Part I — A discussion of principles. Journal of Hydrology, 10(3), 282–290. DOI ↗

Nelder, J. A., & Mead, R. (1965). A simplex method for function minimization. The Computer Journal, 7(4), 308–313. DOI ↗

Schaefer, J. T. (1990). The critical success index as an indicator of warning skill. Weather and Forecasting, 5(4), 570–575. DOI ↗

Storn, R., & Price, K. (1997). Differential evolution — A simple and efficient heuristic for global optimization over continuous spaces. Journal of Global Optimization, 11(4), 341–359. DOI ↗

U.S. Geological Survey. (2023c). National Water Information System (NWIS): USGS water data for the nation. NWIS ↗

Wing, O. E. J., Bates, P. D., Sampson, C. C., Smith, A. M., Johnson, K. A., & Erickson, T. A. (2017). Validation of a 30 m resolution flood hazard model of the conterminous United States. Water Resources Research, 53(9), 7968–7986. DOI ↗