Note

This page was generated from a jupyter notebook.

Unit tests and parameterization for AreaSlopeTransporter#

(Greg Tucker, University of Colorado Boulder, October 2022)

This notebook gives background on the default parameters for the AreaSlopeTransporter component, and also provides the calculations for a set of unit tests that are used in the code.

Parameters for the Einstein-Brown transport equation#

The AreaSlopeTransporter component calculates fluvial volumetric sediment flux, \(Q_s\), from a generic area-slope power law:

\[Q_s = k_s A^m S^n\]

This section works out the transport parameters \(k_s\), \(m\), and \(n\) for the Einstein-Brown law, based on Willgoose’s 1991 appendix but adding the extra step of integrating across a channel of width \(W = a Q^b\) to get total volumetric transport rate. These parameters are used as the default in the component.

Converting to area-slope form#

(This follows Willgoose et al. (1991a WRR) appendix):

Dimensionless transport rate:

\[\phi = 40 \tau_*^3\]

Definition of dimensionless xport rate:

\[\phi = \frac{M_s}{\rho_s F_1 \sqrt{g(s - 1) D^3}}\]

where \(M_s\) sediment discharge in mass/time/width, \(s\) is sed specific grav, \(D\) is grain diameter.

\(\tau_*\) is the usual:

\[\tau_* = \frac{\tau_b}{(\rho_s - \rho) g D}\]

The weirdo factor is:

\[F_1 = \sqrt{\frac{2}{3} + \frac{36\nu^2}{g D^3 (s-1)}} - \sqrt{\frac{36\nu^2}{gD^3(s-1)}}\]

From plenty of other derivations, we can cast \(\tau_b\) in terms of discharge and slope. Here’s one version using the Manning equation for a wide channel:

\[\tau_b = \rho g H S\]
\[Q = \frac{1}{n} W H^{5/3} S^{1/2}\]
\[H^{5/3} = \frac{Qn}{WS^{1/2}}\]
\[H = \frac{(Qn)^{3/5}}{W^{3/5}S^{3/10}}\]

Check the units here… we have L\(^{9/5 - 3/5 - 1/5}\) T\(^{-3/5+3/5}\)…ok.

Plug in:

\[\tau_b = \rho g \frac{(Qn)^{3/5}}{W^{3/5}S^{3/10}} S\]

Simplify

\[\tau_b = \rho g n^{3/5} \left(\frac{Q}{W}\right)^{3/5} S^{7/10}\]

So then,

\[\phi = \frac{40}{(\rho_s - \rho)^3 g^3 D^3} \left(\rho g n^{3/5} \left(\frac{Q}{W}\right)^{3/5} S^{7/10}\right)^3\]

which converts to:

\[\phi = \frac{40\rho^3 g^3 n^{9/5}}{(\rho_s - \rho)^3 g^3 D^3} \left( \left(\frac{Q}{W}\right)^{3/5} S^{7/10}\right)^3\]
\[\phi = \frac{40 n^{9/5}}{(s - 1)^3 D^3} \left(\frac{Q}{W}\right)^{1.8} S^{2.1}\]

Check units again: L\(^{-3/5 - 3 + 3.6 = 0}\), T\(^{9/5 - 9/5 = 0}\).

Now the annoying \(\phi\):

\[q_s = \frac{M_s}{\rho_s} = F_1\sqrt{g (s-1) D^3} \frac{40 n^{9/5}}{(s - 1)^3 D^3} \left(\frac{Q}{W}\right)^{1.8} S^{2.1}\]
\[q_s = \frac{M_s}{\rho_s} = F_1\sqrt{g} \frac{40 n^{9/5}}{(s - 1)^{5/2} D^{3/2}} \left(\frac{Q}{W}\right)^{1.8} S^{2.1}\]

Now it’s time to bring in the pesky channel-width issue:

\[W = a Q^b\]
\[q_s = \frac{M_s}{\rho_s} = F_1\sqrt{g} \frac{40 n^{9/5}}{(s - 1)^{5/2} D^{3/2}} \left(\frac{Q^{1-b}}{a}\right)^{1.8} S^{2.1}\]
\[q_s = \frac{M_s}{\rho_s} = F_1\sqrt{g} \frac{40 n^{9/5}}{(s - 1)^{5/2} D^{3/2}a^{9/5}} Q^{1.8(1-b)} S^{2.1}\]

Using \(b=1/2\), we have:

\[q_s = F_1\sqrt{g} \frac{40 n^{9/5}}{(s - 1)^{5/2} D^{3/2}a^{9/5}} Q^{0.9} S^{2.1}\]

Let’s now cast in terms of total flux, again with \(b=1/2\):

\[Q_s = a F_1\sqrt{g} \frac{40 n^{9/5}}{(s - 1)^{5/2} D^{3/2}a^{9/5}} Q^{1.4} S^{2.1}\]

That’s instantaneous. Let’s factor in an intermittency factor, \(I\), and combine pieces,

\[Q_s = k_{sq} I Q^{1.4} S^{2.1}\]

For \(F_1\), we need a grain diameter, densities, and kinematic viscosity. Using 0.05 m for grain diameter,

Next, we’ll convert this into an area-slope formulation. Let’s assume \(Q_{bf} = RA\) (not ideal, given common sub-linear \(A\) scaling, but let’s run with it). Then,

\[\boxed{Q_s = k_s A^{1.4} S^{2.1}}\]

where

\[k_s = k_{sq} I R^{0.9}\]

Estimating a value for \(k_s\)#

Ok, now for some numbers. Let’s use \(n=0.01\) in SI units. For bankfull width, if we use \(b = 1/2\) and \(a = 5\) s\(^{1/2}\)/m, and a bankfull runoff rate of 10 m/y, what would we have for width?

[ ]:
import numpy as np

a = 5.0
b = 0.5

area = np.array([1.0e6, 10.0e6, 100.0e6, 1000.0e6])
discharge = 1.0e-7 * area
width = a * discharge**b
print("At a bankfull discharge of:", discharge, "cms...")
print("...the corresponding bankfull width would be:", width)

Note that this is just a rough guess at \(a\). One should ideally interrogate hydraulic geometry, but here the purpose is simply to identify a value that is basically reasonable.

Here’s one example. Boulder Creek at Orodell has an annual peak (snow-melt) flow 500 cfs. How wide would it be at a = 1, 5, 10? Drainage area is 102 mi\(^2\), or 264 km\(^2\). The channel is roughly 20 m wide here (though possibly artifically narrowed by the road). If we assume the annual maximum flow is the bankfull event, how wide would the channel be if the coefficient were 5 and the exponent 1/2?

[ ]:
Qbf = 500 * (0.3048**3)  # bankfull flow, converted to cms
w = 5 * Qbf**0.5  # predicted width, m
am2 = 102 * (5280 * 0.3048) ** 2  # while we're at it, convert drainage area to m2
print("Peak discharge (cms):", Qbf)
print("Predicted width (m):", w)
print("Drainage area (km2):", am2 / 1e6)
print("Runoff rate (m/s):", Qbf / am2)
print("Runoff rate (m/y):", Qbf * 3600 * 365.25 * 24 / am2)

The above turns out to be quite close to the estimated width of 20 m. Although this is just one example, and not a proper fit by any means, for our purposes it suffices to demonstrate that an \(a\) value of 5 (in SI units) is not completely crazy.

For the sake of estimating a default value for \(k_s\), we’ll choose values for the kinematic viscosity of water (which is temperature-dependent; the value \(10^{-6}\) Pa\(\cdot\)s applies to 20 \(^\circ\)C), grain diameter (5 cm), and sediment immersed specific gravity (1.65, representing quartz). Clearly this means that the coefficient value depends in particular on grain diameter, and to a lesser extent on grain density.

Next, the Einstein-Brown \(F_1\) parameter:

[ ]:
nu = 1.0e-6  # kinematic viscosity of water, m2/s
D = 0.05  # grain diameter, m
sm1 = 1.65  # immersed specific gravity
g = 9.8

term = (36.0 * nu**2) / (g * D**3 * sm1)
F1 = ((2.0 / 3.0) + term) ** 0.5 - term**0.5
print("F1 =", F1)

This allows us to calculate \(k_{sq}\), here with \(n=0.01\) and \(a=5\),

[ ]:
n = 0.01  # Manning roughness coefficient in SI units
a = 5.0  # width-discharge factor, SI

ksq = a * F1 * g**0.5 * ((40.0 * n**4.5) / (sm1**2.5 * D**1.5 * a**4.5))
print("ksq", ksq)

Next, we’ll calculate the corresponding \(k_s\) by applying a bankfull runoff rate (2 m/y) and an intermittency factor (0.01):

[ ]:
R = 2.0  # bankfull runoff rate
intermittency = 0.01  # intermittency factor

