Skip to content

File Schemas

Watershed Description Files

Network File

{
  "network_file": "/path/to/network.parquet"
}

The network file is a parquet file. It has 1 row per river in the watershed, and a network stabilized by Network.write_stabilized also has 1 row per sub-reach it added. Required for all routing:

Column Data Type Description
riverId integer Unique ID of a river segment
nextRiverId integer ID of downstream river segment, or -1 for outlet reaches
muskingumK float Muskingum k parameter (length / velocity), in seconds
muskingumX float Muskingum x parameter, expected in [0, 0.5]
riverIndex integer Position of the river in depth first search order, one more than the row before
upstreamCount integer Number of rivers upstream of the river, not counting itself

muskingumK and muskingumX may be stored in any float type. A Network holds them in float64 whatever the file stores, and writes them in float64.

Optional columns:

Column Data Type Description
synthetic boolean Written by Network.write_stabilized: True for an added sub-reach
parentRiverId integer Written by Network.write_stabilized: the river each sub-reach was split from

A network file with synthetic rows routes with the channel, grid, and ecmwf_grib forcings. The catchment forcing refuses it, since a catchment runoff file has one row per river and none for the added sub-reaches.

These columns typically come from preprocessing and calibration workflows:

  1. topology (riverId, nextRiverId) from vector network processing
  2. depth first search order (riverIndex, upstreamCount) from a depth first search of that topology
  3. channel routing (muskingumK, muskingumX) from hydraulic assumptions and/or calibration

River Ordering Requirement

Rows (rivers) must be sorted in depth first search (DFS) order: each river comes after every river upstream of it, and the rivers upstream of a river are the rows immediately before it, so every river's whole upstream watershed is one contiguous range of rows ending at that river. riverIndex numbers the rows in that order and upstreamCount counts each river's upstream rivers, so a river's watershed is the rows from its riverIndex minus its upstreamCount to its riverIndex. That is what divides the rivers into blocks that route concurrently, and what rr subset cuts a basin with.

river-route never reorders the network table, so every file with one entry per river (grid weights, catchment runoff, channel state) must list the rivers in this same order, and routing refuses runoff that does not. Configs.deep_validate checks the order and both columns against the topology:

import river_route as rr

rr.Configs(network_file='/path/to/network.parquet').deep_validate()

examples/migrate_v2_to_v3.py sorts a v2 params file into DFS order, writes both columns, and reorders the files that follow it.

Forcing Files

You need a time series of per-catchment runoff to be routed. It is given as forcing_files, read in the form forcing names:

  1. catchment: files already aggregated to catchments
  2. grid: gridded runoff depths with x and y dimensions, aggregated with a weight table (grid_weights_file)
  3. ecmwf_grib: ECMWF GRIB files of runoff depths on a reduced gaussian grid, aggregated with a weight table

Runoff Depths Warning

There are many projections for grid cells, different names of variables, various file formats, and units of the runoff depths. You should be certain you can correctly calculate catchment volumes from runoff depth grids separately before using the calculations performed by river-route. Do not blindly trust this result!

Pre-aggregated Catchment Files

{
  "forcing": "catchment",
  "forcing_files": [
    "/path/to/catchment_runoff.nc"
  ]
}

Ordering River IDs

The riverId values must be the same values and order as the riverId column of the network file

Catchment runoff is given as netcdf with 2 dimensions, riverId and time, in that order, so each river's series is contiguous and is read straight into the river major arrays the router works in. The riverId dimension must contain exactly the same IDs and be sorted in the same order as the riverId column of the network file. The names and the order are fixed and cannot be configured:

Variable Dimensions Description
catchment_runoff (riverId, time) Incremental runoff of each catchment per step, as a volume or a depth
catchment_area (riverId,) Area of each catchment in m², the factor between depths and volumes

The units attribute of catchment_runoff is required and says which form it takes: m3 for volumes, or a depth unit (m or mm). Depths and volumes are equivalent: routing uses volumes, so depths are converted to meters and multiplied by catchment_area when they are read. catchment_runoff names the area variable with the CF attribute cell_measures = "area: catchment_area". RunoffForcing.to_netcdf writes this schema, and the grid runoff classes write it from their grids with aggregate_to_file.

Gridded Runoff Depths

{
  "forcing": "grid",
  "forcing_files": [
    "/path/to/grid1.nc",
    "/path/to/grid2.nc"
  ],
  "grid_weights_file": "/path/to/weight_table.nc"
}

