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.
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.
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 --jsonIt 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.
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. backgroundrun_workflow - Container baseline (Dockerfile + docker-compose)
- Publication-ready figures (7+ types)
See:
- Documentation site — concepts, user guide, agent/MCP, and full reference
QUICKSTART.md— install, run, and operate via agents- Honest status — what the system actually claims today (0/11 research-grade)
ROADMAP.md— phased plan with checkboxesPROGRESS.md— running progress journalDECISIONS.md— architecture decision recordsdocs/AGENT_WORKFLOW.md— implemented negotiate → run → evidence flowdocs/ARCHITECTURE.md— system architecture
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-calibratedefaults to the historicalCN2,ALPHA_BFscope unless--parametersis 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_calibrationis 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_runis 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/ \
--jsonThe 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.
# 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# 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 mcpVolume 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)
# 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| 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) |
pip install "swatplus-builder[mcp]"
swat mcp-check # pre-flight: exits 0 if all imports + tools OK
swat mcp # start stdio MCP serverMCP 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-checkfails withModuleNotFoundError: No module named 'mcp', theswatentry 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 pythoninside the env wherepip 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
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.
Every run persists soil realism metadata in metadata.json:
soil_mode:high_fidelity|fallback|syntheticpct_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.
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)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(seeQUICKSTART.md); thelock-benchmark/locked-calibratecommands above remain valid lower-level primitives. For the current honest validation status across the basin suite, seedocs/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.
- 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
TxtInOutand runs calibrations. swatplus-builder builds theTxtInOut. They are complementary. - No SWAT+ engine bundled. Bring your own
swatplus_exeand mount it at runtime.
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.
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.