{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "d78c4190",
   "metadata": {},
   "source": [
    "# 11 — PDE solvers (Fokker–Planck, Poisson)\n",
    "\n",
    "CPU-only finite-difference solvers. Doc page:\n",
    "[pde.rst](../../docs/source/algorithms/pde.rst).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "e2943612",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T14:05:20.836605Z",
     "iopub.status.busy": "2026-05-12T14:05:20.836332Z",
     "iopub.status.idle": "2026-05-12T14:05:21.554858Z",
     "shell.execute_reply": "2026-05-12T14:05:21.553141Z"
    }
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from optimizr import _core as opt\n",
    "plt.rcParams['figure.figsize'] = (8.5, 4.5)\n",
    "plt.rcParams['figure.dpi'] = 110\n",
    "plt.rcParams['axes.grid'] = True\n",
    "plt.rcParams['grid.alpha'] = 0.3\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "defeff96",
   "metadata": {},
   "source": [
    "## Cellule 1 — Diffusion d'une gaussienne (Fokker–Planck 1-D)\n",
    "\n",
    "**Théorème (Fokker–Planck / Kolmogorov forward).** Pour la diffusion\n",
    "$dX_t = \\mu\\,dt + \\sigma\\,dW_t$, la densité $p(t, x)$ vérifie\n",
    "$$\\partial_t p = -\\mu\\,\\partial_x p + \\tfrac{1}{2}\\sigma^2\\,\\partial_{xx} p.$$\n",
    "\n",
    "**Équation pivot (densité gaussienne).**\n",
    "$$p(t, x) = \\frac{1}{\\sqrt{2\\pi(\\sigma^2 t + s_0^2)}}\n",
    "   \\exp\\!\\left(-\\frac{(x-\\mu t)^2}{2(\\sigma^2 t + s_0^2)}\\right).$$\n",
    "\n",
    "**Ce que la cellule vérifie.** Le primitive\n",
    "`fokker_planck_constant(mu, sigma_sq, init_sigma, ...)` reproduit la\n",
    "gaussienne analytique pour $\\mu = 0.1$, $\\sigma^2 = 0.16$,\n",
    "$s_0 = 0.2$, $T = 1$.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "211170d1",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T14:05:21.558941Z",
     "iopub.status.busy": "2026-05-12T14:05:21.558558Z",
     "iopub.status.idle": "2026-05-12T14:05:22.231875Z",
     "shell.execute_reply": "2026-05-12T14:05:22.230545Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Erreur sup |p_num - p_ana| = 6.454e-04\n",
      "Masse finale (≈ 1)         = 1.0000\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 102008 characters]",
      "text/plain": [
       "<Figure size 1320x440 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "mu, sigma_sq, init_sigma = 0.1, 0.16, 0.2\n",
    "T, n_t = 1.0, 200\n",
    "n_x = 201\n",
    "res = opt.fokker_planck_constant(mu, sigma_sq, init_sigma,\n",
    "                                 -3.0, 3.0, n_x, T, n_t)\n",
    "xs = np.array(res['x_grid'])\n",
    "ts = np.array(res['time_grid'])\n",
    "density = np.array(res['density']).reshape(n_t + 1, n_x)\n",
    "p_final = density[-1]\n",
    "\n",
    "# ground truth gaussienne\n",
    "sig_eff = np.sqrt(sigma_sq * T + init_sigma ** 2)\n",
    "analytic = np.exp(-(xs - mu * T) ** 2 / (2 * sig_eff ** 2))\n",
    "analytic /= np.trapezoid(analytic, xs)\n",
    "\n",
    "err = float(np.max(np.abs(p_final - analytic)))\n",
    "mass = float(np.trapezoid(p_final, xs))\n",
    "print(f\"Erreur sup |p_num - p_ana| = {err:.3e}\")\n",
    "print(f\"Masse finale (≈ 1)         = {mass:.4f}\")\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n",
    "axes[0].plot(xs, density[0], ':', label='p(0, x)', alpha=0.6)\n",
    "axes[0].plot(xs, p_final, lw=2, label=f'p({T}, x) — schéma')\n",
    "axes[0].plot(xs, analytic, '--', lw=1.5, label='gaussienne analytique')\n",
    "axes[0].set_xlabel('x'); axes[0].set_ylabel('densité')\n",
    "axes[0].set_title(\"Coupes initiale / finale\")\n",
    "axes[0].legend()\n",
    "im = axes[1].imshow(density.T, aspect='auto', origin='lower',\n",
    "                    extent=[0, T, xs.min(), xs.max()],\n",
    "                    cmap='inferno')\n",
    "axes[1].set_xlabel('t'); axes[1].set_ylabel('x')\n",
    "axes[1].set_title(\"Évolution spatio-temporelle\")\n",
    "plt.colorbar(im, ax=axes[1], label='p(t, x)')\n",
    "fig.tight_layout(); plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ae66c1a1",
   "metadata": {},
   "source": [
    "**Résultat attendu.** Erreur sup faible et masse $\\approx 1$.\n",
    "\n",
    "**Lecture du graphique.** Gauche : pic initial fin (pointillé) qui\n",
    "s'étale en gaussienne décalée vers la droite (drift $\\mu T = 0.1$).\n",
    "Droite : carte chaleur de $p(t, x)$, qui s'élargit dans le temps.\n",
    "\n",
    "**Conclusion.** Le solveur Fokker–Planck est validé sur le cas\n",
    "gaussien.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d7b22c65",
   "metadata": {},
   "source": [
    "## Cellule 2 — Poisson 2D sur un carré (équation harmonique)\n",
    "\n",
    "**Théorème (Poisson Dirichlet).** Pour $f$ régulière et conditions\n",
    "nulles sur $\\partial\\Omega = [0,1]^2$, $-\\Delta u = f$ admet une\n",
    "unique solution dans $H^1_0$.\n",
    "\n",
    "**Équation pivot.** Si $f = 2\\pi^2 \\sin(\\pi x)\\sin(\\pi y)$ alors\n",
    "$$u(x, y) = \\sin(\\pi x)\\sin(\\pi y).$$\n",
    "\n",
    "**Démonstration.** Calcul direct : $-\\Delta u = 2\\pi^2 \\sin(\\pi x)\n",
    "\\sin(\\pi y) = f$.  $\\square$\n",
    "\n",
    "**Ce que la cellule vérifie.** Le solveur SOR `poisson_2d_zero_boundary`\n",
    "résout l'équation et l'erreur tend vers zéro.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "576f1eb5",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T14:05:22.235032Z",
     "iopub.status.busy": "2026-05-12T14:05:22.234767Z",
     "iopub.status.idle": "2026-05-12T14:05:22.746208Z",
     "shell.execute_reply": "2026-05-12T14:05:22.744955Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Itérations SOR : 290\n",
      "Résidu final   : 9.737e-07\n",
      "Erreur sup     : 4.896e-04\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 56504 characters]",
      "text/plain": [
       "<Figure size 1210x440 with 4 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "nx_grid = 41\n",
    "xs = ys = np.linspace(0, 1, nx_grid)\n",
    "X, Y = np.meshgrid(xs, ys, indexing='ij')\n",
    "f = 2 * np.pi ** 2 * np.sin(np.pi * X) * np.sin(np.pi * Y)\n",
    "res = opt.poisson_2d_zero_boundary(\n",
    "    f.flatten().tolist(), nx_grid, nx_grid,\n",
    "    0.0, 1.0, 0.0, 1.0,\n",
    ")\n",
    "u = np.array(res['u']).reshape(nx_grid, nx_grid)\n",
    "\n",
    "u_exact = np.sin(np.pi * X) * np.sin(np.pi * Y)\n",
    "err = float(np.max(np.abs(u - u_exact)))\n",
    "print(f\"Itérations SOR : {res['iterations']}\")\n",
    "print(f\"Résidu final   : {res['residual']:.3e}\")\n",
    "print(f\"Erreur sup     : {err:.3e}\")\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(11, 4))\n",
    "im0 = axes[0].imshow(u, origin='lower', extent=[0, 1, 0, 1], cmap='viridis')\n",
    "axes[0].set_title('u numérique'); plt.colorbar(im0, ax=axes[0])\n",
    "im1 = axes[1].imshow(u - u_exact, origin='lower',\n",
    "                      extent=[0, 1, 0, 1], cmap='RdBu_r')\n",
    "axes[1].set_title('erreur signée'); plt.colorbar(im1, ax=axes[1])\n",
    "fig.tight_layout(); plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce3f9fdf",
   "metadata": {},
   "source": [
    "**Résultat attendu.** Erreur sup faible (< $10^{-2}$) ; résidu\n",
    "SOR petit.\n",
    "\n",
    "**Lecture du graphique.** Cloche centrée à hauteur $1$ ;\n",
    "carte d'erreur quasi nulle, symétrique autour de zéro.\n",
    "\n",
    "**Conclusion.** Le solveur elliptique est validé.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7d53c3c5",
   "metadata": {},
   "source": [
    "## Cellule 3 — Exemple concret : diffusion thermique sur une plaque\n",
    "\n",
    "**Modèle physique.** Plaque carrée $[0,1]^2$ chauffée par une source\n",
    "gaussienne au centre, conditions de Dirichlet nulles sur le bord.\n",
    "Équilibre régi par\n",
    "$$-\\Delta u = q(x, y),\\qquad u\\big|_{\\partial\\Omega} = 0.$$\n",
    "\n",
    "**Ce que la cellule vérifie.** Profil radial décroissant de température\n",
    "sous source ponctuelle.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "adc915d4",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T14:05:22.749447Z",
     "iopub.status.busy": "2026-05-12T14:05:22.749181Z",
     "iopub.status.idle": "2026-05-12T14:05:23.199414Z",
     "shell.execute_reply": "2026-05-12T14:05:23.198332Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Température max au centre : 0.290\n",
      "Température max au bord   : 0.00e+00\n",
      "Itérations SOR            : 820\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 158952 characters]",
      "text/plain": [
       "<Figure size 660x550 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "nx_grid = 81\n",
    "xs = ys = np.linspace(0, 1, nx_grid)\n",
    "X, Y = np.meshgrid(xs, ys, indexing='ij')\n",
    "q = 50.0 * np.exp(-((X - 0.5) ** 2 + (Y - 0.5) ** 2) / (2 * 0.05 ** 2))\n",
    "res = opt.poisson_2d_zero_boundary(\n",
    "    q.flatten().tolist(), nx_grid, nx_grid,\n",
    "    0.0, 1.0, 0.0, 1.0,\n",
    ")\n",
    "u = np.array(res['u']).reshape(nx_grid, nx_grid)\n",
    "\n",
    "print(f\"Température max au centre : {u.max():.3f}\")\n",
    "print(f\"Température max au bord   : {u[0, :].max():.2e}\")\n",
    "print(f\"Itérations SOR            : {res['iterations']}\")\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(6, 5))\n",
    "im = ax.imshow(u, origin='lower', extent=[0, 1, 0, 1],\n",
    "               cmap='inferno', interpolation='bilinear')\n",
    "ax.contour(X, Y, u, levels=8, colors='white', linewidths=0.5, alpha=0.6)\n",
    "ax.set_title(\"Plaque chauffée par une source ponctuelle (équilibre)\")\n",
    "ax.set_xlabel('x'); ax.set_ylabel('y')\n",
    "plt.colorbar(im, ax=ax, label='température')\n",
    "fig.tight_layout(); plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7419240e",
   "metadata": {},
   "source": [
    "**Résultat attendu.** Température décroissante du centre vers le\n",
    "bord (Dirichlet), profil radial visible.\n",
    "\n",
    "**Lecture du graphique.** Iso-températures (lignes blanches) circulaires\n",
    "autour de la source ; température nulle sur les bords.\n",
    "\n",
    "**Conclusion.** Le même solveur Poisson 2D résout des problèmes\n",
    "mathématiques académiques *et* des problèmes physiques concrets\n",
    "(transfert thermique, électrostatique).\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "rhftlab",
   "language": "python",
   "name": "rhftlab"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
