Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Diffusion

DiffusionMicroMacro.gif

Diffusion at Micro, Meso and Macroscopic scales.

Microscopic Aspects of Diffusion: Random Walk

  • Microscopic theories of diffusion are built on random walk models, which lead to the derivation of the diffusion equation.

  • We begin by considering an unbiased random walk with n n steps, all originating from r0=0 r_0 = 0 .

  • Repeating this process NN times allows us to compute ensemble-averaged quantities. For instance, the average single-step displacement, confirms that the random walk is unbiased.

    ri=0,\langle r_i \rangle = 0,
  • The total displacement after n n steps is given by:

    Rn=i=0nriR_n = \sum^{n}_{i=0} r_i
  • Taking the ensemble average:

    Rn=i=0nri=0.\langle R_n \rangle = \sum^{n}_{i=0} \langle r_i \rangle = 0.
  • A more insightful measure is the mean square displacement (MSD), which quantifies fluctuations of the random walker relative to the origin as a function of time.

Source
import numpy as np

import matplotlib.pyplot as plt
import matplotlib.animation as animation
from mpl_toolkits.mplot3d import Axes3D
from IPython.display import HTML



def random_walk(num_steps, max_step=0.05):
    """Return a 3D random walk as (num_steps, 3) array."""
    start_pos = 0.5
    steps = np.random.uniform(-max_step, max_step, size=(num_steps, 3))
    walk = start_pos + np.cumsum(steps, axis=0)
    return walk


def update_lines(num, walks, lines):
    for line, walk in zip(lines, walks):
        line.set_data_3d(walk[:num, :].T)
    return lines


# Data: 40 random walks as (num_steps, 3) arrays
num_steps = 30
walks = [random_walk(num_steps) for index in range(40)]

# Attaching 3D axis to the figure
fig = plt.figure()
ax = fig.add_subplot(projection="3d")

# Create lines initially without data
lines = [ax.plot([], [], [])[0] for _ in walks]

# Setting the Axes properties
ax.set(xlim3d=(0, 1), xlabel='X')
ax.set(ylim3d=(0, 1), ylabel='Y')
ax.set(zlim3d=(0, 1), zlabel='Z')

# Creating the Animation object
ani = animation.FuncAnimation(
    fig, update_lines, num_steps, fargs=(walks, lines), interval=100)

plt.close()
HTML(ani.to_jshtml())
Loading...

Mean Square Displacement and Diffusion Coefficient

  • Expressing the number of steps in terms of time increments n=tδt n = \frac{t}{\delta t} , we compute the ensemble average over N N random walkers:

Rn2=i=1nj=1nrirj=iri2\langle R^2_n\rangle = \sum^{n}_{i=1} \sum^{n}_{j=1} \langle r_i r_j \rangle = \sum_i \langle r^2_i \rangle
  • Where the cross terms vanished becasue of independence of random variables rirj=rirj=0\langle r_i r_j \rangle = \langle r_i \rangle \langle r_j \rangle=0.

  • Since all dimensions are equal we can write for e.g, 3D random walker r2=3x2\langle r^2\rangle= 3\langle x^2\rangle or more generally for dd dimensions

Rn2=i=1ndli2=dnl2\langle R^2_n\rangle = \sum^{n}_{i=1} d \cdot \langle l^2_i \rangle = d \cdot n \cdot \langle l^2 \rangle
Rn2=dtδtl2\langle R^2_n\rangle = d \cdot \frac{t}{\delta t} \cdot \langle l^2 \rangle

Grouping constants together, we define the diffusion coefficient DD, which characterizes the spreading of a particle:

D=δx22δtD = \frac{\langle \delta x^2 \rangle}{2\delta t}
  • Using CLT we can also predict that after large number of steps displacements with respect to origin will be distributed following Gaussian with MSD controlling width of the gaussian

P(x,t)14πDtexp(x24Dt)P(x, t) \approx \frac{1}{\sqrt{4 \pi D t }} \exp\left(-\frac{x^2}{4Dt }\right)
Source
# Re-run the full script to ensure all variables are defined correctly

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import linregress

# Parameters
num_steps = 100  # Number of steps
num_walks = 40   # Number of random walks
max_step = 0.05  # Maximum step size
dt = 1  # Time step

