6. Catchment analysis.¶
We start with “Swamp Mountain” from the previous notebooks. This is slightly modified so that there are no lakes / pits right at the boundary.
The catchments are identified by first finding all the outflow points of the mesh (local minima that correspond to the boundary mask) and then using the transpose of the downhill-propagation matrix \(D^T\) to run information (the unique ID of each outflow points) up to the top of the catchment.
The stopping condition is that no further change occurs.
Note in the context of multiple pathways, this operation produces a fuzzy catchment. The first thing we do in this notebook is to specify downhill_neighbours=1
Notebook contents¶
import matplotlib.pyplot as plt
import numpy as np
from quagmire import tools as meshtools
%matplotlib inline
Construct the swamp-mountain mesh¶
This time we take care to avoid lakes etc on the boundaries as this makes the catchment analysis more complicated. Visualise the mesh to make sure that this works.
from quagmire import QuagMesh
from quagmire import QuagMesh # all routines we need are within this class
from quagmire import QuagMesh
minX, maxX = -5.0, 5.0
minY, maxY = -5.0, 5.0,
spacingX = 0.05
spacingY = 0.05
x, y, simplices = meshtools.elliptical_mesh(minX, maxX, minY, maxY, spacingX, spacingY, 1.)
DM = meshtools.create_DMPlex(x, y, simplices)
DM = meshtools.refine_DM(DM, refinement_levels=2)
mesh = QuagMesh(DM, downhill_neighbours=1)
# Note ... this is what refinement does
x = mesh.coords[:,0]
y = mesh.coords[:,1]
print( "\nNumber of points in the triangulation: {}".format(mesh.npoints))
Underlying Mesh type: TriMesh
0 - Delaunay triangulation 0.3174192500000004s
0 - Calculate node weights and area 0.02463904100000036s
0 - Find boundaries 0.018807584000001043s
0 - cKDTree 0.022422916000039095s
0 - Construct neighbour cloud arrays 0.6958790830000225s, (0.4476076249999892s + 0.24823249999997188s)
0 - Construct rbf weights 0.07766774999998916s
Number of points in the triangulation: 156925
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 -= height.min()
## Add smoothed random noise to make some "lakes"
mesh._construct_rbf_weights(delta=mesh.delta*3.0)
randpts1 = np.where(np.random.random(height.shape)>0.995, -1.0, 0.0)
hrand1 = 20.0 * mesh.rbf_smoother(randpts1, iterations=10)
heightn = height + hrand1 * np.exp(-radius**2/15.0)
with mesh.deform_topography():
mesh.downhill_neighbours = 2
mesh.topography.data = heightn
# let's use a rainfall proportional to height (any choice is ok)
rainfall_fn = mesh.topography ** 2.0
flowrate_fn = mesh.upstream_integral_fn(rainfall_fn)
str_power_fn = mesh.upstream_integral_fn(rainfall_fn)**2.0 * mesh.slope ** 2.0
0 - Build downhill matrices 3.9534281659999806s
0 - Build upstream areas 1.520386708999979s
Process the surface to fill swamps and lakes¶
## Quagmire also has a swamp filling algorithm
mesh1s = QuagMesh(DM)
with mesh1s.deform_topography():
mesh1s.topography.data = mesh.topography.data
mesh1s.low_points_local_patch_fill(its=3, smoothing_steps=2, fraction=0.5)
for i in range(0,5):
mesh1s.low_points_swamp_fill(ref_height=-0.01)
# In parallel, we can't break if ANY processor has work to do (barrier / sync issue)
low_points2 = mesh1s.identify_global_low_points()
print("{} : {}".format(i,low_points2[0]))
if low_points2[0] == 0:
break
Underlying Mesh type: TriMesh
0 - Delaunay triangulation 0.2958527910000157s
0 - Calculate node weights and area 0.025907167000013942s
0 - Find boundaries 0.018733249999968393s
0 - cKDTree 0.020705749999933687s
0 - Construct neighbour cloud arrays 0.6505634170000576s, (0.42587704200002463s + 0.22465216600005533s)
0 - Construct rbf weights 0.05773158399995282s
0 - Build downhill matrices 2.4506870000000163s
0 - Build upstream areas 1.5300563330000614s
Low point local patch fill
0 - Build downhill matrices 2.43383074999997s
0 - Build downhill matrices 2.4526279579999937s
0 - Build downhill matrices 2.4396555829999897s
Low point local patch fill 10.608140625000033 seconds
0 - Build upstream areas 1.5366657079999868s
137 iterations, time = 1.2963485420000325
Build low point catchments - 1.2967541669999036 seconds
0 Sort spills - 0.0075850000000627915
0 Gather spill data - 6.412499999441934e-05
0 Sort all spills - 5.966699995951785e-05
0 - Build downhill matrices 2.4458806249999725s
Low point swamp fill 3.838560457999961 seconds
0 - Build upstream areas 1.7615719579999904s
0 : 72
169 iterations, time = 1.3446816670000317
Build low point catchments - 1.3451523749999978 seconds
0 Sort spills - 0.004237167000042064
0 Gather spill data - 6.758300003184559e-05
0 Sort all spills - 4.949999993186793e-05
0 - Build downhill matrices 2.4374321249999866s
Low point swamp fill 3.833339459000058 seconds
0 - Build upstream areas 1.972699499999976s
1 : 16
282 iterations, time = 1.536034083000004
Build low point catchments - 1.5364633750000394 seconds
0 Sort spills - 0.001867416999971283
0 Gather spill data - 6.645799999205337e-05
0 Sort all spills - 0.00011812500008545612
0 - Build downhill matrices 2.451272208999967s
Low point swamp fill 4.021867333000046 seconds
0 - Build upstream areas 2.0030419999999367s
2 : 7
282 iterations, time = 1.6317061669999475
Build low point catchments - 1.6321679579999682 seconds
0 Sort spills - 0.001334499999984473
0 Gather spill data - 6.400000006578921e-05
0 Sort all spills - 2.237499995771941e-05
0 - Build downhill matrices 2.4534335420000843s
Low point swamp fill 4.112114665999911 seconds
0 - Build upstream areas 1.980670000000032s
3 : 0
rainfall_fn_1s = mesh1s.topography ** 2.0
flowrate_fn_1s = mesh1s.upstream_integral_fn(rainfall_fn_1s)
str_power_fn_1s = mesh1s.upstream_integral_fn(rainfall_fn_1s)**2.0 * mesh1s.slope ** 2.0
Locating and viewing the outflow points¶
quagmire
provides a mechanism to find the outflow points of a domain and return the node values. Note: in parallel these are the local node numbers and are not a unique ID. To do this, we can obtain the global ID from PETSc but it does also help to have the indices all be small numbers so we can map colours effectively.
outflows = mesh1s.identify_outflow_points()
print("Mesh has {} outflows".format(outflows.shape[0]))
Mesh has 21 outflows
import quagmire
print(quagmire.mesh.check_object_is_a_q_mesh_and_raise(mesh1s))
True
import k3d
low_points = mesh.identify_low_points()
lowsxyz = np.column_stack([mesh.tri.points[low_points], mesh.topography.data[low_points]])
xyz = np.column_stack([mesh1s.tri.points, mesh1s.topography.data])
xyz2 = np.column_stack([mesh.tri.points, mesh.topography.data])
indices = mesh.tri.simplices.astype(np.uint32)
plot = k3d.plot(camera_auto_fit=False, grid_visible=False, lighting=0.75)
surface = k3d.mesh(xyz2, indices, wireframe=False, flat_shading=True, side="double",
color_map = k3d.colormaps.matplotlib_color_maps.Blues,
attribute=flowrate_fn_1s.evaluate(mesh),
color_range = [0.0,1.0]
)
lakes_surface = k3d.mesh(xyz-(0.0,0.0,0.005), indices, wireframe=False, flat_shading=True, side="double",
color=0x0010C0, opacity=0.5
)
lows = k3d.points(lowsxyz, point_size=0.05, color=0xFF0000)
plot += surface + lows + lakes_surface
plot.display()
## Stream power / slope where the lakes / swamps are located:
Set neighbours to 1 and compute “uphill” connectivity¶
In serial, i.e. for this demonstration, we number the outflow points by their order in the local mesh numbering. We can then use the mesh.uphill_propagation
routine to propagate this information from the outflow to the top of the catchment.
This routine returns the mesh data (for this processor) of a globally synchronised map of the information propagated from the selected points.
The routine is used in the computation of flood fills for the swamp algorithm and should be polished up a bit (sorry).
## Unique catchments requires the downhill matrix with downhill_neighbours=1
mesh1s.near_neighbours=1
# mesh1s.update_height(mesh1s.heightVariable.data)
## Need a unique ID that works in parallel ... global node number would work but
## not that easy to map to colours in lavavu
from petsc4py import PETSc
outflows
outflowID = mesh1s.lgmap_row.apply(outflows.astype(PETSc.IntType))
# But on 1 proc, this is easier / better:
outflowID = np.array(range(0, outflows.shape[0]))
ctmt = mesh1s.uphill_propagation(outflows, outflowID, its=99999, fill=-999999).astype(np.int)
282 iterations, time = 1.5911248339999702
<ipython-input-20-a1316dff9ef9>:11: DeprecationWarning: `np.int` is a deprecated alias for the builtin `int`. To silence this warning, use `int` by itself. Doing this will not modify any behavior and is safe. When replacing `np.int`, you may wish to use e.g. `np.int64` or `np.int32` to specify the precision. If you wish to review your current use, check the release note link for additional information.
Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations
ctmt = mesh1s.uphill_propagation(outflows, outflowID, its=99999, fill=-999999).astype(np.int)
Visualise the catchment information¶
import k3d
xyz = np.column_stack([mesh1s.tri.points, mesh1s.topography.data])
xyz2 = np.column_stack([mesh.tri.points, mesh.topography.data])
indices = mesh.tri.simplices.astype(np.uint32)
plot = k3d.plot(camera_auto_fit=False, grid_visible=False, lighting=0.75)
surface = k3d.mesh(xyz2, indices, wireframe=False, flat_shading=True, side="double",
color_map = k3d.colormaps.matplotlib_color_maps.Blues,
attribute=flowrate_fn_1s.evaluate(mesh),
color_range = [0.0,1.0]
)
lakes_surface = k3d.mesh(xyz-(0.0,0.0,0.01), indices, wireframe=False, flat_shading=True, side="double",
color=0x0010C0, opacity=0.5)
catchments = k3d.mesh(xyz2+(0.0,0.0,0.01), indices, wireframe=False, flat_shading=True, side="double",
color_map = k3d.colormaps.matplotlib_color_maps.Greens,
attribute=ctmt, opacity=0.5
# color_range = [0.0,1.0]
)
plot += surface + lows + lakes_surface + catchments
plot.display()
/Users/lmoresi/mambaforge/envs/underworld/lib/python3.9/site-packages/traittypes/traittypes.py:97: UserWarning: Given trait value dtype "float64" does not match required type "float32". A coerced copy has been created.
warnings.warn(