GRLP random networks: generate and analyze at scale

The hand-built 5-segment network showed the mechanics of confluences. This capstone does two things that only make sense at scale:

  1. Generate a whole river network with no DEM required — a random Shreve (1966, 1974) binary tree, wired up with discharges and widths automatically.

  2. Analyze its structure — Strahler stream orders, Horton ratios, and Hack-type scaling — the descriptors that characterize real drainage networks.

This is the quickest way to get a runnable GRLP network for experiments. Reference: McNab et al. (2025, ESurf).

import random

import numpy as np
from matplotlib import pyplot as plt

import grlp

# Seed both RNGs for a reproducible network; drop these to draw a new one.
random.seed(7)
np.random.seed(7)

1. Generate a Shreve random network

generate_random_network builds the topology and populates it with node spacing, discharges (accumulating downstream), and valley widths. The main knobs:

  • magnitude — the number of channel heads (the network’s “size”).

  • max_length — the length of the longest source-to-outlet path [m].

  • mean_discharge — sets segment discharges from drainage area.

It returns the Network and its topology object.

net, topo = grlp.generate_random_network(
    magnitude=8,
    max_length=2.0e4,
    mean_discharge=10.,
)
net.set_niter(3)
net.get_z_lengths()
print('%d segments, %d channel heads'
      % (len(net.segments),
         len(net.list_of_channel_head_segment_IDs)))
15 segments, 8 channel heads

2. Evolve to steady state and plot the long profiles

Exactly as for the hand-built network — one call evolves the whole graph.

net.evolve_threshold_width_river_network(nt=200, dt=3.15e11)

plt.figure(figsize=(10, 6))
for lp in net.segments:
    if lp.downstream_segment_IDs:
        ds = net.segments[lp.downstream_segment_IDs[0]]
        plt.plot([lp.x[-1] / 1000., ds.x[0] / 1000.],
                 [lp.z[-1], ds.z[0]], 'k-', lw=1, alpha=0.5)
    else:
        plt.plot([lp.x[-1] / 1000., lp.x_ghost_downstream / 1000.],
                 [lp.z[-1], lp.z_bl], 'k-', lw=1, alpha=0.5)
    plt.plot(lp.x / 1000., lp.z, '-', lw=2)
plt.xlabel('Downstream distance [km]', fontsize=14)
plt.ylabel('Elevation [m]', fontsize=14)
plt.title('Shreve random network at steady state')
plt.tight_layout()
plt.show()
../_images/c686e04695bec29cd043f94e162a682fb1b0cf2f875c0300c669fee1a979859f.png

3. The network planform

Network.plot() draws the branching map view — the dendritic pattern of the random tree.

