Skip to content

Commit 33622be

Browse files
author
Daniel Precioso, PhD
committed
Enhance 1D and 2D PDE modules with detailed explanations on reaction-diffusion systems, boundary conditions, and stability analysis. Update Gierer-Meinhardt and Gray-Scott models to clarify numerical methods and implementation steps.
1 parent c379ea1 commit 33622be

6 files changed

Lines changed: 386 additions & 50 deletions

File tree

modules/pde-1d/index.qmd

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ subtitle: "Gierer-Meinhardt and Turing Instability"
44
format: html
55
---
66

7-
This session introduces reaction-diffusion PDEs on a line. We will discretize space with finite differences, advance in time with explicit Euler, and use Turing instability to predict when spatial patterns should appear [@turing1952chemical; @gierer1972theory].
7+
This session introduces reaction-diffusion PDEs on a line. We will discretize space with vectorized finite differences, advance in time with explicit Euler, and use Turing instability, steady states, Jacobians, and spatial modes to predict when spatial patterns should appear [@turing1952chemical; @gierer1972theory].
88

99
## From Networks to PDEs
1010

@@ -28,7 +28,7 @@ First, we start with the **Gierer-Meinhardt model in 1D**. This is the cleanest
2828

2929
[Gierer-Meinhardt 1D](gierer-meinhardt-1d.qmd){.btn .btn-primary}
3030

31-
Then we ask the theoretical question: **when should patterns appear at all?** That is where Turing instability enters.
31+
Then we ask the theoretical question: **when should patterns appear at all?** That is where Turing instability enters, through steady states, Jacobians, spatial modes, and the mode matrices that define Turing space.
3232

3333
[Turing Instability](turing-instability.qmd){.btn .btn-primary}
3434

@@ -42,13 +42,13 @@ Once the numerical and theoretical pieces are in place, package them in the assi
4242
- Explain how boundary conditions affect a PDE solver.
4343
- Simulate a reaction-diffusion system on an interval.
4444
- Connect observed patterns with Turing instability.
45-
- Predict unstable spatial modes on a finite domain.
45+
- Use steady states, Jacobians, and Laplacian eigenmodes to predict unstable spatial modes on a finite domain.
4646

4747
## Flow of This Session
4848

4949
1. Start in 1D and implement the Laplacian with centered differences.
5050
2. Add explicit Euler time stepping and boundary conditions.
51-
3. Study Turing instability and connect the theory to the patterns you see.
51+
3. Study Turing instability from the equilibrium, the Jacobian, and the mode matrices.
5252
4. Use the theory to predict unstable modes on a finite interval.
5353
5. Complete the 1D assignment.
5454

@@ -57,7 +57,7 @@ Once the numerical and theoretical pieces are in place, package them in the assi
5757
Every page in this session follows the same numerical loop:
5858

5959
1. Create a spatial grid.
60-
2. Approximate the Laplacian with finite differences.
60+
2. Approximate the Laplacian with vectorized finite differences.
6161
3. Evaluate the reaction terms.
6262
4. Update the fields with Euler's method.
6363
5. Enforce the boundary conditions.

modules/pde-1d/turing-instability.qmd

