Skip to content

Wrapper#

Wrapper#

hapi.wrapper.Wrapper #

Connects rainfall-runoff model components with spatial routing.

The Wrapper class connects different components together including the lumped run of the distributed model with the spatial routing for Hapi and for FW1 (triangular routing).

Methods:

Name Description
run_muskingum

Run distributed RRM with Muskingum spatial routing.

run_muskingum_with_lake

Run distributed RRM with lake and Muskingum spatial routing.

FW1

Run distributed RRM with triangular routing.

run_maxbas_with_lake

Run distributed RRM with lake and triangular routing.

Lumped

Run a lumped conceptual model with optional routing.

Source code in src/hapi/wrapper.py
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
class Wrapper:
    """Connects rainfall-runoff model components with spatial routing.

    The Wrapper class connects different components together including
    the lumped run of the distributed model with the spatial routing
    for Hapi and for FW1 (triangular routing).

    Methods:
        run_muskingum: Run distributed RRM with Muskingum spatial routing.
        run_muskingum_with_lake: Run distributed RRM with lake and Muskingum
            spatial routing.
        FW1: Run distributed RRM with triangular routing.
        run_maxbas_with_lake: Run distributed RRM with lake and triangular
            routing.
        Lumped: Run a lumped conceptual model with optional routing.
    """

    @staticmethod
    def run_muskingum(run: DistributedRun) -> SimulationResults:
        """Run the distributed rainfall-runoff model with spatial routing.

        Connects two modules:

        1. The distributed rainfall-runoff model that runs separately
           for each cell.
        2. The spatial routing scheme that routes flow following the
           river network.

        Args:
            run: The validated inputs. Reads the drivers, the flow network, the parameter
                cube and the conceptual model setup, and honours
                :attr:`~hapi.runs.DistributedRun.skip_hydraulic_cells`, which leaves cells
                with a positive `river_geometry.bankfull_depth` for a 1D hydraulic model
                to route instead.

        Returns:
            SimulationResults: The run's output. Nothing is written to the caller's model;
            the entry point in :mod:`hapi.run` is what puts it on `model.results`.
        """
        # run the rainfall runoff model separately
        results = distrrm.run_lumped_model(run)

        # run the GIS part to rout from cell to another. It records
        # `RoutingKind.MUSKINGUM` on the results, which is what makes the outlet-cell
        # shortcut in `extract_discharge` valid for them.
        distrrm.route_muskingum(run, results)
        return results

    @staticmethod
    def run_muskingum_with_lake(run: DistributedRun, Lake: Lake) -> SimulationResults:
        """Run the distributed RRM with lake simulation and routing.

        Connects three modules: the lake module, the distributed
        rainfall-runoff module, and the spatial routing module. The
        lake discharge is simulated using HBVLake, routed via
        Muskingum, and added to the downstream cell before spatial
        routing.

        Args:
            run: The validated inputs. See :class:`~hapi.runs.DistributedRun`, which
                `DistributedRun.from_model(model)` builds and checks.
            Lake: The lake record, carrying:

                - MeteoData (numpy.ndarray): 2D array with columns
                  for precipitation, evapotranspiration, temperature,
                  and long-term average temperature.
                - Parameters (numpy.ndarray): Lake model parameters.
                - CatArea (float): Lake catchment area in km2.
                - LakeArea (float): Lake surface area in km2.
                - StageDischargeCurve (numpy.ndarray): Stage-discharge
                  relationship.
                - InitialCond (list): Initial condition values.
                - OutflowCell (tuple): Row and column indices of the
                  lake outflow cell.

        Returns:
            SimulationResults: The run's output. Nothing is written to the caller's model;
            the entry point in :mod:`hapi.run` is what puts it on `model.results`.
        """
        meteo_data, lake_parameters, outflow_cell = _lake_inputs(Lake)
        plake = meteo_data[:, 0]
        et = meteo_data[:, 1]
        t = meteo_data[:, 2]
        tm = meteo_data[:, 3]

        # lake simulation
        Lake.Qlake, _ = HBVLake().simulate(
            plake,
            t,
            et,
            lake_parameters,
            [run.period.conversion_factor, Lake.CatArea, Lake.LakeArea],
            Lake.StageDischargeCurve,
            0,
            init_st=Lake.InitialCond,
            ll_temp=tm,
            lake_sim=True,
        )
        # qlake is in m3/sec
        # lake routing
        Lake.QlakeR = routing.muskingum_v(
            Lake.Qlake,
            Lake.Qlake[0],
            lake_parameters[11],
            lake_parameters[12],
            run.period.conversion_factor,
        )

        # subcatchment
        results = distrrm.run_lumped_model(run)

        # `ParameterSet.values` is a flat sequence for a lumped run and a cube for a
        # distributed one; this path is distributed, so index it as the cube it is.
        parameters = np.asarray(run.parameters.values)
        # routing lake discharge with DS cell k & x and adding to cell Q
        qlake = routing.muskingum_v(
            Lake.QlakeR,
            Lake.QlakeR[0],
            parameters[outflow_cell[0], outflow_cell[1], 10],
            parameters[outflow_cell[0], outflow_cell[1], 11],
            run.period.conversion_factor,
        )

        # No padding: `HBVLake.simulate` already prepends the initial-state slot, exactly as
        # the distributed model does, and `muskingum_v` preserves length -- so `qlake` is
        # already `simulation_steps` long and lines up with `quz` slot for slot. Appending a
        # step here made it one longer than the array it is added to, which raised for every
        # input and left this entry point unrunnable.
        # both lake & Quz are in m3/s
        quz = results.quz
        quz[outflow_cell[0], outflow_cell[1], :] = (
            quz[outflow_cell[0], outflow_cell[1], :] + qlake
        )

        # run the GIS part to rout from cell to another. It records
        # `RoutingKind.MUSKINGUM` on the results.
        distrrm.route_muskingum(run, results)
        return results

    @staticmethod
    def run_maxbas(run: DistributedRun) -> SimulationResults:
        """Run the distributed RRM with triangular function-1 routing.

        Connects two modules:

        1. The distributed rainfall-runoff module.
        2. The triangular function-1 (MAXBAS) routing method.

        The output discharge is computed as the sum of routed upper
        zone and unrouted lower zone discharge across all cells.

        :meth:`~hapi.rrm.distrrm.DistributedRRM.route_maxbas` fills the per-cell output
        fields (`q_total`, `quz_routed`, `qlz_translated`) and records
        `RoutingKind.MAXBAS`, so the discharge options of `results.save` /
        `results.animate` work on this path; see that method for the MAXBAS semantics.

        Args:
            run: The validated inputs. See :class:`~hapi.runs.DistributedRun`, which
                `DistributedRun.from_model(model)` builds and checks.

        Returns:
            SimulationResults: The run's output. Nothing is written to the caller's model;
            the entry point in :mod:`hapi.run` is what puts it on `model.results`.
        """
        # subcatchment
        results = distrrm.run_lumped_model(run)

        distrrm.route_maxbas(run, results)

        steps = run.meteo.simulation_steps
        qlz1 = np.array(
            [np.nansum(results.qlz[:, :, i]) for i in range(steps)]
        )  # average of all cells (not routed mm/timestep)
        quz1 = np.array(
            [np.nansum(results.quz[:, :, i]) for i in range(steps)]
        )  # average of all cells (routed mm/timestep)

        results.qout = (qlz1 + quz1)[:-1]
        return results

    @staticmethod
    def run_maxbas_with_lake(run: DistributedRun, Lake: Lake) -> SimulationResults:
        """Run the distributed RRM with lake and triangular routing.

        Connects three modules:

        1. The distributed rainfall-runoff module.
        2. The triangular function-1 (MAXBAS) routing method.
        3. The lake simulation module.

        The lake discharge is simulated using HBVLake, routed via
        Muskingum, and combined with the subcatchment discharge that
        has been routed using the triangular function.

        Args:
            run: The validated inputs. See :class:`~hapi.runs.DistributedRun`, which
                `DistributedRun.from_model(model)` builds and checks.
            Lake: The lake record. See :meth:`run_muskingum_with_lake` for the fields it
                must carry; this path reads the same ones.

        Returns:
            SimulationResults: The run's output. Nothing is written to the caller's model;
            the entry point in :mod:`hapi.run` is what puts it on `model.results`.
        """
        meteo_data, lake_parameters, outflow_cell = _lake_inputs(Lake)
        plake = meteo_data[:, 0]
        et = meteo_data[:, 1]
        t = meteo_data[:, 2]
        tm = meteo_data[:, 3]

        # lake simulation
        Lake.Qlake, _ = HBVLake().simulate(
            plake,
            t,
            et,
            lake_parameters,
            [run.period.conversion_factor, Lake.CatArea, Lake.LakeArea],
            Lake.StageDischargeCurve,
            0,
            init_st=Lake.InitialCond,
            ll_temp=tm,
            lake_sim=True,
        )

        # qlake is in m3/sec
        # lake routing
        Lake.QlakeR = routing.muskingum_v(
            Lake.Qlake,
            Lake.Qlake[0],
            lake_parameters[11],
            lake_parameters[12],
            run.period.conversion_factor,
        )

        # subcatchment
        results = distrrm.run_lumped_model(run)

        # `route_maxbas` fills the subcatchment fields only: the lake is a lumped inflow
        # with no spatial extent, so it enters `qout` below but never `q_total`.
        distrrm.route_maxbas(run, results)

        steps = run.meteo.simulation_steps
        qlz1 = np.array(
            [np.nansum(results.qlz[:, :, i]) for i in range(steps)]
        )  # average of all cells (not routed mm/timestep)
        quz1 = np.array(
            [np.nansum(results.quz[:, :, i]) for i in range(steps)]
        )  # average of all cells (routed mm/timestep)

        qout = qlz1 + quz1

        # qout = (qlz1 + quz1) * area / (run.period.conversion_factor * 3.6)

        # Both series run over `simulation_steps`, and the non-lake FW1 path returns
        # `qout[:-1]` -- dropping the trailing slot, not the leading initial-state one. The
        # lake series has to be trimmed the same way or the two cannot be added at all.
        results.qout = qout[:-1] + Lake.QlakeR[:-1]
        return results

    @staticmethod
    def run_lumped(
        run: LumpedRun, Routing: int = 0, RoutingFn: Callable | None = None
    ) -> SimulationResults:
        """Run a lumped conceptual model with optional routing.

        Executes a lumped rainfall-runoff model (e.g., HBV) to
        compute the upper and lower zone discharge, then optionally
        routes the combined discharge using the provided routing
        function.

        The discharge is converted from mm/timestep to m3/s using the catchment area and
        the period's conversion factor.

        Args:
            run: The validated inputs. See :class:`~hapi.runs.LumpedRun`, which
                `LumpedRun.from_model(model)` builds and checks. Reads the `(time, 4)`
                driver record, the parameter set and the conceptual model setup.
            Routing (int, optional): Flag to enable routing. Set to
                0 to disable, nonzero to enable. Defaults to 0.
            RoutingFn (callable): Routing function to apply to the
                discharge hydrograph. Must be callable.

        Returns:
            SimulationResults: The run's output, with the total discharge in `q_total` and
            `routing` set to `RoutingKind.LUMPED`. Nothing is written to the caller's
            model; :meth:`~hapi.run.Run.run_lumped` is what indexes `q_total` by the period
            and puts the frame on `model.Qsim`.

        Raises:
            TypeError: If `RoutingFn` is not callable when
                routing is enabled.
        """
        ### input data validation
        if Routing != 0:
            if RoutingFn is None or not callable(RoutingFn):
                raise TypeError(
                    "routing function should be of type callable (function that takes "
                    f"arguments), got {type(RoutingFn).__name__}"
                )

        # data
        p = run.data[:, 0]
        et = run.data[:, 1]
        t = run.data[:, 2]
        tm = run.data[:, 3]

        # from the conceptual model calculate the upper and lower response mm/time step
        quz, qlz, state_variables = run.model_setup.model.simulate(
            p,
            t,
            et,
            tm,
            run.parameters.values,
            init_st=run.model_setup.initial_cond,
            q_init=run.model_setup.q_init,
            snow=run.parameters.snow,
        )
        # q mm , area sq km  (1000**2)/1000/f/60/60 = 1/(3.6*f)
        # if daily tfac=24 if hourly tfac=1 if 15 min tfac=0.25
        factor = run.model_setup.area / run.period.conversion_factor
        # A lumped run has no spatial routing at all, so the routed fields stay None and
        # the routing kind says why -- rather than a MAXBAS flag left over from elsewhere.
        results = SimulationResults(
            routing=RoutingKind.LUMPED,
            quz=quz * factor,
            qlz=qlz * factor,
            state_variables=state_variables,
            run=run,
        )
        # The lumped total discharge is exactly what `q_total` means, so it goes there rather
        # than onto the catchment as `Qsim`. `Run.run_lumped` is what indexes it by the period
        # and puts the frame on the model -- so this engine writes nothing outside `results`.
        # The conceptual model allocates one slot more than it fills: `simulate` sizes its
        # arrays `len(prec) + 1` and writes indices 0..n-1, so index 0 carries the initial
        # state, 1..n-1 the simulated steps, and index n is never written. `[:-1]` drops
        # that unwritten trailing slot -- the leading initial-state one is kept, which is
        # why `q_total[0]` is the warm-up value rather than a simulated step.
        #
        # Trimmed once, here, rather than inside each routing branch: both routed branches
        # used to do it and the unrouted one did not, so `Run.run_lumped(model)` -- the
        # entry point's own default, `Route=0` -- produced an `n + 1` series and then raised
        # `Length of values (1096) does not match length of index (1095)`.
        q_total = (results.quz + results.qlz)[:-1]

        if Routing != 0 and run.parameters.maxbas:
            route = RoutingFn
            assert route is not None  # noqa: S101 - guarded above
            q_total = route(np.array(q_total), run.parameters.values[-1])
        elif Routing != 0:
            route = RoutingFn
            assert route is not None  # noqa: S101 - guarded above
            q_total = route(
                np.array(q_total),
                q_total[0],
                run.parameters.values[-2],
                run.parameters.values[-1],
                run.period.dt,
            )
        results.q_total = q_total
        return results

