HBV-Lab 1.4.0__tar.gz → 1.4.2__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.
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/__init__.py +1 -1
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/mcp_server.py +83 -11
- {hbv_lab-1.4.0 → hbv_lab-1.4.2/HBV_Lab.egg-info}/PKG-INFO +27 -7
- hbv_lab-1.4.0/README.md → hbv_lab-1.4.2/PKG-INFO +386 -325
- hbv_lab-1.4.0/HBV_Lab.egg-info/PKG-INFO → hbv_lab-1.4.2/README.md +345 -366
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/setup.py +3 -3
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/HBV_model.py +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/calibration.py +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/hbv_step.py +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/response.py +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/routing.py +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/snow.py +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/soil.py +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/uncertainty.py +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab.egg-info/SOURCES.txt +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab.egg-info/dependency_links.txt +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab.egg-info/entry_points.txt +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab.egg-info/requires.txt +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab.egg-info/top_level.txt +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/LICENSE +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/setup.cfg +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/tests/test_hbv_model.py +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/tests/test_hbv_step.py +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/tests/test_response.py +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/tests/test_snow.py +0 -0
- {hbv_lab-1.4.0 → hbv_lab-1.4.2}/tests/test_soil.py +0 -0
|
@@ -140,6 +140,12 @@ def load_data(
|
|
|
140
140
|
optional but required for calibration, uncertainty analysis and performance
|
|
141
141
|
metrics. Dates use ``date_format`` (e.g. '%Y%m%d' for 19810101). ``warmup_end`` /
|
|
142
142
|
``start_date`` / ``end_date`` are optional date filters (same format).
|
|
143
|
+
|
|
144
|
+
Potential ET may be given as a full daily series OR as exactly 12 values (monthly
|
|
145
|
+
means, the HBV-light convention) — in the latter case it is **expanded to a daily
|
|
146
|
+
series automatically**. The return reports ``pet_handling`` so you know which path
|
|
147
|
+
was taken, plus a ``data_quality`` summary (valid/missing counts and min/max per
|
|
148
|
+
column) so you don't have to open the file to sanity-check the inputs.
|
|
143
149
|
"""
|
|
144
150
|
import pandas as pd
|
|
145
151
|
|
|
@@ -151,6 +157,9 @@ def load_data(
|
|
|
151
157
|
else:
|
|
152
158
|
df = pd.read_csv(file_path)
|
|
153
159
|
|
|
160
|
+
# PET in the raw file: 12 non-NaN values => monthly means (will be expanded daily)
|
|
161
|
+
raw_pet_valid = int(df[pet_column].notna().sum()) if pet_column in df.columns else 0
|
|
162
|
+
|
|
154
163
|
model.load_data(
|
|
155
164
|
data=df,
|
|
156
165
|
date_column=date_column,
|
|
@@ -163,6 +172,36 @@ def load_data(
|
|
|
163
172
|
start_date=(start_date or None),
|
|
164
173
|
end_date=(end_date or None),
|
|
165
174
|
)
|
|
175
|
+
|
|
176
|
+
# Per-column data-quality summary over the loaded (filtered) window
|
|
177
|
+
data_quality = {}
|
|
178
|
+
col_map = {
|
|
179
|
+
"precipitation": precip_column,
|
|
180
|
+
"temperature": temp_column,
|
|
181
|
+
"potential_et": pet_column,
|
|
182
|
+
"observed_discharge": (obs_q_column or None),
|
|
183
|
+
}
|
|
184
|
+
for label, col in col_map.items():
|
|
185
|
+
if col and col in model.data.columns:
|
|
186
|
+
s = model.data[col]
|
|
187
|
+
has = bool(s.notna().any())
|
|
188
|
+
data_quality[label] = {
|
|
189
|
+
"valid": int(s.notna().sum()),
|
|
190
|
+
"missing": int(s.isna().sum()),
|
|
191
|
+
"pct_missing": round(float(s.isna().mean() * 100), 1),
|
|
192
|
+
"min": round(float(s.min()), 3) if has else None,
|
|
193
|
+
"max": round(float(s.max()), 3) if has else None,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# How PET was interpreted
|
|
197
|
+
pet_in_data = pet_column in model.data.columns
|
|
198
|
+
if raw_pet_valid == 12:
|
|
199
|
+
pet_handling = "expanded_from_12_monthly_means"
|
|
200
|
+
elif pet_in_data and bool(model.data[pet_column].notna().all()):
|
|
201
|
+
pet_handling = "daily_series"
|
|
202
|
+
else:
|
|
203
|
+
pet_handling = "sparse_or_missing"
|
|
204
|
+
|
|
166
205
|
return {
|
|
167
206
|
"model_id": model_id,
|
|
168
207
|
"n_timesteps": int(len(model.data)),
|
|
@@ -170,6 +209,8 @@ def load_data(
|
|
|
170
209
|
"end_date": str(model.end_date),
|
|
171
210
|
"time_step": model.time_step,
|
|
172
211
|
"has_observed_discharge": bool(obs_q_column),
|
|
212
|
+
"pet_handling": pet_handling,
|
|
213
|
+
"data_quality": data_quality,
|
|
173
214
|
}
|
|
174
215
|
|
|
175
216
|
|
|
@@ -372,10 +413,12 @@ async def calibrate(
|
|
|
372
413
|
elif "iteration" in message.lower() or "maxiter" in message.lower():
|
|
373
414
|
status = "hit_iteration_budget"
|
|
374
415
|
guidance = (
|
|
375
|
-
"
|
|
376
|
-
"continue (it resumes from the current parameters)."
|
|
416
|
+
"Used the full iteration budget and the objective is still improving - "
|
|
417
|
+
"call calibrate again to continue (it resumes from the current parameters)."
|
|
377
418
|
if still_improving
|
|
378
|
-
else "
|
|
419
|
+
else "Used the full iteration budget and the objective has plateaued "
|
|
420
|
+
"(improvement < 1e-3 over recent iterations); further iterations are "
|
|
421
|
+
"unlikely to help much. Stop here, or widen ranges if parameters are at_bound."
|
|
379
422
|
)
|
|
380
423
|
else:
|
|
381
424
|
status, guidance = "failed", f"Optimizer stopped without converging: {message}"
|
|
@@ -416,15 +459,23 @@ def evaluate_uncertainty(
|
|
|
416
459
|
objective: str = "NSE",
|
|
417
460
|
save_best: int = 10,
|
|
418
461
|
seed: int = 42,
|
|
462
|
+
output_file: str = "",
|
|
419
463
|
) -> dict:
|
|
420
464
|
"""Monte-Carlo uncertainty analysis (requires obs data loaded).
|
|
421
465
|
|
|
422
|
-
Samples the parameter ranges ``n_runs`` times and keeps the ``save_best`` runs
|
|
423
|
-
|
|
424
|
-
|
|
466
|
+
Samples the parameter ranges ``n_runs`` times and keeps the ``save_best`` runs, then
|
|
467
|
+
forms a 95% prediction band from them. Returns diagnostics: the 95% band
|
|
468
|
+
``coverage`` (fraction of observations inside the band), ``mean_band_width``, and the
|
|
469
|
+
per-parameter posterior **quantiles** (p5/p25/p50/p75/p95) across the best sets.
|
|
470
|
+
|
|
471
|
+
If ``output_file`` is given, the full **per-timestep prediction band** is written to
|
|
472
|
+
that CSV (Date, Observed, Calibrated, BestRun, Q5, Q95) — this is the band itself,
|
|
473
|
+
ready to plot. Sampling is uniform over the parameter ranges (this is parameter, not
|
|
474
|
+
predictive, uncertainty).
|
|
425
475
|
"""
|
|
426
476
|
model = _get(model_id)
|
|
427
477
|
import numpy as np
|
|
478
|
+
import pandas as pd
|
|
428
479
|
|
|
429
480
|
out = model.evaluate_uncertainty(
|
|
430
481
|
n_runs=n_runs,
|
|
@@ -448,17 +499,36 @@ def evaluate_uncertainty(
|
|
|
448
499
|
else:
|
|
449
500
|
coverage_95 = mean_band_width = None
|
|
450
501
|
|
|
451
|
-
# Per-parameter posterior
|
|
502
|
+
# Per-parameter posterior quantiles across the best parameter sets
|
|
452
503
|
posterior: dict = {}
|
|
453
504
|
for s in out["best_parameter_sets"]:
|
|
454
505
|
for params in s["parameters"].values():
|
|
455
506
|
for name, info in params.items():
|
|
456
|
-
posterior.setdefault(name, []).append(info["default"])
|
|
457
|
-
|
|
458
|
-
name: {"
|
|
507
|
+
posterior.setdefault(name, []).append(float(info["default"]))
|
|
508
|
+
parameter_posteriors = {
|
|
509
|
+
name: {f"p{p}": round(float(np.percentile(v, p)), 4) for p in (5, 25, 50, 75, 95)}
|
|
459
510
|
for name, v in posterior.items()
|
|
460
511
|
}
|
|
461
512
|
|
|
513
|
+
# Optionally write the per-timestep prediction band to CSV
|
|
514
|
+
band_file = None
|
|
515
|
+
if output_file:
|
|
516
|
+
band = pd.DataFrame()
|
|
517
|
+
if "dates" in df.columns:
|
|
518
|
+
band["Date"] = df["dates"].values
|
|
519
|
+
band["Observed"] = df["observed"].values
|
|
520
|
+
if "original" in df.columns:
|
|
521
|
+
band["Calibrated"] = df["original"].values
|
|
522
|
+
if "best_1" in df.columns:
|
|
523
|
+
band["BestRun"] = df["best_1"].values
|
|
524
|
+
band["Q5"] = df["q5"].values
|
|
525
|
+
band["Q95"] = df["q95"].values
|
|
526
|
+
out_dir = os.path.dirname(output_file)
|
|
527
|
+
if out_dir:
|
|
528
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
529
|
+
band.to_csv(output_file, index=False)
|
|
530
|
+
band_file = os.path.abspath(output_file)
|
|
531
|
+
|
|
462
532
|
return {
|
|
463
533
|
"model_id": model_id,
|
|
464
534
|
"objective": objective,
|
|
@@ -468,7 +538,9 @@ def evaluate_uncertainty(
|
|
|
468
538
|
"current_performance": round(float(out["original_performance"]), 4),
|
|
469
539
|
"coverage_95": coverage_95, # fraction of observations inside the 95% band
|
|
470
540
|
"mean_band_width": mean_band_width, # mean (q95 - q5), mm/day
|
|
471
|
-
"
|
|
541
|
+
"parameter_posteriors": parameter_posteriors, # p5/p25/p50/p75/p95 per parameter
|
|
542
|
+
"band_file": band_file, # per-timestep band CSV, if output_file given
|
|
543
|
+
"uncertainty_type": "parameter", # uniform parameter sampling (not predictive)
|
|
472
544
|
}
|
|
473
545
|
|
|
474
546
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: HBV_Lab
|
|
3
|
-
Version: 1.4.
|
|
4
|
-
Summary: An
|
|
3
|
+
Version: 1.4.2
|
|
4
|
+
Summary: An agentic, object-oriented Python implementation of a lumped conceptual HBV hydrological model — with calibration, uncertainty analysis, and a built-in MCP server that lets AI agents drive the workflow.
|
|
5
5
|
Home-page: https://github.com/abdallaox/HBV_python_implementation
|
|
6
6
|
Author: Abdalla Mohammed
|
|
7
7
|
Author-email: abdalla.mohammed.ox@gmail.com
|
|
8
8
|
License: MIT
|
|
9
|
-
Keywords: hydrology HBV-model rainfall-runoff hydrological-modelling
|
|
9
|
+
Keywords: hydrology HBV-model rainfall-runoff hydrological-modelling MCP model-context-protocol agent agentic LLM
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
@@ -39,9 +39,9 @@ Dynamic: requires-dist
|
|
|
39
39
|
Dynamic: requires-python
|
|
40
40
|
Dynamic: summary
|
|
41
41
|
|
|
42
|
-
# HBV_Lab
|
|
42
|
+
# HBV_Lab — an agentic HBV hydrological model with an MCP server
|
|
43
43
|
|
|
44
|
-
**An intuitive, object-oriented Python implementation of a lumped conceptual HBV rainfall–runoff model
|
|
44
|
+
**An intuitive, object-oriented Python implementation of a lumped conceptual HBV rainfall–runoff model — with built-in calibration and uncertainty analysis, and a Model Context Protocol (MCP) server that lets AI agents drive the whole workflow.**
|
|
45
45
|
|
|
46
46
|
[](https://pypi.org/project/HBV_Lab/)
|
|
47
47
|
[](https://pypi.org/project/HBV_Lab/)
|
|
@@ -59,6 +59,11 @@ full model is wrapped in a single `HBVModel` object that handles data loading, s
|
|
|
59
59
|
calibration, uncertainty analysis, plotting, and persistence. It is well suited to research
|
|
60
60
|
prototyping, method development, and hydrology education.
|
|
61
61
|
|
|
62
|
+
**It is also agent-ready:** a built-in [MCP](https://modelcontextprotocol.io/) server exposes the
|
|
63
|
+
model as tools, so an AI agent (Claude Code, Claude Desktop, or any MCP client) can build, calibrate,
|
|
64
|
+
validate and run uncertainty analysis on HBV models through natural language — see
|
|
65
|
+
[Use it as an MCP server](#use-it-as-an-mcp-server-for-ai-agents).
|
|
66
|
+
|
|
62
67
|
> **Scope at a glance:** lumped (single-cell, spatially averaged) · conceptual · daily time step ·
|
|
63
68
|
> requires precipitation, temperature and potential evapotranspiration as input · 14 calibratable
|
|
64
69
|
> parameters. Please read [Scope, Assumptions & Limitations](#scope-assumptions--limitations) before
|
|
@@ -68,6 +73,8 @@ prototyping, method development, and hydrology education.
|
|
|
68
73
|
|
|
69
74
|
## Features
|
|
70
75
|
|
|
76
|
+
- **Agent-ready MCP server** — drive the full *create → load → calibrate → validate → uncertainty*
|
|
77
|
+
workflow from an AI agent over stdio or HTTP, with live calibration progress and structured results.
|
|
71
78
|
- **Complete HBV structure** — snow, soil, groundwater response (two reservoirs, three runoff
|
|
72
79
|
components) and `MAXBAS` triangular routing.
|
|
73
80
|
- **Object-oriented API** — one `HBVModel` object holds data, parameters, states and results.
|
|
@@ -197,6 +204,12 @@ Install once:
|
|
|
197
204
|
pip install "HBV_Lab[mcp]"
|
|
198
205
|
```
|
|
199
206
|
|
|
207
|
+
> **Upgrading:** stop/close the MCP client (and any running `hbv-mcp` process) **before**
|
|
208
|
+
> upgrading — a running server holds `hbv-mcp.exe` open on Windows, so an in-place
|
|
209
|
+
> `pip install -U` can fail with a file-lock error. After upgrading, **fully restart the
|
|
210
|
+
> client** (or remove and re-add the server) so it re-discovers the tool list; clients fetch
|
|
211
|
+
> tools only once per connection, so upgrading mid-session leaves them showing the old tools.
|
|
212
|
+
|
|
200
213
|
#### Option A — local (stdio)
|
|
201
214
|
|
|
202
215
|
No need to start anything yourself; the agent runs `hbv-mcp` for you. Just add it to your client's
|
|
@@ -271,9 +284,16 @@ inspect the improving metric, and decide whether to keep going, widen ranges (vi
|
|
|
271
284
|
model), `load_data` the validation window on the clone, and `run_model` — the calibrated parameters
|
|
272
285
|
carry over with no manual transfer. `compare_models` tabulates calibration vs. validation metrics.
|
|
273
286
|
|
|
287
|
+
**Transparent data loading.** `load_data` reports `pet_handling` (e.g.
|
|
288
|
+
`expanded_from_12_monthly_means` when PET is supplied as 12 monthly means — the HBV-light convention —
|
|
289
|
+
and auto-expanded to daily) and a per-column `data_quality` summary (valid/missing counts, min/max), so
|
|
290
|
+
an agent can sanity-check the forcing without opening the file.
|
|
291
|
+
|
|
274
292
|
**Richer uncertainty.** `evaluate_uncertainty` returns the 95% prediction-band **coverage** (fraction
|
|
275
|
-
of observations inside the band), **mean band width**, and per-parameter
|
|
276
|
-
|
|
293
|
+
of observations inside the band), **mean band width**, and per-parameter posterior **quantiles**
|
|
294
|
+
(p5/p25/p50/p75/p95). Pass `output_file` to write the full **per-timestep prediction band**
|
|
295
|
+
(Date, Observed, Calibrated, BestRun, Q5, Q95) to CSV, ready to plot. (Sampling is uniform over the
|
|
296
|
+
parameter ranges — parameter, not predictive, uncertainty.)
|
|
277
297
|
|
|
278
298
|
## Inputs & outputs
|
|
279
299
|
|