🌐 EN | 🇯🇵 JP | Last sync: 2025-11-16

Chapter 1: Boltzmann Equation and H-Theorem

Learning Objectives

  • Understand the concept and physical meaning of distribution functions
  • Derive the Boltzmann equation and master the treatment of collision terms
  • Prove the H-theorem and understand its relationship to entropy increase
  • Learn the relaxation time approximation (BGK approximation) and acquire practical solution methods
  • Simulate relaxation processes of gas molecules using Python

1.1 Distribution Functions and Phase Space

Theory: One-Particle Distribution Function

The most fundamental quantity describing a gas molecular system is the one-particle distribution function \( f(\mathbf{r}, \mathbf{v}, t) \). This represents the expected number of particles at time \( t \) within the range from position \( \mathbf{r} \) to \( \mathbf{r} + d\mathbf{r} \) and velocity \( \mathbf{v} \) to \( \mathbf{v} + d\mathbf{v} \).

Normalization condition for the distribution function: \[ N = \int f(\mathbf{r}, \mathbf{v}, t) \, d\mathbf{r} \, d\mathbf{v} \] where \( N \) is the total number of particles.

Macroscopic physical quantities are expressed as velocity moments of the distribution function: \[ n(\mathbf{r}, t) = \int f(\mathbf{r}, \mathbf{v}, t) \, d\mathbf{v} \quad \text{(number density)} \] \[ \mathbf{u}(\mathbf{r}, t) = \frac{1}{n} \int \mathbf{v} f(\mathbf{r}, \mathbf{v}, t) \, d\mathbf{v} \quad \text{(flow velocity)} \]

Code Example 1: Visualization of Maxwell Distribution

import numpy as np import matplotlib.pyplot as plt from scipy.stats import maxwell class MaxwellDistribution: """Analysis and visualization of Maxwell velocity distribution""" def __init__(self, temperature, mass): """ Parameters: temperature: Temperature [K] mass: Particle mass [kg] """ self.T = temperature self.m = mass self.kB = 1.380649e-23 # Boltzmann constant # Characteristic velocities self.v_mean = np.sqrt(8 * self.kB * self.T / (np.pi * self.m)) self.v_rms = np.sqrt(3 * self.kB * self.T / self.m) self.v_prob = np.sqrt(2 * self.kB * self.T / self.m) def distribution(self, v): """Maxwell velocity distribution function""" a = np.sqrt(self.kB * self.T / self.m) return maxwell.pdf(v, scale=a) def plot_distribution(self): """Plot distribution function""" v = np.linspace(0, 4*self.v_rms, 1000) f_v = self.distribution(v) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5)) # Velocity distribution ax1.plot(v, f_v, 'b-', linewidth=2, label='Maxwell distribution') ax1.axvline(self.v_prob, color='r', linestyle='--', label=f'Most probable velocity = {self.v_prob:.1f} m/s') ax1.axvline(self.v_mean, color='g', linestyle='--', label=f'Mean velocity = {self.v_mean:.1f} m/s') ax1.axvline(self.v_rms, color='purple', linestyle='--', label=f'RMS velocity = {self.v_rms:.1f} m/s') ax1.set_xlabel('Velocity v [m/s]', fontsize=12) ax1.set_ylabel('Probability density f(v)', fontsize=12) ax1.set_title(f'Maxwell Velocity Distribution (T={self.T}K)', fontsize=14, fontweight='bold') ax1.legend() ax1.grid(True, alpha=0.3) # Temperature dependence temperatures = [100, 300, 500, 1000] for T in temperatures: md = MaxwellDistribution(T, self.m) v = np.linspace(0, 3000, 1000) ax2.plot(v, md.distribution(v), label=f'T={T}K') ax2.set_xlabel('Velocity v [m/s]', fontsize=12) ax2.set_ylabel('Probability density f(v)', fontsize=12) ax2.set_title('Temperature Dependence', fontsize=14, fontweight='bold') ax2.legend() ax2.grid(True, alpha=0.3) plt.tight_layout() return fig # Execution example: Nitrogen molecule (room temperature) m_N2 = 28.014 * 1.66054e-27 # kg maxwell_N2 = MaxwellDistribution(temperature=300, mass=m_N2) fig = maxwell_N2.plot_distribution() plt.show() print(f"Most probable velocity: {maxwell_N2.v_prob:.2f} m/s") print(f"Mean velocity: {maxwell_N2.v_mean:.2f} m/s") print(f"RMS velocity: {maxwell_N2.v_rms:.2f} m/s")

1.2 Derivation of the Boltzmann Equation

Theory: Boltzmann Equation

The time evolution of the distribution function is described by the Boltzmann equation, which considers the effects of external forces and collisions: \[ \frac{\partial f}{\partial t} + \mathbf{v} \cdot \nabla_{\mathbf{r}} f + \frac{\mathbf{F}}{m} \cdot \nabla_{\mathbf{v}} f = \left( \frac{\partial f}{\partial t} \right)_{\text{coll}} \]

Physical meaning of each term on the left-hand side:

  • \( \frac{\partial f}{\partial t} \): Time change of distribution
  • \( \mathbf{v} \cdot \nabla_{\mathbf{r}} f \): Flow in position space
  • \( \frac{\mathbf{F}}{m} \cdot \nabla_{\mathbf{v}} f \): Flow in velocity space due to external force
  • Right-hand side: Change in distribution due to collisions (collision term)