Ordering River IDs

The riverId values must be the same values and order as the riverId column of the network file

Runoff depths are given in a netCDF file with 3 dimensions: time, y, and x. The dimension names can be overridden with var_t, var_y, and var_x. The runoff depth variable name can be overridden with var_runoff (default 'ro').

With accumulation_type: cumulative, each file's runoff is a running total, differenced into each step's runoff within that file: the first step of a file is its total at that step, never differenced against the last step of the file before it. This is the convention of forecast files, each accumulating from the start of its own forecast. Files that continue one accumulation, such as one forecast split into several files, are each routed as if their accumulation began at their first step.

Weights need to be recomputed if the grid resolution, grid extent, or catchment boundaries change. The grid weights netCDF has the following variables, each with the one dimension index, a row per weight:

Column Data Type Description
riverId integer Unique ID of a river segment
x_index integer The x index of the runoff grid cell that overlaps with the catchment boundary
y_index integer The y index of the runoff grid cell that overlaps with the catchment boundary
x float The x coordinate of the runoff grid cell
y float The y coordinate of the runoff grid cell
area_sqm float Area of the grid cell–catchment overlap in square meters
proportion float Fraction of catchment area covered by this grid cell, sums to 1.0 per riverId

The names are fixed. A weight table made before v3 names the river id river_id, and must be renamed riverId before it is read. The weight table functions of river_route.forcings.weights read the catchments' riverId column and write these names.

Reduced Gaussian Grid Runoff Depths

{
  "forcing": "ecmwf_grib",
  "forcing_files": [
    "/path/to/ro_20260927_00z_cf.grib"
  ],
  "grid_weights_file": "/path/to/gridweights_O1280.nc"
}

Runoff depths are given as ECMWF GRIB files on a global reduced gaussian grid, such as the octahedral O1280 grid of the IFS. This forcing is specialized to that format; runoff in any other form must be prepared as catchment or grid forcing. Every message whose shortName is var_runoff (default 'ro') is read with eccodes as one time step, in the order of the files and of their messages, at its validity date and time. Choosing files whose grid, ensemble member, and steps suit the weight table and the routing is left to the caller. IFS forecast runoff accumulates from the start of the forecast, so this forcing is always cumulative, differenced within each file as gridded cumulative runoff is, and has no accumulation_type option.

river_route.forcings.ReducedGaussianGrid.from_grib reads the grid of a file from its metadata: N and the number of cells on each row. Its cell_polygons are the area each cell represents, one polygon per cell of the world in cell order: a box of longitude and latitude halfway to the cells beside it and between latitude edges that give each row of cells the area of its gaussian quadrature weight, in two parts on either edge of the map for the cells centered on 180 degrees. river_route.forcings.reduced_grid_weights intersects those boxes with the catchments. Its weight table has the columns of the table above with cell_index, the position of the cell in the values of a GRIB message, in place of x_index and y_index.

Channel State Files

A channel state file is a parquet file with two columns, one row per river, listing the rivers of the network file in the same order. channel_state_final_file is written in this format, so a final state starts the next run as its channel_state_init_file.

Column Type Description
riverId integer ID of the river, in the order of the network file
Q float Discharge of the river in m³/s at the end of the run

With use_subreaches, the default, a state file has one row per sub-reach instead, each river's sub-reaches upstream to downstream with its riverId repeated on each. Routing refuses a state file without riverId or whose rivers are not the rivers of the network file in order.

Output Files

Routed Discharge

Routed discharge is written by river_route.router.writers.zarr_writer unless another writer is set, to a zarr store with 2 dimensions: riverId and time. It has 1 variable named Q of shape (riverId, time) and dtype float32, chunked so that each chunk holds every time step of a block of rivers.

The values are rounded to writers.ZARR_KEEPBITS mantissa bits, a relative error of at most 2^-13, and each chunk is compressed with writers.ZARR_COMPRESSOR, Blosc lz4 with bitshuffle. On a year of the Amazon that is 2.71x smaller than the raw array and faster to write than storing it uncompressed, since less of it reaches the disk.

The river dimension comes first in every array format, because that is the layout the kernels write in place: each river's whole series is contiguous. That is also the layout a writer is handed, as a C-order (river, time) array, so nothing is transposed on the way to the file. Writing (time, riverId) instead costs about twice the kernel time on a large network, since each river's series then has to be transposed out in blocks.

river_route.router.writers.netcdf_writer writes the same (riverId, time) layout to an uncompressed netCDF file.