HBV-Lab 1.2.0__tar.gz → 1.3.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.
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/HBV_Lab/__init__.py +1 -1
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/HBV_Lab/calibration.py +37 -10
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/HBV_Lab/mcp_server.py +75 -9
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/HBV_Lab.egg-info/PKG-INFO +8 -1
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/PKG-INFO +8 -1
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/README.md +7 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/setup.py +1 -1
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/HBV_Lab/HBV_model.py +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/HBV_Lab/hbv_step.py +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/HBV_Lab/response.py +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/HBV_Lab/routing.py +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/HBV_Lab/snow.py +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/HBV_Lab/soil.py +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/HBV_Lab/uncertainty.py +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/HBV_Lab.egg-info/SOURCES.txt +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/HBV_Lab.egg-info/dependency_links.txt +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/HBV_Lab.egg-info/entry_points.txt +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/HBV_Lab.egg-info/requires.txt +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/HBV_Lab.egg-info/top_level.txt +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/LICENSE +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/setup.cfg +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/tests/test_hbv_model.py +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/tests/test_hbv_step.py +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/tests/test_response.py +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/tests/test_snow.py +0 -0
- {hbv_lab-1.2.0 → hbv_lab-1.3.0}/tests/test_soil.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
class calibration:
|
|
2
2
|
|
|
3
3
|
def calibrate(self, method='Nelder-Mead', objective='NSE', iterations=100,
|
|
4
|
-
verbose=True, plot_results=True):
|
|
4
|
+
verbose=True, plot_results=True, progress_callback=None):
|
|
5
5
|
"""
|
|
6
6
|
Calibrate an HBV model's parameters to optimize the objective function.
|
|
7
7
|
|
|
@@ -29,11 +29,19 @@ class calibration:
|
|
|
29
29
|
Whether to print progress information
|
|
30
30
|
plot_results : bool, default True
|
|
31
31
|
Whether to plot the final results after calibration
|
|
32
|
-
|
|
32
|
+
progress_callback : callable, optional
|
|
33
|
+
Called once per optimizer iteration as
|
|
34
|
+
``progress_callback(iteration, total_iterations, current_value, best_value)``,
|
|
35
|
+
where the values are in human-facing objective terms (e.g. NSE). Useful for
|
|
36
|
+
driving an external progress UI (e.g. the MCP server). Exceptions raised by
|
|
37
|
+
the callback are swallowed so they cannot interrupt calibration.
|
|
38
|
+
|
|
33
39
|
Returns:
|
|
34
40
|
--------
|
|
35
41
|
dict
|
|
36
|
-
Dictionary
|
|
42
|
+
Dictionary with keys ``parameters`` (optimized parameter dict),
|
|
43
|
+
``performance`` (final metrics), ``optimization_result`` (the SciPy result),
|
|
44
|
+
and ``trajectory`` (best objective value per iteration).
|
|
37
45
|
"""
|
|
38
46
|
import scipy.optimize as opt
|
|
39
47
|
import numpy as np
|
|
@@ -154,15 +162,21 @@ class calibration:
|
|
|
154
162
|
else:
|
|
155
163
|
raise ValueError(f"Unknown objective function: {objective}")
|
|
156
164
|
|
|
157
|
-
# Callback function to track progress
|
|
165
|
+
# Callback function to track progress.
|
|
166
|
+
# objective_function always returns a value to be MINIMIZED (for NSE/KGE it
|
|
167
|
+
# returns the negative metric), so the best-so-far starts at +inf for every
|
|
168
|
+
# objective and improves downward.
|
|
158
169
|
num_iter = [0]
|
|
159
|
-
best_value = [float('inf')
|
|
170
|
+
best_value = [float('inf')]
|
|
160
171
|
start_time = time.time()
|
|
161
|
-
|
|
172
|
+
# Per-iteration best-so-far objective values, in human-facing terms
|
|
173
|
+
# (e.g. NSE/KGE as-is, RMSE/MAE as-is). Returned to the caller.
|
|
174
|
+
trajectory = []
|
|
175
|
+
|
|
162
176
|
def callback(params):
|
|
163
177
|
num_iter[0] += 1
|
|
164
178
|
current_value = objective_function(params)
|
|
165
|
-
|
|
179
|
+
|
|
166
180
|
# For NSE and KGE, we're minimizing the negative value
|
|
167
181
|
if objective in ['NSE', 'KGE']:
|
|
168
182
|
display_value = -current_value
|
|
@@ -170,10 +184,22 @@ class calibration:
|
|
|
170
184
|
else:
|
|
171
185
|
display_value = current_value
|
|
172
186
|
is_better = current_value < best_value[0]
|
|
173
|
-
|
|
187
|
+
|
|
174
188
|
if is_better:
|
|
175
189
|
best_value[0] = current_value
|
|
176
|
-
|
|
190
|
+
|
|
191
|
+
# Best objective so far, in human-facing terms
|
|
192
|
+
best_display = -best_value[0] if objective in ['NSE', 'KGE'] else best_value[0]
|
|
193
|
+
trajectory.append(best_display)
|
|
194
|
+
|
|
195
|
+
# Optional external progress hook (e.g. for an MCP/agent UI).
|
|
196
|
+
# Kept defensive: a misbehaving callback must not break calibration.
|
|
197
|
+
if progress_callback is not None:
|
|
198
|
+
try:
|
|
199
|
+
progress_callback(num_iter[0], iterations, display_value, best_display)
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
|
|
177
203
|
if verbose and num_iter[0] % max(1, iterations // 10) == 0:
|
|
178
204
|
elapsed = time.time() - start_time
|
|
179
205
|
print(f"Iteration {num_iter[0]}/{iterations}, "
|
|
@@ -238,7 +264,8 @@ class calibration:
|
|
|
238
264
|
return {
|
|
239
265
|
'parameters': optimized_params,
|
|
240
266
|
'performance': self.performance_metrics,
|
|
241
|
-
'optimization_result': result
|
|
267
|
+
'optimization_result': result,
|
|
268
|
+
'trajectory': trajectory
|
|
242
269
|
}
|
|
243
270
|
|
|
244
271
|
except Exception as e:
|
|
@@ -27,7 +27,7 @@ import matplotlib
|
|
|
27
27
|
|
|
28
28
|
matplotlib.use("Agg") # headless: no GUI needed on a server
|
|
29
29
|
|
|
30
|
-
from mcp.server.fastmcp import FastMCP
|
|
30
|
+
from mcp.server.fastmcp import FastMCP, Context
|
|
31
31
|
|
|
32
32
|
from . import HBVModel, __version__
|
|
33
33
|
|
|
@@ -68,6 +68,20 @@ def _find_group(model: HBVModel, name: str) -> str | None:
|
|
|
68
68
|
return None
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
def _downsample(seq, n: int = 20) -> list:
|
|
72
|
+
"""At most ``n`` evenly-spaced rounded points from ``seq`` (keeps first and last).
|
|
73
|
+
|
|
74
|
+
Keeps the calibration trajectory compact in the tool result regardless of how many
|
|
75
|
+
iterations ran.
|
|
76
|
+
"""
|
|
77
|
+
seq = [round(float(v), 4) for v in seq]
|
|
78
|
+
if len(seq) <= n:
|
|
79
|
+
return seq
|
|
80
|
+
step = (len(seq) - 1) / (n - 1)
|
|
81
|
+
idx = sorted({int(round(k * step)) for k in range(n)})
|
|
82
|
+
return [seq[i] for i in idx]
|
|
83
|
+
|
|
84
|
+
|
|
71
85
|
# --- tools --------------------------------------------------------------------
|
|
72
86
|
@mcp.tool()
|
|
73
87
|
def create_model(name: str = "") -> dict:
|
|
@@ -210,35 +224,87 @@ def run_model(model_id: str) -> dict:
|
|
|
210
224
|
|
|
211
225
|
|
|
212
226
|
@mcp.tool()
|
|
213
|
-
def calibrate(
|
|
227
|
+
async def calibrate(
|
|
214
228
|
model_id: str,
|
|
215
229
|
objective: str = "NSE",
|
|
216
230
|
method: str = "Nelder-Mead",
|
|
217
231
|
iterations: int = 1000,
|
|
232
|
+
ctx: Context = None,
|
|
218
233
|
) -> dict:
|
|
219
234
|
"""Calibrate the model to observed discharge (requires obs data loaded).
|
|
220
235
|
|
|
221
236
|
``objective`` is one of NSE / KGE / RMSE / MAE. ``method`` defaults to the
|
|
222
237
|
gradient-free 'Nelder-Mead' (recommended for HBV's piecewise objective).
|
|
223
|
-
|
|
238
|
+
|
|
239
|
+
Progress: while running, the server emits MCP progress notifications each
|
|
240
|
+
optimizer iteration (visible in clients that surface them).
|
|
241
|
+
|
|
242
|
+
Incremental use: each call continues from the model's *current* parameters, so an
|
|
243
|
+
agent can call calibrate repeatedly with a small ``iterations`` budget (e.g. 50),
|
|
244
|
+
inspect the improving metric and ``objective_trajectory`` between calls, and decide
|
|
245
|
+
whether to keep going, widen parameter ranges, or switch objective.
|
|
246
|
+
|
|
247
|
+
Returns the optimized parameters, final metrics, optimizer status, and the
|
|
248
|
+
best-objective-per-iteration trajectory (downsampled).
|
|
224
249
|
"""
|
|
250
|
+
import asyncio
|
|
251
|
+
|
|
225
252
|
model = _get(model_id)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
253
|
+
loop = asyncio.get_running_loop()
|
|
254
|
+
updates: asyncio.Queue = asyncio.Queue()
|
|
255
|
+
|
|
256
|
+
def progress_cb(i, total, current, best):
|
|
257
|
+
# Runs in the worker thread — hand the update back to the event loop safely.
|
|
258
|
+
loop.call_soon_threadsafe(updates.put_nowait, (i, total, current, best))
|
|
259
|
+
|
|
260
|
+
async def report(i, total, current, best):
|
|
261
|
+
if ctx is None:
|
|
262
|
+
return
|
|
263
|
+
try:
|
|
264
|
+
await ctx.report_progress(i, total)
|
|
265
|
+
await ctx.info(
|
|
266
|
+
f"calibrate iter {i}/{total}: {objective}={current:.4f} (best {best:.4f})"
|
|
267
|
+
)
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
# Run the (blocking) calibration in a worker thread so we can stream progress.
|
|
272
|
+
task = asyncio.create_task(
|
|
273
|
+
asyncio.to_thread(
|
|
274
|
+
model.calibrate,
|
|
275
|
+
method=method,
|
|
276
|
+
objective=objective,
|
|
277
|
+
iterations=iterations,
|
|
278
|
+
verbose=False,
|
|
279
|
+
plot_results=False,
|
|
280
|
+
progress_callback=progress_cb,
|
|
281
|
+
)
|
|
232
282
|
)
|
|
283
|
+
while not task.done():
|
|
284
|
+
try:
|
|
285
|
+
i, total, current, best = await asyncio.wait_for(updates.get(), timeout=0.25)
|
|
286
|
+
await report(i, total, current, best)
|
|
287
|
+
except asyncio.TimeoutError:
|
|
288
|
+
pass
|
|
289
|
+
while not updates.empty(): # flush any stragglers
|
|
290
|
+
await report(*updates.get_nowait())
|
|
291
|
+
out = await task # re-raises any error from the worker thread
|
|
292
|
+
|
|
293
|
+
result = out["optimization_result"]
|
|
294
|
+
traj = out.get("trajectory", [])
|
|
233
295
|
return {
|
|
234
296
|
"model_id": model_id,
|
|
235
297
|
"objective": objective,
|
|
236
298
|
"method": method,
|
|
299
|
+
"n_iterations": int(getattr(result, "nit", len(traj))),
|
|
300
|
+
"success": bool(getattr(result, "success", True)),
|
|
301
|
+
"message": str(getattr(result, "message", "")),
|
|
237
302
|
"optimized_parameters": {
|
|
238
303
|
group: {name: round(info["default"], 6) for name, info in params.items()}
|
|
239
304
|
for group, params in model.params.items()
|
|
240
305
|
},
|
|
241
306
|
"performance_metrics": _metrics(model),
|
|
307
|
+
"objective_trajectory": _downsample(traj, 20),
|
|
242
308
|
}
|
|
243
309
|
|
|
244
310
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: HBV_Lab
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: An intuitive, object-oriented and user-friendly Python implementation of a lumped conceptual HBV hydrological model for educational and research purposes.
|
|
5
5
|
Home-page: https://github.com/abdallaox/HBV_python_implementation
|
|
6
6
|
Author: Abdalla Mohammed
|
|
@@ -257,6 +257,13 @@ run → calibrate → plot*. Model state is kept server-side (each tool takes a
|
|
|
257
257
|
series are passed by **file path**, not through the agent's context — tools return compact metrics and
|
|
258
258
|
output-file paths.
|
|
259
259
|
|
|
260
|
+
**Calibration progress & agent steering.** `calibrate` emits MCP progress notifications every
|
|
261
|
+
optimizer iteration (clients that surface them show a live progress/log view), and its result
|
|
262
|
+
includes the optimizer status and the best-objective-per-iteration `objective_trajectory`. Because
|
|
263
|
+
each call continues from the model's *current* parameters, an agent can also calibrate incrementally
|
|
264
|
+
— call `calibrate` with a small `iterations` budget, inspect the improving metric, and decide whether
|
|
265
|
+
to keep going, widen ranges, or switch objective between rounds.
|
|
266
|
+
|
|
260
267
|
## Inputs & outputs
|
|
261
268
|
|
|
262
269
|
**Inputs** (daily, consistent units): precipitation (mm), air temperature (°C) and potential
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: HBV_Lab
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: An intuitive, object-oriented and user-friendly Python implementation of a lumped conceptual HBV hydrological model for educational and research purposes.
|
|
5
5
|
Home-page: https://github.com/abdallaox/HBV_python_implementation
|
|
6
6
|
Author: Abdalla Mohammed
|
|
@@ -257,6 +257,13 @@ run → calibrate → plot*. Model state is kept server-side (each tool takes a
|
|
|
257
257
|
series are passed by **file path**, not through the agent's context — tools return compact metrics and
|
|
258
258
|
output-file paths.
|
|
259
259
|
|
|
260
|
+
**Calibration progress & agent steering.** `calibrate` emits MCP progress notifications every
|
|
261
|
+
optimizer iteration (clients that surface them show a live progress/log view), and its result
|
|
262
|
+
includes the optimizer status and the best-objective-per-iteration `objective_trajectory`. Because
|
|
263
|
+
each call continues from the model's *current* parameters, an agent can also calibrate incrementally
|
|
264
|
+
— call `calibrate` with a small `iterations` budget, inspect the improving metric, and decide whether
|
|
265
|
+
to keep going, widen ranges, or switch objective between rounds.
|
|
266
|
+
|
|
260
267
|
## Inputs & outputs
|
|
261
268
|
|
|
262
269
|
**Inputs** (daily, consistent units): precipitation (mm), air temperature (°C) and potential
|
|
@@ -216,6 +216,13 @@ run → calibrate → plot*. Model state is kept server-side (each tool takes a
|
|
|
216
216
|
series are passed by **file path**, not through the agent's context — tools return compact metrics and
|
|
217
217
|
output-file paths.
|
|
218
218
|
|
|
219
|
+
**Calibration progress & agent steering.** `calibrate` emits MCP progress notifications every
|
|
220
|
+
optimizer iteration (clients that surface them show a live progress/log view), and its result
|
|
221
|
+
includes the optimizer status and the best-objective-per-iteration `objective_trajectory`. Because
|
|
222
|
+
each call continues from the model's *current* parameters, an agent can also calibrate incrementally
|
|
223
|
+
— call `calibrate` with a small `iterations` budget, inspect the improving metric, and decide whether
|
|
224
|
+
to keep going, widen ranges, or switch objective between rounds.
|
|
225
|
+
|
|
219
226
|
## Inputs & outputs
|
|
220
227
|
|
|
221
228
|
**Inputs** (daily, consistent units): precipitation (mm), air temperature (°C) and potential
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|