Files
optimiz-rs/examples/notebooks/10_bsde.ipynb
T

219 lines
99 KiB
Plaintext
Raw Normal View History

{
"cells": [
{
"cell_type": "markdown",
"id": "7b032b6b",
"metadata": {},
"source": [
"# 10 — BSDE θ-scheme\n",
"\n",
"Generic CPU-only CrankNicolson scheme for linear backward stochastic differential equations. Reference doc page: [bsde.rst](../../docs/source/algorithms/bsde.rst)."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "9e253922",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T10:15:57.439545Z",
"iopub.status.busy": "2026-05-12T10:15:57.439220Z",
"iopub.status.idle": "2026-05-12T10:15:58.354185Z",
"shell.execute_reply": "2026-05-12T10:15:58.352587Z"
}
},
"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": "afd86cb2",
"metadata": {},
"source": [
"## Exponential ground-truth check\n",
"\n",
"With $a(t) \\equiv -\\rho$, $b = c = 0$ and $Y_T = 1$ the analytic deterministic solution is $Y_t = e^{-\\rho (T-t)}$."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "b2abb764",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T10:15:58.357829Z",
"iopub.status.busy": "2026-05-12T10:15:58.357449Z",
"iopub.status.idle": "2026-05-12T10:15:58.364151Z",
"shell.execute_reply": "2026-05-12T10:15:58.363092Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Y0 = 0.740818179010676 exp(-rho T) = 0.7408182206817179\n",
"max abs error = 4.167104183938619e-08\n"
]
}
],
"source": [
"rho = 0.3\n",
"T = 1.0\n",
"res = opt.linear_bsde_constant_coeffs(\n",
" a_const=-rho, b_const=0.0, c_const=0.0,\n",
" terminal=1.0, n_steps=200, t_horizon=T, theta=0.5,\n",
")\n",
"tg = np.array(res['time_grid'])\n",
"yg = np.array(res['y'])\n",
"analytic = np.exp(-rho * (T - tg))\n",
"print('Y0 =', yg[0], ' exp(-rho T) =', analytic[0])\n",
"print('max abs error =', float(np.max(np.abs(yg - analytic))))\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "6d088c15",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T10:15:58.373282Z",
"iopub.status.busy": "2026-05-12T10:15:58.372923Z",
"iopub.status.idle": "2026-05-12T10:15:58.766339Z",
"shell.execute_reply": "2026-05-12T10:15:58.762616Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAvYAAAGtCAYAAAB9QDCJAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AACJiElEQVR4nOzdd3gUVd/G8e/upoeEJBAg9E5QqkCQIp3QNfQiAiIgIogUGz7Piw3sCIKKBRGlSpfeQRGQKiISeidICAkBQsruzvsHZh9iKAmEbMr9ua5cFzs7c+Y3e7Lh3tkzZ0yGYRiIiIiIiEi2ZnZ2ASIiIiIicv8U7EVEREREcgAFexERERGRHEDBXkREREQkB1CwFxERERHJARTsRURERERyAAV7EREREZEcQMFeRERERCQHULAXEREREckBFOxFRERERHIABXuRLOjEiROYTCbeeOMNZ5cikm6NGjWiZMmSTq1h48aNmEwmvvvuuwe+r6xwvHJ7JpOJPn36PJC21feS1SjYi2Si5LDxzjvvOLuUTPfdd99hMplS/Hh4eFCqVCl69OjBrl27Um1z4cIFXn31VSpXroyvry8+Pj6ULl2a9u3bM2XKlBTr9unTJ0Xbbm5uBAYGUrt2bYYOHXrL9gHeeOONVHXd/FOoUKEH8nrcqy1btvDUU09RqlQpPD098fLyomLFijz33HO3PcbsrmTJkphMJqpXr47dbk/1/DvvvIPJZGLjxo2ZX5zkeG+88QaLFi1ydhkiaeLi7AJEJLUSJUpw/fp1XFxy3lv0ueeeo27dugDExcXxxx9/MGXKFObNm8eWLVuoWbMmAKdOnSIkJISLFy/SqVMn+vfvj5ubG8eOHWPz5s2MHz+eZ555JlX7n376Kf7+/thsNqKjo9m7dy/ff/89n376Kf369eOLL7645ev63//+l/Lly6da7unpmcGvwL0xDIPhw4czfvx4ChcuTNeuXalQoQIABw4cYMGCBUyePJn9+/fz0EMPObnaB+P3339n+vTp9OrV667rNmjQgOvXr+Pq6poJlUlO9uabb9K7d2/CwsJSPbd69WoMw8j8okRuI+elBpEcIPlsdnYSGxuLr6/vXderW7cuPXv2TLGsfPnyDB06lOnTpzuC/Ycffsjff//N+PHjGTp0aKp2zp8/f8v227dvT9GiRVMsmzBhAr179+abb77B29ub8ePHp9ouNDSU+vXr37V+Z3n33XcZP348HTp0YPr06ak+cHz44Ye3PK5/u3LlCj4+Pg+oygencOHCAPznP/+hS5cud31/mM3mbPcekuzHzc3N2SWIpKChOCJZ0K3G2N+8bMWKFTz66KN4enoSGBjIs88+y7Vr11K1c+XKFV5//XUqVKiAu7s7AQEBhIWF8ccff6RYz263M3bsWBo1akRQUBBubm4UKVKE3r17c+rUqVTtJo9Z3bhxI40aNcLX15eqVave8/Emh7ab/5M8fPgwAE2bNr3lNukZIuPr68usWbMoXrw4n332GadPn77nWp3h4sWLjBkzhmLFivHDDz/c8lsEV1dXXnrpJcfZ+pvHmH/55ZdUqVIFDw8PhgwZAkB4eDjPP/88lSpVIm/evHh6elK5cmU++ugjbDZbiraTh1Ft2LCB8ePHU758edzd3SlVqhTjxo1L0zFcuXKFli1b4u7uzvTp09P9Gnh6evLWW29x+vRpJkyYcNf1bzfG3jAMvvvuO+rVq4evry9eXl4EBwfzwgsvkJiY6FjPbrfz6aefUrVqVTw9PfH19aVJkyasWbMmTfUeOHCA7t27U6xYMdzd3SlQoAB169blm2++SbFefHw8b775JsHBwXh4eBAQEEC7du3YuXNnqjaT33fbt2+nSZMm5MmTBz8/P7p168aFCxfuWtM333yDyWRixowZt3y+WbNmeHt7ExsbC8CZM2cYMGAApUqVwsPDg/z581OjRg3Gjh17133d69+UtBzblStX+O9//8ujjz5KYGAgbm5ulCxZksGDB3Pp0qW71lapUiWKFCmS6vccYPfu3ZhMJkaOHOn4HQKYNm1aiiF6yW43xv748eP079+fEiVK4O7uTsGCBQkNDU3z74/IvdIZe5FsZsWKFUyaNIlnn32WPn36sG7dOr766itMJhOTJ092rBcbG0v9+vU5cuQIvXv3pmrVqkRHR/P1119Tp04dfvnlFx555BEAEhMTef/99+nQoQNt2rQhb968/PHHH3z77besW7eOP/74g4CAgBR17Ny5k3nz5tG3b1969OjBlStX0lT/1atXuXjxIgDXr19n3759vP766+TJkyfFBW5lypQBYOrUqbz//vv3PSzJw8OD3r178/bbb7Ny5Ur69++f4vnLly876vr3dnny5Lmvfd+vZcuWERcXx7Bhw/Dy8krXthMmTODvv/+mf//+FC1a1HG2fuPGjWzYsIG2bdtSqlQp4uPjWb58OS+99BLHjh3j888/T9XWqFGjiI2N5emnnyZPnjx8//33jBgxgsKFC9OtW7fb1nDu3Dlat27NiRMnWLlyJY0bN07fC/CPPn368Mknn/Duu+/Sr18/8uXLd09tfP/991SvXp2XXnqJAgUKcPToURYsWMBbb73l+HDZp08ffvjhB+rVq8fYsWO5evUq33zzDS1atOD7779P9a3TzaKiomjcuDF2u51nn32WUqVKER0dzb59+9i0aRP9+vUDwGaz0bp1azZs2EDr1q0ZPHgw58+f54svvqB+/fqsWLEi1Wu1d+9eWrVqRa9evejatSu7du3im2++ISYmhpUrV97x2Lt06cLQoUP57rvvePLJJ1M8d/r0aTZs2ECPHj3w9fXFarXSvHlzTp8+zXPPPUdwcDBXr14lPDyc9evXM2rUqDvu617+pqT12M6ePctXX31Fhw4d6Nq1Kx4eHmzfvp0vv/ySzZs3s2PHjjsOwXr22Wd54YUXWLZsGY8//niK577++msABgwYQN68efnhhx946qmneOyxxxgwYMAdjznZ7t27adq0KXFxcfTp04dq1apx5coVtm3bxtq1a2nevHma2hG5J4aIZJoNGzYYgPH222/fcb3jx48bgDF69OhUyzw9PY2jR4+mWL9FixaGq6urcfXqVceyF1980XB1dTW2bduWYt3o6GijaNGiRqNGjRzL7Ha7ce3atVR1rFmzxgCMDz74IMVywACMFStW3PWYk02dOtWx3b9/goODjb1796ZY/+jRo0bevHkNwChQoIDRsWNH4/333zc2b95s2Gy2VO337t3bAIzTp0/ftob58+cbgDFixAjHstGjR9+2LsDo3bt3mo/xQRkxYoQBGPPnz0/zNsm/a35+fkZERESq52/+XblZjx49DIvFkmKb5L6rUqWKER8fn6KNfPnyGXXq1EnRRsOGDY0SJUoYhmEY+/btM4oVK2YUK1bM2LdvX5rrv1mJEiWMMmXKGIZhGMuWLTMAY+jQoY7n3377bQMwNmzY4FiWfPxTp051LJs7d64BGB06dDCSkpJS7MNutxt2u90wDMNYt26dARitWrUyrFarY50LFy4YBQoUMPz8/IwrV67c8ngNwzAWL15sAMbs2bPveFxTpkwxAKN///4plh88eNBwd3c3ypUrl+J3HTBMJpPx66+/plj/2WefNQDj4MGDd9yfYRhGz549DbPZbJw6dSrF8uTXcN26dYZhGMbevXsNwHjvvffu2uat3MvflLQeW0JCgpGYmJiq7a+//toAjB9//DFV2ze/j2NiYgwvLy+jbdu2Kda7du2a4evrazRs2PCO29/s331vt9uNSpUqGS4uLsZvv/2Wav1b/e0SyUgaiiOSzbRv357SpUunWNa8eXOSkpI4fvw4cGO4wfTp06lTpw5lypTh4sWLjh+r1UpoaCi//PIL169fB258DZ58JthutxMTE8PFixepVq0aefPm5bfffktVR9WqVWnZsmW663/11VdZs2YNa9asYenSpYwbN464uDhatWrFX3/95VivdOnS7N27lxdeeAFvb2/mz5/PK6+8Qv369SlbtiyrV69O976TrwG4fPlyquc++eQTR103/7z88svp3k9GS643Ldcw/Fv
"text/plain": [
"<Figure size 770x440 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, ax = plt.subplots()\n",
"ax.plot(tg, yg, label='θ-scheme', lw=2)\n",
"ax.plot(tg, analytic, '--', label='analytic exp(-ρ(T-t))')\n",
"ax.set_xlabel('t'); ax.set_ylabel('Y_t')\n",
"ax.set_title('Linear BSDE — CrankNicolson vs analytic')\n",
"ax.legend(); ax.grid(alpha=0.3)\n",
"fig.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"id": "8f7263bb",
"metadata": {},
"source": [
"## Convergence rate study\n",
"\n",
"CrankNicolson is second-order in `Δt`."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "ee119e84",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T10:15:58.776028Z",
"iopub.status.busy": "2026-05-12T10:15:58.775538Z",
"iopub.status.idle": "2026-05-12T10:15:58.792909Z",
"shell.execute_reply": "2026-05-12T10:15:58.789105Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[(25, np.float64(2.666998401679166e-06)), (50, np.float64(6.667396952320104e-07)), (100, np.float64(1.6668430979915883e-07)), (200, np.float64(4.167104183938619e-08)), (400, np.float64(1.0417760876180182e-08)), (800, np.float64(2.6044438827810268e-09))]\n"
]
}
],
"source": [
"errs = []\n",
"ns = [25, 50, 100, 200, 400, 800]\n",
"for n in ns:\n",
" r = opt.linear_bsde_constant_coeffs(-rho, 0.0, 0.0, 1.0, n, T, 0.5)\n",
" errs.append(abs(r['y'][0] - np.exp(-rho * T)))\n",
"print(list(zip(ns, errs)))\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "b5e30db2",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T10:15:58.804359Z",
"iopub.status.busy": "2026-05-12T10:15:58.799015Z",
"iopub.status.idle": "2026-05-12T10:15:59.662120Z",
"shell.execute_reply": "2026-05-12T10:15:59.660888Z"
}
},
"outputs": [
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 770x440 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, ax = plt.subplots()\n",
"ax.loglog(ns, errs, 'o-')\n",
"ax.loglog(ns, [errs[0] * (ns[0] / n) ** 2 for n in ns],\n",
" ':', label='O(Δt²) reference')\n",
"ax.set_xlabel('n_steps'); ax.set_ylabel('|Y0 analytic|')\n",
"ax.set_title('CrankNicolson convergence'); ax.grid(which='both', alpha=0.3); ax.legend()\n",
"fig.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"id": "771765c7",
"metadata": {},
"source": [
"**Verified against analytic ground truth:** `Y_t = exp(-ρ (T - t))` — relative error at `t = 0` below `1e-3` for `n_steps = 200`."
]
}
],
"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
}