3. Meshes for Surface Processes¶
This notebook introduces the QuagMesh
object, which builds upon the QuagMesh
and introduces methods for finding the stream connectivity, catchment identification and handling local minima.
Here we demonstrate the stream flow components of the QuagMesh
Note: Again, the API for the structured mesh is identical
from quagmire.tools import meshtools
from quagmire import QuagMesh, QuagMesh
from quagmire import function as fn
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
minX, maxX = -5.0, 5.0
minY, maxY = -5.0, 5.0,
dx, dy = 0.02, 0.02
x, y, simplices = meshtools.elliptical_mesh(minX, maxX, minY, maxY, dx, dy, random_scale=1.0)
DM = meshtools.create_DMPlex_from_points(x, y, bmask=None)
mesh = QuagMesh(DM, downhill_neighbours=1)
meshn = QuagMesh(DM, downhill_neighbours=1) # A copy that we will use with a roughened topography
print ("Triangulation has {} points".format(mesh.npoints))
Underlying Mesh type: TriMesh
0 - Delaunay triangulation 0.10814945799999975s
0 - Calculate node weights and area 0.010434500000000568s
0 - Find boundaries 0.007506959000000535s
0 - cKDTree 0.007003291000000189s
0 - Construct neighbour cloud arrays 0.24624329099999898s, (0.1655212499999994s + 0.08068958299999984s)
0 - Construct rbf weights 0.023653665999999518s
Underlying Mesh type: TriMesh
0 - Delaunay triangulation 0.11084924999999934s
0 - Calculate node weights and area 0.010404082999999176s
0 - Find boundaries 0.007980375000000706s
0 - cKDTree 0.007130709000000124s
0 - Construct neighbour cloud arrays 0.24403991700000027s, (0.16525795799999976s + 0.07875279199999952s)
0 - Construct rbf weights 0.021775334000000868s
Triangulation has 62234 points
Height field and Rainfall¶
We generate the usual cylindrically symmetry domed surface and add multiple channels incised along the boundary. Here is it interesting to leave out the random noise to see how discretisation error influences the surface flow paths.
The QuagMesh
stores a rainfall pattern that is used to compute the stream power assuming everything goes into the surface runoff it also records a sediment distribution pattern (etc).
radius = np.sqrt((x**2 + y**2))
theta = np.arctan2(y,x) + 0.1
height = np.exp(-0.025*(x**2 + y**2)**2) + 0.25 * (0.2*radius)**4 * np.cos(5.0*theta)**2
height += 0.5 * (1.0-0.2*radius)
heightn = height + np.random.random(height.size) * 0.005 # random noise
with mesh.deform_topography():
mesh.topography.data = height
with meshn.deform_topography():
meshn.topography.data = heightn
0 - Build downhill matrices 1.0192250000000005s
0 - Build upstream areas 0.5904518340000013s
0 - Build downhill matrices 0.47453312500000067s
0 - Build upstream areas 0.5565477909999998s
rainfall = mesh.add_variable(name="Rainfall")
rainfall.data = (mesh.topography**2).evaluate(mesh)
mesh.cumulative_flow(rainfall.data)**2
array([5.03668335, 5.02298575, 5.01836541, ..., 2.66829351, 2.3281053 ,
3.99135784])
(mesh.upstream_integral_fn((mesh.topography**2))**2).evaluate(mesh)
array([2.33382378e-05, 1.72046623e-05, 9.10228538e-06, ...,
2.47019494e-06, 7.29065051e-06, 4.01484423e-06])
# rbf1 = mesh.build_rbf_smoother(1.0, 1)
# rbf01 = mesh.build_rbf_smoother(0.1, 1)
# rbf001 = mesh.build_rbf_smoother(0.01, 1)
# print(rbf1.smooth_fn(rainfall, iterations=1).evaluate(0.0,0.0))
# print(rbf01.smooth_fn(rainfall, iterations=1).evaluate(0.0,0.0))
# print(rbf001.smooth_fn(rainfall, iterations=1).evaluate(0.0,0.0))
# rbf001.smooth_fn(rainfall, iterations=1).evaluate(mesh)
rainfall.evaluate(mesh)
array([2.24425563, 2.24120185, 2.24017084, ..., 0.47308102, 0.42065372,
0.36434709])
(meshn.topography**2).evaluate(mesh) # slightly different
array([2.24583735, 2.25110091, 2.24041793, ..., 0.47503408, 0.42540432,
0.36483562])
Upstream area and stream power¶
Integrating information upstream is a key component of stream power laws that are often used in landscape evolution models. This is computed by multiple \(\mathbf{D} \cdot \mathbf{A}_{\mathrm{upstream}}\) evaluations to accumulate the area downstream node-by-node on the mesh.
A QuagMesh
object has a cumulative_flow method that computes this operation. There is also a quagmire function wrapper of this method that can be used as an operator to compute the area-weighted sum. This function is the numerical approximation of the upstream integral:
upstream_precipitation_integral_fn = mesh.upstream_integral_fn(rainfall_pattern)
rainfall_fn = (mesh.topography**2.0)
upstream_precipitation_integral_fn = mesh.upstream_integral_fn(rainfall_fn)
stream_power_fn = upstream_precipitation_integral_fn**2 * mesh.slope**1.0 * mesh.mask
stream_power_fn.evaluate(mesh)
array([1.01149038e-06, 1.42140223e-06, 7.31194445e-07, ...,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00])
Tools: stream power smoothing¶
It may be that some smoothing is helpful in stabilizing the effect of the stream power term in the topography evolution equation. The following examples may be helpful.
Note that we provide an operator called streamwise_smoothing_fn
which is conservative, a centre weighted smoothing kernel that only operates on nodes that are connected to each other in the stream network.
## We can apply some smoothing to this if necessary
rbf_smoother = mesh.build_rbf_smoother(0.1, 1)
rbf_smooth_str_power_fn = rbf_smoother.smooth_fn(stream_power_fn)
print(rbf_smooth_str_power_fn.evaluate(mesh))
str_smooth_str_power_fn = mesh.streamwise_smoothing_fn(stream_power_fn)
print(str_smooth_str_power_fn.evaluate(mesh))
[4.92762608e-06 5.05863511e-06 5.90557871e-06 ... 1.03472052e-05
1.40392478e-05 2.45570950e-05]
(array([2.4806877e-06, 2.3550435e-06, 1.4648087e-06, ..., 0.0000000e+00,
0.0000000e+00, 0.0000000e+00], dtype=float32), array([0, 0, 0, ..., 0, 0, 0], dtype=int32))
## We could also smooth the components that make up the stream power
rbf_smoothed_slope_fn = rbf_smoother.smooth_fn(mesh.slope)
rbf_smooth_str_power_fn2 = upstream_precipitation_integral_fn**2 * rbf_smoothed_slope_fn**1.0 * mesh.mask
print(rbf_smooth_str_power_fn2.evaluate(mesh))
str_smoothed_slope_fn = mesh.streamwise_smoothing_fn(mesh.slope)
str_smooth_str_power_fn2 = upstream_precipitation_integral_fn**2 * str_smoothed_slope_fn**1.0 * mesh.mask
print(str_smooth_str_power_fn2.evaluate(mesh))
[2.20452134e-06 1.63820484e-06 8.61642731e-07 ... 0.00000000e+00
0.00000000e+00 0.00000000e+00]
[1.18953900e-06 1.46293268e-06 7.58957690e-07 ... 0.00000000e+00
0.00000000e+00 0.00000000e+00]
colorby_1 = stream_power_fn.evaluate(mesh).astype(np.float32)
colorby_2 = rbf_smooth_str_power_fn.evaluate(mesh).astype(np.float32)
colorby_3 = str_smooth_str_power_fn.evaluate(mesh)[0].astype(np.float32)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-25-785bd48ed6f1> in <module>
1 colorby_1 = stream_power_fn.evaluate(mesh).astype(np.float32)
2 colorby_2 = rbf_smooth_str_power_fn.evaluate(mesh).astype(np.float32)
----> 3 colorby_3 = str_smooth_str_power_fn.evaluate(mesh).astype(np.float32)
AttributeError: 'tuple' object has no attribute 'astype'
str_smooth_str_power_fn.evaluate(mesh)[0]
array([2.4806877e-06, 2.3550435e-06, 1.4648087e-06, ..., 0.0000000e+00,
0.0000000e+00, 0.0000000e+00], dtype=float32)
import k3d
plot = k3d.plot(grid_visible=False, lighting=0.75)
indices = mesh.tri.simplices.astype(np.uint32)
points = np.column_stack([mesh.tri.points, height]).astype(np.float32)
plot += k3d.mesh(points+(00.0,0.0,0.0), indices, wireframe=False, flat_shading=True, side="double",
color_map = k3d.colormaps.matplotlib_color_maps.Blues,
attribute=colorby_1,
color_range = [0.0,1.0]
)
plot += k3d.mesh(points+(15.0,0.0,0.0), indices, wireframe=False, flat_shading=True, side="double",
color_map = k3d.colormaps.matplotlib_color_maps.Blues,
attribute=colorby_2,
color_range = [0.0,1.0]
)
plot += k3d.mesh(points+(30.0,0.0,0.0), indices, wireframe=False, flat_shading=True, side="double",
color_map = k3d.colormaps.matplotlib_color_maps.Blues,
attribute=colorby_3,
color_range = [0.0,1.0]
)
plot.display()
Outflow analysis¶
The topography we have defined has multiple outflow points, which, in the analytic case, should be equal. If they differ, this is a result of the discretisation.
When we introduce random noise we also (usually) introduce some internal low points in the mesh that capture some of the surface flow.
outflow_nodes = mesh.identify_outflow_points()
low_point_nodes = mesh.identify_low_points()
cumulative_rain = mesh.upstream_integral_fn(rainfall_fn).evaluate(mesh)
outflow_std_mesh = cumulative_rain[outflow_nodes]
print("{} outflow nodes:".format(len(outflow_nodes)))
print(outflow_nodes)
print("{} internal low point nodes:".format(len(low_point_nodes)))
print(outflow_nodes)
print(outflow_std_mesh)
outflow_standard_mesh = cumulative_rain[outflow_nodes]
# ---
cumulative_rain_n = meshn.upstream_integral_fn(rainfall_fn).evaluate(meshn)
outflow_nodes = meshn.identify_outflow_points()
low_point_nodes = meshn.identify_low_points()
outflow_rough_mesh = cumulative_rain_n[outflow_nodes]
cumulative_flow_rough_mesh = cumulative_rain_n[np.hstack((outflow_nodes, low_point_nodes))]
print("{} outflow nodes:".format(len(outflow_nodes)))
print(outflow_nodes)
print("{} internal low point nodes:".format(len(meshn.identify_low_points())))
print(low_point_nodes)
print(outflow_rough_mesh)
18 outflow nodes:
[61753 61803 61853 61902 61903 61952 61953 61954 61955 62002 62052 62102
62151 62152 62201 62202 62203 62204]
0 internal low point nodes:
[61753 61803 61853 61902 61903 61952 61953 61954 61955 62002 62052 62102
62151 62152 62201 62202 62203 62204]
[1.35633608e+00 2.91068294e+00 6.94590720e+00 3.80703311e+00
1.25669386e-01 1.76046638e+00 8.94107460e-04 5.28614675e-02
4.44502805e-02 1.79498622e+00 4.04115816e+00 6.07827447e+00
3.67500277e+00 1.27469995e-01 1.74984466e+00 1.25728405e-04
6.31638902e-02 1.78825614e-04]
19 outflow nodes:
[61753 61754 61803 61852 61902 61904 61952 61954 61955 61956 62002 62052
62053 62102 62151 62152 62201 62202 62204]
103 internal low point nodes:
[15115 15585 16959 17428 17551 17568 17578 17617 17750 18259 18328 18339
18341 18367 18478 18505 18559 18643 18759 18776 19358 19439 19487 19495
19608 19635 19692 19834 19859 19948 20029 20089 20200 20226 20229 20371
20444 20454 20483 20600 20831 21208 21432 21456 21471 21530 21707 21973
22186 22196 22240 22327 22433 22901 23078 23086 23144 23297 23510 23663
23664 23941 24009 24066 24176 24192 24520 24660 24675 24817 24848 25112
25412 25447 25451 25908 26029 26060 26088 26728 27034 27037 27360 27669
28030 28524 28672 28717 28899 29064 29066 29201 29527 29751 30416 30567
30587 31111 31667 32230 32394 39018 40997]
[7.57480425e-02 8.27023262e-04 4.80509858e-01 6.74360207e+00
1.42015934e+00 1.10397800e-01 8.56749326e-03 5.28614679e-02
9.19741051e-05 4.43583062e-02 2.38263792e-01 3.89014476e+00
9.78246603e-04 5.97105246e+00 3.78350619e+00 1.27618092e-01
1.66715499e-02 2.84870317e-02 6.21668983e-02]
print(outflow_standard_mesh.sum())
print(outflow_rough_mesh.sum())
print(cumulative_flow_rough_mesh.sum())
34.534505676164535
23.056012399787203
30.904065177114536
import matplotlib.pyplot as plt
%matplotlib inline
# plot bar graph of cumulative rain for each outflow point
fig = plt.figure(figsize=(12,6))
ax1 = fig.add_subplot(111, xlabel='outflow node', ylabel='cumulative rain')
ax1.bar(np.array(range(0,len(outflow_std_mesh))), width=0.4, height=outflow_std_mesh)
ax1.bar(np.array(range(0,len(outflow_rough_mesh)))+0.5, width=0.4, height=outflow_rough_mesh)
plt.show()