run_lumped(run: LumpedRun, Routing: int = 0, RoutingFn: Callable | None = None) -> SimulationResults staticmethod #

Run a lumped conceptual model with optional routing.

Executes a lumped rainfall-runoff model (e.g., HBV) to compute the upper and lower zone discharge, then optionally routes the combined discharge using the provided routing function.

The discharge is converted from mm/timestep to m3/s using the catchment area and the period's conversion factor.

Parameters:

Name Type Description Default
run LumpedRun

The validated inputs. See :class:~hapi.runs.LumpedRun, which LumpedRun.from_model(model) builds and checks. Reads the (time, 4) driver record, the parameter set and the conceptual model setup.

required
Routing int

Flag to enable routing. Set to 0 to disable, nonzero to enable. Defaults to 0.

0
RoutingFn callable

Routing function to apply to the discharge hydrograph. Must be callable.

None

Returns:

Type Description
SimulationResults

The run's output, with the total discharge in q_total and

SimulationResults

routing set to RoutingKind.LUMPED. Nothing is written to the caller's

model;

meth:~hapi.run.Run.run_lumped is what indexes q_total by the period

SimulationResults

and puts the frame on model.Qsim.

Raises:

Type Description
TypeError

If RoutingFn is not callable when routing is enabled.

Source code in src/hapi/wrapper.py
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
@staticmethod
def run_lumped(
    run: LumpedRun, Routing: int = 0, RoutingFn: Callable | None = None
) -> SimulationResults:
    """Run a lumped conceptual model with optional routing.

    Executes a lumped rainfall-runoff model (e.g., HBV) to
    compute the upper and lower zone discharge, then optionally
    routes the combined discharge using the provided routing
    function.

    The discharge is converted from mm/timestep to m3/s using the catchment area and
    the period's conversion factor.

    Args:
        run: The validated inputs. See :class:`~hapi.runs.LumpedRun`, which
            `LumpedRun.from_model(model)` builds and checks. Reads the `(time, 4)`
            driver record, the parameter set and the conceptual model setup.
        Routing (int, optional): Flag to enable routing. Set to
            0 to disable, nonzero to enable. Defaults to 0.
        RoutingFn (callable): Routing function to apply to the
            discharge hydrograph. Must be callable.

    Returns:
        SimulationResults: The run's output, with the total discharge in `q_total` and
        `routing` set to `RoutingKind.LUMPED`. Nothing is written to the caller's
        model; :meth:`~hapi.run.Run.run_lumped` is what indexes `q_total` by the period
        and puts the frame on `model.Qsim`.

    Raises:
        TypeError: If `RoutingFn` is not callable when
            routing is enabled.
    """
    ### input data validation
    if Routing != 0:
        if RoutingFn is None or not callable(RoutingFn):
            raise TypeError(
                "routing function should be of type callable (function that takes "
                f"arguments), got {type(RoutingFn).__name__}"
            )

    # data
    p = run.data[:, 0]
    et = run.data[:, 1]
    t = run.data[:, 2]
    tm = run.data[:, 3]

    # from the conceptual model calculate the upper and lower response mm/time step
    quz, qlz, state_variables = run.model_setup.model.simulate(
        p,
        t,
        et,
        tm,
        run.parameters.values,
        init_st=run.model_setup.initial_cond,
        q_init=run.model_setup.q_init,
        snow=run.parameters.snow,
    )
    # q mm , area sq km  (1000**2)/1000/f/60/60 = 1/(3.6*f)
    # if daily tfac=24 if hourly tfac=1 if 15 min tfac=0.25
    factor = run.model_setup.area / run.period.conversion_factor
    # A lumped run has no spatial routing at all, so the routed fields stay None and
    # the routing kind says why -- rather than a MAXBAS flag left over from elsewhere.
    results = SimulationResults(
        routing=RoutingKind.LUMPED,
        quz=quz * factor,
        qlz=qlz * factor,
        state_variables=state_variables,
        run=run,
    )
    # The lumped total discharge is exactly what `q_total` means, so it goes there rather
    # than onto the catchment as `Qsim`. `Run.run_lumped` is what indexes it by the period
    # and puts the frame on the model -- so this engine writes nothing outside `results`.
    # The conceptual model allocates one slot more than it fills: `simulate` sizes its
    # arrays `len(prec) + 1` and writes indices 0..n-1, so index 0 carries the initial
    # state, 1..n-1 the simulated steps, and index n is never written. `[:-1]` drops
    # that unwritten trailing slot -- the leading initial-state one is kept, which is
    # why `q_total[0]` is the warm-up value rather than a simulated step.
    #
    # Trimmed once, here, rather than inside each routing branch: both routed branches
    # used to do it and the unrouted one did not, so `Run.run_lumped(model)` -- the
    # entry point's own default, `Route=0` -- produced an `n + 1` series and then raised
    # `Length of values (1096) does not match length of index (1095)`.
    q_total = (results.quz + results.qlz)[:-1]

    if Routing != 0 and run.parameters.maxbas:
        route = RoutingFn
        assert route is not None  # noqa: S101 - guarded above
        q_total = route(np.array(q_total), run.parameters.values[-1])
    elif Routing != 0:
        route = RoutingFn
        assert route is not None  # noqa: S101 - guarded above
        q_total = route(
            np.array(q_total),
            q_total[0],
            run.parameters.values[-2],
            run.parameters.values[-1],
            run.period.dt,
        )
    results.q_total = q_total
    return results