net.plot()
../_images/e0af4d2128f5350e1ef20015b59d11557f6814a256285b16e6413f6cde03e99d.png
{0: {'x': array([17.14285714, 17.24489796, 17.34693878, 17.44897959, 17.55102041,
         17.65306122, 17.75510204, 17.85714286, 17.95918367, 18.06122449,
         18.16326531, 18.26530612, 18.36734694, 18.46938776, 18.57142857,
         18.67346939, 18.7755102 , 18.87755102, 18.97959184, 19.08163265,
         19.18367347, 19.28571429, 19.3877551 , 19.48979592, 19.59183673,
         19.69387755, 19.79591837, 19.89795918, 20.        , 20.        ]),
  'y': array([2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.,
         2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.])},
 1: {'x': array([14.28571429, 14.3877551 , 14.48979592, 14.59183673, 14.69387755,
         14.79591837, 14.89795918, 15.        , 15.10204082, 15.20408163,
         15.30612245, 15.40816327, 15.51020408, 15.6122449 , 15.71428571,
         15.81632653, 15.91836735, 16.02040816, 16.12244898, 16.2244898 ,
         16.32653061, 16.42857143, 16.53061224, 16.63265306, 16.73469388,
         16.83673469, 16.93877551, 17.04081633, 17.14285714, 17.14285714]),
  'y': array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
         1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 2.])},
 2: {'x': array([14.28571429, 14.3877551 , 14.48979592, 14.59183673, 14.69387755,
         14.79591837, 14.89795918, 15.        , 15.10204082, 15.20408163,
         15.30612245, 15.40816327, 15.51020408, 15.6122449 , 15.71428571,
         15.81632653, 15.91836735, 16.02040816, 16.12244898, 16.2244898 ,
         16.32653061, 16.42857143, 16.53061224, 16.63265306, 16.73469388,
         16.83673469, 16.93877551, 17.04081633, 17.14285714, 17.14285714]),
  'y': array([3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3.,
         3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 2.])},
 3: {'x': array([11.42857143, 11.53061224, 11.63265306, 11.73469388, 11.83673469,
         11.93877551, 12.04081633, 12.14285714, 12.24489796, 12.34693878,
         12.44897959, 12.55102041, 12.65306122, 12.75510204, 12.85714286,
         12.95918367, 13.06122449, 13.16326531, 13.26530612, 13.36734694,
         13.46938776, 13.57142857, 13.67346939, 13.7755102 , 13.87755102,
         13.97959184, 14.08163265, 14.18367347, 14.28571429, 14.28571429]),
  'y': array([2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.,
         2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 3.])},
 4: {'x': array([11.42857143, 11.53061224, 11.63265306, 11.73469388, 11.83673469,
         11.93877551, 12.04081633, 12.14285714, 12.24489796, 12.34693878,
         12.44897959, 12.55102041, 12.65306122, 12.75510204, 12.85714286,
         12.95918367, 13.06122449, 13.16326531, 13.26530612, 13.36734694,
         13.46938776, 13.57142857, 13.67346939, 13.7755102 , 13.87755102,
         13.97959184, 14.08163265, 14.18367347, 14.28571429, 14.28571429]),
  'y': array([4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4.,
         4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 3.])},
 5: {'x': array([ 8.57142857,  8.67346939,  8.7755102 ,  8.87755102,  8.97959184,
          9.08163265,  9.18367347,  9.28571429,  9.3877551 ,  9.48979592,
          9.59183673,  9.69387755,  9.79591837,  9.89795918, 10.        ,
         10.10204082, 10.20408163, 10.30612245, 10.40816327, 10.51020408,
         10.6122449 , 10.71428571, 10.81632653, 10.91836735, 11.02040816,
         11.12244898, 11.2244898 , 11.32653061, 11.42857143, 11.42857143]),
  'y': array([3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3.,
         3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 4.])},
 6: {'x': array([5.71428571, 5.81632653, 5.91836735, 6.02040816, 6.12244898,
         6.2244898 , 6.32653061, 6.42857143, 6.53061224, 6.63265306,
         6.73469388, 6.83673469, 6.93877551, 7.04081633, 7.14285714,
         7.24489796, 7.34693878, 7.44897959, 7.55102041, 7.65306122,
         7.75510204, 7.85714286, 7.95918367, 8.06122449, 8.16326531,
         8.26530612, 8.36734694, 8.46938776, 8.57142857, 8.57142857]),
  'y': array([2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.,
         2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 3.])},
 7: {'x': array([5.71428571, 5.81632653, 5.91836735, 6.02040816, 6.12244898,
         6.2244898 , 6.32653061, 6.42857143, 6.53061224, 6.63265306,
         6.73469388, 6.83673469, 6.93877551, 7.04081633, 7.14285714,
         7.24489796, 7.34693878, 7.44897959, 7.55102041, 7.65306122,
         7.75510204, 7.85714286, 7.95918367, 8.06122449, 8.16326531,
         8.26530612, 8.36734694, 8.46938776, 8.57142857, 8.57142857]),
  'y': array([4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4.,
         4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 3.])},
 8: {'x': array([2.85714286, 2.95918367, 3.06122449, 3.16326531, 3.26530612,
         3.36734694, 3.46938776, 3.57142857, 3.67346939, 3.7755102 ,
         3.87755102, 3.97959184, 4.08163265, 4.18367347, 4.28571429,
         4.3877551 , 4.48979592, 4.59183673, 4.69387755, 4.79591837,
         4.89795918, 5.        , 5.10204082, 5.20408163, 5.30612245,
         5.40816327, 5.51020408, 5.6122449 , 5.71428571, 5.71428571]),
  'y': array([3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3.,
         3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 4.])},
 9: {'x': array([2.85714286, 2.95918367, 3.06122449, 3.16326531, 3.26530612,
         3.36734694, 3.46938776, 3.57142857, 3.67346939, 3.7755102 ,
         3.87755102, 3.97959184, 4.08163265, 4.18367347, 4.28571429,
         4.3877551 , 4.48979592, 4.59183673, 4.69387755, 4.79591837,
         4.89795918, 5.        , 5.10204082, 5.20408163, 5.30612245,
         5.40816327, 5.51020408, 5.6122449 , 5.71428571, 5.71428571]),
  'y': array([5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5.,
         5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 4.])},
 10: {'x': array([0.        , 0.10204082, 0.20408163, 0.30612245, 0.40816327,
         0.51020408, 0.6122449 , 0.71428571, 0.81632653, 0.91836735,
         1.02040816, 1.12244898, 1.2244898 , 1.32653061, 1.42857143,
         1.53061224, 1.63265306, 1.73469388, 1.83673469, 1.93877551,
         2.04081633, 2.14285714, 2.24489796, 2.34693878, 2.44897959,
         2.55102041, 2.65306122, 2.75510204, 2.85714286, 2.85714286]),
  'y': array([4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4.,
         4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 4., 5.])},
 11: {'x': array([0.        , 0.10204082, 0.20408163, 0.30612245, 0.40816327,
         0.51020408, 0.6122449 , 0.71428571, 0.81632653, 0.91836735,
         1.02040816, 1.12244898, 1.2244898 , 1.32653061, 1.42857143,
         1.53061224, 1.63265306, 1.73469388, 1.83673469, 1.93877551,
         2.04081633, 2.14285714, 2.24489796, 2.34693878, 2.44897959,
         2.55102041, 2.65306122, 2.75510204, 2.85714286, 2.85714286]),
  'y': array([6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6.,
         6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 5.])},
 12: {'x': array([ 8.57142857,  8.67346939,  8.7755102 ,  8.87755102,  8.97959184,
          9.08163265,  9.18367347,  9.28571429,  9.3877551 ,  9.48979592,
          9.59183673,  9.69387755,  9.79591837,  9.89795918, 10.        ,
         10.10204082, 10.20408163, 10.30612245, 10.40816327, 10.51020408,
         10.6122449 , 10.71428571, 10.81632653, 10.91836735, 11.02040816,
         11.12244898, 11.2244898 , 11.32653061, 11.42857143, 11.42857143]),
  'y': array([6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6.,
         6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 6., 4.])},
 13: {'x': array([5.71428571, 5.81632653, 5.91836735, 6.02040816, 6.12244898,
         6.2244898 , 6.32653061, 6.42857143, 6.53061224, 6.63265306,
         6.73469388, 6.83673469, 6.93877551, 7.04081633, 7.14285714,
         7.24489796, 7.34693878, 7.44897959, 7.55102041, 7.65306122,
         7.75510204, 7.85714286, 7.95918367, 8.06122449, 8.16326531,
         8.26530612, 8.36734694, 8.46938776, 8.57142857, 8.57142857]),
  'y': array([5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5.,
         5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 5., 6.])},
 14: {'x': array([5.71428571, 5.81632653, 5.91836735, 6.02040816, 6.12244898,
         6.2244898 , 6.32653061, 6.42857143, 6.53061224, 6.63265306,
         6.73469388, 6.83673469, 6.93877551, 7.04081633, 7.14285714,
         7.24489796, 7.34693878, 7.44897959, 7.55102041, 7.65306122,
         7.75510204, 7.85714286, 7.95918367, 8.06122449, 8.16326531,
         8.26530612, 8.36734694, 8.46938776, 8.57142857, 8.57142857]),
  'y': array([7., 7., 7., 7., 7., 7., 7., 7., 7., 7., 7., 7., 7., 7., 7., 7., 7.,
         7., 7., 7., 7., 7., 7., 7., 7., 7., 7., 7., 7., 6.])}}

