Skip to content

Commit 565c919

Browse files
authored
New tutorial on new grass.tools NumPy integration usind Landlab example (#96)
1 parent 94bf648 commit 565c919

File tree

8 files changed

+291
-0
lines changed

8 files changed

+291
-0
lines changed
36.6 KB
Loading
17.8 KB
Loading
13.8 KB
Loading
17.3 KB
Loading
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
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+
other-links:
12+
- text: "Intro to GRASS Python API"
13+
href: "https://grass.osgeo.org/grass-devel/manuals/python_intro.html"
14+
icon: link
15+
- text: "Check out new xarray-grass interface"
16+
href: "https://github.com/lrntct/xarray-grass"
17+
icon: link
18+
format:
19+
ipynb: default
20+
html:
21+
toc: true
22+
code-tools: true
23+
code-copy: true
24+
code-fold: false
25+
engine: jupyter
26+
execute:
27+
eval: false
28+
jupyter: python3
29+
---
30+
31+
# Introduction
32+
33+
This short tutorial shows how to integrate NumPy arrays with GRASS tools to create a smooth workflow for scientific modeling and analysis in Python.
34+
35+
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.
36+
37+
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.
38+
39+
::: {.callout-note title="How to run this tutorial?"}
40+
41+
This tutorial is prepared to be run in a Jupyter notebook locally
42+
or using services such as Google Colab. You can [download the notebook here](grass_numpy_integration.ipynb).
43+
44+
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)
45+
and [Get started with GRASS in Google Colab](../get_started/grass_gis_in_google_colab.qmd).
46+
47+
:::
48+
49+
![](logos.webp)
50+
51+
# Generating a fractal surface in GRASS
52+
53+
First we will import GRASS packages:
54+
55+
```{python}
56+
# import standard Python packages
57+
import os
58+
import sys
59+
import subprocess
60+
61+
# for some environments, this path setup can be skipped
62+
sys.path.append(
63+
subprocess.check_output(["grass", "--config", "python_path"], text=True).strip()
64+
)
65+
66+
# import GRASS Python packages
67+
import grass.script as gs
68+
import grass.jupyter as gj
69+
from grass.tools import Tools
70+
```
71+
72+
Create a new GRASS project called "landlab". Since we will be working with artifical data, we don't need to
73+
provide any coordinate reference system, resulting in a generic cartesian system.
74+
75+
```{python}
76+
gs.create_project("landlab")
77+
```
78+
79+
Initialize a session in this project and create a `Tools` object we will use for calling GRASS tools:
80+
81+
```{python}
82+
session = gj.init("landlab")
83+
tools = Tools()
84+
```
85+
86+
Since we are generating artificial data, we need to specify the dimensions (number of rows and columns).
87+
We also need to let GRASS know the actual coordinates we want to use; we will do all that by setting
88+
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),
89+
the coordinates of the upper-right (nort-east) corner are number of rows times cell resolution and number of columns times cell resolution.
90+
We chose a cell resolution of 10 horizontal units, which would impact certain terrain analyses, such as slope computation, and certain hydrology processes.
91+
92+
```{python}
93+
rows = 200
94+
cols = 250
95+
resolution = 10
96+
tools.g_region(s=0, w=0, n=rows * resolution, e=cols * resolution, res=resolution)
97+
```
98+
99+
## Creating a fractal surface as a NumPy array
100+
101+
We will create a simple fractal surface with [r.surf.fractal](https://grass.osgeo.org/grass-stable/manuals/r.surf.fractal.html).
102+
103+
{{< 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 >}}
104+
This way GRASS provides the array as the result of the call:
105+
106+
```{python}
107+
import numpy as np
108+
109+
fractal = tools.r_surf_fractal(output=np.array, seed=6)
110+
```
111+
112+
::: {.callout-note title="NumPy arrays in GRASS version < 8.5"}
113+
114+
Directly passing NumPy arrays to GRASS tools and receiving them back is a new feature in GRASS v8.5.
115+
If you work with older versions of GRASS, you can use
116+
[grass.script.array](https://grass.osgeo.org/grass-stable/manuals/libpython/grass.script.html#script.array.array):
117+
118+
```{python}
119+
import grass.script as gs
120+
import grass.script.array as ga
121+
122+
gs.run_command("r.surf.fractal", seed=6, output="fractal")
123+
fractal = ga.array("fractal")
124+
```
125+
126+
:::
127+
128+
Now we can display `fractal` array e.g., using matplotlib library:
129+
130+
```{python}
131+
import matplotlib.pyplot as plt
132+
133+
plt.figure()
134+
plt.imshow(fractal, cmap='terrain')
135+
plt.colorbar(label='Elevation')
136+
plt.title('Topography from r.surf.fractal')
137+
plt.xlabel('X-coordinate')
138+
plt.ylabel('Y-coordinate')
139+
plt.show()
140+
```
141+
142+
![Fractal surface generated with r.surf.fractal displayed using matplotlib](fractal_numpy.webp)
143+
144+
We can modify the array:
145+
146+
```{python}
147+
fractal *= 0.1
148+
fractal = np.abs(fractal)
149+
150+
plt.figure()
151+
plt.imshow(fractal, cmap='terrain')
152+
plt.colorbar(label='Elevation')
153+
plt.title('Modified topography from r.surf.fractal')
154+
plt.xlabel('X-coordinate')
155+
plt.ylabel('Y-coordinate')
156+
plt.show()
157+
```
158+
159+
![Modified fractal surface](fractal_numpy_abs.webp)
160+
161+
# From GRASS to Landlab
162+
163+
Now, let's use Landlab's modeling capabilities to burn in an initial drainage network using the
164+
Landlab's [Fastscape Eroder](https://landlab.readthedocs.io/en/latest/generated/api/landlab.components.stream_power.fastscape_stream_power.html).
165+
166+
Create the `RasterModelGrid` specifying the same dimensions we used for creating the fractal surface.
167+
Add the terrain elevations as a 1D array (flattened with `ravel`) to the grid's nodes under the standard field name "topographic__elevation".
168+
169+
```{python}
170+
from landlab import RasterModelGrid
171+
172+
grid = RasterModelGrid((rows, cols), xy_spacing=resolution)
173+
grid.add_field("topographic__elevation", fractal.ravel(), at="node")
174+
```
175+
176+
Run the erosion of the landscape:
177+
178+
```{python}
179+
from landlab.components import LinearDiffuser
180+
from landlab.components import FlowAccumulator, FastscapeEroder
181+
182+
fa = FlowAccumulator(grid, flow_director="D8")
183+
fsp = FastscapeEroder(grid, K_sp=0.01, m_sp=0.5, n_sp=1.0)
184+
ld = LinearDiffuser(grid, linear_diffusivity=1)
185+
186+
for i in range(100):
187+
fa.run_one_step()
188+
fsp.run_one_step(dt=1.0)
189+
ld.run_one_step(dt=1.0)
190+
```
191+
192+
Extract the resulting NumPy array and visualize it:
193+
194+
```{python}
195+
elevation = grid.at_node['topographic__elevation'].reshape(grid.shape)
196+
197+
plt.figure()
198+
plt.imshow(elevation, cmap='terrain', origin='upper')
199+
plt.colorbar(label='Elevation')
200+
plt.title('Topography after erosion')
201+
plt.xlabel('X-coordinate')
202+
plt.ylabel('Y-coordinate')
203+
plt.show()
204+
```
205+
206+
![Eroded fractal surface with Landlab](fractal_landlab.webp)
207+
208+
# From Landlab to GRASS
209+
210+
Now we will bring the eroded topography back to GRASS for additional hydrology modeling.
211+
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.
212+
213+
{{< fa wand-magic-sparkles >}} The `Tools` API allows us to directly plugin the NumPy `elevation` array into the tool call. {{< fa wand-magic-sparkles >}}
214+
215+
```{python}
216+
tools.r_watershed(elevation=elevation, accumulation="accumulation")
217+
tools.r_stream_extract(
218+
elevation=elevation,
219+
accumulation="accumulation",
220+
threshold=300,
221+
stream_vector="streams",
222+
)
223+
224+
```
225+
226+
And visualize them using `gj.Map` on top of shaded relief:
227+
228+
```{python}
229+
tools.r_relief(input=elevation, output="relief")
230+
231+
m = gj.Map(width=400)
232+
m.d_rast(map="relief")
233+
m.d_vect(map="streams", type="line", color="blue", width=2)
234+
m.show()
235+
```
236+
237+
![Streams derived from eroded topography in GRASS](streams.webp)
238+
239+
Now if we want to store the eroded topography as a native GRASS raster, we can use
240+
[grass.script.array](https://grass.osgeo.org/grass-stable/manuals/libpython/grass.script.html#script.array.array)
241+
to create a GRASS array object with the dimensions given by the current computational region.
242+
Then we copy the NumPy array and write the raster map as GRASS native raster.
243+
244+
```{python}
245+
import grass.script.array as ga
246+
247+
# Create a new grasss.script.array object
248+
grass_elevation = ga.array()
249+
# Copy values from elevation array
250+
grass_elevation[...] = elevation
251+
# Write new GRASS raster map 'elevation'
252+
grass_elevation.write("elevation")
253+
```
254+
255+
::: {.callout-note title="What about N-dimensional arrays?"}
256+
257+
For 3D arrays, you can use [grass.script.array3d](https://grass.osgeo.org/grass-stable/manuals/libpython/grass.script.html#script.array.array3d) package.
258+
259+
For N-dimensional arrays, check out the new [xarray-grass](https://github.com/lrntct/xarray-grass) bridge developed by [Laurent Courty](https://github.com/lrntct) {{< fa rocket >}}
260+
:::
261+
262+
Let's visualize the new GRASS elevation map with streams by setting its color
263+
scheme to `srtm_percent` and rendering it in 3D using the
264+
[grass.jupyter.Map3D](https://grass.osgeo.org/grass-stable/manuals/libpython/grass.jupyter.html?highlight=map3d#module-grass.jupyter.map3d) class:
265+
266+
```{python}
267+
tools.r_colors(map="elevation", color="srtm_percent")
268+
elevation_3dmap = gj.Map3D()
269+
elevation_3dmap.render(
270+
elevation_map="elevation",
271+
resolution_fine=1,
272+
vline="streams",
273+
vline_width=3,
274+
vline_color="blue",
275+
vline_height=3,
276+
position=[0.50, 0.00],
277+
height=5600,
278+
perspective=10,
279+
)
280+
elevation_3dmap.show()
281+
```
282+
283+
![Resulting elevation with streams in 3D](elevation_3d.webp)
284+
285+
And now let's augment it with [Nano Banana AI](https://nanobanana.ai/):
286+
287+
![AI edited landscape](thumbnail.webp)
288+
289+
---
290+
291+
The development of this tutorial and the `grass.tools` package (developed by Vaclav Petras) was supported by NSF Award #2322073, granted to Natrx, Inc.
22.2 KB
Loading
27.3 KB
Loading
60.2 KB
Loading

0 commit comments

Comments
 (0)