Detailed form of the collision term: \[ \left( \frac{\partial f}{\partial t} \right)_{\text{coll}} = \int \int [f(\mathbf{v}')f(\mathbf{v}_1') - f(\mathbf{v})f(\mathbf{v}_1)] \sigma(\Omega) |\mathbf{v} - \mathbf{v}_1| \, d\Omega \, d\mathbf{v}_1 \] where \( \sigma(\Omega) \) is the differential scattering cross section.

Code Example 2: Boltzmann Equation in Free Flow

import numpy as np import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation class FreeBoltzmannSolver: """Boltzmann equation solver for collision-free flow""" def __init__(self, Nx=100, Nv=100, Lx=10.0, v_max=3.0): """ Parameters: Nx: Number of position grid points Nv: Number of velocity grid points Lx: Spatial domain size v_max: Maximum value of velocity domain """ self.Nx = Nx self.Nv = Nv self.Lx = Lx self.v_max = v_max # Grid generation self.x = np.linspace(0, Lx, Nx) self.v = np.linspace(-v_max, v_max, Nv) self.dx = self.x[1] - self.x[0] self.dv = self.v[1] - self.v[0] # Distribution function (2D array: x, v) self.f = np.zeros((Nx, Nv)) def initialize_gaussian(self, x0, v0, sigma_x, sigma_v): """Gaussian initial distribution""" X, V = np.meshgrid(self.x, self.v, indexing='ij') self.f = np.exp(-((X - x0)**2 / (2*sigma_x**2) + (V - v0)**2 / (2*sigma_v**2))) self.f /= np.sum(self.f) * self.dx * self.dv # Normalization def step(self, dt): """Time evolution (one step) - Upwind method""" f_new = self.f.copy() for i in range(self.Nx): for j in range(self.Nv): v_ij = self.v[j] if v_ij > 0: # Right-moving advection if i > 0: f_new[i, j] = self.f[i, j] - v_ij * dt / self.dx * \ (self.f[i, j] - self.f[i-1, j]) elif v_ij < 0: # Left-moving advection if i < self.Nx - 1: f_new[i, j] = self.f[i, j] - v_ij * dt / self.dx * \ (self.f[i+1, j] - self.f[i, j]) self.f = f_new def get_density(self): """Number density n(x)""" return np.sum(self.f, axis=1) * self.dv def get_mean_velocity(self): """Mean velocity u(x)""" n = self.get_density() u = np.sum(self.f * self.v[np.newaxis, :], axis=1) * self.dv return u / (n + 1e-10) def plot_state(self, t): """Plot current state""" fig, axes = plt.subplots(2, 2, figsize=(14, 10)) # Distribution function f(x,v) ax = axes[0, 0] im = ax.contourf(self.x, self.v, self.f.T, levels=20, cmap='viridis') ax.set_xlabel('Position x', fontsize=12) ax.set_ylabel('Velocity v', fontsize=12) ax.set_title(f'Distribution function f(x,v) at t={t:.2f}', fontsize=14, fontweight='bold') plt.colorbar(im, ax=ax) # Number density n(x) ax = axes[0, 1] n = self.get_density() ax.plot(self.x, n, 'b-', linewidth=2) ax.set_xlabel('Position x', fontsize=12) ax.set_ylabel('Number density n(x)', fontsize=12) ax.set_title('Number Density Distribution', fontsize=14, fontweight='bold') ax.grid(True, alpha=0.3) # Mean velocity u(x) ax = axes[1, 0] u = self.get_mean_velocity() ax.plot(self.x, u, 'r-', linewidth=2) ax.set_xlabel('Position x', fontsize=12) ax.set_ylabel('Mean velocity u(x)', fontsize=12) ax.set_title('Velocity Field', fontsize=14, fontweight='bold') ax.grid(True, alpha=0.3) # Velocity distribution (at specific positions) ax = axes[1, 1] for x_pos in [self.Nx//4, self.Nx//2, 3*self.Nx//4]: ax.plot(self.v, self.f[x_pos, :], label=f'x={self.x[x_pos]:.2f}') ax.set_xlabel('Velocity v', fontsize=12) ax.set_ylabel('Distribution f(v)', fontsize=12) ax.set_title('Velocity Distribution (at each position)', fontsize=14, fontweight='bold') ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() return fig # Simulation execution solver = FreeBoltzmannSolver(Nx=100, Nv=100) solver.initialize_gaussian(x0=2.0, v0=1.0, sigma_x=0.5, sigma_v=0.3) dt = 0.01 for step in range(100): solver.step(dt) fig = solver.plot_state(t=1.0) plt.show()

1.3 H-Theorem and Entropy Increase

Theory: H-Theorem

Boltzmann defined the H-function as: \[ H(t) = \int f(\mathbf{r}, \mathbf{v}, t) \ln f(\mathbf{r}, \mathbf{v}, t) \, d\mathbf{r} \, d\mathbf{v} \] This function is related to thermodynamic entropy \( S \) by \( S = -k_B H \).

