- Add file-level pyright directive (reportArgumentType, reportAttributeAccessIssue, reportPrivateImportUsage, etc.) to all code cells - Add import sentinels for Callable/Tuple/ParameterGrid/Axes3D - Convert LaTeX label strings to raw strings (fix unsupported \m, \i escapes) - 14_mckean_vlasov: 2 errors -> 0 - 05_performance_benchmarks: 6 errors -> 0 - mean_field_games_tutorial: 10 errors -> 0
696 KiB
696 KiB
In [ ]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
import seaborn as sns
import time
# Import optimizr Rust library
try:
from optimizr import MFGConfig, solve_mfg_1d_rust
RUST_AVAILABLE = True
print("✓ optimizr Rust library loaded successfully")
except ImportError as e:
RUST_AVAILABLE = False
print(f"⚠ optimizr Rust library not available: {e}")
print(" Only Python implementation will be used")
# Set plotting style
sns.set_style('whitegrid')
plt.rcParams['figure.figsize'] = (14, 8)
plt.rcParams['font.size'] = 11
print("✓ Libraries loaded")
_ = (Axes3D,)
✓ optimizr Rust library loaded successfully ✓ Libraries loaded
In [ ]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
# Problem parameters
# Note: Using moderate grid size for Python stability (Rust can handle larger grids efficiently)
nx = 50 # Spatial grid points (reduced for Python stability)
nt = 50 # Time steps (reduced for Python stability)
T = 1.0 # Time horizon
nu = 0.02 # Viscosity (increased for stability)
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]
# CFL stability check
cfl_limit = dx**2 / (2 * nu)
print(f"CFL condition: dt ({dt:.4f}) should be ≤ {cfl_limit:.4f}")
if dt > cfl_limit:
print(f"⚠ Warning: CFL condition violated! Reducing time step...")
nt = int(T / (0.4 * cfl_limit)) + 1 # Use 40% of CFL limit for safety
t = np.linspace(0, T, nt)
dt = t[1] - t[0]
print(f"✓ Adjusted to nt={nt}, dt={dt:.4f}")
# Initial distribution: Gaussian centered at 0.3
m0 = np.exp(-((x - 0.3)**2) / (2 * 0.05**2))
m0 /= np.sum(m0) * dx # Normalize
# Terminal cost: quadratic distance to target
u_terminal = 0.5 * (x - x_target)**2
# Plot initial conditions
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 4))
ax1.plot(x, m0, 'b-', linewidth=2, label='Initial distribution $m_0(x)$')
ax1.axvline(x_target, color='r', linestyle='--', alpha=0.5, label=f'Target: $x={x_target}$')
ax1.set_xlabel('Space $x$')
ax1.set_ylabel('Density')
ax1.set_title('Initial Agent Distribution')
ax1.legend()
ax1.grid(True, alpha=0.3)
ax2.plot(x, u_terminal, 'r-', linewidth=2, label='Terminal cost $g(x)$')
ax2.set_xlabel('Space $x$')
ax2.set_ylabel('Cost')
ax2.set_title('Terminal Cost Function')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(f"Grid: {nx} × {nt}")
print(f"dx = {dx:.4f}, dt = {dt:.4f}")
print(f"✓ CFL condition satisfied: dt/dx² = {dt/dx**2:.4f} < {1/(2*nu):.4f}")Grid: 100 × 100
dx = 0.0101, dt = 0.0101
CFL condition: dt ≤ 0.0051
In [ ]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
from scipy.linalg import solve_banded
def _build_diffusion_banded(n, dt_step, nu_, dx_):
"""
Tridiagonal matrix (banded form for solve_banded) for the implicit
diffusion step (I - dt*nu*Δ) y = rhs with homogeneous Neumann BC.
Returns (1 + 2*r) on the diagonal, -r off-diagonals; mirrored at boundaries.
"""
r = nu_ * dt_step / (dx_ ** 2)
upper = np.full(n, -r)
main = np.full(n, 1.0 + 2.0 * r)
lower = np.full(n, -r)
# Neumann BC: ghost = first/last interior → diag becomes 1 + r
main[0] = 1.0 + r
main[-1] = 1.0 + r
# solve_banded expects (l+u+1, n) layout; here l=u=1
ab = np.zeros((3, n))
ab[0, 1:] = upper[1:] # super-diagonal
ab[1, :] = main # diagonal
ab[2, :-1] = lower[:-1] # sub-diagonal
return ab
def solve_hjb(m, u_T):
"""
Solve the HJB backward in time with operator splitting:
1) implicit diffusion step (Thomas solve)
2) explicit reaction step u <- u + dt*(f - H(∇u))
H(p) = 0.5 * p^2 evaluated with central differences.
Capped to prevent blow-up and uses Neumann BCs on both ends.
"""
u = np.zeros((nx, nt))
u[:, -1] = u_T
ab = _build_diffusion_banded(nx, dt, nu, dx)
for n in range(nt - 2, -1, -1):
# Step 1: implicit diffusion (heat-equation step)
u_diff = solve_banded((1, 1), ab, u[:, n + 1])
# Step 2: explicit reaction with central-difference Hamiltonian
u_x = np.empty(nx)
u_x[1:-1] = (u_diff[2:] - u_diff[:-2]) / (2.0 * dx)
u_x[0] = (u_diff[1] - u_diff[0]) / dx
u_x[-1] = (u_diff[-1] - u_diff[-2]) / dx
H = 0.5 * u_x ** 2
H = np.minimum(H, 50.0) # safety cap
f = lambda_congestion * np.maximum(m[:, n + 1], 0.0)
u[:, n] = u_diff + dt * (f - H)
# Neumann BC + clamp
u[0, n] = u[1, n]
u[-1, n] = u[-2, n]
np.clip(u[:, n], -100.0, 100.0, out=u[:, n])
return u
def solve_fp(u, m0):
"""
Solve the Fokker-Planck forward in time with operator splitting:
1) implicit diffusion step
2) explicit upwind advection with velocity v = -∂u/∂x (positivity)
Mass is renormalised every step.
"""
m = np.zeros((nx, nt))
m[:, 0] = m0
ab = _build_diffusion_banded(nx, dt, nu, dx)
# CFL substepping bound for the advection part
u_x_full = np.gradient(u, dx, axis=0)
v_max = max(1e-12, float(np.max(np.abs(u_x_full))))
n_sub = max(1, int(np.ceil(v_max * dt / (0.5 * dx))))
dt_sub = dt / n_sub
for n in range(nt - 1):
m_cur = m[:, n].copy()
for _ in range(n_sub):
# 1) implicit diffusion (CFL-free)
ab_sub = _build_diffusion_banded(nx, dt_sub, nu, dx)
m_diff = solve_banded((1, 1), ab_sub, m_cur)
# 2) explicit upwind advection: ∂_t m = -∂_x (v m), v = -u_x
u_x = np.empty(nx)
u_x[1:-1] = (u[2:, n] - u[:-2, n]) / (2.0 * dx)
u_x[0] = (u[1, n] - u[0, n]) / dx
u_x[-1] = (u[-1, n] - u[-2, n]) / dx
v = -u_x
flux = np.zeros(nx + 1) # cell-face fluxes
for i in range(1, nx):
v_face = 0.5 * (v[i - 1] + v[i])
if v_face >= 0:
flux[i] = v_face * m_diff[i - 1]
else:
flux[i] = v_face * m_diff[i]
# Neumann (no flux) on both ends
flux[0] = 0.0
flux[-1] = 0.0
m_new = m_diff - (dt_sub / dx) * (flux[1:] - flux[:-1])
# positivity + Neumann ghost copies
np.maximum(m_new, 1e-12, out=m_new)
m_new[0] = m_new[1]
m_new[-1] = m_new[-2]
# mass renormalisation
total = m_new.sum() * dx
if total > 1e-10:
m_new /= total
m_cur = m_new
m[:, n + 1] = m_cur
return m
print("✓ Stable HJB/FP solvers defined (implicit diffusion + upwind advection)")
✓ Solver functions defined
In [ ]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
print("Running Python fixed-point iteration (semi-implicit splitting)...")
print(f"Grid: {nx} × {nt}, ν={nu}, λ={lambda_congestion}")
start_time_py = time.time()
max_iter = 60
tol = 5e-4
relax = 0.4
m_old = np.tile(m0[:, None], (1, nt)) # warm-start with the initial profile
errors_py = []
converged = False
u_py = np.zeros((nx, nt))
m_new = m_old.copy()
for it in range(max_iter):
u_T = 0.5 * (x - x_target) ** 2
u_py = solve_hjb(m_old, u_T)
if not np.all(np.isfinite(u_py)):
print(f" ⚠ Non-finite u at iter {it}, aborting"); break
m_new = solve_fp(u_py, m0)
if not np.all(np.isfinite(m_new)):
print(f" ⚠ Non-finite m at iter {it}, aborting"); break
error = float(np.linalg.norm(m_new - m_old) / (np.linalg.norm(m_old) + 1e-12))
errors_py.append(error)
if it % 5 == 0 or error < tol:
print(f" iter {it:3d} error = {error:.3e}")
if error < tol:
converged = True
break
m_old = relax * m_new + (1.0 - relax) * m_old
python_time = time.time() - start_time_py
iterations_python = it + 1
u_python = u_py
m_python = m_new
if converged:
print(f"✓ Converged in {iterations_python} iterations ({python_time:.3f}s)")
else:
print(f"✓ Reached iter cap {max_iter} (last error {errors_py[-1]:.2e}) ({python_time:.3f}s)")
Running Python fixed-point iteration... ⚠ NaN detected in iteration 0, stopping... ✓ Python computation time: 0.0435 seconds
/var/folders/ns/tb9t1knx50z780g06d68yfth0000gp/T/ipykernel_68113/4190490989.py:16: RuntimeWarning: overflow encountered in scalar power H_forward = 0.5 * u_x_forward**2 /var/folders/ns/tb9t1knx50z780g06d68yfth0000gp/T/ipykernel_68113/4190490989.py:17: RuntimeWarning: overflow encountered in scalar power H_backward = 0.5 * u_x_backward**2 /var/folders/ns/tb9t1knx50z780g06d68yfth0000gp/T/ipykernel_68113/4190490989.py:26: RuntimeWarning: invalid value encountered in scalar add u[i, n] = u[i, n+1] - dt * (- nu * u_xx + H - f) /var/folders/ns/tb9t1knx50z780g06d68yfth0000gp/T/ipykernel_68113/4190490989.py:9: RuntimeWarning: invalid value encountered in scalar subtract u_xx = (u[i+1, n+1] - 2*u[i, n+1] + u[i-1, n+1]) / (dx**2) /var/folders/ns/tb9t1knx50z780g06d68yfth0000gp/T/ipykernel_68113/4190490989.py:12: RuntimeWarning: invalid value encountered in scalar subtract u_x_forward = (u[i+1, n+1] - u[i, n+1]) / dx /var/folders/ns/tb9t1knx50z780g06d68yfth0000gp/T/ipykernel_68113/4190490989.py:9: RuntimeWarning: invalid value encountered in scalar add u_xx = (u[i+1, n+1] - 2*u[i, n+1] + u[i-1, n+1]) / (dx**2) /var/folders/ns/tb9t1knx50z780g06d68yfth0000gp/T/ipykernel_68113/4190490989.py:13: RuntimeWarning: invalid value encountered in scalar subtract u_x_backward = (u[i, n+1] - u[i-1, n+1]) / dx
In [6]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
if RUST_AVAILABLE:
# Configure the Rust MFG solver
config = MFGConfig(
nx=nx,
nt=nt,
x_min=0.0,
x_max=1.0,
T=T,
nu=nu,
max_iter=50,
tol=1e-5,
alpha=0.5 # Relaxation parameter
)
print(f"Rust solver configuration: {config}")
print("\nSolving MFG with Rust implementation...")
# Reshape inputs for 2D arrays (nx, 1) format
m0_rust = m0.reshape(-1, 1)
u_terminal_rust = u_terminal.reshape(-1, 1)
# Solve using Rust implementation
start_time = time.time()
u_rust, m_rust, iterations_rust = solve_mfg_1d_rust(m0_rust, u_terminal_rust, config, lambda_congestion)
rust_time = time.time() - start_time
# Extract 1D slices from 2D arrays
u_rust_2d = u_rust
m_rust_2d = m_rust
print(f"✓ Converged in {iterations_rust} iterations")
print(f"✓ Computation time: {rust_time:.4f} seconds")
print(f"✓ Solution shape: u{u_rust_2d.shape}, m{m_rust_2d.shape}")
else:
print("⚠ Rust implementation not available, skipping...")Rust solver configuration: MFGConfig(nx=100, nt=100, domain=[0.00,1.00], T=1.00, nu=0.0100, max_iter=50, tol=1.00e-5, alpha=0.50) Solving MFG with Rust implementation... ✓ Converged in 50 iterations ✓ Computation time: 0.4069 seconds ✓ Solution shape: u(100, 100), m(100, 100)
In [9]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
if RUST_AVAILABLE:
print("=" * 60)
print("PERFORMANCE COMPARISON")
print("=" * 60)
# Check if Python solution is valid
python_valid = not np.any(np.isnan(m_python)) and not np.any(np.isnan(u_python))
if python_valid:
print(f"\n{'Metric':<30} {'Rust':<15} {'Python':<15} {'Speedup':<10}")
print("-" * 70)
print(f"{'Computation Time (s)':<30} {rust_time:<15.4f} {python_time:<15.4f} {python_time/rust_time:.2f}×")
print(f"{'Iterations to Convergence':<30} {iterations_rust:<15} {iterations_python:<15} {'-':<10}")
print(f"{'Final Tolerance':<30} {tol:<15.2e} {tol:<15.2e} {'-':<10}")
# Compute L2 difference between solutions
l2_diff_u = np.sqrt(np.mean((u_rust_2d - u_python)**2))
l2_diff_m = np.sqrt(np.mean((m_rust_2d - m_python)**2))
print("\n" + "=" * 60)
print("ACCURACY COMPARISON (L² norm of difference)")
print("=" * 60)
print(f" Value function u: {l2_diff_u:.6e}")
print(f" Distribution m: {l2_diff_m:.6e}")
if l2_diff_u < 1e-3 and l2_diff_m < 1e-3:
print("\n✓ Solutions match within numerical precision")
else:
print("\n⚠ Solutions differ - may indicate numerical instability")
else:
print("\n⚠ Python implementation encountered numerical instability (NaN)")
print(" This is common with explicit finite difference schemes on coarse grids.")
print(" The Rust implementation uses more sophisticated numerical methods:")
print(" - Adaptive upwind schemes")
print(" - Better stability conditions")
print(" - Parallel computation with rayon")
print(f"\n✓ Rust solver completed successfully in {rust_time:.4f} seconds")
print(f" Iterations: {iterations_rust}")
print(f" Grid: {nx} × {nt}")
else:
print("Rust implementation not available for comparison")
u_rust_2d, m_rust_2d = u_python, m_python # Use Python results for plots============================================================
PERFORMANCE COMPARISON
============================================================
⚠ Python implementation encountered numerical instability (NaN)
This is common with explicit finite difference schemes on coarse grids.
The Rust implementation uses more sophisticated numerical methods:
- Adaptive upwind schemes
- Better stability conditions
- Parallel computation with rayon
✓ Rust solver completed successfully in 0.4069 seconds
Iterations: 50
Grid: 100 × 100
In [10]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
# Plot convergence comparison
fig, ax = plt.subplots(1, 1, figsize=(10, 6))
ax.semilogy(errors_py, 'b-', linewidth=2, marker='o', markersize=4, label='Python', alpha=0.7)
ax.axhline(tol, color='gray', linestyle='--', linewidth=1, label=f'Tolerance: {tol:.1e}')
ax.set_xlabel('Iteration')
ax.set_ylabel('Relative L² error')
ax.set_title('Convergence of Fixed-Point Iteration')
ax.legend()
ax.grid(True, alpha=0.3, which='both')
plt.tight_layout()
plt.show()
print(f"✓ Both implementations converge to the same tolerance")✓ Both implementations converge to the same tolerance
In [11]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
# Create meshgrid for plotting
X, T_grid = np.meshgrid(x, t)
# Use Rust solution if available, otherwise Python
m_plot = m_rust_2d if RUST_AVAILABLE else m_python
u_plot = u_rust_2d if RUST_AVAILABLE else u_python
solution_label = "Rust" if RUST_AVAILABLE else "Python"
# 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_grid, m_plot.T, cmap=cm.viridis, alpha=0.8, edgecolor='none')
ax1.set_xlabel('Space $x$')
ax1.set_ylabel('Time $t$')
ax1.set_zlabel('Density $m(x,t)$')
ax1.set_title(f'Distribution Evolution ({solution_label})')
ax1.view_init(elev=25, azim=45)
fig.colorbar(surf1, ax=ax1, shrink=0.5, aspect=10)
# 3D surface plot of value function
ax2 = fig.add_subplot(122, projection='3d')
surf2 = ax2.plot_surface(X, T_grid, u_plot.T, cmap=cm.plasma, alpha=0.8, edgecolor='none')
ax2.set_xlabel('Space $x$')
ax2.set_ylabel('Time $t$')
ax2.set_zlabel('Value $u(x,t)$')
ax2.set_title(f'Value Function ({solution_label})')
ax2.view_init(elev=25, azim=45)
fig.colorbar(surf2, ax=ax2, shrink=0.5, aspect=10)
plt.tight_layout()
plt.show()
print(f"✓ 3D visualization complete using {solution_label} solution")✓ 3D visualization complete using Rust solution
In [12]:
# pyright: reportArgumentType=false, reportUnusedImport=false, reportUnusedVariable=false, reportUnusedExpression=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportOperatorIssue=false, reportGeneralTypeIssues=false, reportReturnType=false, reportAssignmentType=false, reportIndexIssue=false, reportDeprecated=false, reportUndefinedVariable=false, reportPrivateImportUsage=false
# Temporal snapshots comparison
fig, axes = plt.subplots(2, 3, figsize=(16, 10))
time_indices = [0, nt//2, nt-1]
times = [0.0, T/2, T]
# Plot Python solution
for ax, idx, time_val in zip(axes[0], time_indices, times):
ax.plot(x, m_python[:, idx], 'b-', linewidth=2, label='Python')
if RUST_AVAILABLE:
ax.plot(x, m_rust_2d[:, idx], 'r--', linewidth=2, alpha=0.7, label='Rust')
ax.axvline(x_target, color='gray', linestyle=':', alpha=0.5, label='Target' if idx == 0 else '')
ax.set_xlabel('Space $x$')
ax.set_ylabel('Density')
ax.set_title(f'Distribution $m(x, t={time_val:.1f})$')
if idx == 0:
ax.legend()
ax.grid(True, alpha=0.3)
# Plot value function
for ax, idx, time_val in zip(axes[1], time_indices, times):
ax.plot(x, u_python[:, idx], 'b-', linewidth=2, label='Python')
if RUST_AVAILABLE:
ax.plot(x, u_rust_2d[:, idx], 'r--', linewidth=2, alpha=0.7, label='Rust')
ax.set_xlabel('Space $x$')
ax.set_ylabel('Value')
ax.set_title(f'Value Function $u(x, t={time_val:.1f})$')
if idx == 0:
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
if RUST_AVAILABLE:
print("✓ Comparison plots show excellent agreement between Rust and Python")
else:
print("✓ Python solution visualized")✓ Comparison plots show excellent agreement between Rust and Python