Build and calibrate SWAT+ hydrologic models from a USGS gauge ID — with evidence-backed, claim-governed results.
Project description
swatplus-builder
Calibrated SWAT+ models from a single gauge ID — with evidence you can audit.
swatplus-builder builds and calibrates SWAT+ hydrologic models in Python,
starting from one USGS streamgage ID. It can be driven by a person or by an AI
agent — and either way, the software, not the operator, decides what each
result is allowed to claim, through runtime gates, provenance, locked reruns,
and evidence-backed claim tiers.
The whole pipeline runs in Python with no desktop GIS — no QGIS, PyQGIS, or
the QSWATPlus plugin. GIS work uses WhiteboxTools + rasterio + geopandas, and
the SQLite → TxtInOut translation uses the vendored
SWAT+ Editor Python API.
📚 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? Read
QUICKSTART.md. It covers requirements, install, engine/reference-DB bootstrap, the canonical one-command workflow, and how to operate the pipeline through an AI agent (MCP).
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, independently verifies a locked rerun, and writes a machine-readable evidence bundle with explicit allowed/blocked claims. The package — not the agent — decides what may be claimed.
Status
Alpha, v0.4.0 — locked-benchmark calibration, 13-tool agent (MCP) surface, container baseline.
- 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
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 on CN2 + ALPHA_BF only
↓ # (calls verify automatically unless --skip-verify)
3. metrics reported # delta NSE/KGE vs locked baseline, independently verified
Rules (enforced by the toolchain):
- Parameters are restricted to
CN2andALPHA_BF— no silent scope expansion. - Calibrated metrics are always delta-reported against the locked baseline.
verify_calibrationis mandatory — it re-runs the best solution independently to confirm reproducibility.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/ \
--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 — bring your own swatplus_exe and mount it at runtime.
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)
# 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):
{
"mcpServers": {
"swatplus-builder": {
"command": "swat",
"args": ["mcp"],
"env": {
"SWATPLUS_EXE": "/usr/local/bin/swatplus_exe",
"SWATPLUS_BUILDER_ARTIFACTS": "/data/artifacts"
}
}
}
}
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
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|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.
Agent-facing API
from swatplus_builder.tools import (
build_watershed,
create_hrus,
generate_swat_project,
run_swat,
)
ws = build_watershed(
dem_path="data/dem.tif",
outlet=(-77.123, 41.456),
stream_threshold_cells=500,
workdir="runs/marsh_creek/",
)
hrus = create_hrus(ws, "data/nlcd_2019.tif", "data/gnatsgo_mukey.tif")
project = generate_swat_project(ws, hrus, "data/weather/", "2000-01-01", "2010-12-31", "marsh_creek_v1")
result = run_swat(project, threads=4)
Locked-benchmark API (for direct Python use):
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 locked real-engine calibration protocol has been run and independently verified on two USGS basins (CN2 + ALPHA_BF only, real-engine DDS):
| 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.
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
TxtInOutand runs calibrations. swatplus-builder builds theTxtInOut. They are complementary. - No SWAT+ engine bundled. Bring your own
swatplus_exeand 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.4.0},
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). Reference databases downloaded at install time from the ai-hydro/swatplus-reference-data mirror. pySWATPlus is GPL-3.0 and is an optional dependency — see DECISIONS.md for the licensing posture.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file swatplus_builder-0.6.3.tar.gz.
File metadata
- Download URL: swatplus_builder-0.6.3.tar.gz
- Upload date:
- Size: 2.9 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d3e9ddcdbac4da26586beb4981ea7e24b1bbd9d71521b6e47a3f07a47f1e234b
|
|
| MD5 |
4f3d697f83c606e7237af097335fcf9e
|
|
| BLAKE2b-256 |
d57ad2693216ec595c9764c5e0d88b8b1abd319f0d524ff53b1834585ee8713d
|
File details
Details for the file swatplus_builder-0.6.3-py3-none-any.whl.
File metadata
- Download URL: swatplus_builder-0.6.3-py3-none-any.whl
- Upload date:
- Size: 2.9 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd46e831e7b9eca6e2d45f694537c9ff6a84a8fbc3ac74d993005a9431ef8ab8
|
|
| MD5 |
db93558d920a8c01bbd1ef890d9b0ed2
|
|
| BLAKE2b-256 |
ff4fc0b7687af759b65e6e8d28226a81f1378369e4d1880e5deb31b355bc87b0
|