Diffusion#

What you will learn

  • Microscopic model of diffusion and its link to ranom walk

  • Mean square displacement as measure of diffusive motion

  • Mesoscopic model of diffuion and how to simulate Brownian motion

  • Macroscopic model of diffusion and Fick’s law

DiffusionMicroMacro.gif

Fig. 5 Diffusion at Micro, Meso and Macro 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 \) steps, all originating from \( r_0 = 0 \).

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

    \[ \langle r_i \rangle = 0, \]
  • The total displacement after \( n \) steps is given by:

    \[ R_n = \sum^{n}_{i=0} r_i \]
  • Taking the ensemble average:

    \[ \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.

Hide code cell 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())

Mean Square Displacement and Diffusion Coefficient#

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

\[ \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 \(\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 \(\langle r^2\rangle= 3\langle x^2\rangle\) or more generally for \(d\) dimensions

\[ \langle R^2_n\rangle = \sum^{n}_{i=1} d \cdot \langle l^2_i \rangle = d \cdot n \cdot \langle l^2 \rangle \]
\[ \langle R^2_n\rangle = d \cdot \frac{t}{\delta t} \cdot \langle l^2 \rangle \]

Grouping constants together, we define the diffusion coefficient \(D\), which characterizes the spreading of a particle:

\[ D = \frac{\langle \delta x^2 \rangle}{2\delta t} \]

Mean Square Displacement (MSD)

\[ \langle R^2 (t) \rangle = 2d D t \]
  • Rimension \(d=1, 2, 3 ...\)

  • Time \(t\)

  • Displacement from the origin \(R\)

  • 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) \approx \frac{1}{\sqrt{4 \pi D t }} \exp\left(-\frac{x^2}{4Dt }\right) \]
Hide code cell 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.00042189801824824794
../_images/b05971019b1137187089c787431908a9c8bab15e21ebe0d70b8baef814b03f1a.png

Chapman-Kolmogorov and Transition probabilities#

  • Key idea of time evolving probabilities: The total probability of going from \(y_1\) to \(y_3\) is obtained by considering every possible intermediate state \(y_2\) and adding up the probabilities of taking those two consecutive transitions

../_images/CK-visual.png

Fig. 6 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.#

  • 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.

Chapman-Kolmogorov and transition probabilities

\[ P(y_3, t_3 | y_1, t_1) = \int P(y_3, t_3 | y_2, t_2 ) P(y_2, t_2 | y_1, t_1 ) \, dy_2 \]
  • \(P(y_2, t_2 | y_1, t_1 )\): Probability of transitioning from an initial state to intermediate state.

  • \(P(y_2, t_2 | y_1, t_1 )\): Probability of transitioning from an intermediate state to final state.

  • \(P(y_3, t_3 | y_1, t_1 )\): Probability of transitioning from an initial state to final state.

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

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

\[ P(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 \(x\) from previous step can happen either from point to the left \(x-\delta x\) or from point to the right \(x+\delta x\).

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

\[ 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\pm \delta x, t) \) using a Taylor series in small \( \delta x \) we get:

\[ 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+\delta t) = P(x, t) + \frac{(\delta x)^2}{2} \frac{\partial^2 P}{\partial x^2}. \]
  • Rearranging to form time derivative:

\[ \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 \( \delta t \to 0 \) and defining the diffusion coefficient \( D = \frac{(\delta x)^2}{2 \delta t} \), we obtain the diffusion equation

Diffution Equation

\[ \frac{\partial P(x,t)}{\partial t} = D \frac{\partial^2 P(x,t)}{\partial x^2} \]
  • Probability at \(x\) changes faster (time derivative) if there is a larger imabalance of probability around point x (second derivative of \(x\)).

  • 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) = \frac{1}{\sqrt{4 \pi D t }} \exp\left(-\frac{x^2}{4Dt }\right) \]

Mesoscopic aspects of Diffusion#

DiffusionMicroMacro.gif

Fig. 7 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

Fig. 8 Visual explanation of computing probability of solute being at position and time \((x, t+\Delta t)\) given that it started some \(x-\Delta x\) position away where \(\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)\) be the probability density of finding the solute particle at position \(x\) at time \(t\).

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

  • Using the CK relation, the probability of finding the particle at \(x\) at time \(t+\Delta t\) is given by:

\[ 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-\Delta x, t)\) in a Taylor series:

\[ P(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:

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

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

\[ \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 \(\Delta t\rightarrow 0\)

\[ D = \frac{1}{2\Delta t}\int_{-\infty}^{\infty} \Phi(\Delta x) (\Delta x)^2 d(\Delta x)\]
Hide code cell 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()
../_images/88fdefd6a7a9434745a66c04b26c1e4cd90e5debc7f2abb11823ba429901918b.png ../_images/cbb6c639f23900c9bc770a250db6a4b4d6c5fa91b38274da344dbb6f3fb3ee07.png

Simulating Brownian Motion#

Brownian Motion Animation

Fig. 9 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)\) 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 \(dt\) as a normally distributed random variable with variance \(\sigma^2 = 2Dt\)

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

Simulating Brownian Motion with Normal Random Variables

\[ x(t+dt) = x(t) + \sqrt{2D dt} \cdot N(0,1) \]
  • \(dt\) simulation time step

  • \(D\) Diffusion coeficient

  • 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

Hide code cell 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()
../_images/438bef516b216c288280b027e31b8cfccc938e89d7260cf53274d4629d3b58f7.png

Macroscopic Aspects of Diffusion#

diffflux

Fig. 10 Consider a small volume element at point \(x\). The difference in incoming and outgoing fluxes \(\frac{dJ(x)}{dx}\) equates change of matter \(\frac{d\rho(x,t)}{dx}\) over time. For 2D and 3D cases when we conside flux from all sides flux differene is \(\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.

Continuity equation

\[ \frac{\partial \rho}{\partial t} + \nabla \cdot \mathbf{J} = 0 \]
  • \(\rho(\mathbf{r}, t)\) represents the local particle density (how many particles are in a given volume).

  • \(\mathbf{J}(\mathbf{r}, t)\) represents the particle flux (how many particles are flowing through a unit area per unit time).

  • 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.

Fick’s law (linear transport)

\[ \mathbf{J} = -D \nabla \rho \]
  • \(D\) is the diffusion coefficient, a measure of how easily particles diffuse.

  • \(\nabla \rho\) density gradient, a measure of density changes across space.

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

Diffusion Equation

\[ \frac{\partial \rho}{\partial t} = D \nabla^2 \rho \]

Analytic Solutions of Diffusion Equation#

Method

Best for

Boundary Conditions

Initial Conditions

Solution Form

Fourier Series

Finite domains (\([0,L]\))

Dirichlet/Neumann/Periodic

Arbitrary but expandable in sinusoidal modes

Discrete sum over sinusoidal modes

Fourier Transform

Infinite domains \((-\infty, \infty)\)

No boundaries or natural decay at \(\pm\infty\)

Arbitrary, especially delta functions or Gaussians

Continuous integral representation

Fourier transform solution#

  • Consider free diffussion with initial condition where all particles start at the origin \(\rho(x,t=0) = \delta(x)\) and evolve with no boundaries \(\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.

\[ \rho(x,t) = \frac{1}{\sqrt{4\pi D t}} e^{-x^2 / (4Dt)} \]
Hide code cell 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())

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 \(A_n\) determined from inital conditions \(\rho(x,0)\).

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

\[ \rho(x,t) = \sum_{n=1}^{\infty} A_n e^{-D (n \pi / L)^2 t} \sin\left(\frac{n \pi x}{L}\right). \]
Hide code cell 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())

Numerical Solution to Diffusion Equations#

Hide code cell 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()
../_images/552203c15aff4a169c99cd9916b4954679f2f2bdaea29727a79481f1daa0340e.png
Hide code cell 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()
../_images/b3ecbf0463a3db59f49f64141199cc70e64de8495d51c0d7b35f48595bdb2e0a.png

Micro, Meso, and Macro Scales#

Interpretation of Time Scales#

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

  • Mesoscopic: \(t\) aggregates many microscopic steps, leading to a normal distribution.

  • Macroscopic: \(t\) 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 \(\delta t\) for an observation period \(t=n\delta t\)

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

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

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

where the diffusion coefficient is \(D = \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 \(\delta t\), leading to observable random displacements over a coarse-grained time \(t\).

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

    \[ x(t) \sim \mathcal{N}(0, 2Dt) \]
    \[ 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:

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

    \[ 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.

References#

The mighty little books

More in depth