# import standard Python packages
import os
import sys
import subprocess
# for some environments, this path setup can be skipped
sys.path.append("grass", "--config", "python_path"], text=True).strip()
subprocess.check_output([
)
# import GRASS Python packages
import grass.script as gs
import grass.jupyter as gj
from grass.tools import Tools
Using GRASS, NumPy, and Landlab for Scientific Modeling
Learn how to integrate NumPy arrays with GRASS tools on an example using Landlab modeling framework.
Introduction
This short tutorial shows how to integrate NumPy arrays with GRASS tools to create a smooth workflow for scientific modeling and analysis in Python.
Specifically, it demonstrates a complete workflow: generating a terrain surface in GRASS, importing it into Landlab (an open-source Python library for running numerical models of Earth-surface dynamics), running an erosion model, and then returning the results to GRASS for further hydrologic analysis.
With the grass.tools API (introduced in GRASS v8.5), tools can now accept and return NumPy arrays directly, rather than working only with GRASS rasters. While native rasters remain the most efficient option for large-scale processing, NumPy arrays make it easy to connect GRASS with the broader Python scientific ecosystem, enabling advanced analysis and seamless use of familiar libraries.
This tutorial is prepared to be run in a Jupyter notebook locally or using services such as Google Colab. You can download the notebook here.
If you are not sure how to get started with GRASS, checkout the tutorials Get started with GRASS & Python in Jupyter Notebooks and Get started with GRASS in Google Colab.
Generating a fractal surface in GRASS
First we will import GRASS packages:
Create a new GRASS project called “landlab”. Since we will be working with artifical data, we don’t need to provide any coordinate reference system, resulting in a generic cartesian system.
"landlab") gs.create_project(
Initialize a session in this project and create a Tools
object we will use for calling GRASS tools:
= gj.init("landlab")
session = Tools() tools
Since we are generating artificial data, we need to specify the dimensions (number of rows and columns). We also need to let GRASS know the actual coordinates we want to use; we will do all that by setting the computational region. Lower-left (south-west) corner of the data will be at the coordinates (0, 0), the coordinates of the upper-right (nort-east) corner are number of rows times cell resolution and number of columns times cell resolution. We chose a cell resolution of 10 horizontal units, which would impact certain terrain analyses, such as slope computation, and certain hydrology processes.
= 200
rows = 250
cols = 10
resolution =0, w=0, n=rows * resolution, e=cols * resolution, res=resolution) tools.g_region(s
Creating a fractal surface as a NumPy array
We will create a simple fractal surface with r.surf.fractal.
The trick is to use the output
parameter with the value np.array
to request a NumPy array instead of a native GRASS raster. This way GRASS provides the array as the result of the call:
import numpy as np
= tools.r_surf_fractal(output=np.array, seed=6) fractal
Directly passing NumPy arrays to GRASS tools and receiving them back is a new feature in GRASS v8.5. If you work with older versions of GRASS, you can use grass.script.array:
import grass.script as gs
import grass.script.array as ga
"r.surf.fractal", seed=6, output="fractal")
gs.run_command(= ga.array("fractal") fractal
Now we can display fractal
array e.g., using matplotlib library:
import matplotlib.pyplot as plt
plt.figure()='terrain')
plt.imshow(fractal, cmap='Elevation')
plt.colorbar(label'Topography from r.surf.fractal')
plt.title('X-coordinate')
plt.xlabel('Y-coordinate')
plt.ylabel( plt.show()
We can modify the array:
*= 0.1
fractal = np.abs(fractal)
fractal
plt.figure()='terrain')
plt.imshow(fractal, cmap='Elevation')
plt.colorbar(label'Modified topography from r.surf.fractal')
plt.title('X-coordinate')
plt.xlabel('Y-coordinate')
plt.ylabel( plt.show()
From GRASS to Landlab
Now, let’s use Landlab’s modeling capabilities to burn in an initial drainage network using the Landlab’s Fastscape Eroder.
Create the RasterModelGrid
specifying the same dimensions we used for creating the fractal surface. Add the terrain elevations as a 1D array (flattened with ravel
) to the grid’s nodes under the standard field name “topographic__elevation”.
from landlab import RasterModelGrid
= RasterModelGrid((rows, cols), xy_spacing=resolution)
grid "topographic__elevation", fractal.ravel(), at="node") grid.add_field(
Run the erosion of the landscape:
from landlab.components import LinearDiffuser
from landlab.components import FlowAccumulator, FastscapeEroder
= FlowAccumulator(grid, flow_director="D8")
fa = FastscapeEroder(grid, K_sp=0.01, m_sp=0.5, n_sp=1.0)
fsp = LinearDiffuser(grid, linear_diffusivity=1)
ld
for i in range(100):
fa.run_one_step()=1.0)
fsp.run_one_step(dt=1.0) ld.run_one_step(dt
Extract the resulting NumPy array and visualize it:
= grid.at_node['topographic__elevation'].reshape(grid.shape)
elevation
plt.figure()='terrain', origin='upper')
plt.imshow(elevation, cmap='Elevation')
plt.colorbar(label'Topography after erosion')
plt.title('X-coordinate')
plt.xlabel('Y-coordinate')
plt.ylabel( plt.show()
From Landlab to GRASS
Now we will bring the eroded topography back to GRASS for additional hydrology modeling. We will derive streams using the r.watershed and r.stream.extract tools.
The Tools
API allows us to directly plugin the NumPy elevation
array into the tool call.
=elevation, accumulation="accumulation")
tools.r_watershed(elevation
tools.r_stream_extract(=elevation,
elevation="accumulation",
accumulation=300,
threshold="streams",
stream_vector )
And visualize them using gj.Map
on top of shaded relief:
input=elevation, output="relief")
tools.r_relief(
= gj.Map(width=400)
m map="relief")
m.d_rast(map="streams", type="line", color="blue", width=2)
m.d_vect( m.show()
Now if we want to store the eroded topography as a native GRASS raster, we can use grass.script.array to create a GRASS array object with the dimensions given by the current computational region. Then we copy the NumPy array and write the raster map as GRASS native raster.
import grass.script.array as ga
# Create a new grasss.script.array object
= ga.array()
grass_elevation # Copy values from elevation array
= elevation
grass_elevation[...] # Write new GRASS raster map 'elevation'
"elevation") grass_elevation.write(
For 3D arrays, you can use grass.script.array3d package.
For N-dimensional arrays, check out the new xarray-grass bridge developed by Laurent Courty
Let’s visualize the new GRASS elevation map with streams by setting its color scheme to srtm_percent
and rendering it in 3D using the grass.jupyter.Map3D class:
map="elevation", color="srtm_percent")
tools.r_colors(= gj.Map3D()
elevation_3dmap
elevation_3dmap.render(="elevation",
elevation_map=1,
resolution_fine="streams",
vline=3,
vline_width="blue",
vline_color=3,
vline_height=[0.50, 0.00],
position=5600,
height=10,
perspective
) elevation_3dmap.show()
And now let’s augment it with Nano Banana AI:
The development of this tutorial and the grass.tools
package (developed by Vaclav Petras) was supported by NSF Award #2322073, granted to Natrx, Inc.