{ "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 }