Sediment transport
This documents the user-facing interface: every parameter, its units, its default, and how to choose between the alternative methods. It assumes you know ANUGA, and it does not derive the physics.
Labels like [E-1] name a term in the physics; each links to the
equation that defines it in Sediment transport physics specification, the
specification the implementation was written from, which is published with
these docs. Section numbers such as “spec 2.6” point into the same page.
A sediment fraction is a tracer with settling parameters attached, so Passive Tracers covers the transport, boundary and conservation machinery that this page builds on.
Verification evidence for these terms is in
anuga/shallow_water/tests/test_sediment_*.py; runnable
examples are in examples/sediment/.
The shortest useful program
import anuga
domain = anuga.rectangular_cross_domain(40, 10, len1=100.0, len2=25.0)
domain.set_flow_algorithm('DE0')
domain.set_quantity('elevation', lambda x, y: -0.01 * x)
domain.set_quantity('stage', lambda x, y: -0.01 * x + 0.5) # 0.5 m deep
domain.set_quantity('friction', 0.03)
domain.set_boundary({'left': anuga.Dirichlet_boundary([0.8, 1.0, 0.0]),
'right': anuga.Dirichlet_boundary([-0.5, 0.0, 0.0]),
'top': anuga.Reflective_boundary(domain),
'bottom': anuga.Reflective_boundary(domain)})
domain.add_sediment_fraction('sand', diameter=2.0e-4) # <- the only new line
for t in domain.evolve(yieldstep=1.0, finaltime=30.0):
pass
print('peak concentration : %.3f' % domain.get_tracer('sand').max())
After 30 s this scours 5-22 cm from the bed and carries a peak concentration of about 0.145.
Note
The stage follows the bed (-0.01 * x + 0.5) and the boundaries drive
flow through the channel. A flat stage over a sloping bed is hydrostatic
– the water does not move, the bed shear stress never reaches the Shields
threshold, and nothing erodes. Sediment needs flow; if a model reports zero
concentration everywhere, check the hydrodynamics before the sediment
settings.
add_sediment_fraction is the entry point. One call gives you a transported
concentration, erosion, deposition, the settling velocity, the bed exchange,
and the limiters, with defaults chosen for a sand bed. It creates the sediment
operator and registers the fractional step for you, so there is nothing else to
wire up.
Everything below is about changing those defaults.
Print what you configured
print(domain.sediment_summary())
This is the single most useful call in the interface. It reports the active configuration – every law selected, every scalar in force, and each grain size’s derived settling velocity – as text. For the program above, every line but the first is a default:
sediment configuration
fractions : 1 -- sand (d=0.0002 m)
erosion : Shields / Smith-McLean, non-cohesive (sand, gravel) [E-1]
deposition : D = d* c v_s [D-1]
near-bed d* : constant, per fraction
shear closure : quadratic drag, tau_b = rho f_c |v|^2 [T-1]
friction closure : constant n, from the domain friction quantity
bedload : off
bed evolution : True (Phase 4, evolving)
porosity lambda : 0.3
c_max [L-2] : 0.3
c_pack [L-4] : 0.65
rho_w : 1000 kg/m3
erodible base [L-5]: none (unlimited depth)
angle of repose : off
erodible region : whole domain
([E-1] and the like are cross-references to the term in the physics;
see the Sediment physics appendix -- the description before each
label is the whole story.)
per fraction:
sand d=0.0002 m v_s=2.6695e-02 m/s R=1.65 tau_c*=0.04
Settling velocity in particular is derived, not set: if v_s is not what
you expected, the diameter or the fluid properties are not what you thought.
Print this at the top of every run.
Lines appear only when they apply – configure the Rouse near-bed profile, for
instance, and an a/h floor line joins the list. Everything below this point
is about changing one of these defaults, and the summary is how you check that
the change took.
The interface at a glance
Choices are made by naming the physics, never by setting a flag:
call |
chooses |
spec |
|---|---|---|
|
sediment transport on, and the domain-wide parameters |
2.2 |
|
a sediment fraction to carry, and its own properties |
2.2 |
|
the erosion law |
4.1.1 |
|
the deposition law and near-bed ratio |
4.4 |
|
how |
3.2 |
|
the friction factor feeding |
3.3 |
|
bedload transport, or off |
5 |
|
the scalar physical properties |
2.4, 6 |
|
the depth below which nothing erodes |
4.5 |
|
where erosion may act at all |
4.5 |
|
relaxation of over-steep bed slopes |
7 |
|
an external source |
2.6 |
|
inflow concentration, per boundary tag |
2.5 |
See also
- Sediment physics: choosing the laws
What each of those calls is choosing between – the erosion, deposition, shear and bedload laws, and how to tell which one your problem wants. The defaults are a working sand-bed configuration, so you can leave them alone until you need to say otherwise.
- Sediment transport physics specification
Where the bracketed labels are defined.
[E-1],[T-1]and the rest name a term in the physics; they appear in this page, in the source, and in the output ofsediment_summary(), and each links to its equation there.
Order does not matter, with one exception noted under add_sediment_fraction
below: call them before evolve(), in whatever order reads best.
Anything not in that table is internal. The domain carries roughly fifteen
sediment_* arrays (sediment_qbx, sediment_settling_velocity,
sediment_erosion_mode, …) that exist to be handed to the C kernel. Setting
them directly can leave the GPU mapping stale, and no validation runs. Use the
setters; they invalidate the device mapping for you.
Setting sediment up
Sediment transport is configured through two calls, and the split between them is the point: a parameter belongs to exactly one of the two, so there is never a question of which call wins.
domain.initialize_sediment_operator(porosity=0.28, rho_w=1000.0) # the run
domain.add_sediment_fraction('sand', diameter=2.0e-4) # a grain
domain.add_sediment_fraction('silt', diameter=2.0e-5, tau_c_star=0.11) # another
initialize_sediment_operator takes what describes the run;
add_sediment_fraction takes what describes one fraction. Neither accepts the
other’s parameters – passing diameter= to the first, or rho_w= to the
second, is a TypeError rather than a silently ignored argument.
The two may be called in either order, and initialize_sediment_operator is
optional: add_sediment_fraction creates the operator with default domain-wide
parameters if none exists, which is why the program at the top of this page is
a single line.
initialize_sediment_operator
domain.initialize_sediment_operator(
porosity=None, c_max=None, c_pack=None, bed_evolution=None, rho_w=None,
description=None, label=None, logging=False, verbose=False)
Switches sediment transport on and returns the operator. The physical
parameters are the same ones set_sediment_parameters
takes, and are documented there; passing them here is a convenience, and
set_sediment_parameters can still change them afterwards.
One operator per domain. Calling it again returns the same operator, applying any parameters given the second time. That is not just tidiness: the kernel makes a single pass over every registered fraction, so a second operator in the fractional-step list would apply the bed exchange twice per timestep.
op = domain.initialize_sediment_operator(porosity=0.28)
op is domain.initialize_sediment_operator(rho_w=1025.0) # True
The return value is the operator, which you need only for controlling operator order – see Controlling operator order below.
add_sediment_fraction
domain.add_sediment_fraction(
name, diameter,
rho_s=2650.0, tau_c_star=0.04, d_star=1.0, beta=None,
initial_concentration=0.0, reference_height=None, **settling_kwargs)
parameter |
units |
default |
meaning |
|---|---|---|---|
|
– |
required |
label; also the tracer name |
|
m |
required |
grain diameter \(d\); sets \(v_s\) via |
|
kg/m3 |
2650 |
sediment density (quartz); enters as \(R = \rho_s/\rho_w - 1\) |
|
– |
0.04 |
critical Shields stress \(\tau_c^{*}\), |
|
– |
1.0 |
near-bed ratio \(d^{*} = c_b/c\); 1.0 is well-mixed |
|
– |
0.0 |
volumetric \(c\), uniform |
|
– |
domain’s |
edge reconstruction limiter, shared by all tracers |
|
m |
|
Rouse reference height \(a\) in |
Multiple fractions are independent: each has its own concentration, settling velocity and critical stress, and each exchanges with the same bed. Call it once per fraction.
There is no rho_w here. Water density is a property of the fluid, and there
is one fluid, so it lives on initialize_sediment_operator and
set_sediment_parameters. Changing it afterwards recomputes \(R\) and
\(v_s\) for every fraction already registered.
What belongs to a fraction, and what to the run
Only the grain’s own properties are per fraction; every physics choice, and the bed itself, is shared.
Per fraction |
Set by |
|---|---|
|
|
settling velocity \(v_s\) |
computed from |
|
|
|
|
submerged specific gravity \(R\) |
computed from |
Shared by every fraction |
Set by |
|---|---|
|
|
erosion law, |
|
deposition law, |
|
shear closure; friction mode and its parameters |
|
bedload mode, |
|
|
one |
the bed |
one |
Because the shared settings live on the domain, not on the operator, order
does not matter: set_bed_material('cohesive') called after both fractions
are registered applies to both.
Fractions occupy tracer slots in call order, so fraction s is tracer
s. The one ordering rule: do not interleave add_tracer and
add_sediment_fraction on the same domain if you rely on that correspondence.
Controlling operator order
Fractional-step operators run in the order they are created. If another operator must run before the sediment one – an external source that the bed exchange then consumes, say – create the sediment operator at the point you want it in the sequence, and add the fractions afterwards:
My_source_operator(domain, ...) # runs first
domain.initialize_sediment_operator() # then this
for nm, d50 in grain_sizes:
domain.add_sediment_fraction(nm, diameter=d50)
add_sediment_fraction will not displace an operator that already exists, so the
order established here survives however many fractions follow. Ordinary
models do not need this: calling add_sediment_fraction straight away, as
everywhere else on this page, puts the operator in a sensible place by itself.
Choosing a name
name is a label you choose, not a value from a list. Nothing is inferred
from it: diameter and the settling parameters do all the work, so a grain
size called 'boulder' with diameter=2e-5 is silt, and behaves like
silt.
domain.add_sediment_fraction('fine_sand', diameter=1.5e-4)
domain.add_sediment_fraction('mud', diameter=2.0e-5)
domain.get_sediment_names() # ['fine_sand', 'mud']
domain.get_tracer('fine_sand') # its concentration, per cell
It is also the tracer name, which is what constrains it. It must be a non-empty string, unique on the domain, and it may not be
the name of a quantity –
stage,elevation,friction,xmomentum,ymomentum,height,x,y,xvelocity,yvelocity. Both a quantity and a tracer are written to the sww as<name>_c, so a fraction calledstagewould overwrite the stage in the output.anything beginning
max_, which is reserved for the running maximaCollect_max_quantities_operatorwrites.
Both are refused rather than allowed to corrupt the output. The name also
becomes the sww variable <name>_c, so pick something you will recognise
when you open the file six months later.
Warning
Do not confuse this with set_sediment_friction(bed='sand'). That
'sand' is one of a fixed set – 'sand', 'gravel',
'boulder' – selecting a roughness closure, and has nothing to do with
what you called your fraction. name is the only sediment argument
that is free text; every other choice below names a physics option from a
fixed vocabulary.
If you leave a fraction out
name and diameter describe one fraction, so they travel together:
both are required positional parameters, and omitting either is a
TypeError at the call rather than a partial registration.
domain.add_sediment_fraction('sand')
# TypeError: add_sediment_fraction() missing 1 required positional argument: 'diameter'
Setting sediment up with no fraction at all is legal, and is what
initialize_sediment_operator on its own does – see Controlling operator order.
It is a configured run waiting for its sediment, not an error. But it is only
useful as a step on the way to add_sediment_fraction: an operator with no grain
sizes transports nothing, and evolving that way warns rather than completing
silently with the bed untouched.
Note
This includes bedload. Moving the bed without carrying anything in suspension is a reasonable thing to want, and it does not remove the need for a fraction: the diameter and \(R\) that set the Shields stress – and so the transport vector \(\mathbf{q}_b\) – live on a grain size. With none registered, the bedload kernel returns immediately and the bed does not move.
For bed evolution with no suspended sediment, register the fraction and choose the total-load formula, which turns the suspended exchange off:
domain.add_sediment_fraction('sand', diameter=2.0e-4)
domain.set_bedload('engelund_hansen') # [K-5], total load
On the channel at the top of this page that scours about 14 cm of bed while the concentration stays at zero throughout.
Initial and boundary concentrations
domain.set_tracer('sand', 0.001) # uniform, or an array of centroids
domain.set_tracer_boundary('sand', 'inflow', 0.02) # entering across 'inflow'
Both are volumetric concentration c (dimensionless), not h*c. The
conserved quantity is m = h*c; the interface works in c throughout.
Scalar parameters
domain.set_sediment_parameters(porosity=0.30, c_max=0.30, c_pack=0.65,
bed_evolution=True, rho_w=1000.0)
All optional; only what you pass is changed. All are validated.
parameter |
units |
default |
meaning |
|---|---|---|---|
|
– |
0.30 |
bed porosity |
|
– |
0.30 |
|
|
– |
0.65 |
|
|
– |
|
whether the bed moves |
|
kg/m3 |
1000 |
fluid density used to form dimensional |
bed_evolution is the coupling stage
False gives a fixed bed: sediment is entrained and deposited, and
concentration evolves, but elevation never changes. Choose it when
comparing against analytic solutions, which assume constant depth;
comparing against RDycore v1.0, which is configured this way;
isolating a transport question from a morphology question.
True (the default) evolves the bed through [G-4] and [G-5]. Choose it
for any real morphological problem. Both bed terms are applied in a single
fractional step.
The non-erodible base
domain.set_erodible_base(depth=0.5) # 0.5 m of erodible material
domain.set_erodible_base(elevation=z_rock) # or an absolute surface, (n,)
domain.set_erodible_base() # remove it again
By default the bed is bottomless: erosion lowers it for as long as the flow
has the strength to. That is right for a deep alluvial channel and wrong
wherever the erodible layer is finite – a reach floored by an outcrop, a lined
culvert, a dam apron, a soil layer of known depth over rock. [L-5] gives it a
floor.
The base is a per-centroid field, because bedrock is a surface. depth=
measures down from the elevation set so far and is recorded as an elevation at
the moment of the call, so later changes to the elevation quantity do not drag
it around. elevation= gives the surface directly, in the domain’s datum.
Scalars broadcast; arrays must be (n,). Give exactly one.
A base above the bed is rejected rather than silently accepted – it would mean negative erodible thickness, which is a mistake, not a configuration.
domain.erodible_thickness() # (n,) metres remaining; 0 means bedrock
sediment_summary() reports the range and how many cells have reached bedrock.
What it guarantees
The limit is applied to the source, not by clamping elevation. Erosion is
scaled back to what the remaining thickness can supply, so the sediment that is
not eroded never enters the water column and the budget still closes to machine
precision. Clamping z afterwards would leave suspended sediment that came
from nowhere.
Where several classes compete for the last of the material they are scaled by one shared proportional factor, not served in registration order: the bed carries no per-class stratigraphy, so no class has a better claim, and the answer must not depend on the order you registered the fractions. Deposition is never scaled – it is what replenishes the bed.
The two transport routes give different strengths of guarantee, and it is worth knowing which you are relying on:
route |
floor is |
why |
|---|---|---|
suspended exchange |
exact |
the limit is on the exchange term itself |
bedload |
within one step’s flux |
bedload is a divergence; see below |
Bedload only redistributes, and stays exactly conservative with a base present, because the limit is applied to the transport vector and to whole edges – both of which the two cells sharing an edge evaluate identically. The price is that the floor is not exact: closing an edge for a cell that cannot pay also cancels its neighbour’s inflow, so the deficit migrates. Measured overshoot is 5.1e-6 m on a 1.0e-2 m layer. If you need bedload’s floor exact, that is a known limitation with a known fix (iterating the exhaustion flag to a fixed point), not a mystery.
Restricting erosion to a region
The base says how deep erosion may go; a region says where it may happen at all.
domain.set_erodible_region(polygon=breach) # ONLY here erodes
domain.set_erodible_region(polygon=apron, erodible=False) # everywhere BUT here
domain.set_erodible_region(center=[x, y], radius=25.0) # a circle
domain.set_erodible_region(indices=ids) # triangles directly
domain.set_erodible_region(my_region) # a Region object
domain.set_erodible_region() # remove it
The keyword arguments are the ones the region-based operators
(Erosion_operator and friends) already take, resolved by the same Region
class, so a polygon that selects a set of cells there selects the same set
here. A region that selects no cells is rejected rather than silently doing
nothing – that is almost always a polygon in the wrong coordinates.
Passing a Region is the general form. Region understands more than the
keywords above – line=, poly=, expand_polygon= – so build one and hand
it over when you need those:
from anuga.abstract_2d_finite_volumes.region import Region
domain.set_erodible_region(Region(domain, line=thalweg))
domain.set_erodible_region(Region(domain, polygon=reach, expand_polygon=True))
Both do reach through: a line selects the cells it crosses (83 of 960 on a test
mesh), and expand_polygon genuinely changes the selection (504 cells against
480), because it intersects on vertices rather than centroids.
It must be a Region built on this domain – one built elsewhere carries
triangle indices that mean nothing here, and is refused rather than silently
mis-selecting. A bare list of points passed positionally is refused too, with a
message pointing at polygon=: taking it as a region would select every cell
and look like it worked.
Locked means unscourable, not inert. A locked cell is held at the
elevation it has when you call this, by giving it zero erodible thickness –
the restriction is [L-5] with the layer set to nothing, not a separate
mechanism. Sediment may still settle onto it, which is what a concrete apron
or a rock bar does in the field, and that new material is erodible again
because it now sits above the base. Under genuinely erosive flow such a cell
sits at exactly net zero: the limiter scales erosion back until it just
cancels deposition, so nothing piles up on a scoured apron.
The two compose, in either order, and neither discards the other:
domain.set_erodible_base(depth=0.4) # 0.4 m of erodible material
domain.set_erodible_region(polygon=reach) # but only inside this reach
Where they disagree the stricter wins. sediment_summary() reports both, and
the thickness range it prints covers only the erodible cells – locked ones
carry zero thickness and would otherwise drag the minimum to zero whatever the
layer is.
Cost
None when unset. With no base configured the kernels take the path they took
before the feature existed, and produce bitwise identical results – which is
asserted, not assumed, in test_sediment_erodible_base.py check E1.
Angle-of-repose relaxation
domain.set_angle_of_repose(35.0) # degrees; FG21 use 35
domain.set_angle_of_repose(None) # off again (the default)
Where the centroid-to-centroid bed slope exceeds the critical angle, bed material is moved downslope until it does not. Without it, scour will cut a vertical face that in the field would collapse.
It is off by default, and that is a considered default. FG21, whose formulation this is, are explicit that it is a numerical heuristic, not physics – real slope failures are advective. It exists to stop the rest of the model breaking on over-steep slopes, and it has a side effect worth knowing before you switch it on: it limits the steepness of canyon walls and knickpoints, and so suppresses knickpoint retreat that may be real.
Mass is conserved. Material removed from an over-steep cell is deposited on
its neighbours, never discarded (measured drift 2.3e-13 m3 on 4.0e2 m3 of bed).
This is the sharpest difference from ANUGA’s sanddune_erosion_operator, which
lowers an over-steep cell and lets the material vanish.
It respects [L-5]: a cell cannot slump away material it is not allowed to
lose, so a locked cell or one already at its base stays put and its neighbours
relax around it.
The sweep count, which will surprise you
This is an explicit diffusion solve, and convergence from a badly over-steep bed is slow. An over-steep cone (36.8 degrees) needed 793 sweeps to reach a 30 degree limit from cold.
That is not what the per-timestep cap is sized for. In a running model the bed
is already near-relaxed and each step needs a handful of sweeps. The cap
(max_sweeps, default 50) exists for the pathological case, and hitting it
is not fatal – progress carries over, so the bed keeps relaxing on
subsequent steps. It is reported so you know relaxation is lagging rather than
finished:
Sediment_transport_operator: angle-of-repose relaxation hit its 50-sweep cap at
t = 0.3 s; the bed may still exceed 30.0 degrees.
If you start from a bed steeper than the critical angle, expect that on the
first few steps. Either let it settle, or raise max_sweeps for that run.
operator.repose_sweeps and operator.repose_cap_hits are available if you
want to watch it, and timestepping_statistics() prints the sweep count.
relax defaults to 1.0. That is the fastest stable setting, not an
aggressive one – the kernel already divides by the edge count for stability,
and 1.0 converged the cone in 793 sweeps against 2400+ at 0.3. Lower it only if
you see something pathological.
In parallel
The kernel sweeps internally so it stays on the device, and elevation is exchanged once per timestep afterwards. Spec 7 asks for an exchange per sweep. The consequence is that relaxation crosses a subdomain boundary one sweep per timestep rather than one per sweep, so a slump spanning a boundary relaxes more slowly there than it would in serial. Serial and single-subdomain results are unaffected. See the specification, section 7.1.
External sources
domain.set_tracer_source('sand', values) # array over centroids, or a scalar
Adds a source to the tracer equation directly (spec 2.6), in units of m
(that is, h*c) per second. This is what the manufactured-solution tests use
to impose an analytic forcing, and it is the hook for anything the bed
exchange does not describe – a lateral inflow, a point discharge, a
prescribed release.
Running on the GPU
domain.set_compute_mode('unified')
'legacy' is the CPU/OpenMP path; 'unified' is the shared path that can
run on the device. Both paths share the same kernel file, so the physics is
the same code – core_kernels.c – and test_sediment_gpu.py holds
them to agreement.
Nothing about the sediment configuration changes between modes: set it up the same way and switch the mode.
'unified' selects the unified code path; whether that path actually
offloads to a device is a property of the build, not of this call. A build
without offload compiles the same kernels under CPU_ONLY_MODE and runs them
on the host. set_compute_mode('unified') therefore does not fail on a
machine with no GPU, but it also does not report one:
domain.get_compute_mode() reads 'unified' either way. To find out what
you are actually running on, ask the build:
import anuga
anuga.gpu_offload_enabled() # True if this build offloads
To confirm kernels are reaching the device on a run, set
NVCOMPILER_ACC_NOTIFY=1 in the environment. Polling nvidia-smi is
unreliable for this – the sampling interval misses short kernel bursts.
Call set_compute_mode after the sediment setup. Each setter
invalidates the device mapping, so configuring sediment after selecting
'unified' simply forces the mapping to be rebuilt.
Coming from the erosion operators
ANUGA has carried a family of erosion operators for a long time –
Polygonal_erosion_operator, Circular_erosion_operator,
Bed_shear_erosion_operator, Flat_slice_erosion_operator,
Flat_fill_slice_erosion_operator and Sanddune_erosion_operator. They
still work and nothing is scheduled for removal, but they are the expensive
way to evolve a bed: each runs in Python on the host every timestep, and none
is GPU-safe, so under compute mode 'unified' each one forces a
GPU-to-host sync on every RK step.
Measured on 115,200 triangles, the overhead an erosion operator adds above a plain run is 12.9x that of the entire sediment transport module on the GPU, and 3.3x on the CPU – while modelling less and conserving nothing.
Structural equivalents
erosion operator argument |
sediment equivalent |
|---|---|
|
|
|
|
Sanddune’s repose relaxation |
|
eroded material simply disappears |
|
Bed_shear_erosion_operator
This one maps almost exactly. It forms 1000 * 9.81 * d * EN_slope – that
is \(\rho g h S\) with \(S\) the energy slope – and then erodes
de = tau_b / shear_factor * dt. That is the cohesive Hanson & Simon law
[E-3], \(E = K_e(\tau_b - \tau_c)\), with no threshold and
\(K_e = 1/\texttt{shear\_factor}\):
domain.add_sediment_fraction('sand', diameter=2.0e-4)
domain.set_shear_closure('energy_slope') # tau_b = rho g h S [T-7e]
domain.set_bed_material('cohesive', tau_crit=1e-9,
K_e=0.5 / shear_factor) # E = K_e tau_b
domain.set_deposition(law='threshold', tau_d=0.0) # no redeposition
domain.set_erodible_base(elevation=base)
domain.set_erodible_region(polygon=polygon)
Use 'energy_slope' rather than 'depth_slope': the old operator used the
free-surface slope, and choosing the bed slope instead is what costs the
agreement. For both, \(S\) is the least-squares gradient of the centroid
values over the cell and its neighbours (one-sided at boundaries), not the
edge values the limiter rebuilds each step, so a wall cell sees the same
slope as its neighbours. 'depth_slope' on an evolving bed feeds back on
itself – erosion roughens the bed and steeper local slopes erode faster –
which is the closure’s own property; anugaSed contains it with the global
clamp described in the specification. ANUGA offers two stated bounds
instead: set_shear_closure('depth_slope', max_slope=0.05) caps the slope
per cell, and set_shear_closure('depth_slope', freeze_slope=True) takes
the slope from the bed at setup and keeps it, so the closure sees the reach
slope it was written for while the bed evolves under it
(bed_slope_magnitude() returns that slope). Prefer the
other two closures when the bed moves and neither bound is wanted. On a sloping channel over 30 s, correlation of the bed-change field
against the original operator:
closure |
correlation with the old operator |
|---|---|
|
0.815 |
|
0.964 |
|
0.988 |
K_e needs calibrating: the two gradient reconstructions differ, so
1/shear_factor is the right form but not the right constant. Halving it
matched the case above. Calibrate against a run you trust.
The others
operator |
notes |
|---|---|
|
Erode at |
|
Not erosion: they set elevation to a target value. Use
|
|
Erosion plus repose. The repose half is
|
Warning
Nothing prevents enabling an erosion operator and sediment transport on
the same domain. Both write elevation and the bed changes simply add.
One conserves mass and the other does not, so the sum is unlikely to mean
anything – pick one.
Choosing a configuration
If you do not know where to start:
Sand bed, flood or dam break, morphology wanted. Defaults, plus one sediment fraction:
domain.add_sediment_fraction('sand', diameter=2e-4). Addset_bedload('wong_parker_eq24')if the grains are coarse enough to move along the bed.Fine cohesive sediment, muddy estuary.
set_bed_material('cohesive')with atau_crityou trust,d*left at 1.0.Deep, slow, stratified flow.
set_deposition(near_bed='rouse'), and give each class areference_height.Shallow flow over gravel.
set_sediment_friction('wilson', bed='gravel', grain_size=...).Reproducing anugaSed.
set_bed_material('cohesive')andset_shear_closure('depth_slope'); seeexamples/sediment/.Comparing against an analytic solution.
set_sediment_parameters(bed_evolution=False)and leaved*at 1.0.A finite erodible layer over rock.
set_erodible_base(depth=...), and checkerodible_thickness()afterwards to see where it bit.Scour confined to one structure or reach.
set_erodible_region(polygon=...), orerodible=Falseto lock an apron while the rest of the domain erodes.A dune or a steep bank that should collapse rather than stand vertical.
set_angle_of_repose(35.0), and read section 11.1 first.
Then print sediment_summary() and check it says what you meant.
What is not implemented
Vegetation drag (spec 8) is Phase 5 and absent. Neither validation rung of
spec 10 – Rio Puerco, the crater breach – has been attempted; the evidence
in anuga/shallow_water/tests/test_sediment_*.py is verification (the
equations are solved correctly), which is a different claim from validation
(they are the right equations for the field case). One step towards the
latter is in the validation suite, which runs nightly with the other
analytical cases: validation_tests/analytical_exact/sediment_settling
compares the deposition term, the sediment mass balance and the bed update
against the reference solution for sediment settling out of still water, and
validation_tests/analytical_exact/sediment_erosion compares the
Smith–McLean entrainment law, its threshold, the depth-slope closure (slope
frozen) and the bed lowering against the per-cell reference for a bed that
erodes under still water. validation_tests/analytical_exact/sediment_settling_basin
compares the steady concentration profile and bed-rise rate of a settling
basin with flow against the closed-form exponential, and closes the
sediment budget between boundaries, water column and bed. validation_tests/analytical_exact/sediment_equilibrium_flow
compares the suspended load that clear water picks up in normal flow on a
Manning slope, under the quadratic-drag closure, against the closed-form
approach to the equilibrium concentration.
validation_tests/analytical_exact/sediment_bed_hump runs the classic
Exner test, a bed hump migrating under Grass bedload, against its
characteristic solution. The notebook Sediment Transport Notebook Example walks
through the settling case.