Skip to content

Validation and Calibration Reference

pywmp.validation and pywmp.calibration provide tools for comparing model output against gauge and spatial observations, and for automated parameter estimation.


HydroMetrics

class HydroMetrics(obs, sim)

Computes standard hydrologic performance metrics given observed and simulated time series.

Parameter Type Description
obs array-like Observed discharge or stage series
sim array-like Simulated series aligned to obs

Metrics returned

Metric Symbol Formula Reference
Nash-Sutcliffe Efficiency NSE 1 − Σ(obs−sim)² / Σ(obs−obs_mean)² Nash & Sutcliffe, 1970
Kling-Gupta Efficiency KGE 1 − √[(r−1)² + (α−1)² + (β−1)²] Gupta et al., 2009
KGE revised KGE' Modified α component (CV ratio) Kling et al., 2012
Root Mean Square Error RMSE √(mean[(obs−sim)²])
Percent Bias PBIAS 100 × Σ(sim−obs)/Σobs Moriasi et al., 2007
Peak flow error ΔQp (sim_peak − obs_peak)/obs_peak
Peak timing error Δtp time(sim_peak) − time(obs_peak)

KGE decomposes performance into three components:

  • r — Pearson correlation coefficient
  • α — variability ratio (σ_sim / σ_obs, or CV_sim / CV_obs for KGE')
  • β — bias ratio (μ_sim / μ_obs)

Performance ratings (Moriasi et al., 2007)

Rating NSE KGE |PBIAS|
Very Good > 0.75 ≥ 0.75 < 10%
Good 0.65–0.75 0.65–0.75 10–15%
Satisfactory 0.50–0.65 0.50–0.65 15–25%
Unsatisfactory < 0.50 < 0.50 > 25%

Methods

HydroMetrics.nse()            -> float
HydroMetrics.kge()            -> float
HydroMetrics.kge_prime()      -> float
HydroMetrics.kge_components() -> KGEComponents
HydroMetrics.rmse()           -> float
HydroMetrics.pbias()          -> float
HydroMetrics.summary()        -> dict
HydroMetrics.rating()         -> str   # "Very Good" | "Good" | "Satisfactory" | "Unsatisfactory"

KGEComponents

@dataclass
class KGEComponents:
    kge:   float
    r:     float    # correlation
    alpha: float    # variability ratio
    beta:  float    # bias ratio

ModelVsObserved

class ModelVsObserved(station, sim_series, variable='Q')

Pairs simulated output with a USGS NWIS gauge record and computes all HydroMetrics.

Parameter Description
station GaugeStation instance (site number + date range)
sim_series TimeSeries from SimResults
variable 'Q' (discharge) or 'H' (stage)
Example — download, pair, and score
from pywmp.validation import ModelVsObserved, GaugeStation, download_streamflow

station = GaugeStation("02301500", start="2023-08-27", end="2023-08-29")
q_obs   = download_streamflow("02301500", start="2023-08-27", end="2023-08-29")

mv = ModelVsObserved(station, sim_results.outflows["Outlet"])
m  = mv.metrics()
print(m.summary())
# NSE 0.81 · KGE 0.78 · |PBIAS| 6.2 % → Very Good
mv.plot("output/validation_plot.png")

Gauge data is downloaded via USGS Water Services REST API using the same HyRiver infrastructure as pywmp.datasets.


SpatialFloodValidation

class SpatialFloodValidation(sim_depth, ref_mask, valid_mask=None, depth_threshold=0.1)

Compares a simulated depth raster against a binary reference flood extent.

Parameter Type Description
sim_depth ndarray Simulated max depth (ft or m)
ref_mask ndarray[bool] Reference flood extent (True = flooded)
valid_mask ndarray[bool] Optional AOI mask; only cells within AOI are scored
depth_threshold float Depth above which a cell is considered flooded (default 0.1 ft)

Contingency table metrics

Metric Definition Reference
CSI (Critical Success Index) TP / (TP + FP + FN) Schaefer, 1990
Hit Rate (HR) TP / (TP + FN) Wing et al., 2017
False Alarm Ratio (FAR) FP / (TP + FP) Wing et al., 2017
F1 Score 2TP / (2TP + FP + FN)

Validation against FEMA NFHL AE zones typically yields CSI ≈ 0.25–0.55 for bathtub-style simulations in flat coastal terrain (Wing et al., 2017). For high-resolution (1–10 m) HLL simulations, CSI > 0.6 indicates good agreement with SAR-derived extents (Bates et al., 2010).

Methods

SpatialFloodValidation.metrics()              -> dict   # CSI, HR, FAR, F1, TP, FP, FN, TN
SpatialFloodValidation.plot_contingency(ax=None)
SpatialFloodValidation.export_geotiff(path)            # TP/FP/FN/TN raster
Example — compare simulation against FEMA flood zones
from pywmp.validation import SpatialFloodValidation
import geopandas as gpd

fema = gpd.read_file("data/fema_sfha.gpkg")

val = SpatialFloodValidation(
    simulated_depth_tif="output/flood_depth_100yr.tif",
    reference=fema,
    depth_threshold_ft=0.5,
)
m = val.metrics()
print(f"CSI {m['CSI']:.3f}  HR {m['HitRate']:.3f}  FAR {m['FAR']:.3f}")
val.plot_contingency()  # Green = TP, Blue = FN, Red = FP

Suspended sediment rating curve

download_ssc(site_no, start, end)            -> TimeSeries
fit_rating_curve(q, ssc)                     -> RatingCurve
estimate_annual_load_rating(q_annual, curve) -> float  # tons/yr
Example — fit SSC rating curve from USGS data
from pywmp.validation import download_ssc, download_streamflow, \
                             fit_rating_curve, estimate_annual_load_rating

ssc = download_ssc("02301500", start="2020-01-01", end="2023-12-31")
q   = download_streamflow("02301500", start="2020-01-01", end="2023-12-31")

curve = fit_rating_curve(q=q, ssc=ssc)
print(f"SSC = {curve.a:.3f} × Q^{curve.b:.3f}")

load = estimate_annual_load_rating(q_annual=q, rating_curve=curve)
print(f"Mean annual load: {load:.1f} tons/yr")

CalibrationEngine

class CalibrationEngine(model_fn, observed, params, objective='NSE',
                         method='differential_evolution', **solver_kwargs)

Optimises params by minimising/maximising the objective function computed from model_fn output vs. observed.

Parameter Description
model_fn Callable that maps ParameterSet → TimeSeries; runs the simulation
observed Observed TimeSeries
params List of ParameterBound instances
objective 'NSE', 'KGE', 'KGE_prime', 'RMSE', or 'PBIAS'
method 'differential_evolution' (default), 'sce_ua', or 'nelder_mead'

Solver choices

Method Reference Notes
differential_evolution Storn & Price, 1997 Global; recommended for >3 parameters
sce_ua Duan et al., 1992 Classic hydrology calibration; 3–15 parameters
nelder_mead Nelder & Mead, 1965 Local; use for final refinement near optimum

Methods

CalibrationEngine.run(max_iter=500, popsize=15, tol=1e-4) -> CalibrationResults
CalibrationEngine.run_parallel(n_workers=4, ...)           -> CalibrationResults

ParameterBound

@dataclass
class ParameterBound(name, lo, hi, initial=None, log_scale=False)
Field Description
name Parameter name string (must match model_fn kwarg)
lo, hi Lower and upper bounds
initial Starting point; if None, midpoint of [lo, hi]
log_scale If True, search on log₁₀ scale (useful for Ks and similar order-of-magnitude parameters)

ParameterSet

@dataclass
class ParameterSet:
    values: dict[str, float]   # {name: optimised_value}
    objective_value: float
    n_evaluations: int
    converged: bool

CalibrationResults

@dataclass
class CalibrationResults:
    best: ParameterSet
    all_trials: list[ParameterSet]
    convergence_curve: ndarray   # objective per iteration
    runtime_s: float

GaugeStation

@dataclass
class GaugeStation(site_no, start, end, variable='Q')

Thin wrapper for a USGS NWIS gauge record. Lazy-loads data on first access via the USGS Water Services REST API.


Download helpers

download_streamflow(site_no, start, end) -> TimeSeries
download_stage(site_no, start, end)      -> TimeSeries
download_ssc(site_no, start, end)        -> TimeSeries   # suspended sediment

Full calibration example
from pywmp.calibration import CalibrationEngine, ParameterBound
from pywmp.validation import download_streamflow, HydroMetrics

params = [
    ParameterBound("cn",     lo=70,  hi=98,  initial=85),
    ParameterBound("lag_hr", lo=1.0, hi=8.0, initial=4.0),
    ParameterBound("K_hr",   lo=0.3, hi=3.0, initial=1.0, log_scale=True),
]

def run_model(cn, lag_hr, K_hr):
    # build and run simulation, return TimeSeries
    ...

engine = CalibrationEngine(
    model_fn=run_model,
    observed=download_streamflow("02290000", "2019-01-01", "2020-12-31"),
    params=params,
    objective="KGE",
    method="differential_evolution",
)
results = engine.run(max_iter=400)
print(f"Best KGE: {results.best.objective_value:.3f}")
print(f"Params:   {results.best.values}")

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 ↗

Gupta, H. V., Kling, H., Yilmaz, K. K., & Martinez, G. F. (2009). Decomposition of the mean squared error and NSE performance criteria. 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., et al. (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. 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. (2023). National Water Information System (NWIS). USGS NWIS ↗

Wing, O. E. J., et al. (2017). Validation of a 30 m resolution flood hazard model of the conterminous United States. Water Resources Research, 53(9), 7968–7986. DOI ↗