# Function to generate 3D random walk
def random_walk(num_steps, max_step=0.05):
    """Return a 3D random walk as (num_steps, 3) array."""
    start_pos = np.zeros(3)
    steps = np.random.uniform(-max_step, max_step, size=(num_steps, 3))
    walk = start_pos + np.cumsum(steps, axis=0)
    return walk

# Function to compute MSD
def mean_square_displacement(walks):
    """Compute MSD over all walks."""
    squared_displacements = [np.sum(walk**2, axis=1) for walk in walks]
    return np.mean(squared_displacements, axis=0)

# Generate random walks
walks = [random_walk(num_steps, max_step) for _ in range(num_walks)]

# Select a single trajectory to visualize
single_walk = walks[0]

# Compute MSD
msd = mean_square_displacement(walks)

# Estimate diffusion coefficient from MSD (slope method)
time = np.arange(num_steps)
slope, intercept, _, _, _ = linregress(time, msd)
D_fit = slope / 6  # In 3D, MSD = 6Dt


# Estimate diffusion coefficient from step fluctuations
step_fluctuations = np.array([np.mean(np.sum(np.diff(walk, axis=0)**2, axis=1)) for walk in walks])
D_step = np.mean(step_fluctuations) / (6 * dt)
print('estimate from step fluctuations', D_step)

# Create figure for single trajectory and MSD
fig, axes = plt.subplots(1, 2, figsize=(12, 6))

# Plot a single trajectory in 3D
ax1 = fig.add_subplot(121, projection='3d')
ax1.plot(single_walk[:, 0], single_walk[:, 1], single_walk[:, 2], label="Single Trajectory", color='b', alpha=0.7)
ax1.scatter(single_walk[0, 0], single_walk[0, 1], single_walk[0, 2], color='g', marker='o', label="Start")
ax1.scatter(single_walk[-1, 0], single_walk[-1, 1], single_walk[-1, 2], color='r', marker='o', label="End")
ax1.set_title("3D Random Walk Trajectory")
ax1.set_xlabel("X Position")
ax1.set_ylabel("Y Position")
ax1.set_zlabel("Z Position")
ax1.legend()

# Plot MSD and diffusion coefficient estimations
ax2 = axes[1]
ax2.plot(time, msd, label="MSD (Simulated)", color='b')
ax2.plot(time, slope * time + intercept, '--', label=f"Linear Fit (D={D_fit:.4f})", color='r')

ax2.set_xlabel("Time step")
ax2.set_ylabel("Mean Square Displacement (MSD)")
ax2.set_title("MSD & Diffusion Estimation")
ax2.legend()
ax2.grid(alpha=0.3)

plt.tight_layout()
plt.show()
estimate from step fluctuations 0.0004229477939639462
<Figure size 1200x600 with 3 Axes>

Chapman-Kolmogorov and Transition probabilities

  • Key idea of time evolving probabilities: The total probability of going from y1y_1 to y3y_3 is obtained by considering every possible intermediate state y2y_2 and adding up the probabilities of taking those two consecutive transitions

Graphical representation of the Chapman–Kolmogorov equa- tion which relates the transition probability P(y_3, t_3 |y_1, t_1 ) to go from the start position y_1 at t_1 to the end position y_3 at t_3 to the transition probabilities to go from the start position to any position at t_2 and then from there to the end position.

