Tutorial: Numpy simultions of 2D gas#
import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import interactive
import matplotlib.animation as animation
from IPython.display import HTML
class GasSimulation:
def __init__(self, N=100, L=10.0, dt=0.01, radius=0.2, temperature=1.0, mass=1.0, steps=500):
"""Initialize gas particle simulation."""
self.N, self.L, self.dt, self.radius, self.temperature, self.mass, self.steps = N, L, dt, radius, temperature, mass, steps
# Initialize positions (left side) and velocities (Maxwell-Boltzmann distribution)
self.positions = np.column_stack((np.random.uniform(0, L/2, N), np.random.uniform(0, L, N)))
self.velocities = np.random.normal(0, np.sqrt(temperature / mass), (N, 2))
# Pre-allocate arrays for performance
self.positions_history = np.zeros((steps, N, 2), dtype=np.float32)
self.velocities_history = np.zeros((steps, N, 2), dtype=np.float32)
def update_positions(self):
"""Update positions and handle wall collisions."""
self.positions += self.velocities * self.dt
collisions = (self.positions - self.radius < 0) | (self.positions + self.radius > self.L) # Grab particles that collided with the the walls
self.velocities[collisions] *= -1 # Reverse the velocity upon collision
self.positions = np.clip(self.positions, self.radius, self.L - self.radius) # Clip positions and make sure no particle is exceeding the limits
def run_simulation(self):
"""Run simulation and store trajectory history."""
for step in range(self.steps):
self.update_positions()
self.positions_history[step], self.velocities_history[step] = self.positions.copy(), self.velocities.copy()
# Create and run simulation
sim = GasSimulation(N=100, steps=500)
sim.run_simulation()
print(sim.positions_history.shape) # Needs some tools to summarize the information
(500, 100, 2)
Post-processing simulation#
Show code cell source
def visualize(sim):
def scatter_plot(frame=0):
plt.figure(figsize=(5, 5))
plt.xlim(0, sim.L), plt.ylim(0, sim.L)
plt.title(f"Frame {frame + 1} / {len(sim.positions_history)}")
plt.scatter(*sim.positions_history[frame].T, s=20, c='blue', alpha=0.6)
plt.show()
return interactive(scatter_plot, frame=(0, sim.steps - 1, 1))
def animate(sim):
L, pos, steps = sim.L, sim.positions_history, sim.steps
fig, ax = plt.subplots(figsize=(5, 5))
ax.set_xlim(0, L), ax.set_ylim(0, L)
scatter = ax.scatter([], [], s=20, c='blue', alpha=0.6)
def update(frame):
scatter.set_offsets(pos[frame])
return scatter,
ani = animation.FuncAnimation(fig, update, frames=steps, interval=20, blit=True)
plt.close()
return HTML(ani.to_jshtml())
# Create and run simulation
sim = GasSimulation(N=100, steps=500)
sim.run_simulation()
visualize(sim)
animate(sim)
Project 1: Plot velocity and particle distributions#
#plot_speed_distribution(sim.velocities_history)
#plot_position_heatmap(sim.positions_history)
Project 2: Implement elastic collisions between gas particles#
1. Conservation Laws#
When two particles collide elastically, both momentum and kinetic energy are conserved.
Momentum Conservation#
Kinetic Energy Conservation#
where:
\(v_{1,\text{init}}\), \(v_{2,\text{init}}\) are the initial speeds of particles 1 and 2.
\(v_{1,\text{final}}\), \(v_{2,\text{final}}\) are the final speeds after the collision.
2. Collision Setup#
For two colliding particles, we define:
Positions: \( \mathbf{r}_1, \mathbf{r}_2 \)
Velocities (before collision): \( \mathbf{v}_{1,\text{init}}, \mathbf{v}_{2,\text{init}} \)
Masses: \( m_1, m_2 \)
The relative velocity between the two particles before the collision is:
The unit vector along the collision axis (the direction along the line joining the particle centers) is:
3. Velocities After Collision#
After the collision, the velocity component along the collision axis changes, while the perpendicular components remain unchanged.
Using momentum and energy conservation, the updated velocities are:
where:
\( \mathbf{v}_{\text{rel}} \cdot \hat{\mathbf{r}} \) is the projection of relative velocity along the collision axis.
Projection is negative when particles moving towards each other and positive if moving away from each other.
The prefactors \( \frac{2 m_2}{m_1 + m_2} \) and \( \frac{2 m_1}{m_1 + m_2} \) ensure momentum and energy conservation.
Need to update positions by checking for collisions#
for i in range(self.N):
for j in range(i + 1, self.N):
r_rel = self.positions[i] - self.positions[j] # Relative position
v_rel = self.velocities[i] - self.velocities[j] # Relative velocity
dist = np.linalg.norm(r_rel)
if dist < 2 * self.radius: # Collision condition
# Fill out code
# Ensure that particles moving towards each other and not away from each other to avoid extra computation