Metadata-Version: 2.4
Name: swatplus-builder
Version: 0.7.5
Summary: Build and calibrate SWAT+ hydrologic models from a USGS gauge ID — with evidence-backed, claim-governed results.
Project-URL: Homepage, https://github.com/AI-Hydro/swatplus-builder
Project-URL: Documentation, https://ai-hydro.github.io/swatplus-builder/
Project-URL: Issues, https://github.com/AI-Hydro/swatplus-builder/issues
Project-URL: Changelog, https://github.com/AI-Hydro/swatplus-builder/blob/main/CHANGELOG.md
Author: Mohammad Galib
License: MIT
License-File: LICENSE
Keywords: calibration,claim-governance,gis,hru,hydrology,mcp,swat+,swatplus,watershed
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Topic :: Scientific/Engineering :: Hydrology
Requires-Python: <3.14,>=3.10
Requires-Dist: networkx>=3.1
Requires-Dist: numpy>=1.24
Requires-Dist: pandas>=2.0
Requires-Dist: peewee>=3.17
Requires-Dist: pydantic>=2.5
Requires-Dist: rich>=13.7
Requires-Dist: typer>=0.12
Provides-Extra: all
Requires-Dist: adlfs>=2024.2; extra == 'all'
Requires-Dist: exactextract>=0.2; extra == 'all'
Requires-Dist: fiona>=1.9; extra == 'all'
Requires-Dist: fsspec>=2024.2; extra == 'all'
Requires-Dist: geopandas>=0.14; extra == 'all'
Requires-Dist: matplotlib<4.0,>=3.7; extra == 'all'
Requires-Dist: mcp>=1.2; extra == 'all'
Requires-Dist: mkdocs-material>=9.5; extra == 'all'
Requires-Dist: mkdocs>=1.6; extra == 'all'
Requires-Dist: mkdocstrings[python]>=0.25; extra == 'all'
Requires-Dist: mypy>=1.10; extra == 'all'
Requires-Dist: planetary-computer>=1.0; extra == 'all'
Requires-Dist: pre-commit>=3.7; extra == 'all'
Requires-Dist: py3dep>=0.16; extra == 'all'
Requires-Dist: pyarrow>=15; extra == 'all'
Requires-Dist: pydaymet>=0.16; extra == 'all'
Requires-Dist: pyflwdir>=0.5; extra == 'all'
Requires-Dist: pygeohydro>=0.16; extra == 'all'
Requires-Dist: pygeoogc>=0.16; extra == 'all'
Requires-Dist: pygridmet>=0.16; extra == 'all'
Requires-Dist: pymoo>=0.6.1; extra == 'all'
Requires-Dist: pynhd>=0.16; extra == 'all'
Requires-Dist: pyproj>=3.6; extra == 'all'
Requires-Dist: pystac-client>=0.7; extra == 'all'
Requires-Dist: pyswatplus>=1.3.0; extra == 'all'
Requires-Dist: pytest-cov>=5.0; extra == 'all'
Requires-Dist: pytest>=8.0; extra == 'all'
Requires-Dist: rasterio>=1.3; extra == 'all'
Requires-Dist: rasterstats>=0.20; extra == 'all'
Requires-Dist: ruff>=0.5; extra == 'all'
Requires-Dist: salib>=1.5.0; extra == 'all'
Requires-Dist: shapely>=2.0; extra == 'all'
Requires-Dist: whitebox>=2.3; extra == 'all'
Provides-Extra: dev
Requires-Dist: matplotlib<4.0,>=3.7; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pre-commit>=3.7; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
Requires-Dist: mkdocs>=1.6; extra == 'docs'
Requires-Dist: mkdocstrings[python]>=0.25; extra == 'docs'
Provides-Extra: gis
Requires-Dist: exactextract>=0.2; extra == 'gis'
Requires-Dist: fiona>=1.9; extra == 'gis'
Requires-Dist: geopandas>=0.14; extra == 'gis'
Requires-Dist: pyflwdir>=0.5; extra == 'gis'
Requires-Dist: pyproj>=3.6; extra == 'gis'
Requires-Dist: rasterio>=1.3; extra == 'gis'
Requires-Dist: rasterstats>=0.20; extra == 'gis'
Requires-Dist: shapely>=2.0; extra == 'gis'
Requires-Dist: whitebox>=2.3; extra == 'gis'
Provides-Extra: gridmet
Requires-Dist: pygridmet>=0.16; extra == 'gridmet'
Provides-Extra: hyriver
Requires-Dist: py3dep>=0.16; extra == 'hyriver'
Requires-Dist: pydaymet>=0.16; extra == 'hyriver'
Requires-Dist: pygeohydro>=0.16; extra == 'hyriver'
Requires-Dist: pygeoogc>=0.16; extra == 'hyriver'
Requires-Dist: pynhd>=0.16; extra == 'hyriver'
Provides-Extra: mcp
Requires-Dist: mcp>=1.2; extra == 'mcp'
Provides-Extra: soils
Requires-Dist: adlfs>=2024.2; extra == 'soils'
Requires-Dist: fsspec>=2024.2; extra == 'soils'
Requires-Dist: planetary-computer>=1.0; extra == 'soils'
Requires-Dist: pyarrow>=15; extra == 'soils'
Requires-Dist: pystac-client>=0.7; extra == 'soils'
Provides-Extra: swatplus
Requires-Dist: pymoo>=0.6.1; extra == 'swatplus'
Requires-Dist: pyswatplus>=1.3.0; extra == 'swatplus'
Requires-Dist: salib>=1.5.0; extra == 'swatplus'
Provides-Extra: viz
Requires-Dist: matplotlib<4.0,>=3.7; extra == 'viz'
Description-Content-Type: text/markdown