run_maxbas(run: DistributedRun) -> SimulationResults staticmethod #

Run the distributed RRM with triangular function-1 routing.

Connects two modules:

  1. The distributed rainfall-runoff module.
  2. The triangular function-1 (MAXBAS) routing method.

The output discharge is computed as the sum of routed upper zone and unrouted lower zone discharge across all cells.

:meth:~hapi.rrm.distrrm.DistributedRRM.route_maxbas fills the per-cell output fields (q_total, quz_routed, qlz_translated) and records RoutingKind.MAXBAS, so the discharge options of results.save / results.animate work on this path; see that method for the MAXBAS semantics.

Parameters:

Name Type Description Default
run DistributedRun

The validated inputs. See :class:~hapi.runs.DistributedRun, which DistributedRun.from_model(model) builds and checks.

required

Returns:

Type Description
SimulationResults

The run's output. Nothing is written to the caller's model;

the entry point in

mod:hapi.run is what puts it on model.results.

Source code in src/hapi/wrapper.py
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
@staticmethod
def run_maxbas(run: DistributedRun) -> SimulationResults:
    """Run the distributed RRM with triangular function-1 routing.

    Connects two modules:

    1. The distributed rainfall-runoff module.
    2. The triangular function-1 (MAXBAS) routing method.

    The output discharge is computed as the sum of routed upper
    zone and unrouted lower zone discharge across all cells.

    :meth:`~hapi.rrm.distrrm.DistributedRRM.route_maxbas` fills the per-cell output
    fields (`q_total`, `quz_routed`, `qlz_translated`) and records
    `RoutingKind.MAXBAS`, so the discharge options of `results.save` /
    `results.animate` work on this path; see that method for the MAXBAS semantics.

    Args:
        run: The validated inputs. See :class:`~hapi.runs.DistributedRun`, which
            `DistributedRun.from_model(model)` builds and checks.

    Returns:
        SimulationResults: The run's output. Nothing is written to the caller's model;
        the entry point in :mod:`hapi.run` is what puts it on `model.results`.
    """
    # subcatchment
    results = distrrm.run_lumped_model(run)

    distrrm.route_maxbas(run, results)

    steps = run.meteo.simulation_steps
    qlz1 = np.array(
        [np.nansum(results.qlz[:, :, i]) for i in range(steps)]
    )  # average of all cells (not routed mm/timestep)
    quz1 = np.array(
        [np.nansum(results.quz[:, :, i]) for i in range(steps)]
    )  # average of all cells (routed mm/timestep)

    results.qout = (qlz1 + quz1)[:-1]
    return results