H-Theorem: For solutions of the Boltzmann equation, \[ \frac{dH}{dt} \leq 0 \] holds, with equality only for the Maxwell distribution.

Outline of proof: \[ \frac{dH}{dt} = \int (\ln f + 1) \left( \frac{\partial f}{\partial t} \right)_{\text{coll}} d\mathbf{r} d\mathbf{v} \] Using properties of the collision term, it can be shown that this integral is always non-positive. For the Maxwell distribution \( f_M \propto \exp(-\beta m v^2/2) \), \( \frac{dH}{dt} = 0 \), which corresponds to the equilibrium state.

Code Example 3: Time Evolution of H-Function

import numpy as np import matplotlib.pyplot as plt class HTheoremDemo: """Demonstration of H-theorem""" def __init__(self, N_particles=1000, N_bins=50): """ Parameters: N_particles: Number of particles N_bins: Number of velocity histogram bins """ self.N = N_particles self.N_bins = N_bins # Velocity range self.v_min, self.v_max = -5.0, 5.0 self.bins = np.linspace(self.v_min, self.v_max, N_bins+1) self.bin_centers = 0.5 * (self.bins[:-1] + self.bins[1:]) def initialize_two_maxwellians(self, T1=1.0, T2=4.0): """Mixture of two Maxwell distributions at different temperatures""" N1 = self.N // 2 N2 = self.N - N1 v1 = np.random.normal(0, np.sqrt(T1), N1) v2 = np.random.normal(0, np.sqrt(T2), N2) self.velocities = np.concatenate([v1, v2]) def compute_H(self, velocities): """Compute H-function""" # Approximate distribution function with histogram hist, _ = np.histogram(velocities, bins=self.bins, density=True) # H = ∫ f ln(f) dv ≈ Σ f_i ln(f_i) Δv H = 0.0 dv = self.bins[1] - self.bins[0] for f_i in hist: if f_i > 1e-10: # Avoid division by zero H += f_i * np.log(f_i) * dv return H def maxwell_boltzmann_collision(self, dt, cross_section=0.1): """Simplified collision process""" N_collisions = int(cross_section * self.N * dt) for _ in range(N_collisions): # Randomly select two particles i, j = np.random.choice(self.N, size=2, replace=False) v_i = self.velocities[i] v_j = self.velocities[j] # Center-of-mass and relative velocities v_cm = 0.5 * (v_i + v_j) v_rel = v_i - v_j # Scatter at random angle theta = np.random.uniform(0, 2*np.pi) v_rel_new = v_rel * np.array([np.cos(theta), np.sin(theta)])[0] # Post-collision velocities self.velocities[i] = v_cm + 0.5 * v_rel_new self.velocities[j] = v_cm - 0.5 * v_rel_new def simulate(self, T_max=10.0, dt=0.1): """Time evolution simulation""" times = np.arange(0, T_max, dt) H_values = [] for t in times: H = self.compute_H(self.velocities) H_values.append(H) self.maxwell_boltzmann_collision(dt) return times, H_values def plot_evolution(self, times, H_values): """Time evolution of H-function and distribution function changes""" fig, axes = plt.subplots(2, 2, figsize=(14, 10)) # Time evolution of H-function ax = axes[0, 0] ax.plot(times, H_values, 'b-', linewidth=2) ax.set_xlabel('Time t', fontsize=12) ax.set_ylabel('H-function', fontsize=12) ax.set_title('H-theorem: dH/dt ≤ 0', fontsize=14, fontweight='bold') ax.grid(True, alpha=0.3) ax.axhline(y=H_values[-1], color='r', linestyle='--', label='Equilibrium value') ax.legend() # Time derivative of H-function ax = axes[0, 1] dH_dt = np.gradient(H_values, times[1] - times[0]) ax.plot(times, dH_dt, 'r-', linewidth=2) ax.axhline(y=0, color='k', linestyle='--', alpha=0.5) ax.set_xlabel('Time t', fontsize=12) ax.set_ylabel('dH/dt', fontsize=12) ax.set_title('Entropy Production Rate', fontsize=14, fontweight='bold') ax.grid(True, alpha=0.3) # Initial distribution ax = axes[1, 0] demo_init = HTheoremDemo(self.N, self.N_bins) demo_init.initialize_two_maxwellians() ax.hist(demo_init.velocities, bins=self.bins, density=True, alpha=0.7, color='blue', label='Initial state') ax.set_xlabel('Velocity v', fontsize=12) ax.set_ylabel('Probability density f(v)', fontsize=12) ax.set_title('Initial Velocity Distribution (Two-temperature mixture)', fontsize=14, fontweight='bold') ax.legend() ax.grid(True, alpha=0.3) # Final distribution ax = axes[1, 1] ax.hist(self.velocities, bins=self.bins, density=True, alpha=0.7, color='green', label='Equilibrium state') # Theoretical Maxwell distribution T_eq = np.var(self.velocities) v_theory = np.linspace(self.v_min, self.v_max, 200) f_maxwell = (1/np.sqrt(2*np.pi*T_eq)) * np.exp(-v_theory**2/(2*T_eq)) ax.plot(v_theory, f_maxwell, 'r--', linewidth=2, label='Maxwell distribution') ax.set_xlabel('Velocity v', fontsize=12) ax.set_ylabel('Probability density f(v)', fontsize=12) ax.set_title('Equilibrium Velocity Distribution', fontsize=14, fontweight='bold') ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() return fig # Simulation execution demo = HTheoremDemo(N_particles=10000, N_bins=50) demo.initialize_two_maxwellians(T1=1.0, T2=4.0) times, H_values = demo.simulate(T_max=20.0, dt=0.1) fig = demo.plot_evolution(times, H_values) plt.show() print(f"Initial H value: {H_values[0]:.4f}") print(f"Final H value: {H_values[-1]:.4f}") print(f"ΔH = {H_values[-1] - H_values[0]:.4f} < 0 (entropy increase)")

