{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "4e9fb3ed",
   "metadata": {},
   "source": [
    "# `plotstyle` showcase\n",
    "\n",
    "A tour of the reusable matplotlib building blocks in `plotstyle` — a KIT\n",
    "(Karlsruhe Institute of Technology) corporate-design color palette plus a\n",
    "handful of small helpers (`new_figure`, `colorbar`, `savefig`,\n",
    "`style_legend`, `panel_label`) so every figure produced for a thesis chapter\n",
    "or a talk slide looks consistent.\n",
    "\n",
    "Install the extra once: `pip install -e \".[plotting]\"` (or `uv sync --extra plotting`).\n",
    "This notebook requires a working local LaTeX (`latex` + `dvipng`) install —\n",
    "`plotstyle` always renders text through real TeX, no mathtext fallback.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "9c81491e",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-22T12:40:29.369269Z",
     "iopub.status.busy": "2026-07-22T12:40:29.369083Z",
     "iopub.status.idle": "2026-07-22T12:40:29.757832Z",
     "shell.execute_reply": "2026-07-22T12:40:29.757066Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "LaTeX text rendering enabled: True\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "import plotstyle as ps\n",
    "\n",
    "ps.use()\n",
    "print(\"LaTeX text rendering enabled:\", plt.rcParams[\"text.usetex\"])\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9f004413",
   "metadata": {},
   "source": [
    "`use()` also sets up the plot chrome applied by every figure below: only the\n",
    "bottom spine is kept (left/top/right removed), tick marks stay on the left\n",
    "and bottom axes, the x-axis gets shorter minor ticks between the major ones\n",
    "(the y-axis doesn't — its major gridlines already mark position), gridlines\n",
    "are light-grey and horizontal only, major tick labels are darker than minor\n",
    "ones, and titles (figure and axes alike) are left-aligned rather than\n",
    "centered — all visible in the line plot in section 2. All text, including\n",
    "tick numerals, renders through real LaTeX in Latin Modern Sans (via the\n",
    "`sfmath` package and `fontenc`'s T1 encoding, so math-mode numbers stay sans\n",
    "instead of falling back to a serif math font, and plain characters like \"|\"\n",
    "render correctly).\n",
    "\n",
    "**Titling convention used throughout this notebook:** prefer\n",
    "`new_figure(title=..., params=...)` (section 3) for a single axes; reserve\n",
    "plain `ax.set_title()` for per-panel titles in multi-axes figures (section 8)\n",
    "where there's no single title that could cover every panel.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b20c3f51",
   "metadata": {},
   "source": [
    "## 1. The categorical palette\n",
    "\n",
    "Nine fixed-order, colorblind-safe-where-possible hues via\n",
    "`plotstyle.colors.CATEGORICAL` / `get_color(i)` — KIT green, orange, KIT\n",
    "blue, brown, cyan, purple, pea green, red, and yellow (in that order, chosen\n",
    "to keep adjacent hues distinguishable). The order never changes; a 10th\n",
    "series should fold into \"Other\" or move to a facet rather than cycling back\n",
    "to slot 0. KIT yellow reads poorly on a white surface — pair it with a\n",
    "direct label rather than relying on the fill alone.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "5146dcca",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-22T12:40:29.759851Z",
     "iopub.status.busy": "2026-07-22T12:40:29.759611Z",
     "iopub.status.idle": "2026-07-22T12:40:30.131619Z",
     "shell.execute_reply": "2026-07-22T12:40:30.130873Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 20836 characters]",
      "text/plain": [
       "<Figure size 800x450 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['#009682', '#DF9B1B', '#4664AA', '#A78230', '#23A1E0', '#A3107C', '#8CB63C', '#A22223', '#FCE500']\n"
     ]
    }
   ],
   "source": [
    "fig, ax = ps.new_figure(\"thesis-wide\", title=\"Categorical palette (fixed order)\")\n",
    "ax.grid(False)\n",
    "for i, hex_color in enumerate(ps.colors.CATEGORICAL):\n",
    "    ax.bar(i, 1, color=hex_color, width=0.8)\n",
    "ax.set_xticks(range(len(ps.colors.CATEGORICAL)))\n",
    "ax.set_xticklabels([f\"slot {i}\" for i in range(len(ps.colors.CATEGORICAL))])\n",
    "ax.set_yticks([])\n",
    "ax.set_ylim(0, 1.1)\n",
    "ax.spines[\"left\"].set_visible(False)\n",
    "plt.show()\n",
    "\n",
    "print(list(ps.colors.CATEGORICAL))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f524b9a2",
   "metadata": {},
   "source": [
    "## 2. Line plots: color, and optionally linestyle\n",
    "\n",
    "By default `axes.prop_cycle` only varies color — every line solid — so the\n",
    "brand colors read cleanly without extra visual noise.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "a4edc295",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-22T12:40:30.134787Z",
     "iopub.status.busy": "2026-07-22T12:40:30.134429Z",
     "iopub.status.idle": "2026-07-22T12:40:30.316449Z",
     "shell.execute_reply": "2026-07-22T12:40:30.315034Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 91760 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, ax = ps.new_figure(\n",
    "    \"thesis-single\",\n",
    "    title=\"Default: no linestyle cycling\",\n",
    "    params={\"cycle_linestyles\": False},\n",
    ")\n",
    "x = np.linspace(0, 10, 200)\n",
    "for i in range(5):\n",
    "    ax.plot(x, np.sin(x - i * 0.4) * (i + 1), label=f\"series {i}\")\n",
    "ax.set_xlabel(\"Time (s)\")\n",
    "ax.set_ylabel(r\"Amplitude $A(t)$\")\n",
    "ps.style_legend(ax, title=\"Series\")\n",
    "ps.panel_label(ax, \"a\")\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c8f33d18",
   "metadata": {},
   "source": [
    "Pass `cycle_linestyles=True` to `use()` to also vary linestyle — useful when\n",
    "a figure might be printed or projected in grayscale, or viewed by someone\n",
    "with color-vision deficiency, so series stay distinguishable without color.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "59d68b50",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-22T12:40:30.318967Z",
     "iopub.status.busy": "2026-07-22T12:40:30.318798Z",
     "iopub.status.idle": "2026-07-22T12:40:30.511821Z",
     "shell.execute_reply": "2026-07-22T12:40:30.510506Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 85908 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "ps.use(cycle_linestyles=True)\n",
    "\n",
    "fig, ax = ps.new_figure(\n",
    "    \"thesis-single\",\n",
    "    title=\"Opt-in: linestyle cycling\",\n",
    "    params={\"cycle_linestyles\": True},\n",
    ")\n",
    "for i in range(5):\n",
    "    ax.plot(x, np.sin(x - i * 0.4) * (i + 1), label=f\"series {i}\")\n",
    "ax.set_xlabel(\"Time (s)\")\n",
    "ax.set_ylabel(r\"Amplitude $A(t)$\")\n",
    "ps.style_legend(ax, title=\"Series\")\n",
    "ps.panel_label(ax, \"a\")\n",
    "plt.show()\n",
    "\n",
    "ps.use()  # back to the no-cycle default for the rest of this notebook\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "22ecdcf9",
   "metadata": {},
   "source": [
    "## 3. Figure titles and parameter subtitles\n",
    "\n",
    "`new_figure(title=..., params=...)` sets a left-aligned, bold figure-level\n",
    "title — preferred over an axes title even for a single axes, so single- and\n",
    "multi-panel figures stay consistent. `params` is a dict rendered as a\n",
    "smaller subtitle line: `key1: value1 | key2: value2 | ...` — handy for\n",
    "recording the run parameters that produced a plot, as seen in the two line\n",
    "plots above.\n",
    "\n",
    "This is the recommended way to title a single-axes figure — every example in\n",
    "this notebook uses it instead of `ax.set_title()`. The exception is\n",
    "multi-axes figures, where each panel needs its own title; section 8 uses\n",
    "plain `ax.set_title()` per subplot for exactly that reason.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "dc8f77ea",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-22T12:40:30.514127Z",
     "iopub.status.busy": "2026-07-22T12:40:30.513894Z",
     "iopub.status.idle": "2026-07-22T12:40:30.657177Z",
     "shell.execute_reply": "2026-07-22T12:40:30.656188Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 46292 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, ax = ps.new_figure(\n",
    "    \"thesis-single\",\n",
    "    title=\"Measured signal\",\n",
    "    params={\"N\": 512, \"sigma\": 1.2, \"seed\": 42},\n",
    ")\n",
    "ax.plot(x, np.sin(x))\n",
    "ax.set_xlabel(\"Time (s)\")\n",
    "ax.set_ylabel(r\"Amplitude $A(t)$\")\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a8e9bbe1",
   "metadata": {},
   "source": [
    "## 4. Figure size presets\n",
    "\n",
    "`new_figure(preset=...)` ships named sizes (inches) tuned for thesis print and\n",
    "slide decks. Pass an explicit `(w, h)` tuple to bypass the presets entirely.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "2edb0c4c",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-22T12:40:30.658897Z",
     "iopub.status.busy": "2026-07-22T12:40:30.658728Z",
     "iopub.status.idle": "2026-07-22T12:40:30.711393Z",
     "shell.execute_reply": "2026-07-22T12:40:30.710597Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 27176 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, ax = ps.new_figure((6, 4), title=\"plotstyle.FIGSIZES presets (drawn to scale)\")\n",
    "colors_iter = iter(ps.colors.CATEGORICAL)\n",
    "y = 0.0\n",
    "for name, (w, h) in ps.FIGSIZES.items():\n",
    "    color = next(colors_iter)\n",
    "    ax.add_patch(plt.Rectangle((0, y), w, h, fill=False, edgecolor=color, linewidth=2))\n",
    "    ax.text(w + 0.15, y + h / 2, f\"{name}  {w}\\u00d7{h} in\", va=\"center\", fontsize=10, color=color)\n",
    "    y += h + 0.5\n",
    "ax.set_xlim(0, 14)\n",
    "ax.set_ylim(0, y)\n",
    "ax.set_aspect(\"equal\")\n",
    "ax.axis(\"off\")\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1d390e31",
   "metadata": {},
   "source": [
    "## 5. Sequential colormap — magnitude\n",
    "\n",
    "One hue, light → dark: `colors.sequential_cmap()` is a KIT-blue ramp\n",
    "(brand blue at full value, tinting lighter toward zero) for continuous\n",
    "magnitude data (heatmaps, density fields).\n",
    "\n",
    "`ps.colorbar(im, ax)` is used here instead of `fig.colorbar(im, ax=ax)` —\n",
    "with `ax.set_aspect(\"equal\")` on a non-square array, the default colorbar\n",
    "sizes itself to the axes' nominal box and ends up taller than what's\n",
    "actually drawn; `ps.colorbar` appends a same-size axes via\n",
    "`make_axes_locatable` so it always matches.\n",
    "\n",
    "`ps.no_spines(ax)` turns off all four spines — the themed bottom spine\n",
    "(see `use()`) implies an x baseline that doesn't mean anything for\n",
    "pixel/bin-indexed data like this.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "74a67468",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-22T12:40:30.713724Z",
     "iopub.status.busy": "2026-07-22T12:40:30.713553Z",
     "iopub.status.idle": "2026-07-22T12:40:30.869396Z",
     "shell.execute_reply": "2026-07-22T12:40:30.868495Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 27192 characters]",
      "text/plain": [
       "<Figure size 500x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, ax = ps.new_figure(\"square\", title=\"Sequential colormap (KIT blue)\")\n",
    "X, Y = np.meshgrid(np.linspace(-3, 3, 60), np.linspace(-3, 3, 90))\n",
    "Z = np.exp(-(X**2 + Y**2))\n",
    "im = ax.pcolormesh(X, Y, Z, cmap=ps.colors.sequential_cmap(), shading=\"auto\")\n",
    "ps.no_spines(ax)\n",
    "ps.colorbar(im, ax, label=\"Density\")\n",
    "ax.set_aspect(\"equal\")\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13388fdd",
   "metadata": {},
   "source": [
    "## 6. Diverging colormap — polarity\n",
    "\n",
    "KIT blue ↔ KIT red with a neutral gray midpoint: `colors.diverging_cmap()`\n",
    "for signed data such as a correlation matrix, always centered on the data's\n",
    "true zero (`vmin`/`vmax` symmetric around it). Another `ps.no_spines(ax)`\n",
    "call here for the same reason as section 5 — a correlation matrix has no\n",
    "meaningful axis baseline either.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "bcd8ec15",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-22T12:40:30.871197Z",
     "iopub.status.busy": "2026-07-22T12:40:30.871032Z",
     "iopub.status.idle": "2026-07-22T12:40:31.057109Z",
     "shell.execute_reply": "2026-07-22T12:40:31.056321Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 29772 characters]",
      "text/plain": [
       "<Figure size 500x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "rng = np.random.default_rng(0)\n",
    "data = rng.normal(size=(6, 200))\n",
    "corr = np.corrcoef(data)\n",
    "\n",
    "fig, ax = ps.new_figure(\"square\", title=\"Diverging colormap (KIT blue / KIT red)\")\n",
    "im = ax.imshow(corr, cmap=ps.colors.diverging_cmap(), vmin=-1, vmax=1)\n",
    "ps.no_spines(ax)\n",
    "ps.colorbar(im, ax, label=\"Correlation\")\n",
    "ax.set_xticks(range(6))\n",
    "ax.set_yticks(range(6))\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d0fa0677",
   "metadata": {},
   "source": [
    "## 7. Status colors\n",
    "\n",
    "A small, fixed, *reserved* scale (good / warning / serious / critical) —\n",
    "never reused for \"series 4\", and always paired with a label rather than\n",
    "color alone.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "37d1b539",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-22T12:40:31.058905Z",
     "iopub.status.busy": "2026-07-22T12:40:31.058635Z",
     "iopub.status.idle": "2026-07-22T12:40:31.180725Z",
     "shell.execute_reply": "2026-07-22T12:40:31.179970Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 28548 characters]",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, ax = ps.new_figure(\"thesis-single\", title=\"Status colors (reserved, always labeled)\")\n",
    "labels = list(ps.colors.STATUS.keys())\n",
    "values = [92, 74, 45, 12]\n",
    "bars = ax.barh(labels, values, color=[ps.colors.STATUS[k] for k in labels])\n",
    "for bar, value in zip(bars, values):\n",
    "    ax.text(value + 1.5, bar.get_y() + bar.get_height() / 2, str(value), va=\"center\", fontsize=10)\n",
    "ax.set_xlim(0, 108)\n",
    "ax.set_xlabel(\"Checks passing (score out of 100)\")\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8a96d616",
   "metadata": {},
   "source": [
    "## 8. Multi-panel figures with panel labels\n",
    "\n",
    "`panel_label()` adds the `(a)`/`(b)`/… labels expected in multi-panel thesis\n",
    "and paper figures, combining several of the building blocks above. By\n",
    "default it sits in the bottom-right corner, colored to match the axes'\n",
    "xlabel, on a light-grey semi-transparent rounded box with a slim solid\n",
    "border — pass `box=False` for bare text instead. The figure-level\n",
    "`title`/`params` from section 3 still set the figure's overall title — but\n",
    "now each panel also gets its own plain `ax.set_title()`, since no single\n",
    "title could describe all four at once. This is the one place in this\n",
    "notebook where `ax.set_title()` is the right call.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "55084ae6",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-22T12:40:31.182935Z",
     "iopub.status.busy": "2026-07-22T12:40:31.182749Z",
     "iopub.status.idle": "2026-07-22T12:40:31.651425Z",
     "shell.execute_reply": "2026-07-22T12:40:31.650568Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 75624 characters]",
      "text/plain": [
       "<Figure size 800x450 with 4 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, axes = ps.new_figure(\n",
    "    \"thesis-wide\",\n",
    "    nrows=2,\n",
    "    ncols=2,\n",
    "    title=\"A multi-panel figure built from plotstyle building blocks\",\n",
    "    params={\"seed\": 0},\n",
    ")\n",
    "\n",
    "axes[0, 0].plot(x, np.sin(x), label=\"sin\")\n",
    "axes[0, 0].plot(x, np.cos(x), label=\"cos\")\n",
    "axes[0, 0].set_title(\"Trigonometric\")\n",
    "ps.style_legend(axes[0, 0], title=\"Function\")\n",
    "ps.panel_label(axes[0, 0], \"a\")\n",
    "\n",
    "axes[0, 1].hist(rng.normal(size=1000), bins=30, color=ps.get_color(2))\n",
    "axes[0, 1].set_title(\"Histogram\")\n",
    "ps.panel_label(axes[0, 1], \"b\")\n",
    "\n",
    "axes[1, 0].pcolormesh(X, Y, Z, cmap=ps.colors.sequential_cmap(), shading=\"auto\")\n",
    "axes[1, 0].set_title(\"Sequential\")\n",
    "ps.no_spines(axes[1, 0])\n",
    "ps.panel_label(axes[1, 0], \"c\")\n",
    "\n",
    "axes[1, 1].imshow(corr, cmap=ps.colors.diverging_cmap(), vmin=-1, vmax=1)\n",
    "axes[1, 1].set_title(\"Diverging\")\n",
    "ps.no_spines(axes[1, 1])\n",
    "ps.panel_label(axes[1, 1], \"d\")\n",
    "\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4dbdac0a",
   "metadata": {},
   "source": [
    "## 9. Saving figures\n",
    "\n",
    "`savefig()` creates parent directories and writes one file per format — PDF\n",
    "by default, which slots straight into ETPlot's own gallery pipeline\n",
    "(PDF \\u2192 PNG) with no extra conversion step.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "52f994d6",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-22T12:40:31.652959Z",
     "iopub.status.busy": "2026-07-22T12:40:31.652808Z",
     "iopub.status.idle": "2026-07-22T12:40:33.380105Z",
     "shell.execute_reply": "2026-07-22T12:40:33.378530Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[PosixPath('output/multi_panel_demo.pdf'), PosixPath('output/multi_panel_demo.png')]\n"
     ]
    }
   ],
   "source": [
    "from pathlib import Path\n",
    "\n",
    "written = ps.savefig(fig, Path(\"output\") / \"multi_panel_demo\", formats=(\"pdf\", \"png\"))\n",
    "print(written)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5a51f67a",
   "metadata": {},
   "source": [
    "## Recap\n",
    "\n",
    "That's the full building-block set: `use()`, `new_figure()` (with `title`,\n",
    "`params`, and size presets), `colorbar()`, `colors` (categorical / sequential\n",
    "/ diverging / status), `style_legend()`, `panel_label()`, and `savefig()`.\n",
    "Call `ps.use()` once at the top of a plot-producing script and every figure\n",
    "inherits the same validated, modern look — in a thesis chapter or on a\n",
    "slide. For titles specifically: default to `new_figure(title=..., params=...)`;\n",
    "only reach for `ax.set_title()` when a figure has multiple axes and each one\n",
    "needs its own title.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "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.14.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