Graphical representation of the Chapman–Kolmogorov equa- tion which relates the transition probability P(y3,t3y1,t1)P(y_3, t_3 |y_1, t_1 ) to go from the start position y1y_1 at t1t_1 to the end position y3y_3 at t3t_3 to the transition probabilities to go from the start position to any position at t2t_2 and then from there to the end position.

  • The Chapman-Kolmogorov (CK) relation is simply a formal restatement of basic probability rules (product rule and sum rule), tailored for stochastic processes evolving in time.

  • A situation of great interest to us is to start with some initial distirbution of molecules described by P(x,t)P(x',t) and connect it a distribution p(x,t+δt)p(x, t+\delta t).

  • Connecting two probability distirbutions at different times is done via p(x,t+δtx,t)p(x, t+\delta t| x', t) which can be interpreted as a transition probability

P(x,t+δt)=P(x,t+δtx,t)P(x,t)dxP(x, t+\delta t) = \int P(x, t+\delta t | x', t ) \cdot P(x', t ) \, dx'

Diffusion Equation emerges from random walk

  • Probability for particle to land at position xx from previous step can happen either from point to the left xδxx-\delta x or from point to the right x+δxx+\delta x.

  • Hence CK relation in this case becomes simple sum of probabnilities of being in those locations multiplied by 1/21/2 the transition probability to make the respective jumps

P(x,t+δt)=12P(xδx,t)+12P(x+δx,t).P(x, t+\delta t) = \frac{1}{2} P(x - \delta x, t ) + \frac{1}{2} P(x + \delta x, t).
  • We can also arrive at this same expression by pluging delta function representation of random walk into the integral expression of CK expression (see the delta function primer below).

  • These types of expressions which relate probability of an advanced step to previous steps or previous time increments are referred to as recurrence relations.

  • Expanding probabilities P(x±δx,t) P(x\pm \delta x, t) using a Taylor series in small δx \delta x we get:

P(x±δx,t)P(x,t)±δxPx+(δx)222Px2.P(x \pm \delta x, t ) \approx P(x, t ) \pm \delta x \frac{\partial P}{\partial x} + \frac{(\delta x)^2}{2} \frac{\partial^2 P}{\partial x^2}.
  • Substituting into the recurrence relation and cancelling first derivative terms with oposite signs we arrive at

P(x,t+δt)=P(x,t)+(δx)222Px2.P(x, t+\delta t) = P(x, t) + \frac{(\delta x)^2}{2} \frac{\partial^2 P}{\partial x^2}.
  • Rearranging to form time derivative:

P(x,t+δt)P(x,t)δt=(δx)22δt2Px2.\frac{P(x, t+\delta t) - P(x, t)}{\delta t} = \frac{(\delta x)^2}{2 \delta t} \frac{\partial^2 P}{\partial x^2}.
  • Taking the limit δt0 \delta t \to 0 and defining the diffusion coefficient D=(δx)22δt D = \frac{(\delta x)^2}{2 \delta t} , we obtain the diffusion equation

  • Probability at xx changes faster (time derivative) if there is a larger imabalance of probability around point x (second derivative of xx).

  • One particula solution for the equation that you can verify by plugging into the equation above is once again gaussian with time dependent variance that describes probability distribution of brownian particles over space and time

P(x,t)=14πDtexp(x24Dt)P(x, t) = \frac{1}{\sqrt{4 \pi D t }} \exp\left(-\frac{x^2}{4Dt }\right)

Mesoscopic aspects of Diffusion

DiffusionMicroMacro.gif

Fick’s laws of diffusion, first proposed by Adolf Fick in 1855, were based largely on experimental observations. These laws describe diffusion in a manner analogous to Fourier’s heat equation (1822), which models heat transport. In 1827, Robert Brown observed the random motion of microscopic particles suspended in a fluid, later termed Brownian motion. Building on this, Albert Einstein developed a microscopic theory of diffusion in 1905, providing a theoretical foundation for Brownian motion. His work was instrumental in convincing scientists of the particulate nature of matter, a key step toward validating atomic theory.

  • Ranom walk provides a microscopic model of self-diffusion of molecules. Here we consider coarser diffusion on a coarser or mesocopic scale where a hevier solute particle moves erratically as a result of collisions with lighter solvent molecules. This setup is called Brownian motion

  • Einstein’s 1905 derivation of the diffusion equation provides a fundamental description of Brownian motion. This derivation bridges statistical mechanics and macroscopic diffusion laws.

Brownian Motion Animation

Visual explanation of computing probability of solute being at position and time (x,t+Δt)(x, t+\Delta t) given that it started some xΔxx-\Delta x position away where Δx\Delta x is variable and in fact gaussian distributed due to numerous collisions with solvent molecules which make such summed jump a gaussian random variable per CLT

  • Let P(x,t)P(x,t) be the probability density of finding the solute particle at position xx at time tt.

  • Probability of molecule at position xΔxx-\Delta x making a jump Δx\Delta x in a time interval Δt\Delta t is given by transition probability Φ(Δx)=P(x,t+ΔtxΔx,t)\Phi(\Delta x) = P(x, t+\Delta t | x - \Delta x, t).

  • Using the CK relation, the probability of finding the particle at xx at time t+Δtt+\Delta t is given by:

P(x,t+Δt)=Φ(Δx)P(xΔx,t)d(Δx)P(x,t+\Delta t) = \int_{-\infty}^{\infty} \Phi(\Delta x ) P(x-\Delta x, t) d(\Delta x)
  • For small displacements, we expand P(xΔx,t)P(x-\Delta x, t) in a Taylor series:

P(xΔx,t)P(x,t)ΔxPx+(Δx)222Px2P(x-\Delta x, t) \approx P(x,t) - \Delta x \frac{\partial P}{\partial x} + \frac{(\Delta x)^2}{2} \frac{\partial^2 P}{\partial x^2}
  • Plugging this expansion into integral equation to second order and assuming symmetry of transition probability we get:

Φ(Δx)Δxd(Δx)=0,\int_{-\infty}^{\infty} \Phi(\Delta x) \Delta x d(\Delta x) = 0,
  • Taking difference of probabilities and dividing by time-step we get

P(x,t+Δt)P(x,t)Δt=D2Px2\frac{P(x,t+\Delta t) - P(x,t )}{\Delta t}= D \frac{\partial^2 P}{\partial x^2}
  • Taking the limit Δt0\Delta t \to 0, we recover the diffusion equation

P(x,t)t=D2P(x,t)x2\frac{\partial P(x,t)}{\partial t} = D \frac{\partial^2 P(x,t)}{\partial x^2}
  • Where we now have the diffusion defined as second moment of transition probability in the limit of Δt0\Delta t\rightarrow 0

D=12ΔtΦ(Δx)(Δx)2d(Δx)D = \frac{1}{2\Delta t}\int_{-\infty}^{\infty} \Phi(\Delta x) (\Delta x)^2 d(\Delta x)
Source
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from mpl_toolkits.mplot3d import Axes3D

# Define the spatial grid
x = np.linspace(-5, 5, 200)

# Define initial Gaussian distribution parameters
sigma_initial = 0.3
D = 0.1  # Diffusion coefficient
t_max = 5  # Maximum time
num_frames = 10  # Number of plotted time steps

# Function to compute the Gaussian profile at a given time
def gaussian(x, sigma):
    return np.exp(-x**2 / (2 * sigma**2)) / (np.sqrt(2 * np.pi) * sigma)

# Compute time steps
times = np.linspace(0, t_max, num_frames)
sigma_values = np.sqrt(sigma_initial**2 + 2 * D * times)

# Generate colors using a sequential colormap
colors = cm.Blues(np.linspace(0.3, 1, num_frames))

# Plot 1D Gaussian evolution over time
plt.figure(figsize=(8, 6))
for i, sigma in enumerate(sigma_values):
    plt.plot(x, gaussian(x, sigma), color=colors[i], label=f'$t={times[i]:.1f}$')

# Add arrows indicating the broadening of the distribution
plt.arrow(0, 1.2, 0.5, 0, head_width=0.05, head_length=0.2, fc='black', ec='black')
plt.arrow(0, 1.2, -0.5, 0, head_width=0.05, head_length=0.2, fc='black', ec='black')

# Labels and formatting
plt.xlabel("Position $x$")
plt.ylabel("Density $\\rho(x,t)$")
plt.title("Time Evolution of Gaussian Due to Diffusion")
plt.legend()
plt.grid(True)
plt.show()


# Define the spatial grid for 3D visualization
x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x, y)

# Define initial Gaussian distribution parameters
sigma_initial = 0.3
D = 0.1  # Diffusion coefficient
t_max = 5  # Maximum time
num_frames = 3  # Number of time snapshots

# Function to compute the 2D Gaussian profile at a given time
def gaussian_2d(X, Y, sigma):
    return np.exp(-(X**2 + Y**2) / (2 * sigma**2)) / (2 * np.pi * sigma**2)

# Compute time steps
times = np.linspace(0, t_max, num_frames)
sigma_values = np.sqrt(sigma_initial**2 + 2 * D * times)

# Create 3D plots for different time steps
fig = plt.figure(figsize=(12, 4))

for i, sigma in enumerate(sigma_values):
    ax = fig.add_subplot(1, num_frames, i + 1, projection='3d')
    Z = gaussian_2d(X, Y, sigma)
    ax.plot_surface(X, Y, Z, cmap='Blues', edgecolor='none', alpha=0.8)
    
    ax.set_title(f'$t={times[i]:.1f}$')
    ax.set_xlabel("$x$")
    ax.set_ylabel("$y$")
    ax.set_zlabel("$\\rho(x,y,t)$")
    ax.set_zlim(0, 0.6)

plt.suptitle("3D Visualization of Gaussian Diffusion")
plt.show()
<Figure size 800x600 with 1 Axes>
<Figure size 1200x400 with 3 Axes>

Simulating Brownian Motion

figure-md - Unknown Directive
<img src="https://upload.wikimedia.org/wikipedia/commons/c/c2/Brownian_motion_large.gif" alt="Brownian Motion Animation" style="width:10%">  

Animation of Brownian Motion
  • Brownian motion describes the random movement of a particle suspended in a solvent composed of much smaller molecules. This motion arises from a large number of independent random collisions with solvent molecules.

  • We have worked out a mesoscopic theory of diffusion and obtained probability distribution as a function of time P(x,t)P(x, t) for solute moolecules in dilute solution.

  • Here we switch perspective to random variables, and make use of CLT to approximate the displacement of the particle over a small time step dtdt as a normally distributed random variable with variance σ2=2Dt\sigma^2 = 2Dt

x(t+dt)x(t)N(0,2Ddt)x(t+dt) - x(t) \sim \mathcal{N}(0, \sqrt{2D dt})
  • Using transformation N(μ,σ2)=μ+σN(0,1)N(\mu, \sigma^2) = \mu + \sigma N(0,1) to normal random variable we arrive at simple recipie for simulating brownian motion.

  • This formulation highlights the connection between Brownian motion and Gaussian distributions. Specifically, we rewrite the update step using the general form of a normally distributed random variable:

  • In the future we will meet Langevin equation which is also a mesoscopic model, but it sits at a finer level of description compared to the standard Brownian motion model. In langevin description we will explicitly model the forces and momentum relaxation of brownian particle

Source
# Re-run the full script to ensure all variables are defined correctly

import numpy as np
import matplotlib.pyplot as plt

# Parameters
num_steps = 1000  # Number of steps
num_walks = 10000  # Number of random walks for probability distribution
dt = 1  # Time step
D = 1  # Diffusion coefficient

# Corrected step size for Brownian motion
step_size = np.sqrt(2 * D * dt)

# Storage for probability distributions at selected time steps
time_steps = [10, 100, 500, 1000]  # Chosen time steps for histograms
positions = np.zeros((num_walks, num_steps))

# Simulate 1D Brownian motion
for i in range(num_walks):
    steps = np.random.normal(loc=0, scale=step_size, size=num_steps)
    positions[i, :] = np.cumsum(steps)  # Cumulative sum for position

# Compute Mean Squared Displacement (MSD)
msd = np.mean(positions**2, axis=0)
expected_msd = 2 * D * np.arange(num_steps) * dt  # Theoretical MSD

# Create a single plot for probability distributions at different times
fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(7,4))