Lines changed: 217 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,21 @@ plt.show()
3636
plt.close()
3737
```
3838

39-
## Write the Jacobian at the Fixed Point
39+
## The Linear Stability Pipeline
4040

41-
Let
41+
For a two-species reaction-diffusion system
42+
43+
$$
44+
\partial_t w = D\,\Delta w + R(w), \qquad D = \operatorname{diag}(d_1, d_2),
45+
$$
46+
47+
start from a homogeneous steady state $w_*$ with $R(w_*) = 0$. Write $w = w_* + \eta$ and keep only linear terms:
48+
49+
$$
50+
\partial_t \eta = D\,\Delta \eta + J\eta,
51+
$$
52+
53+
where
4254

4355
$$
4456
J =
@@ -48,11 +60,95 @@ g_u & g_v
4860
\end{pmatrix}
4961
$$ {#eq-pde-turing-jacobian}
5062
51-
be the Jacobian of the reaction terms at the homogeneous steady state. For the Gierer-Meinhardt model, the fixed point is known analytically, so you can hard-code the partial derivatives.
63+
is the Jacobian of the reaction terms at the steady state.
64+
65+
Now expand the perturbation in Laplacian eigenmodes,
66+
67+
$$
68+
\eta(x,t) = c_n(t)\,\phi_n(x), \qquad \Delta \phi_n = \lambda_n \phi_n, \qquad \lambda_n \le 0.
69+
$$
70+
71+
Each spatial mode evolves independently as
72+
73+
$$
74+
\dot c_n = F_n c_n, \qquad F_n = J + \lambda_n D.
75+
$$ {#eq-pde-turing-mode-matrix}
76+
77+
Because $\lambda_n = -k_n^2$, many notes write the same matrix as
78+
79+
$$
80+
A_n = J - k_n^2 D.
81+
$$ {#eq-pde-turing-an}
82+
83+
So the workflow is always the same.
84+
85+
- Find the homogeneous equilibrium.
86+
- Compute the Jacobian at that equilibrium.
87+
- Compute the Laplacian eigenvalues $\lambda_n$ from the domain and the boundary conditions.
88+
- Build $F_n$ or $A_n$.
89+
- Compute the eigenvalues of that $2 \times 2$ matrix.
90+
- Keep the modes whose largest real part is positive.
91+
92+
For a $2 \times 2$ matrix, the growth rates $\sigma$ satisfy
93+
94+
$$
95+
\sigma^2 - \operatorname{tr}(F_n)\sigma + \det(F_n) = 0.
96+
$$
97+
98+
With $D = \operatorname{diag}(1, d)$, the diffusion-shifted matrix has
99+
100+
$$
101+
\operatorname{tr}(A_n) = (f_u + g_v) - (1 + d)k_n^2,
102+
$$
103+
104+
$$
105+
\det(A_n) = (f_u g_v - f_v g_u) - (d f_u + g_v)k_n^2 + d k_n^4.
106+
$$ {#eq-pde-turing-det-an}
107+
108+
Diffusion makes the trace more negative, so the Turing mechanism is detected through the determinant. The flat state is stable when $\operatorname{tr}(J) < 0$ and $\det(J) > 0$. A Turing instability appears when some nonzero mode makes $\det(A_n) < 0$.
109+
110+
## Compute the Gierer-Meinhardt Equilibrium
111+
112+
For the Gierer-Meinhardt model,
113+
114+
$$
115+
f(u, v) = a - b u + \frac{u^2}{v}, \qquad g(u, v) = u^2 - v.
116+
$$
117+
118+
The homogeneous steady state solves $f(u_*, v_*) = 0$ and $g(u_*, v_*) = 0$. From $g(u_*, v_*) = 0$ we get $v_* = u_*^2$. Substituting into $f = 0$ gives
119+
120+
$$
121+
u_* = \frac{a+1}{b}, \qquad v_* = \left(\frac{a+1}{b}\right)^2.
122+
$$ {#eq-pde-turing-gm-steady}
123+
124+
```python
125+
def gierer_meinhardt_equilibrium(a=0.40, b=1.00):
126+
u_star = (a + 1) / b
127+
v_star = u_star**2
128+
return u_star, v_star
129+
```
130+
131+
## Write the Jacobian at the Fixed Point
132+
133+
At a general point $(u, v)$, the Jacobian is
134+
135+
$$
136+
J(u, v) =
137+
\begin{pmatrix}
138+
-b + 2u/v & -u^2/v^2 \\
139+
2u & -1
140+
\end{pmatrix}.
141+
$$
142+
143+
Evaluating at $(u_*, v_*)$ gives the entries needed for the Turing test.
52144
53145
```python
54146
def gierer_meinhardt_jacobian(a=0.40, b=1.00):
55-
# TODO: compute fu, fv, gu, gv at the fixed point
147+
u_star, v_star = gierer_meinhardt_equilibrium(a, b)
148+
fu = -b + 2 * u_star / v_star
149+
fv = -(u_star**2) / (v_star**2)
150+
gu = 2 * u_star
151+
gv = -1.0
56152
return fu, fv, gu, gv
57153
```
58154
@@ -71,7 +167,7 @@ def gierer_meinhardt_jacobian(a=0.40, b=1.00):
71167
72168
## Implement the Turing Test
73169
74-
A common set of conditions for Turing instability is
170+
From $\operatorname{tr}(J) < 0$, $\det(J) > 0$, and the requirement that $\det(A_n)$ becomes negative for some nonzero mode, we recover the standard three conditions
75171
76172
$$
77173
\begin{aligned}
@@ -81,6 +177,8 @@ $$
81177
\end{aligned}
82178
$$ {#eq-pde-turing-conditions}
83179
180+
The last inequality is exactly the statement that the quadratic $\det(A_n)$ dips below zero for some $k_n^2 > 0$.
181+
84182
Translate these three inequalities into Python.
85183
86184
```python
@@ -138,33 +236,36 @@ plt.show()
138236
139237
Mark the point $(a, d) = (0.40, 20)$ on your plot. Is it inside the instability region?
140238
141-
## Scan Spatial Modes in 1D
239+
This is the same Turing-space diagram that appears in the left panel of the interactive Gierer-Meinhardt code, so the algebra and the simulation refer to the same parameter plane.
240+
241+
## Compute $F_n$ and the Unstable Mode List in 1D
142242
143-
The Turing conditions say that some spatial mode should grow, but they do not tell you which one. For Neumann boundary conditions on $[0, L]$, the admissible wavenumbers are
243+
The Turing conditions say that some spatial mode should grow, but they do not tell you which one. For Neumann boundary conditions on $[0, L]$, the Laplacian eigenvalues are
144244
145245
$$
146-
k_n = \frac{n\pi}{L}, \qquad n = 0, 1, 2, \dots
246+
\lambda_n = -\left(\frac{n\pi}{L}\right)^2, \qquad n = 0, 1, 2, \dots
147247
$$ {#eq-pde-turing-1d-modes}
148248
149-
The corresponding diffusion-shifted matrix is
249+
Equivalently, $k_n = n\pi/L$. The mode matrix is
150250
151251
$$
152-
A_n = J - k_n^2\,\mathrm{diag}(1, d).
252+
F_n = J + \lambda_n\,\mathrm{diag}(1, d) = J - k_n^2\,\mathrm{diag}(1, d).
153253
$$ {#eq-pde-turing-1d-matrix}
154254
155-
Complete the function below and return the indices of the unstable modes.
255+
Compute the eigenvalues of each $F_n$, keep the modes with positive growth, and sort them from strongest to weakest.
156256
157257
```python
158258
import numpy as np
159259
160260
def find_unstable_modes_1d(a=0.40, b=1.00, d=30.0, length=40.0, num_modes=10):
161261
fu, fv, gu, gv = gierer_meinhardt_jacobian(a, b)
162262
jac = np.array([[fu, fv], [gu, gv]])
163-
max_eigs = np.zeros(num_modes)
263+
diffusion = np.diag([1.0, d])
264+
unstable_modes = []
164265
165266
for n in range(1, num_modes):
166-
# TODO: compute k_n and the matrix A_n
167-
# TODO: store the largest real part of the eigenvalues
267+
# TODO: compute lambda_n and the matrix F_n
268+
# TODO: store modes with positive largest real part
168269
169270
# TODO: return the unstable modes sorted from strongest to weakest
170271
```
@@ -176,43 +277,52 @@ def find_unstable_modes_1d(a=0.40, b=1.00, d=30.0, length=40.0, num_modes=10):
176277
def find_unstable_modes_1d(a=0.40, b=1.00, d=30.0, length=40.0, num_modes=10):
177278
fu, fv, gu, gv = gierer_meinhardt_jacobian(a, b)
178279
jac = np.array([[fu, fv], [gu, gv]])
179-
max_eigs = np.zeros(num_modes)
280+
diffusion = np.diag([1.0, d])
281+
unstable_modes = []
180282
181283
for n in range(1, num_modes):
182-
k_n = n * np.pi / length
183-
a_n = jac - (k_n**2) * np.diag([1, d])
184-
eigvals = np.linalg.eigvals(a_n)
185-
max_eigs[n] = np.max(eigvals.real)
284+
lambda_n = -(n * np.pi / length) ** 2
285+
f_n = jac + lambda_n * diffusion
286+
growth_rate = np.max(np.linalg.eigvals(f_n).real)
287+
if growth_rate > 0:
288+
unstable_modes.append((n, growth_rate))
186289
187-
unstable = np.where(max_eigs > 0)[0]
188-
return unstable[np.argsort(max_eigs[unstable])[::-1]]
290+
unstable_modes.sort(key=lambda item: item[1], reverse=True)
291+
return unstable_modes
189292
```
190293
:::
191294
295+
The first entry in this list is the dominant linear mode, which predicts the earliest visible wavelength in the simulation.
296+
192297
## Extend the Scan to 2D
193298
194-
In two dimensions, the modes are pairs $(n_x, n_y)$. On a rectangle with side lengths $L_x$ and $L_y$,
299+
In two dimensions, the modes are pairs $(n_x, n_y)$. On a rectangle with side lengths $L_x$ and $L_y$ and Neumann boundaries,
195300
196301
$$
197-
k_{n_x,n_y}^2 = \left(\frac{n_x\pi}{L_x}\right)^2 + \left(\frac{n_y\pi}{L_y}\right)^2.
302+
\lambda_{n_x,n_y} = -\left(\frac{n_x\pi}{L_x}\right)^2 - \left(\frac{n_y\pi}{L_y}\right)^2.
198303
$$ {#eq-pde-turing-2d-modes}
199304
200-
The only real change is that you now scan a grid of mode pairs.
305+
The only real change is that you now scan a grid of mode pairs and build
306+
307+
$$
308+
F_{n_x,n_y} = J + \lambda_{n_x,n_y}\,\mathrm{diag}(1, d).
309+
$$
201310
202311
```python
203312
import numpy as np
204313
205314
def find_unstable_modes_2d(a=0.40, b=1.00, d=30.0, length_x=20.0, length_y=50.0, num_modes=8):
206315
fu, fv, gu, gv = gierer_meinhardt_jacobian(a, b)
207316
jac = np.array([[fu, fv], [gu, gv]])
317+
diffusion = np.diag([1.0, d])
208318
unstable_modes = []
209319
210320
for nx in range(num_modes):
211321
for ny in range(num_modes):
212322
if nx == 0 and ny == 0:
213323
continue
214324
215-
# TODO: compute k_sq and A_n
325+
# TODO: compute lambda_nm and F_nm
216326
# TODO: store mode pairs with positive growth rate
217327
218328
return unstable_modes
@@ -225,21 +335,100 @@ def find_unstable_modes_2d(a=0.40, b=1.00, d=30.0, length_x=20.0, length_y=50.0,
225335
def find_unstable_modes_2d(a=0.40, b=1.00, d=30.0, length_x=20.0, length_y=50.0, num_modes=8):
226336
fu, fv, gu, gv = gierer_meinhardt_jacobian(a, b)
227337
jac = np.array([[fu, fv], [gu, gv]])
338+
diffusion = np.diag([1.0, d])
228339
unstable_modes = []
229340
230341
for nx in range(num_modes):
231342
for ny in range(num_modes):
232343
if nx == 0 and ny == 0:
233344
continue
234345
235-
k_sq = (nx * np.pi / length_x) ** 2 + (ny * np.pi / length_y) ** 2
236-
a_n = jac - k_sq * np.diag([1, d])
237-
growth_rate = np.max(np.linalg.eigvals(a_n).real)
346+
lambda_nm = -((nx * np.pi / length_x) ** 2 + (ny * np.pi / length_y) ** 2)
347+
f_nm = jac + lambda_nm * diffusion
348+
growth_rate = np.max(np.linalg.eigvals(f_nm).real)
238349
if growth_rate > 0:
239350
unstable_modes.append(((nx, ny), growth_rate))
240351
241352
unstable_modes.sort(key=lambda item: item[1], reverse=True)
242353
return unstable_modes
354+
355+
For periodic boundaries, the same loop works after replacing each factor of $\pi / L$ by $2\pi / L$.
356+
357+
## Apply the Same Workflow to Gray-Scott
358+
359+
The theory on this page does not depend on the specific reaction law. Gray-Scott uses the same equilibrium to Jacobian to mode-matrix pipeline [@gray1984autocatalytic; @pearson1993complex].
360+
361+
Write the reaction terms as
362+
363+
$$
364+
R_u(u, v) = -u v^2 + f(1-u), \qquad R_v(u, v) = u v^2 - (f+k)v.
365+
$$
366+
367+
One homogeneous equilibrium is always
368+
369+
$$
370+
(u_*, v_*) = (1, 0).
371+
$$
372+
373+
If $v_* \neq 0$, then $u_* v_* = f + k$ and the nontrivial equilibria satisfy
374+
375+
$$
376+
u_* = \frac{1}{2}\left(1 \pm \sqrt{1 - \frac{4(f+k)^2}{f}}\right), \qquad v_* = \frac{f+k}{u_*},
377+
$$ {#eq-pde-turing-gs-steady}
378+
379+
whenever $1 - 4(f+k)^2/f \ge 0$.
380+
381+
The Gray-Scott Jacobian at a homogeneous equilibrium is
382+
383+
$$
384+
J_{\mathrm{GS}} =
385+
\begin{pmatrix}
386+
-v_*^2 - f & -2u_*v_* \\
387+
v_*^2 & 2u_*v_* - (f+k)
388+
\end{pmatrix}.
389+
$$ {#eq-pde-turing-gs-jacobian}
390+
391+
At the trivial state $(1, 0)$, this Jacobian is diagonal and every linear mode decays. So the classical Turing test is only interesting at a nontrivial homogeneous equilibrium when one exists.
392+
393+
For periodic boundaries on a rectangle,
394+
395+
$$
396+
\lambda_{m,n} = -\left(\frac{2\pi m}{L_x}\right)^2 - \left(\frac{2\pi n}{L_y}\right)^2,
397+
$$
398+
399+
and the mode matrix is
400+
401+
$$
402+
F^{\mathrm{GS}}_{m,n} = J_{\mathrm{GS}} + \lambda_{m,n}\,\mathrm{diag}(d_1, d_2).
403+
$$
404+
405+
```python
406+
import numpy as np
407+
408+
def gray_scott_equilibria(f=0.040, k=0.060):
409+
equilibria = [(1.0, 0.0)]
410+
discriminant = 1 - 4 * (f + k) ** 2 / f
411+
412+
if discriminant >= 0:
413+
root = np.sqrt(discriminant)
414+
for sign in (+1, -1):
415+
u_star = 0.5 * (1 + sign * root)
416+
if u_star > 0:
417+
equilibria.append((u_star, (f + k) / u_star))
418+
419+
return equilibria
420+
421+
422+
def gray_scott_jacobian(u_star, v_star, f=0.040, k=0.060):
423+
return np.array(
424+
[
425+
[-v_star**2 - f, -2 * u_star * v_star],
426+
[v_star**2, 2 * u_star * v_star - (f + k)],
427+
]
428+
)
429+
```
430+
431+
That is the whole bridge from Gierer-Meinhardt to Gray-Scott. Keep the eigenvalue scan, replace the Jacobian, replace the boundary spectrum, then sort the unstable modes and read off the dominant one.
243432
```
244433
:::
245434

0 commit comments

Comments
 (0)