run_maxbas_with_lake(run: DistributedRun, Lake: Lake) -> SimulationResults staticmethod #

Run the distributed RRM with lake and triangular routing.

Connects three modules:

  1. The distributed rainfall-runoff module.
  2. The triangular function-1 (MAXBAS) routing method.
  3. The lake simulation module.

The lake discharge is simulated using HBVLake, routed via Muskingum, and combined with the subcatchment discharge that has been routed using the triangular function.

Parameters:

Name Type Description Default
run DistributedRun

The validated inputs. See :class:~hapi.runs.DistributedRun, which DistributedRun.from_model(model) builds and checks.

required
Lake Lake

The lake record. See :meth:run_muskingum_with_lake for the fields it must carry; this path reads the same ones.

required

Returns:

Type Description
SimulationResults

The run's output. Nothing is written to the caller's model;

the entry point in

mod:hapi.run is what puts it on model.results.

Source code in src/hapi/wrapper.py
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
@staticmethod
def run_maxbas_with_lake(run: DistributedRun, Lake: Lake) -> SimulationResults:
    """Run the distributed RRM with lake and triangular routing.

    Connects three modules:

    1. The distributed rainfall-runoff module.
    2. The triangular function-1 (MAXBAS) routing method.
    3. The lake simulation module.

    The lake discharge is simulated using HBVLake, routed via
    Muskingum, and combined with the subcatchment discharge that
    has been routed using the triangular function.

    Args:
        run: The validated inputs. See :class:`~hapi.runs.DistributedRun`, which
            `DistributedRun.from_model(model)` builds and checks.
        Lake: The lake record. See :meth:`run_muskingum_with_lake` for the fields it
            must carry; this path reads the same ones.

    Returns:
        SimulationResults: The run's output. Nothing is written to the caller's model;
        the entry point in :mod:`hapi.run` is what puts it on `model.results`.
    """
    meteo_data, lake_parameters, outflow_cell = _lake_inputs(Lake)
    plake = meteo_data[:, 0]
    et = meteo_data[:, 1]
    t = meteo_data[:, 2]
    tm = meteo_data[:, 3]

    # lake simulation
    Lake.Qlake, _ = HBVLake().simulate(
        plake,
        t,
        et,
        lake_parameters,
        [run.period.conversion_factor, Lake.CatArea, Lake.LakeArea],
        Lake.StageDischargeCurve,
        0,
        init_st=Lake.InitialCond,
        ll_temp=tm,
        lake_sim=True,
    )

    # qlake is in m3/sec
    # lake routing
    Lake.QlakeR = routing.muskingum_v(
        Lake.Qlake,
        Lake.Qlake[0],
        lake_parameters[11],
        lake_parameters[12],
        run.period.conversion_factor,
    )

    # subcatchment
    results = distrrm.run_lumped_model(run)

    # `route_maxbas` fills the subcatchment fields only: the lake is a lumped inflow
    # with no spatial extent, so it enters `qout` below but never `q_total`.
    distrrm.route_maxbas(run, results)

    steps = run.meteo.simulation_steps
    qlz1 = np.array(
        [np.nansum(results.qlz[:, :, i]) for i in range(steps)]
    )  # average of all cells (not routed mm/timestep)
    quz1 = np.array(
        [np.nansum(results.quz[:, :, i]) for i in range(steps)]
    )  # average of all cells (routed mm/timestep)

    qout = qlz1 + quz1

    # qout = (qlz1 + quz1) * area / (run.period.conversion_factor * 3.6)

    # Both series run over `simulation_steps`, and the non-lake FW1 path returns
    # `qout[:-1]` -- dropping the trailing slot, not the leading initial-state one. The
    # lake series has to be trimmed the same way or the two cannot be added at all.
    results.qout = qout[:-1] + Lake.QlakeR[:-1]
    return results

