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.
Files changed (26) hide show
  1. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/__init__.py +1 -1
  2. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/mcp_server.py +83 -11
  3. {hbv_lab-1.4.0 → hbv_lab-1.4.2/HBV_Lab.egg-info}/PKG-INFO +27 -7
  4. hbv_lab-1.4.0/README.md → hbv_lab-1.4.2/PKG-INFO +386 -325
  5. hbv_lab-1.4.0/HBV_Lab.egg-info/PKG-INFO → hbv_lab-1.4.2/README.md +345 -366
  6. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/setup.py +3 -3
  7. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/HBV_model.py +0 -0
  8. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/calibration.py +0 -0
  9. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/hbv_step.py +0 -0
  10. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/response.py +0 -0
  11. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/routing.py +0 -0
  12. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/snow.py +0 -0
  13. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/soil.py +0 -0
  14. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab/uncertainty.py +0 -0
  15. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab.egg-info/SOURCES.txt +0 -0
  16. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab.egg-info/dependency_links.txt +0 -0
  17. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab.egg-info/entry_points.txt +0 -0
  18. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab.egg-info/requires.txt +0 -0
  19. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/HBV_Lab.egg-info/top_level.txt +0 -0
  20. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/LICENSE +0 -0
  21. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/setup.cfg +0 -0
  22. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/tests/test_hbv_model.py +0 -0
  23. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/tests/test_hbv_step.py +0 -0
  24. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/tests/test_response.py +0 -0
  25. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/tests/test_snow.py +0 -0
  26. {hbv_lab-1.4.0 → hbv_lab-1.4.2}/tests/test_soil.py +0 -0
@@ -24,7 +24,7 @@ from .soil import soil_routine
24
24
  from .response import response_routine_two_tanks
25
25
  from .routing import route_with_maxbas
26
26
 
27
- __version__ = "1.4.0"
27
+ __version__ = "1.4.2"
28
28
 
29
29
  __all__ = [
30
30
  "HBVModel",
@@ -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
- "Hit the iteration budget; still improving - call calibrate again to "
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 "Hit the iteration budget but the objective has plateaued; likely converged."
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
- Returns the best vs. current performance; full prediction intervals stay in the
424
- model and can be persisted with save_results.
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 ranges across the best parameter sets
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
- posterior_ranges = {
458
- name: {"min": round(float(min(v)), 4), "max": round(float(max(v)), 4)}
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
- "parameter_posterior_ranges": posterior_ranges,
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.0
4
- Summary: An intuitive, object-oriented and user-friendly Python implementation of a lumped conceptual HBV hydrological model for educational and research purposes.
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, with built-in calibration and uncertainty analysis.**
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
  [![PyPI version](https://img.shields.io/pypi/v/HBV_Lab.svg)](https://pypi.org/project/HBV_Lab/)
47
47
  [![Python versions](https://img.shields.io/pypi/pyversions/HBV_Lab.svg)](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 **posterior ranges**, not
276
- just the best/current objective.
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