Skip to content

Validation and Calibration Module 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
    alpha: float
    beta:  float

Suspended sediment rating curve

from pywmp.validation import download_ssc, fit_rating_curve, estimate_annual_load_rating

# Download observed SSC from USGS
ssc = download_ssc("02301500", start="2020-01-01", end="2023-12-31")
q   = download_streamflow("02301500", start="2020-01-01", end="2023-12-31")

# Fit log-linear rating curve: SSC = a × Q^b
curve = fit_rating_curve(q=q, ssc=ssc)
print(f"Rating curve: SSC = {curve.a:.3f} × Q^{curve.b:.3f}")

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

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)
# Download, pair, score
mv = ModelVsObserved(GaugeStation("02290000", start="2021-06-01", end="2021-09-30"), sim_q)
m = mv.metrics()
print(m.nse(), m.kge(), m.rating())

Gauge data is downloaded via USGS Water Services REST API (USGS, 2023c), 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 (FEMA, 2023) typically yields CSI ≈ 0.25–0.55 for bathtub-style simulations in flat coastal terrain, consistent with the findings of Wing et al. (2017) for 30m national-scale models.

For high-resolution (1–10m) HLL simulations, CSI > 0.6 indicates good agreement with SAR-derived extents, following the benchmarks in 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

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 Class Reference Notes
Differential evolution CalibrationEngine default Storn & Price, 1997 Global; recommended for >3 parameters
SCE-UA method='sce_ua' Duan et al., 1992 Classic hydrology calibration; 3–15 parameters
Nelder-Mead simplex method='nelder_mead' Nelder & Mead, 1965 Local; use for final refinement near optimum

Differential evolution (Storn & Price, 1997) is recommended as the default because it requires no gradient information and is robust to multi-modal response surfaces, which are common in lumped hydrologic parameter spaces (Duan et al., 1992).

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)

Defines search bounds for a single calibration parameter.

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₁₀ space (useful for orders-of-magnitude parameters such as Ks)

ParameterSet

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

CalibrationResults

@dataclass
class CalibrationResults:
    best: ParameterSet
    all_trials: list[ParameterSet]   # full population history
    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 from USGS Water Services REST API (USGS, 2023c) on first access.


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

Usage example

from pywmp.calibration import CalibrationEngine, ParameterBound, ParameterSet
from pywmp.validation import HydroMetrics, ModelVsObserved, GaugeStation

# --- Define parameter search space ---
params = [
    ParameterBound("cn",        lo=70, hi=98,   initial=85),    # SCS Curve Number
    ParameterBound("initial_k", lo=0.10, hi=0.60, initial=0.30),  # Manning k (lag)
    ParameterBound("lag_hr",    lo=1.0, hi=8.0,  initial=4.0),
]

# --- Calibrate against USGS gauge ---
def run_model(cn, initial_k, lag_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",                      # Gupta et al. (2009)
    method="differential_evolution",      # Storn & Price (1997)
)
results = engine.run(max_iter=400)
print(f"Best KGE: {results.best.objective_value:.3f}")
print(f"Rating:   {HydroMetrics(...).rating()}")  # Moriasi et al. (2007)

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: 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 ↗