SPD Animation LLM Benchmark
Spd Animation
Table of Contents
This is a GenAI benchmark to help make the community aware of current large language model capabilities. In this benchmark, we test the ability of a model to write a script to create and run a streampower-diffusion landlab model, and save the results as an animation.
1. Prompt
Please make me a script that runs a 50x50 streampower diffusion model in landlab and generate an animation of a landscape evolving.
2. Gemini 3.1 Flash Lite
2.1. Response
To create a 50x50 landscape evolution model in Landlab, we will combine the FlowAccumulator (to route water), FastscapeEroder (to perform stream power incision), and LinearDiffuser (to smooth the landscape).
The script below uses matplotlib.animation to save the output as a .gif.
2.1.1. Prerequisites
Ensure you have the necessary libraries installed:
pip install landlab matplotlib numpy
2.1.2. Landlab Evolution Script
import numpy as np import matplotlib.pyplot as plt import matplotlib.animation as animation from landlab import RasterModelGrid from landlab.components import FlowAccumulator, FastscapeEroder, LinearDiffuser from landlab.plot import imshow_grid # 1. Setup Grid mg = RasterModelGrid((50, 50), xy_spacing=10.0) z = mg.add_zeros("topographic__elevation", at="node") # Add some initial random noise to the landscape z += np.random.rand(mg.number_of_nodes) * 0.1 # 2. Boundary Conditions # Set edges to closed, except for the bottom (outlet) mg.set_closed_boundaries_at_grid_edges(True, True, True, False) # 3. Initialize Components fr = FlowAccumulator(mg, flow_director='FlowDirectorD8') sp = FastscapeEroder(mg, K_sp=0.0001, m_sp=0.5, n_sp=1.0) ld = LinearDiffuser(mg, linear_diffusivity=0.01) # 4. Simulation Parameters dt = 1000 total_time = 500000 uplift_rate = 0.0001 n_steps = int(total_time // dt) # 5. Animation Setup fig, ax = plt.subplots() artists = [] # 6. Run Model for i in range(n_steps): # Uplift z[mg.core_nodes] += uplift_rate * dt # Process fr.run_one_step() sp.run_one_step(dt) ld.run_one_step(dt) # Store frames if i % 20 == 0: im = imshow_grid(mg, z, cmap='terrain', allow_colorbar=False, plot_name=f"Time: {i*dt}") artists.append([im]) # 7. Generate Animation ani = animation.ArtistAnimation(fig, artists, interval=50, blit=True) ani.save('landscape_evolution.gif', writer='pillow') plt.show()
2.1.3. Key Components Explained
- RasterModelGrid((50, 50), …)
- Defines your simulation grid dimensions.
- FlowAccumulator
- Essential for any fluvial model; it calculates how water flows across the grid nodes.
- FastscapeEroder
- Implements the stream power incision law, where erosion rate is proportional to drainage area and slope.
- LinearDiffuser
- Simulates hillslope processes like soil creep, which rounds off ridges and fills valleys.
- ArtistAnimation
-
This method collects the visual output of the grid at specific intervals and stitches them into a GIF. The pillow writer is used here as it is cross-platform and doesn’t require external software like FFmpeg.
Would you like to adjust the erosion parameters to create a more incised or more subdued landscape?
3. Gemini 3.5 Flash
3.1. Response
Here is a complete, self-contained Python script using the Landlab framework to run a \(50 \times 50\) landscape evolution model. It couples stream power (fluvial incision) with linear diffusion (hillslope creep) and uses Matplotlib to animate the landscape’s evolution over time.
3.1.1. Prerequisites
You will need landlab, numpy, and matplotlib installed. If you don’t have them, you can install them via pip:
pip install landlab numpy matplotlib
3.1.2. The Script
This script initializes a flat grid with a tiny amount of random noise (to give the rivers a place to start), uplifts the landscape continuously, and forces the bottom edge to act as an open boundary (outlet) where water and sediment can escape.
import matplotlib.animation as animation import matplotlib.pyplot as plt import numpy as np from landlab import RasterModelGrid from landlab.components import FlowAccumulator, LinearDiffuser, StreamPowerEroder # ========================================== # 1. Setup Grid and Parameters # ========================================== num_rows = 50 num_cols = 50 dx = 10.0 # Grid spacing in meters grid = RasterModelGrid((num_rows, num_cols), xy_spacing=dx) # Create the elevation field and add random noise to initiate drainage z = grid.add_zeros("topographic__elevation", at="node") np.random.seed(42) z += np.random.rand(grid.number_of_nodes) * 0.1 # Set boundary conditions: Bottom edge open, others closed grid.set_closed_boundaries_at_grid_edges( right_is_closed=True, top_is_closed=True, left_is_closed=True, bottom_is_closed=False, ) # Time parameters dt = 100.0 # Timestep in years total_steps = 200 # Number of frames in animation uplift_rate = 0.001 # Uplift rate in m/yr # Component parameters K_sp = 0.001 # Stream power erodibility coefficient m_sp = 0.5 # Area exponent n_sp = 1.0 # Slope exponent D = 0.01 # Hillslope diffusivity (m^2/yr) # ========================================== # 2. Initialize Landlab Components # ========================================== fa = FlowAccumulator(grid, surface="topographic__elevation", flow_director="D8") sp = StreamPowerEroder(grid, K_sp=K_sp, m_sp=m_sp, n_sp=n_sp) ld = LinearDiffuser(grid, linear_diffusivity=D) # ========================================== # 3. Setup Animation Plotting # ========================================== fig, ax = plt.subplots(figsize=(8, 6)) # Reshape elevation for 2D plotting z_matrix = z.reshape((num_rows, num_cols)) # Initial plot setup im = ax.imshow( z_matrix, cmap="terrain", extent=[0, num_cols * dx, 0, num_rows * dx], origin="lower", ) fig.colorbar(im, ax=ax, label="Elevation (m)") title = ax.set_title("Landscape Evolution: Year 0") ax.set_xlabel("X Distance (m)") ax.set_ylabel("Y Distance (m)") # ========================================== # 4. Animation Update Function # ========================================== def update_landscape(frame): # 1. Apply tectonic uplift to core (interior) nodes z[grid.core_nodes] += uplift_rate * dt # 2. Route flow across the landscape fa.run_one_step() # 3. Fluvial incision (Stream Power) sp.run_one_step(dt) # 4. Hillslope creep (Diffusion) ld.run_one_step(dt) # Update visual data z_matrix = z.reshape((num_rows, num_cols)) im.set_data(z_matrix) im.set_clim(vmin=z_matrix.min(), vmax=z_matrix.max()) title.set_text(f"Landscape Evolution: Year {int((frame + 1) * dt)}") return [im, title] # ========================================== # 5. Run and Save Animation # ========================================== ani = animation.FuncAnimation( fig, update_landscape, frames=total_steps, interval=50, # Delay between frames in ms blit=False, ) # To view live while running, uncomment the line below: # plt.show() # Save the animation as an MP4 file (requires ffmpeg) output_filename = "landscape_evolution.mp4" print(f"Running simulation and saving to {output_filename}...") ani.save(output_filename, writer="ffmpeg", fps=20) print("Done!")
3.1.3. Key Mechanics of this Script
- Boundary Conditions
- By setting bottomisclosed=False, the bottom row functions as a base-level boundary. Water and sediment empty out here, forcing dendritic channel networks to carve upward into the grid from this side.
- Component Coupling
- In every loop iteration (updatelandscape), the script routes water (FlowAccumulator), cuts valleys via water flow (StreamPowerEroder), and smooths steep ridges via gravity/soil creep (LinearDiffuser).
- Ffmpeg Requirement
- To save the animation as an .mp4, your machine needs ffmpeg installed. If you don’t have it, you can change the extension to “landscapeevolution.gif” and swap the writer to writer=“pillow”.
3.2. Result
The script runs, generates a nice simple landscape evolution animation
4. Claude Haiku 4.5
4.1. Response
I’ll create a Python script that runs a stream power diffusion model in LandLab and generates an animation of the landscape evolution. [Editors note: This was generated as a standalone file to download]
4.1.1. spd_animation.py
#!/usr/bin/env python3 """ Stream Power + Diffusion (SPD) landscape evolution model in LandLab. Runs a 50x50 grid, evolves the topography, and generates an animation. """ import numpy as np import matplotlib.pyplot as plt import matplotlib.animation as animation from matplotlib.colors import LightSource from landlab import RasterModelGrid from landlab.components import FlowRouter, StreamPowerEroder, LinearDiffuser from landlab.io.netcdf import write_netcdf def run_spd_model( grid_size=50, n_steps=500, dt=100, k_sp=1e-4, # Stream power erodibility k_diff=0.01, # Diffusivity coefficient m=0.5, # Stream power slope exponent n=1.0, # Stream power discharge exponent ): """ Run SPD landscape evolution model. Parameters ---------- grid_size : int Grid dimensions (grid_size x grid_size) n_steps : int Number of model iterations dt : float Time step in years k_sp : float Stream power erodibility coefficient k_diff : float Hillslope diffusivity m : float Stream power slope exponent n : float Stream power discharge exponent Returns ------- grid : RasterModelGrid Final model grid elevation_history : list Elevation arrays at each saved timestep time_steps : list Corresponding model times (years) """ # Create grid grid = RasterModelGrid((grid_size, grid_size), xy_spacing=50.0) # Add elevation field (initial condition: gentle noise on a slope) np.random.seed(42) x, y = grid.x_of_node, grid.y_of_node base_elev = 1000 + 0.001 * y # Gentle slope towards north noise = np.random.normal(0, 5, grid.number_of_nodes) elevation = base_elev + noise grid.add_field("topographic__elevation", elevation, at="node") # Set boundary conditions (fixed elevation at edges) grid.set_watershed_boundary_condition_outlet_id( 0, grid.nodes[0, 0], -9999 ) # Initialize components flow_router = FlowRouter(grid) sp_eroder = StreamPowerEroder(grid, K=k_sp, m=m, n=n, threshold_sp=0) diffuser = LinearDiffuser(grid, linear_diffusivity=k_diff) # Storage for animation elevation_history = [] time_steps = [] save_interval = max(1, n_steps // 50) # Save ~50 frames current_time = 0 print(f"Running {n_steps} steps on a {grid_size}x{grid_size} grid...") for step in range(n_steps): # Route flow flow_router.run_one_step() # Erode with stream power sp_eroder.run_one_step(dt) # Diffuse hillslopes diffuser.run_one_step(dt) current_time += dt # Save frame for animation if step % save_interval == 0: elevation_history.append(grid["topographic__elevation"].copy()) time_steps.append(current_time) if (step + 1) % 50 == 0: print(f" Step {step + 1}/{n_steps} (t={current_time/1000:.1f} ky)") print(f"Model complete. Final time: {current_time/1000:.1f} ky") return grid, elevation_history, time_steps def create_animation(elevation_history, time_steps, output_file="spd_evolution.mp4"): """ Create and save animation of landscape evolution. Parameters ---------- elevation_history : list List of elevation arrays time_steps : list List of corresponding times output_file : str Output video filename """ fig, ax = plt.subplots(figsize=(10, 9)) # Set up color limits for consistent scaling vmin = min(arr.min() for arr in elevation_history) vmax = max(arr.max() for arr in elevation_history) # Reshape for 2D visualization grid_size = int(np.sqrt(len(elevation_history[0]))) im = None def update(frame): nonlocal im # Reshape elevation to 2D grid elev_2d = elevation_history[frame].reshape((grid_size, grid_size)) # Clear previous image if im is not None: im.remove() # Plot with hillshade for better visualization ls = LightSource(azdeg=315, altdeg=45) hillshade = ls.hillshade(elev_2d, vert_exag=2) im = ax.imshow( hillshade, cmap="gray", origin="lower", extent=[0, grid_size * 50, 0, grid_size * 50], ) # Add elevation contours contours = ax.contour( elev_2d, levels=10, colors="white", alpha=0.3, extent=[0, grid_size * 50, 0, grid_size * 50], linewidths=0.5, ) time_ka = time_steps[frame] / 1000 ax.set_title( f"SPD Landscape Evolution\nTime: {time_ka:.1f} ka", fontsize=14, fontweight="bold", ) ax.set_xlabel("Distance (m)") ax.set_ylabel("Distance (m)") return [im] anim = animation.FuncAnimation( fig, update, frames=len(elevation_history), interval=50, blit=True, repeat=True, ) print(f"Saving animation to {output_file}...") anim.save(output_file, writer="ffmpeg", dpi=100) print(f"Animation saved!") plt.close(fig) def plot_summary(grid, elevation_history): """Plot initial and final topography.""" grid_size = int(np.sqrt(len(elevation_history[0]))) fig, axes = plt.subplots(1, 2, figsize=(14, 6)) elev_init = elevation_history[0].reshape((grid_size, grid_size)) elev_final = elevation_history[-1].reshape((grid_size, grid_size)) im0 = axes[0].imshow(elev_init, cmap="viridis", origin="lower") axes[0].set_title("Initial Topography", fontweight="bold") axes[0].set_xlabel("Grid index") axes[0].set_ylabel("Grid index") plt.colorbar(im0, ax=axes[0], label="Elevation (m)") im1 = axes[1].imshow(elev_final, cmap="viridis", origin="lower") axes[1].set_title("Final Topography", fontweight="bold") axes[1].set_xlabel("Grid index") axes[1].set_ylabel("Grid index") plt.colorbar(im1, ax=axes[1], label="Elevation (m)") # Add relief and slope info relief_init = elev_init.max() - elev_init.min() relief_final = elev_final.max() - elev_final.min() fig.text( 0.5, 0.02, f"Initial relief: {relief_init:.1f} m | Final relief: {relief_final:.1f} m", ha="center", fontsize=11, ) plt.tight_layout() plt.subplots_adjust(bottom=0.08) plt.savefig("spd_summary.png", dpi=150, bbox_inches="tight") print("Summary figure saved to spd_summary.png") plt.close(fig) if __name__ == "__main__": # Run model grid, elev_hist, times = run_spd_model( grid_size=50, n_steps=500, dt=100, k_sp=1e-4, k_diff=0.01, m=0.5, n=1.0, ) # Create animation create_animation(elev_hist, times, "spd_evolution.mp4") # Plot summary plot_summary(grid, elev_hist) print("\nDone! Check spd_evolution.mp4 and spd_summary.png")
I’ve created a complete SPD landscape evolution script for you. Here’s what it does: Key features:
- Runs a 50×50 grid with stream power erosion + hillslope diffusion
- 500 timesteps (100-year increments, easily tunable)
- Saves ~50 frames for smooth animation
- Generates an MP4 with hillshade visualization and elevation contours
- Creates a summary PNG comparing initial vs. final topography
To run it
python spd_animation.py
Requirements:
pip install landlab matplotlib numpy ffmpeg-python
The model parameters are customizable in the run_spd_model() call:
k_sp=1e-4- Stream power erodibility (higher = more incision)
- ~kdiff=0.001
- Hillslope diffusivity (higher = more smoothing)
-
mandn - Exponents in the stream power law
dt- Time step (100 years per step works well)
n_steps-
Total iterations
The animation visualizes landscape evolution with hillshading (standard geomorphology style), contours for elevation structure, and a time counter in ka. The summary figure shows relief change over the model run.
4.2. Result
Script did not run as written, it uses a hallucinated FlowRouter component, instead of FlowAccumulator.
Traceback (most recent call last): File "/home/jo/cub/csdms/genai_benchmarkets/spd_benchmark_claude-haiku-4.5.py", line 12, in <module> from landlab.components import FlowRouter, StreamPowerEroder, LinearDiffuser ImportError: cannot import name 'FlowRouter' from 'landlab.components' (/home/jo/micromamba/envs/csdms_env/lib/python3.12/site-packages/landlab/components/__init__.py)
Created: 2026-06-02 Tue 11:15
2.2. Result
Code did not run as written, with the following error trace. I’m not sure if it’s because it’s using matplotlib.animation incorrectly or if it’s a different bug.