Skip to content

AI-Hydro/swatplus-builder

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

168 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

swatplus-builder

PyPI Python License: MIT Docs DOI

swatplus-builder: Claim-governed SWAT+ workflows with auditable evidence

A calibrated SWAT+ model from one gauge ID — and a machine-readable record of exactly what you may, and may not, claim about it.

swatplus-builder turns a single USGS streamgage ID into a built, run, and calibrated SWAT+ hydrologic model — entirely in Python, with no desktop GIS. A person or an AI agent can drive it; either way the software, not the operator, decides what each result is allowed to claim, through runtime gates, sealed provenance, fresh reproducibility reruns, withheld-period transfer checks, and explicit claim tiers.

What makes it different

Most modeling tools will happily report a number. This one reports a number and the evidence that authorizes it — and refuses to label a result "research-grade" until every gate (physical realism, soil provenance, routing closure, verified calibration) actually passes. A blocked claim is not a crash; it is classified evidence. That makes both the successes and the limitations of an automated build inspectable — the core property you need when an LLM agent, not a hydrologist, is at the controls.

Pipeline at a glance

USGS gauge ID
   └─▶ delineate ─▶ HRUs ─▶ weather + soils ─▶ SWAT+ project ─▶ engine run
          └─▶ lock benchmark ─▶ gated calibration ─▶ verified locked rerun
                 └─▶ evidence bundle  { allowed claims · blocked claims · tier }

No QGIS, PyQGIS, or the QSWATPlus plugin: GIS uses WhiteboxTools + rasterio + geopandas, and the SQLite → TxtInOut translation uses the vendored SWAT+ Editor Python API.

The canonical end-to-end path is a single command:

swat workflow run --usgs-id <id> --model-family full \
  --start 2000-01-01 --end 2019-12-31 --warmup-years 3 \
  --calibrate --claim-tier research_grade --json

It builds the model, runs the engine, locks a benchmark, runs gated diagnostic calibration, reruns the selected solution from a fresh copy, checks transfer on a withheld period, and writes a machine-readable evidence bundle with explicit allowed/blocked claims. The package — not the agent — decides what may be claimed.

📚 Full documentation: https://ai-hydro.github.io/swatplus-builder/ — concepts (claim governance, locked calibration, the evidence bundle), a user guide, the agent/MCP surface, and a CLI/Python/schema reference. New here? Start with QUICKSTART.md.


Status

Alpha, v0.7.13 — sealed locked-benchmark calibration, withheld-period claim governance, calibrated dashboard evidence, and 13-tool agent (MCP) surface.

  • Pure-Python GIS (WhiteboxTools, rasterio, geopandas)
  • Automated SWAT+ project generation
  • Weather (GridMET / synthetic)
  • USGS NWIS observed discharge fetch
  • Two-pass outlet evaluation (auto-select → strict-pin)
  • NSE / KGE / BFI metrics (evaluate_run — authoritative)
  • Locked-benchmark calibration protocol (lock → calibrate → verify)
  • pySWATPlus bridge with fail-loud diagnostics artifact
  • 13-tool MCP server (swat mcp / docker-compose mcp service), incl. background run_workflow
  • Container baseline (Dockerfile + docker-compose)
  • Publication-ready figures (7+ types)

See:


Authoritative calibration path

The lock → calibrate → verify chain is the only scientifically defensible route to reported calibration metrics.

1. swat lock-benchmark     # snapshot baseline metrics + alignment CSV
       ↓
2. swat locked-calibrate   # real-engine DDS against declared parameters
       ↓                   # (calls verify automatically unless --skip-verify)
3. metrics reported        # fresh locked rerun; workflow also checks transfer

Rules (enforced by the toolchain):

  • Standalone swat locked-calibrate defaults to the historical CN2,ALPHA_BF scope unless --parameters is supplied.
  • The governed end-to-end workflow uses basin-screened full-mode parameters before calibration. Eligible controls currently include volume/process partition parameters (PET_CO, ESCO, EPCO, CN3_SWF, CN2, LATQ_CO, PERCO), baseflow/subsurface parameters (LAT_TTIME, ALPHA_BF, RCHG_DP), and timing/channel/snow parameters (SURLAG, CH_N2, CH_K2, SFTMP, SMTMP), with dead controls excluded.
  • No silent scope expansion: candidate parameters must be declared, screened, recorded, and reproducibly rerun before claim use.
  • Calibrated metrics are always delta-reported against the locked baseline.
  • A new benchmark lock seals the alignment, metrics, outlet provenance, and static TxtInOut configuration. Calibration refuses a changed or legacy unsealed artifact until it is relocked.
  • verify_calibration is mandatory — it re-runs the best solution from a fresh copy to confirm reproducibility. The end-to-end workflow separately withholds its chronological validation period and requires that transfer gate before a calibrated claim can pass.
  • evaluate_run is the authoritative metric source for all reporting.

One-liner for agents:

swat locked-calibrate \
  --benchmark-dir artifacts/locks/usgs_01547700/benchmark \
  --base-txtinout TxtInOut/ \
  --out-dir artifacts/calibration/ \
  --json

Bridge diagnostics (non-authoritative / fail-loud)

The pySWATPlus bridge (swat calibrate --calibration-engine pyswatplus) is a secondary calibration path. When it fails, it writes a structured bridge_failure_diagnostic.json artifact (timestamp, traceback, staged file manifest, failure stage) and exits non-zero. Do not rely on raw bridge objective values for reporting — the bridge metric parity layer redirects all reported metrics through evaluate_run.

If the bridge path fails: check bridge_failure_diagnostic.json under the calibration artifacts directory. The real-engine path (swat calibrate --real-engine or swat locked-calibrate) is the currently reliable authoritative route.


Install

# Core only
pip install swatplus-builder

# With GIS stack (recommended)
pip install "swatplus-builder[gis]"

# With HyRiver helpers (USGS gauges, NHDPlus, py3dep, GridMET)
pip install "swatplus-builder[gis,hyriver]"

# Full dev environment
pip install -e ".[all]"

SWAT+ engine binary is not a pip dependency. After downloading from swat.tamu.edu/software/plus:

swat setup engine --path /path/to/swatplus_exe   # installs to ~/.swatplus_builder/bin/
swat setup engine                                 # no args: print status / download help

Container quick-start

# Build image
docker compose build

# Check runtime health (no binary mounted — expect degraded)
docker compose run --rm swat health --json

# Run with binary mounted
SWATPLUS_BIN_DIR=/path/to/swatplus_dir docker compose run --rm swat version
SWATPLUS_BIN_DIR=/path/to/swatplus_dir docker compose run --rm swat health

# Run locked-calibrate inside container (artifacts persisted to ./artifacts/)
SWATPLUS_BIN_DIR=/path/to/swatplus_dir docker compose run --rm swat \
  locked-calibrate \
  --benchmark-dir /data/artifacts/locks/usgs_01547700/benchmark \
  --base-txtinout /data/TxtInOut \
  --out-dir /data/artifacts/calibration \
  --json

# MCP stdio server (for agent connections)
SWATPLUS_BIN_DIR=/path/to/swatplus_dir docker compose run --rm mcp

Volume mounts (configured in docker-compose.yml):

  • ./artifacts/data/artifacts (persisted run/calibration artifacts)
  • $SWATPLUS_BIN_DIR/opt/swatplus (SWAT+ engine binary directory, read-only)
  • $SWATPLUS_DATASETS_DIR/data (reference datasets SQLite)

CLI (swat)

# One-time setup
swat setup engine --path /path/to/swatplus_exe   # install binary to ~/.swatplus_builder/bin/
swat setup engine                                 # show download instructions + current status

# Version with git SHA
swat version
swat version --json   # machine-readable

# Runtime health check (deterministic exit codes: 0=healthy, 1=degraded, 2=unhealthy)
swat health
swat health --json

# Full pipeline (one-liner for existing TxtInOut)
swat run --txtinout TxtInOut/ --threads 4

# Locked-benchmark protocol
swat lock-benchmark \
  --txtinout TxtInOut/ \
  --observed-csv observed.csv \
  --out-dir artifacts/locks/my_basin \
  --basin-id usgs_01547700

swat locked-calibrate \
  --benchmark-dir artifacts/locks/my_basin/benchmark \
  --base-txtinout TxtInOut/ \
  --out-dir artifacts/calibration/my_basin \
  --parameters CN2,ALPHA_BF \
  --json

# Multi-basin readiness table
swat readiness-table --locks-root artifacts/locks/ --json

# Inspect persisted run metadata
swat inspect <run_path>

# Benchmark validation over a basin suite
swat validate --basins basins/curated_v1.json

# Launch MCP server (stdio)
swat mcp

Exit-code contract

Code Meaning
0 Success
1 Runtime / engine failure (external tool failed, bridge error)
2 User / config error (bad arguments, missing required files, unknown parameters)
3 Quality gate failure (e.g., --min-improvement-nse not met)

MCP server — 13-tool surface

pip install "swatplus-builder[mcp]"
swat mcp-check           # pre-flight: exits 0 if all imports + tools OK
swat mcp                 # start stdio MCP server

MCP client config (Claude Desktop / Cursor / any MCP host) — no env vars needed when the engine is installed via swat setup engine --path:

{
  "mcpServers": {
    "swatplus-builder": {
      "command": "swat",
      "args": ["mcp"]
    }
  }
}

If you installed the engine manually via SWATPLUS_EXE instead:

{
  "mcpServers": {
    "swatplus-builder": {
      "command": "swat",
      "args": ["mcp"],
      "env": { "SWATPLUS_EXE": "/path/to/swatplus_exe" }
    }
  }
}

Mixed conda/venv? If swat mcp-check fails with ModuleNotFoundError: No module named 'mcp', the swat entry point is running under the wrong Python. Pin the interpreter explicitly:

