Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
9d2e098
docs(examples): load each config by its literal filename
MAfarrag Sep 1, 2026
9a0cd4f
docs(examples): write each config path from the repo root
MAfarrag Sep 1, 2026
df6c3d5
feat(results): give a run its own result object instead of nine attri…
MAfarrag Sep 1, 2026
164abe0
refactor(run)!: state what a run needs instead of subclassing Catchment
MAfarrag Sep 1, 2026
6593556
refactor!: drop every compatibility shim and rename the legacy API
MAfarrag Sep 1, 2026
9feb095
refactor(period)!: make the simulation span one object instead of six…
MAfarrag Sep 1, 2026
6ac7b62
refactor(conceptual)!: group the parameter and model fields, and enfo…
MAfarrag Sep 1, 2026
e6f851c
fix(routing): read the kinematic-wave skip at the entry point, not in…
MAfarrag Sep 1, 2026
ec8726c
feat(examples): restore a runnable flood-model script, and warn what …
MAfarrag Sep 1, 2026
a1481d2
refactor(runs)!: split the builder from the finished model
MAfarrag Sep 6, 2026
11fe300
refactor(calibration)!: hold the catchment instead of being one
MAfarrag Sep 6, 2026
3d90669
refactor(protocols): state the spatial-distribution contract, and fin…
MAfarrag Sep 6, 2026
d22df14
perf(routing): index the cells by accumulation value instead of resca…
MAfarrag Sep 6, 2026
bbebd1c
fix(routing): route cells whose accumulation value is not a whole number
MAfarrag Sep 6, 2026
3dc3177
fix(inputs): guard FlowNetwork's grid invariant on replacement, and c…
MAfarrag Sep 6, 2026
73fd4a3
perf(results): stop allocating the state array on runs that never rea…
MAfarrag Sep 6, 2026
5d9fb6b
refactor(results)!: move the plotting and saving off Catchment
MAfarrag Sep 10, 2026
b6cdbbb
test(results): cover the edges of the methods that moved off Catchment
MAfarrag Sep 10, 2026
641ed67
docs(results): document the moved methods, and fix the docstrings tha…
MAfarrag Sep 10, 2026
3d01db1
fix(wrapper): trim the lumped initial-state slot on every branch, not…
MAfarrag Sep 10, 2026
24250ed
fix(routing): let the routers record the routing they applied
MAfarrag Sep 10, 2026
b214d9a
fix(calibration): let a wrongly-wired objective function reach the ca…
MAfarrag Sep 10, 2026
9d96d15
fix(runs): hold the flow-path-length raster to the catchment grid
MAfarrag Sep 10, 2026
445c76b
fix(run): require the fourth lake driver the wrapper actually reads
MAfarrag Sep 10, 2026
34f2f4d
fix(catchment): give the outlet hydrograph the same length on every r…
MAfarrag Sep 10, 2026
a7f9729
fix(conceptual): check the parameter bounds against the width they de…
MAfarrag Sep 10, 2026
8f2ea95
fix(calibration): copy a trial's parameters out of the optimizer's re…
MAfarrag Sep 10, 2026
3a84f2e
perf(period): build the derived calendar once
MAfarrag Sep 10, 2026
40baad4
docs(catchment): stop promising result properties that do not exist
MAfarrag Sep 10, 2026
8ee0ac2
fix(examples): finish the Calibration is-a to has-a migration in the …
MAfarrag Sep 10, 2026
7a8e2d9
docs: correct the pages that still describe removed calibration and r…
MAfarrag Sep 10, 2026
415438f
docs(api): document the four new public modules, and drop the constan…
MAfarrag Sep 10, 2026
8e8f922
fix(inputs): close the rasters once they are read, and stop handing o…
MAfarrag Sep 10, 2026
036650c
docs(examples): leave the pre-rename notebooks alone until they are m…
MAfarrag Sep 10, 2026
5136103
test: rename the classes and tests that still name the removed API
MAfarrag Sep 10, 2026
fc62305
fix(results): correct the animate option list, and the dtype the mask…
MAfarrag Sep 10, 2026
1df51e5
style(tests): sort the inspect import into the stdlib block
MAfarrag Sep 10, 2026
67e5de0
style: apply ruff-format to the migrated calibration scripts and tests
MAfarrag Sep 10, 2026
cdecd64
test: cover every guard this round's review fixes touched
MAfarrag Sep 10, 2026
bc3e1b7
docs: document what this round's fixes changed, and put calibration u…
MAfarrag Sep 10, 2026
bd4c978
fix(conceptual): stop holding the search space to the conceptual mode…
MAfarrag Sep 10, 2026
c7720fc
fix(calibration): read the objective's arity from its signature, not …
MAfarrag Sep 15, 2026
7b48c6e
fix(examples): call the spatial distribution the way its signature reads
MAfarrag Sep 15, 2026
8b6938e
fix(catchment): name the missing outlet series instead of reshaping None
MAfarrag Sep 15, 2026
0e0ed9f
docs: make the two run walkthroughs match the API they document
MAfarrag Sep 15, 2026
611fd41
fix: close the three gaps round 1's own fixes left behind
MAfarrag Sep 15, 2026
2a92454
fix(calibration): forward the objective's extra arguments, and guard …
MAfarrag Sep 15, 2026
2d19a87
docs: correct seven descriptions that contradict the code they describe
MAfarrag Sep 15, 2026
787320e
fix: correct the swapped bounds, and the two docs pages left on the o…
MAfarrag Sep 15, 2026
1c1efec
docs(examples): say that the Jiboa example's dataset is not in the re…
MAfarrag Sep 15, 2026
00d052f
test: cover the calibration guards, the one module this branch never …
MAfarrag Sep 15, 2026
18051cc
docs: state the three contracts this round's fixes changed
MAfarrag Sep 15, 2026
f401c0a
style(tests): apply ruff-format to the calibration guard tests
MAfarrag Sep 15, 2026
028efd6
refactor(catchment): score a gauge in one place, and name the repeate…
MAfarrag Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,5 +126,6 @@ Quick start
- class names: PascalCase (Model, MyClass).
- class method/function: snake_case (get_file, read_config). They should have a verb in them, because they perform some action.

