Skip to content

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) is where the optionality is resolved.

CatchmentLike#

hapi.protocols.CatchmentLike #

Bases: Protocol

A catchment under assembly: the period is settled, the inputs may not be.

Every field but period is optional, because that is the truth about a builder -- and it is why an entry point narrows before it runs anything, rather than dereferencing these.

Attributes:

Name Type Description
period SimulationPeriod

The span the model covers. Settled at construction, so never None.

meteo MeteoInputs | None

The three driver cubes, once assigned.

flow_network FlowNetwork | None

The routing network and grid, once assigned.

parameters ParameterSet | None

The parameter set, once read_parameters has run.

model_setup ConceptualModelSetup | None

The conceptual model, once read_lumped_model has run.

data ndarray | None

The lumped driver record, once read_lumped_inputs has run.

river_geometry RiverGeometry | None

The hydraulic rasters, once read_river_geometry has run.

flow_path_length_arr ndarray | None

The flow-path-length raster, once read.

routing_method str

Which routing the parameter set was calibrated for.

results SimulationResults | None

Where a run's output lands. None before the first run.

Source code in src/hapi/protocols.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class CatchmentLike(Protocol):
    """A catchment under assembly: the period is settled, the inputs may not be.

    Every field but `period` is optional, because that is the truth about a builder -- and it
    is why an entry point narrows before it runs anything, rather than dereferencing these.

    Attributes:
        period: The span the model covers. Settled at construction, so never `None`.
        meteo: The three driver cubes, once assigned.
        flow_network: The routing network and grid, once assigned.
        parameters: The parameter set, once `read_parameters` has run.
        model_setup: The conceptual model, once `read_lumped_model` has run.
        data: The lumped driver record, once `read_lumped_inputs` has run.
        river_geometry: The hydraulic rasters, once `read_river_geometry` has run.
        flow_path_length_arr: The flow-path-length raster, once read.
        routing_method: Which routing the parameter set was calibrated for.
        results: Where a run's output lands. `None` before the first run.
    """

    period: SimulationPeriod
    meteo: MeteoInputs | None
    flow_network: FlowNetwork | None
    parameters: ParameterSet | None
    model_setup: ConceptualModelSetup | None
    data: np.ndarray | None
    river_geometry: RiverGeometry | None
    flow_path_length_arr: np.ndarray | None
    routing_method: str
    results: SimulationResults | None

SupportsQsim#

hapi.protocols.SupportsQsim #

Bases: CatchmentLike, Protocol

A catchment that also has somewhere for a lumped hydrograph to land.

Attributes:

Name Type Description
Qsim Any

Where Run.run_lumped puts the routed series, as a frame indexed by the period.

Source code in src/hapi/protocols.py
66
67
68
69
70
71
72
73
class SupportsQsim(CatchmentLike, Protocol):
    """A catchment that also has somewhere for a lumped hydrograph to land.

    Attributes:
        Qsim: Where `Run.run_lumped` puts the routed series, as a frame indexed by the period.
    """

    Qsim: Any

SpatialDistribution#

hapi.protocols.SpatialDistribution #

Bases: Protocol

What a calibration needs of the thing that maps a flat vector onto the model's grid.

The optimiser searches over a flat vector; the model runs on a (rows, cols, n) array. A spatial-distribution object is what converts one into the other, and :class:hapi.rrm.parameters.Parameters is the implementation that ships here.

The calibration entry points used to type this argument Callable[..., Any], which was doubly wrong: a calibration never calls it, and what it actually does is read four members off it. So the annotation described a function while the code used an object, and mypy had nothing to check the four accesses against.

Attributes:

Name Type Description
Function Callable[..., Any]

The distribution strategy, chosen by Parameters.__init__ from the function argument -- an attribute holding a callable rather than a method, which is why it is declared as one. A calibration calls it with the trial vector alone; callers outside may pass kub / klb too, hence the open signature.

Par3d ndarray

The (rows, cols, no_parameters) array Function fills in. Read straight after each call, so the two are a pair: calling Function is what makes this current.

no_parameters int

Parameters per cell. Strides the Muskingum K/X pairs out of the trial vector when the constraints are built.

no_elem int

Cells inside the domain. Sizes the two inequality constraints per cell.

Source code in src/hapi/protocols.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
class SpatialDistribution(Protocol):
    """What a calibration needs of the thing that maps a flat vector onto the model's grid.

    The optimiser searches over a flat vector; the model runs on a `(rows, cols, n)` array. A
    spatial-distribution object is what converts one into the other, and
    :class:`hapi.rrm.parameters.Parameters` is the implementation that ships here.

    The calibration entry points used to type this argument `Callable[..., Any]`, which was
    doubly wrong: a calibration never calls it, and what it actually does is read four members
    off it. So the annotation described a function while the code used an object, and mypy had
    nothing to check the four accesses against.

    Attributes:
        Function: The distribution strategy, chosen by `Parameters.__init__` from the `function`
            argument -- an attribute holding a callable rather than a method, which is why it is
            declared as one. A calibration calls it with the trial vector alone; callers outside
            may pass `kub` / `klb` too, hence the open signature.
        Par3d: The `(rows, cols, no_parameters)` array `Function` fills in. Read straight after
            each call, so the two are a pair: calling `Function` is what makes this current.
        no_parameters: Parameters per cell. Strides the Muskingum K/X pairs out of the trial
            vector when the constraints are built.
        no_elem: Cells inside the domain. Sizes the two inequality constraints per cell.
    """

    Function: Callable[..., Any]
    Par3d: np.ndarray
    no_parameters: int
    no_elem: int