basinledger 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- basinledger-0.1.0/.gitignore +8 -0
- basinledger-0.1.0/CHANGELOG.md +20 -0
- basinledger-0.1.0/CITATION.cff +38 -0
- basinledger-0.1.0/LICENSE +28 -0
- basinledger-0.1.0/PKG-INFO +262 -0
- basinledger-0.1.0/README.md +229 -0
- basinledger-0.1.0/docs/design.md +604 -0
- basinledger-0.1.0/docs/figures/synthetic-depletion.png +0 -0
- basinledger-0.1.0/docs/figures/synthetic-drawdown.png +0 -0
- basinledger-0.1.0/docs/figures/synthetic-recharge.png +0 -0
- basinledger-0.1.0/docs/prior-art.md +156 -0
- basinledger-0.1.0/docs/requests.md +68 -0
- basinledger-0.1.0/examples/README.md +14 -0
- basinledger-0.1.0/examples/plaster-creek/README.md +72 -0
- basinledger-0.1.0/examples/plaster-creek/basin.toml +35 -0
- basinledger-0.1.0/examples/plaster-creek/calibration/record.toml +44 -0
- basinledger-0.1.0/examples/plaster-creek/campus.yaml +3 -0
- basinledger-0.1.0/examples/plaster-creek/check_calibration.py +69 -0
- basinledger-0.1.0/examples/plaster-creek/fetch.py +42 -0
- basinledger-0.1.0/examples/synthetic/README.md +49 -0
- basinledger-0.1.0/examples/synthetic/basin.toml +32 -0
- basinledger-0.1.0/examples/synthetic/fetch.py +45 -0
- basinledger-0.1.0/examples/synthetic/mill.toml +5 -0
- basinledger-0.1.0/examples/synthetic/model.py +72 -0
- basinledger-0.1.0/examples/synthetic/notebook.py +146 -0
- basinledger-0.1.0/examples/synthetic/small_user.toml +3 -0
- basinledger-0.1.0/examples/synthetic/surface.toml +71 -0
- basinledger-0.1.0/examples/synthetic/two_way.toml +35 -0
- basinledger-0.1.0/examples/synthetic/user.toml +6 -0
- basinledger-0.1.0/pyproject.toml +64 -0
- basinledger-0.1.0/src/basinledger/__init__.py +7 -0
- basinledger-0.1.0/src/basinledger/__main__.py +66 -0
- basinledger-0.1.0/src/basinledger/calibration.py +227 -0
- basinledger-0.1.0/src/basinledger/chain.py +106 -0
- basinledger-0.1.0/src/basinledger/config.py +214 -0
- basinledger-0.1.0/src/basinledger/coupling/__init__.py +0 -0
- basinledger-0.1.0/src/basinledger/coupling/exchange.py +83 -0
- basinledger-0.1.0/src/basinledger/coupling/modflow6.py +181 -0
- basinledger-0.1.0/src/basinledger/coupling/nextgen.py +149 -0
- basinledger-0.1.0/src/basinledger/coupling/recharge.py +43 -0
- basinledger-0.1.0/src/basinledger/coupling/swatplus.py +146 -0
- basinledger-0.1.0/src/basinledger/driver.py +420 -0
- basinledger-0.1.0/src/basinledger/forcing.py +66 -0
- basinledger-0.1.0/src/basinledger/hydrofabric.py +26 -0
- basinledger-0.1.0/src/basinledger/ledger.py +89 -0
- basinledger-0.1.0/src/basinledger/mapping.py +85 -0
- basinledger-0.1.0/src/basinledger/routes.py +120 -0
- basinledger-0.1.0/src/basinledger/rules.py +86 -0
- basinledger-0.1.0/src/basinledger/testing.py +188 -0
- basinledger-0.1.0/src/basinledger/units.py +35 -0
- basinledger-0.1.0/src/basinledger/users.py +226 -0
- basinledger-0.1.0/tests/conftest.py +43 -0
- basinledger-0.1.0/tests/test_calibration.py +69 -0
- basinledger-0.1.0/tests/test_chain.py +48 -0
- basinledger-0.1.0/tests/test_cli.py +45 -0
- basinledger-0.1.0/tests/test_config.py +41 -0
- basinledger-0.1.0/tests/test_exchange.py +100 -0
- basinledger-0.1.0/tests/test_mapping.py +56 -0
- basinledger-0.1.0/tests/test_modflow6.py +62 -0
- basinledger-0.1.0/tests/test_nextgen.py +243 -0
- basinledger-0.1.0/tests/test_one_way.py +130 -0
- basinledger-0.1.0/tests/test_package.py +61 -0
- basinledger-0.1.0/tests/test_restart.py +46 -0
- basinledger-0.1.0/tests/test_rules.py +34 -0
- basinledger-0.1.0/tests/test_surface.py +93 -0
- basinledger-0.1.0/tests/test_users.py +141 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-11-04)
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- A daily driver for coupled models through BMI, with a ledger that enters every transfer as sent and as received
|
|
8
|
+
and stops a run on any day they differ.
|
|
9
|
+
- Surface models: SWAT+ live through swatplus-bmi, or a finished NextGen (NGIAB) run replayed day by day.
|
|
10
|
+
- MODFLOW 6 through libmf6 and modflowapi: SWAT+ recharge in, wells, and the two-way stream-aquifer exchange through
|
|
11
|
+
the river package, with SWAT+'s aquifer passing recharge through.
|
|
12
|
+
- The water-user interface: any BMI component reporting withdrawal, consumption and return (and optionally gain,
|
|
13
|
+
storage, loads, return temperature, electricity), integrated from hourly to daily, its balance checked every step.
|
|
14
|
+
CoolingWaterBmi plugs in by role.
|
|
15
|
+
- Routes (wells, SWAT+ intakes and point sources, outfalls, offsite), utility chains, and feedback rules
|
|
16
|
+
(curtailment, dry mode, drawdown limit).
|
|
17
|
+
- Restarts from a saved day, matching unbroken runs bit for bit.
|
|
18
|
+
- Calibration records per model, statistics, and a forward model; applying parameters and PEST++ are planned.
|
|
19
|
+
- Examples: synthetic (Osu_1hru with small MODFLOW 6 models) and Plaster Creek, Michigan (calibrated NextGen
|
|
20
|
+
streamflow).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use Basin Ledger in your work, please cite it as below."
|
|
3
|
+
title: "Basin Ledger: a basin's water balanced day by day across coupled models (SWAT+, NextGen, MODFLOW 6 and water users) through BMI"
|
|
4
|
+
type: software
|
|
5
|
+
authors:
|
|
6
|
+
- family-names: Sommerlot
|
|
7
|
+
given-names: Andrew
|
|
8
|
+
affiliation: Northline Hydrology
|
|
9
|
+
version: 0.1.0
|
|
10
|
+
date-released: 2026-11-04
|
|
11
|
+
license: BSD-3-Clause
|
|
12
|
+
repository-code: "https://github.com/northlinehydrology/basin-ledger"
|
|
13
|
+
keywords:
|
|
14
|
+
- hydrology
|
|
15
|
+
- groundwater-surface water interaction
|
|
16
|
+
- model coupling
|
|
17
|
+
- Basic Model Interface
|
|
18
|
+
- SWAT+
|
|
19
|
+
- MODFLOW 6
|
|
20
|
+
- NextGen
|
|
21
|
+
- water use
|
|
22
|
+
references:
|
|
23
|
+
- type: software
|
|
24
|
+
title: "swatplus-bmi: stepping the SWAT+ watershed model one day at a time through the Basic Model Interface"
|
|
25
|
+
authors:
|
|
26
|
+
- family-names: Sommerlot
|
|
27
|
+
given-names: Andrew
|
|
28
|
+
doi: 10.5281/zenodo.23963201
|
|
29
|
+
- type: article
|
|
30
|
+
title: "The MODFLOW Application Programming Interface for simulation control and software interoperability"
|
|
31
|
+
authors:
|
|
32
|
+
- family-names: Hughes
|
|
33
|
+
given-names: Joseph D.
|
|
34
|
+
- name: "and others"
|
|
35
|
+
journal: "Environmental Modelling & Software"
|
|
36
|
+
volume: 148
|
|
37
|
+
year: 2022
|
|
38
|
+
doi: 10.1016/j.envsoft.2021.105257
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Andrew Sommerlot
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: basinledger
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A basin's water balanced day by day across coupled models (SWAT+, NextGen, MODFLOW 6 and water users) through BMI
|
|
5
|
+
Project-URL: Homepage, https://github.com/northlinehydrology/basin-ledger
|
|
6
|
+
Project-URL: Documentation, https://github.com/northlinehydrology/basin-ledger/blob/main/docs/design.md
|
|
7
|
+
Project-URL: Issues, https://github.com/northlinehydrology/basin-ledger/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/northlinehydrology/basin-ledger/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Northline Hydrology <info@northlinehydrology.com>
|
|
10
|
+
License-Expression: BSD-3-Clause
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: BMI,MODFLOW 6,NextGen,SWAT+,groundwater,hydrology,model coupling,water use
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Hydrology
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: bmipy>=2.0
|
|
23
|
+
Requires-Dist: flopy>=3.9
|
|
24
|
+
Requires-Dist: modflowapi>=0.2
|
|
25
|
+
Requires-Dist: numpy>=1.26
|
|
26
|
+
Requires-Dist: swatplus-bmi>=0.1.1
|
|
27
|
+
Provides-Extra: nextgen
|
|
28
|
+
Requires-Dist: netcdf4<1.7.5,>=1.6; extra == 'nextgen'
|
|
29
|
+
Provides-Extra: users
|
|
30
|
+
Requires-Dist: pyyaml>=6; extra == 'users'
|
|
31
|
+
Requires-Dist: waterledger-models>=3.9; extra == 'users'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# Basin Ledger
|
|
35
|
+
|
|
36
|
+
A basin's water, quantity and quality, balanced day by day across coupled models.
|
|
37
|
+
|
|
38
|
+
The code was written with AI assistance (Claude, by Anthropic), designed and directed by Andrew Sommerlot. See
|
|
39
|
+
[AI use](#ai-use).
|
|
40
|
+
|
|
41
|
+
Basin Ledger couples a watershed model, a groundwater model and the models of large water users through BMI, and
|
|
42
|
+
steps them together one day at a time with one Python driver:
|
|
43
|
+
|
|
44
|
+
- **SWAT+** through [swatplus-bmi](https://github.com/northlinehydrology/swatplus-bmi)
|
|
45
|
+
([doi:10.5281/zenodo.23963201](https://doi.org/10.5281/zenodo.23963201)), which runs stock SWAT+ 62.0.1 under
|
|
46
|
+
outside control;
|
|
47
|
+
- **NextGen**, as finished NextGen In A Box runs replayed day by day, with results keyed to hydrofabric ids;
|
|
48
|
+
- **MODFLOW 6** through its own BMI/XMI (`libmf6`), from Python with USGS's
|
|
49
|
+
[modflowapi](https://github.com/MODFLOW-ORG/modflowapi);
|
|
50
|
+
- **water users** as BMI components from
|
|
51
|
+
[waterledger-models](https://github.com/northlinehydrology/waterledger), starting with data center cooling
|
|
52
|
+
(`CoolingWaterBmi`).
|
|
53
|
+
|
|
54
|
+
Every volume and mass that moves between models is entered in a ledger twice, as sent and as received. A day where
|
|
55
|
+
they differ stops the run. Basin Ledger is a sibling of [Water Ledger](https://waterledger.org). Water Ledger
|
|
56
|
+
estimates one campus's water from public data. Basin Ledger follows that water through a basin's streams and aquifers.
|
|
57
|
+
|
|
58
|
+
**Status: early (0.1.0).** Built and tested:
|
|
59
|
+
|
|
60
|
+
- the water-user interface, with controls a rule can set (a withdrawal limit, dry mode);
|
|
61
|
+
- routes: MODFLOW 6 wells, SWAT+ water allocation intakes and point sources, outfalls, water from and to outside the
|
|
62
|
+
basin;
|
|
63
|
+
- the utility chain: sources, losses, sewer, plant outfall;
|
|
64
|
+
- feedback rules: curtailment, dry mode, drawdown limit;
|
|
65
|
+
- SWAT+ recharge into MODFLOW 6 (one-way), and the two-way stream-aquifer exchange through MODFLOW 6's river package;
|
|
66
|
+
- NextGen as the surface model: a finished NGIAB run replayed day by day, with users on its own hourly forcing;
|
|
67
|
+
- restarts from a saved day that match unbroken runs bit for bit;
|
|
68
|
+
- the ledger and the command line;
|
|
69
|
+
- calibration records per model, and the calibration module's structure. An example is a screening run unless every
|
|
70
|
+
model it runs is calibrated.
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
|
|
74
|
+
- **synthetic:** SWAT+'s Osu_1hru with small MODFLOW 6 models, for the tests;
|
|
75
|
+
- **[Plaster Creek](examples/plaster-creek/), Michigan:** calibrated NextGen streamflow at USGS 04119055 (KGE 0.87
|
|
76
|
+
calibration, 0.81 validation, from [nextgen-michigan](https://github.com/northlinehydrology/nextgen-michigan)),
|
|
77
|
+
with an assumed campus on utility water. Only the streamflow at the gage is calibrated.
|
|
78
|
+
|
|
79
|
+
Still to come ([docs/design.md](docs/design.md)):
|
|
80
|
+
|
|
81
|
+
- SWAT+ built on the NextGen hydrofabric, for water quality on the same catchments;
|
|
82
|
+
- MODFLOW 6 recharge from NextGen (CFE's percolation);
|
|
83
|
+
- AORC forcing;
|
|
84
|
+
- applying calibration parameters, and PEST++ IES.
|
|
85
|
+
|
|
86
|
+
## How it works
|
|
87
|
+
|
|
88
|
+
```mermaid
|
|
89
|
+
flowchart LR
|
|
90
|
+
U["water users (BMI)<br/>hourly: withdrawal, consumption,<br/>return, loads, power"] -- "day's volumes" --> D
|
|
91
|
+
D["driver<br/>one day at a time"] -- "update()" --> S["SWAT+<br/>(swatplus-bmi)"]
|
|
92
|
+
S -- "aquifer recharge, m/s" --> D
|
|
93
|
+
D -- "recharge by cell, well rates<br/>(set between prepare and solve)" --> M["MODFLOW 6<br/>(libmf6, modflowapi)"]
|
|
94
|
+
M -- "simulated flows, heads" --> D
|
|
95
|
+
D --> L[("ledger<br/>sent = received,<br/>every day")]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
A NextGen run takes SWAT+'s place as a replay. It gives calibrated flows on NextGen geography, but without feedback
|
|
99
|
+
(see [docs/design.md](docs/design.md#surface-models-swat-or-nextgen)).
|
|
100
|
+
|
|
101
|
+
Places are examples, not code. Each example (`examples/<place>/`) is one config file that wires real models for one
|
|
102
|
+
basin and its users, plus a script that fetches its public data and a short write-up. The engine knows nothing about
|
|
103
|
+
any place or any kind of water user.
|
|
104
|
+
|
|
105
|
+
## Install
|
|
106
|
+
|
|
107
|
+
Needs Python 3.11 or later, and gfortran for SWAT+ (on macOS, Homebrew's `gcc`).
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install "basinledger[users,nextgen]" # or: uv add "basinledger[users,nextgen]"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The extras:
|
|
114
|
+
|
|
115
|
+
- **`users`:** the water-user models from `waterledger-models` (`CoolingWaterBmi`);
|
|
116
|
+
- **`nextgen`:** netCDF4, for reading NextGen runs.
|
|
117
|
+
|
|
118
|
+
Two model programs aren't Python packages, and come separately:
|
|
119
|
+
|
|
120
|
+
- **SWAT+** is built by swatplus-bmi, which is installed with Basin Ledger. It fetches SWAT+ 62.0.1, patches it and
|
|
121
|
+
builds it, in a few minutes:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
swatplus-bmi build
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- **MODFLOW 6's shared library** (`libmf6`) comes from USGS's release through flopy's `get-modflow`, also installed
|
|
128
|
+
with Basin Ledger. Basin Ledger looks for it where `:flopy` puts it, or at `$LIBMF6`:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
get-modflow :flopy --repo modflow6 --subset mf6,libmf6.dylib # libmf6.so on Linux
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
From a source checkout, for development and the tests, with [uv](https://docs.astral.sh/uv/):
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
git clone https://github.com/northlinehydrology/basin-ledger && cd basin-ledger
|
|
138
|
+
uv sync
|
|
139
|
+
uv run python examples/synthetic/fetch.py --get # builds SWAT+ (swatplus-bmi build) and fetches libmf6 (get-modflow)
|
|
140
|
+
uv run pytest # about a minute on one core; -m "not slow" takes about 10 s
|
|
141
|
+
uv run --group examples marimo edit examples/synthetic/notebook.py
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Continuous testing:
|
|
145
|
+
|
|
146
|
+
- **On every push:** CI runs the whole suite on Ubuntu and macOS (`.github/workflows/test.yml`).
|
|
147
|
+
- **Every night:** a run checks against the newest releases of swatplus-bmi, waterledger-models, flopy and
|
|
148
|
+
MODFLOW 6 (`nightly.yml`).
|
|
149
|
+
|
|
150
|
+
## Use
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
uv run python -m basinledger run examples/synthetic/surface.toml # writes ledger.csv, series.csv, summary.json
|
|
154
|
+
uv run python -m basinledger run examples/synthetic/surface.toml --baseline # the same without its water users
|
|
155
|
+
uv run python -m basinledger run examples/synthetic/two_way.toml --save-at 365 --save runs/warm
|
|
156
|
+
uv run python -m basinledger run examples/synthetic/two_way.toml --restart runs/warm --run scenario
|
|
157
|
+
uv run python -m basinledger status examples/synthetic/surface.toml # calibrated, or a screening run
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Or from Python:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from basinledger.driver import Basin
|
|
164
|
+
|
|
165
|
+
b = Basin.from_config("examples/synthetic/basin.toml", run="pumping")
|
|
166
|
+
b.run() # every day of the SWAT+ project; the ledger checks each day
|
|
167
|
+
print(b.ledger.totals()) # m3 (and kg, MWh) per account for the whole run
|
|
168
|
+
b.finalize()
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
An example's config (abridged; the full schema is in `src/basinledger/config.py`):
|
|
172
|
+
|
|
173
|
+
```toml
|
|
174
|
+
[swatplus]
|
|
175
|
+
project = "{swatplus_refdata}/Osu_1hru"
|
|
176
|
+
|
|
177
|
+
[modflow6]
|
|
178
|
+
builder = "model:build" # model.py writes the MODFLOW 6 simulation for the run's days
|
|
179
|
+
|
|
180
|
+
[recharge]
|
|
181
|
+
map = "all:aqu001" # SWAT+ aquifers to MODFLOW 6 cells by area, or a CSV map
|
|
182
|
+
|
|
183
|
+
[[wells]]
|
|
184
|
+
name = "pw1"
|
|
185
|
+
cell = [0, 20, 28]
|
|
186
|
+
|
|
187
|
+
[[utilities]]
|
|
188
|
+
name = "city"
|
|
189
|
+
sources = [{route = "well:pw1", share = 1.0}]
|
|
190
|
+
losses = 0.10
|
|
191
|
+
outfall = "swat:point_source:wwtp"
|
|
192
|
+
|
|
193
|
+
[[users]]
|
|
194
|
+
name = "plant"
|
|
195
|
+
component = "basinledger.testing:SyntheticUser" # or waterledger_models.datacenter.bmi:CoolingWaterBmi
|
|
196
|
+
withdrawal = "utility:city"
|
|
197
|
+
return = "utility:city"
|
|
198
|
+
|
|
199
|
+
[[rules]]
|
|
200
|
+
name = "plant well limit"
|
|
201
|
+
kind = "drawdown_limit"
|
|
202
|
+
user = "plant"
|
|
203
|
+
cell = [0, 20, 28]
|
|
204
|
+
head_min = 88.3
|
|
205
|
+
limit = 0.002
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+

|
|
209
|
+
|
|
210
|
+
*The synthetic example: SWAT+'s Osu_1hru recharge drives a small MODFLOW 6 aquifer, and a synthetic user pumps one
|
|
211
|
+
well. Drawdown is the difference from the same run without the user.*
|
|
212
|
+
|
|
213
|
+
## Water users
|
|
214
|
+
|
|
215
|
+
Any BMI component that reports its withdrawal, consumption and return can be a water user. Its gain, storage,
|
|
216
|
+
constituent loads, return temperature and electricity are optional. A component that uses its own variable names is
|
|
217
|
+
mapped role by role in the config. The engine checks each user's balance every step:
|
|
218
|
+
withdrawal + gain = consumption + return + change in storage. It then integrates the hourly rates into the daily
|
|
219
|
+
volumes the hydrologic models take. See [docs/design.md](docs/design.md#water-users).
|
|
220
|
+
|
|
221
|
+
## What the tests check
|
|
222
|
+
|
|
223
|
+
| What | How |
|
|
224
|
+
|---|---|
|
|
225
|
+
| Hourly to daily | daily volumes equal hourly rates integrated by hand; masses summed, temperature flow-weighted |
|
|
226
|
+
| A user's balance | closes every hour; a component that loses water is refused |
|
|
227
|
+
| Any component plugs in | a renamed component with other units, and `CoolingWaterBmi`, mapped by role |
|
|
228
|
+
| Area maps | volume kept exactly; over-covered cells and mismatched areas refused |
|
|
229
|
+
| MODFLOW 6 control | values set between prepare and solve are the ones MODFLOW 6 uses; daily steps enforced |
|
|
230
|
+
| Recharge | sent = received every day, against MODFLOW 6's budget file and SWAT+'s own printed recharge |
|
|
231
|
+
| Wells | pumped = requested = the user's withdrawal, every day |
|
|
232
|
+
| MODFLOW 6's budget | closes every day to 1e-7 of the gross flow, with and without pumping |
|
|
233
|
+
| Physics | pumping draws down most at the well, and depletes the river by less than it pumps, more over time |
|
|
234
|
+
| Surface routes | SWAT+ takes what was asked (or reports the shortfall); the channel's flow changes the same day by what the users put in and took out |
|
|
235
|
+
| Utility chain | production = delivery + losses = the sum of its sources; sewer = plant outfall |
|
|
236
|
+
| Rules | act on exactly the days after the trigger; a curtailed user takes nothing; dry mode uses less water and more electricity |
|
|
237
|
+
| Two-way exchange | SWAT+'s aquifer returns nothing and stores exactly its recharge; each day's river exchange reaches SWAT+ the next day, whole |
|
|
238
|
+
| Restarts | saved after a warm-up and carried on, runs match unbroken runs bit for bit |
|
|
239
|
+
| Command line, calibration | outputs and status written; statistics on known values; the forward model writes simulated values at observations |
|
|
240
|
+
|
|
241
|
+
## Prior work
|
|
242
|
+
|
|
243
|
+
Earlier SWAT and SWAT+ couplings with MODFLOW either compile MODFLOW-NWT into one program (SWAT-MODFLOW,
|
|
244
|
+
SWAT+MODFLOW) or use SWAT+'s own gwflow module. See [docs/prior-art.md](docs/prior-art.md).
|
|
245
|
+
|
|
246
|
+
## Citing
|
|
247
|
+
|
|
248
|
+
Cite the archived software (Zenodo; the DOI will be listed here once the first release is archived) and, where SWAT+
|
|
249
|
+
is used, swatplus-bmi ([doi:10.5281/zenodo.23963201](https://doi.org/10.5281/zenodo.23963201)). Details in
|
|
250
|
+
[CITATION.cff](CITATION.cff).
|
|
251
|
+
|
|
252
|
+
## AI use
|
|
253
|
+
|
|
254
|
+
Basin Ledger was designed and directed by Andrew Sommerlot and built with an AI coding assistant (Claude, by
|
|
255
|
+
Anthropic), which wrote most of the code, tests and documentation under his direction and review. Whether it is right
|
|
256
|
+
rests on the tests: each model's own budget closes, and every transfer is checked against what the other model
|
|
257
|
+
received.
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
BSD 3-Clause ([LICENSE](LICENSE)). SWAT+ is under the GNU LGPL 2.1 and runs as its own process through swatplus-bmi.
|
|
262
|
+
MODFLOW 6 is public domain (USGS).
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Basin Ledger
|
|
2
|
+
|
|
3
|
+
A basin's water, quantity and quality, balanced day by day across coupled models.
|
|
4
|
+
|
|
5
|
+
The code was written with AI assistance (Claude, by Anthropic), designed and directed by Andrew Sommerlot. See
|
|
6
|
+
[AI use](#ai-use).
|
|
7
|
+
|
|
8
|
+
Basin Ledger couples a watershed model, a groundwater model and the models of large water users through BMI, and
|
|
9
|
+
steps them together one day at a time with one Python driver:
|
|
10
|
+
|
|
11
|
+
- **SWAT+** through [swatplus-bmi](https://github.com/northlinehydrology/swatplus-bmi)
|
|
12
|
+
([doi:10.5281/zenodo.23963201](https://doi.org/10.5281/zenodo.23963201)), which runs stock SWAT+ 62.0.1 under
|
|
13
|
+
outside control;
|
|
14
|
+
- **NextGen**, as finished NextGen In A Box runs replayed day by day, with results keyed to hydrofabric ids;
|
|
15
|
+
- **MODFLOW 6** through its own BMI/XMI (`libmf6`), from Python with USGS's
|
|
16
|
+
[modflowapi](https://github.com/MODFLOW-ORG/modflowapi);
|
|
17
|
+
- **water users** as BMI components from
|
|
18
|
+
[waterledger-models](https://github.com/northlinehydrology/waterledger), starting with data center cooling
|
|
19
|
+
(`CoolingWaterBmi`).
|
|
20
|
+
|
|
21
|
+
Every volume and mass that moves between models is entered in a ledger twice, as sent and as received. A day where
|
|
22
|
+
they differ stops the run. Basin Ledger is a sibling of [Water Ledger](https://waterledger.org). Water Ledger
|
|
23
|
+
estimates one campus's water from public data. Basin Ledger follows that water through a basin's streams and aquifers.
|
|
24
|
+
|
|
25
|
+
**Status: early (0.1.0).** Built and tested:
|
|
26
|
+
|
|
27
|
+
- the water-user interface, with controls a rule can set (a withdrawal limit, dry mode);
|
|
28
|
+
- routes: MODFLOW 6 wells, SWAT+ water allocation intakes and point sources, outfalls, water from and to outside the
|
|
29
|
+
basin;
|
|
30
|
+
- the utility chain: sources, losses, sewer, plant outfall;
|
|
31
|
+
- feedback rules: curtailment, dry mode, drawdown limit;
|
|
32
|
+
- SWAT+ recharge into MODFLOW 6 (one-way), and the two-way stream-aquifer exchange through MODFLOW 6's river package;
|
|
33
|
+
- NextGen as the surface model: a finished NGIAB run replayed day by day, with users on its own hourly forcing;
|
|
34
|
+
- restarts from a saved day that match unbroken runs bit for bit;
|
|
35
|
+
- the ledger and the command line;
|
|
36
|
+
- calibration records per model, and the calibration module's structure. An example is a screening run unless every
|
|
37
|
+
model it runs is calibrated.
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
|
|
41
|
+
- **synthetic:** SWAT+'s Osu_1hru with small MODFLOW 6 models, for the tests;
|
|
42
|
+
- **[Plaster Creek](examples/plaster-creek/), Michigan:** calibrated NextGen streamflow at USGS 04119055 (KGE 0.87
|
|
43
|
+
calibration, 0.81 validation, from [nextgen-michigan](https://github.com/northlinehydrology/nextgen-michigan)),
|
|
44
|
+
with an assumed campus on utility water. Only the streamflow at the gage is calibrated.
|
|
45
|
+
|
|
46
|
+
Still to come ([docs/design.md](docs/design.md)):
|
|
47
|
+
|
|
48
|
+
- SWAT+ built on the NextGen hydrofabric, for water quality on the same catchments;
|
|
49
|
+
- MODFLOW 6 recharge from NextGen (CFE's percolation);
|
|
50
|
+
- AORC forcing;
|
|
51
|
+
- applying calibration parameters, and PEST++ IES.
|
|
52
|
+
|
|
53
|
+
## How it works
|
|
54
|
+
|
|
55
|
+
```mermaid
|
|
56
|
+
flowchart LR
|
|
57
|
+
U["water users (BMI)<br/>hourly: withdrawal, consumption,<br/>return, loads, power"] -- "day's volumes" --> D
|
|
58
|
+
D["driver<br/>one day at a time"] -- "update()" --> S["SWAT+<br/>(swatplus-bmi)"]
|
|
59
|
+
S -- "aquifer recharge, m/s" --> D
|
|
60
|
+
D -- "recharge by cell, well rates<br/>(set between prepare and solve)" --> M["MODFLOW 6<br/>(libmf6, modflowapi)"]
|
|
61
|
+
M -- "simulated flows, heads" --> D
|
|
62
|
+
D --> L[("ledger<br/>sent = received,<br/>every day")]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A NextGen run takes SWAT+'s place as a replay. It gives calibrated flows on NextGen geography, but without feedback
|
|
66
|
+
(see [docs/design.md](docs/design.md#surface-models-swat-or-nextgen)).
|
|
67
|
+
|
|
68
|
+
Places are examples, not code. Each example (`examples/<place>/`) is one config file that wires real models for one
|
|
69
|
+
basin and its users, plus a script that fetches its public data and a short write-up. The engine knows nothing about
|
|
70
|
+
any place or any kind of water user.
|
|
71
|
+
|
|
72
|
+
## Install
|
|
73
|
+
|
|
74
|
+
Needs Python 3.11 or later, and gfortran for SWAT+ (on macOS, Homebrew's `gcc`).
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install "basinledger[users,nextgen]" # or: uv add "basinledger[users,nextgen]"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The extras:
|
|
81
|
+
|
|
82
|
+
- **`users`:** the water-user models from `waterledger-models` (`CoolingWaterBmi`);
|
|
83
|
+
- **`nextgen`:** netCDF4, for reading NextGen runs.
|
|
84
|
+
|
|
85
|
+
Two model programs aren't Python packages, and come separately:
|
|
86
|
+
|
|
87
|
+
- **SWAT+** is built by swatplus-bmi, which is installed with Basin Ledger. It fetches SWAT+ 62.0.1, patches it and
|
|
88
|
+
builds it, in a few minutes:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
swatplus-bmi build
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- **MODFLOW 6's shared library** (`libmf6`) comes from USGS's release through flopy's `get-modflow`, also installed
|
|
95
|
+
with Basin Ledger. Basin Ledger looks for it where `:flopy` puts it, or at `$LIBMF6`:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
get-modflow :flopy --repo modflow6 --subset mf6,libmf6.dylib # libmf6.so on Linux
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
From a source checkout, for development and the tests, with [uv](https://docs.astral.sh/uv/):
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
git clone https://github.com/northlinehydrology/basin-ledger && cd basin-ledger
|
|
105
|
+
uv sync
|
|
106
|
+
uv run python examples/synthetic/fetch.py --get # builds SWAT+ (swatplus-bmi build) and fetches libmf6 (get-modflow)
|
|
107
|
+
uv run pytest # about a minute on one core; -m "not slow" takes about 10 s
|
|
108
|
+
uv run --group examples marimo edit examples/synthetic/notebook.py
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Continuous testing:
|
|
112
|
+
|
|
113
|
+
- **On every push:** CI runs the whole suite on Ubuntu and macOS (`.github/workflows/test.yml`).
|
|
114
|
+
- **Every night:** a run checks against the newest releases of swatplus-bmi, waterledger-models, flopy and
|
|
115
|
+
MODFLOW 6 (`nightly.yml`).
|
|
116
|
+
|
|
117
|
+
## Use
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
uv run python -m basinledger run examples/synthetic/surface.toml # writes ledger.csv, series.csv, summary.json
|
|
121
|
+
uv run python -m basinledger run examples/synthetic/surface.toml --baseline # the same without its water users
|
|
122
|
+
uv run python -m basinledger run examples/synthetic/two_way.toml --save-at 365 --save runs/warm
|
|
123
|
+
uv run python -m basinledger run examples/synthetic/two_way.toml --restart runs/warm --run scenario
|
|
124
|
+
uv run python -m basinledger status examples/synthetic/surface.toml # calibrated, or a screening run
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Or from Python:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from basinledger.driver import Basin
|
|
131
|
+
|
|
132
|
+
b = Basin.from_config("examples/synthetic/basin.toml", run="pumping")
|
|
133
|
+
b.run() # every day of the SWAT+ project; the ledger checks each day
|
|
134
|
+
print(b.ledger.totals()) # m3 (and kg, MWh) per account for the whole run
|
|
135
|
+
b.finalize()
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
An example's config (abridged; the full schema is in `src/basinledger/config.py`):
|
|
139
|
+
|
|
140
|
+
```toml
|
|
141
|
+
[swatplus]
|
|
142
|
+
project = "{swatplus_refdata}/Osu_1hru"
|
|
143
|
+
|
|
144
|
+
[modflow6]
|
|
145
|
+
builder = "model:build" # model.py writes the MODFLOW 6 simulation for the run's days
|
|
146
|
+
|
|
147
|
+
[recharge]
|
|
148
|
+
map = "all:aqu001" # SWAT+ aquifers to MODFLOW 6 cells by area, or a CSV map
|
|
149
|
+
|
|
150
|
+
[[wells]]
|
|
151
|
+
name = "pw1"
|
|
152
|
+
cell = [0, 20, 28]
|
|
153
|
+
|
|
154
|
+
[[utilities]]
|
|
155
|
+
name = "city"
|
|
156
|
+
sources = [{route = "well:pw1", share = 1.0}]
|
|
157
|
+
losses = 0.10
|
|
158
|
+
outfall = "swat:point_source:wwtp"
|
|
159
|
+
|
|
160
|
+
[[users]]
|
|
161
|
+
name = "plant"
|
|
162
|
+
component = "basinledger.testing:SyntheticUser" # or waterledger_models.datacenter.bmi:CoolingWaterBmi
|
|
163
|
+
withdrawal = "utility:city"
|
|
164
|
+
return = "utility:city"
|
|
165
|
+
|
|
166
|
+
[[rules]]
|
|
167
|
+
name = "plant well limit"
|
|
168
|
+
kind = "drawdown_limit"
|
|
169
|
+
user = "plant"
|
|
170
|
+
cell = [0, 20, 28]
|
|
171
|
+
head_min = 88.3
|
|
172
|
+
limit = 0.002
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+

|
|
176
|
+
|
|
177
|
+
*The synthetic example: SWAT+'s Osu_1hru recharge drives a small MODFLOW 6 aquifer, and a synthetic user pumps one
|
|
178
|
+
well. Drawdown is the difference from the same run without the user.*
|
|
179
|
+
|
|
180
|
+
## Water users
|
|
181
|
+
|
|
182
|
+
Any BMI component that reports its withdrawal, consumption and return can be a water user. Its gain, storage,
|
|
183
|
+
constituent loads, return temperature and electricity are optional. A component that uses its own variable names is
|
|
184
|
+
mapped role by role in the config. The engine checks each user's balance every step:
|
|
185
|
+
withdrawal + gain = consumption + return + change in storage. It then integrates the hourly rates into the daily
|
|
186
|
+
volumes the hydrologic models take. See [docs/design.md](docs/design.md#water-users).
|
|
187
|
+
|
|
188
|
+
## What the tests check
|
|
189
|
+
|
|
190
|
+
| What | How |
|
|
191
|
+
|---|---|
|
|
192
|
+
| Hourly to daily | daily volumes equal hourly rates integrated by hand; masses summed, temperature flow-weighted |
|
|
193
|
+
| A user's balance | closes every hour; a component that loses water is refused |
|
|
194
|
+
| Any component plugs in | a renamed component with other units, and `CoolingWaterBmi`, mapped by role |
|
|
195
|
+
| Area maps | volume kept exactly; over-covered cells and mismatched areas refused |
|
|
196
|
+
| MODFLOW 6 control | values set between prepare and solve are the ones MODFLOW 6 uses; daily steps enforced |
|
|
197
|
+
| Recharge | sent = received every day, against MODFLOW 6's budget file and SWAT+'s own printed recharge |
|
|
198
|
+
| Wells | pumped = requested = the user's withdrawal, every day |
|
|
199
|
+
| MODFLOW 6's budget | closes every day to 1e-7 of the gross flow, with and without pumping |
|
|
200
|
+
| Physics | pumping draws down most at the well, and depletes the river by less than it pumps, more over time |
|
|
201
|
+
| Surface routes | SWAT+ takes what was asked (or reports the shortfall); the channel's flow changes the same day by what the users put in and took out |
|
|
202
|
+
| Utility chain | production = delivery + losses = the sum of its sources; sewer = plant outfall |
|
|
203
|
+
| Rules | act on exactly the days after the trigger; a curtailed user takes nothing; dry mode uses less water and more electricity |
|
|
204
|
+
| Two-way exchange | SWAT+'s aquifer returns nothing and stores exactly its recharge; each day's river exchange reaches SWAT+ the next day, whole |
|
|
205
|
+
| Restarts | saved after a warm-up and carried on, runs match unbroken runs bit for bit |
|
|
206
|
+
| Command line, calibration | outputs and status written; statistics on known values; the forward model writes simulated values at observations |
|
|
207
|
+
|
|
208
|
+
## Prior work
|
|
209
|
+
|
|
210
|
+
Earlier SWAT and SWAT+ couplings with MODFLOW either compile MODFLOW-NWT into one program (SWAT-MODFLOW,
|
|
211
|
+
SWAT+MODFLOW) or use SWAT+'s own gwflow module. See [docs/prior-art.md](docs/prior-art.md).
|
|
212
|
+
|
|
213
|
+
## Citing
|
|
214
|
+
|
|
215
|
+
Cite the archived software (Zenodo; the DOI will be listed here once the first release is archived) and, where SWAT+
|
|
216
|
+
is used, swatplus-bmi ([doi:10.5281/zenodo.23963201](https://doi.org/10.5281/zenodo.23963201)). Details in
|
|
217
|
+
[CITATION.cff](CITATION.cff).
|
|
218
|
+
|
|
219
|
+
## AI use
|
|
220
|
+
|
|
221
|
+
Basin Ledger was designed and directed by Andrew Sommerlot and built with an AI coding assistant (Claude, by
|
|
222
|
+
Anthropic), which wrote most of the code, tests and documentation under his direction and review. Whether it is right
|
|
223
|
+
rests on the tests: each model's own budget closes, and every transfer is checked against what the other model
|
|
224
|
+
received.
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
BSD 3-Clause ([LICENSE](LICENSE)). SWAT+ is under the GNU LGPL 2.1 and runs as its own process through swatplus-bmi.
|
|
229
|
+
MODFLOW 6 is public domain (USGS).
|