4. Analyze the network structure

Random Shreve networks reproduce the statistical laws of real drainage networks. GRLP computes the classic descriptors.

Strahler stream order — headwater streams are order 1; two streams of order n join to make order n+1.

net.compute_strahler_orders()
print('Strahler order of each segment:',
      [int(o) for o in net.segment_orders])
Strahler order of each segment: [3, 1, 3, 1, 3, 2, 1, 2, 1, 2, 1, 1, 2, 1, 1]

Horton ratios — the near-constant ratios between successive stream orders (Horton 1945; Schumm 1956): how quickly stream number, length, and discharge change from one order to the next. (A stream of a given order is a maximal chain of same-order segments, so there are fewer streams than segments at the higher orders.)

net.compute_horton_ratios()
print('Number of streams per order:',
      {int(k): int(v) for k, v in net.order_counts.items()})
print('Bifurcation ratio (R_B): %.2f' % net.bifurcation_ratio)
print('Length ratio      (R_L): %.2f' % net.length_ratio)
print('Discharge ratio   (R_Q): %.2f' % net.discharge_ratio)
Number of streams per order: {1: 8, 2: 2, 3: 1}
Bifurcation ratio (R_B): 2.83
Length ratio      (R_L): 1.73
Discharge ratio   (R_Q): 2.65

Hack-type scaling — Hack’s law relates downstream distance to upstream drainage (here discharge) as a power law. find_hack_parameters fits the coefficient k and exponent p.

hack = net.find_hack_parameters()
print('Hack coefficient k = %.4g' % hack['k'])
print('Hack exponent    p = %.3f' % hack['p'])
Hack coefficient k = 0.0009035
Hack exponent    p = 1.052

Wrap-up

That’s the three-step path through GRLP: a single long profile, a hand-built network, and a generated-and-analyzed random network. For larger network studies (including the McNab et al., 2025 experiments) and the full API, see grlp.readthedocs.io.