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

444 lines
324 KiB
Plaintext
Raw Normal View History

{
"cells": [
{
"cell_type": "markdown",
"id": "2f5c1703",
"metadata": {},
"source": [
"# `volterra` -- 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",
"Four CPU-only generic numerical primitives are demonstrated against analytic ground truths:\n",
"\n",
"1. **`solve_fractional_ode`** -- Caputo fractional Adams predictor-corrector (Diethelm-Ford-Freed 2002).\n",
"2. **`geometric_grid_lift`** -- multi-exponential approximation of a convolution kernel.\n",
"3. **`solve_volterra`** -- generic second-kind Volterra integral equation.\n",
"4. **`fourier_invert`** -- recover a probability density from its characteristic function.\n",
"\n",
"Each section displays the equation, calls the Rust primitive through the PyO3 bindings, plots numerical vs. analytic solutions, and asserts a tolerance bound."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "8464cdb2",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T09:45:33.974500Z",
"iopub.status.busy": "2026-05-12T09:45:33.973532Z",
"iopub.status.idle": "2026-05-12T09:45:35.800033Z",
"shell.execute_reply": "2026-05-12T09:45:35.798250Z"
}
},
"outputs": [],
"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",
"rng = np.random.default_rng(0)\n",
"errors = {}"
]
},
{
"cell_type": "markdown",
"id": "17129256",
"metadata": {},
"source": [
"## 1. Fractional Caputo Adams solver\n",
"\n",
"Solve, for $\\alpha \\in (0, 1)$,\n",
"\n",
"$$\n",
"D^{\\alpha} h(t) = F(t, h(t)), \\qquad h(0) = h_0,\n",
"$$\n",
"\n",
"with the predictor-corrector\n",
"\n",
"$$\n",
"h^{P}_{n+1} = h_0 + \\frac{\\Delta t^{\\alpha}}{\\alpha\\,\\Gamma(\\alpha)} \\sum_{k=0}^{n} \\big[(n+1-k)^{\\alpha} - (n-k)^{\\alpha}\\big]\\, F(t_k, h_k),\n",
"$$\n",
"\n",
"$$\n",
"h_{n+1} = h_0 + \\frac{\\Delta t^{\\alpha}}{\\Gamma(\\alpha + 2)}\\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",
"**Ground truth.** For the linear test equation $D^{\\alpha} h = -h$, $h(0) = 1$, the exact solution is the Mittag-Leffler function\n",
"\n",
"$$\n",
"h(t) = E_{\\alpha}(-t^{\\alpha}) = \\sum_{k=0}^{\\infty} \\frac{(-t^{\\alpha})^{k}}{\\Gamma(\\alpha k + 1)}.\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "3b101abb",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T09:45:35.805793Z",
"iopub.status.busy": "2026-05-12T09:45:35.805117Z",
"iopub.status.idle": "2026-05-12T09:45:37.554817Z",
"shell.execute_reply": "2026-05-12T09:45:37.552764Z"
}
},
"outputs": [
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 1100x400 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",
"T, N = 2.0, 800\n",
"alphas = [0.3, 0.5, 0.7, 0.9]\n",
"fig, ax = 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",
" ax[0].plot(t, h_num, label=f\"alpha={a} num\")\n",
" ax[0].plot(t, h_exact, '--', alpha=0.6, label=f\"alpha={a} exact\")\n",
" ax[1].semilogy(t[1:], np.abs(h_num - h_exact)[1:], label=f\"alpha={a}\")\n",
"ax[0].set_xlabel(\"t\"); ax[0].set_ylabel(\"h(t)\"); ax[0].legend(fontsize=7); ax[0].set_title(\"D^a h = -h, h(0)=1\")\n",
"ax[1].set_xlabel(\"t\"); ax[1].set_ylabel(\"|h_num - E_a(-t^a)|\"); ax[1].legend(fontsize=7); ax[1].set_title(\"pointwise error (log)\")\n",
"plt.tight_layout(); plt.show()\n",
"errors['solve_fractional_ode'] = max_err\n",
"assert max_err < 5e-2, f\"max err = {max_err}\"\n",
"print(f\"max error vs Mittag-Leffler = {max_err:.3e}\")"
]
},
{
"cell_type": "markdown",
"id": "3ec2f158",
"metadata": {},
"source": [
"## 2. Markovian lift on a geometric grid\n",
"\n",
"Approximate a convolution kernel admitting the integral representation\n",
"\n",
"$$\n",
"K(t) = \\int_{0}^{\\infty} e^{-\\gamma t}\\, \\nu(d\\gamma)\n",
"$$\n",
"\n",
"by\n",
"\n",
"$$\n",
"K(t) \\;\\approx\\; \\sum_{j=1}^{N} c_j\\, e^{-\\gamma_j t},\n",
"\\qquad c_j \\ge 0,\n",
"$$\n",
"\n",
"with rates on a geometric grid and weights fitted by non-negative least squares.\n",
"\n",
"**Target.** The rough kernel\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$."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "d1ce930a",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T09:45:37.562632Z",
"iopub.status.busy": "2026-05-12T09:45:37.561864Z",
"iopub.status.idle": "2026-05-12T09:45:39.631556Z",
"shell.execute_reply": "2026-05-12T09:45:39.630065Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABEEAAAGGCAYAAACUtJ9/AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAC8X0lEQVR4nOzdd3xTdffA8c9NmnRPuiiUtuy9h2wQEBBRRARRkKG4EEXEgf4UefQRJ6JSRXGguAAV5FFkygbZu+xVoIO2dO+M3x9pQktbaEvaNOl5v159aTPuPQmQ3Hvu+Z6jGI1GI0IIIYQQQgghhBAOTmXrAIQQQgghhBBCCCGqgiRBhBBCCCGEEEIIUSNIEkQIIYQQQgghhBA1giRBhBBCCCGEEEIIUSNIEkQIIYQQQgghhBA1giRBhBBCCCGEEEIIUSNIEkQIIYQQQgghhBA1giRBhBBCCCGEEEIIUSNIEkQIIYQQQgghhBA1giRBhBC88cYbKIpCYmJihZ7fp08fWrZsaeWoqkZ4eDjjx4+3dRhCCCFEtbRw4UIUReH8+fOW2/r06UOfPn2KPC4+Pp4RI0ZQq1YtFEVh7ty5VRqnmaIovPHGG1bdZkmvVwhhvyQJIkQlMh84mH+cnJyoU6cO48eP5/Lly7YOT5SDoig8/fTTJd5n/nPes2ePVfZ1+fJlRo4ciY+PD15eXtxzzz2cPXu2TM9ds2YNjzzyCC1btkStVhMeHm6VmIQQQogbee6551i9ejUzZsxg0aJFDBo0iJUrV1o9IVFZoqKieOONN4oke4QQjsnJ1gEIURP85z//ISIigpycHP79918WLlzI1q1bOXLkCC4uLrYOT1QjGRkZ9O3bl9TUVF555RU0Gg0fffQRvXv35sCBA9SqVeuGz//pp59YvHgx7du3JyQkpIqiFkIIUZOsWbOm2G3//PMP99xzD9OnT7fcNm/ePCIjI+0iERIVFcWsWbPo06dPsQsIJb1eIYT9kkoQIarA4MGDGTNmDI8++ihfffUV06dP58yZM6xYscLWodmFzMxMW4dQZT777DNOnTrFn3/+yYsvvshzzz3HmjVriI2N5cMPP7zp899++23S0tLYtm0bbdq0qYKIhRBC1DRarRatVlvktitXruDj42PV/RiNRrKzs626zYoo6fVWd6UdOxkMBnJycipl20LYC0mCCGEDPXv2BODMmTNFbv/nn3/o2bMn7u7u+Pj4cM8993Ds2LEijxk/fnyJSxzMfT0Ky87O5plnnsHf3x9PT0/uvvtuLl++XOp62ZSUFMaPH4+Pjw/e3t5MmDCBrKysCr3GNWvW4ObmxujRo9HpdAAcP36cESNG4Ofnh4uLCx07diyWCDIvLdm0aRNPPfUUgYGB1K1bF7jWeyQqKoq+ffvi5uZGnTp1eO+994rtPzc3l5kzZ9KwYUOcnZ0JDQ3lxRdfJDc3t0Kvp6r8+uuvdOrUiU6dOllua9q0Kf369WPJkiU3fX5ISAgajaYyQxRCCFHDFe6RYf7eNhqNREZGWpYAjx8/nsjISIAiS4NvJDw8nLvuuovVq1fTsWNHXF1d+eKLLwDTMcrUqVMJDQ3F2dmZhg0b8u6772IwGG64zQsXLvDUU0/RpEkTXF1dqVWrFvfff3+RZS8LFy7k/vvvB6Bv376WWDdu3Fjs9cbHx+Pk5MSsWbOK7evEiRMoisK8efMst1U0brO///7bcmzo6enJkCFDOHr0aJHHjB8/Hg8PD86cOcOdd96Jp6cnDz30EHBtOe+PP/5IixYtcHZ2ZtWqVQDs37+fwYMH4+XlhYeHB/369ePff/8tsu0bHZcJYa9kOYwQNmD+4vX19bXctm7dOgYPHkz9+vV54403yM7O5tNPP6V79+7s27evQr0dxo8fz5IlSxg7diy33XYbmzZtYsiQIaU+fuTIkURERDB79mz27dvHV199RWBgIO+++2659vvnn38yYsQIRo0axTfffINarebo0aN0796dOnXq8PLLL+Pu7s6SJUsYNmwYv/32G/fee2+RbTz11FMEBATw+uuvF7nikJyczKBBgxg+fDgjR47k119/5aWXXqJVq1YMHjwYMF3luPvuu9m6dSuPPfYYzZo14/Dhw3z00UecPHmS5cuXl+v1mOXk5JTYPDYjI6PYbbm5uaSnp5dpu/7+/pa4Dx06xMSJE4s9pnPnzqxZs4b09HQ8PT3LGbkQQghROXr16sWiRYsYO3YsAwYM4OGHHwagQYMGxMTEsHbtWhYtWlTm7Z04cYLRo0fz+OOPM2nSJJo0aUJWVha9e/fm8uXLPP7449SrV4/t27czY8YMYmNjb9iEdffu3Wzfvp0HHniAunXrcv78eT7//HP69OlDVFQUbm5u9OrVi2eeeYZPPvmEV155hWbNmgFY/ltYUFAQvXv3ZsmSJcycObPIfYsXL0atVlsSKrcSN8CiRYsYN24cAwcO5N133yUrK4vPP/+cHj16sH///iLHhjqdjoEDB9KjRw8++OAD3NzcLPf9888/LFmyhKeffhp/f3/Cw8M5evQoPXv2xMvLixdffBGNRsMXX3xBnz592LRpE126dCkSS2nHZULYJaMQotJ8++23RsC4bt06Y0JCgvHixYvGX3/91RgQEGB0dnY2Xrx40fLYtm3bGgMDA41JSUmW2w4ePGhUqVTGhx9+2HLbuHHjjGFhYcX2NXPmTGPhf9J79+41AsapU6cWedz48eONgHHmzJnFnjtx4sQij7333nuNtWrVuunr7N27t7FFixZGo9Fo/O2334wajcY4adIko16vtzymX79+xlatWhlzcnIstxkMBmO3bt2MjRo1stxmfs969Ohh1Ol0xfYDGL///nvLbbm5ucbg4GDjfffdZ7lt0aJFRpVKZdyyZUuR58+fP98IGLdt22a5LSwszDhu3Libvkbgpj+7d+8u9jrK8mOWkJBgBIz/+c9/iu0/MjLSCBiPHz9+01jNhgwZUuLfFSGEEKKszN9n586ds9zWu3dvY+/evYs8DjBOnjy5yG2TJ082lud0IywszAgYV61aVeT2N9980+ju7m48efJkkdtffvllo1qtNkZHRxeJo/AxTlZWVrH97Nixo9jxxNKlS42AccOGDcUef/3r/eKLL4yA8fDhw0Ue17x5c+Ptt99eobivl56ebvTx8TFOmjSpyO1xcXFGb2/vIrePGzfOCBhffvnlYtsBjCqVynj06NEitw8bNsyo1WqNZ86csdwWExNj9PT0NPbq1cty242Oy4SwV7IcRogq0L9/fwICAggNDWXEiBG4u7uzYsUKSzlhbGwsBw4cYPz48fj5+Vme17p1awYMGMDKlSvLvU9zqeNTTz1V5PYpU6aU+pwnnniiyO89e/YkKSmJtLS0Mu3z559/ZtSoUTz++ON88cUXqFSmj5irV6/yzz//MHLkSNLT00lMTCQxMZGkpCQGDhzIqVOnik3LmTRpEmq1utg+PDw8GDNmjOV3rVZL586di0xPWbp0Kc2aNaNp06aWfSUmJnL77bcDsGHDhjK9nuvdc889rF27ttjPCy+8UOyxAwcOLPGxJf2Ymdc9Ozs7F9ueuYFudVgbLYQQQlSWiIgIBg4cWOS2pUuX0rNnT3x9fYt8r/fv3x+9Xs/mzZtL3Z6rq6vl//Pz80lKSqJhw4b4+Piwb9++CsU4fPhwnJycWLx4seW2I0eOEBUVxahRo6wS99q1a0lJSWH06NFFnqtWq+nSpUuJxzJPPvlkidvq3bs3zZs3t/yu1+tZs2YNw4YNo379+pbba9euzYMPPsjWrVuLHfuVdlwmhD2S5TBCVIHIyEgaN25Mamoq33zzDZs3by5yonvhwgUAmjRpUuy5zZo1Y/Xq1WRmZuLu7l7mfV64cAGVSkVERESR2xs2bFjqc+rVq1fkd/NyneTkZLy8vG64v3PnzjFmzBjuv/9+Pv300yL3nT59GqPRyGuvvcZrr71W4vOvXLlCnTp1LL9fH7dZ3bp1i60p9vX15dChQ5bfT506xbFjxwgICCh1XxVRt25d+vfvX+z2S5cuFbutdu3a1K5du1zbNx+oldS3xNzErPDBnBB
"text/plain": [
"<Figure size 1100x400 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"max relative error on rough kernel = 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",
"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, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"ax[0].loglog(t_eval, k_target, label='target K(t)')\n",
"ax[0].loglog(t_eval, k_lift, '--', label='lift sum c_j exp(-g_j t)')\n",
"ax[0].set_xlabel('t'); ax[0].set_ylabel('K(t)'); ax[0].legend(); ax[0].set_title(f'Rough kernel H={H}')\n",
"ax[1].loglog(t_eval, np.abs(k_lift - k_target) / np.abs(k_target))\n",
"ax[1].set_xlabel('t'); ax[1].set_ylabel('relative error'); ax[1].set_title('lift relative error')\n",
"plt.tight_layout(); plt.show()\n",
"errors['geometric_grid_lift'] = rel_err\n",
"assert rel_err < 0.5, f'relative err = {rel_err}'\n",
"print(f'max relative error on rough kernel = {rel_err:.3e}')"
]
},
{
"cell_type": "markdown",
"id": "7b414887",
"metadata": {},
"source": [
"## 3. Generic second-kind Volterra equation\n",
"\n",
"Solve\n",
"\n",
"$$\n",
"y(t) = g(t) + \\int_{0}^{t} K(t - s,\\, y(s))\\, ds\n",
"$$\n",
"\n",
"by trapezoidal product integration,\n",
"\n",
"$$\n",
"y_n = g_n + \\Delta t\\,\\Big[ \\tfrac{1}{2} K(t_n, y_0) + \\sum_{k=1}^{n-1} K(t_n - t_k, y_k) + \\tfrac{1}{2} K(0, y_n) \\Big],\n",
"$$\n",
"\n",
"with the implicit step solved by fixed-point iteration.\n",
"\n",
"**Ground truth.** Take $g(t) = 1$ and $K(t, y) = y$. Then $y(t) = 1 + \\int_0^t y(s)\\, ds$ is equivalent to $y' = y,\\, y(0) = 1$, with exact solution\n",
"\n",
"$$\n",
"y(t) = e^{t}.\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "f0dcd786",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T09:45:39.636022Z",
"iopub.status.busy": "2026-05-12T09:45:39.635722Z",
"iopub.status.idle": "2026-05-12T09:45:40.961468Z",
"shell.execute_reply": "2026-05-12T09:45:40.959903Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABEEAAAGGCAYAAACUtJ9/AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAACX+0lEQVR4nOzdd1xV5R8H8M+9F7jsPZQ9VBRRQHDgninOstTMFEephZqSDW24KlNLLaPMhlZqpZU2zJ2K4kIUFw5QQEX2nhe49/z+QO6vG0NQ4DA+79fLV91zzj3nw9B77vc+z/eRCIIggIiIiIiIiIiomZOKHYCIiIiIiIiIqCGwCEJERERERERELQKLIERERERERETUIrAIQkREREREREQtAosgRERERERERNQisAhCRERERERERC0CiyBERERERERE1CKwCEJERERERERELQKLIERERERERETUIrAIQk1KXFwcJBIJtmzZInYUakDOzs6YOnWq2DGalLy8PFhbW2Pbtm21el6PHj3w+uuvP9a1+feUiKj56d+/P/r37y92DADA0qVLIZFIxI7RZNy9exe6uroICwtTb5s6dSqcnZ3r9bobN26Eo6MjFApFvV6HqLZYBKF6M3r0aOjr6yM3N7fKYyZNmgQdHR2kp6c/8nX+/vtvLF269JGfT5U7cOAAZsyYAU9PT8hksnp/oawvn3/++WO9GT958iR69+4NfX19tGrVCvPmzUNeXl61z9m7dy+0tbWhp6eHEydOVHlcff7ufvLJJzAyMsKzzz5bq+e98cYbCAkJQVJSUr3kIiKilmf79u1Yv3692DFarOXLl6N79+7o1atXg1536tSpKC4uxpdfftmg1yV6GBZBqN5MmjQJhYWF2LVrV6X7CwoK8Pvvv2PYsGGwsLB45Ov8/fffWLZs2SM/nyq3fft2bN++HSYmJrC1tRU1y40bN/DVV1890nMfpwgSGRmJQYMGoaCgAGvXrsULL7yATZs2Ydy4cVU+JyIiAuPHj4e7uztsbW0xZswYXL9+vdJj6+t3t6SkBJ988gleeOEFyGSyWj13zJgxMDY2xueff17nuYiIqOk6cOAADhw48EjPresiyNtvv43CwsI6O19zlpqaiu+++w6zZ89u8Gvr6uoiMDAQa9euhSAIDX59oqqwCEL1ZvTo0TAyMsL27dsr3f/7778jPz8fkyZN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
"text/plain": [
"<Figure size 1100x400 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 = np.abs(y_num - y_exact)\n",
"max_err = float(err.max())\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"ax[0].plot(t, y_num, label='numerical')\n",
"ax[0].plot(t, y_exact, '--', label='exp(t)')\n",
"ax[0].set_xlabel('t'); ax[0].set_ylabel('y(t)'); ax[0].legend(); ax[0].set_title('Volterra: y = 1 + int_0^t y(s) ds')\n",
"ax[1].semilogy(t[1:], err[1:])\n",
"ax[1].set_xlabel('t'); ax[1].set_ylabel('|y_num - exp(t)|'); ax[1].set_title('pointwise error (log)')\n",
"plt.tight_layout(); plt.show()\n",
"errors['solve_volterra'] = max_err\n",
"assert max_err < 1e-2, f'max err = {max_err}'\n",
"print(f'max error vs exp(t) = {max_err:.3e}')"
]
},
{
"cell_type": "markdown",
"id": "19a8f090",
"metadata": {},
"source": [
"## 4. Fourier inversion of a characteristic function\n",
"\n",
"Recover a density from $\\varphi(u) = \\mathbb{E}[e^{i u X}]$ via\n",
"\n",
"$$\n",
"f(x) \\;\\approx\\; \\frac{\\Delta u}{\\pi}\\, \\sum_{k=0}^{N_u - 1} w_k \\big[\\,\\Re\\varphi(u_k)\\,\\cos(u_k x) + \\Im\\varphi(u_k)\\,\\sin(u_k x)\\,\\big].\n",
"$$\n",
"\n",
"**Ground truth.** Standard normal $X \\sim \\mathcal{N}(0, 1)$ has\n",
"\n",
"$$\n",
"\\varphi(u) = e^{-u^2 / 2}, \\qquad f(x) = \\frac{1}{\\sqrt{2\\pi}}\\, e^{-x^2 / 2}.\n",
"$$\n",
"\n",
"We supply $\\varphi$ in the trigonometric form $\\varphi(u) = \\cos(-u^2/2) + i \\sin(-u^2/2)$ rescaled by $e^{-u^2/2}$ -- equivalently, real part $e^{-u^2/2}$ and imaginary part $0$, which matches $\\cos(-u^2/2) e^{-u^2/2}$ and $\\sin(-u^2/2) e^{-u^2/2}$ when one writes the Gaussian characteristic function in polar form with phase $-u^2/2$ collapsing to $0$ for a real symmetric law."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "bf8f64a8",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T09:45:40.965231Z",
"iopub.status.busy": "2026-05-12T09:45:40.964938Z",
"iopub.status.idle": "2026-05-12T09:45:41.737924Z",
"shell.execute_reply": "2026-05-12T09:45:41.736418Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABEEAAAGGCAYAAACUtJ9/AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOydeXgTZdfG7yRt0r2lFCgthZayVqBFdgEBKZRNRFFwBVFBX+EFRFBR2VFEERG/Cr4om6iAKyjIDrLKUij7ThcotAVK9zZpkvn+SCedmcxka9rpcn7XlQs688wzZ9I0yXPPOfdRMAzDgCAIgiAIgiAIgiAIooajlDsAgiAIgiAIgiAIgiCIyoBEEIIgCIIgCIIgCIIgagUkghAEQRAEQRAEQRAEUSsgEYQgCIIgCIIgCIIgiFoBiSAEQRAEQRAEQRAEQdQKSAQhCIIgCIIgCIIgCKJWQCIIQRAEQRAEQRAEQRC1AhJBCIIgCIIgCIIgCIKoFZAIQhAEQRAEQRAEQRBErYBEEIIgXIJCocDs2bPlDoMgCIIgiEqid+/e6N27t9xhAABmz54NhUIhdxjVhps3b8LDwwOHDh0yb3v55ZcRHh5eoeddvnw5GjduDK1WW6HnIQhrkAhCEFWY1atXQ6FQmB8eHh4ICQlBXFwcli5diry8PLlDlOTw4cOYPXs2srOz5Q6FIAiCIIgqxo8//oglS5bIHUatZe7cuejSpQu6d+9eqed9+eWXodPp8M0331TqeQmCi4JhGEbuIAiCEGf16tUYM2YM5s6di4iICJSUlCA9PR379u3Dzp070bhxY2zevBnt2rWTO1QUFxfDzc0Nbm5uAIBFixZh2rRpSEpKqvC7CgRBEARBVD46nQ4AoFarHT52yJAhOHfuHJKTk10Si16vh16vh4eHh0vmq8ncvXsXoaGhWLNmDZ577jnz9pdffhn79u1z2e9EinfffRcbNmxAUlISZe8QskCZIARRDRg4cCBefPFFjBkzBtOnT8f27duxa9cuZGZmYujQoSgqKpI7RHh4eJgFkMqEYZgqcf0VSWFhodwhEARBEIQFarXaKQGkInBzc6tWAkhBQYHodld8rykuLobRaJTcv27dOri5ueHxxx8v13mcZcSIEUhJScHevXtlOT9BkAhCENWUxx57DDNmzEBKSgrWrVvH23fp0iU8/fTTCAwMhIeHBzp27IjNmzfzxrClNocOHcKUKVNQr149eHt748knn8Tdu3d5Y0+cOIG4uDgEBQXB09MTEREReOWVV3hjuJ4gs2fPxrRp0wAAERER5nKe5ORk9OrVC9HR0aLX1LJlS8TFxVm97vDwcAwZMgTbt29Hx44d4enpaU6pzM7OxuTJkxEWFgaNRoNmzZph4cKFFl8EjEYjvvzyS7Rt2xYeHh6oV68eBgwYgBMnTpjH6PV6zJs3D5GRkdBoNAgPD8f777/Pq2EdMmQImjZtKhpnt27d0LFjR962devWoUOHDvD09ERgYCCeffZZ3Lx5kzemd+/eaNOmDRISEvDoo4/Cy8sL77//PkaPHo2goCCUlJRYnKt///5o2bKl1eeNIAiCqN2wnhmXLl3CiBEj4Ofnh7p162LSpEkoLi7mjbXnMxCw9ATZt28fFAoFNm7ciI8++giNGjWCh4cH+vbti2vXrvGO27JlC1JSUszfEcLDw8EwDIKCgjBlyhTzWKPRiICAAKhUKl6J7cKFC+Hm5ob8/Hze9XHZuXMnevTogYCAAPj4+KBly5Z4//33eWO0Wi1mzZqFZs2aQaPRICwsDO+8847dnhVHjx7FgAED4O/vDy8vL/Tq1Yvns8GN7cKFC3j++edRp04d9OjRA4D17zU3btzAM888g8DAQHh5eaFr167YsmULb272OV+/fj0+/PBDhIaGwsvLC7m5uZIx//HHH+jSpQt8fHxsXl9BQQHefvtt83erli1bYtGiRRAWExQVFWHixIkICgqCr68vhg4dirS0NFHPuA4dOiAwMBCbNm2yeX6CqAgq/7YtQRAu46WXXsL777+PHTt2YOzYsQCA8+fPo3v37ggNDcV7770Hb29vbNy4EcOGDcOvv/6KJ598kjfHf//7X9SpUwezZs1CcnIylixZggkTJmDDhg0AgMzMTPTv3x/16tXDe++9h4CAACQnJ+O3336TjOupp57ClStX8NNPP+GLL75AUFAQAKBevXp46aWXMHbsWJw7dw5t2rQxH3P8+HFcuXIFH374oc3rvnz5Mp577jm8/vrrGDt2LFq2bInCwkL06tULaWlpeP3119G4cWMcPnwY06dPx507d3h1x6+++ipWr16NgQMH4rXXXoNer8eBAwfw77//moWL1157DWvWrMHTTz+Nt99+G0ePHsWCBQtw8eJF/P777wCAkSNHYtSoUTh+/Dg6depknj8lJQX//vsvPvvsM/O2jz76CDNmzMCIESPw2muv4e7du/jqq6/w6KOP4tSpUwgICDCPvX//PgYOHIhnn30WL774Iho0aABvb2+sXbsW27dvx5AhQ8xj09PTsWfPHsyaNcvm80YQBEEQI0aMQHh4OBYsWIB///0XS5cuxYMHD7B27VrzGHs+A63xySefQKlUYurUqcjJycGnn36KF154AUePHgUAfPDBB8jJycGtW7fwxRdfAAB8fHygUCjQvXt37N+/3zzXmTNnkJOTA6VSiUOHDmHw4MEAgAMHDqB9+/aSC/nz589jyJAhaNeuHebOnQuNRoNr167xBAqj0YihQ4fi4MGDGDduHFq3bo2zZ8/iiy++wJUrV/DHH39Yvc49e/Zg4MCB6NChA2bNmgWlUolVq1bhsccew4EDB9C5c2fe+GeeeQbNmzfHxx9/zBMRxL7XZGRk4JFHHkFhYSEmTpyIunXrYs2aNRg6dCh++eUXi+9z8+bNg1qtxtSpU6HVaiUzdEpKSnD8+HH85z//sXptgCkrZejQodi7dy9effVVxMTEYPv27Zg2bRrS0tLMvzvAVEqzceNGvPTSS+jatSv++ecf8+9KjIcffthCLCKISoMhCKLKsmrVKgYAc/z4cckx/v7+TPv27c0/9+3bl2nbti1TXFxs3mY0GplHHnmEad68ucXcsbGxjNFoNG9/6623GJVKxWRnZzMMwzC///67zRgYhmEAMLNmzTL//NlnnzEAmKSkJN647OxsxsPDg3n33Xd52ydOnMh4e3sz+fn5Vs/TpEkTBgCzbds23vZ58+Yx3t7ezJUrV3jb33vvPUalUjGpqakMwzDMnj17GADMxIkTLeZmn4fExEQGAPPaa6/x9k+dOpUBwOzZs4dhGIbJyclhNBoN8/bbb/PGffrpp4xCoWBSUlIYhmGY5ORkRqVSMR999BFv3NmzZxk3Nzfe9l69ejEAmOXLl/PGGgwGplGjRszIkSN52xcvXswoFArmxo0bIs8WQRAEQZiYNWsWA4AZOnQob/ubb77JAGBOnz7NMIz9n4EMY/rM6tWrl/nnvXv3MgCY1q1bM1qt1rz9yy+/ZAAwZ8+eNW8bPHgw06RJE4s4P/vsM0alUjG5ubkMwzDM0qVLmSZNmjCdO3c2f3cwGAxMQEAA89Zbb1lcH8sXX3zBAGDu3r0r+Zx8//33jFKpZA4cOMDbvnz5cgYAc+jQIcljjUYj07x5cyYuLo73PaqwsJCJiIhg+vXrZxHbc889ZzGP1PeayZMnMwB4seXl5TERERFMeHg4YzAYGIYpe86bNm3KFBYWSsbLcu3aNQYA89VXX1nsGz16NO938scffzAAmPnz5/PGPf3004xCoWCuXbvGMAzDJCQkMACYyZMn88a9/PLLFt8PWcaNG8d4enrajJcgKgIqhyGIao6Pj4+5S0xWVhb27NmDESNGIC8vD/fu3cO9e/dw//59xMXF4erVq0hLS+MdP27cOF76aM+ePWEwGJCSkgIA5gyFv/76S7QUw1H8/f3xxBNP4KeffjLfBTEYDNiwYQOGDRsGb29vm3NERERYlM38/PPP6NmzJ+rUqWO+7nv37iE2NhYGg8F8V+nXX3+FQqEQzZxgn4etW7cCAC8dFwDefvttADCnovr5+WH
"text/plain": [
"<Figure size 1100x400 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"max error vs N(0,1) density = 1.665e-15\n"
]
}
],
"source": [
"def phi_normal(u):\n",
" # Standard normal phi(u) = exp(-u^2 / 2), purely real.\n",
" return (float(np.exp(-0.5 * u * u)), 0.0)\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.0 * np.pi)) * np.exp(-0.5 * x * x)\n",
"err = np.abs(f_num - f_exact)\n",
"max_err = float(err.max())\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 4))\n",
"ax[0].plot(x, f_num, label='Fourier inversion')\n",
"ax[0].plot(x, f_exact, '--', label='exact N(0,1)')\n",
"ax[0].set_xlabel('x'); ax[0].set_ylabel('f(x)'); ax[0].legend(); ax[0].set_title('Density recovery')\n",
"ax[1].semilogy(x, err)\n",
"ax[1].set_xlabel('x'); ax[1].set_ylabel('|f_num - f_exact|'); ax[1].set_title('pointwise error (log)')\n",
"plt.tight_layout(); plt.show()\n",
"errors['fourier_invert'] = max_err\n",
"assert max_err < 1e-3, f'max err = {max_err}'\n",
"print(f'max error vs N(0,1) density = {max_err:.3e}')"
]
},
{
"cell_type": "markdown",
"id": "ef513b3c",
"metadata": {},
"source": [
"## Summary\n",
"\n",
"Verified against analytic ground truth -- max error per primitive (filled in at execution time):\n",
"\n",
"* `solve_fractional_ode` vs. Mittag-Leffler $E_{\\alpha}(-t^{\\alpha})$\n",
"* `geometric_grid_lift` vs. rough kernel $t^{H-1/2}/\\Gamma(H+1/2)$\n",
"* `solve_volterra` vs. $e^{t}$\n",
"* `fourier_invert` vs. standard normal density"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "c74e905c",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T09:45:41.743078Z",
"iopub.status.busy": "2026-05-12T09:45:41.742650Z",
"iopub.status.idle": "2026-05-12T09:45:41.748208Z",
"shell.execute_reply": "2026-05-12T09:45:41.747112Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"solve_fractional_ode max error = 5.625e-04\n",
"geometric_grid_lift max error = 1.688e-02\n",
"solve_volterra max error = 1.232e-06\n",
"fourier_invert max error = 1.665e-15\n",
"\n",
"Verified against analytic ground truth -- max error = 0.016883453274562938\n"
]
}
],
"source": [
"for name, e in errors.items():\n",
" print(f'{name:30s} max error = {e:.3e}')\n",
"print('\\nVerified against analytic ground truth -- max error =', max(errors.values()))"
]
}
],
"metadata": {
"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
}