Skip to content

Commit 5f7ab53

Browse files
committed
re-add kinematic routing
1 parent 85c7ef8 commit 5f7ab53

File tree

1 file changed

+98
-38
lines changed

1 file changed

+98
-38
lines changed

geb/hydrology/routing.py

Lines changed: 98 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,15 @@ def __init__(
190190
assert (Q_initial[self.waterbody_id != -1] == 0).all()
191191
self.Q_prev = Q_initial
192192

193+
def get_total_storage(self):
194+
"""
195+
Get the total storage of the river network, which is the sum of the
196+
available storage in each cell.
197+
"""
198+
return self.get_available_storage(maximum_abstraction_ratio=1.0)
193199

200+
201+
@njit(cache=True)
194202
def update_node_kinematic(
195203
Qin, Qold, q, alpha, beta, deltaT, deltaX, epsilon=np.float64(0.0001)
196204
):
@@ -265,7 +273,25 @@ def calculate_discharge_from_storage(
265273
# The momentum equation (solved for Q), see eq. 18 in https://gmd.copernicus.org/articles/13/3267/2020/
266274
return (river_storage / (river_length * river_alpha)) ** (1 / river_beta)
267275

276+
def get_available_storage(self, maximum_abstraction_ratio=0.9):
277+
"""
278+
Get the available storage of the river network, which is the sum of the
279+
available storage in each cell.
280+
"""
281+
assert not np.isnan(self.Q_prev).any()
282+
assert (self.Q_prev >= 0.0).all()
283+
284+
river_storage = self.calculate_river_storage_from_discharge(
285+
discharge=self.Q_prev,
286+
river_alpha=self.river_alpha,
287+
river_length=self.river_length,
288+
river_beta=self.river_beta,
289+
waterbody_id=self.waterbody_id,
290+
)
291+
return river_storage * maximum_abstraction_ratio
292+
268293
@staticmethod
294+
@njit(cache=True)
269295
def _step(
270296
Qold,
271297
sideflow_m3,
@@ -290,14 +316,15 @@ def _step(
290316
river_length: np.ndarray
291317
Array of floats containing the channel length, must be > 0
292318
"""
293-
Qkx = np.full_like(Qold, np.nan)
319+
Qnew = np.full_like(Qold, np.nan)
320+
over_abstraction_m3 = np.zeros_like(Qold, dtype=np.float32)
294321

295322
for i in range(upstream_matrix_from_up_to_downstream.shape[0]):
296323
node = idxs_up_to_downstream[i]
297324
upstream_nodes = upstream_matrix_from_up_to_downstream[i]
298325

299326
Qin = np.float32(0.0)
300-
sideflow_node = sideflow_m3[node] / dt / river_length[node]
327+
sideflow_node_m3 = sideflow_m3[node]
301328

302329
for upstream_node in upstream_nodes:
303330
if upstream_node == -1:
@@ -306,46 +333,50 @@ def _step(
306333
if is_waterbody_outflow[upstream_node]:
307334
# if upstream node is an outflow add the outflow of the waterbody
308335
# to the sideflow
309-
node_waterbody_id = waterbody_id[upstream_node]
336+
upstream_node_waterbody_id = waterbody_id[upstream_node]
310337

311338
# make sure that the waterbody ID is valid
312-
assert node_waterbody_id != -1
313-
waterbody_outflow_m3 = outflow_per_waterbody_m3[node_waterbody_id]
339+
assert upstream_node_waterbody_id != -1
340+
waterbody_outflow_m3 = outflow_per_waterbody_m3[
341+
upstream_node_waterbody_id
342+
]
314343

315-
waterbody_storage_m3[node_waterbody_id] -= waterbody_outflow_m3
344+
waterbody_storage_m3[upstream_node_waterbody_id] -= (
345+
waterbody_outflow_m3
346+
)
316347

317348
# make sure that the waterbody storage does not go below 0
318-
assert waterbody_storage_m3[node_waterbody_id] >= 0
349+
assert waterbody_storage_m3[upstream_node_waterbody_id] >= 0
319350

320-
sideflow_node += waterbody_outflow_m3 / dt / river_length[node]
351+
sideflow_node_m3 += waterbody_outflow_m3
321352

322353
elif (
323354
waterbody_id[upstream_node] != -1
324355
): # if upstream node is a waterbody, but not an outflow
325356
assert sideflow_m3[upstream_node] == 0
326357

327358
else: # in normal case, just take the inflow from upstream
328-
assert not np.isnan(Qkx[upstream_node])
329-
Qin += Qkx[upstream_node]
359+
assert not np.isnan(Qnew[upstream_node])
360+
Qin += Qnew[upstream_node]
330361

331-
Qkx[node] = update_node_kinematic(
362+
Qnew[node] = update_node_kinematic(
332363
Qin,
333364
Qold[node],
334-
sideflow_node,
365+
sideflow_node_m3 / dt / river_length[node],
335366
river_alpha[node],
336367
river_beta,
337368
dt,
338369
river_length[node],
339370
)
340-
return Qkx
371+
return Qnew, over_abstraction_m3
341372

342373
def step(
343374
self,
344375
sideflow_m3,
345376
waterbody_storage_m3,
346377
outflow_per_waterbody_m3,
347378
):
348-
Q = self._step(
379+
Q, over_abstraction_m3 = self._step(
349380
Qold=self.Q_prev,
350381
sideflow_m3=sideflow_m3,
351382
waterbody_storage_m3=waterbody_storage_m3,
@@ -364,7 +395,7 @@ def step(
364395

365396
outflow_at_pits_m3 = (Q[self.is_pit] * self.dt).sum()
366397

367-
return Q, waterbody_storage_m3, outflow_at_pits_m3
398+
return Q, over_abstraction_m3, waterbody_storage_m3, outflow_at_pits_m3
368399

369400

370401
class Accuflux(Router):
@@ -376,13 +407,6 @@ def get_available_storage(self, maximum_abstraction_ratio=0.9):
376407
assert (self.Q_prev >= 0.0).all()
377408
return self.Q_prev * self.dt * maximum_abstraction_ratio
378409

379-
def get_total_storage(self):
380-
"""
381-
Get the total storage of the river network, which is the sum of the
382-
available storage in each cell.
383-
"""
384-
return self.get_available_storage(maximum_abstraction_ratio=1.0)
385-
386410
@staticmethod
387411
@njit(cache=True)
388412
def _step(
@@ -398,6 +422,7 @@ def _step(
398422
):
399423
Qold += sideflow_m3 / dt
400424
Qnew = np.full_like(Qold, 0.0)
425+
over_abstraction_m3 = np.zeros_like(Qold, dtype=np.float32)
401426
for i in range(upstream_matrix_from_up_to_downstream.shape[0]):
402427
node = idxs_up_to_downstream[i]
403428
upstream_nodes = upstream_matrix_from_up_to_downstream[i]
@@ -440,9 +465,14 @@ def _step(
440465
if node_waterbody_id != -1:
441466
waterbody_storage_m3[node_waterbody_id] += inflow_volume
442467
else:
443-
Qnew[node] = inflow_volume / dt
468+
Qnew_node = inflow_volume / dt
469+
if Qnew_node < 0.0:
470+
# if the new discharge is negative, we have over-abstraction
471+
over_abstraction_m3[node] = -Qnew_node * dt
472+
Qnew_node = 0.0
473+
Qnew[node] = Qnew_node
444474
assert Qnew[node] >= 0.0, "Discharge cannot be negative"
445-
return Qnew
475+
return Qnew, over_abstraction_m3
446476

447477
def step(
448478
self,
@@ -453,7 +483,7 @@ def step(
453483
outflow_at_pits_m3 = (
454484
self.get_total_storage()[self.is_pit].sum() + sideflow_m3[self.is_pit].sum()
455485
)
456-
Q = self._step(
486+
Q, over_abstraction_m3 = self._step(
457487
dt=self.dt,
458488
Qold=self.Q_prev,
459489
sideflow_m3=sideflow_m3,
@@ -465,7 +495,7 @@ def step(
465495
waterbody_id=self.waterbody_id,
466496
)
467497
self.Q_prev = Q
468-
return Q, waterbody_storage_m3, outflow_at_pits_m3
498+
return Q, over_abstraction_m3, waterbody_storage_m3, outflow_at_pits_m3
469499

470500

471501
class Routing(Module):
@@ -539,12 +569,31 @@ def __init__(self, model, hydrology):
539569
/ np.sqrt(river_slope)
540570
) ** self.var.river_beta
541571

542-
self.router = Accuflux(
543-
dt=self.var.routing_step_length_seconds,
544-
ldd=ldd,
545-
mask=~self.grid.mask,
546-
Q_initial=self.grid.var.discharge_m3_s,
547-
)
572+
self.routing_algorithm = "kinematic_wave"
573+
574+
if self.routing_algorithm == "kinematic_wave":
575+
self.router = KinematicWave(
576+
dt=self.var.routing_step_length_seconds,
577+
ldd=ldd,
578+
mask=~self.grid.mask,
579+
Q_initial=self.grid.var.discharge_m3_s,
580+
river_width=self.grid.var.river_width,
581+
river_length=self.grid.var.river_length,
582+
river_alpha=self.grid.var.river_alpha,
583+
river_beta=self.var.river_beta,
584+
)
585+
elif self.routing_algorithm == "accuflux":
586+
self.router = Accuflux(
587+
dt=self.var.routing_step_length_seconds,
588+
ldd=ldd,
589+
mask=~self.grid.mask,
590+
Q_initial=self.grid.var.discharge_m3_s,
591+
)
592+
else:
593+
raise ValueError(
594+
f"Unknown routing algorithm: {self.routing_algorithm}. "
595+
"Available algorithms are 'kinematic_wave' and 'accuflux'."
596+
)
548597

549598
def spinup(self):
550599
# Initialize discharge with zero
@@ -557,9 +606,15 @@ def step(self, total_runoff, channel_abstraction_m3, return_flow):
557606
pre_storage = self.hydrology.lakes_reservoirs.var.storage.copy()
558607
pre_river_storage_m3 = self.router.get_total_storage()
559608

560-
self.router.Q_prev = (
561-
self.router.Q_prev
562-
- channel_abstraction_m3 / self.var.routing_step_length_seconds
609+
channel_abstraction_m3_per_routing_step = (
610+
channel_abstraction_m3 / self.var.n_routing_substeps
611+
)
612+
assert (
613+
channel_abstraction_m3_per_routing_step[self.grid.var.waterBodyID != -1]
614+
== 0.0
615+
).all(), (
616+
"Channel abstraction must be zero for water bodies, "
617+
"but found non-zero value."
563618
)
564619

565620
return_flow_m3_per_routing_step = (
@@ -618,6 +673,7 @@ def step(self, total_runoff, channel_abstraction_m3, return_flow):
618673
evaporation_in_rivers_m3 = 0
619674
waterbody_evaporation_m3 = 0
620675
outflow_at_pits_m3 = 0
676+
over_abstraction_m3 = 0
621677

622678
for subrouting_step in range(self.var.n_routing_substeps):
623679
self.hydrology.lakes_reservoirs.var.storage += (
@@ -647,16 +703,17 @@ def step(self, total_runoff, channel_abstraction_m3, return_flow):
647703
).all(), "outflow cannot be smaller or equal to storage"
648704

649705
side_flow_channel_m3_per_routing_step = (
650-
runoff_m3_per_routing_step + return_flow_m3_per_routing_step
706+
runoff_m3_per_routing_step
707+
+ return_flow_m3_per_routing_step
708+
- channel_abstraction_m3_per_routing_step
651709
)
652710
assert (
653711
side_flow_channel_m3_per_routing_step[self.grid.var.waterBodyID != -1]
654712
== 0
655713
).all()
656714

657715
evaporation_in_rivers_m3_per_routing_step = np.minimum(
658-
self.router.get_available_storage()
659-
+ side_flow_channel_m3_per_routing_step,
716+
self.router.get_total_storage() + side_flow_channel_m3_per_routing_step,
660717
potential_evaporation_in_rivers_m3_per_routing_step,
661718
)
662719
assert (
@@ -672,6 +729,7 @@ def step(self, total_runoff, channel_abstraction_m3, return_flow):
672729

673730
(
674731
self.grid.var.discharge_m3_s,
732+
over_abstraction_m3_routing_step,
675733
self.hydrology.lakes_reservoirs.var.storage,
676734
outflow_at_pits_m3_routing_step,
677735
) = self.router.step(
@@ -689,6 +747,7 @@ def step(self, total_runoff, channel_abstraction_m3, return_flow):
689747
actual_evaporation_from_water_bodies_per_routing_step_m3
690748
)
691749
evaporation_in_rivers_m3 += evaporation_in_rivers_m3_per_routing_step
750+
over_abstraction_m3 += over_abstraction_m3_routing_step
692751

693752
assert not np.isnan(self.grid.var.discharge_m3_s).any()
694753

@@ -700,6 +759,7 @@ def step(self, total_runoff, channel_abstraction_m3, return_flow):
700759
influxes=[
701760
total_runoff * self.grid.var.cell_area,
702761
return_flow * self.grid.var.cell_area,
762+
over_abstraction_m3,
703763
],
704764
outfluxes=[
705765
channel_abstraction_m3,

0 commit comments

Comments
 (0)