# Plot probability distributions at different times
for t in time_steps:
    hist_data, bin_edges = np.histogram(positions[:, t-1], bins=50, density=True)
    bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
    ax1.plot(bin_centers, hist_data, label=f"Simulated (t={t})", alpha=0.7)

    # Theoretical Gaussian distribution
    x_vals = np.linspace(-4*np.sqrt(2*D*t), 4*np.sqrt(2*D*t), 100)
    theoretical_dist = (1 / np.sqrt(4 * np.pi * D * t)) * np.exp(-x_vals**2 / (4 * D * t))
    ax1.plot(x_vals, theoretical_dist, '--', label=f"Theoretical (t={t})")

ax1.set_xlabel("$x$")
ax1.set_ylabel("$P(x,t)$")
ax1.legend()
ax1.grid(alpha=0.3)

# Create an MSD plot on the right
ax2.plot(np.arange(num_steps), msd, label="Simulated MSD", color='b')
ax2.plot(np.arange(num_steps), expected_msd, 'r--', label="Theoretical MSD")
ax2.set_xlabel("$t$")
ax2.set_ylabel("$MSD(t)$")
ax2.legend()
ax2.grid(alpha=0.3)

fig.tight_layout()
<Figure size 700x400 with 2 Axes>

Macroscopic Aspects of Diffusion

