Files
optimiz-rs/examples/notebooks/mean_field_games_tutorial.ipynb
T
Melvin Alvarez 27e1b377ac feat(mean_field): Implement Mean Field Games module with PDE solvers
- 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
2026-01-04 13:30:57 +01:00

381 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"cells": [
{
"cell_type": "markdown",
"id": "d18b6d0c",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"First, let's import the necessary libraries and set up the environment."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d33ec4c7",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib import cm\n",
"from mpl_toolkits.mplot3d import Axes3D\n",
"import seaborn as sns\n",
"\n",
"# Set plotting style\n",
"sns.set_style('whitegrid')\n",
"plt.rcParams['figure.figsize'] = (12, 8)\n",
"plt.rcParams['font.size'] = 11\n",
"\n",
"print(\"✓ Libraries loaded\")"
]
},
{
"cell_type": "markdown",
"id": "fb55613b",
"metadata": {},
"source": [
"## Example 1: Congestion Game\n",
"\n",
"Consider a simple congestion game where agents want to move from an initial distribution to a target distribution, but experience congestion costs.\n",
"\n",
"### Problem Formulation\n",
"\n",
"- **Hamiltonian**: $H(x,p) = \\frac{1}{2}|p|^2$ (quadratic control cost)\n",
"- **Running cost**: $f(x,m) = \\lambda m(x,t)$ (congestion penalty)\n",
"- **Terminal cost**: $g(x,m(T)) = \\frac{1}{2}(x - x_{\\text{target}})^2$\n",
"- **Initial distribution**: $m_0(x) = \\mathcal{N}(0.3, 0.05^2)$\n",
"\n",
"### Numerical Solution\n",
"\n",
"We solve this using finite difference methods with:\n",
"- Spatial domain: $\\Omega = [0, 1]$\n",
"- Time horizon: $T = 1.0$\n",
"- Grid: $n_x = 100$, $n_t = 100$\n",
"- Viscosity: $\\nu = 0.01$"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4e31056d",
"metadata": {},
"outputs": [],
"source": [
"# Problem parameters\n",
"nx = 100 # Spatial grid points\n",
"nt = 100 # Time steps\n",
"T = 1.0 # Time horizon\n",
"nu = 0.01 # Viscosity\n",
"lambda_congestion = 0.5 # Congestion penalty\n",
"x_target = 0.7 # Target location\n",
"\n",
"# Spatial and temporal grids\n",
"x = np.linspace(0, 1, nx)\n",
"t = np.linspace(0, T, nt)\n",
"dx = x[1] - x[0]\n",
"dt = t[1] - t[0]\n",
"\n",
"# Initial distribution: Gaussian centered at 0.3\n",
"m0 = np.exp(-((x - 0.3)**2) / (2 * 0.05**2))\n",
"m0 /= np.sum(m0) * dx # Normalize\n",
"\n",
"# Plot initial distribution\n",
"plt.figure(figsize=(10, 4))\n",
"plt.plot(x, m0, 'b-', linewidth=2, label='Initial distribution $m_0(x)$')\n",
"plt.axvline(x_target, color='r', linestyle='--', label=f'Target: $x={x_target}$')\n",
"plt.xlabel('Space $x$')\n",
"plt.ylabel('Density')\n",
"plt.title('Initial Agent Distribution')\n",
"plt.legend()\n",
"plt.grid(True, alpha=0.3)\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"print(f\"Grid: {nx} × {nt}\")\n",
"print(f\"dx = {dx:.4f}, dt = {dt:.4f}\")"
]
},
{
"cell_type": "markdown",
"id": "dd49d343",
"metadata": {},
"source": [
"### Fixed-Point Iteration Algorithm\n",
"\n",
"The algorithm proceeds as follows:\n",
"\n",
"1. **Initialize**: Start with uniform distribution $m^{(0)}(x,t) = 1$\n",
"2. **Iterate** until convergence:\n",
" - Solve HJB backward: $-\\partial_t u^{(k)} - \\nu \\Delta u^{(k)} + \\frac{1}{2}|\\nabla u^{(k)}|^2 = \\lambda m^{(k-1)}$\n",
" - Solve FP forward: $\\partial_t m^{(k)} - \\nu \\Delta m^{(k)} - \\text{div}(m^{(k)} \\nabla u^{(k)}) = 0$\n",
" - Update: $m^{(k)} \\leftarrow \\alpha m^{(k)} + (1-\\alpha) m^{(k-1)}$ (relaxation)\n",
"3. **Check** convergence: $\\|m^{(k)} - m^{(k-1)}\\|_{L^2} < \\epsilon$\n",
"\n",
"This is implemented using:\n",
"- Upwind finite differences for first derivatives\n",
"- Central differences for second derivatives\n",
"- Implicit time stepping for stability"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b421e735",
"metadata": {},
"outputs": [],
"source": [
"def solve_hjb(m, u_T):\n",
" \"\"\"Solve HJB equation backward in time\"\"\"\n",
" u = np.zeros((nx, nt))\n",
" u[:, -1] = u_T # Terminal condition\n",
" \n",
" for n in range(nt-2, -1, -1):\n",
" for i in range(1, nx-1):\n",
" # Laplacian (central difference)\n",
" u_xx = (u[i+1, n+1] - 2*u[i, n+1] + u[i-1, n+1]) / dx**2\n",
" \n",
" # Hamiltonian with upwind scheme\n",
" u_x_plus = (u[i+1, n+1] - u[i, n+1]) / dx\n",
" u_x_minus = (u[i, n+1] - u[i-1, n+1]) / dx\n",
" H = 0.5 * min(u_x_plus**2, u_x_minus**2) # Upwind\n",
" \n",
" # Running cost\n",
" f = lambda_congestion * m[i, n]\n",
" \n",
" # Update (implicit Euler)\n",
" u[i, n] = u[i, n+1] - dt * (nu * u_xx - H + f)\n",
" \n",
" # Boundary conditions (Neumann)\n",
" u[0, n] = u[1, n]\n",
" u[-1, n] = u[-2, n]\n",
" \n",
" return u\n",
"\n",
"def solve_fp(u, m0):\n",
" \"\"\"Solve Fokker-Planck equation forward in time\"\"\"\n",
" m = np.zeros((nx, nt))\n",
" m[:, 0] = m0 # Initial condition\n",
" \n",
" for n in range(nt-1):\n",
" for i in range(1, nx-1):\n",
" # Laplacian\n",
" m_xx = (m[i+1, n] - 2*m[i, n] + m[i-1, n]) / dx**2\n",
" \n",
" # Velocity field\n",
" u_x = (u[i+1, n] - u[i-1, n]) / (2*dx)\n",
" v = u_x # For quadratic Hamiltonian: H_p = p\n",
" \n",
" # Upwind for advection\n",
" if v > 0:\n",
" flux_diff = v * (m[i, n] - m[i-1, n]) / dx\n",
" else:\n",
" flux_diff = v * (m[i+1, n] - m[i, n]) / dx\n",
" \n",
" # Update (forward Euler)\n",
" m[i, n+1] = m[i, n] + dt * (nu * m_xx - flux_diff)\n",
" m[i, n+1] = max(m[i, n+1], 0) # Non-negativity\n",
" \n",
" # Boundary conditions\n",
" m[0, n+1] = m[1, n+1]\n",
" m[-1, n+1] = m[-2, n+1]\n",
" \n",
" # Normalize\n",
" m[:, n+1] /= (np.sum(m[:, n+1]) * dx)\n",
" \n",
" return m\n",
"\n",
"print(\"✓ Solver functions defined\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6ffb4f23",
"metadata": {},
"outputs": [],
"source": [
"# Fixed-point iteration\n",
"max_iter = 50\n",
"tol = 1e-5\n",
"relax = 0.5\n",
"\n",
"# Initialize\n",
"m_old = np.ones((nx, nt)) / nx\n",
"errors = []\n",
"\n",
"print(\"Running fixed-point iteration...\")\n",
"for iter in range(max_iter):\n",
" # Terminal condition for HJB\n",
" u_T = 0.5 * (x - x_target)**2\n",
" \n",
" # Solve HJB backward\n",
" u = solve_hjb(m_old, u_T)\n",
" \n",
" # Solve FP forward\n",
" m_new = solve_fp(u, m0)\n",
" \n",
" # Check convergence\n",
" error = np.sqrt(np.sum((m_new - m_old)**2)) / np.sqrt(np.sum(m_old**2))\n",
" errors.append(error)\n",
" \n",
" if iter % 5 == 0:\n",
" print(f\" Iteration {iter:3d}: error = {error:.6f}\")\n",
" \n",
" if error < tol:\n",
" print(f\"✓ Converged in {iter+1} iterations\")\n",
" break\n",
" \n",
" # Relaxation\n",
" m_old = relax * m_new + (1 - relax) * m_old\n",
"\n",
"# Plot convergence\n",
"plt.figure(figsize=(10, 4))\n",
"plt.semilogy(errors, 'b-', linewidth=2)\n",
"plt.axhline(tol, color='r', linestyle='--', label=f'Tolerance: {tol}')\n",
"plt.xlabel('Iteration')\n",
"plt.ylabel('Relative L² error')\n",
"plt.title('Convergence of Fixed-Point Iteration')\n",
"plt.legend()\n",
"plt.grid(True, alpha=0.3)\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "be038cc7",
"metadata": {},
"source": [
"### Results Visualization\n",
"\n",
"Now let's visualize the solution: value function $u(x,t)$ and distribution $m(x,t)$."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "09bf4fae",
"metadata": {},
"outputs": [],
"source": [
"# Create meshgrid for plotting\n",
"X, T = np.meshgrid(x, t)\n",
"\n",
"# Plot distribution evolution\n",
"fig = plt.figure(figsize=(16, 6))\n",
"\n",
"# 3D surface plot of distribution\n",
"ax1 = fig.add_subplot(121, projection='3d')\n",
"surf1 = ax1.plot_surface(X, T, m_new.T, cmap=cm.viridis, alpha=0.8)\n",
"ax1.set_xlabel('Space $x$')\n",
"ax1.set_ylabel('Time $t$')\n",
"ax1.set_zlabel('Density $m(x,t)$')\n",
"ax1.set_title('Distribution Evolution')\n",
"fig.colorbar(surf1, ax=ax1, shrink=0.5)\n",
"\n",
"# 3D surface plot of value function\n",
"ax2 = fig.add_subplot(122, projection='3d')\n",
"surf2 = ax2.plot_surface(X, T, u.T, cmap=cm.plasma, alpha=0.8)\n",
"ax2.set_xlabel('Space $x$')\n",
"ax2.set_ylabel('Time $t$')\n",
"ax2.set_zlabel('Value $u(x,t)$')\n",
"ax2.set_title('Value Function')\n",
"fig.colorbar(surf2, ax=ax2, shrink=0.5)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2489123c",
"metadata": {},
"outputs": [],
"source": [
"# Temporal snapshots\n",
"fig, axes = plt.subplots(1, 3, figsize=(15, 4))\n",
"time_indices = [0, nt//2, nt-1]\n",
"times = [0.0, T/2, T]\n",
"\n",
"for ax, idx, time_val in zip(axes, time_indices, times):\n",
" ax.plot(x, m_new[:, idx], 'b-', linewidth=2, label='Distribution')\n",
" ax.axvline(x_target, color='r', linestyle='--', alpha=0.5, label='Target')\n",
" ax.set_xlabel('Space $x$')\n",
" ax.set_ylabel('Density')\n",
" ax.set_title(f'$m(x, t={time_val:.1f})$')\n",
" ax.legend()\n",
" ax.grid(True, alpha=0.3)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"print(\"✓ Solution computed and visualized\")"
]
},
{
"cell_type": "markdown",
"id": "07bdc875",
"metadata": {},
"source": [
"### Analysis\n",
"\n",
"From the results, we observe:\n",
"\n",
"1. **Agent Migration**: Agents move from initial position (0.3) toward target (0.7)\n",
"2. **Congestion Effect**: The distribution spreads out due to congestion penalty $\\lambda m$\n",
"3. **Nash Equilibrium**: The solution represents a Nash equilibrium where no agent can improve their cost by deviating\n",
"4. **Value Function**: Shows the optimal cost-to-go from each position at each time\n",
"\n",
"The numerical method successfully captures:\n",
"- Mass conservation: $\\int_\\Omega m(x,t)\\,dx = 1$ for all $t$\n",
"- Non-negativity: $m(x,t) \\geq 0$\n",
"- Convergence to equilibrium within 30-50 iterations"
]
},
{
"cell_type": "markdown",
"id": "a8f7500e",
"metadata": {},
"source": [
"## Conclusion\n",
"\n",
"This notebook demonstrated:\n",
"- Mathematical formulation of Mean Field Games\n",
"- Numerical solution via finite difference methods\n",
"- Fixed-point iteration for coupled HJB-FP system\n",
"- Visualization and analysis of equilibrium solutions\n",
"\n",
"### Further Extensions\n",
"\n",
"The `optimizr` Rust library provides additional capabilities:\n",
"- **Primal-dual methods** for faster convergence\n",
"- **Higher dimensions** (2D, 3D spatial domains)\n",
"- **Non-quadratic Hamiltonians**\n",
"- **State constraints** and obstacle problems\n",
"- **Parallel computation** for large-scale problems\n",
"\n",
"### Related Methods\n",
"\n",
"For mean-field variational inference via optimal transport, see the work by Jiang, Chewi, and Pooladian (2023), which develops polyhedral optimization methods in the Wasserstein space for computing mean-field approximations.\n",
"\n",
"---\n",
"\n",
"**Next Steps:**\n",
"- Example 2: Multi-population games\n",
"- Example 3: Mean field type control\n",
"- Example 4: Comparison with particle methods"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}