Routing
The routing module implements hillslope and channel routing algorithms.
Overview
The module provides three main routing components:
- Hillslope routing: Accumulates lateral flow contributions from upslope cells following D8 flow directions
- Channel routing: Routes water through the river network
- Reservoir routing: Optionally simulates reservoir storage dynamics with time-varying regulation and stage-discharge relationships
Hillslope and channel routing functions use Numba JIT compilation for high-performance execution.
Functions
Route lateral flow ONE STEP from upslope cells to immediate downstream neighbors.
This function performs ONE-STEP routing matching MATLAB’s hill_route.m behavior. Each cell receives flow ONLY from its immediate upstream neighbors, NOT from all upslope cells. Water moves gradually cell-by-cell over multiple timesteps.
CRITICAL: This is NOT cumulative routing! To move water from headwaters to outlets requires calling this function once per timestep for many timesteps.
PERFORMANCE: Uses Numba JIT compilation for speedup over pure Python loops.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lateral_flow
|
ndarray
|
2D array of lateral flow from each cell [m³/s]. Shape: (nrows, ncols). NaN values indicate no-data cells. |
required |
flow_direction
|
ndarray
|
2D array of flow directions in MOBIDIC notation (1-8) [dimensionless]. Shape: (nrows, ncols). Flow directions are standardized to MOBIDIC convention during preprocessing, regardless of original format. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Upstream contribution array with same shape as lateral_flow [m³/s]. |
ndarray
|
Each cell contains ONLY flow from immediate upstream neighbors (NOT its own flow). |
Notes
Flow direction coding (D8) - MOBIDIC convention from MATLAB stack8point.m: MOBIDIC notation (1-8): 7 6 5 8 · 4 1 2 3
Direction number -> (row_offset, col_offset) in matrix coordinates: 1: (-1, -1) northwest, 2: (-1, 0) north, 3: (-1, 1) northeast, 4: (0, 1) east, 5: (1, 1) southeast, 6: (1, 0) south, 7: (1, -1) southwest, 8: (0, -1) west
Special values: 0: Outlet cell (no downstream neighbor) -1: Basin outlet marker (set by preprocessor at cell with max flow accumulation)
Examples:
>>> # Simple 3x3 grid - all cells flow to center
>>> lateral_flow = np.ones((3, 3)) * 0.1 # 0.1 m³/s from each cell
>>> # Flow directions in MOBIDIC notation (all point toward center at [1,1])
>>> flow_direction = np.array([[5, 6, 7],
... [4, 0, 8], # center [1,1] is outlet (0 = no flow out)
... [3, 2, 1]]) # all 8 neighbors drain to center
>>> upstream = hillslope_routing(lateral_flow, flow_direction)
>>> upstream[1, 1] # Center receives flow from all 8 neighbors (one-step)
0.8 # 8 neighbors x 0.1 m³/s each = 0.8 m³/s (does NOT include center's own 0.1)
Source code in mobidic/core/routing.py
Route water through river network using linear reservoir method.
The linear routing method models each reach as a simple reservoir with exponential recession. Water is routed from upstream reaches to downstream reaches following the network topology, with contributions from lateral inflows.
The routing equation for each reach is
Q_out(t+dt) = C3 * Q_out(t) + C4 * qL
Where
C3 = exp(-dt/K) [recession coefficient] C4 = 1 - C3 [lateral inflow coefficient] K = lag_time_s [s] (lag time used as storage coefficient) qL = lateral inflow + integrated upstream contributions [m³/s]
PERFORMANCE: Uses Numba JIT compilation for significant speedup over pure Python loops.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
network
|
GeoDataFrame | dict
|
River network GeoDataFrame with columns: - mobidic_id: Internal reach ID (0-indexed) - upstream_1, upstream_2: Upstream reach IDs (NaN if none) - downstream: Downstream reach ID (NaN if outlet) - calc_order: Calculation order (lower values processed first) - lag_time_s: Lag time [s] (used as storage coefficient K) OR dictionary with pre-processed topology (from Simulation._preprocess_network_topology): - ‘upstream_1_idx’: numpy array of first upstream indices - ‘upstream_2_idx’: numpy array of second upstream indices - ‘n_upstream’: numpy array of upstream counts - ‘sorted_reach_idx’: numpy array of reach indices sorted by calc_order - ‘K’: numpy array of storage coefficients - ‘n_reaches’: number of reaches |
required |
discharge_initial
|
ndarray
|
Initial discharge for each reach [m³/s]. Shape: (n_reaches,). Indexed by DataFrame index (not mobidic_id). |
required |
lateral_inflow
|
ndarray
|
Lateral inflow to each reach during this time step [m³/s]. Shape: (n_reaches,). Indexed by DataFrame index (not mobidic_id). |
required |
dt
|
float
|
Time step duration [s]. |
required |
Returns:
| Type | Description |
|---|---|
tuple[ndarray, dict]
|
Tuple containing: - discharge_final: Discharge at end of time step [m³/s], shape (n_reaches,) - routing_state: Dictionary with routing diagnostics: - ‘C3’: Recession coefficients for each reach - ‘C4’: Lateral inflow coefficients for each reach - ‘qL_total’: Total inflow (lateral + upstream) for each reach [m³/s] |
Raises:
| Type | Description |
|---|---|
ValueError
|
If network is missing required columns |
ValueError
|
If array shapes don’t match network size |
Notes
- Reaches are processed in calc_order to ensure upstream reaches are computed before downstream reaches
- For upstream contributions, the function computes the mean integral of the exponential decay: ∫[C3^t dt] over the time step
- Negative discharges are clipped to zero with a warning
Examples:
>>> # Simple 2-reach network: reach 0 flows into reach 1
>>> network = gpd.GeoDataFrame({
... 'mobidic_id': [0, 1],
... 'upstream_1': [np.nan, 0],
... 'upstream_2': [np.nan, np.nan],
... 'downstream': [1, np.nan],
... 'calc_order': [0, 1],
... 'lag_time_s': [3600.0, 7200.0], # 1 hour and 2 hours
... 'geometry': [...]
... })
>>> Q_init = np.array([10.0, 5.0]) # m³/s
>>> qL = np.array([2.0, 1.0]) # m³/s lateral inflow
>>> Q_final, state = linear_channel_routing(network, Q_init, qL, dt=900)
Source code in mobidic/core/routing.py
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 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 | |
Route water through reservoirs.
This function performs reservoir water balance calculations including: - Volume update based on inflows and outflows - Stage calculation from volume - Discharge calculation from stage using regulation curves - Sub-stepping for numerical stability - Interaction with soil water balance in reservoir basins
Matching MATLAB: reservoir_routing_include.m (lines 1-225)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reservoirs_data
|
list
|
List of Reservoir objects from preprocessing |
required |
reservoir_states
|
list[ReservoirState]
|
List of current ReservoirState objects |
required |
reach_discharge
|
ndarray
|
Array of reach discharges (m3/s) |
required |
surface_runoff
|
ndarray
|
2D grid of surface runoff rates (m/s) |
required |
lateral_flow
|
ndarray
|
2D grid of lateral flow rates (m/s) |
required |
soil_wg
|
ndarray
|
2D grid of gravitational water content (m) |
required |
soil_wg0
|
ndarray
|
2D grid of gravitational water capacity (m) |
required |
current_time
|
datetime
|
Current simulation time |
required |
dt
|
float
|
Time step (s) |
required |
cell_area
|
float
|
Grid cell area (m2) |
required |
base_substeps
|
int
|
Base number of sub-steps (default: N_SUBSTEP_RESERVOIR from constants) |
N_SUBSTEP_RESERVOIR
|
Returns:
| Type | Description |
|---|---|
tuple[list[ReservoirState], ndarray, ndarray, ndarray, ndarray]
|
Tuple of: - Updated reservoir states - Modified reach discharge array - Modified surface runoff grid - Modified lateral flow grid - Modified soil_wg grid |
Source code in mobidic/core/reservoir.py
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 | |
State variables for a single reservoir.
Attributes:
| Name | Type | Description |
|---|---|---|
volume |
float
|
Current reservoir volume (m3) |
stage |
float
|
Current water stage/level (m) |
inflow |
float
|
Current inflow from upstream reaches (m3/s) |
outflow |
float
|
Current outflow/release (m3/s) |
withdrawal |
float
|
Current withdrawal for water use (m3/s) (not implemented yet) |
Source code in mobidic/core/reservoir.py
Design features
Hillslope routing
- One-step routing: Each call routes water ONE STEP to immediate downstream neighbors
- Gradual propagation: Water moves cell-by-cell over multiple timesteps
- D8 flow direction: Supports MOBIDIC notation (1-8)
- NaN handling: Robust handling of no-data cells
- Outlet detection: Handles outlet cells (flow_dir = 0 or -1)
- Numba acceleration: JIT-compiled kernel for maximum performance
Channel routing
Currently implements linear reservoir routing with the following features:
- Linear reservoir model: Simple exponential decay for each reach
- Topological routing: Processes reaches in correct upstream→downstream order
- Binary tree topology: Supports 0, 1, or 2 upstream tributaries per reach
- Mean integral method: Computes mean upstream discharge over time step
- Mass conservative: Total water balance preserved
- Configurable storage: Uses storage coefficient (K) calculated by the preprocessor from network attributes
Reservoir routing
- Volume-stage-discharge model: Simulates reservoir storage dynamics with stage-storage and stage-discharge relationships
- Cubic spline interpolation: Smooth stage-volume curves using cubic splines
- Time-varying regulation: Supports multiple regulation periods (e.g., seasonal winter/summer operations)
- Adaptive sub-stepping: Automatically divides timestep for numerical stability based on discharge variability
- Negative volume handling: Gracefully handles edge cases when reservoir would empty
- Basin zeroing: Automatically zeros surface runoff and lateral flow in reservoir basin pixels
- Network integration: Identifies inlet/outlet reaches, adds outflow to outlet lateral inflow, zeros inlet discharge
Routing equations
The linear reservoir routing equation for each reach is:
where \(Q\) is discharge, \(q_L\) is lateral inflow (surface + groundwater), \(A\) is a diagonal matrix of inverse characteristic times (\(1/K\)), and \(U\) is a binary topology matrix indicating tributary connections.
Linear reservoir model
For each reach:
Where:
- \(K\) = storage coefficient (lag time) [s]
- \(q_L\) = lateral inflow + integrated upstream contributions [m³/s]
- \(\Delta t\) = time step [s]
The mean integral of upstream discharge is computed as:
Special cases:
- \(C_3 = 1\) (\(K \to \infty\)): No attenuation, \(\overline{Q} = q_L / C_4\)
- \(C_3 \approx 0\) (\(K \to 0\)): Instant routing, \(\overline{Q} = q_L / C_4\)
Reservoir routing model
For each reservoir at each timestep:
1. Volume update:
where \(V\) is volume [m³], \(Q_{\text{in}}\) is total inflow (upstream discharge + basin contributions) [m³/s], \(Q_{\text{out}}\) is regulated discharge [m³/s], and \(W\) is withdrawal [m³/s].
2. Stage calculation:
Using cubic spline interpolation of the stage-storage curve.
3. Regulation period determination:
Based on the current date and regulation schedule, select the active regulation curve (e.g., “winter” or “summer”).
4. Discharge calculation:
Using linear interpolation of the stage-discharge curve for the active period.
5. Sub-stepping:
If discharge variability is high, divide \(\Delta t\) into \(N\) sub-steps where \(N\) is determined by:
This ensures numerical stability by limiting the rate of volume change per sub-step.
6. Negative volume handling:
If \(V(t + \Delta t) < 0\), reduce \(Q_{\text{out}}\) such that \(V(t + \Delta t) = 0\):