HBV-Lab 1.4.1__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.1 → hbv_lab-1.4.2}/HBV_Lab/__init__.py +1 -1
  2. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/HBV_Lab/mcp_server.py +78 -8
  3. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/HBV_Lab.egg-info/PKG-INFO +10 -3
  4. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/PKG-INFO +10 -3
  5. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/README.md +9 -2
  6. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/setup.py +1 -1
  7. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/HBV_Lab/HBV_model.py +0 -0
  8. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/HBV_Lab/calibration.py +0 -0
  9. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/HBV_Lab/hbv_step.py +0 -0
  10. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/HBV_Lab/response.py +0 -0
  11. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/HBV_Lab/routing.py +0 -0
  12. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/HBV_Lab/snow.py +0 -0
  13. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/HBV_Lab/soil.py +0 -0
  14. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/HBV_Lab/uncertainty.py +0 -0
  15. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/HBV_Lab.egg-info/SOURCES.txt +0 -0
  16. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/HBV_Lab.egg-info/dependency_links.txt +0 -0
  17. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/HBV_Lab.egg-info/entry_points.txt +0 -0
  18. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/HBV_Lab.egg-info/requires.txt +0 -0
  19. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/HBV_Lab.egg-info/top_level.txt +0 -0
  20. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/LICENSE +0 -0
  21. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/setup.cfg +0 -0
  22. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/tests/test_hbv_model.py +0 -0
  23. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/tests/test_hbv_step.py +0 -0
  24. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/tests/test_response.py +0 -0
  25. {hbv_lab-1.4.1 → hbv_lab-1.4.2}/tests/test_snow.py +0 -0
  26. {hbv_lab-1.4.1 → 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.1"
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
 
@@ -418,15 +459,23 @@ def evaluate_uncertainty(
418
459
  objective: str = "NSE",
419
460
  save_best: int = 10,
420
461
  seed: int = 42,
462
+ output_file: str = "",
421
463
  ) -> dict:
422
464
  """Monte-Carlo uncertainty analysis (requires obs data loaded).
423
465
 
424
- Samples the parameter ranges ``n_runs`` times and keeps the ``save_best`` runs.
425
- Returns the best vs. current performance; full prediction intervals stay in the
426
- 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).
427
475
  """
428
476
  model = _get(model_id)
429
477
  import numpy as np
478
+ import pandas as pd
430
479
 
431
480
  out = model.evaluate_uncertainty(
432
481
  n_runs=n_runs,
@@ -450,17 +499,36 @@ def evaluate_uncertainty(
450
499
  else:
451
500
  coverage_95 = mean_band_width = None
452
501
 
453
- # Per-parameter posterior ranges across the best parameter sets
502
+ # Per-parameter posterior quantiles across the best parameter sets
454
503
  posterior: dict = {}
455
504
  for s in out["best_parameter_sets"]:
456
505
  for params in s["parameters"].values():
457
506
  for name, info in params.items():
458
- posterior.setdefault(name, []).append(info["default"])
459
- posterior_ranges = {
460
- 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)}
461
510
  for name, v in posterior.items()
462
511
  }
463
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
+
464
532
  return {
465
533
  "model_id": model_id,
466
534
  "objective": objective,
@@ -470,7 +538,9 @@ def evaluate_uncertainty(
470
538
  "current_performance": round(float(out["original_performance"]), 4),
471
539
  "coverage_95": coverage_95, # fraction of observations inside the 95% band
472
540
  "mean_band_width": mean_band_width, # mean (q95 - q5), mm/day
473
- "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)
474
544
  }
475
545
 
476
546
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: HBV_Lab
3
- Version: 1.4.1
3
+ Version: 1.4.2
4
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
@@ -284,9 +284,16 @@ inspect the improving metric, and decide whether to keep going, widen ranges (vi
284
284
  model), `load_data` the validation window on the clone, and `run_model` — the calibrated parameters
285
285
  carry over with no manual transfer. `compare_models` tabulates calibration vs. validation metrics.
286
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
+
287
292
  **Richer uncertainty.** `evaluate_uncertainty` returns the 95% prediction-band **coverage** (fraction
288
- of observations inside the band), **mean band width**, and per-parameter **posterior ranges**, not
289
- 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.)
290
297
 
291
298
  ## Inputs & outputs
292
299
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: HBV_Lab
3
- Version: 1.4.1
3
+ Version: 1.4.2
4
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
@@ -284,9 +284,16 @@ inspect the improving metric, and decide whether to keep going, widen ranges (vi
284
284
  model), `load_data` the validation window on the clone, and `run_model` — the calibrated parameters
285
285
  carry over with no manual transfer. `compare_models` tabulates calibration vs. validation metrics.
286
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
+
287
292
  **Richer uncertainty.** `evaluate_uncertainty` returns the 95% prediction-band **coverage** (fraction
288
- of observations inside the band), **mean band width**, and per-parameter **posterior ranges**, not
289
- 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.)
290
297
 
291
298
  ## Inputs & outputs
292
299
 
@@ -243,9 +243,16 @@ inspect the improving metric, and decide whether to keep going, widen ranges (vi
243
243
  model), `load_data` the validation window on the clone, and `run_model` — the calibrated parameters
244
244
  carry over with no manual transfer. `compare_models` tabulates calibration vs. validation metrics.
245
245
 
246
+ **Transparent data loading.** `load_data` reports `pet_handling` (e.g.
247
+ `expanded_from_12_monthly_means` when PET is supplied as 12 monthly means — the HBV-light convention —
248
+ and auto-expanded to daily) and a per-column `data_quality` summary (valid/missing counts, min/max), so
249
+ an agent can sanity-check the forcing without opening the file.
250
+
246
251
  **Richer uncertainty.** `evaluate_uncertainty` returns the 95% prediction-band **coverage** (fraction
247
- of observations inside the band), **mean band width**, and per-parameter **posterior ranges**, not
248
- just the best/current objective.
252
+ of observations inside the band), **mean band width**, and per-parameter posterior **quantiles**
253
+ (p5/p25/p50/p75/p95). Pass `output_file` to write the full **per-timestep prediction band**
254
+ (Date, Observed, Calibrated, BestRun, Q5, Q95) to CSV, ready to plot. (Sampling is uniform over the
255
+ parameter ranges — parameter, not predictive, uncertainty.)
249
256
 
250
257
  ## Inputs & outputs
251
258
 
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as f:
5
5
 
6
6
  setup(
7
7
  name='HBV_Lab',
8
- version='1.4.1',
8
+ version='1.4.2',
9
9
  packages=find_packages(include=['HBV_Lab', 'HBV_Lab.*']),
10
10
  install_requires=[
11
11
  'numpy',
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes