Bed friction¶
GeoSWE models bed resistance with Manning friction, enabled by
Config(friction="manning"). The friction source is
with Manning roughness \(n\) and \(|\mathbf{u}|=\sqrt{u^2+v^2}\). It is stiff in thin films and is applied point-implicitly each step, after the hyperbolic update. Because the drag is collinear with the velocity, the implicit update reduces to a scalar factor \(\alpha\) on the predictor velocity, \(\mathbf{u}^{\,\text{new}} = \alpha\,\mathbf{u}^{*}\), and GeoSWE offers two closed forms:
with \(C_f = g\,n^2 h^{-4/3}\). Both are unconditionally stable and dissipative. The quadratic root is the exact solution of the implicit update and is the form used in every run reported in the paper: on the steady sheet-flow benchmark (benchmark/sheetflow_plane) it reproduces the exact film depth to within 0.8 % at 3 m and 0.3 % at 1 m.
Setting the roughness¶
Switch friction on in the configuration, then give the roughness as one number or as a map:
import numpy as np
from geoswe import Mesh2D, Config, Solver2D
nx, ny = 64, 48
mesh = Mesh2D(nx=nx, ny=ny, dx=10.0, dy=10.0)
cfg = Config(friction="manning", manning_n=0.03) # one value everywhere
solver = Solver2D(mesh, cfg, np.zeros((3, nx, ny)), np.zeros((nx, ny)))
n = np.where(np.arange(nx)[:, None] < nx // 2, 0.03, 0.10) + np.zeros((nx, ny))
solver.set_manning(n) # or a map of shape (nx, ny)
set_manning() pads the map for you. On the GPU in
single precision it stores a map with at most 256 distinct values (typical of
land-cover classes) as a one-byte class per cell plus a small table, which is
the form the single-kernel time step reads.
The two lower-level forms take arrays on the solver’s padded grid,
(nx + 2*ngh, ny + 2*ngh): Config.manning_field (a full field) and
set_manning_table() (a uint8 class array plus a
table). The table is bit-identical to a full field at 1 byte per cell instead
of 4, and is what the large runs use.
A roughness without friction="manning" has no effect, and the three entry points
say so differently: Config(manning_n=...) or Config(manning_field=...) warns at
construction, set_manning() switches friction on for you,
and set_manning_table() warns and leaves it off, so an
explicit Config stays explicit.
In Solver1D, friction supports only the linearized update without the velocity
cap: Config(friction="manning", friction_quadratic_alpha=False, friction_velocity_cap_ms=float("inf")).
Typical Manning values: ~0.025 (channels, smooth surfaces), ~0.03–0.05 (urban, grass), ~0.1+ (dense vegetation).
Stability safeguards¶
friction_velocity_cap_ms(default 15 m/s): where the predictor speed \(|\mathbf{u}^{*}|\) exceeds the cap, \(n\) is raised locally to \(\max(n, n_{\mathrm{cri}})\) with \(n_{\mathrm{cri}} = (\Delta t\, g\, h^{-4/3} |\mathbf{u}^{*}|)^{-1/2}\), which damps the excursion over several steps instead of clipping the velocity. It is a safeguard against sharp DEM steps, not a roughness model. How often it fires depends on the terrain: on a compressed runGEOSWE_VCAP_COUNT=1counts the activations and prints the total, which is how to tell whether it is doing real work on yours. Set tofloat("inf")to disable (fine for smooth float64 cases).The friction step never increases momentum and zeroes it in dry cells, so it composes safely with wetting/drying.