run_muskingum(run: DistributedRun) -> SimulationResults staticmethod #

Run the distributed rainfall-runoff model with spatial routing.

Connects two modules:

  1. The distributed rainfall-runoff model that runs separately for each cell.
  2. The spatial routing scheme that routes flow following the river network.

Parameters:

Name Type Description Default
run DistributedRun

The validated inputs. Reads the drivers, the flow network, the parameter cube and the conceptual model setup, and honours :attr:~hapi.runs.DistributedRun.skip_hydraulic_cells, which leaves cells with a positive river_geometry.bankfull_depth for a 1D hydraulic model to route instead.

required

Returns:

Type Description
SimulationResults

The run's output. Nothing is written to the caller's model;

the entry point in

mod:hapi.run is what puts it on model.results.

Source code in src/hapi/wrapper.py
 72
 73
 74
 75
 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
@staticmethod
def run_muskingum(run: DistributedRun) -> SimulationResults:
    """Run the distributed rainfall-runoff model with spatial routing.

    Connects two modules:

    1. The distributed rainfall-runoff model that runs separately
       for each cell.
    2. The spatial routing scheme that routes flow following the
       river network.

    Args:
        run: The validated inputs. Reads the drivers, the flow network, the parameter
            cube and the conceptual model setup, and honours
            :attr:`~hapi.runs.DistributedRun.skip_hydraulic_cells`, which leaves cells
            with a positive `river_geometry.bankfull_depth` for a 1D hydraulic model
            to route instead.

    Returns:
        SimulationResults: The run's output. Nothing is written to the caller's model;
        the entry point in :mod:`hapi.run` is what puts it on `model.results`.
    """
    # run the rainfall runoff model separately
    results = distrrm.run_lumped_model(run)

    # run the GIS part to rout from cell to another. It records
    # `RoutingKind.MUSKINGUM` on the results, which is what makes the outlet-cell
    # shortcut in `extract_discharge` valid for them.
    distrrm.route_muskingum(run, results)
    return results

