2. Meshes with Topography¶
This notebook introduces the QuagMesh
object, which builds the following data structures:
hill slope
downhill propagation matrices
upstream area
in addition to the data structures inherited from QuagMesh
. These form the necessary structures to propagate information from higher to lower elevations. Derivatives are computed on the mesh to calculate the height field, smoothing operators are available to reduce short wavelength features and artefacts.
In this notebook we setup a height field and calculate its derivatives on an unstructued mesh. We smooth the derivatives using the radial-basis function (RBF) smoothing kernel.
Note: 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
# from scipy.ndimage import imread
# from quagmire import tools as meshtools
# from quagmire import QuagMesh
%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)
DM = meshtools.create_DMPlex_from_points(x, y, bmask=None)
mesh = QuagMesh(DM, downhill_neighbours=1)
print ("Triangulation has {} points".format(mesh.npoints))
Underlying Mesh type: TriMesh
0 - Delaunay triangulation 0.11218824999999555s
0 - Calculate node weights and area 0.020123624999996537s
0 - Find boundaries 0.007983166000002484s
0 - cKDTree 0.006998583000004999s
0 - Construct neighbour cloud arrays 0.2953688329999977s, (0.19133074999999877s + 0.10399454199999525s)
0 - Construct rbf weights 0.028541665999995303s
Triangulation has 62234 points
Height field¶
We generate a cylindrically symmetry domed surface and add multiple channels incised along the boundary. The height and slope fields reside as attributes on the QuagMesh
instance.
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 ## Less so
height += 0.5 * (1.0-0.2*radius)
height += np.random.random(height.size) * 0.01 # random noise
# This fails because the topography variable is locked
mesh.topography.data = height
# This unlocks the variable and rebuilds the necessary downhill data structures
with mesh.deform_topography():
print("Update topography data array (automatically rebuilds matrices)")
mesh.topography.data = height
print("Update topography data array (automatically rebuilds matrices ONCE ONLY)")
mesh.topography.data = height + 0.01
quagmire.MeshVariable: h(x,y) - is locked
Update topography data array (automatically rebuilds matrices)
Update topography data array (automatically rebuilds matrices ONCE ONLY)
0 - Build downhill matrices 1.1500198329999947s
0 - Build upstream areas 0.5090339589999999s
mesh.topography.data
array([1.50868874, 1.51634837, 1.50855902, ..., 0.70499342, 0.66836758,
0.61934834])
s = mesh.slope
s
Derivatives and slopes¶
The slope of the topography is defined through a built in lazy-evaluate function mesh.slope
(which was described in the Functions notebook). Other gradients are available through the usual quagmire mathematics functions.
If you want more control of the underlying operations, derivatives can also be evaluated on the mesh using the inbuilt routine in the stripy
object. It employs automatically selected tension factors to preserve shape properties of the data and avoid overshoot and undershoot associated with steep gradients. Note: In parallel it is wise to check if this tensioning introduces artefacts near the boundaries.
dfdx, dfdy = mesh.derivative_grad(f, nit=10, tol=1e-8):
where nit
and tol
control the convergence criteria.
Smoothing¶
We have included the capacity to build (Gaussian) Radial Basis Function kernels on the mesh that can be used for smoothing operations. Radial-basis function (RBF) smoothing kernel works by setting up a series of gaussian functions based on the distance \(d\) between neighbouring nodes and a scaling factor, \(\Delta\):
delta
is set to the mean distance between nodes by default, but it may be changed to increase or decrease the smoothness:
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(rbf1.smooth_fn(height, iterations=1).evaluate(0.0,0.0))
print(rbf01.smooth_fn(rainfall, iterations=1).evaluate(0.0,0.0))
rbf005 = mesh.build_rbf_smoother(0.05, 1)
rbf010 = mesh.build_rbf_smoother(0.10, 1)
rbf050 = mesh.build_rbf_smoother(0.50, 1)
rbf_slope005 = rbf005.smooth_fn(mesh.slope).evaluate(mesh)
rbf_slope010 = rbf010.smooth_fn(mesh.slope).evaluate(mesh)
rbf_slope050 = rbf050.smooth_fn(mesh.slope).evaluate(mesh)
NOTE - Building the RBF smoothing machinery is expensive and cannot be reused if the kernel properties are changed. We therefore have a two-stage implementation which builds and caches the smoothing matrices and defines a quagmire function that can be used in the usual way.
import k3d
plot = k3d.plot(grid_visible=False)
indices = mesh.tri.simplices.astype(np.uint32)
points = np.column_stack([mesh.tri.points, height]).astype(np.float32)
plt_mesh = k3d.mesh(points, indices, wireframe=False, flat_shading=False, side="double",
color_map = k3d.colormaps.basic_color_maps.CoolWarm,
attribute=mesh.slope.evaluate(mesh),
# color_range = [-1.1,2.01]
)
plot += plt_mesh
plot.display()