Skip to main content

Build and calibrate SWAT+ hydrologic models from a USGS gauge ID — with evidence-backed, claim-governed results.

Project description

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, provenance hashes, independently verified reruns, 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, 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.

📚 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.12 — locked-benchmark calibration, claim-governed diagnostic calibration, 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        # delta NSE/KGE vs locked baseline, independently verified

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 independently verified before claim use.
  • Calibrated metrics are always delta-reported against the locked baseline.
  • verify_calibration is mandatory — it re-runs the best solution independently to confirm reproducibility.
  • 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.12},
  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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

swatplus_builder-0.7.12.tar.gz (3.0 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

swatplus_builder-0.7.12-py3-none-any.whl (2.9 MB view details)

Uploaded Python 3

File details

Details for the file swatplus_builder-0.7.12.tar.gz.

File metadata

  • Download URL: swatplus_builder-0.7.12.tar.gz
  • Upload date:
  • Size: 3.0 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for swatplus_builder-0.7.12.tar.gz
Algorithm Hash digest
SHA256 aad5ba9ce697e40ad8a80d3b9c749227e59dbec00a95e53b32a4666d996bc468
MD5 0630e8e654f1c245b57a05e6f82572b0
BLAKE2b-256 3ae912ed1341ef967baccccad9db8c8707c1e365aab0b51c9db08f05a0e128d6

See more details on using hashes here.

File details

Details for the file swatplus_builder-0.7.12-py3-none-any.whl.

File metadata

File hashes

Hashes for swatplus_builder-0.7.12-py3-none-any.whl
Algorithm Hash digest
SHA256 968c774a347960731d9804b77575c4dbf247e1692d0e25b1f6960018c88bd8f4
MD5 f6b272506457427b80a5c0f4133c4b51
BLAKE2b-256 afe0dd453c2ad2dc05aebf1780e26f6d2aaac61d089ef0aa3524ae2372b7a50c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page