1.4 Relaxation Time Approximation (BGK Approximation)

Theory: BGK Collision Operator

Computing the complete collision term is very complex. Bhatnagar-Gross-Krook (BGK) approximated the collision term using a relaxation time \( \tau \): \[ \left( \frac{\partial f}{\partial t} \right)_{\text{coll}} = -\frac{f - f^{(0)}}{\tau} \] where \( f^{(0)} \) is the local Maxwell distribution: \[ f^{(0)}(\mathbf{v}) = n \left( \frac{m}{2\pi k_B T} \right)^{3/2} \exp\left( -\frac{m(\mathbf{v} - \mathbf{u})^2}{2k_B T} \right) \]

With this approximation, the Boltzmann equation becomes: \[ \frac{\partial f}{\partial t} + \mathbf{v} \cdot \nabla_{\mathbf{r}} f = -\frac{f - f^{(0)}}{\tau} \] which greatly simplifies numerical computations.

Code Example 4: Numerical Solution of BGK Equation

import numpy as np import matplotlib.pyplot as plt class BGKSolver: """BGK equation solver""" def __init__(self, Nx=100, Nv=64, Lx=10.0, v_max=5.0, tau=1.0): """ Parameters: Nx: Number of spatial grid points Nv: Number of velocity grid points Lx: Spatial domain size v_max: Maximum value of velocity domain tau: Relaxation time """ self.Nx = Nx self.Nv = Nv self.Lx = Lx self.v_max = v_max self.tau = tau # Grid self.x = np.linspace(0, Lx, Nx) self.v = np.linspace(-v_max, v_max, Nv) self.dx = self.x[1] - self.x[0] self.dv = self.v[1] - self.v[0] # Distribution function self.f = np.zeros((Nx, Nv)) def local_maxwell(self, n, u, T): """Local Maxwell distribution""" f_eq = np.zeros((self.Nx, self.Nv)) for i in range(self.Nx): for j in range(self.Nv): v_rel = self.v[j] - u[i] f_eq[i, j] = n[i] / np.sqrt(2*np.pi*T[i]) * \ np.exp(-v_rel**2 / (2*T[i])) return f_eq def compute_moments(self): """Compute macroscopic quantities""" # Number density n = np.sum(self.f, axis=1) * self.dv # Mean velocity u = np.sum(self.f * self.v[np.newaxis, :], axis=1) * self.dv / (n + 1e-10) # Temperature v_rel_sq = np.zeros(self.Nx) for i in range(self.Nx): v_rel_sq[i] = np.sum(self.f[i, :] * (self.v - u[i])**2) * self.dv T = v_rel_sq / (n + 1e-10) return n, u, T def step_BGK(self, dt): """Time evolution of BGK equation""" # Compute macroscopic quantities n, u, T = self.compute_moments() # Local Maxwell distribution f_eq = self.local_maxwell(n, u, T) # BGK collision term collision_term = -(self.f - f_eq) / self.tau # Advection term (Upwind method) advection_term = np.zeros_like(self.f) for j in range(self.Nv): v_j = self.v[j] if v_j > 0: advection_term[1:, j] = -v_j * (self.f[1:, j] - self.f[:-1, j]) / self.dx else: advection_term[:-1, j] = -v_j * (self.f[1:, j] - self.f[:-1, j]) / self.dx # Time evolution self.f += dt * (advection_term + collision_term) def initialize_shock(self, x_shock=5.0): """Initial conditions for shock wave problem""" n_L, u_L, T_L = 2.0, 1.0, 1.0 # Left side n_R, u_R, T_R = 1.0, 0.0, 0.5 # Right side for i in range(self.Nx): if self.x[i] < x_shock: n, u, T = n_L, u_L, T_L else: n, u, T = n_R, u_R, T_R for j in range(self.Nv): v_rel = self.v[j] - u self.f[i, j] = n / np.sqrt(2*np.pi*T) * np.exp(-v_rel**2/(2*T)) def plot_solution(self, t): """Plot solution""" n, u, T = self.compute_moments() fig, axes = plt.subplots(2, 2, figsize=(14, 10)) # Distribution function ax = axes[0, 0] im = ax.contourf(self.x, self.v, self.f.T, levels=20, cmap='plasma') ax.set_xlabel('Position x', fontsize=12) ax.set_ylabel('Velocity v', fontsize=12) ax.set_title(f'Distribution function f(x,v) at t={t:.2f}', fontsize=14, fontweight='bold') plt.colorbar(im, ax=ax) # Number density ax = axes[0, 1] ax.plot(self.x, n, 'b-', linewidth=2) ax.set_xlabel('Position x', fontsize=12) ax.set_ylabel('Number density n(x)', fontsize=12) ax.set_title('Density Profile', fontsize=14, fontweight='bold') ax.grid(True, alpha=0.3) # Flow velocity ax = axes[1, 0] ax.plot(self.x, u, 'r-', linewidth=2) ax.set_xlabel('Position x', fontsize=12) ax.set_ylabel('Flow velocity u(x)', fontsize=12) ax.set_title('Velocity Profile', fontsize=14, fontweight='bold') ax.grid(True, alpha=0.3) # Temperature ax = axes[1, 1] ax.plot(self.x, T, 'g-', linewidth=2) ax.set_xlabel('Position x', fontsize=12) ax.set_ylabel('Temperature T(x)', fontsize=12) ax.set_title('Temperature Profile', fontsize=14, fontweight='bold') ax.grid(True, alpha=0.3) plt.tight_layout() return fig # Simulation execution solver = BGKSolver(Nx=100, Nv=64, tau=0.5) solver.initialize_shock(x_shock=5.0) dt = 0.01 for step in range(200): solver.step_BGK(dt) fig = solver.plot_solution(t=2.0) plt.show()

