Files
optimiz-rs/examples/notebooks/08_volterra.ipynb
T

750 lines
579 KiB
Plaintext
Raw Normal View History

{
"cells": [
{
"cell_type": "markdown",
"id": "dab877f7",
"metadata": {},
"source": [
"# 08 — Volterra and Fractional Solvers\n",
"\n",
"Companion notebook for the [`volterra` documentation page](https://optimiz-r.readthedocs.io/en/latest/algorithms/volterra.html).\n",
"\n",
"This notebook follows the depth and structure of\n",
"`03_optimal_control_tutorial.ipynb`. Each section opens with a precise\n",
"mathematical reminder (definition, theorem, derivation), validates the\n",
"corresponding `optimizr` primitive against an analytic ground truth, runs a\n",
"convergence study where relevant, and ends with a concrete physical\n",
"application.\n",
"\n",
"The four CPU-only Rust primitives demonstrated are:\n",
"\n",
"1. `solve_fractional_ode` — Caputo fractional Adams predictorcorrector\n",
" (DiethelmFordFreed 2002).\n",
"2. `geometric_grid_lift` — multi-exponential Markovian lift of a\n",
" convolution kernel (Abi JaberEl Euch 2019).\n",
"3. `solve_volterra` — generic second-kind Volterra integral equation by\n",
" product trapezoidal rule.\n",
"4. `fourier_invert` — recovery of a probability density from its\n",
" characteristic function via discrete cosine/sine transform.\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "3dee49a3",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T17:22:45.901419Z",
"iopub.status.busy": "2026-05-12T17:22:45.901131Z",
"iopub.status.idle": "2026-05-12T17:22:46.682392Z",
"shell.execute_reply": "2026-05-12T17:22:46.680664Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"volterra notebook ready.\n"
]
}
],
"source": [
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from optimizr import _core as opt\n",
"from scipy.special import gamma as Gamma\n",
"\n",
"plt.rcParams['figure.figsize'] = (10, 4)\n",
"plt.rcParams['figure.dpi'] = 110\n",
"plt.rcParams['axes.grid'] = True\n",
"plt.rcParams['grid.alpha'] = 0.3\n",
"\n",
"rng = np.random.default_rng(0)\n",
"errors = {}\n",
"print('volterra notebook ready.')\n"
]
},
{
"cell_type": "markdown",
"id": "0f66f007",
"metadata": {},
"source": [
"## 1. Fractional calculus and the Caputo derivative\n",
"\n",
"### Definitions\n",
"\n",
"For $\\alpha \\in (0, 1)$ and a sufficiently regular function $h : [0, T] \\to\n",
"\\mathbb{R}$, the **Caputo fractional derivative** is\n",
"\n",
"$$\n",
"D^\\alpha h(t) \\;=\\; \\frac{1}{\\Gamma(1 - \\alpha)} \\int_0^t \\frac{h'(s)}{(t - s)^\\alpha}\\, ds.\n",
"$$\n",
"\n",
"It interpolates between the ordinary derivative ($\\alpha \\to 1$) and a\n",
"non-local memory operator. The associated **Cauchy problem**\n",
"\n",
"$$\n",
"D^\\alpha h(t) \\;=\\; F(t, h(t)), \\qquad h(0) = h_0,\n",
"$$\n",
"\n",
"is equivalent to the Volterra integral equation\n",
"\n",
"$$\n",
"h(t) \\;=\\; h_0 + \\frac{1}{\\Gamma(\\alpha)} \\int_0^t (t - s)^{\\alpha - 1}\\, F(s, h(s))\\, ds.\n",
"$$\n",
"\n",
"### Mittag-Leffler closed form\n",
"\n",
"For the linear test problem $D^\\alpha h = -h$, $h(0) = 1$, the unique\n",
"solution is the **Mittag-Leffler function** of order $\\alpha$:\n",
"\n",
"$$\n",
"E_\\alpha(z) \\;=\\; \\sum_{k=0}^\\infty \\frac{z^k}{\\Gamma(\\alpha k + 1)}, \\qquad\n",
"h(t) = E_\\alpha(-t^\\alpha).\n",
"$$\n",
"\n",
"It generalises the exponential ($\\alpha = 1 \\Rightarrow E_1(z) = e^z$) and\n",
"exhibits a **slow algebraic tail** $E_\\alpha(-t^\\alpha) \\sim\n",
"\\frac{t^{-\\alpha}}{\\Gamma(1 - \\alpha)}$ as $t \\to \\infty$ — the signature of\n",
"*sub-exponential relaxation*.\n",
"\n",
"### Adams predictorcorrector (DiethelmFordFreed 2002)\n",
"\n",
"On a uniform grid $t_n = n \\Delta t$, the predictor\n",
"\n",
"$$\n",
"h^P_{n+1} \\;=\\; h_0 + \\frac{\\Delta t^\\alpha}{\\Gamma(\\alpha + 1)} \\sum_{k=0}^n\n",
"\\big[ (n + 1 - k)^\\alpha - (n - k)^\\alpha \\big]\\, F(t_k, h_k),\n",
"$$\n",
"\n",
"and the corrector\n",
"\n",
"$$\n",
"h_{n+1} \\;=\\; h_0 + \\frac{\\Delta t^\\alpha}{\\Gamma(\\alpha + 2)}\n",
"\\Big[ F(t_{n+1}, h^P_{n+1}) + \\sum_{k=0}^n a_{n+1,k}\\, F(t_k, h_k) \\Big],\n",
"$$\n",
"\n",
"deliver an order $\\min(2, 1 + \\alpha)$ approximation. The history sum\n",
"explicitly encodes the **memory** intrinsic to fractional dynamics, hence\n",
"the higher per-step cost compared to a Markovian RungeKutta integrator.\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "9db5720c",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T17:22:46.686123Z",
"iopub.status.busy": "2026-05-12T17:22:46.685746Z",
"iopub.status.idle": "2026-05-12T17:22:48.370914Z",
"shell.execute_reply": "2026-05-12T17:22:48.369466Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABK4AAAGtCAYAAAA/Njx2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AAEAAElEQVR4nOzdd3RU1frw8e/U9N5DEoK00BN6FRCQIiJFhWuhWCNXxVeNol4r1h+KXkXlWkDFhiCKSgdBOgiELh0ChJDey0xm5rx/DBkY0nvh+aw1KzP77LPLzIGcPLOLSlEUBSGEEEIIIYQQQgghGhh1fTdACCGEEEIIIYQQQoiSSOBKCCGEEEIIIYQQQjRIErgSQgghhBBCCCGEEA2SBK6EEEIIIYQQQgghRIMkgSshhBBCCCGEEEII0SBJ4EoIIYQQQgghhBBCNEgSuBJCCCGEEEIIIYQQDZIEroQQQgghhBBCCCFEgySBKyGEEEIIIYQQQgjRIEngSgghhBBCCCGEEEI0SBK4EkIIIYQQQgghhBANkgSuhBBCCCGEEEIIIUSDJIErIa5TX331FR06dMDd3Z3x48eTnp5OeHg4iYmJdvmGDBmCn59fPbWybtRmHytbdnp6Or6+vrz99ttVqm/z5s2oVCq2b99epfOFEEKI68FXX32FSqVi48aN9d2UYhpy2xqjwsJCIiIiiI6OtksPDw9n0KBB9dOoBuDs2bOoVCpeeeWVKpcxZMgQRowYUXONEqIUErgSohErLCzE0dERlUplezg6OtKsWTNGjRrF/PnzMZlMxc6bO3cur732GkuXLuXcuXPs3LmTiRMnMnXqVAICAuzyxsbG0qVLl7rqUr2ozT5WtuxXXnkFnU7H448/XuzY0qVL6d27Ny4uLnh5eTFmzBgOHTpkl2fAgAEMHz6cJ554AkVRqt3+ynj77beZOHEirVu3Rq1Wo9Vq67R+IYQQoi79+uuv1fqjX9SNuXPnEhcXx4svvljfTWly3njjDVavXs3y5cvruymiiZPAlRCN2KFDhzAYDIwYMYKFCxeycOFC5s6dy7Rp04iPj+f++++nd+/epKWl2c4pLCzkpZde4sMPP6Rt27Z4enrSoUMH9u/fz9NPP21X/pkzZ0hPT2/Sgava7GNly7506RKffvopjzzyCM7OznbHvvzySyZMmEBubi7vvPMOL7zwAvv376dv374cPHjQLu9TTz3Frl276vwm4rnnnmPNmjWEhoYWC4AKIYQQDcm9995Lfn4+N954Y5XL+PXXX3n11VdrsFVWNdE2YWUwGHjrrbf417/+RbNmzeq7OU1O79696du3Ly+//HJ9N0U0cRK4EqIR2717NwDjxo3jnnvu4Z577uGBBx7g9ddfZ9++fTz77LPs2bOH6dOn287ZunUrJpOJkSNH2tIMBgPPP/88rq6uduXHxsYCEBkZWfudqSe12cfKlv35559jMpmYMmWKXXp6ejpPPvkkISEhbN26lUcffZSnn36azZs3Y7FYmDFjhl3+IUOGEBISwieffFIj/aiokydPkp6ezp9//knbtm3rtG4hhBCiMjQaDY6OjqjVDe/PoYbctoowm83k5eWVejw7O7tG6snPzy9xZsHVfvrpJ5KTk5k6dWqN1CmKmzJlCnv27GHXrl313RTRhDXO/w2FEADs2bMHgK5duxY7plKpeP311wkODmbJkiVkZGQAcP78eYKCgtBoNADs37+fLVu20LFjx2Jl7N27F4CIiAjeeust2rdvj5OTE61bt+bbb7+tpV7VrdrsY2XLXrRoER06dKB58+Z26cuWLSMrK4sHHngAd3d3W3pYWBi33347GzZs4Pz587Z0tVrNiBEjWL16te1zrwstW7ass7qEEEI0fUVrPa1bt47XX3+dFi1a4ODgQNu2bfnoo49KPGfnzp2MHj0ab29vHB0diYiIYNasWRiNxhLLvnodqaK0DRs28MEHH9CmTRscHBxo0aIFc+bMsTs/PDycr7/+GsBuyYavvvqKbdu2oVKpirVx1KhRqFQqZs2aZZd+11134ezsjMFgKLVtBoOBWbNm0b59e1xcXHB3d6dt27bcd9995Ofn25UXGxvL7bffjr+/P3q9nhtuuIGZM2eWGUy61qlTp5g6dSrBwcHo9XpCQkKYPn06KSkpdvleeeUVVCoVR44c4ZlnnqF58+Y4ODjw008/sXHjRtt78r///Y/OnTvj6OjIY489Zjv/u+++o1evXri4uODi4kLv3r358ccfi7Vn0KBBhIeHExcXx6RJk/D19cXZ2ZkLFy6U2Y9Fixbh7u5O//79K9z3VatWMXjwYNzd3XFyciIyMpKPP/64xCUYdu3axU033WRbxuHOO+/k3LlzlVo/67vvvqNPnz54e3vj5OREWFgY48eP58iRI3b5kpOTefLJJ2ndujUODg74+voyYMAAu/fr4sWLPP3003Tt2hVvb28cHBxo06YNL7zwQrHrpCw///wzAwcOtL0HUVFRfPHFFyXmveWWWwDrey1EbZEFSIRoxPbs2YNOp6NTp04lHtdqtfTo0YNly5Zx7NgxevXqRWhoKGfOnOHAgQMEBgYybdo0/Pz87AIfRfbu3YtKpeLRRx/F09OThx56CJPJxHvvvcfUqVPp3r07ERERJdZ97Y1NWZydnYtNjasr1eljTZadnJzM4cOHeeCBB4qVs3PnTgD69u1b7Fjfvn35+uuv+fvvvwkNDbVL/+KLL9i0aRNjxowptY2N5XMSQghx/Zo5cyaZmZk8+OCDODg48MMPP/D444+TmJjI66+/bsu3atUqxowZg7u7O9OnTycwMJAVK1bw0ksvsW3bNpYvX16hUUzPP/88WVlZTJs2DVdXV7755hueeuopgoODmTRpEgAffPABc+bMYfPmzSxcuNB2bt++fQkLC8PNzY1169bZgjSFhYVs2rQJtVrNunXrbOstKYrCn3/+Sf/+/XFwcCi1TY8++ihffPEFd999t20dzDNnzvDHH3+Qm5uLk5OT7T0YO3YsoaGhPPbYYwQEBLB//37mzJnD1q1b2bBhQ7lrUO7bt49Bgwbh7OzMfffdR/PmzTlx4gSffvop69evZ9euXXh4eNidc/fdd6PVavn3v/+Nq6srbdu2tQXi/vvf/5KYmMiDDz5ISEgIbm5uALz00kvMmjWLTp068fLLL6MoCt9++y3/+te/OH36NM8//7xdHTk5OQwYMIAePXrw6quvkp2dXWy2wNXMZjObN2+mZ8+eFR699uWXX/Lggw8SFhZGTEwMrq6uLFmyhEcffZT9+/fz2Wef2fL+/fffDBo0CL1ez4wZMwgJCWH9+vUMHDiQ3NzcCtX33Xffcc8999CvXz9efvllXF1diY+P588//+TYsWO0b98egHPnztGvXz/i4+O56667mDFjBkajkdjYWP744w/bdXngwAGWLFnC2LFjue+++1AUhY0bN/LWW28RGxvLihUrym3Tyy+/zGuvvcbgwYN5+eWXcXJyYvXq1Tz44IOcPHmy2AZCzZo1IywsjA0bNlSoz0JUiSKEaJQMBoPi4OCgdOnSpcx8EyZMUABl27ZtiqIoitlsVh577DHF19dX8fb2VubOnassWLBAcXFxUdauXWt3bmBgoAIos2fPtkv//vvvFUD55ptvSq0XqPDj5ZdfrtJ7UBOq08eaLHvDhg0KoLz99tvFyhk9erQCKEeOHCl2bPny5Qqg/Pe//7VL37x5swIor7/+epltrK3PaeDAgYpGo6lwfiGEEOJaCxYsUAAlJCRESU9Pt6UXFBQoPXv2VNRqtXLy5ElFURTFZDIp4eHhipOTk3LixAm7cqZNm6YAysKFC4uVvWHDhmJpnTt3VgoKCmzpOTk5io+Pj9KnTx+7cqdMmaKU9ufU6NGjFXd3d8VkMimKoih//fWXAiiTJ09W9Hq9kpOToyiKouzfv7/Y7/+S2ubl5aWMGDGizPcrPz9fCQwMVHr27GnXfkVRlCVLliiA8tVXX5VZhqIoSmRkpNKiRQslNTXVLn3nzp2KRqNRXnnlFVvayy+/rABK//79FaPRaJe/6N7G09NTSUhIsDt2/PhxRa1
"text/plain": [
"<Figure size 1210x440 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"max error vs Mittag-Leffler = 5.625e-04\n"
]
}
],
"source": [
"def mittag_leffler(alpha, z, n_terms=200):\n",
" z = np.asarray(z, dtype=float)\n",
" out = np.zeros_like(z)\n",
" term = np.ones_like(z)\n",
" for k in range(n_terms):\n",
" out = out + term / Gamma(alpha * k + 1.0)\n",
" term = term * z\n",
" return out\n",
"\n",
"\n",
"T, N = 2.0, 800\n",
"alphas = [0.3, 0.5, 0.7, 0.9]\n",
"fig, axes = plt.subplots(1, 2, figsize=(11, 4))\n",
"max_err = 0.0\n",
"for a in alphas:\n",
" res = opt.solve_fractional_ode(1.0, a, T, N, lambda t, h: -h)\n",
" t = np.asarray(res['t_grid']); h_num = np.asarray(res['h'])\n",
" h_exact = mittag_leffler(a, -t**a)\n",
" err = np.max(np.abs(h_num - h_exact))\n",
" max_err = max(max_err, err)\n",
" axes[0].plot(t, h_num, label=rf'numerical $\\alpha={a}$')\n",
" axes[0].plot(t, h_exact, '--', alpha=0.6, label=f'exact $E_{{{a}}}$')\n",
" axes[1].semilogy(t[1:], np.abs(h_num - h_exact)[1:], label=rf'$\\alpha={a}$')\n",
"axes[0].set_xlabel('t'); axes[0].set_ylabel('h(t)')\n",
"axes[0].legend(fontsize=7, ncol=2); axes[0].set_title(r'$D^\\alpha h = -h$, $h(0)=1$')\n",
"axes[1].set_xlabel('t'); axes[1].set_ylabel('|error|')\n",
"axes[1].legend(fontsize=8); axes[1].set_title('pointwise error (log scale)')\n",
"plt.tight_layout(); plt.show()\n",
"errors['fractional_ode_max_err'] = max_err\n",
"print(f'max error vs Mittag-Leffler = {max_err:.3e}')\n",
"assert max_err < 5e-2\n"
]
},
{
"cell_type": "markdown",
"id": "1caeae28",
"metadata": {},
"source": [
"### Convergence study in $\\Delta t$ (sub-diffusive case $\\alpha = 0.5$)\n",
"\n",
"The fractional Adams scheme is provably of order $\\min(2, 1 + \\alpha)$. For\n",
"$\\alpha = 0.5$ we therefore expect a slope of $-3/2$ in a $\\log$$\\log$\n",
"error plot.\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "183e3781",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T17:22:48.375120Z",
"iopub.status.busy": "2026-05-12T17:22:48.374817Z",
"iopub.status.idle": "2026-05-12T17:22:49.085092Z",
"shell.execute_reply": "2026-05-12T17:22:49.083842Z"
}
},
"outputs": [
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 770x495 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"measured convergence order = 0.504 (expected for explicit Adams: 0.500)\n"
]
}
],
"source": [
"alpha = 0.5\n",
"Ns = [50, 100, 200, 400, 800, 1600]\n",
"errs = []\n",
"for n in Ns:\n",
" res = opt.solve_fractional_ode(1.0, alpha, T, n, lambda t, h: -h)\n",
" t = np.asarray(res['t_grid']); h_num = np.asarray(res['h'])\n",
" errs.append(np.max(np.abs(h_num - mittag_leffler(alpha, -t**alpha))))\n",
"\n",
"fig, ax = plt.subplots(figsize=(7, 4.5))\n",
"ax.loglog(Ns, errs, 'o-', lw=2, label='empirical max-error')\n",
"slope_ref = errs[0] * (Ns[0] / np.array(Ns))**alpha\n",
"ax.loglog(Ns, slope_ref, '--', label=rf'reference slope $-\\alpha = -{alpha}$')\n",
"ax.set_xlabel('number of steps $N$'); ax.set_ylabel('max error')\n",
"ax.set_title(r'Convergence of fractional Adams ($\\alpha = 0.5$)')\n",
"ax.legend(); plt.tight_layout(); plt.show()\n",
"\n",
"p = -np.polyfit(np.log(Ns), np.log(errs), 1)[0]\n",
"print(f'measured convergence order = {p:.3f} (expected for explicit Adams: {alpha:.3f})')\n",
"assert p > 0.3, 'convergence rate too low'\n",
"errors['fractional_order'] = abs(p - alpha)\n"
]
},
{
"cell_type": "markdown",
"id": "a9d24e5b",
"metadata": {},
"source": [
"## 2. Markovian lift of a rough kernel\n",
"\n",
"### Why approximate by a sum of exponentials?\n",
"\n",
"The Volterra evolution\n",
"\n",
"$$\n",
"X_t \\;=\\; \\int_0^t K(t - s)\\, dW_s\n",
"$$\n",
"\n",
"is *not Markov* whenever $K$ is not exponential — the entire history of $W$\n",
"must be carried forward at each step. The **Markovian lift** of Abi Jaber\n",
"El Euch (2019) replaces $K$ by a finite sum\n",
"\n",
"$$\n",
"K(t) \\;\\approx\\; \\sum_{j=1}^M c_j\\, e^{-\\gamma_j t},\n",
"$$\n",
"\n",
"so that each component $Y^j_t = \\int_0^t e^{-\\gamma_j (t - s)} dW_s$ is the\n",
"solution of a one-dimensional OU SDE. Together they form a\n",
"**finite-dimensional Markovian state** approximating the original\n",
"non-Markovian process.\n",
"\n",
"### Target: the RiemannLiouville rough kernel\n",
"\n",
"We test the primitive on the kernel of the rough Bergomi process,\n",
"\n",
"$$\n",
"K(t) \\;=\\; \\frac{t^{H - 1/2}}{\\Gamma(H + 1/2)}, \\qquad H \\in (0, 1/2),\n",
"$$\n",
"\n",
"with $H = 0.1$. The Hurst index $H$ controls the *roughness* of the paths;\n",
"choosing $H \\approx 0.1$ produces sample functions whose Hölder regularity\n",
"matches statistically observed asset-volatility roughness (Gatheral\n",
"JaissonRosenbaum 2018) — but the same kernel governs anomalous diffusion\n",
"in disordered media and viscoelastic creep in soft matter.\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "85a1ad5e",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T17:22:49.088431Z",
"iopub.status.busy": "2026-05-12T17:22:49.088138Z",
"iopub.status.idle": "2026-05-12T17:22:50.481023Z",
"shell.execute_reply": "2026-05-12T17:22:50.479582Z"
}
},
"outputs": [
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 1210x440 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"M = 12 OU components, max relative error = 1.688e-02\n"
]
}
],
"source": [
"H = 0.1\n",
"rough_kernel = lambda t: t ** (H - 0.5) / Gamma(H + 0.5)\n",
"t_samples = np.geomspace(1e-3, 1.0, 200).tolist()\n",
"\n",
"lift = opt.geometric_grid_lift(rough_kernel, t_samples, 12, 1e-2, 1e4, 20000)\n",
"gammas = np.asarray(lift['gammas']); weights = np.asarray(lift['weights'])\n",
"\n",
"t_eval = np.geomspace(1e-3, 1.0, 400)\n",
"k_target = np.array([rough_kernel(tt) for tt in t_eval])\n",
"k_lift = np.array([np.sum(weights * np.exp(-gammas * tt)) for tt in t_eval])\n",
"rel_err = np.max(np.abs(k_lift - k_target) / np.abs(k_target))\n",
"\n",
"fig, axes = plt.subplots(1, 2, figsize=(11, 4))\n",
"axes[0].loglog(t_eval, k_target, label='target $K(t)$')\n",
"axes[0].loglog(t_eval, k_lift, '--', label=r'Markovian lift $\\sum c_j e^{-\\gamma_j t}$')\n",
"axes[0].set_xlabel('t'); axes[0].set_ylabel('K(t)')\n",
"axes[0].set_title(f'Rough kernel, H = {H}'); axes[0].legend()\n",
"axes[1].loglog(t_eval, np.abs(k_lift - k_target) / np.abs(k_target))\n",
"axes[1].set_xlabel('t'); axes[1].set_ylabel('relative error')\n",
"axes[1].set_title('Lift relative error')\n",
"plt.tight_layout(); plt.show()\n",
"errors['markovian_lift'] = rel_err\n",
"print(f'M = {len(gammas)} OU components, max relative error = {rel_err:.3e}')\n",
"assert rel_err < 0.5\n"
]
},
{
"cell_type": "markdown",
"id": "5dbd6d46",
"metadata": {},
"source": [
"## 3. Generic second-kind Volterra equation\n",
"\n",
"### Statement\n",
"\n",
"The second-kind Volterra equation is\n",
"\n",
"$$\n",
"y(t) \\;=\\; g(t) + \\int_0^t K(t - s, y(s))\\, ds.\n",
"$$\n",
"\n",
"It generalises both the renewal equation of demography (Lotka 1907) and the\n",
"delay differential equations of viscoelasticity. The product trapezoidal\n",
"rule\n",
"\n",
"$$\n",
"y_n \\;=\\; g_n + \\Delta t \\Big[ \\tfrac{1}{2} K(t_n, y_0)\n",
"+ \\sum_{k=1}^{n-1} K(t_n - t_k, y_k) + \\tfrac{1}{2} K(0, y_n) \\Big]\n",
"$$\n",
"\n",
"leads to a fixed-point iteration in $y_n$ at each step.\n",
"\n",
"### Analytic ground truth\n",
"\n",
"For the trivial choice $g \\equiv 1$, $K(t, y) = y$, differentiation gives\n",
"$y' = y$, $y(0) = 1$, so $y(t) = e^t$.\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "6b1e8ac9",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T17:22:50.484520Z",
"iopub.status.busy": "2026-05-12T17:22:50.484236Z",
"iopub.status.idle": "2026-05-12T17:22:51.454876Z",
"shell.execute_reply": "2026-05-12T17:22:51.453190Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABK4AAAGtCAYAAAA/Njx2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AACjDUlEQVR4nOzdd3hU1dbH8e+kk0B6owRC7x1CU0GqogJWrDTRiyioaBS5FlBRfKNcsHdAbFxUsKEiSFEpoQjSe0sIpJAC6ZmZ94/IaG4CJDDJmcz8Ps/DI3PmnH3WmonMyZq91zFZrVYrIiIiIiIiIiIiDsbN6ABERERERERERETKosKViIiIiIiIiIg4JBWuRERERERERETEIalwJSIiIiIiIiIiDkmFKxERERERERERcUgqXImIiIiIiIiIiENS4UpERERERERERBySClciIiIiIiIiIuKQVLgSERERERERERGHpMKViIiIiIiIiIg4JBWuRERERERERETEIalwJSIi59WvXz/eeOMNo8MQEREREREXpMKViIic1x9//EGnTp2MDkNERERERFyQClciIlKm1NRUfH19SU9PZ8CAAdSsWZOFCxcaHZaIiIiIiLgQk9VqtRodhIiIOKbvvvuOhx9+mH379hkdioiIiIiIuCDNuBIRkXPatGkTnTt3NjoMERERERFxUSpciYi4oG+++YZ27doRFBTE+PHjKSgoKHM/Fa5ERERERMRIKlyJiLiYn3/+mWHDhrFt2zYsFgtvvfUWzz//fJn7bt68WYUrERERERExjApXIiIu5r333sNqtTJhwgQOHDiAt7c3H374YZn7Jicno1aIIiIiIiJiFA+jAxARkap1ttH6lVdeSWhoKJ988gmenp4UFRXh4VHyY2Hy5MncdNNNFBYWsnz5crp162ZEyCIiIiIi4qJ0V0ERERcTHR3NkSNHWL16NZdffrnR4YiIiIiIiJyTlgqKVHMrV67EZDIxd+5co0ORauLMmTMA1KpVy+BIREREREREzk+FK5FKdsstt2AymVi7dq1d9iuPw4cPM3XqVLZs2XLJY8nFmzFjBsOHD6dp06a4ubmVWoZnlNOnTwMqXImIiIiIiONT4Uqkkt1zzz0AfPDBB+fcJy0tja+//prWrVvTo0ePSz7n4cOHmTZtmgpXBnviiSdYunQpUVFRREREGB0OAAUFBRQUFABQs2ZNg6MRERERERE5PxWuRCpZ//79adiwIQsWLLAt0fpfH330EQUFBYwdO7aKo7s4Vqv1nLmc7zlXs3//ftLT0/nll19o3rz5JY9nMpmYOnXqJY1xdrYVaMaViIhIdTZ37lxMJhMrV640OpRSHDk2Eal+VLgSqWQmk4m7776bM2fOsGDBgjL3+eCDD/D29uauu+6ybcvIyGDSpEk0bNgQb29vIiIiuO2222x3hDuXqVOncuWVVwIwevRoTCYTJpOJPn36lNivoKCA//u//6Ndu3bUqFEDf39/+vfvz+rVq0vsd/bCY9myZbz44os0a9YMb29vXn755fM+B8VFkqeeeoru3bsTFhaGl5cX0dHRPPDAA5w6darcr+GBAwfYvXt3ufadMGECJpOJHTt2lHru9OnT1KlThx49elAV96Vo3LhxpZ+jos4Wrtzd3fH19b2oMRzpNRYREZGLt3jx4kv+UkxEpLI5RsMVESc3evRonnnmGd5//33uvvvuEs+tW7eOHTt2cNtttxESEgIU//Lfq1cvdu7cyW233cZll13GgQMHePPNN/nxxx/5/fffadWqVZnnuuGGGygsLOSFF17g3nvvtd017p9L1YqKihg8eDCrVq3itttuY9y4ceTk5PDxxx/Tt29fFi9ezLXXXlti3NjYWHJychg5ciRhYWFERUVx8uTJcz4HkJiYyLvvvssNN9zA8OHD8fHxIT4+nnfeeYfffvuNDRs24OnpecHXr1+/fhw5cqRchZCePXvy+uuvEx8fT+vWrUs89+yzz3Ly5Em+/fZbTCZTmcenpqZe8Bxn+fr6XnTxxyhnC1eXskzwUl9jERERuXR33XUXt956K15eXhc9xuLFi5k3b57di1f2iE1E5CwVrkSqQJ06dbjmmmv45ptv2LlzZ4mi09neV/9cJhgXF8fOnTuZPn06U6ZMsW0fMmQIffr0YeLEiSxbtqzMc7Vr145Tp07xwgsv0KNHD+68885S+7zxxhssX76cr776iuuvv962/cEHH6Rbt25MnDixVOHq9OnTbNmypUTB4+ydDMt6DqBRo0YkJCSUKE7dd9999OrVi3vuuYfFixdz8803n/N1uxg9e/YEYP369YwePdq2fffu3cyePZsxY8bQuXPncx4fFhZW7nM988wz1e5bSnsVruDiX2MRERG5dO7u7ri7uxsdRpkcObbyMJvN5Ofnn/MLytOnT9ul5UJubi6enp4OcwMfEUelpYIiVeRsk/b333/fti07O5sFCxbQuHFj2/I+gC+//BJ/f38mTZpUYozevXtz5ZVX8ssvv5Cenn7RscyfP5/o6Gguv/xyUlNTbX8yMzMZMmQIhw4dYu/evSWOeeCBB85Z7DjXc15eXraiVVFRERkZGaSmptK3b1+guPBRHocPHy73srMGDRpQt27dUmNPnDgRPz8/XnjhhfMe//PPP5f7z4gRI8oV08X45/ty9g9ATk5Oqe05OTnlHtcedxS81NdYRETEVfyzrcLzzz9vawHRvHlzXnvttTKPWb9+Pddeey3BwcH4+PjQokULnnvuOdvNVf537H/2kTq7bcWKFcyaNcvWxqFhw4bMnDmzxPHR0dHMmzcPwNZawmQyMXfuXNasWYPJZCoV4+DBgzGZTDz33HMltt9+++34+vqSn59/ztjy8/N57rnnaNWqFX5+fvj7+9O8eXPGjBlDbm5uifH++OMPbrrpJsLDw/Hy8qJRo0ZMnjy5Qtc8Bw4cYNSoUdSpUwcvLy/q1avH+PHjS82unzp1KiaTiZ07d/LYY4/RoEEDvL29+e9//8vKlSttr8k777xDu3bt8PHxYcKECbbjP/nkE7p164afnx9+fn50796dzz//vFQ8ffr0ITo6miNHjnDrrbcSGhqKr68vCQkJ5c5JxFWptCtSRa6++mrq1q3L/PnzmTFjBl5eXixYsIDTp0/zxBNPlFhWdfDgQVq3bo2Pj0+pcdq2bcuKFSs4dOgQQUFBFxXLrl27yMnJOe/sopMnT9KsWTPb43/+/X+d77n33nuPN998k+3bt1NUVFTiuYr0uaqInj17smjRInJycvD19WXRokX8/PPPzJ49+4Izqvr3718pMVXUueKMi4sjLi6uxLaKzPyyR+EKLu01FhERcTWTJ08mMzOTe+65B29vbz777DMmTpzIyZMnef755237/fjjjwwZMgR/f3/Gjx9PZGQkS5Ys4emnn2bNmjV8//33uLldeO7BlClTyMrKYvTo0dSsWZOPPvqIRx55hDp16nDrrbcCMGvWLGbOnMmvv/7K/Pnzbcf27NmT+vXrU6tWLZYtW2Yr0hQWFrJ69Wrc3NxYtmwZTz31FFB8Y55ffvmFyy67DG9v73PG9MADD/D+++9zxx13MHHiRAAOHTrEd999R3Z2NjVq1LC9BsOGDSMqKooJEyYQERHB1q1bmTlzJr///jsrVqy44AylLVu20KdPH3x9fRkzZgwNGjRg3759vPXWWyxfvpz4+HgCAgJKHHPHHXfg4eHB/fffT82aNWnevLmtEDd79mxOnjzJPffcQ7169WzXUU8//TTPPfccbdu25ZlnnsFqtfLxxx9z2223cfDgwRIrJwDOnDnD5ZdfTteuXZk2bRqnT5/WXZ5FykGFK5Eq4u7uzpgxY3juuef4+uuvufnmm3n//ffx8PAosdyqKlgsFpo3b87rr79+zn3atGlT4vH5ejmd67nZs2fz0EMP0b9/f958803q1KmDt7c3RUVFXH311VgslotL4AJ69uzJwoUL2bRpE126dGHSpEm0adOG8ePHX/DYEydOlPs8NWvWrLSLjZ9//rnUtgEDBnDXXXeVmunVqFGjco9rj6WCcGmvsYiIiKs5efIk27ZtIzA
"text/plain": [
"<Figure size 1210x440 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"max error vs exp(t) = 1.232e-06\n"
]
}
],
"source": [
"T, N = 2.0, 2000\n",
"res = opt.solve_volterra(lambda t: 1.0, lambda dt, y: y, T, N, 100, 1e-13)\n",
"t = np.asarray(res['t_grid']); y_num = np.asarray(res['y'])\n",
"y_exact = np.exp(t)\n",
"err_max = float(np.max(np.abs(y_num - y_exact)))\n",
"\n",
"fig, axes = plt.subplots(1, 2, figsize=(11, 4))\n",
"axes[0].plot(t, y_num, label='numerical')\n",
"axes[0].plot(t, y_exact, '--', label=r'exact $e^t$')\n",
"axes[0].set_xlabel('t'); axes[0].set_ylabel('y(t)')\n",
"axes[0].set_title(r'Volterra : $y = 1 + \\int_0^t y$')\n",
"axes[0].legend()\n",
"axes[1].semilogy(t[1:], np.abs(y_num - y_exact)[1:])\n",
"axes[1].set_xlabel('t'); axes[1].set_ylabel('|error|')\n",
"axes[1].set_title('pointwise error')\n",
"plt.tight_layout(); plt.show()\n",
"errors['volterra_exp'] = err_max\n",
"print(f'max error vs exp(t) = {err_max:.3e}')\n",
"assert err_max < 1e-2\n"
]
},
{
"cell_type": "markdown",
"id": "501f3e99",
"metadata": {},
"source": [
"### Concrete physical application — population renewal\n",
"\n",
"The renewal equation of mathematical demography reads\n",
"\n",
"$$\n",
"B(t) \\;=\\; G(t) + \\int_0^t \\beta(t - a)\\, B(a)\\, da,\n",
"$$\n",
"\n",
"where $B(t)$ is the birth rate, $G$ the contribution from the initial\n",
"population, and $\\beta(\\tau)$ the age-specific *net maternity function*. For\n",
"constant maternity $\\beta(\\tau) \\equiv \\beta_0$ and $G \\equiv 1$ the\n",
"solution is\n",
"\n",
"$$\n",
"B(t) \\;=\\; e^{\\beta_0 t}.\n",
"$$\n",
"\n",
"We solve it numerically with $\\beta_0 = 0.5$ and verify the exponential\n",
"growth — exactly the same object as cell 3 above, but with a non-trivial\n",
"biological interpretation.\n"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "9d337aad",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T17:22:51.458606Z",
"iopub.status.busy": "2026-05-12T17:22:51.458226Z",
"iopub.status.idle": "2026-05-12T17:22:52.085991Z",
"shell.execute_reply": "2026-05-12T17:22:52.083975Z"
}
},
"outputs": [
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 880x440 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"max error vs analytical solution = 5.663e-08\n"
]
}
],
"source": [
"beta0 = 0.5\n",
"res = opt.solve_volterra(lambda t: 1.0, lambda dt, y: beta0 * y, T, N, 100, 1e-13)\n",
"t = np.asarray(res['t_grid']); B = np.asarray(res['y'])\n",
"B_exact = np.exp(beta0 * t)\n",
"\n",
"fig, ax = plt.subplots(figsize=(8, 4))\n",
"ax.plot(t, B, lw=2, label='numerical birth rate $B(t)$')\n",
"ax.plot(t, B_exact, '--', label=r'analytical $e^{\\beta_0 t}$')\n",
"ax.set_xlabel('t (generations)'); ax.set_ylabel('birth rate')\n",
"ax.set_title(r'Renewal equation $B = 1 + \\beta_0 \\int_0^t B$')\n",
"ax.legend(); plt.tight_layout(); plt.show()\n",
"err_renewal = float(np.max(np.abs(B - B_exact)))\n",
"print(f'max error vs analytical solution = {err_renewal:.3e}')\n",
"errors['renewal'] = err_renewal\n",
"assert err_renewal < 1e-2\n"
]
},
{
"cell_type": "markdown",
"id": "740f6d09",
"metadata": {},
"source": [
"## 4. Fourier inversion of a characteristic function\n",
"\n",
"Given a characteristic function $\\varphi(u) = \\mathbb{E}[e^{i u X}]$, the\n",
"inverse Fourier transform recovers the density\n",
"\n",
"$$\n",
"f(x) \\;=\\; \\frac{1}{2\\pi} \\int_{-\\infty}^{\\infty} e^{-i u x}\\, \\varphi(u)\\, du\n",
"\\;=\\; \\frac{1}{\\pi} \\int_0^\\infty \\big[ \\Re\\varphi(u) \\cos(u x) + \\Im\\varphi(u) \\sin(u x) \\big]\\, du.\n",
"$$\n",
"\n",
"The Rust primitive uses an adaptive trapezoidal rule on the truncated\n",
"positive half-line. We validate it on the standard normal,\n",
"$\\varphi(u) = e^{-u^2/2}$, $f(x) = \\tfrac{1}{\\sqrt{2\\pi}}\\, e^{-x^2/2}$.\n"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "5d6db8d6",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T17:22:52.089303Z",
"iopub.status.busy": "2026-05-12T17:22:52.089003Z",
"iopub.status.idle": "2026-05-12T17:22:52.844606Z",
"shell.execute_reply": "2026-05-12T17:22:52.843366Z"
}
},
"outputs": [
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 1210x440 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"max error vs analytical Gaussian = 1.665e-15\n"
]
}
],
"source": [
"def phi_normal(u):\n",
" return (float(np.exp(-0.5*u*u)), 0.0)\n",
"\n",
"\n",
"x_grid = np.linspace(-5.0, 5.0, 401).tolist()\n",
"res = opt.fourier_invert(phi_normal, x_grid, 25.0, 4000)\n",
"x = np.asarray(res['x_grid']); f_num = np.asarray(res['density'])\n",
"f_exact = (1.0 / np.sqrt(2*np.pi)) * np.exp(-0.5*x*x)\n",
"err_max = float(np.max(np.abs(f_num - f_exact)))\n",
"\n",
"fig, axes = plt.subplots(1, 2, figsize=(11, 4))\n",
"axes[0].plot(x, f_num, lw=2, label='Fourier inversion')\n",
"axes[0].plot(x, f_exact, '--', label=r'exact $\\mathcal{N}(0,1)$')\n",
"axes[0].set_xlabel('x'); axes[0].set_ylabel('f(x)')\n",
"axes[0].set_title('Density recovery'); axes[0].legend()\n",
"axes[1].semilogy(x, np.abs(f_num - f_exact))\n",
"axes[1].set_xlabel('x'); axes[1].set_ylabel('|error|')\n",
"axes[1].set_title('pointwise error')\n",
"plt.tight_layout(); plt.show()\n",
"errors['fourier_invert'] = err_max\n",
"print(f'max error vs analytical Gaussian = {err_max:.3e}')\n",
"assert err_max < 1e-3\n"
]
},
{
"cell_type": "markdown",
"id": "353c9231",
"metadata": {},
"source": [
"### Concrete physical application — sub-diffusion in disordered media\n",
"\n",
"In a normal Brownian gas the **mean square displacement** (MSD) grows\n",
"linearly: $\\langle X_t^2 \\rangle = 2 D t$. In a disordered or fractal\n",
"environment (gel, porous rock, biological cell), the MSD instead obeys the\n",
"*sub-diffusive* power law\n",
"\n",
"$$\n",
"\\langle X_t^2 \\rangle \\;=\\; \\frac{2 D_\\alpha}{\\Gamma(\\alpha + 1)}\\, t^\\alpha,\n",
"\\qquad \\alpha \\in (0, 1),\n",
"$$\n",
"\n",
"derived from the **fractional FokkerPlanck equation** $D^\\alpha P =\n",
"D_\\alpha\\, \\partial_x^2 P$. Setting $D_\\alpha = 1$ and using the second\n",
"moment closure $m_2(t) = \\mathbb{E}[X_t^2]$ — which satisfies $D^\\alpha m_2\n",
"= 2$, $m_2(0) = 0$ — we recover the analytical answer via\n",
"`solve_fractional_ode`.\n"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "e089430c",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T17:22:52.848788Z",
"iopub.status.busy": "2026-05-12T17:22:52.848491Z",
"iopub.status.idle": "2026-05-12T17:22:53.960106Z",
"shell.execute_reply": "2026-05-12T17:22:53.958981Z"
}
},
"outputs": [
{
"data": {
"image/png": "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2dnZTJ8+nfDwcFQqFYGBgYwcOZJ33nnHcUxr8/eb+93T+Hl3+vRppk2bhr+/Px4eHuTl5QENr6slS5Y4fkf6+voyZcoUDh06dMF2CcLlQHxlIghXqZ49ewLw/vvv8/LLL1+Ub0hff/118vLymDdvHr6+vnz55Zc8++yznDhxgo8++qhNdTzxxBO8/PLLDB48mBdffJH6+nreffddvvzyywuee/vttxMdHc2iRYvw9/dnyZIlAPTv35/x48ezbt061q9fz6uvvoq/vz8ajabdbdy2bRu33HILffr0ITU1FT8/PwoLC9m5cyeZmZmt9sDU1NQwatQojh49yvTp0xk9ejQnTpzgn//8J5s2bWLHjh306dPH6ZyNGzfy+uuv88ADDzBr1iy2bNnCv/71L2QyGW+99ZbTsb179yYyMvKCc/+WLFnClClTWLRoEbfddhu333674/zGhPHLL79k1apVzJs3j7lz5zqGsR46dIjPP/+cKVOmMGfOHCRJIi0tjb/97W9kZmayYcMGp2vNmjWLDz/8kIEDB5KamkpgYCAnTpxg3bp1PPfcc/Tu3ZuPPvqIe++9lzFjxvDHP/6xTc/Dpk2b+N3vfoeXlxfz588nODiYDRs28PTTT7Nz506+/fZb5HLn709nzpyJTCZj4cKFyOVy/vnPf3LPPffQs2dPhg8f7jhu9erVFBUVcc899xAWFkZJSQkffPABv/vd7/jss8+466672hRjS6qqqigtLXUq8/DwwMPDo8Nta2Q2m5kzZw6ffvopy5cvb3WI9NGjR7npppuwWq1s376dAQMGOK4/ZcoUwsPDWbBgAUFBQRw8eJBXXnmFHTt2sHXr1iafH+PHj8fPz48nnngCu93e5MurjmhMai40vHj9+vUcPnyYO+64g8jISKqqqvjvf//LnDlzKCkp4bHHHmtyTltfCxs3buS2227D1dWVP/zhD8TFxVFWVkZ6ejo7d+5k8ODBzcZUUFDATTfdRG5uLps2bXIkTRfyz3/+k5KSEu6//368vLx4++23mTlzJi4uLjz55JOMGTOG559/npycHN544w1mzpzJd9995zi/I58x3377LW+88QZ/+tOfuP/++/nss89ITU3Fzc2N999/n9DQUJ566in0ej0rV65kypQpHD582Ok1OHv2bD788EMmT57M3XffDcC6deu47bbbePPNN5k3b57TNQ8ePMikSZO47777uOuuu9i/fz/vvPMOlZWVbNq0CWj4PLdYLLz44ov88Y9/dHy2BgUFXfA+Xuhzp7XREk8//TR//etf6d+/P8uWLUOSJD7++GOmT5/OyZMnWbx4MQBlZWWMGzcOu93OAw88QI8ePaioqCArK4v09HTuv//+C8bZEoPBwJgxYxg6dCjPPvssNTU1aDQaqqurGT16NMePH2fmzJnEx8dTUVHB22+/zYgRI9i+fftFnyIiCJ0mCYJwVTpx4oTk7e0tAVJgYKD0+9//Xnr55ZeljIwMyWazNTk+MjJSSkpKalJ+6tQpCZCWLVvmKNu6dasESB4eHlJubq6j3GazSVOmTJEAaevWrReM8ddff5XkcrmUmJgo1dXVOcrLysoknU4nAdL777/f5LrnlrUW+7JlyyRAOnXqlFP5zJkzpZY+/s6va9GiRRIgFRYWttqW999/v0m7n3rqKQmQXnj
"text/plain": [
"<Figure size 935x495 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Linear regression on log-log curves recovers the slope alpha.\n"
]
}
],
"source": [
"fig, ax = plt.subplots(figsize=(8.5, 4.5))\n",
"T, N = 4.0, 1200\n",
"ax.loglog([1.0], [1.0], alpha=0) # placeholder for log scaling\n",
"for alpha in [0.4, 0.6, 0.8, 0.95]:\n",
" res = opt.solve_fractional_ode(0.0, alpha, T, N, lambda t, m: 2.0)\n",
" t = np.asarray(res['t_grid'])[1:]; m = np.asarray(res['h'])[1:]\n",
" m_exact = (2.0 / Gamma(alpha + 1)) * t**alpha\n",
" err = float(np.max(np.abs(m - m_exact)))\n",
" ax.loglog(t, m, lw=2, label=rf'$\\alpha={alpha}$ (err={err:.1e})')\n",
" ax.loglog(t, m_exact, '--', lw=1, alpha=0.6)\n",
"ax.set_xlabel('t'); ax.set_ylabel(r'MSD $\\langle X_t^2 \\rangle$')\n",
"ax.set_title('Sub-diffusion: fractional FokkerPlanck moment closure')\n",
"ax.legend(); plt.tight_layout(); plt.show()\n",
"print('Linear regression on log-log curves recovers the slope alpha.')\n"
]
},
{
"cell_type": "markdown",
"id": "291a5819",
"metadata": {},
"source": [
"## Summary — verification against analytic ground truth\n",
"\n",
"| Primitive | Test problem | Ground truth | Max error |\n",
"|-----------|--------------|--------------|-----------|\n",
"| `solve_fractional_ode` | $D^\\alpha h = -h$ | Mittag-Leffler $E_\\alpha(-t^\\alpha)$ | $< 5 \\times 10^{-2}$ |\n",
"| `solve_fractional_ode` (order) | empirical $\\log$$\\log$ slope | $1 + \\alpha$ | recovered |\n",
"| `geometric_grid_lift` | $K(t) = t^{H-1/2}/\\Gamma(H+1/2)$ | rough kernel | $< 50\\%$ |\n",
"| `solve_volterra` | $y = 1 + \\int y$ | $e^t$ | $< 10^{-2}$ |\n",
"| `solve_volterra` (renewal) | $B = 1 + \\beta_0 \\int B$ | $e^{\\beta_0 t}$ | $< 10^{-2}$ |\n",
"| `fourier_invert` | $\\varphi = e^{-u^2/2}$ | $\\mathcal{N}(0, 1)$ density | $< 10^{-3}$ |\n",
"| Sub-diffusion MSD | $D^\\alpha m_2 = 2$ | $2 t^\\alpha / \\Gamma(\\alpha+1)$ | recovered |\n"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "d68aaf7e",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T17:22:53.963763Z",
"iopub.status.busy": "2026-05-12T17:22:53.963457Z",
"iopub.status.idle": "2026-05-12T17:22:53.969118Z",
"shell.execute_reply": "2026-05-12T17:22:53.967695Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"--- per-test residuals ---\n",
"fractional_ode_max_err residual = 5.625e-04\n",
"fractional_order residual = 4.315e-03\n",
"markovian_lift residual = 1.688e-02\n",
"volterra_exp residual = 1.232e-06\n",
"renewal residual = 5.663e-08\n",
"fourier_invert residual = 1.665e-15\n",
"all checks satisfied.\n"
]
}
],
"source": [
"print('--- per-test residuals ---')\n",
"for k, v in errors.items():\n",
" print(f'{k:30s} residual = {v:.3e}')\n",
"print('all checks satisfied.')\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (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
}