diffflux

Consider a small volume element at point xx. The difference in incoming and outgoing fluxes dJ(x)dx\frac{dJ(x)}{dx} equates change of matter dρ(x,t)dx\frac{d\rho(x,t)}{dx} over time. For 2D and 3D cases when we conside flux from all sides flux differene is J\nabla{J}

  • The continutity expresses the principle of conservation of particle number in a system. Change of matter from small volume element dV is equal to difference of fluxes from all sides of volume element.

  • We assume a system where the particle flux is proportional to the negative gradient of the density (Fick’s law). This assumption holds true in systems where particle motion exhibits random walk behavior.

  • Substituting linear transport expression for J\mathbf{J} into the continuity equation results in diffusion equation.

Analytic Solutions of Diffusion Equation

MethodBest forBoundary ConditionsInitial ConditionsSolution Form
Fourier SeriesFinite domains ([0,L][0,L])Dirichlet/Neumann/PeriodicArbitrary but expandable in sinusoidal modesDiscrete sum over sinusoidal modes
Fourier TransformInfinite domains (,)(-\infty, \infty)No boundaries or natural decay at ±\pm\inftyArbitrary, especially delta functions or GaussiansContinuous integral representation

Fourier transform solution

  • Consider free diffussion with initial condition where all particles start at the origin ρ(x,t=0)=δ(x)\rho(x,t=0) = \delta(x) and evolve with no boundaries ρ(±)=0\rho(\pm \infty)=0

  • Unbounded diffusion can be handeled by Fourier transoform of probability (generating functions) which admits simple solution as a Gaussian function as we have seen earlier.