1.5 Practical Numerical Simulation

Code Example 5: Direct Simulation of Gas Molecules (DSMC Method)

import numpy as np import matplotlib.pyplot as plt class DSMCSimulator: """Direct Simulation Monte Carlo method""" def __init__(self, N_particles=10000, Lx=10.0, N_cells=50): """ Parameters: N_particles: Number of particles Lx: Spatial domain size N_cells: Number of cells """ self.N = N_particles self.Lx = Lx self.N_cells = N_cells self.dx = Lx / N_cells # Particle positions and velocities self.x = np.random.uniform(0, Lx, N_particles) self.v = np.zeros(N_particles) def initialize_two_stream(self): """Two-stream initial conditions""" N_half = self.N // 2 self.v[:N_half] = np.random.normal(2.0, 0.5, N_half) self.v[N_half:] = np.random.normal(-2.0, 0.5, self.N - N_half) def advect(self, dt): """Particle advection""" self.x += self.v * dt # Periodic boundary conditions self.x = self.x % self.Lx def collide(self, dt, sigma_collision=0.5): """Particle collision processing""" # Classify particles by cell cell_indices = (self.x / self.dx).astype(int) for cell in range(self.N_cells): # Get particles in this cell particles_in_cell = np.where(cell_indices == cell)[0] N_cell = len(particles_in_cell) if N_cell < 2: continue # Estimate number of collisions N_collisions = int(0.5 * N_cell * sigma_collision * dt / self.dx + 0.5) for _ in range(N_collisions): # Randomly select two particles if N_cell >= 2: i, j = np.random.choice(particles_in_cell, size=2, replace=False) # Relative velocity v_rel = self.v[i] - self.v[j] # Post-collision velocities (isotropic scattering) v_cm = 0.5 * (self.v[i] + self.v[j]) theta = np.random.uniform(0, 2*np.pi) v_rel_new = abs(v_rel) * np.cos(theta) self.v[i] = v_cm + 0.5 * v_rel_new self.v[j] = v_cm - 0.5 * v_rel_new def compute_distribution(self, N_bins=50): """Compute phase space distribution""" x_bins = np.linspace(0, self.Lx, N_bins+1) v_bins = np.linspace(self.v.min()-1, self.v.max()+1, N_bins+1) H, xedges, vedges = np.histogram2d(self.x, self.v, bins=[x_bins, v_bins]) return H.T, xedges, vedges def run_simulation(self, T_max=5.0, dt=0.01): """Run simulation""" times = np.arange(0, T_max, dt) entropy = [] for t in times: self.advect(dt) self.collide(dt) # Compute entropy H, _, _ = self.compute_distribution(N_bins=30) H_norm = H / (H.sum() + 1e-10) S = -np.sum(H_norm * np.log(H_norm + 1e-10)) entropy.append(S) return times, entropy def plot_phase_space(self, t): """Plot phase space""" fig, axes = plt.subplots(2, 2, figsize=(14, 10)) # Phase space scatter plot ax = axes[0, 0] ax.scatter(self.x, self.v, alpha=0.3, s=1) ax.set_xlabel('Position x', fontsize=12) ax.set_ylabel('Velocity v', fontsize=12) ax.set_title(f'Phase space t={t:.2f}', fontsize=14, fontweight='bold') ax.set_xlim(0, self.Lx) # Phase space density ax = axes[0, 1] H, xedges, vedges = self.compute_distribution(N_bins=50) im = ax.contourf(xedges[:-1], vedges[:-1], H, levels=20, cmap='viridis') ax.set_xlabel('Position x', fontsize=12) ax.set_ylabel('Velocity v', fontsize=12) ax.set_title('Distribution Function Density', fontsize=14, fontweight='bold') plt.colorbar(im, ax=ax) # Spatial density ax = axes[1, 0] x_hist, x_bins = np.histogram(self.x, bins=50, density=True) ax.bar(x_bins[:-1], x_hist, width=x_bins[1]-x_bins[0], alpha=0.7) ax.set_xlabel('Position x', fontsize=12) ax.set_ylabel('Density', fontsize=12) ax.set_title('Spatial Density Distribution', fontsize=14, fontweight='bold') ax.grid(True, alpha=0.3) # Velocity distribution ax = axes[1, 1] v_hist, v_bins = np.histogram(self.v, bins=50, density=True) ax.bar(v_bins[:-1], v_hist, width=v_bins[1]-v_bins[0], alpha=0.7) # Maxwell distribution (theoretical) T = np.var(self.v) v_theory = np.linspace(self.v.min(), self.v.max(), 200) f_maxwell = (1/np.sqrt(2*np.pi*T)) * np.exp(-v_theory**2/(2*T)) ax.plot(v_theory, f_maxwell, 'r--', linewidth=2, label='Maxwell distribution') ax.set_xlabel('Velocity v', fontsize=12) ax.set_ylabel('Probability density', fontsize=12) ax.set_title('Velocity Distribution', fontsize=14, fontweight='bold') ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() return fig # Simulation execution dsmc = DSMCSimulator(N_particles=20000, Lx=10.0, N_cells=50) dsmc.initialize_two_stream() times, entropy = dsmc.run_simulation(T_max=5.0, dt=0.01) # Plot time evolution fig_phase = dsmc.plot_phase_space(t=5.0) plt.show() # Entropy increase fig_entropy, ax = plt.subplots(figsize=(10, 6)) ax.plot(times, entropy, 'b-', linewidth=2) ax.set_xlabel('Time t', fontsize=12) ax.set_ylabel('Entropy S', fontsize=12) ax.set_title('Entropy Increase by DSMC Method', fontsize=14, fontweight='bold') ax.grid(True, alpha=0.3) plt.show() print(f"Initial entropy: {entropy[0]:.4f}") print(f"Final entropy: {entropy[-1]:.4f}") print(f"Entropy increase: {entropy[-1] - entropy[0]:.4f}")

