{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "5298c815",
   "metadata": {},
   "source": [
    "# 13 — Quadratic-impact control\n",
    "\n",
    "Doc page: [quadratic_impact_control.rst](../../docs/source/algorithms/quadratic_impact_control.rst).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "2784f52d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T14:05:39.068751Z",
     "iopub.status.busy": "2026-05-12T14:05:39.068281Z",
     "iopub.status.idle": "2026-05-12T14:05:39.780916Z",
     "shell.execute_reply": "2026-05-12T14:05:39.779456Z"
    }
   },
   "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": "c41c1654",
   "metadata": {},
   "source": [
    "## Cellule 1 — Riccati générique sur un horizon fini\n",
    "\n",
    "**Théorème (HJB quadratique 1-D).** Pour $dq_t = u_t\\,dt + \\sigma\\,dW_t$\n",
    "et coût $L(q, u) = \\gamma u^2 + \\varphi q^2$, $g(q) = A q^2$, la\n",
    "fonction valeur est $V(t, q) = h(t) q^2$ avec\n",
    "$$h'(t) = h(t)^2 / \\gamma - \\varphi,\\qquad h(T) = A.$$\n",
    "Le feedback optimal est $u^*(t) = -(h(t)/\\gamma)\\,q(t)$.\n",
    "\n",
    "**Équation pivot.** Pour $\\gamma = \\varphi = A = 1$, le point fixe\n",
    "est $h^* = 1$ (car $h^2 - 1 = 0 \\Rightarrow h = 1$).\n",
    "\n",
    "**Ce que la cellule vérifie.** Le primitive\n",
    "`quadratic_impact_control_py(gamma=1, phi=1, A=1, T, n)` retourne\n",
    "$h(t) \\equiv 1$ et donc un gain de feedback unitaire.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "ea45dc8d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T14:05:39.784224Z",
     "iopub.status.busy": "2026-05-12T14:05:39.783876Z",
     "iopub.status.idle": "2026-05-12T14:05:40.235857Z",
     "shell.execute_reply": "2026-05-12T14:05:40.234066Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "max |h(t) - 1| = 0.000e+00\n",
      "Feedback gain : k(0) = 1.0000,  k(T) = 1.0000\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 34128 characters]",
      "text/plain": [
       "<Figure size 935x495 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "res = opt.quadratic_impact_control_py(gamma=1.0, phi=1.0,\n",
    "                                       a_terminal=1.0,\n",
    "                                       t_horizon=0.5, n_steps=500)\n",
    "ts = np.array(res['time_grid'])\n",
    "h = np.array(res['h'])\n",
    "k = np.array(res['feedback_gain'])\n",
    "\n",
    "err = float(np.max(np.abs(h - 1.0)))\n",
    "print(f\"max |h(t) - 1| = {err:.3e}\")\n",
    "print(f\"Feedback gain : k(0) = {k[0]:.4f},  k(T) = {k[-1]:.4f}\")\n",
    "\n",
    "fig, ax = plt.subplots()\n",
    "ax.plot(ts, h, lw=2, label=r'$h(t)$')\n",
    "ax.plot(ts, k, '--', lw=2, label=r'$k(t) = h(t)/\\gamma$')\n",
    "ax.axhline(1.0, ls=':', color='gray', alpha=0.6, label='point fixe = 1')\n",
    "ax.set_xlabel('t'); ax.set_ylabel('value')\n",
    "ax.set_title(\"Riccati quadratique scalaire\")\n",
    "ax.legend()\n",
    "fig.tight_layout(); plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "32009801",
   "metadata": {},
   "source": [
    "**Résultat attendu.** $h(t) \\equiv 1$ à précision machine.\n",
    "\n",
    "**Lecture du graphique.** Lignes plates à 1.\n",
    "\n",
    "**Conclusion.** Le primitive est validé sur le point fixe analytique.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ad7c6258",
   "metadata": {},
   "source": [
    "## Cellule 2 — Étude paramétrique : pénalisation $\\varphi$\n",
    "\n",
    "**Théorème.** Le point fixe $h^* = \\sqrt{\\gamma \\varphi}$ croît\n",
    "avec $\\varphi$.\n",
    "\n",
    "**Équation pivot.**\n",
    "$$h^* = \\sqrt{\\gamma \\varphi},\n",
    "   \\qquad k^* = h^*/\\gamma = \\sqrt{\\varphi/\\gamma}.$$\n",
    "\n",
    "**Ce que la cellule vérifie.** Sweep $\\varphi \\in \\{0.1, 1, 4, 10\\}$ :\n",
    "$h(0)$ s'aligne sur $\\sqrt{\\varphi}$ pour $\\gamma = 1$.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "06bd916e",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T14:05:40.239729Z",
     "iopub.status.busy": "2026-05-12T14:05:40.239404Z",
     "iopub.status.idle": "2026-05-12T14:05:40.631944Z",
     "shell.execute_reply": "2026-05-12T14:05:40.630049Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "phi= 0.10 : h(0) = 0.3162, h* = 0.3162\n",
      "phi= 1.00 : h(0) = 1.0000, h* = 1.0000\n",
      "phi= 4.00 : h(0) = 2.0000, h* = 2.0000\n",
      "phi=10.00 : h(0) = 3.1623, h* = 3.1623\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 32840 characters]",
      "text/plain": [
       "<Figure size 935x495 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "phis = [0.1, 1.0, 4.0, 10.0]\n",
    "fig, ax = plt.subplots()\n",
    "for phi in phis:\n",
    "    r = opt.quadratic_impact_control_py(1.0, phi, np.sqrt(phi),\n",
    "                                         t_horizon=2.0, n_steps=500)\n",
    "    h_arr = np.array(r['h'])\n",
    "    h_star = np.sqrt(phi)\n",
    "    ax.plot(r['time_grid'], h_arr,\n",
    "            label=fr'$\\varphi = {phi}$, $h^*={h_star:.2f}$')\n",
    "    print(f\"phi={phi:5.2f} : h(0) = {h_arr[0]:.4f}, h* = {h_star:.4f}\")\n",
    "ax.set_xlabel('t'); ax.set_ylabel('h(t)')\n",
    "ax.set_title(r'Sensibilité de $h$ au coefficient $\\varphi$')\n",
    "ax.legend()\n",
    "fig.tight_layout(); plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bc4e642c",
   "metadata": {},
   "source": [
    "**Résultat attendu.** Plus $\\varphi$ est grand, plus le gain de\n",
    "feedback est élevé.\n",
    "\n",
    "**Lecture du graphique.** Quatre lignes plates à des hauteurs\n",
    "$\\sqrt{\\varphi}$.\n",
    "\n",
    "**Conclusion.** Le compromis état/contrôle est gouverné par le\n",
    "ratio $\\varphi/\\gamma$.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e89f285e",
   "metadata": {},
   "source": [
    "## Cellule 3 — Exemple concret : régulation autour d'un set-point\n",
    "\n",
    "**Modèle.** En boucle fermée $\\dot q = -k q$ avec $k = h/\\gamma$,\n",
    "la dynamique est $q(t) = q_0 e^{-kt}$.  Cas pratique : régulateur\n",
    "thermique scalaire qui ramène la température vers $0$ (écart à la\n",
    "consigne).\n",
    "\n",
    "**Équation pivot.**\n",
    "$$q(t) = q_0\\,e^{-(h^*/\\gamma)\\,t}.$$\n",
    "\n",
    "**Ce que la cellule vérifie.** Pour différents $\\varphi$, on intègre\n",
    "$\\dot q = -k(t) q$ et on observe la vitesse de retour vers $0$.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "913f4685",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T14:05:40.637900Z",
     "iopub.status.busy": "2026-05-12T14:05:40.637528Z",
     "iopub.status.idle": "2026-05-12T14:05:41.242971Z",
     "shell.execute_reply": "2026-05-12T14:05:41.241582Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "phi=0.25 : q(T) = 0.2227\n",
      "phi=1.00 : q(T) = 0.0494\n",
      "phi=4.00 : q(T) = 0.0024\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 81296 characters]",
      "text/plain": [
       "<Figure size 1320x440 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "T, n_steps = 3.0, 600\n",
    "dt = T / n_steps\n",
    "q0 = 1.0\n",
    "fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n",
    "for phi in [0.25, 1.0, 4.0]:\n",
    "    r = opt.quadratic_impact_control_py(1.0, phi, np.sqrt(phi),\n",
    "                                         t_horizon=T, n_steps=n_steps)\n",
    "    k = np.array(r['feedback_gain'])\n",
    "    q = np.zeros(n_steps + 1); q[0] = q0\n",
    "    for i in range(n_steps):\n",
    "        q[i + 1] = q[i] - dt * k[i] * q[i]\n",
    "    ts = np.array(r['time_grid'])\n",
    "    axes[0].plot(ts, q, lw=2, label=fr'$\\varphi = {phi}$')\n",
    "    axes[1].plot(ts, k, lw=2, label=fr'$\\varphi = {phi}$')\n",
    "    print(f\"phi={phi:.2f} : q(T) = {q[-1]:.4f}\")\n",
    "axes[0].set_xlabel('t'); axes[0].set_ylabel('q(t)')\n",
    "axes[0].set_title(\"Écart à la consigne\")\n",
    "axes[0].legend()\n",
    "axes[1].set_xlabel('t'); axes[1].set_ylabel('k(t) (gain)')\n",
    "axes[1].set_title(\"Gain de feedback\")\n",
    "axes[1].legend()\n",
    "fig.tight_layout(); plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "db02a95b",
   "metadata": {},
   "source": [
    "**Résultat attendu.** Plus $\\varphi$ est grand, plus $q$ retourne\n",
    "rapidement à $0$.\n",
    "\n",
    "**Lecture du graphique.** Décroissance exponentielle visible ; gain\n",
    "plat (régime stationnaire de la Riccati).\n",
    "\n",
    "**Conclusion.** Le primitive fournit le gain optimal pour tout\n",
    "problème linéaire-quadratique scalaire à pénalité d'impact.\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
}