ρ(x,t)=14πDtex2/(4Dt)\rho(x,t) = \frac{1}{\sqrt{4\pi D t}} e^{-x^2 / (4Dt)}
Source
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from IPython.display import HTML

# Define the spatial grid
x = np.linspace(-5, 5, 200)

# Define initial Gaussian distribution parameters
sigma_initial = 0.3
D = 0.1  # Diffusion coefficient
t_max = 5  # Maximum time
num_frames = 50  # Number of animation frames

# Function to compute the Gaussian profile at a given time
def gaussian(x, sigma):
    return np.exp(-x**2 / (2 * sigma**2)) / (np.sqrt(2 * np.pi) * sigma)

# Set up the figure and axis
fig, ax = plt.subplots()
line, = ax.plot([], [], lw=2)

ax.set_xlim(-5, 5)
ax.set_ylim(0, 1.5)
ax.set_xlabel("Position (x)")
ax.set_ylabel("Probability Density")
ax.set_title("1D Gaussian Spreading Due to Diffusion")

# Initialize function for animation
def init():
    line.set_data([], [])
    return line,

# Update function for animation
def update(frame):
    t = frame * (t_max / num_frames)
    sigma_t = np.sqrt(sigma_initial**2 + 2 * D * t)
    y = gaussian(x, sigma_t)
    line.set_data(x, y)
    return line,

# Create animation
ani = animation.FuncAnimation(fig, update, frames=num_frames, init_func=init, blit=True)
plt.close()
# Display animation as HTML
HTML(ani.to_jshtml())
Loading...

Fourier Series solution

  • Another technique we use for solving PDEs like diffusion or Schrodinger equation is Separation of variables.

  • Solution is expressed in terms of Fourier series with coefficients AnA_n determined from inital conditions ρ(x,0)\rho(x,0).

  • Unlike fourier transoform techique any finite boundary condtition (e.g [0,L][0, L]) can be handled. Downside being is that we dont have compact analytic solution.

ρ(x,t)=n=1AneD(nπ/L)2tsin(nπxL).\rho(x,t) = \sum_{n=1}^{\infty} A_n e^{-D (n \pi / L)^2 t} \sin\left(\frac{n \pi x}{L}\right).
Source
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from IPython.display import HTML


# Define parameters
L = 1.0       # Domain length (0 to L)
D = 0.1       # Diffusion coefficient
N = 50        # Number of Fourier modes
T_max = 1.0   # Maximum time for animation
dt = 0.01     # Time step
x_points = 100  # Number of spatial points

# Define spatial domain
x = np.linspace(0, L, x_points)

# Define initial condition: Density in the left-hand corner (step function)
def initial_condition(x, L):
    return np.where(x < L/4, 1, 0)  # Step function for left-hand quarter

