- Add complete mean_field module with 6 submodules - Implement HJB and Fokker-Planck PDE solvers with rayon parallelization - Add forward-backward fixed-point iteration algorithm - Include Nash equilibrium and optimal transport utilities - Add comprehensive Jupyter notebook tutorial with: * Mathematical formulation (HJB and FP equations) * Finite difference methods explanation * Complete congestion game example * 3D visualizations and convergence plots * Citations to Jiang, Chewi, Pooladian (2023) paper - All tests passing (5 tests in mean_field module) - Based on 'Numerical Methods for Mean Field Games' PDF algorithms
12 KiB
12 KiB
In [ ]:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
import seaborn as sns
# Set plotting style
sns.set_style('whitegrid')
plt.rcParams['figure.figsize'] = (12, 8)
plt.rcParams['font.size'] = 11
print("✓ Libraries loaded")In [ ]:
# Problem parameters
nx = 100 # Spatial grid points
nt = 100 # Time steps
T = 1.0 # Time horizon
nu = 0.01 # Viscosity
lambda_congestion = 0.5 # Congestion penalty
x_target = 0.7 # Target location
# Spatial and temporal grids
x = np.linspace(0, 1, nx)
t = np.linspace(0, T, nt)
dx = x[1] - x[0]
dt = t[1] - t[0]
# Initial distribution: Gaussian centered at 0.3
m0 = np.exp(-((x - 0.3)**2) / (2 * 0.05**2))
m0 /= np.sum(m0) * dx # Normalize
# Plot initial distribution
plt.figure(figsize=(10, 4))
plt.plot(x, m0, 'b-', linewidth=2, label='Initial distribution $m_0(x)$')
plt.axvline(x_target, color='r', linestyle='--', label=f'Target: $x={x_target}$')
plt.xlabel('Space $x$')
plt.ylabel('Density')
plt.title('Initial Agent Distribution')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(f"Grid: {nx} × {nt}")
print(f"dx = {dx:.4f}, dt = {dt:.4f}")In [ ]:
def solve_hjb(m, u_T):
"""Solve HJB equation backward in time"""
u = np.zeros((nx, nt))
u[:, -1] = u_T # Terminal condition
for n in range(nt-2, -1, -1):
for i in range(1, nx-1):
# Laplacian (central difference)
u_xx = (u[i+1, n+1] - 2*u[i, n+1] + u[i-1, n+1]) / dx**2
# Hamiltonian with upwind scheme
u_x_plus = (u[i+1, n+1] - u[i, n+1]) / dx
u_x_minus = (u[i, n+1] - u[i-1, n+1]) / dx
H = 0.5 * min(u_x_plus**2, u_x_minus**2) # Upwind
# Running cost
f = lambda_congestion * m[i, n]
# Update (implicit Euler)
u[i, n] = u[i, n+1] - dt * (nu * u_xx - H + f)
# Boundary conditions (Neumann)
u[0, n] = u[1, n]
u[-1, n] = u[-2, n]
return u
def solve_fp(u, m0):
"""Solve Fokker-Planck equation forward in time"""
m = np.zeros((nx, nt))
m[:, 0] = m0 # Initial condition
for n in range(nt-1):
for i in range(1, nx-1):
# Laplacian
m_xx = (m[i+1, n] - 2*m[i, n] + m[i-1, n]) / dx**2
# Velocity field
u_x = (u[i+1, n] - u[i-1, n]) / (2*dx)
v = u_x # For quadratic Hamiltonian: H_p = p
# Upwind for advection
if v > 0:
flux_diff = v * (m[i, n] - m[i-1, n]) / dx
else:
flux_diff = v * (m[i+1, n] - m[i, n]) / dx
# Update (forward Euler)
m[i, n+1] = m[i, n] + dt * (nu * m_xx - flux_diff)
m[i, n+1] = max(m[i, n+1], 0) # Non-negativity
# Boundary conditions
m[0, n+1] = m[1, n+1]
m[-1, n+1] = m[-2, n+1]
# Normalize
m[:, n+1] /= (np.sum(m[:, n+1]) * dx)
return m
print("✓ Solver functions defined")In [ ]:
# Fixed-point iteration
max_iter = 50
tol = 1e-5
relax = 0.5
# Initialize
m_old = np.ones((nx, nt)) / nx
errors = []
print("Running fixed-point iteration...")
for iter in range(max_iter):
# Terminal condition for HJB
u_T = 0.5 * (x - x_target)**2
# Solve HJB backward
u = solve_hjb(m_old, u_T)
# Solve FP forward
m_new = solve_fp(u, m0)
# Check convergence
error = np.sqrt(np.sum((m_new - m_old)**2)) / np.sqrt(np.sum(m_old**2))
errors.append(error)
if iter % 5 == 0:
print(f" Iteration {iter:3d}: error = {error:.6f}")
if error < tol:
print(f"✓ Converged in {iter+1} iterations")
break
# Relaxation
m_old = relax * m_new + (1 - relax) * m_old
# Plot convergence
plt.figure(figsize=(10, 4))
plt.semilogy(errors, 'b-', linewidth=2)
plt.axhline(tol, color='r', linestyle='--', label=f'Tolerance: {tol}')
plt.xlabel('Iteration')
plt.ylabel('Relative L² error')
plt.title('Convergence of Fixed-Point Iteration')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()In [ ]:
# Create meshgrid for plotting
X, T = np.meshgrid(x, t)
# Plot distribution evolution
fig = plt.figure(figsize=(16, 6))
# 3D surface plot of distribution
ax1 = fig.add_subplot(121, projection='3d')
surf1 = ax1.plot_surface(X, T, m_new.T, cmap=cm.viridis, alpha=0.8)
ax1.set_xlabel('Space $x$')
ax1.set_ylabel('Time $t$')
ax1.set_zlabel('Density $m(x,t)$')
ax1.set_title('Distribution Evolution')
fig.colorbar(surf1, ax=ax1, shrink=0.5)
# 3D surface plot of value function
ax2 = fig.add_subplot(122, projection='3d')
surf2 = ax2.plot_surface(X, T, u.T, cmap=cm.plasma, alpha=0.8)
ax2.set_xlabel('Space $x$')
ax2.set_ylabel('Time $t$')
ax2.set_zlabel('Value $u(x,t)$')
ax2.set_title('Value Function')
fig.colorbar(surf2, ax=ax2, shrink=0.5)
plt.tight_layout()
plt.show()In [ ]:
# Temporal snapshots
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
time_indices = [0, nt//2, nt-1]
times = [0.0, T/2, T]
for ax, idx, time_val in zip(axes, time_indices, times):
ax.plot(x, m_new[:, idx], 'b-', linewidth=2, label='Distribution')
ax.axvline(x_target, color='r', linestyle='--', alpha=0.5, label='Target')
ax.set_xlabel('Space $x$')
ax.set_ylabel('Density')
ax.set_title(f'$m(x, t={time_val:.1f})$')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("✓ Solution computed and visualized")