Code Example 6: Temperature Relaxation Process

import numpy as np import matplotlib.pyplot as plt class TemperatureRelaxation: """Simulation of temperature relaxation process""" def __init__(self, N1=5000, N2=5000, T1=2.0, T2=0.5): """ Mixing of two gases at different temperatures Parameters: N1, N2: Number of particles in each gas T1, T2: Initial temperatures """ self.N1 = N1 self.N2 = N2 # Initial velocity distributions self.v1 = np.random.normal(0, np.sqrt(T1), N1) self.v2 = np.random.normal(0, np.sqrt(T2), N2) def compute_temperatures(self): """Compute temperatures of each gas""" T1 = np.var(self.v1) T2 = np.var(self.v2) return T1, T2 def collide_step(self, dt, collision_rate=0.1): """Temperature relaxation by collisions""" N_collisions = int(collision_rate * min(self.N1, self.N2) * dt) for _ in range(N_collisions): # Randomly select one particle from each gas i = np.random.randint(0, self.N1) j = np.random.randint(0, self.N2) # Collision (energy and momentum conservation) v_cm = 0.5 * (self.v1[i] + self.v2[j]) v_rel = self.v1[i] - self.v2[j] # Scattering angle theta = np.random.uniform(0, 2*np.pi) v_rel_new = abs(v_rel) * np.cos(theta) # Post-collision velocities self.v1[i] = v_cm + 0.5 * v_rel_new self.v2[j] = v_cm - 0.5 * v_rel_new def run_simulation(self, T_max=50.0, dt=0.1): """Run simulation""" times = np.arange(0, T_max, dt) T1_history = [] T2_history = [] for t in times: T1, T2 = self.compute_temperatures() T1_history.append(T1) T2_history.append(T2) self.collide_step(dt) return times, np.array(T1_history), np.array(T2_history) def plot_relaxation(self, times, T1_hist, T2_hist): """Plot relaxation process""" fig, axes = plt.subplots(2, 2, figsize=(14, 10)) # Temperature time evolution ax = axes[0, 0] ax.plot(times, T1_hist, 'r-', linewidth=2, label='Gas 1') ax.plot(times, T2_hist, 'b-', linewidth=2, label='Gas 2') # Equilibrium temperature (theoretical) T_eq = (self.N1 * T1_hist[0] + self.N2 * T2_hist[0]) / (self.N1 + self.N2) ax.axhline(y=T_eq, color='k', linestyle='--', label=f'Equilibrium temperature={T_eq:.3f}') ax.set_xlabel('Time t', fontsize=12) ax.set_ylabel('Temperature T', fontsize=12) ax.set_title('Temperature Relaxation Process', fontsize=14, fontweight='bold') ax.legend() ax.grid(True, alpha=0.3) # Temperature difference time evolution ax = axes[0, 1] dT = np.abs(T1_hist - T2_hist) ax.semilogy(times, dT, 'g-', linewidth=2) # Exponential decay fit from scipy.optimize import curve_fit def exp_decay(t, A, tau): return A * np.exp(-t/tau) try: popt, _ = curve_fit(exp_decay, times[10:], dT[10:], p0=[dT[10], 10.0]) ax.semilogy(times, exp_decay(times, *popt), 'r--', label=f'Fit: τ={popt[1]:.2f}') ax.legend() except: pass ax.set_xlabel('Time t', fontsize=12) ax.set_ylabel('Temperature difference |T1 - T2|', fontsize=12) ax.set_title('Temperature Difference Relaxation (log scale)', fontsize=14, fontweight='bold') ax.grid(True, alpha=0.3) # Initial velocity distribution ax = axes[1, 0] v1_init = np.random.normal(0, np.sqrt(T1_hist[0]), self.N1) v2_init = np.random.normal(0, np.sqrt(T2_hist[0]), self.N2) ax.hist(v1_init, bins=50, alpha=0.5, density=True, color='red', label='Gas 1 (high temp)') ax.hist(v2_init, bins=50, alpha=0.5, density=True, color='blue', label='Gas 2 (low temp)') ax.set_xlabel('Velocity v', fontsize=12) ax.set_ylabel('Probability density', fontsize=12) ax.set_title('Initial Velocity Distribution', fontsize=14, fontweight='bold') ax.legend() ax.grid(True, alpha=0.3) # Final velocity distribution ax = axes[1, 1] ax.hist(self.v1, bins=50, alpha=0.5, density=True, color='red', label='Gas 1') ax.hist(self.v2, bins=50, alpha=0.5, density=True, color='blue', label='Gas 2') # Equilibrium Maxwell distribution v_range = np.linspace(-5, 5, 200) f_eq = (1/np.sqrt(2*np.pi*T_eq)) * np.exp(-v_range**2/(2*T_eq)) ax.plot(v_range, f_eq, 'k--', linewidth=2, label='Maxwell distribution') ax.set_xlabel('Velocity v', fontsize=12) ax.set_ylabel('Probability density', fontsize=12) ax.set_title('Equilibrium Velocity Distribution', fontsize=14, fontweight='bold') ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() return fig # Simulation execution relaxation = TemperatureRelaxation(N1=10000, N2=10000, T1=3.0, T2=0.5) times, T1_hist, T2_hist = relaxation.run_simulation(T_max=50.0, dt=0.1) fig = relaxation.plot_relaxation(times, T1_hist, T2_hist) plt.show() print(f"Initial temperatures: T1={T1_hist[0]:.3f}, T2={T2_hist[0]:.3f}") print(f"Final temperatures: T1={T1_hist[-1]:.3f}, T2={T2_hist[-1]:.3f}") print(f"Theoretical equilibrium temperature: {(10000*T1_hist[0] + 10000*T2_hist[0])/20000:.3f}")