ks = ksq * intermittency * R**0.9
print("ks", ks)

For landscape evolution, it makes sense to compute \(Q_s\) in m\(^3\)/y rather than m\(^3\)/s, so we will factor a unit conversion into the coefficient:

[ ]:
# Convert ks to time units of years:
ksy = ks * 365.25 * 24 * 3600.0
print("ksy", ksy)

If we round this off, we have our default value for \(k_s\): 0.0055, with length units of m and time units of years.

Now we’re ready to do a sanity check by examining the slope-area relationship. Let sediment flux be erosion rate times drainage area (ignoring fines and dissolved load), \(Q_s = EA\). Equilibrium means:

\[EA = k_s A^{1.4} S^{2.1}\]
\[S = \left(\frac{E}{k_s}\right)^{1/2.1} A^{-0.4/2.1}\]
\[S \approx \left(\frac{E}{k_s}\right)^{0.48} A^{-0.19}\]
[ ]:
print(1 / 2.1)
print(0.4 / 2.1)

What does this imply for gradients given \(E=10^{-4}\) m/y, and $A:nbsphinx-math:in `(10, 100, 1000) :math:`km^2$?

[ ]:
E = 1.0e-4
A = np.array([10.0e6, 100.0e6, 1000.0e6])

print("S=", (E / ksy) ** 0.48 * A ** (-0.19))

These seem like reasonable values for gradient.

Unit tests#

Default parameters#

Based on the derivation above, the default parameters will be \(k_s = 0.0055\), \(m=1.4\), \(n=2.1\).

Slope-area for one grid cell#

Consider a grid cell 1000 x 1000 m with an uplift rate of \(10^{-4}\) m/y. The sediment input rate is therefore 100 m\(^3\)/y. The resulting outflux should equal this:

\[Q_s = 0.0055 (10^6)^{1.4} S^{2.1} = 100 m^3/y\]
\[S = \left( \frac{(100)}{(0.0055)(10^6)^{1.4}}\right)^{1/2.1}\]
[ ]:
Seq = (100.0 / (0.0055 * 1.0e6**1.4)) ** (1.0 / 2.1)
elev_eq = Seq * 1000.0
print("Equilibrium slope:", Seq)
print("Equilibrium elevation:", elev_eq)

# test: elevation at the node should be 1,068 cm when rounded to the nearest cm.
print("Rounded elevation in cm:", int(round(elev_eq * 100)))

Transport capacity for a single cell#

Assume an elevation of 1.0 m, \(\Delta x = 100\) m. Drainage area therefore is 10,000 m\(^2\) and slope is 0.01. Therefore with default parameters, the volumetric sediment discharge should be:

[ ]:
Qs = 0.0055 * (100 * 100) ** 1.4 * (0.01) ** 2.1
print("Qs:", Qs)
print("Qs in liters per year (to nearest liter):", int(Qs * 1000))

Sediment rate of change#

Consider two core nodes flowing in one direction toward a single open boundary. Grid is raster with \(\Delta x = 100\) m. Slope is 0.01. Calculate \(dz/dt\) at each grid node.

[ ]:
A = 1.0e4 * np.array([3, 2, 1])  # drainage area (m2); flow is right to left
Qsout = np.zeros(len(A))
Qsin = np.zeros(len(A))
Qsout[1:] = 0.0055 * A[1:] ** 1.4 * 0.01**2.1  # sediment outflux from each node, m3/y
Qsin[:2] = Qsout[1:]  # sediment influx to each node, m3/y
dzdt = (Qsin - Qsout) / 1.0e4  # rate of elevation change, m/y
print(Qsin)
print(Qsout)
print(dzdt)

# test: answer should be array([ 0.   ,  0.365,  0.138])
print(np.round(Qsout, 3))

# test: answer should be array([ 0.365,  0.138,  0.   ])
print(np.round(Qsin, 3))

# test: answer should be array([ -2.26400000e-05,  -1.38200000e-05])
print(np.round(dzdt[1:], 8))

run_one_step#

Running one time step of 10,000 years with the above case should lower the nodes as follows:

[ ]:
elev = np.array([0.0, 1.0, 2.0])
elev[1:] += dzdt[1:] * 10000.0  # erode for one time step of 10,000 years
print(elev)

# test: should be array([0.    , 0.7736, 1.8618])
np.round(elev, 4)

These unit tests are embedded into the code as doctests.


Generated by nbsphinx from a Jupyter notebook.