Planetary [Py]

A simple example simulating the evolution of a planet under the action of vertical uplift and bedrock channel erosion.

This example is pretty similar to Mountain [Py], although solving equation (1) without the hillslope linear diffusion term \(K_D \nabla^2 h\) on a spherical domain, leveraging Fastscapelib’s support for the HEALPix grid.

Note

Fastscapelib support for HEALPix is only available in the Linux and MacOS packages published on conda-forge.

import healpy as hp
import numpy as np
import matplotlib
import matplotlib.pyplot as plt

import fastscapelib as fs
# Theme that looks reasonably fine on both dark/light modes
matplotlib.style.use('Solarize_Light2')
matplotlib.rcParams['axes.grid'] = False

Setup the Grid, Flow Graph and Eroders

Create a HealpixGrid with a given value of nside, which corresponds to a number of refinement levels from the initial tessellation of the sphere. From this value we can compute the grid resolution and total number of nodes.

nside = 100

print("total nb. of nodes: ", hp.nside2npix(nside))
print("approx. resolution (km): ", hp.nside2resol(nside) * 6.371e3)
total nb. of nodes:  120000
approx. resolution (km):  65.19614456327078

As “boundary” conditions, we set a strip of a given width along the equator that will represent the planet’s ocean. This is done via the healpy library that allows handling HEALPix grids from within Python.

# set thin strips on each side from the equator (coastlines)
# with "FIXED VALUE" node status
nodes_status = np.zeros(hp.nside2npix(nside), dtype=np.uint8)
ocean_idx = hp.query_strip(nside, np.deg2rad(85), np.deg2rad(91))
nodes_status[ocean_idx] = fs.NodeStatus.FIXED_VALUE.value

# set "ghost" nodes between the two thin strips (ocean)
ocean_idx = hp.query_strip(nside, np.deg2rad(85.5), np.deg2rad(90.5))
nodes_status[ocean_idx] = fs.NodeStatus.GHOST.value

mask = nodes_status == fs.NodeStatus.GHOST.value

Create the grid object with a radius equal to the Earth’s radius.

grid = fs.HealpixGrid(nside, nodes_status, 6.371e6)

Create the flow graph and eroder objects. Set a mask for the flow graph from the “ocean” grid nodes.

flow_graph = fs.FlowGraph(
    grid,
    [fs.SingleFlowRouter(), fs.MSTSinkResolver()],
)
flow_graph.mask = mask

spl_eroder = fs.SPLEroder(
    flow_graph,
    k_coef=1.2e-11,
    area_exp=0.45,
    slope_exp=1,
    tolerance=1e-5,
)

urate = 9e-4

dt = 1e4

Create and Run a Simulation

Create a simple simulation runner.

def run_simulation(nsteps):
    rng = np.random.Generator(np.random.PCG64(123456789))
    init_elevation = rng.uniform(0, 1, size=grid.shape)
    elevation = init_elevation.copy()
    drainage_area = np.zeros_like(elevation)
    uplift_rate = np.full_like(elevation, urate)
    uplift_rate[grid.nodes_status() > fs.NodeStatus.CORE.value] = 0.

    for step in range(nsteps):
        uplifted_elevation = elevation + uplift_rate * dt
        
        # flow routing
        flow_graph.update_routes(uplifted_elevation)
        
        # flow accumulation (drainage area)
        flow_graph.accumulate(drainage_area, 1.0)
        
        # apply channel erosion then hillslope diffusion
        spl_erosion = spl_eroder.erode(uplifted_elevation, drainage_area, dt)
        
        # update topography
        elevation = uplifted_elevation - spl_erosion

    return elevation, drainage_area

Run the model for a few dozens of time steps (total simulation time: 400 000 years).

elevation, drainage_area = run_simulation(40)

Plot Outputs

  • Topographic elevation

hp.orthview(elevation, nest=False, cmap=plt.cm.copper)
../_images/c31465c65a1c2e0e1643236fbdd16e9bb4428c6b391052b9d3ced57833a0c110.png
  • Drainage area (log)

hp.orthview(np.log(drainage_area), nest=False);
../_images/f53f865fd7e7585e5d0ed502214dd328b44c3cfd6fafbc6cfb2a8c96684a51b9.png