# Import modules
import time
import numpy as np
import matplotlib.pyplot as plt
from scipy.ndimage import convolve
from IPython.display import clear_output
73 Reaction-Diffusion
The Gray-Scott model describes the reaction and diffusion of two chemicals, A and B, and can generate a wide variety of patterns depending on the rates of reaction, diffusion, feed, and kill rates of the chemicals. The discretized equations governing the system for chemical A and B are:
A' = A + ( D_A \nabla^2 A - AB^2 + f(1-A)) \ \Delta t
B' = B + ( D_B \nabla^2 B - AB^2 - (k+f)B) \ \Delta t
D_A is the diffusion coefficient of chemical A
D_B is the diffusion coefficient of chemical B
f feed rate of chemical A
k kill rate of chemical B
\nabla^2 is the Laplacian operator in two dimensions
The Laplacian operator measures how a function changes in all directions around a given point. It does this by calculating the second derivative of the function in both the x and y directions. Applying the Laplacian operator at a specific point is akin to examining the immediate neighborhood around that point to determine whether the point is a peak, a valley, or flat terrain. Peaks and valleys indicate local maxima and minima, where the function’s value increases or decreases most steeply in all directions. In contrast, flat areas show where the function’s value changes minimally. This calculation yields a single value representing the divergence or convergence of the function’s gradient at that point.
The problem can be coded using constant boundary conditions or using the Born-von Karman conditions, which aims at simulating a finite media as an infinite system that “wraps around” at the edges similar to a torus. The heavy lifting in this code is done by the convolve
function from the SciPy module, which avoids having to code an expensive regular for
loop to scan every pixel. Another option would be to use the Numba module, which can substantially speed up Python code, but the trade off is that some functions, like convolve
, cannot be compiled, and sticking to explicit loop-based approaches for computing the Laplacian might be more straightforward in this scenario.
# Set random seed
5)
np.random.seed(
# Model parameters
= 2e-4 # Diffusion coefficient for chemical A
Da = 1e-4 # Diffusion coefficient for chemical B
Db = 0.075 # Feed rate of A
f = 0.035 # kill rate of B
k
# Laplacian weights
= np.array([ [0.05, 0.2, 0.05], [0.2, -1, 0.2], [0.05, 0.2, 0.05] ])
W
# Grid setup
= 100
rows = 100
cols = 1.0 / cols # x step
dx = 1.0 / rows # y step
dy
# Time stepping
= 2000 # Simulation time
T = 0.01 # Time step
dt
# Initialization
= np.ones((rows, cols))
A = np.zeros((rows, cols))
B
# Set initial conditions
= 1000
seeds
# Small area in the center
#A[size//2-5:size//2+5, size//2-5:size//2+5] = 0.0
#B[size//2-5:size//2+5, size//2-5:size//2+5] = 1.0
# Random seeds of A and B into each other
0,rows,seeds), np.random.randint(0,cols,seeds)] = 0.0
A[np.random.randint(0,rows,seeds), np.random.randint(0,cols,seeds)] = 1.0
B[np.random.randint(
# Pre-allocate arrays
= np.full((rows, cols, T), np.nan)
A_all = np.full((rows, cols, T), np.nan)
B_all
# Create function that computes A and B in each time step
def update(A, B, W, Da, Db, f, k, dt, dx):
= np.copy(A)
A_new = np.copy(B)
B_new
# Compute Laplacian using convolution and the weights matrix
= convolve(A, W, mode='wrap') / (dx*dy)
La = convolve(B, W, mode='wrap') / (dx*dy)
Lb
# Compute reaction and update each chemical
= A*B*B
reaction += (Da*La - reaction + f*(1-A)) * dt
A_new += (Db*Lb + reaction - (f+k)*B) * dt
B_new
return A_new, B_new
# Get time at the beginning of the simulation
= time.perf_counter()
tic
# Run the simulation
for t in range(T):
= update(A, B, W, Da, Db, f, k, dt, dx)
A, B
# Save current matrices
= A
A_all[:,:,t] = B
B_all[:,:,t]
# Get time at end of simulation
= time.perf_counter()
toc
# Let us know that the interpreter finished
print('Done in ',str(toc-tic))
Done in 1.564100638963282
def blend(M,N,alpha,rule):
"""Function to blend the matrices of the two chemicals"""
if rule == 'exp':
# Exponential merging
= alpha * np.exp(M) + (1 - alpha) * np.exp(N)
I
elif rule == 'log':
# Logarithmic merging (natural logarithm of one plus the input array to prevent log(0))
= alpha * np.log1p(M) + (1 - alpha) * np.log1p(N)
I
elif rule == 'linear':
= alpha * M + (1 - alpha) * N
I
# Normalize results
= (I - np.min(I)) / (np.max(I) - np.min(I))
I
return I
# Blend images before looping through frames
= blend(A_all, B_all, alpha=0.5, rule='log') img
# Visualize changes frame by frame
= 10 # set this to 1 for highest temporal detail)
frame_rate
for k in range(0, img.shape[2], frame_rate):
=True)
clear_output(wait='magma', interpolation='bilinear')
plt.imshow(img[:,:,k], cmap'off')
plt.axis( plt.show()
References
Here is an amazing online source that constituted the basis for this code: https://www.karlsims.com/rd.html