Splitting the Flow
Baseflow Separation
The stream you see is two flows in disguise: quick storm runoff and slow, steady groundwater seeping in. Drag a slider, watch the code rewrite itself, and see the hydrograph split before your eyes.
Your path through this module
1 · Two flows, one stream
When it rains, two very different processes contribute to the stream at the same time:
⚡
Quickflow (stormflow)Overland flow and shallow subsurface flow that responds rapidly to rain. Rises fast, peaks in hours to days, then recedes.
🌱
BaseflowGroundwater seeping slowly from the saturated zone into the stream. Sustains the stream between storms. Changes on a seasonal timescale.
The Baseflow Index (BFI) is the fraction of total streamflow that is baseflow:
A BFI near 1 means the stream is groundwater-dominated (spring-fed rivers in chalk catchments). A BFI near 0 means flashy, ephemeral flow with almost no groundwater contribution. Most temperate rivers have a BFI between 0.4 and 0.8.
2 · The Lyne–Hollick digital filter
Lyne & Hollick (1979) proposed the simplest viable digital filter for automated baseflow separation. For each time step t:
f(t) = α · f(t−1) + (1+α)/2 · [Q(t) − Q(t−1)]
b(t) = Q(t) − max(0, f(t))
f(t) = high-frequency (quickflow) component ·
b(t) = baseflow ·
α = filter parameter (0 < α < 1)
The filter runs 3 passes: forward → backward → forward. This removes phase lag and makes the separation symmetric in time. The parameter α controls the smoothing:
More smoothing → baseflow tracks the slow trend only. BFI tends lower.
Less smoothing → baseflow follows Q more closely. BFI tends higher.
Nathan & McMahon (1990) recommended value for daily streamflow data.
3 · Drag α — watch the code rewrite itself
Move the slider below. As you drag, the value of alpha in the Python
cell updates in real time — this is live code editing via
aihydro.rewriteCell. Click Run to execute with the
current α and see the separated hydrograph.
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
alpha = 0.925 # ← drag the slider to change this
# ── Synthetic 365-day hydrograph ─────────────────────────
rng = np.random.default_rng(42)
days = np.arange(365)
b_true = 2.0 + 1.2 * np.sin(2 * np.pi * (days - 80) / 365)
storms = np.zeros(365)
idx = rng.choice(365, 15, replace=False)
storms[idx] = rng.exponential(4.0, 15)
uh = np.exp(-np.arange(20) / 4.0); uh /= uh.sum()
qf = np.convolve(storms, uh, mode='full')[:365]
Q = np.clip(b_true + qf + rng.normal(0, 0.1, 365), 0.05, None)
# ── Lyne–Hollick filter (3-pass) ─────────────────────────
def lyne_hollick(Q, alpha, passes=3):
b = Q.copy()
for pi in range(passes):
f = np.zeros_like(Q)
fwd = (pi % 2 == 0)
if fwd:
for i in range(1, len(Q)):
f[i] = alpha * f[i-1] + (1+alpha)/2 * (Q[i] - Q[i-1])
else:
for i in range(len(Q)-2, -1, -1):
f[i] = alpha * f[i+1] + (1+alpha)/2 * (Q[i] - Q[i+1])
b = np.minimum(b, Q - np.clip(f, 0, None))
b = np.clip(b, 0, None)
return b
base = lyne_hollick(Q, alpha)
BFI = base.sum() / Q.sum()
# ── Plot ─────────────────────────────────────────────────
fig, ax = plt.subplots(figsize=(9, 4.2))
ax.fill_between(days, 0, base, color='#34d399', alpha=0.55, label='Baseflow')
ax.fill_between(days, base, Q, color='#00A3FF', alpha=0.28, label='Quickflow')
ax.plot(days, Q, color='#00DDFF', lw=1.6, label='Total Q')
ax.plot(days, base, color='#34d399', lw=1.1, ls='--')
ax.set(xlabel='Day of year', ylabel='Q (m³/s)',
title=f'Lyne–Hollick separation · α = {alpha:.3f} · BFI = {BFI:.3f}')
ax.set_facecolor('#0f0f1e'); fig.patch.set_facecolor('#0a0a15')
ax.tick_params(colors='#7dd3fc'); ax.yaxis.label.set_color('#7dd3fc')
ax.xaxis.label.set_color('#7dd3fc'); ax.title.set_color('white')
for sp in ax.spines.values(): sp.set_color('#334466')
ax.legend(framealpha=0.4, fontsize=9, labelcolor='white')
plt.tight_layout()
print(f"α = {alpha:.3f}")
print(f"BFI = {BFI:.4f} ({BFI*100:.1f}% of flow is baseflow)")
print(f"Mean Q = {Q.mean():.2f} m³/s | Mean base = {base.mean():.2f} m³/s")
4 · Before and after — drag to compare
The wipe control below shows the same synthetic hydrograph: left = raw total discharge only; right = baseflow separated (green = baseflow; blue = quickflow; teal line = total Q). Drag the divider left and right to compare.
Synthetic data (α = 0.925, BFI ≈ 0.55). The real split depends on the actual gauge record — Module 5 fetches live USGS data you can run through this filter.
5 · Checkpoint
Three questions — pass all three to mark this module complete.
1 · The Baseflow Index (BFI) is defined as…
2 · In the Lyne–Hollick filter, increasing α closer to 1 will…
3 · Which type of catchment would you expect to have a high BFI (close to 1)?
Sources & Further Reading
- Lyne, V. & Hollick, M. (1979). Stochastic time-variable rainfall-runoff modelling. Inst. Eng. Australia Nat. Conf. Publ. 79/10, 89–93. — Original filter formulation.
- Nathan, R. J. & McMahon, T. A. (1990). Evaluation of automated techniques for baseflow and recession analyses. Water Resources Research, 26(7), 1465–1473. — Recommends α = 0.925 for daily data; systematic evaluation of the Lyne–Hollick filter.
- Dingman, S. L. (2015). Physical Hydrology, 3rd ed. Waveland Press. — Baseflow and groundwater–stream exchange, Chapter 6.
- Author
- Mohammad Galib · Purdue University
- License
- CC-BY-4.0 — free to reuse with attribution
- Module ID
baseflow-separation· v0.1.1- Course
- Introduction to Hydrology ·
intro-hydro-2026 - Data
- Synthetic hydrograph (NumPy random seed 42); no real gauge data
- Dependencies
- numpy, matplotlib (kernel cell); compare wipe is pure DOM