Note: If running on Colab, uncomment and run the pip install cell below.
Lab 8: Earthquake Nucleation and Dynamic Rupture#
Learning Objectives#
Simulate the spring–slider instability and identify the transition between stable and unstable slip.
Visualize slip-weakening friction and compute fracture energy \(G\).
Explore how friction parameters (\(\tau_s\), \(\tau_d\), \(D_c\)) control the critical nucleation length \(L_c\).
Use an energy-balance argument to distinguish rupture propagation from arrest.
Prerequisites#
Lecture: Earthquake Nucleation and Dynamic Rupture (Module 8)
Lecture: Earthquake Source Dynamics (Module 7d)
Estimated Completion Time#
~60 minutes
# Uncomment if running on Colab:
# !pip install numpy matplotlib scipy
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
# ── figure defaults ──────────────────────────────────────────────────────────────────
plt.rcParams.update({
"figure.dpi": 110,
"axes.grid": True,
"grid.alpha": 0.3,
"font.size": 11,
"axes.titlesize": 12,
})
1. Spring–Slider Instability#
The simplest model of fault instability is a block on a frictional surface, loaded through a spring by a moving plate.
The driving stress is:
where \(k\) is the spring stiffness, \(v_{pl}\) is the plate velocity, and \(\delta\) is slip.
We use a velocity-weakening friction law:
where \(v\) is the slip velocity, \(v_0\) is a reference velocity, and \(\tau_s > \tau_d\) provides weakening at high velocity.
Predict#
Before running the code: If you decrease the spring stiffness \(k\) while keeping friction fixed, do you expect the system to become more or less unstable? Why?
def spring_slider_rhs(t, y, k, m, v_pl, tau_s, tau_d, v0):
"""
Right-hand side for the spring–slider ODE system.
State vector y = [delta, v] where delta is slip and v is slip velocity.
"""
delta, v = y
# Driving stress from the spring
tau_spring = k * (v_pl * t - delta)
# Velocity-weakening friction
tau_friction = tau_d + (tau_s - tau_d) * v0 / (abs(v) + v0)
# Newton's second law: m * dv/dt = tau_spring - tau_friction
dvdt = (tau_spring - tau_friction) / m
ddelta_dt = v
return [ddelta_dt, dvdt]
# ── Physical parameters ─────────────────────────────────────────────────────────
m = 1.0 # block mass (normalized)
v_pl = 1e-3 # plate velocity
tau_s = 1.0 # static friction strength
tau_d = 0.4 # dynamic friction strength
v0 = 1e-3 # reference velocity for friction law
t_end = 5000 # simulation duration
# Initial conditions: block at rest, no initial slip
y0 = [0.0, 0.0]
# Compare two stiffness values
k_values = [0.3, 1.5]
labels = [f"k = {k} (weak spring)" for k in k_values]
fig, axes = plt.subplots(2, 1, figsize=(10, 6), sharex=True)
for k, label in zip(k_values, labels):
sol = solve_ivp(
spring_slider_rhs, [0, t_end], y0,
args=(k, m, v_pl, tau_s, tau_d, v0),
max_step=1.0, rtol=1e-8, atol=1e-10,
)
axes[0].plot(sol.t, sol.y[0], label=label)
axes[1].plot(sol.t, sol.y[1], label=label)
axes[0].set_ylabel(r"Slip $\delta$")
axes[0].legend()
axes[0].set_title("Spring–slider simulation: slip vs. time")
axes[1].set_ylabel(r"Slip velocity $v$")
axes[1].set_xlabel("Time")
axes[1].legend()
axes[1].set_title("Slip velocity vs. time")
plt.tight_layout()
plt.show()
Explain#
Interpretation: Compare the two panels.
Which stiffness produces stick–slip behavior (sudden jumps in slip)?
Which produces smooth, stable sliding?
How does this relate to the instability criterion \(|d\tau_f/d\delta| > k\)?
Do: Find the critical stiffness#
Sweep through a range of \(k\) values and classify each run as “stick–slip” (unstable) or “stable” based on whether the peak slip velocity exceeds a threshold.
k_range = np.linspace(0.1, 2.0, 30)
max_velocities = []
for k in k_range:
sol = solve_ivp(
spring_slider_rhs, [0, t_end], y0,
args=(k, m, v_pl, tau_s, tau_d, v0),
max_step=2.0, rtol=1e-7, atol=1e-9,
)
max_velocities.append(np.max(np.abs(sol.y[1])))
max_velocities = np.array(max_velocities)
fig, ax = plt.subplots(figsize=(8, 4))
ax.semilogy(k_range, max_velocities, "ko-", ms=4)
ax.axhline(10 * v_pl, color="C3", ls="--", label=r"Threshold ($10 \times v_{pl}$)")
ax.set_xlabel("Spring stiffness $k$")
ax.set_ylabel("Peak slip velocity")
ax.set_title("Identifying the critical stiffness")
ax.legend()
plt.tight_layout()
plt.show()
# Estimate critical k
threshold = 10 * v_pl
k_crit_idx = np.where(max_velocities > threshold)[0]
if len(k_crit_idx) > 0:
k_crit_approx = k_range[k_crit_idx[-1]]
print(f"Approximate critical stiffness: k_c \u2248 {k_crit_approx:.2f}")
print(f"Friction weakening rate (\u0394\u03c4/D_c analogy): "
f"({tau_s} - {tau_d}) = {tau_s - tau_d:.1f}")
Check#
Does your estimated \(k_c\) match the theoretical instability criterion? In the slip-weakening framework, the weakening rate is \((\tau_s - \tau_d)/D_c\). With the velocity-weakening law used here, the effective weakening slope scales as \((\tau_s - \tau_d)/v_0\).
2. Slip-Weakening Friction Visualization#
Now let’s look at the slip-weakening friction law directly and compute the fracture energy \(G\).
Predict#
Before running: If you double \(D_c\) while keeping \(\tau_s\) and \(\tau_d\) fixed, how does \(G\) change? What about nucleation difficulty?
def slip_weakening_friction(delta, tau_s, tau_d, Dc):
"""Linear slip-weakening friction: tau(delta)."""
return np.where(delta < Dc, tau_s - (tau_s - tau_d) * delta / Dc, tau_d)
def fracture_energy(tau_s, tau_d, Dc):
"""Fracture energy for linear slip-weakening: G = 0.5 * (tau_s - tau_d) * Dc."""
return 0.5 * (tau_s - tau_d) * Dc
# Parameters to compare
Dc_values = [0.5, 1.0, 2.0]
tau_s_val = 1.0
tau_d_val = 0.4
delta = np.linspace(0, 4, 500)
fig, axes = plt.subplots(1, 2, figsize=(12, 4.5))
# Left: friction curves
for Dc in Dc_values:
tau = slip_weakening_friction(delta, tau_s_val, tau_d_val, Dc)
G = fracture_energy(tau_s_val, tau_d_val, Dc)
axes[0].plot(delta, tau, lw=2, label=rf"$D_c = {Dc}$, $G = {G:.2f}$")
axes[0].set_xlabel(r"Slip $\delta$")
axes[0].set_ylabel(r"Shear stress $\tau$")
axes[0].set_title("Slip-weakening friction for different $D_c$")
axes[0].legend(fontsize=9)
axes[0].set_xlim(0, 4)
# Right: fracture energy as a function of Dc
Dc_sweep = np.linspace(0.01, 3, 100)
G_sweep = fracture_energy(tau_s_val, tau_d_val, Dc_sweep)
axes[1].plot(Dc_sweep, G_sweep, "C1-", lw=2.5)
axes[1].set_xlabel(r"Critical slip distance $D_c$")
axes[1].set_ylabel(r"Fracture energy $G$")
axes[1].set_title(r"$G = \frac{1}{2}(\tau_s - \tau_d)\,D_c$")
# Mark the three Dc values
for Dc in Dc_values:
G = fracture_energy(tau_s_val, tau_d_val, Dc)
axes[1].plot(Dc, G, "ko", ms=7)
axes[1].annotate(rf"$D_c={Dc}$", (Dc, G), textcoords="offset points",
xytext=(8, 5), fontsize=9)
plt.tight_layout()
plt.show()
Explain#
Interpretation:
How does \(G\) scale with \(D_c\)? Is this linear, quadratic, or something else?
Connecting back to Module 7d: the fracture energy \(G\) you computed here is the same quantity that enters the energy budget \(\Delta E_\text{strain} = E_R + E_G + E_H\). What does a large \(G\) imply for radiation efficiency \(\eta_R\)?
3. Critical Nucleation Length#
For a 2-D fault with linear slip-weakening friction, the critical nucleation half-length scales as:
where \(\mu\) is the shear modulus and \(C\) is a geometric constant of order unity (depends on crack mode and geometry).
Predict#
Before running: Which parameter has the strongest effect on \(L_c\) — shear modulus \(\mu\), strength drop \((\tau_s - \tau_d)\), or critical slip distance \(D_c\)?
def nucleation_length(mu, Dc, delta_tau, C=1.0):
"""
Critical nucleation half-length for linear slip-weakening friction.
Parameters
----------
mu : float
Shear modulus (Pa).
Dc : float
Critical slip distance (m).
delta_tau : float
Strength drop tau_s - tau_d (Pa).
C : float
Geometric constant (~1 for mode II).
Returns
-------
Lc : float
Critical nucleation half-length (m).
"""
return C * mu * Dc / delta_tau
# Typical crustal values
mu = 30e9 # shear modulus, 30 GPa
# Sweep Dc and delta_tau
Dc_range = np.logspace(-3, 0, 50) # 1 mm to 1 m
dtau_values = [1e6, 5e6, 10e6, 20e6] # 1–20 MPa
fig, ax = plt.subplots(figsize=(8, 5))
for dtau in dtau_values:
Lc = nucleation_length(mu, Dc_range, dtau)
ax.loglog(Dc_range, Lc, lw=2,
label=rf"$\tau_s - \tau_d = {dtau/1e6:.0f}$ MPa")
ax.set_xlabel(r"Critical slip distance $D_c$ (m)")
ax.set_ylabel(r"Nucleation half-length $L_c$ (m)")
ax.set_title(rf"Critical nucleation length ($\mu = {mu/1e9:.0f}$ GPa)")
ax.legend()
# Reference lines
ax.axhspan(0.1, 10, alpha=0.05, color="C2")
ax.text(2e-3, 2, "Lab scale", fontsize=9, color="C2")
ax.axhspan(10, 1000, alpha=0.05, color="C1")
ax.text(2e-3, 100, "Natural faults", fontsize=9, color="C1")
plt.tight_layout()
plt.show()
# Print some representative values
print("\nRepresentative nucleation lengths:")
for Dc_ex in [0.001, 0.01, 0.1, 1.0]:
for dtau in [5e6, 10e6]:
Lc = nucleation_length(mu, Dc_ex, dtau)
print(f" Dc = {Dc_ex:.3f} m, \u0394\u03c4 = {dtau/1e6:.0f} MPa => Lc = {Lc:.1f} m")
Explain#
Interpretation:
For a typical crustal strength drop of ~5 MPa and \(D_c \approx 0.01\) m (a common estimate), what is the nucleation length?
Why does a larger \(D_c\) make nucleation harder? Connect your answer to the energy argument from the lecture.
What range of \(D_c\) gives nucleation lengths comparable to laboratory samples (~0.1 m)? To natural faults (~100 m)?
4. Energy Balance: Propagation vs. Arrest#
For a mode-II shear crack of half-length \(a\) under a uniform stress drop \(\Delta\sigma\), the energy release rate is:
Rupture propagates when \(\mathcal{G}(a) \geq G_c\), where \(G_c = \frac{1}{2}(\tau_s - \tau_d)D_c\).
The critical crack size is:
Predict#
Before running: If you increase the stress drop \(\Delta\sigma\), does the critical crack size \(a_c\) increase or decrease? Why?
def energy_release_rate(a, delta_sigma, mu):
"""Energy release rate for a mode-II crack."""
return np.pi * delta_sigma**2 * a / mu
def critical_crack_size(mu, Gc, delta_sigma):
"""Critical crack half-length."""
return mu * Gc / (np.pi * delta_sigma**2)
# Parameters
mu = 30e9 # shear modulus, 30 GPa
tau_s = 15e6 # 15 MPa static
tau_d = 10e6 # 10 MPa dynamic
Dc = 0.05 # 5 cm critical slip distance
Gc = 0.5 * (tau_s - tau_d) * Dc # fracture energy
# Stress drops to compare
dsigma_values = [1e6, 3e6, 5e6, 10e6] # 1–10 MPa
a = np.linspace(0, 2000, 500) # crack half-lengths in meters
fig, ax = plt.subplots(figsize=(9, 5))
# Fracture energy line
ax.axhline(Gc, color="k", ls="--", lw=2, label=rf"$G_c = {Gc:.0f}$ J/m$^2$")
for dsig in dsigma_values:
G = energy_release_rate(a, dsig, mu)
ac = critical_crack_size(mu, Gc, dsig)
ax.plot(a, G, lw=2,
label=rf"$\Delta\sigma = {dsig/1e6:.0f}$ MPa, $a_c = {ac:.0f}$ m")
ax.plot(ac, Gc, "o", ms=8, color="k", zorder=5)
ax.set_xlabel("Crack half-length $a$ (m)")
ax.set_ylabel(r"Energy release rate $\mathcal{G}$ (J/m$^2$)")
ax.set_title("Propagation vs. arrest")
ax.legend(fontsize=9)
ax.set_ylim(0, 5 * Gc)
ax.set_xlim(0, 2000)
plt.tight_layout()
plt.show()
# Summary table
print(f"\nFracture energy: G_c = {Gc:.0f} J/m\u00b2")
print(f"Shear modulus: \u03bc = {mu/1e9:.0f} GPa")
print(f"D_c = {Dc*100:.0f} cm\n")
print(f"{'\u0394\u03c3 (MPa)':>12} {'a_c (m)':>12}")
print("-" * 26)
for dsig in dsigma_values:
ac = critical_crack_size(mu, Gc, dsig)
print(f"{dsig/1e6:12.0f} {ac:12.0f}")
Explain#
Interpretation:
How does \(a_c\) depend on \(\Delta\sigma\)? Why does higher stress drop give a smaller critical size?
For \(\Delta\sigma = 3\) MPa (a typical value), what is the critical crack size? Is this consistent with observed nucleation zone sizes?
How does this framework explain why small earthquakes are far more common than large ones?
5. Wrap-Up: Synthesis Questions#
Answer the following questions based on your explorations above.
Spring–slider connection: In the spring–slider model, you found a critical stiffness separating stable from unstable behavior. How does this map to the nucleation length concept on a spatially extended fault?
Parameter sensitivity: You explored how \(D_c\), \(\Delta\tau\), and \(\Delta\sigma\) affect nucleation and propagation. Rank these three in terms of their influence on whether an earthquake occurs, and justify your ranking.
Energy budget bridge: In Lab 7d you computed radiated energy \(E_R\) and fracture energy \(G\) for a given earthquake. Using what you learned today, explain how \(G\) controls not just the energy budget of the earthquake, but also whether it nucleates at all.
Real-world complexity: The models in this lab use homogeneous parameters. Name two features of real faults that would complicate nucleation, and describe qualitatively how each would modify your results.