rainfall_n_fn = (mesh.topography**2.0)
upstream_precipitation_integral_n_fn = meshn.upstream_integral_fn(rainfall_n_fn)
stream_power_n_fn = upstream_precipitation_integral_n_fn**2 * meshn.slope**1.0 * meshn.mask
colorby_2 = stream_power_n_fn.evaluate(meshn)
import k3d
plot = k3d.plot(grid_visible=False, lighting=0.75)
indices = mesh.tri.simplices.astype(np.uint32)
points = np.column_stack([mesh.tri.points, height]).astype(np.float32)
plot += k3d.mesh(points+(00.0,0.0,0.0), indices, wireframe=False, flat_shading=True, side="double",
color_map = k3d.colormaps.matplotlib_color_maps.Blues,
attribute=colorby_1,
color_range = [0.0,1.0]
)
points = np.column_stack([meshn.tri.points, heightn]).astype(np.float32)
plot += k3d.mesh(points+(15.0,0.0,0.0), indices, wireframe=False, flat_shading=True, side="double",
color_map = k3d.colormaps.matplotlib_color_maps.Blues,
attribute=colorby_2,
color_range = [0.0,1.0]
)
plot.display()
Note that the mesh with random noise also has a number of internal low points that do not drain to the outside of the mesh. This is a non-physical aspect of the mesh that we would normally fix and is dealt with in a later notebook.
Downhill transport matrices are introduced in the next example, Ex4-Multiple-downhill-pathways and then we will discuss the infilling of local minima in Example 5.