run_muskingum_with_lake(run: DistributedRun, Lake: Lake) -> SimulationResults staticmethod #

Run the distributed RRM with lake simulation and routing.

Connects three modules: the lake module, the distributed rainfall-runoff module, and the spatial routing module. The lake discharge is simulated using HBVLake, routed via Muskingum, and added to the downstream cell before spatial routing.

Parameters:

Name Type Description Default
run DistributedRun

The validated inputs. See :class:~hapi.runs.DistributedRun, which DistributedRun.from_model(model) builds and checks.

required
Lake Lake

The lake record, carrying:

  • MeteoData (numpy.ndarray): 2D array with columns for precipitation, evapotranspiration, temperature, and long-term average temperature.
  • Parameters (numpy.ndarray): Lake model parameters.
  • CatArea (float): Lake catchment area in km2.
  • LakeArea (float): Lake surface area in km2.
  • StageDischargeCurve (numpy.ndarray): Stage-discharge relationship.
  • InitialCond (list): Initial condition values.
  • OutflowCell (tuple): Row and column indices of the lake outflow cell.
required

Returns:

Type Description
SimulationResults

The run's output. Nothing is written to the caller's model;

the entry point in

mod:hapi.run is what puts it on model.results.

Source code in src/hapi/wrapper.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
@staticmethod
def run_muskingum_with_lake(run: DistributedRun, Lake: Lake) -> SimulationResults:
    """Run the distributed RRM with lake simulation and routing.

    Connects three modules: the lake module, the distributed
    rainfall-runoff module, and the spatial routing module. The
    lake discharge is simulated using HBVLake, routed via
    Muskingum, and added to the downstream cell before spatial
    routing.

    Args:
        run: The validated inputs. See :class:`~hapi.runs.DistributedRun`, which
            `DistributedRun.from_model(model)` builds and checks.
        Lake: The lake record, carrying:

            - MeteoData (numpy.ndarray): 2D array with columns
              for precipitation, evapotranspiration, temperature,
              and long-term average temperature.
            - Parameters (numpy.ndarray): Lake model parameters.
            - CatArea (float): Lake catchment area in km2.
            - LakeArea (float): Lake surface area in km2.
            - StageDischargeCurve (numpy.ndarray): Stage-discharge
              relationship.
            - InitialCond (list): Initial condition values.
            - OutflowCell (tuple): Row and column indices of the
              lake outflow cell.

    Returns:
        SimulationResults: The run's output. Nothing is written to the caller's model;
        the entry point in :mod:`hapi.run` is what puts it on `model.results`.
    """
    meteo_data, lake_parameters, outflow_cell = _lake_inputs(Lake)
    plake = meteo_data[:, 0]
    et = meteo_data[:, 1]
    t = meteo_data[:, 2]
    tm = meteo_data[:, 3]

    # lake simulation
    Lake.Qlake, _ = HBVLake().simulate(
        plake,
        t,
        et,
        lake_parameters,
        [run.period.conversion_factor, Lake.CatArea, Lake.LakeArea],
        Lake.StageDischargeCurve,
        0,
        init_st=Lake.InitialCond,
        ll_temp=tm,
        lake_sim=True,
    )
    # qlake is in m3/sec
    # lake routing
    Lake.QlakeR = routing.muskingum_v(
        Lake.Qlake,
        Lake.Qlake[0],
        lake_parameters[11],
        lake_parameters[12],
        run.period.conversion_factor,
    )

    # subcatchment
    results = distrrm.run_lumped_model(run)

    # `ParameterSet.values` is a flat sequence for a lumped run and a cube for a
    # distributed one; this path is distributed, so index it as the cube it is.
    parameters = np.asarray(run.parameters.values)
    # routing lake discharge with DS cell k & x and adding to cell Q
    qlake = routing.muskingum_v(
        Lake.QlakeR,
        Lake.QlakeR[0],
        parameters[outflow_cell[0], outflow_cell[1], 10],
        parameters[outflow_cell[0], outflow_cell[1], 11],
        run.period.conversion_factor,
    )

    # No padding: `HBVLake.simulate` already prepends the initial-state slot, exactly as
    # the distributed model does, and `muskingum_v` preserves length -- so `qlake` is
    # already `simulation_steps` long and lines up with `quz` slot for slot. Appending a
    # step here made it one longer than the array it is added to, which raised for every
    # input and left this entry point unrunnable.
    # both lake & Quz are in m3/s
    quz = results.quz
    quz[outflow_cell[0], outflow_cell[1], :] = (
        quz[outflow_cell[0], outflow_cell[1], :] + qlake
    )

    # run the GIS part to rout from cell to another. It records
    # `RoutingKind.MUSKINGUM` on the results.
    distrrm.route_muskingum(run, results)
    return results