Interface specifications for parsimonious hydrological models
Project description
sparsehydro
sparsehydro provides abstract interfaces and utilities for building parsimonious (sparse-parameter) hydrological models in Python, with a focus on stormwater systems.
Package workflow
flowchart LR
DATA[("Data\nDataFrame / any type")]
subgraph MODEL["1 · Model"]
M1["model.initialize()"] --> M2["model.validate()"]
end
subgraph PROBLEM["2 · CalibrationProblem"]
CP["column_map:\n observed → 'flow_cfs'\n predicted → 'rdii_cfs'\n rainfall_mm ← 'rain'"]
OBJ["objectives:\n PeakWeightedMSE\n NashSutcliffe"]
end
subgraph SOLVERS["3 · Solvers (any)"]
direction TB
S1["NSGAIISolver\npymoo · NSGA-II"]
S2["ParticleSwarmSolver\nPlatypus · SMPSO/OMOPSO"]
S3["ScipySolver\nscipy · diff-evo / L-BFGS-B"]
S4["PlatypusSolver\nany Platypus algorithm"]
end
subgraph RESULT["4 · CalibrationResult"]
R1["pareto_X / pareto_F"]
R2["best_by('nash_sutcliffe')"]
R3["to_pareto_dataframe()"]
end
subgraph VIZ["5 · Visualization"]
V1["plot_calibration_timeseries()"]
V2["plot_pareto_evolution()"]
V3["plot_parallel_coordinates()"]
V4["plot_calibration_dashboard()"]
end
DATA --> MODEL
MODEL --> PROBLEM
OBJ --> PROBLEM
PROBLEM --> SOLVERS
SOLVERS --> RESULT
RESULT --> VIZ
Object relationships
Every model subclasses the abstract IModel lifecycle and exposes named
ScalarParameter / VectorParameter objects through a built-in registry.
Calibration tooling discovers those parameters automatically, so the same
objects flow unchanged from model construction through to visualization.
classDiagram
direction LR
class ModelState {
<<enumeration>>
CREATED
INITIALIZED
VALIDATED
PREPARED
PREDICTED
FINALIZED
}
class IModel {
<<abstract>>
+str model_name
+ModelState state
+initialize()
+validate() bool
+prepare(data)
+predict() DataFrame
+finalize()
+register_scalar_parameter(p)
+get_scalar_parameter(name) ScalarParameter
}
class ScalarParameter {
+str name
+float value
+float lower_bound
+float upper_bound
+bool calibrate
+is_valid() bool
+normalize() float
+clamp() ScalarParameter
}
class VectorParameter {
+str name
+ndarray values
+int size
+is_valid() bool
}
class IUnitHydroComponent {
<<abstract>>
}
class ITorchModel {
<<abstract>>
+forward() Tensor
}
IModel "1" o-- "*" ScalarParameter : registers
IModel "1" o-- "*" VectorParameter : registers
IModel ..> ModelState : has state
IUnitHydroComponent --|> IModel
ITorchModel --|> IModel
RDIIModel --|> IModel
IAModel --|> IModel
AMMModel --|> IModel
EnsembleModel --|> IModel
SeasonalityModel --|> IModel
RTKTriangle --|> IUnitHydroComponent
GammaUH --|> IUnitHydroComponent
UnitHydrographAdapter --|> IUnitHydroComponent
EnsembleModel "1" o-- "*" IModel : composes
RDIIModel "1" *-- "1" IAModel : contains
RDIIModel "1" *-- "*" RTKTriangle : contains
The calibration layer is solver-agnostic: a single CalibrationProblem wraps a
model, its data, and one or more IObjective functions, then passes unchanged
to any ISolver, which returns a CalibrationResult.
classDiagram
direction LR
class CalibrationProblem {
+IModel model
+objectives IObjective[]
+dict column_map
+param_names str[]
+int n_params
+evaluate(x) ndarray
}
class IObjective {
<<abstract>>
+str name
+bool minimize
+evaluate(observed, predicted) float
}
class ISolver {
<<abstract>>
+solve(problem) CalibrationResult
}
class CalibrationResult {
+ndarray pareto_X
+ndarray pareto_F
+history GenerationRecord[]
+best_by(name) dict
+to_pareto_dataframe() DataFrame
}
CalibrationProblem o-- IModel : wraps
CalibrationProblem o-- "1..*" IObjective : scores with
ISolver ..> CalibrationProblem : consumes
ISolver ..> CalibrationResult : produces
NSGAIISolver --|> ISolver
ParticleSwarmSolver --|> ISolver
ScipySolver --|> ISolver
PlatypusSolver --|> ISolver
MSE --|> IObjective
NashSutcliffe --|> IObjective
KGE --|> IObjective
PeakWeightedMSE --|> IObjective
Calibration workflow
The typical end-to-end calibration loop — build the model, wrap it in a problem, solve, then inspect and visualize the results:
sequenceDiagram
autonumber
actor User
participant Model as IModel
participant Problem as CalibrationProblem
participant Solver as ISolver
participant Obj as IObjective
participant Result as CalibrationResult
participant Viz as Visualization
User->>Model: initialize()
User->>Model: validate()
User->>Problem: CalibrationProblem(model, data, objectives, column_map)
Problem->>Model: prepare(data)
Note over Problem: discovers calibratable<br/>ScalarParameters and bounds
User->>Solver: solve(problem)
loop each candidate / generation
Solver->>Problem: evaluate(x)
Problem->>Model: set parameters and predict()
Model-->>Problem: predicted series
Problem->>Obj: evaluate(observed, predicted)
Obj-->>Problem: scores
Problem-->>Solver: objective vector F
end
Solver-->>Result: pareto_X, pareto_F, history
Result-->>User: best_by(), to_pareto_dataframe()
User->>Viz: plot_pareto_evolution(result)
Features
- Model lifecycle — enforced six-state progression:
CREATED → INITIALIZED → VALIDATED → PREPARED → PREDICTED → FINALIZED - Parameter registry — named scalar and vector parameters with lower/upper bounds,
calibrateflag to freeze individual parameters, normalization, and clamping - Flexible calibration —
CalibrationProblemaccepts any data type via a unifiedcolumn_mapdict; observed targets and predicted outputs are mapped by name or callable - Solver-agnostic — the same
CalibrationProblempasses unchanged to NSGA-II (pymoo), PSO (Platypus SMPSO/OMOPSO), SciPy, or any customISolver - PyTorch support —
ITorchModelcombines the lifecycle interface withnn.Modulefor gradient-based calibration - pandas outputs —
predict()returnspd.DataFrame | pd.Series - Interactive dashboards — Plotly-based plots for Pareto evolution, parallel coordinates, calibration time-series with IQR bands, and multi-panel dashboards
Installation
pip install sparsehydro
Optional extras:
| Extra | Command | Enables |
|---|---|---|
rdii |
pip install sparsehydro[rdii] |
RDII model (pymoo + scipy) |
platypus |
pip install sparsehydro[platypus] |
Full Platypus algorithm suite + PSO |
torch |
pip install sparsehydro[torch] |
Gradient-based calibration via PyTorch |
docs |
pip install sparsehydro[docs] |
Sphinx documentation build |
all |
pip install sparsehydro[all] |
All optional extras |
Interactive charts always require:
pip install plotly
Quick start — custom model
import pandas as pd
from sparsehydro import IModel, ModelState, ScalarParameter
class LinearReservoir(IModel):
model_name = "linear-reservoir"
def initialize(self) -> None:
self.register_scalar_parameter(
ScalarParameter("k", value=0.3, lower_bound=0.0, upper_bound=1.0,
units="1/day", description="Recession coefficient")
)
self._state = ModelState.INITIALIZED
def validate(self) -> bool:
ok = self.parameters_valid()
if ok:
self._state = ModelState.VALIDATED
return ok
def prepare(self, forcing: pd.Series) -> None:
self._forcing = forcing
self._state = ModelState.PREPARED
def predict(self) -> pd.Series:
k = self.get_scalar_parameter("k").value
self._state = ModelState.PREDICTED
return self._forcing * k
def finalize(self) -> None:
self._state = ModelState.FINALIZED
model = LinearReservoir()
model.initialize()
model.validate()
model.prepare(pd.Series([10.0, 8.0, 6.0], name="P_mm"))
print(model.predict())
model.finalize()
Quick start — RDII calibration
import pandas as pd
from sparsehydro.models.rdii import RDIIModel
from sparsehydro.calibration import (
CalibrationProblem,
ParticleSwarmSolver,
PeakWeightedMSE,
NashSutcliffe,
)
from sparsehydro.visualization import (
plot_calibration_timeseries,
plot_pareto_evolution,
)
# 1. Load data (datetime, rainfall_mm, flow_cfs, temperature_c)
df = pd.read_csv("events.csv", parse_dates=["datetime"])
# 2. Create and validate model
model = RDIIModel(n_triangles=3)
model.initialize()
model.validate()
# 3. Define the calibration problem via column_map
problem = CalibrationProblem(
model=model,
data=df,
objectives=[PeakWeightedMSE(), NashSutcliffe()],
column_map={
"observed": "flow_cfs", # target column in data
"predicted": "rdii_cfs", # output column from model.predict()
},
)
# 4. Run PSO calibration (runtime kwargs override constructor defaults)
solver = ParticleSwarmSolver(swarm_size=50, n_evaluations=5_000, seed=42)
result = solver.solve(problem) # full run
# result = solver.solve(problem, n_evaluations=200) # quick test override
# 5. Inspect and visualize
best_x = result.best_by("nash_sutcliffe")
print(result.to_pareto_dataframe())
fig = plot_pareto_evolution(result)
fig.show()
Documentation
Full documentation is available at cbuahin.github.io/sparsehydro.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sparsehydro-0.1.0a1.tar.gz.
File metadata
- Download URL: sparsehydro-0.1.0a1.tar.gz
- Upload date:
- Size: 61.6 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
176314320438a747ef2aeeb741d06e9058c04007eef2c1c686852b29f3a2b8be
|
|
| MD5 |
e7080a1f5cec8c75c56c3312777622d2
|
|
| BLAKE2b-256 |
d0d45db8cd661f603d22bb2742ff26c56f1219d70ad0a570f79f1b26deee47e6
|
File details
Details for the file sparsehydro-0.1.0a1-py3-none-any.whl.
File metadata
- Download URL: sparsehydro-0.1.0a1-py3-none-any.whl
- Upload date:
- Size: 24.1 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f7ce4e65403d9288e6044fe7e3564dacd4cca7a07a1437edb03ef9db2c61fc36
|
|
| MD5 |
77f75e6667f0089df996d8e8bf62a193
|
|
| BLAKE2b-256 |
5d05bc6de249fa63ea76f4df455c57fa13db199a06cf4dae53ca49a69b469bba
|