# Compute Fourier sine series coefficients for the given initial condition
def compute_fourier_coefficients(f, N, L):
    coefficients = []
    for n in range(1, N+1):
        k_n = n * np.pi / L
        A_n = (2 / L) * np.trapz(f * np.sin(k_n * x), x)
        coefficients.append((k_n, A_n))
    return coefficients

# Compute Fourier series solution at time t
def fourier_solution(x, t, coefficients, D, L):
    solution = np.zeros_like(x)
    for k_n, A_n in coefficients:
        solution += A_n * np.sin(k_n * x) * np.exp(-D * k_n**2 * t)
    return solution

# Compute initial condition and Fourier coefficients
rho_0 = initial_condition(x, L)
coefficients = compute_fourier_coefficients(rho_0, N, L)

# Set up the figure and axis for animation
fig, ax = plt.subplots()
ax.set_xlim(0, L)
ax.set_ylim(0, 1)
ax.set_xlabel("Position $x$")
ax.set_ylabel("Density $\\rho(x,t)$")
ax.set_title("Diffusion from Left-Hand Corner (Fourier Series)")

# Line object to update in animation
line, = ax.plot([], [], lw=2, color='b')

# Animation function
def update(frame):
    t = frame * dt
    rho_t = fourier_solution(x, t, coefficients, D, L)
    line.set_data(x, rho_t)
    ax.set_title(f"Fourier Series Solution at $t={t:.2f}$")
    return line,

# Create animation
ani = animation.FuncAnimation(fig, update, frames=int(T_max/dt), interval=50, blit=True)
plt.close()

HTML(ani.to_jshtml())
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[6], line 40
     38 # Compute initial condition and Fourier coefficients
     39 rho_0 = initial_condition(x, L)
---> 40 coefficients = compute_fourier_coefficients(rho_0, N, L)
     42 # Set up the figure and axis for animation
     43 fig, ax = plt.subplots()

Cell In[6], line 27, in compute_fourier_coefficients(f, N, L)
     25 for n in range(1, N+1):
     26     k_n = n * np.pi / L
---> 27     A_n = (2 / L) * np.trapz(f * np.sin(k_n * x), x)
     28     coefficients.append((k_n, A_n))
     29 return coefficients

File /opt/hostedtoolcache/Python/3.12.12/x64/lib/python3.12/site-packages/numpy/__init__.py:792, in __getattr__(attr)
    789     import numpy.char as char
    790     return char.chararray
--> 792 raise AttributeError(f"module {__name__!r} has no attribute {attr!r}")

AttributeError: module 'numpy' has no attribute 'trapz'

Numerical Solution to Diffusion Equations

Source
import numpy as np
import matplotlib.pyplot as plt

# Parameters
D = 1      # Diffusion coefficient
L = 100    # Length of the domain
dx = 1.0   # Spatial step
dt = 0.1   # Time step

# Stability condition: alpha = D * dt / dx^2 ≤ 0.5
assert D * dt / dx**2 <= 0.5, "Choose a smaller dt or larger dx for numerical stability."

num_steps = 500  # Total time steps for simulation
time_steps = [0, 100, 250, 500]  # Selected time steps for snapshots