Code Example 7: Comprehensive Exercise - Rarefied Gas Flow

import numpy as np import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation class RarefiedGasFlow: """Comprehensive simulation of rarefied gas flow""" def __init__(self, Nx=100, Nv=64, Lx=20.0, v_max=5.0, tau=0.5): self.Nx = Nx self.Nv = Nv self.Lx = Lx self.v_max = v_max self.tau = tau self.x = np.linspace(0, Lx, Nx) self.v = np.linspace(-v_max, v_max, Nv) self.dx = self.x[1] - self.x[0] self.dv = self.v[1] - self.v[0] self.f = np.zeros((Nx, Nv)) # Obstacle (wall blocking flow) self.obstacle_start = Nx // 3 self.obstacle_end = Nx // 3 + Nx // 10 def initialize_flow(self, n0=1.0, u0=2.0, T0=1.0): """Initial conditions for flow""" for i in range(self.Nx): for j in range(self.Nv): v_rel = self.v[j] - u0 self.f[i, j] = n0 / np.sqrt(2*np.pi*T0) * \ np.exp(-v_rel**2/(2*T0)) def compute_moments(self): """Macroscopic quantities""" n = np.sum(self.f, axis=1) * self.dv u = np.sum(self.f * self.v[np.newaxis, :], axis=1) * self.dv / (n + 1e-10) v_rel_sq = np.zeros(self.Nx) for i in range(self.Nx): v_rel_sq[i] = np.sum(self.f[i, :] * (self.v - u[i])**2) * self.dv T = v_rel_sq / (n + 1e-10) return n, u, T def apply_boundary_conditions(self): """Boundary conditions (reflection at obstacle)""" # Specular reflection at obstacle for i in range(self.obstacle_start, self.obstacle_end): for j in range(self.Nv): if self.v[j] > 0 and i == self.obstacle_start: # Reflect right-moving velocity to left-moving j_reflect = self.Nv - 1 - j self.f[i, j_reflect] = self.f[i, j] self.f[i, j] = 0 def step(self, dt): """Time evolution (BGK + advection)""" n, u, T = self.compute_moments() # Local Maxwell distribution f_eq = np.zeros_like(self.f) for i in range(self.Nx): for j in range(self.Nv): v_rel = self.v[j] - u[i] f_eq[i, j] = n[i] / np.sqrt(2*np.pi*T[i]) * \ np.exp(-v_rel**2/(2*T[i])) # BGK collision collision = -(self.f - f_eq) / self.tau # Advection (Upwind) advection = np.zeros_like(self.f) for j in range(self.Nv): v_j = self.v[j] if v_j > 0: advection[1:, j] = -v_j * (self.f[1:, j] - self.f[:-1, j]) / self.dx advection[0, j] = -v_j * (self.f[0, j] - self.f[-1, j]) / self.dx # Periodic boundary else: advection[:-1, j] = -v_j * (self.f[1:, j] - self.f[:-1, j]) / self.dx advection[-1, j] = -v_j * (self.f[0, j] - self.f[-1, j]) / self.dx # Time evolution self.f += dt * (advection + collision) # Apply boundary conditions self.apply_boundary_conditions() def plot_solution(self, t): """Solution visualization""" n, u, T = self.compute_moments() fig = plt.figure(figsize=(16, 12)) gs = fig.add_gridspec(3, 2, hspace=0.3, wspace=0.3) # Distribution function ax1 = fig.add_subplot(gs[0, :]) im = ax1.contourf(self.x, self.v, self.f.T, levels=30, cmap='viridis') # Display obstacle ax1.axvspan(self.x[self.obstacle_start], self.x[self.obstacle_end], alpha=0.3, color='red', label='Obstacle') ax1.set_xlabel('Position x', fontsize=12) ax1.set_ylabel('Velocity v', fontsize=12) ax1.set_title(f'Phase space distribution t={t:.2f}', fontsize=14, fontweight='bold') ax1.legend(loc='upper right') plt.colorbar(im, ax=ax1, label='f(x,v)') # Number density ax2 = fig.add_subplot(gs[1, 0]) ax2.plot(self.x, n, 'b-', linewidth=2) ax2.axvspan(self.x[self.obstacle_start], self.x[self.obstacle_end], alpha=0.2, color='red') ax2.set_xlabel('Position x', fontsize=12) ax2.set_ylabel('Number density n', fontsize=12) ax2.set_title('Density Field', fontsize=14, fontweight='bold') ax2.grid(True, alpha=0.3) # Flow velocity ax3 = fig.add_subplot(gs[1, 1]) ax3.plot(self.x, u, 'r-', linewidth=2) ax3.axvspan(self.x[self.obstacle_start], self.x[self.obstacle_end], alpha=0.2, color='red') ax3.set_xlabel('Position x', fontsize=12) ax3.set_ylabel('Flow velocity u', fontsize=12) ax3.set_title('Velocity Field', fontsize=14, fontweight='bold') ax3.grid(True, alpha=0.3) # Temperature ax4 = fig.add_subplot(gs[2, 0]) ax4.plot(self.x, T, 'g-', linewidth=2) ax4.axvspan(self.x[self.obstacle_start], self.x[self.obstacle_end], alpha=0.2, color='red') ax4.set_xlabel('Position x', fontsize=12) ax4.set_ylabel('Temperature T', fontsize=12) ax4.set_title('Temperature Field', fontsize=14, fontweight='bold') ax4.grid(True, alpha=0.3) # Pressure ax5 = fig.add_subplot(gs[2, 1]) p = n * T # Ideal gas ax5.plot(self.x, p, 'purple', linewidth=2) ax5.axvspan(self.x[self.obstacle_start], self.x[self.obstacle_end], alpha=0.2, color='red') ax5.set_xlabel('Position x', fontsize=12) ax5.set_ylabel('Pressure p', fontsize=12) ax5.set_title('Pressure Field', fontsize=14, fontweight='bold') ax5.grid(True, alpha=0.3) return fig # Simulation execution flow = RarefiedGasFlow(Nx=150, Nv=80, Lx=20.0, v_max=6.0, tau=0.3) flow.initialize_flow(n0=1.0, u0=3.0, T0=1.0) dt = 0.005 for step in range(1000): flow.step(dt) if step % 100 == 0: print(f"Step {step}/1000") fig = flow.plot_solution(t=5.0) plt.show() print("\n=== Rarefied gas flow simulation complete ===")