{ "command": "/opt/miniconda3/bin/python", "args": ["-m", "swatplus_builder.mcp.server"], ... }

Find the right path: which python inside the env where pip install swatplus-builder[mcp] succeeded.

Tool tiers:

Tier 0 — Canonical governed workflow (2 tools): run_workflow (background build → run → lock → calibrate → verify → evidence bundle from one gauge ID), workflow_status (poll for completion + evidence pointers)

Tier 1 — Basin workflow (8 tools): build_project, run_basin, calibrate, propose_parameters, compare_runs, query_artifacts, diagnose_failure, validate

Tier 2 — Benchmark / readiness (3 tools): lock_benchmark, locked_calibrate, readiness_table

Teaching an agent the system

SKILL.md at the repo root is a self-contained agent skill file — when to use the system, the 13-tool catalog with signatures, the parameter registry, diagnostic heuristics, basin taxonomy, the locked-benchmark rules, and worked workflows. For Claude Code (and any skill-aware agent), point the agent at SKILL.md to bring it up to competence before it touches a tool.


Soil fidelity flags

Every run persists soil realism metadata in metadata.json:

  • soil_mode: high_fidelity | fallback | synthetic
  • pct_fallback_soils: fraction of basin polygons using fallback soil profiles

Fallback usage >25% emits a warning. Threshold configurable via SWATPLUS_SOIL_FALLBACK_WARN_THRESHOLD. Generated figures include a visible quality annotation for fallback/synthetic runs.

Use swat inspect <run_path> to view persisted metadata.


Python API

The canonical path is the CLI (swat workflow run) or MCP (run_workflow tool). For direct Python use, the locked-benchmark calibration API is the authoritative surface:

from swatplus_builder.calibration.locked_benchmark import (
    lock_benchmark,
    calibrate_against_lock,
    verify_calibration,
    build_readiness_table,
)

lock = lock_benchmark(txtinout_dir, obs_series, out_dir, basin_id="usgs_01547700", outlet_gis_id=1)
evidence = calibrate_against_lock(lock, base_txtinout, out_dir, parameters=["CN2", "ALPHA_BF"])
result = verify_calibration(lock, evidence.best_solution_json, base_txtinout, out_dir)
rows = build_readiness_table(locks_root)

Phase 3E calibration evidence baseline

As of 2026-04-25, the historical locked real-engine calibration protocol had been run and independently verified on two USGS basins using the two-parameter CN2,ALPHA_BF scope. These rows are retained as a historical baseline, not as the current governed full-mode calibration claim:

Basin Baseline NSE Calibrated NSE ΔNSE Baseline KGE Calibrated KGE ΔKGE Status
usgs_01547700 0.1256 0.2107 +0.085 0.036 0.116 +0.080 PASS
usgs_03339000 0.0618 0.3192 +0.257 -0.097 0.187 +0.284 PASS

Both basins: independently verified (re-run of best solution, not calibration-loop metrics). Evidence bundle: tests/_artifacts/phase3e_readiness/real_engine_bundle_20260425/.

Note: the current canonical path is swat workflow run (see QUICKSTART.md); the lock-benchmark / locked-calibrate commands above remain valid lower-level primitives. For the current honest validation status across the basin suite, see docs/PIPELINE_RESEARCH_GRADE_AUDIT.md.

Honest caveats: NSE < 0.5 for both basins — improvement is real and verified but absolute skill is not yet benchmark-grade. Physical realism work (soil conductivity, routing) is needed before positive-skill claims. pySWATPlus bridge is non-authoritative until bridge stability is proven.


What this package does NOT do

  • No QGIS. If you need byte-for-byte QSWATPlus parity, use QSWATPlus. We aim for numerical agreement within a few percent.
  • No pySWATPlus replacement. pySWATPlus edits an existing TxtInOut and runs calibrations. swatplus-builder builds the TxtInOut. They are complementary.
  • No SWAT+ engine bundled. Bring your own swatplus_exe and mount it at runtime.

Citation

If you use swatplus-builder in your research, please cite:

@software{galib_swatplus_builder_2026,
  author       = {Galib, Mohammad and Merwade, Venkatesh},
  title        = {{swatplus-builder: Claim-governed SWAT+ hydrologic
                   modeling from a USGS gauge ID}},
  year         = {2026},
  publisher    = {Zenodo},
  version      = {0.7.13},
  doi          = {10.5281/zenodo.20650908},
  url          = {https://doi.org/10.5281/zenodo.20650908}
}

When reporting a specific result, also cite the run's provenance hash from evidence_summary.json — a metric without its run provenance is not reproducible. See Citing & references.


License

MIT. See LICENSE.

Vendored: swat-model/swatplus-editor (Apache-2.0). The SWAT+ reference databases are not bundled or auto-downloaded — you supply them from the SWAT+ Editor desktop app (see Bootstrap). pySWATPlus is GPL-3.0 and is an optional dependency — see DECISIONS.md for the licensing posture.

Releases

Packages

Contributors

Languages