Skip to content

Commit 21a88c6

Browse files
committed
New tutorial on new NumPy integration in grass.tools
1 parent 17cc372 commit 21a88c6

File tree

7 files changed

+245
-0
lines changed

7 files changed

+245
-0
lines changed
17.8 KB
Loading
13.8 KB
Loading
17.3 KB
Loading
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
---
2+
title: "Using GRASS, NumPy, and Landlab for Scientific Modeling"
3+
author: "Anna Petrasova"
4+
date: 2025-10-01
5+
date-modified: today
6+
categories: [NumPy, raster, terrain, intermediate, Landlab, Python, hydrology, matplotlib]
7+
description: >
8+
Learn how to integrate NumPy arrays with GRASS tools
9+
on an example using Landlab modeling framework.
10+
image: thumbnail.webp
11+
format:
12+
ipynb: default
13+
html:
14+
toc: true
15+
code-tools: true
16+
code-copy: true
17+
code-fold: false
18+
engine: jupyter
19+
execute:
20+
eval: false
21+
jupyter: python3
22+
---
23+
24+
# Using GRASS, NumPy, and Landlab for Scientific Modeling
25+
26+
This short tutorial shows how to integrate NumPy arrays with GRASS tools to create a smooth workflow for scientific modeling and analysis in Python.
27+
28+
Specifically, it demonstrates a complete workflow: generating a terrain surface in GRASS, importing it into [Landlab](https://landlab.csdms.io/) (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.
29+
30+
With the [grass.tools API](https://grass.osgeo.org/grass-devel/manuals/python_intro.html) (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.
31+
32+
::: {.callout-note title="How to run this tutorial?"}
33+
34+
This tutorial is prepared to be run in a Jupyter notebook locally
35+
or using services such as Google Colab. You can [download the notebook here](grass_numpy_integration.ipynb).
36+
37+
If you are not sure how to get started with GRASS, checkout the tutorials [Get started with GRASS & Python in Jupyter Notebooks](../get_started/fast_track_grass_and_python.qmd)
38+
and [Get started with GRASS in Google Colab](../get_started/grass_gis_in_google_colab.qmd).
39+
40+
:::
41+
42+
![](logos.webp)
43+
44+
## Generating a fractal surface in GRASS
45+
46+
First we will import GRASS packages:
47+
48+
49+
```{python}
50+
# import standard Python packages
51+
import os
52+
import sys
53+
import subprocess
54+
55+
sys.path.append(
56+
subprocess.check_output(["grass", "--config", "python_path"], text=True).strip()
57+
)
58+
59+
# import GRASS python packages
60+
import grass.script as gs
61+
import grass.jupyter as gj
62+
from grass.tools import Tools
63+
```
64+
65+
Create a new GRASS project called "landlab". Since we will be working with artifical data, we don't need to
66+
provide any coordinate reference system, resulting in a generic cartesian system.
67+
68+
69+
```{python}
70+
gs.create_project("landlab")
71+
```
72+
73+
Initialize a session in this project and create a `Tools` object we will use for calling GRASS tools:
74+
75+
76+
```{python}
77+
session = gj.init("landlab")
78+
tools = Tools()
79+
```
80+
81+
82+
Since we are generating artificial data, we need to specify the dimensions (number of rows and columns).
83+
We also need to let GRASS know the actual coordinates; we will do that by setting
84+
the [computational region](https://grass.osgeo.org/grass-stable/manuals/g.region.html). Lower-left (south-west) corner of the data will be at the coordinates (0, 0),
85+
the coordinates of the upper-right (nort-east) corner are number of rows times cell resolution and number of columns times cell resolution.
86+
87+
88+
```{python}
89+
rows = 200
90+
cols = 250
91+
resolution = 10
92+
tools.g_region(s=0, w=0, n=rows * resolution, e=cols * resolution, res=resolution)
93+
```
94+
95+
### Creating a fractal surface as a NumPy array
96+
97+
We will create a simple fractal surface with [r.surf.fractal](https://grass.osgeo.org/grass-stable/manuals/r.surf.fractal.html).
98+
99+
{{< fa wand-magic-sparkles >}} The trick is to use the `output` parameter with the value `np.array` to request a NumPy array instead of a native GRASS raster. {{< fa wand-magic-sparkles >}}
100+
This way GRASS provides the array as the result of the call:
101+
102+
103+
```{python}
104+
import numpy as np
105+
106+
fractal = tools.r_surf_fractal(output=np.array, seed=6)
107+
```
108+
109+
::: {.callout-note title="NumPy arrays in GRASS version < 8.5"}
110+
111+
Directly passing NumPy arrays to GRASS tools and receiving them back is a new feature in GRASS v8.5.
112+
If you work with older versions of GRASS, you can use
113+
[grass.script.array](https://grass.osgeo.org/grass-stable/manuals/libpython/grass.script.html#script.array.array) and [grass.script.array3d](https://grass.osgeo.org/grass-stable/manuals/libpython/grass.script.html#script.array.array3d):
114+
115+
```{python}
116+
import grass.script as gs
117+
import grass.script.array as ga
118+
119+
gs.run_command("r.surf.fractal", seed=6, output="fractal")
120+
fractal = ga.array("fractal")
121+
```
122+
123+
:::
124+
125+
Now we can display `fractal` array e.g., using matplotlib library:
126+
127+
128+
```{python}
129+
import matplotlib.pyplot as plt
130+
131+
plt.figure()
132+
plt.imshow(fractal, cmap='terrain')
133+
plt.colorbar(label='Elevation')
134+
plt.title('Topography from r.surf.fractal')
135+
plt.xlabel('X-coordinate')
136+
plt.ylabel('Y-coordinate')
137+
plt.show()
138+
```
139+
140+
![Fractal surface generated with r.surf.fractal displayed using matplotlib](fractal_numpy.webp)
141+
142+
We can modify the array:
143+
144+
```{python}
145+
fractal *= 0.1
146+
fractal = np.abs(fractal)
147+
148+
plt.figure()
149+
plt.imshow(fractal, cmap='terrain')
150+
plt.colorbar(label='Elevation')
151+
plt.title('Modified topography from r.surf.fractal')
152+
plt.xlabel('X-coordinate')
153+
plt.ylabel('Y-coordinate')
154+
plt.show()
155+
```
156+
157+
![Modified fractal surface](fractal_numpy_abs.webp)
158+
159+
## From GRASS to Landlab
160+
161+
Now, let's use Landlab's modeling capabilities to burn in an initial drainage network using the
162+
Landlab's [Fastscape Eroder](https://landlab.readthedocs.io/en/latest/generated/api/landlab.components.stream_power.fastscape_stream_power.html).
163+
164+
Create the `RasterModelGrid` specifying the same dimensions we used for creating the fractal surface.
165+
Add the terrain elevations as a 1D array (flattened with `ravel`) to the grid's nodes under the standard field name "topographic__elevation".
166+
167+
```{python}
168+
from landlab import RasterModelGrid
169+
170+
grid = RasterModelGrid((rows, cols), xy_spacing=resolution)
171+
grid.add_field("topographic__elevation", fractal.ravel(), at="node")
172+
```
173+
174+
Run the erosion of the landscape, extract the resulting NumPy array and visualize it:
175+
176+
```{python}
177+
from landlab.components import LinearDiffuser
178+
from landlab.components import FlowAccumulator, FastscapeEroder
179+
180+
fa = FlowAccumulator(grid, flow_director="D8")
181+
fsp = FastscapeEroder(grid, K_sp=0.01, m_sp=0.5, n_sp=1.0)
182+
ld = LinearDiffuser(grid, linear_diffusivity=1)
183+
184+
for i in range(100):
185+
fa.run_one_step()
186+
fsp.run_one_step(dt=1.0)
187+
ld.run_one_step(dt=1.0)
188+
189+
elevation = grid.at_node['topographic__elevation'].reshape(grid.shape)
190+
191+
plt.figure()
192+
plt.imshow(elevation, cmap='terrain', origin='upper')
193+
plt.colorbar(label='Elevation')
194+
plt.title('Topography after erosion')
195+
plt.xlabel('X-coordinate')
196+
plt.ylabel('Y-coordinate')
197+
plt.show()
198+
```
199+
200+
![Eroded fractal surface with Landlab](fractal_landlab.webp)
201+
202+
## From Landlab to GRASS
203+
204+
Now we will bring the eroded topography back to GRASS for additional hydrology modeling.
205+
We will derive streams using the [r.watershed](https://grass.osgeo.org/grass-stable/manuals/r.watershed.html) and [r.stream.extract](https://grass.osgeo.org/grass-stable/manuals/r.stream.extract.html) tools.
206+
207+
{{< fa wand-magic-sparkles >}} The `Tools` API allows us to directly plugin the NumPy `elevation` array into the tool call. {{< fa wand-magic-sparkles >}}
208+
209+
210+
```{python}
211+
tools.r_watershed(elevation=elevation, accumulation="accumulation")
212+
tools.r_stream_extract(elevation=elevation, accumulation="accumulation", threshold=300, stream_vector="streams")
213+
```
214+
215+
And visualize them using `gj.Map` on top of shaded relief:
216+
217+
218+
```{python}
219+
tools.r_relief(input=elevation, output="relief")
220+
221+
m = gj.Map(width=400)
222+
m.d_rast(map="relief")
223+
m.d_vect(map="streams", type="line", color="blue", width=2)
224+
m.show()
225+
```
226+
227+
![Streams derived from eroded topography in GRASS](streams.webp)
228+
229+
Now if we want to store the eroded topography as a native GRASS raster, we can use
230+
[grass.script.array](https://grass.osgeo.org/grass-stable/manuals/libpython/grass.script.html#script.array.array)
231+
to create a GRASS array object with the dimensions given by the current computational region.
232+
Then we copy the NumPy array and write the raster map as GRASS native raster.
233+
234+
```{python}
235+
# Create a new grasss.script.array object
236+
grass_elevation = ga.array()
237+
# Copy values from elevation array
238+
grass_elevation[...] = elevation
239+
# Write new GRASS raster map 'elevation'
240+
grass_elevation.write("elevation")
241+
```
242+
243+
---
244+
245+
The development of this tutorial was supported by NSF Award #2322073, granted to Natrx, Inc.
22.2 KB
Loading
27.3 KB
Loading
36.6 KB
Loading

0 commit comments

Comments
 (0)