Source code for sparsehydro.calibration.callbacks
"""Built-in callbacks for solver progress reporting.
A callback is any callable with the signature::
def my_callback(info: dict) -> None: ...
The ``info`` dict contains:
+------------------+--------+-----------------------------------------------+
| Key | Type | Description |
+==================+========+===============================================+
| ``generation`` | int | Current generation / iteration (1-based) |
| ``n_evals`` | int | Total function evaluations so far |
| ``n_pareto`` | int | Number of non-dominated solutions |
| ``pareto_F`` | ndarray| Pareto objectives in **minimisation form**, |
| | | shape ``(n_pareto, n_obj)`` |
| ``objective_names`` | list | Objective names (same order as columns) |
| ``minimize_flags`` | list | ``True`` for minimised objectives |
+------------------+--------+-----------------------------------------------+
Pass any callable to a solver via its ``callback`` parameter::
from sparsehydro.calibration import NSGAIISolver, ProgressCallback
cb = ProgressCallback(frequency=10)
solver = NSGAIISolver(pop_size=100, n_gen=200, callback=cb)
result = solver.solve(problem)
"""
from __future__ import annotations
import numpy as np
[docs]
class ProgressCallback:
"""Prints a one-line progress summary after every *frequency* generations.
Output format::
gen= 10 evals= 1000 pareto= 12 mse=1234.5678 nash_sutcliffe=0.7231
:param frequency: Print every *frequency* callback invocations (default 1).
:type frequency: int
"""
def __init__(self, frequency: int = 1) -> None:
if frequency < 1:
raise ValueError(f"frequency must be >= 1; got {frequency!r}")
self.frequency = frequency
self._call_count = 0
def __call__(self, info: dict) -> None:
"""Print a progress line if the invocation count hits *frequency*.
:param info: Progress dictionary supplied by the solver (see the module
docstring for the expected keys).
:type info: dict
:returns: ``None``.
:rtype: None
"""
self._call_count += 1
if self._call_count % self.frequency != 0:
return
gen = info["generation"]
n_evals = info["n_evals"]
n_pareto = info["n_pareto"]
pareto_F = np.asarray(info["pareto_F"])
obj_names = info["objective_names"]
minimize_flags = info["minimize_flags"]
best_parts: list[str] = []
if pareto_F.ndim == 2 and len(pareto_F) > 0:
for j, (name, minimize) in enumerate(zip(obj_names, minimize_flags)):
# pareto_F is in minimisation form; argmin is always "best"
raw = float(pareto_F[:, j].min())
display = raw if minimize else -raw
best_parts.append(f"{name}={display:.4f}")
else:
best_parts = [f"{name}=N/A" for name in obj_names]
obj_str = " ".join(best_parts)
print(f"gen={gen:5d} evals={n_evals:7d} pareto={n_pareto:4d} {obj_str}")