Some CamelCase entry points survive from earlier releases (for example `Run.RunHapi` and `Wrapper.RRMModel`)
because examples and downstream code still call them. New methods are written in snake_case.
The CamelCase entry points that survived from earlier releases (`Run.RunHapi`, `Wrapper.RRMModel` and the rest)
have been renamed to snake_case — `Run.run_distributed`, `Wrapper.run_muskingum`, and so on. There is no
compatibility alias: the old CamelCase spellings are gone.
22 changes: 13 additions & 9 deletions docs/api/catchment.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,25 @@

## Routing methods

`Catchment` and `Calibration` accept exactly three routing methods, matched case-insensitively
and stored in the one spelling the internals compare against:
`Catchment` accepts exactly three routing methods, matched case-insensitively and stored in one
spelling:

| Written as | Stored as | Routes |
|---|---|---|
| `muskingum` | `Muskingum` | Cell to cell along the flow-direction network. |
| `maxbas` | `MAXBAS` | Every cell straight to the outlet through a triangular function. |
| `kinematic` | `Kinematic` | The flood model's own path (`Run.RunFloodModel`). |
| `kinematic` | `Kinematic` | The flood model's own path (`Run.run_flood`). |

Anything else raises a `ValueError` naming the three. Up to and including version 1.7.0 the
constructor stored whatever string it was handed, so a run configured as `"Max_bas"` — or as a
descriptive label such
as `"Muskingum-Cunge"` — was accepted and then silently routed with Muskingum, because
`distrrm.SpatialRouting` compares against `"Muskingum"` exactly. Rejecting the spelling is what
makes that comparison trustworthy; a script passing a spelling outside the table has to be updated
`Calibration` does not take one at all: it holds a `Catchment`, and reads the method off the model
it was given.

