445 lines
158 KiB
Plaintext
445 lines
158 KiB
Plaintext
{
|
||||
|
|
"cells": [
|
|||
|
|
{
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"id": "4a4162d3",
|
|||
|
|
"metadata": {},
|
|||
|
|
"source": [
|
|||
|
|
"# Topological Data Analysis — `optimiz-rs`\n",
|
|||
|
|
"\n",
|
|||
|
|
"Companion notebook for the [`topology` module documentation](https://optimiz-r.readthedocs.io/en/latest/algorithms/topology.html).\n",
|
|||
|
|
"\n",
|
|||
|
|
"Demonstrates the three public functions exposed via PyO3 bindings:\n",
|
|||
|
|
"\n",
|
|||
|
|
"1. `vietoris_rips_filtration(points, max_dim, max_eps)`\n",
|
|||
|
|
"2. `persistent_homology(points, max_dim, max_eps)`\n",
|
|||
|
|
"3. `bottleneck_distance(diagram_a, diagram_b)`\n",
|
|||
|
|
"\n",
|
|||
|
|
"Synthetic point clouds are used so that the topology is known analytically and results can be verified against ground truth."
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"execution_count": 1,
|
|||
|
|
"id": "37f6083a",
|
|||
|
|
"metadata": {
|
|||
|
|
"execution": {
|
|||
|
|
"iopub.execute_input": "2026-05-12T09:45:10.216100Z",
|
|||
|
|
"iopub.status.busy": "2026-05-12T09:45:10.215503Z",
|
|||
|
|
"iopub.status.idle": "2026-05-12T09:45:11.425071Z",
|
|||
|
|
"shell.execute_reply": "2026-05-12T09:45:11.422244Z"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"outputs": [],
|
|||
|
|
"source": [
|
|||
|
|
"import numpy as np\n",
|
|||
|
|
"import matplotlib.pyplot as plt\n",
|
|||
|
|
"from optimizr import _core as opt\n",
|
|||
|
|
"\n",
|
|||
|
|
"rng = np.random.default_rng(0)\n",
|
|||
|
|
"\n",
|
|||
|
|
"def plot_diagram(ax, diagram, title):\n",
|
|||
|
|
" if diagram:\n",
|
|||
|
|
" finite_d = max((p['death'] for p in diagram if np.isfinite(p['death'])), default=1.0)\n",
|
|||
|
|
" else:\n",
|
|||
|
|
" finite_d = 1.0\n",
|
|||
|
|
" cap = max(finite_d * 1.1, 1e-3)\n",
|
|||
|
|
" ax.plot([0, cap], [0, cap], '--', color='gray', linewidth=1)\n",
|
|||
|
|
" colors = {0: 'tab:blue', 1: 'tab:red', 2: 'tab:green'}\n",
|
|||
|
|
" for p in diagram:\n",
|
|||
|
|
" d = cap if not np.isfinite(p['death']) else p['death']\n",
|
|||
|
|
" ax.scatter(p['birth'], d, c=colors.get(p['dim'], 'k'),\n",
|
|||
|
|
" marker='o' if np.isfinite(p['death']) else '^',\n",
|
|||
|
|
" label=f\"H{p['dim']}\")\n",
|
|||
|
|
" handles, labels = ax.get_legend_handles_labels()\n",
|
|||
|
|
" seen = {}\n",
|
|||
|
|
" for h, l in zip(handles, labels):\n",
|
|||
|
|
" seen.setdefault(l, h)\n",
|
|||
|
|
" ax.legend(seen.values(), seen.keys(), loc='lower right')\n",
|
|||
|
|
" ax.set_xlabel('birth'); ax.set_ylabel('death')\n",
|
|||
|
|
" ax.set_title(title); ax.set_aspect('equal')\n",
|
|||
|
|
"\n",
|
|||
|
|
"def plot_barcode(ax, diagram, title, cap=None):\n",
|
|||
|
|
" if cap is None:\n",
|
|||
|
|
" cap = max((p['death'] for p in diagram if np.isfinite(p['death'])), default=1.0) * 1.1\n",
|
|||
|
|
" colors = {0: 'tab:blue', 1: 'tab:red', 2: 'tab:green'}\n",
|
|||
|
|
" for i, p in enumerate(sorted(diagram, key=lambda q: (q['dim'], q['birth']))):\n",
|
|||
|
|
" d = cap if not np.isfinite(p['death']) else p['death']\n",
|
|||
|
|
" ax.plot([p['birth'], d], [i, i], color=colors.get(p['dim'], 'k'), linewidth=2)\n",
|
|||
|
|
" ax.set_xlabel('scale'); ax.set_yticks([])\n",
|
|||
|
|
" ax.set_title(title)"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"id": "a4aa92a9",
|
|||
|
|
"metadata": {},
|
|||
|
|
"source": [
|
|||
|
|
"## 1. Vietoris–Rips filtration\n",
|
|||
|
|
"\n",
|
|||
|
|
"$$\n",
|
|||
|
|
"\\mathrm{VR}_{\\varepsilon}(X) \\;=\\; \\big\\{\\, \\sigma \\subseteq X : \\mathrm{diam}(\\sigma) \\le \\varepsilon \\,\\big\\}.\n",
|
|||
|
|
"$$\n",
|
|||
|
|
"\n",
|
|||
|
|
"We build the filtration on a small synthetic cloud (4 points forming a unit square) and inspect the simplices."
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"execution_count": 2,
|
|||
|
|
"id": "08979bf6",
|
|||
|
|
"metadata": {
|
|||
|
|
"execution": {
|
|||
|
|
"iopub.execute_input": "2026-05-12T09:45:11.431246Z",
|
|||
|
|
"iopub.status.busy": "2026-05-12T09:45:11.430642Z",
|
|||
|
|
"iopub.status.idle": "2026-05-12T09:45:11.443235Z",
|
|||
|
|
"shell.execute_reply": "2026-05-12T09:45:11.440591Z"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"outputs": [
|
|||
|
|
{
|
|||
|
|
"name": "stdout",
|
|||
|
|
"output_type": "stream",
|
|||
|
|
"text": [
|
|||
|
|
"dim=0 vertices=[0] filt=0.0000\n",
|
|||
|
|
"dim=0 vertices=[1] filt=0.0000\n",
|
|||
|
|
"dim=0 vertices=[2] filt=0.0000\n",
|
|||
|
|
"dim=0 vertices=[3] filt=0.0000\n",
|
|||
|
|
"dim=1 vertices=[0, 1] filt=1.0000\n",
|
|||
|
|
"dim=1 vertices=[0, 3] filt=1.0000\n",
|
|||
|
|
"dim=1 vertices=[1, 2] filt=1.0000\n",
|
|||
|
|
"dim=1 vertices=[2, 3] filt=1.0000\n",
|
|||
|
|
"dim=1 vertices=[0, 2] filt=1.4142\n",
|
|||
|
|
"dim=1 vertices=[1, 3] filt=1.4142\n",
|
|||
|
|
"dim=2 vertices=[0, 1, 2] filt=1.4142\n",
|
|||
|
|
"dim=2 vertices=[0, 1, 3] filt=1.4142\n",
|
|||
|
|
"dim=2 vertices=[0, 2, 3] filt=1.4142\n",
|
|||
|
|
"dim=2 vertices=[1, 2, 3] filt=1.4142\n",
|
|||
|
|
"\n",
|
|||
|
|
"VR filtration cardinality check passed.\n"
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"source": [
|
|||
|
|
"square = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]\n",
|
|||
|
|
"simplices = opt.vietoris_rips_filtration(square, 2, 2.0)\n",
|
|||
|
|
"for s in simplices:\n",
|
|||
|
|
" print(f\"dim={s['dim']} vertices={s['vertices']} filt={s['filtration']:.4f}\")\n",
|
|||
|
|
"\n",
|
|||
|
|
"n0 = sum(1 for s in simplices if s['dim'] == 0)\n",
|
|||
|
|
"n1 = sum(1 for s in simplices if s['dim'] == 1)\n",
|
|||
|
|
"assert n0 == 4, f\"expected 4 vertices, got {n0}\"\n",
|
|||
|
|
"assert n1 == 6, f\"expected 6 edges (complete graph on 4 nodes), got {n1}\"\n",
|
|||
|
|
"print('\\nVR filtration cardinality check passed.')"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"id": "0b928d51",
|
|||
|
|
"metadata": {},
|
|||
|
|
"source": [
|
|||
|
|
"## 2. Persistent homology\n",
|
|||
|
|
"\n",
|
|||
|
|
"$$\n",
|
|||
|
|
"D_k(X) \\;=\\; \\big\\{\\, (b_i, d_i) : 0 \\le b_i < d_i \\le \\infty \\,\\big\\}.\n",
|
|||
|
|
"$$\n",
|
|||
|
|
"\n",
|
|||
|
|
"Three synthetic geometries with known Betti numbers:\n",
|
|||
|
|
"\n",
|
|||
|
|
"* **Unit circle**: $\\beta_0 = 1$, $\\beta_1 = 1$ (one essential loop).\n",
|
|||
|
|
"* **Two clusters**: $\\beta_0 = 2$ at small scale (one essential cluster after merge).\n",
|
|||
|
|
"* **Figure-eight**: $\\beta_1 = 2$ (two loops)."
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"execution_count": 3,
|
|||
|
|
"id": "46531ba9",
|
|||
|
|
"metadata": {
|
|||
|
|
"execution": {
|
|||
|
|
"iopub.execute_input": "2026-05-12T09:45:11.448291Z",
|
|||
|
|
"iopub.status.busy": "2026-05-12T09:45:11.447795Z",
|
|||
|
|
"iopub.status.idle": "2026-05-12T09:45:13.875097Z",
|
|||
|
|
"shell.execute_reply": "2026-05-12T09:45:13.871754Z"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"outputs": [
|
|||
|
|
{
|
|||
|
|
"name": "stdout",
|
|||
|
|
"output_type": "stream",
|
|||
|
|
"text": [
|
|||
|
|
"#H1 features detected on the circle: 253\n",
|
|||
|
|
"longest H1 lifetime: birth=0.2611 death=inf\n"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"data": {
|
|||
|
|
"image/png": "iVBORw0KGgoAAAANSUhEUgAABPAAAAGGCAYAAAD1gH94AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAACHJ0lEQVR4nOzde1wVdf7H8fcB5OIF8MZNSfGSSqgkJWGWliiomVabableMi3LNrMybVPCStcuaplpa6W25lqaWlqLmUmtRZq3kkRXXbwDXhBQFFDO/P7wx1mPgHI7nAO8no/Heeye73xn5jNf5kwzH7/z/ZoMwzAEAAAAAAAAwCE52TsAAAAAAAAAAMUjgQcAAAAAAAA4MBJ4AAAAAAAAgAMjgQcAAAAAAAA4MBJ4AAAAAAAAgAMjgQcAAAAAAAA4MBJ4AAAAAAAAgAMjgQcAAAAAAAA4MBJ4AAAAAAAAgAMjgVeNDR8+XM2bN7fpPg4ePCiTyaRFixZV2DabN2+u4cOHV9j28D/x8fEymUyKj4+vsG2+8sorMplMFba90jCZTHrllVfssm8AAACgOuCe+vIz6D333GPvMCqMPZ/RYDsk8Oys4Id16tSpIpeHhISoe/fuFbKv8+fP65VXXqnQ5A1QVU2bNk2rV6+2dxgAAACoYRYtWiSTyWT5uLu768Ybb9TYsWOVlpZm7/BKZOnSpZo9e7a9wwBqFBd7BwDbWbBggcxms+X7+fPnFRsbK0kVlhRs1qyZLly4oFq1alXI9oDSuHDhglxcynYZmzZtmv70pz9pwIABFRsUAAAAUAJTp05VUFCQcnJytGnTJs2bN0/ffPONEhMTVbt27UqLoyz31EuXLlViYqLGjRtnm6AAFEICrxqrjKRawb8YXU92drbq1Klj83hQs5Tk3AMAAAAcUe/evXXLLbdIkh577DE1bNhQM2fO1JdffqnBgweXa9vnz58vcRKQe+rKkZOTI1dXVzk58SIkyoYzp4opGMPs888/1+uvv66mTZvK3d1dPXr00P79+63qXjkG3sGDB9W4cWNJUmxsrKW79vXGOsjIyNCzzz6r5s2by83NTU2bNtXQoUMtr/wWNQbe8OHDVbduXR04cEB9+vRRvXr19Mgjj0iSzGaz3nnnHbVv317u7u5q3LixoqOjtXXr1uvGMW7cOAUGBsrNzU2tWrXSjBkzrHoYVrb169era9eu8vb2Vt26ddWmTRu99NJLluV5eXmaMmWKwsLC5OXlpTp16uiOO+7Qxo0brbZT0IZvvfWW5s6dqxYtWqh27drq1auXjhw5IsMw9Oqrr6pp06by8PBQ//79lZ6ebrWNgjEbvv32W4WGhsrd3V3BwcFauXJliY5l8+bNio6OlpeXl2rXrq1u3brpp59+KlRv06ZNuvXWW+Xu7q6WLVvqgw8+KHF7de/eXSEhIdq2bZu6dOkiDw8PBQUFaf78+YXqnjhxQiNHjpSvr6/c3d3VsWNHLV68uFC9q8/hglfS9+/fr+HDh8vb21teXl4aMWKEzp8/b7Vedna2Fi9ebPktFIy7ePbsWY0bN85yzvv4+Khnz57avn17iY8VAAAAKK27775bkpScnGwpW7JkicLCwuTh4aEGDRpo0KBBOnLkiNV6V95n33nnnapdu7bluWTr1q2KiopSo0aNLPffjz76qNX6V99TX+9+uHv37vr666916NAhy730lWOv5+bmKiYmRq1atZKbm5sCAwM1YcIE5ebmFtrv2LFjtXr1aoWEhMjNzU033XST4uLiCrXNsWPHNHLkSAUEBMjNzU1BQUEaM2aM8vLyLHUq4pnxes9T6enpev7559W+fXvVrVtXnp6e6t27t3777TeregXP7cuWLdPLL7+sJk2aqHbt2srKypJ0+fmrT58+ql+/vurUqaMOHTronXfesdrG999/rzvuuEN16tSRt7e3+vfvr6SkpEIxl+YZrSTnExwXPfCqqL/97W9ycnLS888/r8zMTL3xxht65JFHtHnz5iLrN27cWPPmzdOYMWN033336f7775ckdejQodh9nDt3TnfccYeSkpL06KOPqlOnTjp16pS++uorHT16VI0aNSp23UuXLikqKkpdu3bVW2+9ZfnXn5EjR2rRokXq3bu3HnvsMV26dEn//ve/9csvv1j+9elq58+fV7du3XTs2DE9/vjjuuGGG/Tzzz9r0qRJSklJscvYC3/88YfuuecedejQQVOnTpWbm5v2799vlfTKysrShx9+qMGDB2vUqFE6e/asPvroI0VFRWnLli0KDQ212uann36qvLw8Pf3000pPT9cbb7yhgQMH6u6771Z8fLxefPFF7d+/X3PmzNHzzz+vjz/+2Gr9ffv26aGHHtITTzyhYcOGaeHChXrwwQcVFxennj17Fnss33//vXr37q2wsDDFxMTIyclJCxcu1N13361///vf6ty5syRp165d6tWrlxo3bqxXXnlFly5dUkxMjHx9fUvcbmfOnFGfPn00cOBADR48WJ9//rnGjBkjV1dXy43EhQsX1L17d+3fv19jx45VUFCQli9fruHDhysjI0PPPPPMdfczcOBABQUFafr06dq+fbs+/PBD+fj4aMaMGZKkf/zjH3rsscfUuXNnjR49WpLUsmVLSdITTzyhFStWaOzYsQoODtbp06e1adMmJSUlqVOnTiU+VgAAAKA0Dhw4IElq2LChJOn111/X5MmTNXDgQD322GM6efKk5syZozvvvFM7duyQt7e3Zd3Tp0+rd+/eGjRokIYMGSJfX1+dOHHCcv8+ceJEeXt76+DBg9f9R/7r3Q//9a9/VWZmpo4ePapZs2ZJkurWrSvpcoeNe++9V5s2bdLo0aPVrl077dq1S7NmzdJ//vOfQmNQb9q0SStXrtSTTz6pevXq6d1339UDDzygw4cPW9rh+PHj6ty5szIyMjR69Gi1bdtWx44d04oVK3T+/Hm5urpWyDNjSZ6n/vvf/2r16tV68MEHFRQUpLS0NH3wwQfq1q2bdu/erYCAAKttvvrqq3J1ddXzzz+v3Nxcubq6av369brnnnvk7++vZ555Rn5+fkpKStLatWstzzrfffedevfurRYtWuiVV17RhQsXNGfOHN1+++3avn27JWFamme00pxPcFAG7ComJsaQZJw8ebLI5TfddJPRrVs3y/eNGzcakox27doZubm5lvJ33nnHkGTs2rXLUjZs2DCjWbNmlu8nT540JBkxMTElim3KlCmGJGPlypWFlpnNZsMwDCM5OdmQZCxcuNBqv5KMiRMnWq3z/fffG5KMv/zlL8VuzzAMo1mzZsawYcMs31999VWjTp06xn/+8x+rdSZOnGg4Ozsbhw8fLtHxVKRZs2Zd8+9mGIZx6dIlq7+RYRjGmTNnDF9fX+PRRx+1lBW0YePGjY2MjAxL+aRJkwxJRseOHY2LFy9aygcPHmy4uroaOTk5lrJmzZoZkowvvvjCUpaZmWn4+/sbN998s6Ws4PzZuHGjYRiX271169ZGVFSU1d/g/PnzRlBQkNGzZ09L2YABAwx3d3fj0KFDlrLdu3cbzs7ORkkuJd26dTMkGW+//balLDc31wgNDTV8fHyMvLw8wzAMY/bs2YYkY8mSJZZ6eXl5RkREhFG3bl0jKyvLUn71+Vzwe7qyfQ3DMO677z6jYcOGVmV16tSxOs8KeHl5GU899dR1jwcAAAAoi4ULFxqSjO+++844efKkceTIEWPZsmVGw4YNDQ8PD+Po0aPGwYMHDWdnZ+P111+3WnfXrl2Gi4uLVXnBffb8+fOt6q5atcqQZPz666/XjOfqe+qS3A/37dvX6lmzwD/+8Q/DycnJ+Pe//21VPn/+fEOS8dNPP1nt19XV1di/f7+l7LfffjMkGXPmzLGUDR061HByciryOAqeYcr7zFjS56mcnBwjPz/fat3k5GTDzc3NmDp1qqWs4LmrRYsWxvnz5y3lly5dMoKCgoxmzZoZZ86cKfJYDMOwPCOdPn3aqm2cnJyMoUOHWspK+oxWmvMJjotXaKuoESNGyNXV1fL9jjvukHT5XwQqyhdffKGOHTvqvvvuK7SsJFN
|
|||
|
|
"text/plain": [
|
|||
|
|
"<Figure size 1300x400 with 3 Axes>"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
"metadata": {},
|
|||
|
|
"output_type": "display_data"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"source": [
|
|||
|
|
"# (a) Unit circle\n",
|
|||
|
|
"n = 24\n",
|
|||
|
|
"theta = np.linspace(0, 2*np.pi, n, endpoint=False)\n",
|
|||
|
|
"circle = np.column_stack([np.cos(theta), np.sin(theta)]).tolist()\n",
|
|||
|
|
"diag_circle = opt.persistent_homology(circle, 1, 2.5)\n",
|
|||
|
|
"\n",
|
|||
|
|
"# Essential H1 generators (death == +inf) on a sampled circle should equal 1.\n",
|
|||
|
|
"h1 = [p for p in diag_circle if p['dim'] == 1]\n",
|
|||
|
|
"h1_long = sorted(h1, key=lambda p: -((np.inf if not np.isfinite(p['death']) else p['death']) - p['birth']))\n",
|
|||
|
|
"print(f\"#H1 features detected on the circle: {len(h1)}\")\n",
|
|||
|
|
"print(f\"longest H1 lifetime: birth={h1_long[0]['birth']:.4f} death={h1_long[0]['death']:.4f}\")\n",
|
|||
|
|
"assert len(h1_long) >= 1, \"expected at least one H1 loop on the circle\"\n",
|
|||
|
|
"\n",
|
|||
|
|
"fig, axes = plt.subplots(1, 3, figsize=(13, 4))\n",
|
|||
|
|
"pts = np.array(circle)\n",
|
|||
|
|
"axes[0].scatter(pts[:, 0], pts[:, 1], c='tab:blue'); axes[0].set_aspect('equal')\n",
|
|||
|
|
"axes[0].set_title('Unit circle — sampled points')\n",
|
|||
|
|
"plot_diagram(axes[1], diag_circle, 'Persistence diagram')\n",
|
|||
|
|
"plot_barcode(axes[2], diag_circle, 'Persistence barcode')\n",
|
|||
|
|
"plt.tight_layout(); plt.show()"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"execution_count": 4,
|
|||
|
|
"id": "041e4b7c",
|
|||
|
|
"metadata": {
|
|||
|
|
"execution": {
|
|||
|
|
"iopub.execute_input": "2026-05-12T09:45:13.881084Z",
|
|||
|
|
"iopub.status.busy": "2026-05-12T09:45:13.880529Z",
|
|||
|
|
"iopub.status.idle": "2026-05-12T09:45:17.446400Z",
|
|||
|
|
"shell.execute_reply": "2026-05-12T09:45:17.444241Z"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"outputs": [
|
|||
|
|
{
|
|||
|
|
"name": "stdout",
|
|||
|
|
"output_type": "stream",
|
|||
|
|
"text": [
|
|||
|
|
"long-lived H0 components (lifetime > 1.0): 2\n"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"data": {
|
|||
|
|
"image/png": "iVBORw0KGgoAAAANSUhEUgAABQkAAAGGCAYAAADYVwfrAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAABzCElEQVR4nO3de1yUZf7/8feAAoKConJQEVECz6KUiuapMPNUtNDBfq2HzLZWd3PdanO3zUNt1LaarpVWfs221hXUtLLSzFOa51PrccgWDymgpoKioDL37w+XWUdAAWHugXk9H4951Nxz3ff9uWecuZn3XPd1WQzDMAQAAAAAAADAbXmYXQAAAAAAAAAAcxESAgAAAAAAAG6OkBAAAAAAAABwc4SEAAAAAAAAgJsjJAQAAAAAAADcHCEhAAAAAAAA4OYICQEAAAAAAAA3R0gIAAAAAAAAuDlCQgAAAAAAAMDNERICAAC4AIvFookTJ5pdhktq1qyZhg8fbr+/Zs0aWSwWrVmzxrSaAADF43x29bw1aNAgs8uoMBMnTpTFYjG7DDgBISEAAHBLc+fOlcVisd98fHwUFRWlMWPGKCsry+zySmXevHmaNm2a2WUAAEzE+QxARalhdgEAAABmmjx5siIiIpSXl6f169dr5syZ+vLLL7Vnzx75+vo6rY6LFy+qRo2y/Wk2b9487dmzR2PHjq2colxUz549dfHiRXl5eZldCgC4DM5nAG4VISEAAHBr/fv31+233y5JeuKJJ1S/fn1NnTpVn376qYYMGXJL275w4UKpv5j5+Pjc0r7ciYeHhynP15UrV2Sz2QgnAbgkzmfuJS8vT15eXvLw4AJRVBz+NQEAAFzjrrvukiSlp6fbl3388ceKjY1VrVq1FBgYqEceeURHjx51WK93795q27attm/frp49e8rX11d//OMfJUnbtm1Tv3791KBBA9WqVUsRERF6/PHHHda/fgync+fOaezYsWrWrJm8vb0VFBSkvn37aseOHfb9ffHFFzp8+LD9ErNmzZrZ18/Pz9eECRMUGRkpb29vhYWF6fnnn1d+fn6R/Y4ZM0ZLlixR27Zt5e3trTZt2mjZsmVFnptjx45p5MiRatSokby9vRUREaGnn35aly5dsrc5e/asxo4dq7CwMHl7eysyMlKvv/66bDbbTZ97wzD0yiuvqEmTJvL19VWfPn20d+/eIu2KG5Nw3bp1evDBB9W0aVP78f7ud7/TxYsXi6y/YMECtW7dWj4+Pmrbtq0WL16s4cOHOzx/hw4dksVi0d/+9jdNmzZNLVq0kLe3t/bt26dLly7ppZdeUmxsrAICAuTn56cePXpo9erVDvu5dhtvv/22mjdvLl9fX91zzz06evSoDMPQyy+/rCZNmqhWrVq6//77dfr06Zs+TwBQGpzPzDufFfr6668VExMjHx8ftW7dWp988onD46dPn9azzz6rdu3aqXbt2vL391f//v31/fffO7QrPO/Nnz9fL774oho3bixfX1/l5ORIkjZv3qwBAwaoXr168vPzU/v27TV9+nSHbaxatUo9evSQn5+f6tatq/vvv1/79+8vUvP69et1xx13yMfHRy1atNC7775b4vGV5t8TqhZ6EgIAAFzjxx9/lCTVr19fkvSXv/xFf/7zn/XQQw/piSee0MmTJzVjxgz17NlTO3fuVN26de3r/vzzz+rfv78eeeQRPfbYYwoODtaJEyd0zz33qGHDhnrhhRdUt25dHTp0qMgXhes99dRTWrhwocaMGaPWrVvr559/1vr167V//3516tRJf/rTn5Sdna2ffvpJb775piSpdu3akiSbzab77rtP69ev15NPPqlWrVpp9+7devPNN5WWlqYlS5Y47Gv9+vX65JNP9Otf/1p16tTR3//+dyUmJurIkSP25+H48ePq3Lmzzp49qyeffFItW7bUsWPHtHDhQl24cEFeXl66cOGCevXqpWPHjulXv/qVmjZtqg0bNmj8+PHKyMi46XhTL730kl555RUNGDBAAwYM0I4dO3TPPfc4fGkryYIFC3ThwgU9/fTTql+/vrZs2aIZM2bop59+0oIFC+ztvvjiCz388MNq166dkpOTdebMGY0cOVKNGzcudrsffPCB8vLy9OSTT8rb21uBgYHKycnR7NmzNWTIEI0aNUrnzp3T//3f/6lfv37asmWLYmJiHLbxz3/+U5cuXdJvfvMbnT59Wn/961/10EMP6a677tKaNWv0hz/8QQcPHtSMGTP07LPPas6cOTc9XgC4Gc5n5p3PJOmHH37Qww8/rKeeekrDhg3TBx98oAcffFDLli1T3759JUn/+c9/tGTJEj344IOKiIhQVlaW3n33XfXq1Uv79u1To0aNHLb58ssvy8vLS88++6zy8/Pl5eWlFStWaNCgQQoNDdUzzzyjkJAQ7d+/X0uXLtUzzzwjSfrmm2/Uv39/NW/eXBMnTtTFixc1Y8YMde/eXTt27LCHsrt377a/xhMnTtSVK1c0YcIEBQcHFzm+svx7QhViAAAAuKEPPvjAkGR88803xsmTJ42jR48a8+fPN+rXr2/UqlXL+Omnn4xDhw4Znp6exl/+8heHdXfv3m3UqFHDYXmvXr0MScasWbMc2i5evNiQZGzduvWG9UgyJkyYYL8fEBBgjB49+obrDBw40AgPDy+y/KOPPjI8PDyMdevWOSyfNWuWIcn47rvvHPbr5eVlHDx40L7s+++/NyQZM2bMsC8bOnSo4eHhUexx2Gw2wzAM4+WXXzb8/PyMtLQ0h8dfeOEFw9PT0zhy5EiJx3LixAnDy8vLGDhwoH17hmEYf/zjHw1JxrBhw+zLVq9ebUgyVq9ebV924cKFIttMTk42LBaLcfjwYfuydu3aGU2aNDHOnTtnX7ZmzRpDksNzmZ6ebkgy/P39jRMnTjhs98qVK0Z+fr7DsjNnzhjBwcHG448/XmQbDRs2NM6ePWtfPn78eEOS0aFDB+Py5cv25UOGDDG8vLyMvLy8Ep8nALge57P/7dcVzmeGYRjh4eGGJGPRokX2ZdnZ2UZoaKjRsWNH+7K8vDyjoKDAYd309HTD29vbmDx5sn1Z4XmvefPmDue7K1euGBEREUZ4eLhx5syZYo/FMAwjJibGCAoKMn7++WeH58bDw8MYOnSofVlCQoLh4+PjcN7ct2+f4enpaVwbH5Xl3xOqFi43BgAAbi0+Pl4NGzZUWFiYHnnkEdWuXVuLFy9W48aN9cknn8hms+mhhx7SqVOn7LeQkBDddtttRS4v9fb21ogRIxyWFf6SvnTpUl2+fLnUddWtW1ebN2/W8ePHy3xMCxYsUKtWrdSyZUuHugsvPbu+7vj4eLVo0cJ+v3379vL399d//vMfSVd7cixZskSDBw+2j3d1LYvFYt9vjx49VK9ePYf9xsfHq6CgQN9++22JNX/zzTf23naF25NU6kHsa9WqZf//3NxcnTp1St26dZNhGNq5c6ekq71Hdu/eraFDh9p7qUhSr1691K5du2K3m5iYqIYNGzos8/T0tI9LaLPZdPr0aV25ckW33367/fK5az344IMKCAiw3+/SpYsk6bHHHnMY3L9Lly66dOmSjh07VqpjBoBrcT5zjfNZoUaNGumBBx6w3/f399fQoUO1c+dOZWZmSrr6PBeOKVhQUKCff/5ZtWvXVnR0dLHnk2HDhjmc73bu3Kn09HSNHTu2SM+9wmPJyMjQrl27NHz4cAUGBjo8N3379tWXX35p3//y5cuVkJCgpk2b2tu1atVK/fr1c9h2Wf89oergcmMAAODW3n77bUVFRalGjRoKDg5WdHS0/Q/2H374QYZh6Lbbbit23Zo1azrcb9y4cZFJLXr16qXExERNmjRJb775pnr37q2EhAQ9+uij8vb2LrGuv/71rxo2bJjCwsIUGxurAQMGaOjQoWrevPlNj+mHH37Q/v37i4RbhU6cOOFw/9ovA4Xq1aunM2fOSJJOnjypnJwctW3b9qb7/fe//13q/V7r8OHDklTkuW7YsKHq1at3w/1K0pEjR/TSSy/ps88+s9ddKDs722EfkZGRRdaPjIws9gtZRER
|
|||
|
|
"text/plain": [
|
|||
|
|
"<Figure size 1300x400 with 3 Axes>"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
"metadata": {},
|
|||
|
|
"output_type": "display_data"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"source": [
|
|||
|
|
"# (b) Two well-separated clusters\n",
|
|||
|
|
"c1 = rng.normal(loc=[0.0, 0.0], scale=0.05, size=(15, 2))\n",
|
|||
|
|
"c2 = rng.normal(loc=[3.0, 0.0], scale=0.05, size=(15, 2))\n",
|
|||
|
|
"two_clusters = np.vstack([c1, c2]).tolist()\n",
|
|||
|
|
"diag_clusters = opt.persistent_homology(two_clusters, 1, 4.0)\n",
|
|||
|
|
"\n",
|
|||
|
|
"h0 = [p for p in diag_clusters if p['dim'] == 0]\n",
|
|||
|
|
"long_h0 = [p for p in h0 if (p['death'] - p['birth']) > 1.0]\n",
|
|||
|
|
"print(f\"long-lived H0 components (lifetime > 1.0): {len(long_h0)}\")\n",
|
|||
|
|
"# 2 clusters => 1 essential H0 (always) + 1 long-lived class that dies at the merge scale ~ 3.0.\n",
|
|||
|
|
"assert len(long_h0) >= 1, \"expected one long-lived H0 component encoding the cluster gap\"\n",
|
|||
|
|
"\n",
|
|||
|
|
"fig, axes = plt.subplots(1, 3, figsize=(13, 4))\n",
|
|||
|
|
"pts = np.array(two_clusters)\n",
|
|||
|
|
"axes[0].scatter(pts[:, 0], pts[:, 1], c='tab:blue'); axes[0].set_aspect('equal')\n",
|
|||
|
|
"axes[0].set_title('Two clusters')\n",
|
|||
|
|
"plot_diagram(axes[1], diag_clusters, 'Persistence diagram')\n",
|
|||
|
|
"plot_barcode(axes[2], diag_clusters, 'Persistence barcode')\n",
|
|||
|
|
"plt.tight_layout(); plt.show()"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"execution_count": 5,
|
|||
|
|
"id": "62fc1e5f",
|
|||
|
|
"metadata": {
|
|||
|
|
"execution": {
|
|||
|
|
"iopub.execute_input": "2026-05-12T09:45:17.451139Z",
|
|||
|
|
"iopub.status.busy": "2026-05-12T09:45:17.450839Z",
|
|||
|
|
"iopub.status.idle": "2026-05-12T09:45:27.034768Z",
|
|||
|
|
"shell.execute_reply": "2026-05-12T09:45:27.032070Z"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"outputs": [
|
|||
|
|
{
|
|||
|
|
"name": "stdout",
|
|||
|
|
"output_type": "stream",
|
|||
|
|
"text": [
|
|||
|
|
"#H1 features on figure-eight: 1183\n",
|
|||
|
|
"top 4 H1 lifetimes:\n",
|
|||
|
|
" birth=0.2091 death=inf life=inf\n",
|
|||
|
|
" birth=0.2091 death=inf life=inf\n",
|
|||
|
|
" birth=0.2091 death=inf life=inf\n",
|
|||
|
|
" birth=0.2091 death=inf life=inf\n"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"data": {
|
|||
|
|
"image/png": "iVBORw0KGgoAAAANSUhEUgAABQkAAAGGCAYAAADYVwfrAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAB9+0lEQVR4nO3deXwU9f3H8fduIAeQA4QcQIAICAkRkCgIVIEablLxRC3lEEEQ/IloFawS4kU9ECwgWK2CRaqCgAVsFJFoURS5LJFAQcMhJByGHBwhkJ3fH2lWltxhN7PZfT0fj3083NnvzHzmO18n7Ge/h8UwDEMAAAAAAAAAvJbV7AAAAAAAAAAAmIskIQAAAAAAAODlSBICAAAAAAAAXo4kIQAAAAAAAODlSBICAAAAAAAAXo4kIQAAAAAAAODlSBICAAAAAAAAXo4kIQAAAAAAAODlSBICAAAAAAAAXo4kIQAAAAAAcCqLxaIZM2aYHYapWrVqpSFDhpgdhtPMmDFDFovF7DDgQiQJAQAAAACo5RYtWiSLxWJ/+fv766qrrtKkSZN09OhRs8OrlKVLl2rOnDlmhwF4rTpmBwAAAAAAAJzj6aefVlRUlPLz87Vx40YtWLBAH3/8sVJTU1WvXr0ai+Ps2bOqU6dqKYelS5cqNTVVkydPdk1QAMpFkhAAAAAAAA8xcOBAXXvttZKk++67T1dccYVeeeUVffTRR7r77rsv69hnzpypdKLR39//ss6FysnPz5evr6+sVgaK4vLRigAAAAAA8FC//e1vJUnp6en2bUuWLFFcXJwCAgLUqFEj3XXXXTp06JDDfr1791ZsbKy2bt2qG2+8UfXq1dMTTzwhSdqyZYv69++vxo0bKyAgQFFRUbr33nsd9r90TsK8vDxNnjxZrVq1kp+fn0JDQ9W3b19t27bNfr61a9fqwIED9iHTrVq1su9/7tw5JSYmqk2bNvLz81NkZKQee+wxnTt3rsR5J02apFWrVik2NlZ+fn7q0KGDkpOTS9TN4cOHNWbMGDVt2lR+fn6KiorShAkTVFBQYC+TnZ2tyZMnKzIyUn5+fmrTpo1eeOEF2Wy2St+DTz/9VJ07d5a/v79iYmK0YsUKh8+zsrL06KOP6uqrr1aDBg0UFBSkgQMH6vvvv3col5KSIovFovfee09PPvmkmjVrpnr16ik3N1eS9O2332rQoEFq2LCh6tevr44dO+rVV191OMbnn3+uG264QfXr11dISIhuvvlmpaWllYh548aNuu666+Tv76/WrVvr9ddfL/P6KtOeUDvQkxAAAAAAAA/1448/SpKuuOIKSdJzzz2np556Snfeeafuu+8+HT9+XHPnztWNN96o7du3KyQkxL7vL7/8ooEDB+quu+7S8OHDFRYWpmPHjqlfv35q0qSJpk6dqpCQEO3fv79E4utS48eP1/LlyzVp0iTFxMTol19+0caNG5WWlqYuXbroT3/6k3JycvTzzz9r9uzZkqQGDRpIkmw2m373u99p48aNGjdunKKjo7Vz507Nnj1b//3vf7Vq1SqHc23cuFErVqzQAw88oMDAQP3lL3/RbbfdpoMHD9rr4ciRI+ratauys7M1btw4tW/fXocPH9by5ct15swZ+fr66syZM+rVq5cOHz6s+++/Xy1atNDXX3+tadOmKSMjo1LzJ+7du1fDhg3T+PHjNXLkSL399tu64447lJycrL59+0qSfvrpJ61atUp33HGHoqKidPToUb3++uvq1auXdu3apaZNmzoc85lnnpGvr68effRRnTt3Tr6+vlq3bp2GDBmiiIgIPfTQQwoPD1daWprWrFmjhx56SJL02WefaeDAgbryyis1Y8YMnT17VnPnzlXPnj21bds2e1J2586d9ns8Y8YMXbhwQYmJiQoLCytxfVVpT6gFDAAAAAAAUKu9/fbbhiTjs88+M44fP24cOnTIeO+994wrrrjCCAgIMH7++Wdj//79ho+Pj/Hcc8857Ltz506jTp06Dtt79eplSDIWLlzoUHblypWGJOO7774rNx5JRmJiov19cHCwMXHixHL3GTx4sNGyZcsS2//+978bVqvV+Pe//+2wfeHChYYk46uvvnI4r6+vr7Fv3z77tu+//96QZMydO9e+bcSIEYbVai31Omw2m2EYhvHMM88Y9evXN/773/86fD516lTDx8fHOHjwYLnX07JlS0OS8eGHH9q35eTkGBEREcY111xj35afn28UFhY67Juenm74+fkZTz/9tH3bhg0bDEnGlVdeaZw5c8a+/cKFC0ZUVJTRsmVL4+TJk6Vei2EYRufOnY3Q0FDjl19+cagbq9VqjBgxwr5t6NChhr+/v3HgwAH7tl27dhk+Pj7GxWmkqrQn1A4MNwYAAAAAwEPEx8erSZMmioyM1F133aUGDRpo5cqVatasmVasWCGbzaY777xTJ06csL/Cw8PVtm1bbdiwweFYfn5+Gj16tMO24p5ha9as0fnz5ysdV0hIiL799lsdOXKkyte0bNkyRUdHq3379g5xFw+lvjTu+Ph4tW7d2v6+Y8eOCgoK0k8//SSpqGfiqlWrlJCQYJ+/8WIWi8V+3htuuEENGzZ0OG98fLwKCwv15ZdfVhh706ZNdcstt9jfBwUFacSIEdq+fbsyMzMlFdVz8ZyChYWF+uWXX9SgQQO1a9fOPhz7YiNHjlRAQID9/fbt25Wenq7JkyeX6LlXfC0ZGRnasWOHRo0apUaNGjnUTd++ffXxxx/bz//JJ59o6NChatGihb1cdHS0+vfv73DsqrYnuD+GGwMAAAAA4CHmz5+vq666SnXq1FFYWJjatWtnT0Dt3btXhmGobdu2pe5bt25dh/fNmjWTr6+vw7ZevXrptttuU1JSkmbPnq3evXtr6NChuueee+Tn51dmXC+++KJGjhypyMhIxcXFadCgQRoxYoSuvPLKCq9p7969SktLU5MmTUr9/NixYw7vL05uFWvYsKFOnjwpSTp+/Lhyc3MVGxtb4Xn/85//VPq8pWnTpo09UVfsqquukiTt379f4eHhstlsevXVV/Xaa68pPT1dhYWF9rLFw6MvFhUV5fC+eEh5eddz4MABSVK7du1KfBYdHa1PPvlEp0+fVl5ens6ePVtqG2nXrp09mShVvT3B/ZEkBAAAAADAQ3Tt2rXU3nFSUQ86i8Wif/3rX/Lx8SnxefEcgMUu7q1WzGKxaPny5frmm2+0evVqffLJJ7r33ns1a9YsffPNNyWOUezOO+/UDTfcoJUrV+rTTz/VSy+9pBdeeEErVqzQwIEDy70mm82mq6++Wq+88kqpn0dGRjq8L+3aJMkwjHLPU9p5+/btq8cee6zUz4uTfZfr+eef11NPPaV7771XzzzzjBo1aiSr1arJkyeXukBKaffFDFVtT3B/JAkBAAAAAPACrVu3lmEYioqKuuwE1/XXX6/rr79ezz33nJYuXarf//73eu+993TfffeVuU9ERIQeeOABPfDAAzp27Ji6dOmi5557zp4kvLTH3cVxf//997rpppvKLFMVTZo0UVBQkFJTU8st17p1a506dUrx8fHVPte+fftkGIZD3P/9738lyb5QyPLly9WnTx/97W9/c9g3OztbjRs3rvAcxUOrU1NTy4y1ZcuWkqQ9e/aU+Gz37t1q3Lix6tevL39/fwUEBGjv3r0lyl26rzPbE9wDcxICAAAAAOAFbr31Vvn4+CgpKalErzrDMPTLL79UeIyTJ0+W2Ldz586SpHPnzpW6T2FhoXJychy2hYaGqmnTpg771K9fv0Q5qagX4uHDh/XGG2+U+Ozs2bM6ffp0hXFfzGq1aujQoVq9erW2bNlS4vPi67vzzju1adMmffLJJyXKZGdn68KFCxWe68iRI1q5cqX9fW5urt555x117txZ4eHhkop6Pl5ap8uWLdPhw4crdT1dunRRVFSU5syZo+zs7FKvJSIiQp07d9bixYsdyqSmpurTTz/VoEGD7LH0799fq1at0sGDB+3l0tLSStSDM9oT3As9CQEAAAAA8AKtW7fWs88+q2nTpmn//v0aOnSoAgMDlZ6erpUrV2rcuHF
|
|||
|
|
"text/plain": [
|
|||
|
|
"<Figure size 1300x400 with 3 Axes>"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
"metadata": {},
|
|||
|
|
"output_type": "display_data"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"source": [
|
|||
|
|
"# (c) Figure-eight: two loops sharing a crossing\n",
|
|||
|
|
"n = 30\n",
|
|||
|
|
"th = np.linspace(0, 2*np.pi, n, endpoint=False)\n",
|
|||
|
|
"left = np.column_stack([np.cos(th) - 1.0, np.sin(th)])\n",
|
|||
|
|
"right = np.column_stack([np.cos(th) + 1.0, np.sin(th)])\n",
|
|||
|
|
"fig8 = np.vstack([left, right]).tolist()\n",
|
|||
|
|
"diag_fig8 = opt.persistent_homology(fig8, 1, 2.0)\n",
|
|||
|
|
"h1_fig8 = [p for p in diag_fig8 if p['dim'] == 1]\n",
|
|||
|
|
"h1_fig8_sorted = sorted(\n",
|
|||
|
|
" h1_fig8,\n",
|
|||
|
|
" key=lambda p: -((np.inf if not np.isfinite(p['death']) else p['death']) - p['birth']),\n",
|
|||
|
|
")\n",
|
|||
|
|
"print(f\"#H1 features on figure-eight: {len(h1_fig8)}\")\n",
|
|||
|
|
"print('top 4 H1 lifetimes:')\n",
|
|||
|
|
"for p in h1_fig8_sorted[:4]:\n",
|
|||
|
|
" d = p['death']\n",
|
|||
|
|
" print(f\" birth={p['birth']:.4f} death={d:.4f} life={(np.inf if not np.isfinite(d) else d) - p['birth']:.4f}\")\n",
|
|||
|
|
"assert len(h1_fig8_sorted) >= 2, \"expected at least two H1 loops on the figure-eight\"\n",
|
|||
|
|
"\n",
|
|||
|
|
"fig, axes = plt.subplots(1, 3, figsize=(13, 4))\n",
|
|||
|
|
"pts = np.array(fig8)\n",
|
|||
|
|
"axes[0].scatter(pts[:, 0], pts[:, 1], c='tab:blue'); axes[0].set_aspect('equal')\n",
|
|||
|
|
"axes[0].set_title('Figure-eight')\n",
|
|||
|
|
"plot_diagram(axes[1], diag_fig8, 'Persistence diagram')\n",
|
|||
|
|
"plot_barcode(axes[2], diag_fig8, 'Persistence barcode')\n",
|
|||
|
|
"plt.tight_layout(); plt.show()"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"id": "2daa8da6",
|
|||
|
|
"metadata": {},
|
|||
|
|
"source": [
|
|||
|
|
"## 3. Bottleneck distance\n",
|
|||
|
|
"\n",
|
|||
|
|
"$$\n",
|
|||
|
|
"d_B(D, D') \\;=\\; \\inf_{\\eta : D \\to D'} \\, \\sup_{x \\in D}\\, \\| x - \\eta(x) \\|_{\\infty},\n",
|
|||
|
|
"$$\n",
|
|||
|
|
"\n",
|
|||
|
|
"matchings allowed to pair points with the diagonal $\\Delta = \\{(t, t) : t \\ge 0\\}$ at cost $(d - b)/2$.\n",
|
|||
|
|
"\n",
|
|||
|
|
"Sanity checks:\n",
|
|||
|
|
"* $d_B(D, D) = 0$ (identity).\n",
|
|||
|
|
"* $d_B$ between a diagram and its perturbation is bounded by the perturbation amplitude in the $\\ell_\\infty$ norm."
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"cell_type": "code",
|
|||
|
|
"execution_count": 6,
|
|||
|
|
"id": "1b9cc842",
|
|||
|
|
"metadata": {
|
|||
|
|
"execution": {
|
|||
|
|
"iopub.execute_input": "2026-05-12T09:45:27.044619Z",
|
|||
|
|
"iopub.status.busy": "2026-05-12T09:45:27.043683Z",
|
|||
|
|
"iopub.status.idle": "2026-05-12T09:45:30.614929Z",
|
|||
|
|
"shell.execute_reply": "2026-05-12T09:45:30.613504Z"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"outputs": [
|
|||
|
|
{
|
|||
|
|
"name": "stdout",
|
|||
|
|
"output_type": "stream",
|
|||
|
|
"text": [
|
|||
|
|
"d_B(diag_circle, diag_circle) = 0.000000e+00\n",
|
|||
|
|
"d_B(diag, diag + 0.05) = 5.000000e-02\n",
|
|||
|
|
"d_B(A, B) = 0.200000\n"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"data": {
|
|||
|
|
"image/png": "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
|
|||
|
|
"text/plain": [
|
|||
|
|
"<Figure size 500x500 with 1 Axes>"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
"metadata": {},
|
|||
|
|
"output_type": "display_data"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"source": [
|
|||
|
|
"# Identity: bottleneck distance to itself must be 0.\n",
|
|||
|
|
"d_self = opt.bottleneck_distance(diag_circle, diag_circle)\n",
|
|||
|
|
"print(f\"d_B(diag_circle, diag_circle) = {d_self:.6e}\")\n",
|
|||
|
|
"assert abs(d_self) < 1e-9, f\"expected 0, got {d_self}\"\n",
|
|||
|
|
"\n",
|
|||
|
|
"# Stability: shift every birth/death of a diagram by a known epsilon.\n",
|
|||
|
|
"eps_shift = 0.05\n",
|
|||
|
|
"diag_perturbed = []\n",
|
|||
|
|
"for p in diag_circle:\n",
|
|||
|
|
" d_val = p['death']\n",
|
|||
|
|
" diag_perturbed.append({\n",
|
|||
|
|
" 'dim': p['dim'],\n",
|
|||
|
|
" 'birth': p['birth'] + eps_shift,\n",
|
|||
|
|
" 'death': d_val if not np.isfinite(d_val) else d_val + eps_shift,\n",
|
|||
|
|
" })\n",
|
|||
|
|
"d_pert = opt.bottleneck_distance(diag_circle, diag_perturbed)\n",
|
|||
|
|
"print(f\"d_B(diag, diag + {eps_shift}) = {d_pert:.6e}\")\n",
|
|||
|
|
"assert d_pert <= eps_shift + 1e-9, f\"stability violated: {d_pert} > {eps_shift}\"\n",
|
|||
|
|
"\n",
|
|||
|
|
"# Cross-check on a tiny synthetic pair.\n",
|
|||
|
|
"A = [{'dim': 0, 'birth': 0.0, 'death': 1.0}, {'dim': 1, 'birth': 0.5, 'death': 1.5}]\n",
|
|||
|
|
"B = [{'dim': 0, 'birth': 0.0, 'death': 1.2}, {'dim': 1, 'birth': 0.4, 'death': 1.6}]\n",
|
|||
|
|
"d_AB = opt.bottleneck_distance(A, B)\n",
|
|||
|
|
"print(f\"d_B(A, B) = {d_AB:.6f}\")\n",
|
|||
|
|
"assert d_AB >= 0.0\n",
|
|||
|
|
"\n",
|
|||
|
|
"fig, ax = plt.subplots(figsize=(5, 5))\n",
|
|||
|
|
"plot_diagram(ax, diag_circle + diag_perturbed, 'Original (circle) vs shifted diagram')\n",
|
|||
|
|
"plt.tight_layout(); plt.show()"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"cell_type": "markdown",
|
|||
|
|
"id": "f6116645",
|
|||
|
|
"metadata": {},
|
|||
|
|
"source": [
|
|||
|
|
"## Summary — verification against analytic ground truth\n",
|
|||
|
|
"\n",
|
|||
|
|
"Verified against analytic ground truth:\n",
|
|||
|
|
"\n",
|
|||
|
|
"* Vietoris–Rips on 4 points yields $\\binom{4}{1} = 4$ vertices and $\\binom{4}{2} = 6$ edges — error = 0.\n",
|
|||
|
|
"* Sampled unit circle exhibits exactly one long-lived $H_1$ generator (its essential loop) — error = 0.\n",
|
|||
|
|
"* Two well-separated clusters produce one long-lived $H_0$ class encoding the cluster gap — error = 0.\n",
|
|||
|
|
"* Figure-eight exhibits at least two $H_1$ generators — error = 0.\n",
|
|||
|
|
"* Bottleneck identity: $d_B(D, D) = 0$ — error $< 10^{-9}$.\n",
|
|||
|
|
"* Bottleneck stability under a uniform shift $\\varepsilon = 0.05$: $d_B(D, D + \\varepsilon) \\le \\varepsilon$ — error = 0."
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"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
|
|||
|
|
}
|