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.
Files changed (66) hide show
  1. basinledger-0.1.0/.gitignore +8 -0
  2. basinledger-0.1.0/CHANGELOG.md +20 -0
  3. basinledger-0.1.0/CITATION.cff +38 -0
  4. basinledger-0.1.0/LICENSE +28 -0
  5. basinledger-0.1.0/PKG-INFO +262 -0
  6. basinledger-0.1.0/README.md +229 -0
  7. basinledger-0.1.0/docs/design.md +604 -0
  8. basinledger-0.1.0/docs/figures/synthetic-depletion.png +0 -0
  9. basinledger-0.1.0/docs/figures/synthetic-drawdown.png +0 -0
  10. basinledger-0.1.0/docs/figures/synthetic-recharge.png +0 -0
  11. basinledger-0.1.0/docs/prior-art.md +156 -0
  12. basinledger-0.1.0/docs/requests.md +68 -0
  13. basinledger-0.1.0/examples/README.md +14 -0
  14. basinledger-0.1.0/examples/plaster-creek/README.md +72 -0
  15. basinledger-0.1.0/examples/plaster-creek/basin.toml +35 -0
  16. basinledger-0.1.0/examples/plaster-creek/calibration/record.toml +44 -0
  17. basinledger-0.1.0/examples/plaster-creek/campus.yaml +3 -0
  18. basinledger-0.1.0/examples/plaster-creek/check_calibration.py +69 -0
  19. basinledger-0.1.0/examples/plaster-creek/fetch.py +42 -0
  20. basinledger-0.1.0/examples/synthetic/README.md +49 -0
  21. basinledger-0.1.0/examples/synthetic/basin.toml +32 -0
  22. basinledger-0.1.0/examples/synthetic/fetch.py +45 -0
  23. basinledger-0.1.0/examples/synthetic/mill.toml +5 -0
  24. basinledger-0.1.0/examples/synthetic/model.py +72 -0
  25. basinledger-0.1.0/examples/synthetic/notebook.py +146 -0
  26. basinledger-0.1.0/examples/synthetic/small_user.toml +3 -0
  27. basinledger-0.1.0/examples/synthetic/surface.toml +71 -0
  28. basinledger-0.1.0/examples/synthetic/two_way.toml +35 -0
  29. basinledger-0.1.0/examples/synthetic/user.toml +6 -0
  30. basinledger-0.1.0/pyproject.toml +64 -0
  31. basinledger-0.1.0/src/basinledger/__init__.py +7 -0
  32. basinledger-0.1.0/src/basinledger/__main__.py +66 -0
  33. basinledger-0.1.0/src/basinledger/calibration.py +227 -0
  34. basinledger-0.1.0/src/basinledger/chain.py +106 -0
  35. basinledger-0.1.0/src/basinledger/config.py +214 -0
  36. basinledger-0.1.0/src/basinledger/coupling/__init__.py +0 -0
  37. basinledger-0.1.0/src/basinledger/coupling/exchange.py +83 -0
  38. basinledger-0.1.0/src/basinledger/coupling/modflow6.py +181 -0
  39. basinledger-0.1.0/src/basinledger/coupling/nextgen.py +149 -0
  40. basinledger-0.1.0/src/basinledger/coupling/recharge.py +43 -0
  41. basinledger-0.1.0/src/basinledger/coupling/swatplus.py +146 -0
  42. basinledger-0.1.0/src/basinledger/driver.py +420 -0
  43. basinledger-0.1.0/src/basinledger/forcing.py +66 -0
  44. basinledger-0.1.0/src/basinledger/hydrofabric.py +26 -0
  45. basinledger-0.1.0/src/basinledger/ledger.py +89 -0
  46. basinledger-0.1.0/src/basinledger/mapping.py +85 -0
  47. basinledger-0.1.0/src/basinledger/routes.py +120 -0
  48. basinledger-0.1.0/src/basinledger/rules.py +86 -0
  49. basinledger-0.1.0/src/basinledger/testing.py +188 -0
  50. basinledger-0.1.0/src/basinledger/units.py +35 -0
  51. basinledger-0.1.0/src/basinledger/users.py +226 -0
  52. basinledger-0.1.0/tests/conftest.py +43 -0
  53. basinledger-0.1.0/tests/test_calibration.py +69 -0
  54. basinledger-0.1.0/tests/test_chain.py +48 -0
  55. basinledger-0.1.0/tests/test_cli.py +45 -0
  56. basinledger-0.1.0/tests/test_config.py +41 -0
  57. basinledger-0.1.0/tests/test_exchange.py +100 -0
  58. basinledger-0.1.0/tests/test_mapping.py +56 -0
  59. basinledger-0.1.0/tests/test_modflow6.py +62 -0
  60. basinledger-0.1.0/tests/test_nextgen.py +243 -0
  61. basinledger-0.1.0/tests/test_one_way.py +130 -0
  62. basinledger-0.1.0/tests/test_package.py +61 -0
  63. basinledger-0.1.0/tests/test_restart.py +46 -0
  64. basinledger-0.1.0/tests/test_rules.py +34 -0
  65. basinledger-0.1.0/tests/test_surface.py +93 -0
  66. basinledger-0.1.0/tests/test_users.py +141 -0
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ runs/
5
+ tests/runs/
6
+ __marimo__/
7
+ .pytest_cache/
8
+ dist/
@@ -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
+ ![Drawdown in the synthetic example](https://raw.githubusercontent.com/northlinehydrology/basin-ledger/main/docs/figures/synthetic-drawdown.png)
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
+ ![Drawdown in the synthetic example](https://raw.githubusercontent.com/northlinehydrology/basin-ledger/main/docs/figures/synthetic-drawdown.png)
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).