# swatplus-builder

[![PyPI](https://img.shields.io/pypi/v/swatplus-builder)](https://pypi.org/project/swatplus-builder/)
[![Python](https://img.shields.io/pypi/pyversions/swatplus-builder)](https://pypi.org/project/swatplus-builder/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Docs](https://img.shields.io/badge/docs-ai--hydro.github.io-blue)](https://ai-hydro.github.io/swatplus-builder/)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.20650908.svg)](https://doi.org/10.5281/zenodo.20650908)

<p align="center">
  <img src="assets/images/swatplus-builder-readme-thumbnail.png" alt="swatplus-builder: Claim-governed SWAT+ workflows with auditable evidence" width="100%">
</p>

> **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](https://github.com/swat-model/swatplus-editor).

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

```bash
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`](QUICKSTART.md).

---

## Status

**Alpha, v0.7.4** — locked-benchmark calibration, 13-tool agent (MCP) surface, container baseline.

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

See:

- **[Documentation site](https://ai-hydro.github.io/swatplus-builder/)** — concepts, user guide, agent/MCP, and full reference
- [`QUICKSTART.md`](QUICKSTART.md) — install, run, and operate via agents
- [Honest status](https://ai-hydro.github.io/swatplus-builder/project/status/) — what the system actually claims today (0/11 research-grade)
- [`ROADMAP.md`](ROADMAP.md) — phased plan with checkboxes
- [`PROGRESS.md`](PROGRESS.md) — running progress journal
- [`DECISIONS.md`](DECISIONS.md) — architecture decision records
- [`docs/AGENT_WORKFLOW.md`](docs/AGENT_WORKFLOW.md) — implemented negotiate → run → evidence flow
- [`docs/ARCHITECTURE.md`](docs/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 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:
```bash
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

```bash
# 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](https://swat.tamu.edu/software/plus/):
```bash
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

```bash
# 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`)

```bash
# 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

```bash
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`:

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

If you installed the engine manually via `SWATPLUS_EXE` instead:

```json
{
  "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:
> ```json
> { "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`](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:

```python
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`](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`](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:

```bibtex
@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.4},
  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](https://ai-hydro.github.io/swatplus-builder/project/citing/).

---

## License

MIT. See [LICENSE](LICENSE).

Vendored: [`swat-model/swatplus-editor`](https://github.com/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](https://ai-hydro.github.io/swatplus-builder/getting-started/bootstrap/)). pySWATPlus is GPL-3.0 and is an optional dependency — see `DECISIONS.md` for the licensing posture.
