Generate Synthetic Riverbed Topography for Meandering Rivers
A meander belt grown from an initially straight channel, drawn in the
style of Harold Fisk's 1944 maps of the lower Mississippi. Every colour is a
channel course the model passed through; the cream channel is the river
today. More prints below.
- Publications · Citation
- Theory guide → — the physics and numerics behind the model
- What's new in v2.0.0 · Introduction · Features
- Prerequisites · Installation · Usage
- Examples · Art prints · Output files
- Migrating from v1.x · Testing
- Gallery: v1.x figures and animations
Li, Z., & Garcia, M. H. (2021). pyRiverBed: A Python framework to generate synthetic riverbed topography for constant-width meandering rivers. Computers & Geosciences, 152. doi:10.1016/j.cageo.2021.104755
Rowley, T., Konsoer, K., Langendoen, E. J., Li, Z., Ursic, M., & Garcia, M. H. (2021). Relationship of point bar morphology to channel curvature and planform evolution. Geomorphology, 375. doi:10.1016/j.geomorph.2020.107541
Sun, Y., Song, X., Li, Z., Xu, H., & Bai, Y. (2025). Analytical simulation of meander morphology from equilibrium to long-term evolution: Impacts of channel geometry and vegetation-induced coarsening. International Journal of Sediment Research. doi:10.1016/j.ijsrc.2025.02.003
@article{li2021pyriverbed,
title = {pyRiverBed: A Python framework to generate synthetic riverbed
topography for constant-width meandering rivers},
author = {Li, Zhi and Garcia, Marcelo H.},
journal = {Computers \& Geosciences},
volume = {152},
pages = {104755},
year = {2021},
doi = {10.1016/j.cageo.2021.104755}
}Every equation the model solves, where it comes from, and where it stops being
valid, is written out in THEORY_GUIDE.md:
| Section | What it covers |
|---|---|
| The idea in one page | Why bed topography is a local function of curvature |
| Channel planform | The Kinoshita curve, resampling, Savitzky–Golay smoothing, end extension |
| Curvature | Three estimators, de-spiking, filtering |
| Curvature phase lag | Why the pool sits downstream of the apex |
| Bed topography | The scour factor, the Beck profile, and the centerline depth derived from area conservation |
| 1D to 2D | Polyline offsetting at constant width |
| Mesh and BCs | The TELEMAC-ready triangulation |
| Meander migration | Linearised bend theory, the sign structure that makes meanders grow, the upstream boundary |
| Cutoffs | Neck cutoffs geometrically, chute cutoffs stochastically |
| Assumptions and limits | What the model does not do, stated plainly |
| Choosing parameters | A typical range and a calibration route for every input |
If you are going to publish something made with this tool, read that file first — particularly the assumptions section.
v2.0 is a full rewrite of the software around the same science.
Three frontends, one model
| CLI | pyriverbed run my_river.ini — scriptable, with init, convert, show and art subcommands |
| GUI | pyriverbed gui — rebuilt on ttk: tabbed, validated, threaded, with a live log and planform preview |
| Notebook | from pyriverbed.notebook import quick_run — see notebooks/pyRiverBed_demo.ipynb |
A readable input file. The v1.x steering file was a bare column of 39
numbers whose meaning was purely positional. v2 uses a commented, sectioned
key/value file, and every value is validated with a message that names the
parameter. Old steering files still load, and pyriverbed convert migrates
them.
[chute_cutoff]
enabled = yes # model chute cutoffs (needs migration)
frequency = 0.1 # probability per time step; 0.1 = 10% chance each step
entrance = apex # apex | inflection
min_sinuosity = 1.2 # the chute's slope advantage over the bend it replacesObject-oriented, no globals. RiverBedModel owns its Config, so several
models can run in one process — impossible in v1.x, where every parameter was a
module global that Numba baked into its kernels at first compile.
Fewer dependencies, faster. Numba and tabulate are gone: the numerics are vectorised NumPy and the tables are formatted in-house. Only NumPy, SciPy and Matplotlib are required. Neck cutoff detection went from an O(n²) scan of every node pair to a k-d tree query, which is what makes long runs practical. Every rewritten kernel was checked against v1.1.0 and agrees to floating-point round-off.
Real logging. Everything goes through logging, so a run can be followed
on screen, captured to a file, or piped into the GUI's log pane. Runs report
stage timings, a progress bar with an ETA, and a closing summary.
Age-shaded meander belts. Oxbow lakes are now filled according to when they were abandoned, so a belt reads as a stratigraphy instead of an undifferentiated blob.
Art prints. pyriverbed art renders a run as a print rather than a figure —
seven styles, led by a homage to Harold Fisk's 1944 maps of the Mississippi
meander belt. See the gallery.
Besides neck cutoffs, which are geometric and deterministic, pyRiverBed models chute cutoffs, where flow carves a new, shorter channel across the floodplain while the bend limbs are still well separated. Chute initiation depends on flood history, bank strength and bar topography — none of which the model resolves — so it is treated as conditionally random: the geometry decides where a chute is possible, and a random draw decides whether one happens. All of it is user-controlled:
| Parameter | Meaning |
|---|---|
enabled |
Switch chute cutoff modeling on (needs migration to be on) |
frequency |
Probability of triggering a chute cutoff in one time step, e.g. 0.1 = 10%. Calibrate as dt / recurrence_interval |
start_step |
Time steps of spin-up before chute cutoffs are allowed |
entrance |
Whether chute channels start and end at bend apexes or at inflection points |
span |
Number of entrance points a chute spans; 2 bypasses one full meander loop |
max_valley_angle |
Maximum angle between the chute chord and the valley axis; a chute aligned down-valley takes the largest slope advantage |
min_length_widths |
Minimum length of the bypassed reach, in channel widths |
min_sinuosity |
Minimum sinuosity of the bypassed reach — the slope advantage itself, and the reason a chute forms at all |
end_margin |
Entrance points kept clear of the two ends of the centerline |
Meandering rivers erode their outer banks and deposit sediment on their inner banks. This process makes point bars, which are exposed whenever the water level is relatively low and so are visible in satellite and aerial imagery. It is interesting and educational to apply this tool to manufacture your own meandering river at any scale, or to investigate meandering rivers near you.
This tool can:
- help people working on hydrodynamic and morphodynamic modeling of fluvial processes prepare their FEM triangle meshes and boundary condition files;
- help people working on field surveying interpolate bathymetry data in unexplored zones during their campaigns;
- help people working on laboratory experiments design their flumes.
- Two modes:
- synthetic meandering rivers via the built-in Kinoshita curve calculator;
- your own real river centerlines, read from file.
- Expands the 1D centerline to a 2D river channel by polyline offsetting.
- Calculates riverbed topography through an analytical method.
- Simulates meander channel migration with a linear bend-theory model.
- Detects neck cutoffs geometrically and models chute cutoffs stochastically, recording every oxbow lake.
- Writes finite element mesh and boundary condition files for TELEMAC modeling.
- Renders art prints of a run: seven styles, led by a Harold Fisk homage.
- Cross-platform: macOS, Linux and Windows.
- Three interchangeable frontends: CLI, GUI and Jupyter notebook.
- Python >= 3.10
- NumPy
- SciPy
- Matplotlib
- tkinter — for the GUI only; it ships with most Python installations
- imageio — optional, only to write animated GIFs
- ipywidgets — optional, only for the notebook's interactive form
Using conda and a virtual environment is recommended:
conda create -n pyriverbed python numpy scipy matplotlib imageio
conda activate pyriverbedOR, with pip:
pip3 install numpy scipy matplotlib imageioOn some versions of
Ubuntu in Windows Subsystem for Linux, tkinter is
not installed with Python 3; sudo apt install python3-tk, or the equivalent on
other platforms, fixes it.
The following two are recommended, but not required:
- PyInstaller, to freeze the code and its dependencies into a single package, i.e. to make executables;
- Gifsicle, a command-line tool to optimize GIFs, and its Python wrapper pygifsicle:
from pygifsicle import optimize
optimize("path_to_my_gif.gif")git clone https://github.com/ZhiLiHydro/pyRiverBed.git
cd pyRiverBed
pip install -e .That puts a pyriverbed command on your path. Without installing, everything
still works through python3 -m pyriverbed from the repository root.
pyriverbed init # write a commented pyriverbed.ini to start from
pyriverbed run # run it
pyriverbed run my_river.ini -o out # run a named file, output into out/
pyriverbed convert steering.txt # migrate a v1.x steering file to the v2 format
pyriverbed show my_river.ini # echo the configuration with defaults filled in
pyriverbed art my_river.ini # run, then render art prints of the result
pyriverbed gui # open the graphical interfaceUseful flags: -n/--steps to override the number of time steps, --seed to
make a stochastic run reproducible, --dry-run to validate a configuration
without computing anything, and -v/-q for more or less output.
A run narrates itself:
+> Building channel planform ...
Building channel planform done in 3 ms
nodes 1069
channel length 32.03 m
sinuosity 3.1632
+> Writing riverbed, bankline and mesh files ...
wrote kinoshita_topo.xyz (22449 points)
wrote kinoshita_mesh.t3s, kinoshita_mesh.dat, ... (22449 nodes, 42720 triangles)
migrating [########################......] 80% 8000/10000 eta 24 s sinuosity 2.914, 7 cutoffs
pyriverbed gui opens a tabbed window. Parameters are grouped and validated as
you type, the planform can be previewed before committing to a run, the model
runs on a worker thread so the window stays responsive and can be stopped, and
the log appears in the window rather than in a terminal behind it.
Planform tab, previewing a real centerline before the run starts:
Cutoffs tab, with the neck and chute cutoff criteria side by side:
notebooks/pyRiverBed_demo.ipynb walks
through the whole model, from a one-line run to the effect of every parameter:
from pyriverbed.notebook import quick_run, show, cross_section
result = quick_run(n_bends=3, width=0.6, depth=0.15)
show(result)
result.summary()The results are plain objects — result.bed.z, result.cloud,
result.centerline.curvature — so you can take the synthetic bed and do your
own analysis with it.
import pyriverbed as prb
config = prb.default_config()
config.channel.width = 0.8
config.migration.enabled = True
config.chute_cutoff.enabled = True
config.chute_cutoff.frequency = 0.05
result = prb.RiverBedModel(config).run()
print(result.summary())Task: reproduce the flume studied in this paper.
The defaults are exactly this case, so pyriverbed init followed by
pyriverbed run reproduces it with nothing to change. In the GUI, press
Run pyRiverBed; in a notebook, quick_run().
The bar–pool asymmetry is the heart of the bed model, and it is worth looking at
directly. The flow depth grows linearly towards the outer bank, where the
bed is a scour surface, and decays exponentially towards the inner bank,
where it is the depositional face of a point bar. The depth on the centerline is
not free: it follows from requiring the cross-sectional area to stay equal to
width × depth, so deepening a pool automatically raises the opposite bar.
Task: read centerline coordinates from a file.
The centerline of a randomly picked reach (at 7°32'09.9"S 72°31'16.0"W) of a
randomly picked river — the Juruá River in Brazil — was digitised manually (a
proper centerline extraction tool is recommended for real cases) on a
georeferenced TIFF map, and saved to jurua.txt. Set mode to centerline,
file name to jurua.txt, width to 160 m (estimated), depth to 8 m (arbitrary),
slope to 0 (arbitrary) and lag strength to 6 (estimated); keep the defaults for
everything else.
[run]
mode = centerline
centerline_file = example_centerlines/jurua.txt
[channel]
width = 160
depth = 8
[lag]
strength = 6
[curvature]
smoothing_level = 50 # raise until the curvature reads as a bend sequencesmoothing_level is the parameter that matters most for a real centerline.
Curvature is a second derivative, so digitising noise turns into spurious pools.
Raise it until the curvature signal reads as a sequence of bends rather than
noise — and no further, because over-smoothing flattens the bend apexes and
under-predicts the pool depths.
Eight example centerlines ship in
example_centerlines/: jurua.txt,
mackey_wabash.txt, maier_horseshoe_wabash.txt, shimanto.txt,
straight.txt, trinity.txt, ucayali.txt and white.txt.
Switching migration on steps the planform forward in time. Bends grow, translate downstream, and eventually cut off — by neck when a loop closes on itself, or by chute when flow takes a shorter path across the floodplain. Every abandoned loop is recorded as an oxbow lake and shaded by its age.
[migration]
enabled = yes
n_steps = 20000
dt = 86400 # s
e0 = 2e-7 # 1/s -- e0 * dt is the displacement per step, in widths
ub0 = 2 # inlet noise; needed to grow bends from a straight channel
[curvature]
migration_smoothing_level = 1 # keep small, or meanders diffuse away
[neck_cutoff]
enabled = yes
[chute_cutoff]
enabled = yes
frequency = 0.02 # a 2% chance per step, i.e. one per ~50 steps
start_step = 500
min_sinuosity = 1.3A run also writes a diagnostics figure. Sinuosity climbs while the bends grow
and drops abruptly at every cutoff; that sawtooth is the signature of a meander
belt in dynamic equilibrium. The mean migration rate is the check that
e0 × dt is giving a physically sensible rate.
| Synthetic riverbed | Meander belt |
|---|---|
![]() |
![]() |
Chute cutoff frequency is the one cutoff parameter with no geometric meaning to
fall back on — it stands in for flood history, bank strength and bar topography,
none of which the model resolves — so it has to be calibrated. This is what it
buys you. Three runs, identical in every respect including the random seed,
differing only in chute_cutoff.frequency:
chute_cutoff |
Recurrence at dt = 1 day |
Result over 12,000 steps | |
|---|---|---|---|
| Neck only | enabled = no |
— | 152 neck, 0 chute — sinuosity 2.96 |
| Infrequent chutes | frequency = 0.0005 |
one per ~5 years | 146 neck, 5 chute — sinuosity 2.79 |
| Frequent chutes | frequency = 0.01 |
one per ~3 months | 93 neck, 92 chute — sinuosity 2.30 |
Reproduce it with:
pyriverbed run examples/05_chute_frequency.ini -o out_chuteTwo things are worth reading off the panels.
Chute cutoffs substitute for neck cutoffs rather than adding to them. Going from no chutes to one every three months replaces 59 neck cutoffs with 92 chute cutoffs; the total only rises from 152 to 185. A chute takes a bend out before its limbs can close, so the neck cutoff that bend was heading for never happens. The cumulative-cutoff panel shows the two curves separating slowly, not by the factor you would get if chutes were simply extra events.
What chutes really change is the ceiling on sinuosity. With neck cutoffs alone, every bend grows until it closes on itself, so sinuosity repeatedly ratchets up towards 4 before collapsing. Frequent chutes truncate bends earlier and hold sinuosity near 2.5, visibly below the other two runs for the whole run. The migration rate is almost unaffected: chute frequency does not change how fast banks move, only how long a bend is allowed to survive.
At a realistic recurrence — the infrequent case, five chutes in the 25 years for
which they were allowed — the effect on the reach-scale statistics is small. That is the honest answer for most
rivers, and it is why frequency should be calibrated against an observed cutoff
record rather than guessed: set it to dt / recurrence_interval, and run several
seeds before drawing conclusions from any one of them.
A migration run produces, almost incidentally, something people hang on walls.
In 1944 the geologist Harold Fisk mapped the meander belt of the lower Mississippi for the US Army Corps of Engineers. To show every course the river had taken he drew each one in its own flat colour and let them overlap — the 1880 channel in green, 1820 in salmon-pink, 1765 in light blue, and behind them the prehistoric courses, a palimpsest on cream paper. The sheets were an appendix to a dry government report. They are now the most reproduced piece of fluvial geomorphology there is, sold as prints in a hundred shops.
That is exactly the data pyRiverBed generates: a sequence of channel courses,
and an oxbow lake stamped with the step it was abandoned. So
pyriverbed/art.py draws it the way Fisk drew a survey.
pyriverbed art examples/04_straight_to_meandering.inipyriverbed art my_river.ini -s fisk --paper a2 --art-dpi 300 --format pdfWhere plotting.py draws figures — labelled axes,
colour bars, everything needed to check a number — art.py draws prints: no
axes, no ticks, no legend. The channel is a filled ribbon of its own width, and
the composition is the whole content. Seven styles ship:
Every historical course in its own flat colour, oldest at the back, oxbow lakes in the colour of the era that abandoned them. The live channel is drawn in paper rather than ink — Fisk's "mighty blank" — so the present river reads as the hole punched through every course that came before it. Older courses carry a hatch as well as a hue, which is how Fisk kept overlapping inks apart and what keeps the print legible in greyscale.
The same courses separated by lightness instead of hue, so the print reads as a single object with depth in time rather than a stack of maps. Overprinting density stands for how long the river stayed put.
Pale channel on Prussian blue with drafting furniture. The grid is spaced in channel widths, which is the scale the model actually thinks in, and it doubles as the texture.
The channel stroked several times at increasing width and falling alpha, which fakes a bloom with no image filter and no extra dependency.
The two styles that show what pyRiverBed actually computes: pools against the
outer banks, point bars against the inner ones. bathymetry fills the depth
bands; contour keeps the lines and drops the fill.
bathymetry |
contour |
|---|---|
![]() |
![]() |
from pyriverbed import art
from pyriverbed.notebook import quick_run, show_art, art_styles
result = quick_run("examples/04_straight_to_meandering.ini")
show_art(result, "fisk") # inline, screen resolution
art.render(result, "fisk", art.ArtStyle(paper="a2", dpi=300),
paths=["mississippi.pdf"]) # vector, print resolution
art.save_gallery(result, "prints") # every style at once
print(art_styles())ArtStyle controls the paper (fit, the default, shapes the canvas to the
reach; or a4/a3/a2/letter/tabloid/square), the resolution, how many
courses to draw, the cartouche text, the scale bar and the paper texture. Output
format follows the file extension, so .pdf and .svg give vector prints.
| File | Format | Use |
|---|---|---|
*_topo.xyz |
3-column ASCII point cloud | Blue Kenue, GIS, any interpolator |
*_boundary.i2s |
closed polyline | the banklines, for Blue Kenue |
*_mesh.t3s |
Blue Kenue T3 mesh | build a TELEMAC Selafin geometry file |
*_mesh.dat |
Tecplot FETRIANGLE | visualisation |
*_BC.cli |
TELEMAC boundary conditions | use directly in a TELEMAC run |
*_BC.bc2 |
Blue Kenue BC | inspect and edit BC codes and metadata |
*_pyriverbed.png/pdf |
figure | the three-panel summary |
*_diagnostics.png |
figure | sinuosity and migration rate time series |
*_sinuosity.txt, *_mean_migration_rate.txt |
ASCII | one value per time step |
*_migration0.gif, *_migration1.gif |
animation | the bed and the meander belt |
art/*_<style>.png |
art prints, from pyriverbed art |
|
pyriverbed.log |
text | the full run log |
THEORY_GUIDE.md— the physics and the numerics, every governing equation, and an explicit statement of the assumptions and their limits. Section index above.notebooks/pyRiverBed_demo.ipynb— a guided tour of every parameter, in 13 sections.examples/— five commented input files, and a note on the handful of parameters that interact.pyriverbed.ini— written bypyriverbed init, with a one-line explanation beside every parameter.
| v1.x | v2.0 |
|---|---|
python3 gui4pyriverbed.py |
pyriverbed gui |
python3 pyriverbed.py |
pyriverbed run |
steering.txt (39 positional numbers) |
pyriverbed.ini (commented sections) |
| — | pyriverbed convert steering.txt |
Old steering files are read as they are, so nothing has to be converted to keep working. A v1.x file is loaded in a compatibility mode that reproduces v1.x output; when you convert one to the v2 format, three settings are worth bringing up to the v2 defaults:
curvature.migration_smoothing_level = 2. A v1.x file smooths as many times per migration step as it does when building the centerline, which for a long run means the smoother flattens the meanders faster than the model grows them.migration.end_taper_widths = 2andneck_cutoff.end_margin_widths = 2. These keep the reach ends, where the artificial straight extensions and the inlet transient live, out of the migration and out of the cutoff search.flip.transverse = no. The converter sets this for you; it is now a purely cosmetic mirror of the bed about the centerline.
python -m unittest discover -s tests -v84 tests, using only the standard library's unittest, covering the
configuration round-trip and legacy reader, every curvature estimator against
an analytical circle, the bed model's area conservation, polyline offsetting
against analytical offsets, mesh connectivity, both cutoff mechanisms, the end
taper, every art style, the CLI and notebook APIs, and the LaTeX in these docs
against the constructs GitHub's Markdown renderer silently breaks.
These were produced with pyRiverBed v1.x and are kept here as a record of the project. The physics is the same; only the plotting and the frontends have changed.
Mode 1: Kinoshita curve, annotated with the Blue Kenue and TELEMAC output
Mode 2: the Juruá River, with the Landsat imagery it was digitised from
| Kinoshita curve — synthetic riverbed | Kinoshita curve — river centerline |
|---|---|
![]() |
![]() |
| Juruá River — synthetic riverbed | Juruá River — river centerline |
|---|---|
![]() |
![]() |

