Anything else raises a `ValueError` naming the three. Before this check the constructor stored
whatever string it was handed, so a run configured as `"Max_bas"` — or as a descriptive label such
as `"Muskingum-Cunge"` — was accepted and then silently routed with Muskingum, because the routing
loop compared against `"Muskingum"` exactly. That comparison is gone: which router runs is decided
by the entry point you call, and the stored method is read by `Run.run_flood`, which derives
`skip_hydraulic_cells` from `"Kinematic"`, and by the cross-check against `parameters.maxbas`. One
spelling is what keeps both honest; a script passing a spelling outside the table has to be updated
to one of the three.

A YAML run configuration reaches only the first two: `kinematic` selects the flood model, which
Expand Down
29 changes: 29 additions & 0 deletions docs/api/conceptual.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Conceptual model inputs

The conceptual model's inputs used to be six loose attributes on `Catchment` whose rules were
enforced nowhere in particular. They are three value objects now, each checking its own invariant
in `__post_init__`, so a bad combination is refused where it is made rather than several frames
into a run.

- `ParameterSet` — the parameter values plus the `(snow, maxbas)` pair that fixes their width.
Every route to a parameter set goes through the same width rule, including the per-trial
replacements a calibration makes. It is frozen; use `with_values` to derive a new set from an
optimiser's vector.
- `ConceptualModelSetup` — the model, the catchment area, the initial condition and the initial
discharge, as `read_lumped_model` produces them.
- `ParameterBounds` — the calibration's search space, held to the same width rule as the trial
vectors it bounds.

## ParameterSet
::: hapi.conceptual.ParameterSet

## ConceptualModelSetup
::: hapi.conceptual.ConceptualModelSetup

## ParameterBounds
::: hapi.conceptual.ParameterBounds

## Parameter-count helpers
::: hapi.conceptual.parameter_count

::: hapi.conceptual.validate_parameter_count
21 changes: 21 additions & 0 deletions docs/api/period.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Simulation period

Six attributes on `Catchment` used to describe one thing: `start`, `end` and
`temporal_resolution` were given, and `date_index`, `dt` and `conversion_factor` were derived
from them in the constructor and then stored beside them as if they were independent. Storing a
derivation is how the three drift apart — reassigning `end` left `date_index` describing the old
span, with nothing to notice — and it is why the same `pd.date_range` branch was written out four
times across the package.

`SimulationPeriod` holds the three inputs and derives the rest on read, so they cannot disagree.
It is frozen: a run covers the period it was built for, and a model that needs a different one
gets a new period rather than a mutated one.

```python
model.period.date_index # one entry per step
model.period.days # how many steps
len(model.period) # the same number
```

## SimulationPeriod
::: hapi.period.SimulationPeriod
23 changes: 23 additions & 0 deletions docs/api/protocols.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Protocols

`Run` does not import `Catchment`, and `Catchment` does not inherit from anything in the run
layer. What connects them is stated here: the run layer owns the interfaces, and `Catchment`
satisfies them structurally.

That is dependency inversion, and it buys two checkable things. `hapi.run` and `hapi.wrapper`
carry no runtime dependency on the concrete class, so the arrow between the modules points the
other way. And the requirement is checked by mypy, where it used to live in prose in each
method's docstring — prose does not fail CI.

`CatchmentLike` is deliberately builder-shaped: its fields really are optional, because a
half-built catchment is a legitimate state. Narrowing it into a run
(see [Runs](runs.md)) is where the optionality is resolved.

## CatchmentLike
::: hapi.protocols.CatchmentLike

## SupportsQsim
::: hapi.protocols.SupportsQsim

## SpatialDistribution
::: hapi.protocols.SpatialDistribution
50 changes: 50 additions & 0 deletions docs/api/results.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Results