Exercises

Exercise 1: Derivation of Maxwell Distribution

Derive that the velocity distribution in equilibrium becomes the Maxwell distribution from the variational principle of minimizing the H-function. Use particle number conservation and energy conservation as constraints.

Exercise 2: Linearization of BGK Equation

Consider a small deviation from the Maxwell distribution \( f = f_M(1 + \phi) \), and linearize the BGK equation with respect to \( \phi \). Derive the equation describing sound wave propagation and determine the speed of sound.

Exercise 3: Simulation of Heat Conduction

Solve the BGK equation for a system with a temperature gradient and confirm that Fourier's law of heat conduction \( \mathbf{q} = -\kappa \nabla T \) emerges. Find the relationship between thermal conductivity \( \kappa \) and relaxation time \( \tau \).

Summary

  • The distribution function \( f(\mathbf{r}, \mathbf{v}, t) \) is the fundamental descriptive quantity for non-equilibrium systems
  • The Boltzmann equation describes the time evolution of the distribution function including collision terms
  • The H-theorem provides the microscopic foundation for the law of entropy increase
  • The BGK approximation enables practical numerical calculations
  • The DSMC method provides a solution approach by direct particle simulation
  • These methods are applied to rarefied gas dynamics and material process simulations

References

  1. R. Kubo, "Statistical Mechanics" (Iwanami Shoten)
  2. S. Harris, "An Introduction to the Theory of the Boltzmann Equation" (Dover)
  3. C. Cercignani, "The Boltzmann Equation and Its Applications" (Springer)
  4. G. A. Bird, "Molecular Gas Dynamics and the Direct Simulation of Gas Flows" (Oxford)

Disclaimer