# Initialize grid with an initial delta function at the center
x = np.arange(0, L, dx)
rho = np.zeros_like(x)
rho[L//2] = 1.0 / dx  # Initial peak

# Finite difference loop (explicit scheme)
snapshots = {}
for step in range(num_steps + 1):

    rho_new = rho.copy()

    # Notice that we are excluding boundaries, e.g indieces run from [1 to N-2] excluding 0 and N-1 boundaries of N sized rho
    rho_new[1:-1] = rho[1:-1] + D * dt / dx**2 * ( rho[:-2] - 2 * rho[1:-1] + rho[2:] )
    
    rho = rho_new

    if step in time_steps:
        snapshots[step] = rho.copy()

# Plot snapshots
fig, axes = plt.subplots(1, len(time_steps), figsize=(15, 5))

for ax, step in zip(axes, time_steps):
    ax.plot(x, snapshots[step], label=f't = {step * dt:.1f}')
    ax.set_ylim(0, 1.1 * np.max(rho))
    ax.set_title(f"t = {step * dt:.1f}")
    ax.set_xlabel("x")
    ax.set_ylabel("Probability Density $\\rho(x, t)$")

plt.suptitle("Time Evolution of 1D Diffusion", fontsize=14)
plt.legend()
plt.show()
Source
import numpy as np
import matplotlib.pyplot as plt

# Parameters
D = 1  # Diffusion coefficient
L = 20  # Grid size (L x L)
dx = 1.0  # Grid spacing
dt = 0.1  # Time step

# Stability condition for explicit diffusion scheme: alpha = D * dt / dx^2 ≤ 0.25 (for 2D case)
assert D * dt / dx**2 <= 0.25, "Choose a smaller dt or larger dx for numerical stability."

num_steps = 500  # Total time steps for simulation
time_steps = [0, 100, 250, 500]  # Selected time steps for snapshots

# Initialize grid with an initial delta function at the center
rho = np.zeros((L, L))
rho[L//2, L//2] = 1.0 / (dx**2)  # Initial peak

# Finite difference loop (explicit scheme) with snapshots
snapshots = {}
for step in range(num_steps + 1):
    rho_new = rho.copy()
    rho_new[1:-1, 1:-1] = rho[1:-1, 1:-1] + D * dt / dx**2 * (
        rho[:-2, 1:-1] + rho[2:, 1:-1] + rho[1:-1, :-2] + rho[1:-1, 2:] - 4 * rho[1:-1, 1:-1]
    )
    rho = rho_new

    if step in time_steps:
        snapshots[step] = rho.copy()

# Plot snapshots
fig, axes = plt.subplots(1, len(time_steps), figsize=(15, 5))
for ax, step in zip(axes, time_steps):
    im = ax.imshow(snapshots[step], extent=[-L//2, L//2, -L//2, L//2], cmap="hot", origin="lower")
    ax.set_title(f"t = {step * dt:.1f}")
    ax.set_xlabel("x")
    ax.set_ylabel("y")

# Add colorbar
fig.colorbar(im, ax=axes.ravel().tolist(), label="Probability Density $\\rho(x, y, t)$")
plt.suptitle("Time Evolution of 2D Diffusion", fontsize=14)
plt.show()

Micro, Meso, and Macro Scales

Interpretation of Time Scales

  • Microscopic: t=nδtt = n \delta t represents individual random jumps with binomially distributed steps.

  • Mesoscopic: tt aggregates many microscopic steps, leading to a normal distribution.

  • Macroscopic: tt is a continuous time variable, where diffusion follows a Gaussian probability density

Microscopic: Random Walk

  • At the microscopic scale, diffusion is modeled as a stochastic process where particles undergo random displacements at discrete time steps δt\delta t for an observation period t=nδtt=n\delta t

  • Displacements are sampled from single step binomial random variable generating e.g +1 or -1 with equal probabilities:

XiB(1or+1)X_i \sim \mathcal{B}(-1\,\,or\,+1)
X(t)=X1+X2+...XnX(t) = X_1+X_2 + ... X_n
  • For large nn, according to Central limit theorem the displacements is well approximated as Gaussian

    P(x,t)14πDtex24Dt,P(x, t) \approx \frac{1}{\sqrt{4 \pi D t}} e^{-\frac{x^2}{4 D t}},

where the diffusion coefficient is D=δx22δtD = \frac{\langle \delta x^2 \rangle}{2\delta t} .

Mesoscopic: Einstein’s Relation and Brownian Motion

  • At the mesoscopic level, diffusion results from numerous collisions occurring on a time scale of δt\delta t, leading to observable random displacements over a coarse-grained time tt.

  • The displacement of a Brownian particle follows a normal distribution:

    x(t)N(0,2Dt)x(t) \sim \mathcal{N}(0, 2Dt)
    P(x,t)=14πDtex24Dt.P(x, t) = \frac{1}{\sqrt{4 \pi D t}} e^{-\frac{x^2}{4 D t}}.

Macroscopic: Fick’s Law and Continuum Description

  • At the macroscopic level, diffusion is governed by Fick’s laws, treating concentration as a continuous field:

    Pt=D2P.\frac{\partial P}{\partial t} = D \nabla^2 P.
  • The probability distribution satisfies the diffusion equation solution:

    P(x,t)=14πDtex24Dt,P(x, t) = \frac{1}{\sqrt{4 \pi D t}} e^{-\frac{x^2}{4 D t}},

    which is the same Gaussian form as the mesoscopic level, indicating a smooth deterministic evolution at large scales.