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

182 lines
82 KiB
Plaintext
Raw Normal View History

{
"cells": [
{
"cell_type": "markdown",
"id": "c50e4dfe",
"metadata": {},
"source": [
"# 13 — Quadratic-impact controlled SDE"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "fe0fb749",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T10:16:10.197004Z",
"iopub.status.busy": "2026-05-12T10:16:10.196701Z",
"iopub.status.idle": "2026-05-12T10:16:10.857514Z",
"shell.execute_reply": "2026-05-12T10:16:10.856390Z"
}
},
"outputs": [],
"source": [
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from optimizr import _core as opt\n",
"plt.rcParams['figure.figsize'] = (7, 4)\n",
"plt.rcParams['figure.dpi'] = 110\n"
]
},
{
"cell_type": "markdown",
"id": "e0b5bfa5",
"metadata": {},
"source": [
"## Riccati fixed-point check\n",
"\n",
"$h'(t) = h(t)^2/γ - φ$ with $h(T) = A$. When $γ = φ = A = 1$ the right-hand side is $h^2 - 1 = 0$ at $h = 1$, so `h ≡ 1`."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "959f0a2a",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T10:16:10.860816Z",
"iopub.status.busy": "2026-05-12T10:16:10.860419Z",
"iopub.status.idle": "2026-05-12T10:16:10.868124Z",
"shell.execute_reply": "2026-05-12T10:16:10.865384Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"h drift from 1: 0.0\n"
]
}
],
"source": [
"res = opt.quadratic_impact_control_py(\n",
" gamma=1.0, phi=1.0, a_terminal=1.0,\n",
" t_horizon=0.5, n_steps=500,\n",
")\n",
"tg = np.array(res['time_grid'])\n",
"h = np.array(res['h']); k = np.array(res['feedback_gain'])\n",
"print('h drift from 1:', float(np.max(np.abs(h - 1.0))))\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "e1e343d8",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T10:16:10.872019Z",
"iopub.status.busy": "2026-05-12T10:16:10.871475Z",
"iopub.status.idle": "2026-05-12T10:16:11.159776Z",
"shell.execute_reply": "2026-05-12T10:16:11.158761Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAvYAAAGtCAYAAAB9QDCJAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AABa90lEQVR4nO3dd3hUVf7H8c/MpIeEVCDUQADpRZb8EEWagiAIArKiCMiiorKuAhYQJeiCgIhlFXtX0AUBC1UEFAursuqiGKpBwEDaJKGkzcz9/REzMKaQhBS58349D4/OmXPnnnu+meQzd87csRiGYQgAAADAec1a2wMAAAAAcO4I9gAAAIAJEOwBAAAAEyDYAwAAACZAsAcAAABMgGAPAAAAmADBHgAAADABgj0AAABgAgR7AAAAwAQI9gAAAIAJEOwBVKuEhARZLBYlJSXV9lDKZcKECbJYLOXuv2XLFvXo0UMhISGyWCx67bXXtHXrVvf/1yaLxaIJEybU6hj+LHMBAN6AYA+g3IpC2pn/goKC1LFjRz300EPKycmp7SGWyxNPPFElQdNut2vEiBE6efKkHnvsMb355pu69NJLz32A8LB161YlJCQoMzOztodyXvjggw9ksVhktVq1f//+atlHcnKyZs2apcGDBysmJkYWi0WXXXZZtewLQPlZDMMwansQAM4PW7duVd++fTVq1CgNGzZMkpSamqp///vf2r59uwYOHKj169d7bONwOORwOOTv71+hM+HVKTY2VrGxsdq6dWux+woKCuR0OhUQEHDWx9m4caMGDhyo9957TyNGjHC3u1wu5efny9fXVzabrSqHXiEWi0Xjx4+v1bPlVTEXCQkJmjNnjn755RfFxsZW7QBNaOjQofrhhx907NgxTZs2TfPmzavyfRT9LmjUqJG6deumDz74QP3799emTZuqfF8Ays+ntgcA4PzTuXNnjR071n37jjvuUHx8vDZs2KAdO3aoW7du7vt8fHzk43P+/Krx9fWVr69vufoePXpUkhQREeHRbrVay/XCwBswFzXryJEjWrdunebMmaPvvvtOr732mh566KEqfw5269ZNx44dU7169STpT/OiHfB2LMUBcM5sNpv69u0rSdq7d6/HfaWtsT9x4oQSEhLUoUMHBQYGKjw8XN27d9fTTz/t0a+goECPP/64unXrpuDgYIWEhKhTp06aPXu2u4/L5dK8efPUp08fxcTEyM/PT40aNdL48eP166+/uvslJSXJYrHo4MGD+vTTTz2WFBWNr7xr7IvOhktS37593Y8jlbyufOzYsbJYLHr//fc9Hmf37t0KCQlRt27dlJeX527fsmWLBg0apPDwcPn7+6tt27ZasGCBnE5nsbF8/PHH6tGjhwIDAxUdHa2JEycqLS3trMdQ5MzxPvvss2rbtq0CAgIUGxurhIQEORyOYtskJibq2muvVf369eXv768WLVpo+vTpys7OLvWxS2p788031alTJwUEBKhRo0aaOXOmxzH26dNHc+bMkSQ1b97cPc8JCQnlPr4iERERat26tTZu3FjsPpfLpdjYWLVq1UpV9Ub2iRMndNddd+nqq69Wdna2Bg8erODgYP3lL3/Rl19+WSX7+KNXXnlFhmFo3LhxmjhxopKTk7VmzZoq309ISIg71AP48zh/TqMB+FMrWssbGRl51r5ZWVnq1auXdu7cqaFDh2rixIny9fXVzp07tXLlSk2ZMkVSYagfNGiQPvnkE/Xu3VsPPvigQkND9fPPP2v58uXuwJefn68FCxZoxIgRuvLKK1W3bl3973//0yuvvKJPPvlE//vf/xQREaHo6Gi9+eabuuuuuxQVFaX777/fPabo6OgKHe+bb76pbdu26YUXXtDMmTPVtm3bMvs/99xz+vbbb3XjjTfqu+++U7NmzZSTk6NrrrlGVqtV//73v+Xv7y+pMJxNmjRJXbt21X333aewsDB98cUXmjFjhr777ju988477sdds2aNhg0bpujoaN17772KiIjQypUrdcUVV1ToeCTp6aef1uHDhzV58mRFRETo/fff15w5c7R//369+eab7n7ff/+9Lr30UjkcDt12221q0aKFPv/8cz322GP65JNP9MUXXygoKOis+3v++ed15MgRTZo0SdHR0Vq5cqUeeeQRhYaG6r777pMk3X///YqIiNCqVav0+OOPKyoqSpLUqVOnCh/fU089pX/+858aO3asDh065J5vSVq/fr0OHjyohQsXul+g5eXl6fjx4+V+/IiICFmthefLTp06pUsuuUQ//PCDEhIS9NJLL2ndunW6//779dZbb6lfv3764osvPN7dstvtJb5wK0lAQIDq1Knj0eZyufTyyy/rsssuU5MmTdSwYUM1bNhQL774onvp3JmysrJUUFBQrv35+vqqbt265eoLoBYZAFBOW7ZsMSQZM2bMMFJTU43U1FRj165dxgMPPGBIMpo1a2bk5eV5bDN79mxDkvHLL7+4226//XZDkvHYY48V24fT6XT//6OPPmpIMu644w7D5XKV2s/lchknT54s9lgff/yxIclYuHChR3uzZs2M3r17l3iM48ePN8r7q/HVV181JBlbtmzxaC+ap1dffdWj/X//+58RGBho9OjRwygoKDD+9re/GZKMd999190nOTnZCAgIMIYPH17smBctWmRIMrZu3WoYRuEcxMbGGnXq1DF+/fVXdz+Hw2EMHjzYkGSMHz/+rMdRNN6goCAjKSnJ3e50Oo3hw4cXO8ZevXoZFovF+Pzzzz0eZ86cOYYk4+GHHy5zLoraGjRoYGRkZHjsr23btkZMTIzH45b0M1RZmzZtMiQZ7733nkf7sGHDDD8/PyMlJcXdVlTf8v47c3yLFy82JBl///vfDcMwjJtvvtmQZDgcDuObb74xJBkDBw70GEOzZs3Kva+S6rp+/XpDkrFs2TJ323333WfYbDbj0KFDxfr37t273Psr7flSRJLRv3//MvsAqH6csQdQYY888ogeeeQRj7YBAwbomWeekZ+fX5nbulwuLV26VC1atNCdd95Z7P6iM56S9NZbbyk4OFjz5s0rtjzmzH5FV+cpevzs7Gw5HA516dJFdevW1X/+85+KHmK16Nixo5566inddNNN6tu3rz7//HNNnjxZo0ePdvdZsWKFcnNzNWnSJKWnp3tsP2TIEE2fPl0bNmxQ7969tWPHDiUlJem2225TkyZN3P1sNptmzpyptWvXVmh8Y8eOVbNmzdy3rVarZsyYodWrV+u9995Tnz59lJqaqm3btmnAgAG6+OKLPbafPn26Fi5cqPfee0+zZs066/4mTpyo8PBwj/31799fTz/9tE6cOFHsjHRV6Nu3rxo2bKilS5e6P/D822+/ac2aNRo5cqTHOzcDBw7Uxx9/XO7HbtCggfv/i5a/zJw5U5Lcy6xsNpv+8pe/qFu3btq6dascDod7/fvbb79d7itLNWzYsFjbiy++qLCwMA0fPtzdNnHiRM2fP1+vvvqqHnjgAY/+jz32mOx2e7n2d2adAPx5EewBVNiECRN0/fXXy+FwaPfu3VqwYIEOHz6swMDAs26blpYmu92uPn36eITzkuzZs0dt2rRRcHDwWR939erVWrhwoXbs2KH8/HyP+zIyMs66fU2ZNGmS1q5dq1WrVql9+/Z6/PHHPe7/+eefJRWG+NIcO3ZM0unlT+3atSvWp3379hUeW0mPU9S2b98+SdKBAwckFb5I+aOgoCDFxcWV+xKLLVq0KNZWtJQrPT29WoK91WrVNddco+eff17Hjx9XSEiIXnnlFTkcDt1yyy0efWNiYhQTE1Op/Rw+fFiRkZEeYf9McXFx2rFjh9LS0tx9/vhCqSJSUlL0wQcfaNiwYTp8+LC73WKxqFOnTnrllVc0a9YsjxfIZy4DAmAOBHsAFRYXF+e+ZvUVV1yhAQMGqGvXrrr22mv12Wef1fgVMt5//31dffXV+stf/qLFixeradOm7hcZ1157rVwuV42OpywpKSnavn27pMIzxceOHfM4S1401pdeesmj/Uwlna09H5V1+UujGq/EfO211+rJJ5/UypUrdcMNN+i
"text/plain": [
"<Figure size 770x440 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, ax = plt.subplots()\n",
"ax.plot(tg, h, label='h(t)')\n",
"ax.plot(tg, k, '--', label='k(t) = h(t)/γ')\n",
"ax.axhline(1.0, color='k', alpha=0.3, ls=':', label='fixed point')\n",
"ax.set_xlabel('t'); ax.legend(); ax.grid(alpha=0.3)\n",
"ax.set_title('Riccati fixed point γ=φ=A=1')\n",
"fig.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"id": "4f8b738d",
"metadata": {},
"source": [
"## Sensitivity to the terminal weight\n",
"\n",
"Vary $A$, fix $γ = 1$, $φ = 0.25$, $T = 1$."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "11740dc8",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T10:16:11.162956Z",
"iopub.status.busy": "2026-05-12T10:16:11.162649Z",
"iopub.status.idle": "2026-05-12T10:16:11.541340Z",
"shell.execute_reply": "2026-05-12T10:16:11.539197Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAvYAAAGtCAYAAAB9QDCJAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AACNWklEQVR4nOzdd3hUVcIG8PdOT5v0QEJJAkivioCsSFMQFGURC1gAV3exIC6Wz9VVwYId2V3Xui4qSxRxBayIsGBDLCjSBKQEAgQy6ZlMpt053x9TMpNepmXm/T3PPDO3n8lJ4D33nnuuJIQQICIiIiKiDk0R6gIQEREREVH7MdgTEREREUUABnsiIiIiogjAYE9EREREFAEY7ImIiIiIIgCDPRERERFRBGCwJyIiIiKKAAz2REREREQRgMGeiIiIiCgCMNgTEREREUUABnuiCLR48WJIkoT8/PxQF6VF5s6dC0mSQl0Mvxo3bhxycnJavP4bb7wBSZKwdevWNh2vvdtT+23duhWSJOGNN96IyOO1lCRJmDt3bpu35+8yUdsx2BOFOfd/3t6v2NhYDBo0CI888ghqampCXcQWWb58edgFkGBbt24dFi9eHLTj5efnY/Hixdi5c2eH3D/A3xtqXrD/rojCmSSEEKEuBBE1buvWrRg/fjxmzpyJyy+/HABgMBjw7rvvYvv27Zg8eTI2bNjgs43dbofdbodWqw2bM+E5OTnIyclp8CyczWaDLMvQ6XTBL1iAWK1WCCGg1Wo98+bOnYs333wTDf2zK8sybDYbNBoNFIrWn3NpaHv3786KFSvadQa1MYHeP9D07024cTgcsFqtUKvVUCqVAT9eMH7+bWE2m6FUKqFWq9u0/RtvvIF58+Zhy5YtGDduXLPrN/V3RRRtVKEuABG1zJAhQ3Ddddd5pu+44w6MGDECn332GXbs2IFzzjnHs0ylUkGl6jh/3mq1us0hIFxpNJpWra9UKtsVBtu7fTSSZRkWiwWxsbF+2Z9CoYioxmlb8WdAFDrsikPUQSmVSowfPx4A8Ntvv/ksa6yPvdFoxOLFizFw4EDExMQgOTkZ5557Ll544QWf9Ww2G55//nmcc845iIuLQ0JCAgYPHoyHH37Ys47D4cDSpUsxbtw4ZGZmQqPRoEuXLpgzZw6OHz/uWS8/Px+SJOHYsWP44osvfLoUucvXmj72v/76K2bNmoVu3bpBq9UiIyMDo0ePxr/+9S+f9YQQeO211zBixAjExcUhLi4Oo0ePxrp16+rt090n+Pvvv8eECRMQHx+PpKQkXHPNNSgqKvJZ12Kx4NFHH0X//v0RFxcHvV6PPn364MYbb/TpFlW3j31OTg7efPNNz/HcL3c3k7r9ijdt2gRJkvD44483+HO46aabIEkSDh061OD2ixcv9vx+zJs3z3O8cePGwWKxIC0tDSNGjGhw3++//z4kSar3e+Gtqf27ORwO/P3vf8eQIUMQExMDvV6PCRMm4PPPP290v24t+b0BgMOHD2Pu3LnIysqCRqNB165dceutt6K4uLheeSVJwr59+3DvvfciOzsbWq0W7777rk9f9VdffRX9+/eHTqdD79698dZbbwEATp48iWuuuQapqamIi4vD9OnTcfr0aZ9jNNTn3XveypUrMXjwYOh0OnTp0gX3338/ZFn22cf+/ftx2223YeDAgUhMTERMTAwGDRqEZ599tt66LXXq1ClIkoS77rrLZ/6tt94KSZLwhz/8wWf+/fffD0mSfP6OrVYrnn76aQwePNhTlxdeeCG+/PLLesdrqI+9EALPP/88zjrrLGi1WvTs2RNPPPEENm/e3Oh9AkIILF++HL1794ZWq0Vubi6WLVvms05zf1dE0abjnNIjonoOHz4MAEhNTW123YqKCowZMwa7d+/GtGnTcOONN0KtVmP37t14//33cfvttwNwhvopU6Zg8+bNGDt2LB566CHo9Xr8+uuvWLNmDZYsWQLA+R/9U089hRkzZuCSSy5BYmIidu3ahX//+9/YvHkzdu3ahZSUFKSnp2PlypX485//jLS0NDzwwAOeMqWnp7fq+5aUlGD8+PFwOBz405/+hNzcXJSVlWH37t344osvcNNNN3nWnTdvHt566y1cfvnluPbaawE4A+vvf/97vPTSS5g/f77Pvn/55RdMmTIFN9xwA66++mrs2LED//rXv1BeXu7T1en222/Hv/71L1x77bW44447AABHjx7FRx99hOrqasTExDRY9uXLl2PZsmX46quvsHLlSs/80aNHN7j+hAkT0K1bN7z55ps+PzMAMJlMWLNmDc4//3z06tWrwe1nzJgBm82GpUuX4o9//CPGjBkDAOjUqRO0Wi3mzJmDZcuWYdeuXRg8eLDPtq+99hpiYmJ8rhC1Zv9uc+fOxcqVK/G73/0OS5cuhdFoxL/+9S9MnjwZb731VpP7b8nvzc6dOzFu3DjExsbixhtvRHZ2Nn777Te89NJL2Lx5M77//nskJib67Pfaa6+FSqXCbbfdhvj4ePTp0wcWiwUA8OKLL8JgMOCmm26CXq/Ha6+9hjlz5kCtVuMvf/kLxowZg8ceewz79+/HP//5T8yZMwefffZZo9/B2yuvvIKTJ0/ipptuQnp6Ot5//3088cQT0Ov1uO+++zzrbd26FVu2bMGll16K3NxcmM1mfPLJJ7jnnntw5MgRvPjiiy06nresrCz07dsXmzZt8pm/adMmKBQKbN68ud78s846C927dwfg7No3depUfPHFF5g1axbmz58Pk8mE//znP5gwYQLWrVuHSy+9tMky3HvvvXj22WcxYsQI3HLLLbBYLFixYgXWrl3b6Db3338/KisrMW/ePMTHx+Ott97CXXfdhaysLFxzzTUAWv93RRTxBBGFtS1btggA4i9/+YswGAzCYDCIffv2iQcffFAAENnZ2cJisfhs8/DDDwsA4ujRo555t912mwAgnnvuuXrHkGXZ8/mZZ54RAMQdd9whHA5Ho+s5HA5RXV1db1+ff/65ACCefvppn/nZ2dli7NixDX7HOXPmiJb8c7R+/XoBQLzzzjtNrrdu3ToBQCxbtqzesmnTpgm9Xi8qKys98wAISZLEN99847Pun/70JwFAHDhwwDMvOTlZXHzxxc2WdezYsSI7O9tnXlPfc8WKFQKA2LJli2feX//6VwFAfP311z7rrly5UgAQr7/+epPbu393VqxYUe94Bw4cEJIkidtvv91n/rFjx4RCoRBz5sxp9js2tf/NmzcLAGLKlCnCbrd75hcVFYmMjAyRlJQkqqqqmj1GU783Q4cOFbm5uaKkpMRn/nfffSeUSqVYvHixZ577b+L8888XVqu1we/RuXNnUVpa6pl/+vRpodVqhSRJ4qmnnvLZZuHChfV+Nxr6eTS2b1mWRb9+/URmZqbPfo1GY4Pfdfbs2UKpVIrCwsImj9eY22+/XUiSJM6cOSOEcNYzAHHDDTcIAOLgwYNCCCHKysqEQqEQ8+fP92y7fPlyAUC8//77Pvu0Wq1i2LBhIjc312c+AJ/fH/fv2u9+9zufn315ebno1q1bve/g/l0ePHiwMJvNPj+b1NRUcd555/kcr6X/fhBFA3bFIeognnjiCaSnpyM9PR39+/fHo48+ikmTJmHTpk3N9ud2OBzIy8tDjx49cOedd9Zb7n2z5n/+8x/ExcVh6dKl9brHeK/nHp3Hvf/y8nIUFxdj6NChSExMxHfffdeOb9uwpKQkAMAnn3yC8vLyRtdbuXIlYmJicPXVV6O4uNjnNX36dFRWVuLbb7/12ea8886rd5bvoosuAgAcPHjQpwx79+7FL7/84p8v1QR3d4a63QreeOMNxMbG4sorr2zzvnv37o3x48fjP//5j08Xotdffx0OhwN//OMf27xvAPjvf/8LAHjwwQd9+v6np6fjtttuQ3l5eb0zxa2xZ88e7Ny5E9dccw0cDodPHffo0QO9evVq8Gz6XXfd1ej9HDfeeCOSk5M90506dUKfPn0gSZLn6ozb2LFjAfj+bjSl7r4VCgUmTpyIwsJCGI1Gz/y4uDjPZ4vFgtLSUhQXF+Piiy+GLMv48cc
"text/plain": [
"<Figure size 770x440 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, ax = plt.subplots()\n",
"for A in [0.0, 0.25, 0.5, 1.0, 2.0, 5.0]:\n",
" r = opt.quadratic_impact_control_py(1.0, 0.25, A, 1.0, 1000)\n",
" ax.plot(r['time_grid'], r['h'], label=f'A = {A:g}')\n",
"ax.set_xlabel('t'); ax.set_ylabel('h(t)'); ax.legend(); ax.grid(alpha=0.3)\n",
"ax.set_title('Riccati sensitivity to terminal weight')\n",
"fig.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"id": "df2cd9e3",
"metadata": {},
"source": [
"**Verified:** `h ≡ 1` with `max|h - 1| < 1e-9` at the fixed point."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (rhftlab)",
"language": "python",
"name": "python3"
},
"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
}