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#

Hide 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)
0
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#

m1v1,init+m2v2,init=m1v1,final+m2v2,final

Kinetic Energy Conservation#

12m1v1,init2+12m2v2,init2=12m1v1,final2+12m2v2,final2

where:

  • v1,init, v2,init are the initial speeds of particles 1 and 2.

  • v1,final, v2,final are the final speeds after the collision.

2. Collision Setup#

For two colliding particles, we define:

  • Positions: r1,r2

  • Velocities (before collision): v1,init,v2,init

  • Masses: m1,m2

The relative velocity between the two particles before the collision is:

vrel=v1,initv2,init

The unit vector along the collision axis (the direction along the line joining the particle centers) is:

r^=r1r2|r1r2|

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:

v1,final=v1,init2m2m1+m2(vrelr^)r^
v2,final=v2,init+2m1m1+m2(vrelr^)r^

where:

  • vrelr^ 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 2m2m1+m2 and 2m1m1+m2 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