Every `Run.*` entry point returns a `SimulationResults` and assigns it to `Catchment.results`. That
object is the only home for the arrays a run produced — the catchment carries no result attributes
of its own — and it is also what renders and writes them.

## Reading a run

```python
results = Run.run_distributed(model) # also assigned to model.results

results.q_total # (rows, cols, time) total discharge
results.routing # RoutingKind.MUSKINGUM
results.run.period # the calendar the arrays are indexed by
```

`routing` is not decoration: it decides how one cell of `q_total` may be read. Under Muskingum the
discharge accumulates downstream, so a cell *is* the discharge at that cell. Under MAXBAS every cell
is routed straight to the outlet, so a cell is only that cell's contribution and the hydrograph is
the sum over the domain. Ask `results.outlet_shortcut_valid` rather than assuming.

## Viewing and saving

| Call | Does |
|---|---|
| `results.animate(start, end, option=1)` | Animates a result array or a driver over the grid. |
| `results.save_animation(path, fps=2)` | Writes the animation `animate` built. |
| `results.save(path, result=1, flow_acc_path=...)` | One GeoTIFF per step, or a CSV for a lumped run. |

`animate` and `save` need the run behind the arrays — the calendar to index them by and the grid to
mask them with — which is why `SimulationResults` carries the `DistributedRun` or `LumpedRun` that
produced it. A results object built by hand rather than by a run says so instead of failing on
`None`.

`save` chooses rasters or CSV from `routing`: a lumped run has no grid to write rasters on, and that
is a property of the results rather than something the caller restates. The raster branch needs
`flow_acc_path` because `FlowNetwork` keeps the accumulation *array* but not its projection, so the
georeferencing has to be read back from the file.

Importing the run layer does not import matplotlib or cleopatra: `animate` imports them itself, so a
model run never pays for a plotting stack it does not use.

`Catchment.plot_hydrograph` stayed on the catchment. It reads no result array — it compares `Qsim`
against the observed gauge record, which is an analysis input, not something a run produced.

## SimulationResults
::: hapi.results.SimulationResults

## RoutingKind
::: hapi.results.RoutingKind
26 changes: 26 additions & 0 deletions docs/api/runs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Runs

A `Catchment` is a **builder**: its inputs are `X | None` until the matching `read_*` call has
run, and that is honest. The engines need the opposite — a catchment that is finished.
`DistributedRun` and `LumpedRun` are that finished form.

`from_model` is the single validation seam. Constructing a run *is* the validation: it resolves
every optional input, checks the drivers, the parameter cube, the river geometry and the
flow-path-length raster against the catchment grid, and refuses a combination the engines cannot
run. Every engine entry point takes one of these types, so the checks are enforced by the
signatures rather than by remembering to call them — which is how `Calibration`, going straight
to `Wrapper`, used to skip all of them.

```python
run = DistributedRun.from_model(model) # checked here, once
results = Wrapper.run_muskingum(run) # nothing left to re-check
```

Both are frozen. The checks happen at construction, so a mutable run would let a caller swap an
input in afterwards and reach an engine with something never validated.

## DistributedRun
::: hapi.runs.DistributedRun

## LumpedRun
::: hapi.runs.LumpedRun
38 changes: 20 additions & 18 deletions docs/examples/distributed-model-calib.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ The calibration of the Distributed rainfall runoff model follows the same steps

class Catchment:

def __init__(self, name, StartDate, EndDate, fmt="%Y-%m-%d", SpatialResolution = 'Lumped',
TemporalResolution = "Daily"):
def __init__(self, name, start_data, end, fmt="%Y-%m-%d", spatial_resolution="Lumped",
temporal_resolution="Daily", routing_method="Muskingum"):
"""
=============================================================================
Catchment(name, StartDate, EndDate, fmt="%Y-%m-%d", SpatialResolution = 'Lumped',
Catchment(name, start_data, end, fmt="%Y-%m-%d", spatial_resolution="Lumped",
TemporalResolution = "Daily")
=============================================================================
Parameters
Expand Down Expand Up @@ -42,7 +42,7 @@ start = "2009-01-01"
end = "2011-12-31"
name = "Coello"

Coello = Catchment(name, start, end, SpatialResolution = "Distributed")
Coello = Catchment(name, start, end, spatial_resolution="Distributed")
```

# Read Meteorological Inputs
Expand Down Expand Up @@ -92,26 +92,27 @@ Coello.read_lumped_model(HBV, CatchmentArea, InitialCond)
- to check the performance of the model we need to read the gauge hydrographs

```python
Coello.read_gauge_table("Hapi/Data/00inputs/Discharge/stations/gauges.csv", FlowAccPath)
GaugesPath = "Hapi/Data/00inputs/Discharge/stations/"
Coello.read_gauge_table(Path + "/stations/gauges.csv", FlowAccPath)
GaugesPath = Path + "/stations/"
Coello.read_discharge_gauges(GaugesPath, column='id', fmt="%Y-%m-%d")
```
## 3-Run Object


- The `Run` object connects all the components of the simulation together, the `Catchment` object, the `Lake` object and the `distributedrouting` object
- import the Run object and use the `Catchment` object as a parameter to the `Run` object, then call the RunHapi method to start the simulation
- import the Run object and use the `Catchment` object as a parameter to the `Run`
object, then call the run_distributed method to start the simulation

```python
from hapi.run import Run
Run.RunHapi(Coello)
Run.run_distributed(Coello)
```
- the result of the simulation will be stored as attributes in the Catchment object as follow
- the result of the simulation is returned, and also assigned to `Coello.results` as follow

```python
"""
Outputs:
1-statevariables: [numpy attribute]
1-state_variables:
4D array (rows,cols,time,states) states are [sp,wc,sm,uz,lv]
2-qlz: [numpy attribute]
3D array of the lower zone discharge
Expand Down Expand Up @@ -168,7 +169,8 @@ Coello.plot_hydrograph(plotstart, plotend, gaugei)
## 6-Animation

- The best way to visualize a time series of distributed data is an animation. The `Catchment` object
has a `plot_distributed_results` method which animates any of the model results.
carries a `SimulationResults` object on `model.results`, whose `animate` method animates
any of them.

The keyword arguments are forwarded to
`cleopatra.glyphs.gridded.array_glyph.ArrayGlyph.animate`; see its documentation for the full list.
Expand All @@ -178,7 +180,7 @@ cleopatra 0.30 moved the styling keywords onto typed group objects, so the colou
`.boundary(bounds=...)`), the cell-value labels are
`cells=CellValues(show=True, size=..., background_threshold=...)`, and the frame time-stamp is
`frame_label=FrameLabel(location=[...], color=...)`. The gauge markers are built by Hapi itself
when `gauges=True`.
from the gauge table you pass as `gauges=`.

`option` selects the variable to animate:

Expand All @@ -199,11 +201,11 @@ from cleopatra.styling.scaling import ColorScaling
plotstart = "2009-01-01"
plotend = "2009-04-20"

anim = Coello.plot_distributed_results(
anim = Coello.results.animate(
plotstart,
plotend,
option=1,
gauges=True,
gauges=Coello.GaugesTable,
figsize=(9, 9),
ticks_spacing=5,
interval=200,
Expand All @@ -221,7 +223,7 @@ anim = Coello.plot_distributed_results(
system.

```python
Coello.save_animation("results/anim.gif", fps=2)
Coello.results.save_animation("results/anim.gif", fps=2)
```
## 7-Save the result into rasters

Expand All @@ -232,12 +234,12 @@ start = "2009-01-01"
end = "2010-04-20"
prefix = "Qtot_"

Coello.save_results(
FlowAccPath,
Coello.results.save(
path="results/",
flow_acc_path=FlowAccPath,
result=1,
start=start,
end=end,
path="results/",
prefix=prefix,
)
```
Loading