Files
optimiz-rs/examples/notebooks/13_quadratic_impact.ipynb
T

322 lines
154 KiB
Plaintext
Raw Normal View History

{
"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": "iVBORw0KGgoAAAANSUhEUgAAA5sAAAHjCAYAAACthQuVAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AABjaUlEQVR4nO3deXRTdf7/8VeadKWUrmAFpOxlR4GCKFJBVkUQlRF0hEFQxn1BGfy6gMrOT3HDZVxxAQcHcVQWBUFFqAICslh2ELDahZZC9yT390dtIKQtbbmhKT4f53AO/eTem/f95J20r9x7E4thGIYAAAAAADCRX3UXAAAAAAA4/xA2AQAAAACmI2wCAAAAAExH2AQAAAAAmI6wCQAAAAAwHWETAAAAAGA6wiYAAAAAwHSETQAAAACA6QibAAAAAADTETYBAAAAAKYjbALAX8ikSZNksVh04MCB6i6lQkaNGiWLxVLdZZimOvenpj321W316tWyWCx65513qryNuLg4JSYmmlYTANQ0hE0AqKFK/hg+9V9ISIjatWunp556Snl5edVdYoXMmTPnrP6gx0nvvPOO5syZU91lAAAgSbIYhmFUdxEAgMpbvXq1rrzySt1www0aPHiwJCktLU3/+c9/lJSUpH79+mnZsmVu69jtdtntdgUGBvrMEcO4uDjFxcVp9erVHrcVFRXJ4XAoKCjo3BfmBaNGjdK7774rb/3qTUxM1IEDB0o9eumLj70vK3l+vf322xo1alSVtlFQUCCLxaKAgABziwOAGsJW3QUAAM5Ohw4ddMstt7h+vvfee5WQkKDly5dr48aN6tSpk+s2m80mm63mvPT7+/vL39+/uss45/Ly8uTv72/qY1XTHvvzQWBgYIWWy87OVlhYmJerAYBzj9NoAeA8Y7VadeWVV0qSdu/e7XZbWdftnThxQpMmTVLbtm0VHBysiIgIdenSRS+99JLbckVFRXruuefUqVMn1apVS7Vr11b79u315JNPupZxOp2aOnWqEhMTFRsbq4CAANWvX18jR47Ur7/+6lruwIEDslgsOnjwoL755hu304FL6qvsNY5fffWVunXrpuDgYMXExGj06NFKT0+XxWJxOzpV3vV4pc1RcnKy7rrrLrVt21Z16tRRcHCw2rVrp9mzZ8vhcHhs448//tDIkSMVFRWlWrVq6bLLLtOqVatKrTkxMVFxcXE6ePCgbrrpJkVHRyskJESHDx+WJL3yyivq16+fGjRooICAANWtW1fXX3+9tm3b5rYdi8Wib775RgcPHnSby5IjxmU99nv27NF1112nOnXqqHbt2urbt6+2bNniquv0+yjtKF9Z81lYWKiZM2eqffv2Cg4OVlhYmK666ip9++23pc7F6QzD0IsvvqiLL75YderUUWhoqJo2baoRI0YoJSXFbdn9+/dr7NixatSokQIDA1WvXj317dtXX331lWuZyj6Op6tob5co7ZrNkrGff/5ZV199tSIiIlSnTh3X7X/88YfuuecexcXFKSAgQPXq1dMtt9zCtbYAaiTe4gSA89DevXslSVFRUWdc9tixY+rRo4e2bt2qQYMGafTo0fL399fWrVu1aNEi3X333ZKKg+aAAQO0cuVK9ezZU0888YTCwsL0yy+/aOHChZo8ebKk4oAxY8YMDR06VFdffbXq1Kmjn3/+WW+99ZZWrlypn3/+WZGRkYqJidF7772nBx54QNHR0fq///s/V00xMTGV3ucvvvhCgwcPVkxMjCZMmKDIyEgtWrRI/fv3r/S2Trd69WqtWrVK11xzjRo3bqz8/HwtWbJEDz/8sPbt26e5c+e6ls3OzlaPHj20Z88ejRw5UgkJCdq+fbuuueYaNW3atNTtnzhxQj169FCXLl00efJkHT9+XKGhoZKkmTNnqmvXrrrrrrsUHR2t3bt364033tBXX32lTZs2ubb53nvvacqUKUpPT9dzzz3n2narVq3K3K9Dhw6pe/fuysrK0rhx49SqVSslJSUpMTGxQr1THrvdroEDB+qbb77R8OHDNW7cOOXm5ur9999Xr169tHjxYl1zzTXlbmPq1Kl67LHHNHDgQI0ZM0YBAQH69ddftWzZMv3222+KjY2VJP3000/q3bu3cnNzNWrUKHXs2FHHjx9XUlKSVqxYoT59+kiq3ONYmor29pkcOnRIPXv21HXXXadp06bp999/d413795dJ06c0G233aYWLVroyJEjeuWVV/Tll19qw4YNuuiiiyoy/QDgGwwAQI20atUqQ5IxceJEIy0tzUhLSzN27NhhPP7444Yko1GjRkZBQYHbOk8++aQhydi/f79r7K677jIkGf/v//0/j/twOByu/8+aNcuQZNx7772G0+ksczmn02nk5OR4bOurr74yJBkzZ850G2/UqJHRs2fPUvdx5MiRRkV+VTkcDiMuLs4IDQ01fv31V9e43W43Bg4caEgyRo4c6Rovmbu3337bY1ulzdGJEydKvd8RI0YYVqvVSElJcY2VzP9zzz3ntuz8+fMNSR7707NnT0OSMWHChFLvo7T73rZtm+Hv72/ceeedHttq1KhRqdspbb/+/ve/G5KMTz75xG3ZadOmuXroVKfPY4nS5nPOnDmGJGPRokVuyxYWFhoXX3yx0bhx41LrPNXFF19stGrVqtxlnE6n0bZtW8Nmsxk//PCDx+2n9mZlHsfS9smM3m7UqJEhyXjllVc8tjNkyBAjIiLC2Lt3r9v4/v37jdDQUGPUqFGl1g8AvorTaAGghps2bZpiYmIUExOj1q1b6+mnn1bfvn21YsWKM34widPp1IcffqgmTZro/vvv97jdz+/kr4n3339ftWrV0tSpUz1ObT11uZJPxS3ZflZWltLT09WxY0fVqVNHP/zww1nsbek2btyoAwcO6NZbb1XDhg1d41arVY8++uhZb79WrVqu/xcUFOjo0aNKT09X//795XA4tGHDBtft//3vfxUeHq4777zTbRs33XSTmjdvXuZ9TJgwodz7NgxD2dnZSk9PV7169dSyZcuzmkun06nFixerdevWGjJkiNtt999/v+vIalW99957iouLU48ePZSenu76d+zYMV177bXav3+/du3aVe42wsPDdeTIEX3zzTdlLrNlyxZt27ZNN998sxISEjxuP7U3K/M4lsas3o6MjNTYsWPdxo4dO6b//e9/GjhwoMLCwtzmLDQ0VN26ddPy5csrtH0A8BWcRgsANdyoUaN08803y263a+fOnZoxY4YOHz6s4ODgM66bnp6uzMxMJSYmuv1RXppdu3YpPj7e7Q/2sixevFgzZ87Uxo0bVVhY6Hbb0aNHz7h+ZZWcNty6dWuP29q0aXPW28/NzdXTTz+tBQsWlHrt3Kn7tHfvXrVr167UoN+6dWuP62il4tOGIyIiSr3vb7/9Vk899ZTWrl3r8XU2jRs3ruSenJSamqrjx4+XOmdBQUFq2rSpsrKyqrz9X375Rbm5ueWeEv3HH3+oRYsWZd4+bdo0XXfddUpMTFS9evXUo0cP9e7dW8OHD3dd51gSWC+55JIz1lSZx7EsZvR206ZNZbVa3cZ27dolp9OpDz74QB988EGp653pOQoAvoawCQA1XNOmTXXVVVdJkvr376++ffvq4osv1k033aRvv/32nH/NxaeffqrrrrtOnTt31rPPPquLLrrIFXxvuukmOZ3Oc1pPacqbE7vd7jF2880369NPP9WYMWN0xRVXKDo6WjabTRs3btS//vWvs96nkqNlp9u4caN69+6tJk2aaMqUKWrSpIlCQkJksVh03333KScn56zu1yylzZnT6VTLli09PmTqVG3bti13u127dtWePXu0YsUKrVq1St98840+/vhjPfHEE/r2228VHx9fqTrP9nE0q7dLe7xL1h02bJjHUU8AqKkImwBwnmnVqpXuu+8+zZw5U/Pnz9eIESPKXDY6OloRERHasmWLnE5nuUdOWrRooV27diknJ6fco5vvvvuugoKC9M0337j9UZ2Tk6PMzEyP5c0IwyUfkrNjxw6P27Zv3+4xVvIhLqUdidq3b5/bz8eOHdOnn36qW265Ra+//rr
"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": "iVBORw0KGgoAAAANSUhEUgAAA5sAAAHjCAYAAACthQuVAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AABfo0lEQVR4nO3deXgUZbr+8buTkA5LQlYhgBKCCChgMg6bikAQmHDYQVzQEZE4yAEGWVyYH7KMgJwBdNQB0VFQQWBA4hwRFWWZEQY3TJDFqJGwR4FsPULWTv3+4KSHJntSSXXI93NdXNpv3qp++qlK6U1VddkMwzAEAAAAAICJvKwuAAAAAABw9SFsAgAAAABMR9gEAAAAAJiOsAkAAAAAMB1hEwAAAABgOsImAAAAAMB0hE0AAAAAgOkImwAAAAAA0xE2AQAAAACmI2wCAAAAAExH2AQAAAAAmI6wCQAAAAAwHWETAFCnnTt3TvPnz9e2bdusLgUAAFyGsAkAcLN7927ZbDatWbPGNbZmzRrZbDbt3r27SsuXNl7a3IrKy8vTiBEjtHXrVt12221VWsflxo0bJ5vNVu31lKZfv34KCwursfVfrc6fP6/f/va3atGihWw2m/r06VPmeHX2q+rukwCA//CxugAAqA/Onj2r5cuX6/3339fx48dlGIbCwsJ08803a/DgwXr44YetLtFjHDt2TGvWrNHw4cMVFRVV5txHHnlEFy5c0M6dO9W0adPaKbAaEhIS9Ktf/crqMuqcGTNmaOPGjfrDH/6gyMhINWvWrMzxuqQy+zsA1DWETQCoYSdOnFC3bt10/vx5jR49WnFxcfL19dXRo0e1Z88ePf/88x4VNu+44w5lZ2erQYMGNb58SXOPHTum+fPnKyIiosz/+U5JSVFkZKSWLl2qoKCgKtVam1JSUpSRkaGbb77Z6lLqnI8//lgDBw7U008/XaHx6uzD1d3/K6ui+zsA1EWETQCoYX/605/0888/6/nnn9fvf//7Yj//6aefLKiqdF5eXvLz86uV5avzXm3atCkWMjxZQkKCJBEoquCnn35ScHBwhcers19Vd/8HAPwH92wCQA374YcfJF26X68kzZs3d3udl5en//mf/1GXLl3UsGFDBQQE6M4779Q///lPt3lF91Hu2rVLzz//vG644QbZ7Xa1adNGy5cvL/Y+ubm5+uMf/6gbb7xRjRs3VkBAgNq3b6/x48crOzvbNa+se9YKCgr0zDPPqE2bNrLb7Wrfvr1efPFFtzmVueftyrnz5s1T3759JUkPPfSQbDab2714lelPWX7++Wc9+OCDCgkJUePGjXXbbbdp165dpc434z2//vprSVKHDh20ePFi3XjjjWrYsKHatWuntWvXlrv8v//9b82ZM0c9evRQWFiYfH19FRERocmTJys9Pb3Y/Hnz5slms+nYsWPFftanTx9FRERUqO78/Hw999xzuuWWW9S4cWP5+/urS5cumjt3rtu8zMxMTZ8+3bVvNGvWTPfee69r/79SRXpadA+tYRh64403XPtDREREieNF+1Fp+2BFPktpy9bE72VF9veyZGdna968eerXr58uXLig+++/X02bNlVkZKTefffdCq0DAGoSZzYBoIa1bdtWkrR69WotWbJEPj6lH3oLCgo0aNAg/eMf/9C9996riRMn6uLFi1q7dq1iYmL07rvvavDgwW7LzJ49Ww6HQw899JCaNGmiN998UzNmzFCLFi10zz33uOZNnjxZf/3rXzV27FhNnTpV0qVLO7du3aoLFy6oYcOG5X6WJ598UllZWYqLi5Pdbtf69es1depU/fzzz3rmmWeq0h43I0eOVH5+vhYtWqRHHnlEvXr1kiTXvXhV6c+VHA6HevXqpeTkZD344IPq1q2bDh8+rMGDB7u21eXMeE/pUti02WyaPHmyAgMD9cgjj6igoEDLli3TuHHj9Otf/1odOnQodfnTp0/rlVde0ciRI3X33XfLz89PX3zxhVatWqU9e/boyy+/NP3Sz/z8fMXGxmrHjh3q3bu3nn76aQUEBOjbb7/Vpk2bNH/+fEmXgvBtt92mI0eO6N5779Xtt9+uH3/8UStWrNCHH36ovXv36sYbb3Stt6I9/d3vfqc777xTDzzwgHr16qVHHnlE0qWz2ikpKcXGb7311mp/lpLU1O9left7WX766SfFxMQoOTlZ//M//6O5c+dq48aN+uMf/6hly5bprrvu0jfffKOOHTuWuy4AqDEGAKBG/fjjj0bTpk0NScY111xjjBo1yliyZImxZ88ew+l0us19/vnnDUnGli1b3Mbz8vKM6Ohoo02bNq6x1atXG5KMLl26GDk5Oa7xX375xQgJCTF69uzpto6goCDjN7/5Tbn17tq1y5BkrF69uth7tWrVysjIyHCN5+TkGN26dTO8vLyM5OTkUpcvbbyiY0Uq05/SzJkzx5BkPPfcc27j69evNyQZV/6n0Yz3NAzDaN68uSHJ+NOf/uQ2/vbbbxuSjDfffLPM5XNzc428vLxi46+++qohyfjb3/7mNj537lxDkpGSklJsmd69exutW7cut+Y//elPhiRj6tSpRmFhodvPLt93i3q6cOFCtzm7d+82JBn9+vVzG69sTyUZDz74YLH6ShsvaR+q6Gcpadma/L0sa38vy8CBAw1JxrvvvmsYhmGEh4cb/fv3NwzDMJ577jlDkrFgwYJKrRMAzMZltABQwyIjI3XgwAFNnTpVjRs31jvvvKMnnnhCt99+u66//npt377dNfett95SRESEevXqpfPnz7v+ZGVlaejQoUpJSdH333/vtv7JkyfLbre7Xjdu3Fg9e/YsNi8wMFCHDx/WgQMHqvxZJk2apMDAQNdru92uGTNmqLCwsFYu26tKf670zjvvKDAwUJMmTXIbv+eee9SuXbsaec/U1FT99NNPuvXWWzVz5ky3nxXdH9ioUaMy1+Hr6+s6c1lQUKDMzEydP39eMTExkqTPP/+8zOWrYu3atWrcuLEWLVpU7JEwXl7/+V+Id955RwEBAZo+fbrbnN69e6tv377auXOnMjIyXONm9LSmPktJavL3siq+/PJLffTRR3rwwQc1bNgwHTt2TKmpqerevbskuf55/vz5ar8XAFQHl9ECQC1o3bq1/vznP+vPf/6zzp49q3379ulvf/ub1q9frxEjRujAgQO6/vrr9e233+rixYtlPovx559/1g033OB6HRkZWWxOSEiI0tLS3Mb+/Oc/64EHHlBUVJSuu+469erVSwMHDtRdd91V4S9EufxSyCvHkpOTK7SO6qhKf670448/qnPnzvL19S32sxtvvLHYPYZmvGfR/Zr33ntvsZ8dPHhQktSpU6dSly/y6quvasWKFTp06JAKCgrcflbSfZvV9f3336tDhw5q3LhxmfOOHj2qm266qcT9qHPnztq1a5dSUlJc3xpsRk8rq6KfpSQ1+XtZFfHx8ZKk3/72t5Kkr776SpJ0yy23SLp0qbgkhYaGVvu9AKA6CJsAUMuuueYaDRs2TMOGDdN1112nZ599Vhs2bND/+3//T4WFhWrfvr1eeumlUpe/MpR4e3tX6H2HDBmiY8eO6aOPPtLu3bu1e/durVu3TvPnz9e+ffvK/B9pT1GV/njCexaFzZ49exb72f79++Xv719usPrzn/+sadOm6c4779SKFSvUokUL2e12FRQUKDY2VoWFhW7zrzx7d7krg2pts2I7VkdN/l5WRdE3G0dHR0u6tA9J0q9//WtJ0uHDh91eA4BVCJsAYKGiLzQ5ffq0JOmGG27QyZMn1adPnzK/SKiqAgMDdffdd+vuu++WJL388st69NFH9Ze//EXz5s0rd/kjR45o2LBhxcYk6frrrzelxrJCkhn9adu2rZKTk5WXl1fs7GbRZzH7PRMSEuTr66vOnTsX+9n+/fsVHR1d5ueWpDfeeEMRERH66KOP3C77/Pbbb0ucX/RIkPT09GLfPHv06NESz+xe6YYbbtD333+vCxculHlGsKinubm5bpeOStKhQ4dks9nUpk0bt/XW5H5ekop+ltKWral6y9vuJcnKypIk15d67d+
"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": "iVBORw0KGgoAAAANSUhEUgAABR0AAAGtCAYAAACbedOMAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AADtmElEQVR4nOzdeVhU1R/H8fcwrCI7qLjhvi+oqWXuu+aaa1lppmWlZdpm9Uut1BayxUqzzLQyU1JLzdxSc01z33DfxRREcANk5v7+IEYJVECGAfy8nmee5p65597vHMDOfOcsJsMwDERERERERERERESyiZOjAxAREREREREREZH8RUlHERERERERERERyVZKOoqIiIiIiIiIiEi2UtJRREREREREREREspWSjiIiIiIiIiIiIpKtlHQUERERERERERGRbKWko4iIiIiIiIiIiGQrJR1FREREREREREQkWynpKCIiIiIiIiIiItlKSUcRERERERERERHJVko6iki2+Pzzz/nqq68cHYaIiIiIiIiI5ALOjg5ARPK+UaNG8c0337BixQpHhyIiIiIiIiIiuYCSjiJyR6KiojCZTPz555+UKlXK0eHcEcMw+Oyzz7BarTz33HOYTCZHhyQiIiIiIiKSJ2l6tYjckcDAQEaOHJnnE44AJpOJhg0bMnr0aF5//fUsXWPUqFGYTCaOHj2avcHlIU2bNs0Xvw8iIiKSt5UqVYqmTZvm6D379etnty+ut23bRosWLfDz88NkMjFq1Ci73CcjTCYT/fr1sx0fPXrU4THlhhhEJDUlHUUk01auXInJZLrlIyIiwqExfvvtt3z88ceZrlerVi2WLFnCxIkT+fbbb7M9LhERERFJlpCQwOTJk2ndujWFCxfG1dUVb29vQkNDGTx4MH/99ZejQ8w1kpKS6NatGwcOHODtt9/mu+++48EHH3R0WCIit6Tp1SKSZd27d6dz587pvla0aNEcjia1b7/9lqNHjzJ06NBM173nnntYtmwZixcvJj4+Hnd39+wPMB9bsmQJhmE4OgwRERHJxY4ePUqnTp3YuXMnDRs2ZMiQIRQtWpSrV6+ye/dufvnlFz7//HP++OMPmjVrlqV77Nu3L98sl3P48GEOHz7Mhx9+yODBgx0djohIhijpKCJZVrNmTR555BFHh2FjsVhISEigQIECd3ytOnXqUKdOnWyI6u7j6urq6BBEREQkF4uPj6dDhw7s27ePWbNm0aNHjzTnfPrpp0ybNg0PD48s38fNze1OwsxVzpw5A4C/v7+DIxERyThNrxaRHLFjxw4eeughihYtiqurK8WKFaNz585s3rzZds7GjRvp378/FStWxNPTE09PT+rWrcvUqVPTXC9l7cQ9e/bw8ssvExISgpubG7NmzcJkMrFq1SqOHTuWasr3ypUrbxljZu6fGadPn+bFF1+kdu3a+Pv74+bmRoUKFXj99de5evVqhq9z7do1PvroI+rUqYOnpydeXl7UqFGDkSNHpjovPj6e0aNHU6lSJdzd3fH396djx478/fffaa6Zsh7Pxo0bad68OQULFsTX15fevXtz9uzZVOcmJCTw9ttvU6VKFTw9PfH29qZixYr0798/1fu42ZqOv//+O/Xr18fDw4NChQoxcOBAzp8/f8s1gRYtWsS9996Lh4cHQUFBPPXUU1y+fDnNtS9evMjrr79OxYoVcXNzw9/fny5durBjx44Mt6+IiIjkjK+//prdu3fz4osvpptwBHB2duaJJ57g3nvvtZVZrVbGjh1L06ZNCQ4OtvUp+/bty/Hjx9NcI701HVPK9u/fT+fOnfHx8aFgwYK0b9+egwcPZvg9/PPPP/Tt25eAgAA8PT25//77WbFixU3PP3ToEP369bP1hYsXL84zzzxDVFTUbe9VqlQpmjRpAsDjjz9u69umrCFuGAZfffUV9erVs/VhGzRowLx589K93ooVK2jXrh1+fn64ublRuXJl3nvvPSwWS5pzly5dmqov1r9//9vGHB4eTu3atfHw8CA4OJihQ4dy6dKlVOdktn9sGAbffvst999/P97e3hQoUIBKlSrx3HPPkZiYeMt41q1bR1BQEFWqVOHYsWO3PFdEspdGOopIll25ciXdTofZbMbPz892vGjRIrp27YqrqytPPPEElSpVIjo6mlWrVrFu3TrbiMK5c+eya9cuunfvTkhICLGxscyaNYv+/ftz7tw5Xn755TT36tOnD87Ozjz77LMULFiQihUr8t133zFmzBiioqL46KOPbOdWrlz5lu8nK/fPiB07dhAeHk6XLl3o378/hmGwcuVKxo0bx9atW/ntt99ue41r167Rrl07li9fTpMmTXjzzTfx9vZm7969zJ49m9GjRwPJoz3bt2/PihUraN++PYMHD+bMmTNMnDiRhg0bsmjRojRTlLZv3067du147LHH6NWrF5s3b+brr7/mwoUL/P7777bzBg8ezNdff02fPn147rnnADhy5AgLFizg8uXLtxyJ8Ouvv9K1a1eKFCnCq6++ip+fH7/88gtt27a9aZ1Fixbx2Wef8dRTT9GvXz+WL1/O5MmTMZlMTJo0yXZeXFwcDRs25ODBg/Tt25eaNWsSExPDV199xX333cfq1aupXbv2bdtYREREckZ4eDgAAwcOzFS9xMRE3nvvPR588EEeeOABfHx82LFjB9988w3Lly9nx44dGRoJeOrUKRo3bkynTp147733OHDgABMmTKBz587s3LkTJ6dbj82Ji4ujUaNGtr5HvXr12L17Nx06dKBs2bJpzt+2bRtNmzalQIEC9O/fn5CQEA4cOMDEiRNZvnw5GzduxMfH56b3+/jjj9m0aRNjx47lySefpFGjRgAEBQUByYnI6dOn07lzZ/r06QPAnDlz6Nq1KxMnTmTQoEG2a33zzTcMGDCAWrVq8eqrr+Lr68vatWsZMWIEW7duZebMmbZzFy5cSOfOnQkKCuKVV17B39+fOXPm3LL/tmDBAsaPH88zzzzDE088wfLly/nkk0/Ytm0by5cvx2w2A5nvH/fr14/p06dTq1YtXnrpJQoVKsShQ4eYM2cOb7311k1n2vz888888sgj1KtXj3nz5qX6jCIiOcAQEcmkFStWGMBNHyEhIbZzL1++bAQFBRk+Pj7GoUOH0lzLYrHYnl+6dCnd1xs1amT4+PgYiYmJtvKRI0cagNGwYcNU5SmaNGmSKo6MyMz9byYlriNHjtjKrly5kup9pnj99dcNwNi4ceNtr/vBBx8YgPHcc88ZVqs1TYwppkyZYgDGwIEDU52zb98+w83NzShfvnyq8wHDZDIZa9euTXX+U089ZQDGvn37bGV+fn5G27Ztbxvrf9s+KSnJKFmypOHj42OcPn3aVm61Wo3OnTsbgNG3b19b+ZEjRwzA8PDwSPM706ZNG8PFxSXVz2ro0KGGi4uLsWHDhlTnxsTEGMWLFzeaNm1625hFREQk5wQEBBje3t5pyq1Wq3Hu3LlUj4sXL6Z6/fLly2nqLV261ACM999/P1V5SEiI0aRJkzRlgDFjxoxU5ePGjTMAY/HixbeN/3//+58BGB999FGq8h9//NHWH75RaGioUbp0aSM6OjpV+V9//WWYzWZj1KhRt71nSv976tSpqcrnzZtnAMb48ePT1OnYsaPh7e1txMXFGYZhGJGRkYa7u7vRpUuXNP3JsLAwAzBWrlxpGEZy/7JUqVJGwYIFjePHj9vOS0pKMtq3b3/T/pvJZDL++uuvVNd+7rnn0sSemf7x7NmzDcB48MEHjWvXrqU632q12t5LSgwjR440DMMwPvroI8PJycno3bu3ER8fn+ZeImJ/ml4tIlnWr18/li5dmubxww8/2M5ZsmQJ586dY+jQoZQpUybNNW78JtnT09P2/OrVq0RHR3P+/Hnatm1LbGws+/btS1N/+PDhuLi4ZMv7ycr9M8LDw8P2Pq9du8b58+eJioqiVatWABnamfH777/H09OTsWPHplkQ/cY2/PnnnwFsIx9TVKhQgYcffpgDBw6wc+fOVK/dd999NGjQIFVZSmz79++3lfn6+rJ79262b99+23hvtHn
"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
}