{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "6e96e86f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-19T09:16:19.768052Z",
     "iopub.status.busy": "2026-06-19T09:16:19.767890Z",
     "iopub.status.idle": "2026-06-19T09:16:24.376266Z",
     "shell.execute_reply": "2026-06-19T09:16:24.375516Z"
    },
    "tags": [
     "remove-cell"
    ]
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "An NVIDIA GPU may be present on this machine, but a CUDA-enabled jaxlib is not installed. Falling back to cpu.\n"
     ]
    }
   ],
   "source": [
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import jax\n",
    "import jax.numpy as jnp\n",
    "import matplotlib.pyplot as plt\n",
    "import brainmass\n",
    "import brainstate\n",
    "import braintools\n",
    "import brainunit as u\n",
    "from brainstate.nn import Param\n",
    "brainstate.random.seed(0)\n",
    "brainstate.environ.set(dt=0.1 * u.ms)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ee90ade2",
   "metadata": {},
   "source": [
    "# Creating a Coupling\n",
    "\n",
    "A **coupling** turns the per-region states of a network into the input current\n",
    "each region receives from the others. `brainmass` ships diffusive, additive, and\n",
    "several nonlinear couplings (`DiffusiveCoupling`, `SigmoidalCoupling`, ...). This\n",
    "guide shows how to implement a *new* one against the same stable contract, so it\n",
    "drops into a network exactly like the built-ins.\n",
    "\n",
    "If you only want to *use* the existing couplings through `Network(coupling=...)`,\n",
    "see {doc}`/howto/custom_coupling`. This guide is about the implementation contract.\n",
    "\n",
    "Every coupling in `coupling.py` follows one pattern:\n",
    "\n",
    "1. a **pure kernel function** doing the array math, and\n",
    "2. a thin **`Module`** that holds the `Param`s and the prefetch reads, and calls\n",
    "   the kernel from `update`.\n",
    "\n",
    "We will reproduce that pattern for a new *power-law diffusive* coupling."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d0934a55",
   "metadata": {},
   "source": [
    "## The prefetch / delay contract\n",
    "\n",
    "A coupling never reads another module's state directly. It reads a **prefetch** --\n",
    "a zero-argument callable a node hands out that yields a (optionally delayed)\n",
    "snapshot of one of its states. There are two reads, and their shapes are the whole\n",
    "contract:\n",
    "\n",
    "- **Source** (`prefetch_delay`): every target reads *every* source, so the source\n",
    "  read is shaped `(..., N, N)` -- `x[i, j]` is source `j` as seen by target `i`,\n",
    "  already delayed by the conduction delay `tau_ij = distance_ij / speed`.\n",
    "- **Target** (`prefetch`): each target reads its *own* current state, shaped\n",
    "  `(..., N)`.\n",
    "\n",
    "A diffusive coupling needs both (it forms `x_j - y_i`); an additive one needs only\n",
    "the source. The node builds these reads; the coupling just consumes them:\n",
    "\n",
    "```python\n",
    "src = node.prefetch_delay(coupled_var, (delay_matrix, idx), init=delay_init)  # (.., N, N)\n",
    "tgt = node.prefetch(coupled_var)                                              # (.., N)\n",
    "```\n",
    "\n",
    "`init_maybe_prefetch` (called from the coupling's `init_state`) sizes the delay\n",
    "buffer from `delay / dt`, so a global `dt` must be live in the environment before a\n",
    "delay-coupled network is constructed."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3fbad755",
   "metadata": {},
   "source": [
    "## Step 1 -- the kernel function\n",
    "\n",
    "The kernel does the math on plain arrays (or callables that return them). Mirror\n",
    "`brainmass.diffusive_coupling`: accept the source/target reads, the connection\n",
    "matrix, and the strength `k`; return the `(..., N)` current. Strip units with\n",
    "`brainunit.get_magnitude` before any transcendental (here `abs` / `sign` / a\n",
    "fractional power), then let `k` carry the output's units back."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "8424a4f5",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-19T09:16:24.381052Z",
     "iopub.status.busy": "2026-06-19T09:16:24.379968Z",
     "iopub.status.idle": "2026-06-19T09:16:24.385997Z",
     "shell.execute_reply": "2026-06-19T09:16:24.385178Z"
    }
   },
   "outputs": [],
   "source": [
    "def powerlaw_coupling(delayed_x, y, conn, k, p=1.0):\n",
    "    \"\"\"Power-law diffusive kernel: k * sum_j conn_ij * sign(d) * |d|**p, d = x_ij - y_i.\n",
    "\n",
    "    A generalisation of diffusive coupling: p=1 recovers it; p<1 emphasises small\n",
    "    differences, p>1 emphasises large ones.\n",
    "    \"\"\"\n",
    "    # target read -> (..., N), expand to (..., N, 1) to broadcast against sources\n",
    "    y_val = y() if callable(y) else y\n",
    "    y_exp = u.math.expand_dims(y_val, axis=-1)\n",
    "    # source read -> (..., N, N)\n",
    "    x_mat = delayed_x() if callable(delayed_x) else delayed_x\n",
    "    # difference, made dimensionless for the nonlinearity\n",
    "    diff = u.get_magnitude(x_mat - y_exp)            # (..., N, N)\n",
    "    shaped = jnp.sign(diff) * jnp.abs(diff) ** p\n",
    "    return k * (conn * shaped).sum(axis=-1)          # (..., N)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d453f895",
   "metadata": {},
   "source": [
    "## Step 2 -- the `Module` wrapper\n",
    "\n",
    "The class holds the configuration as `Param`s and stores the prefetch reads. The\n",
    "three required pieces, all mirroring `DiffusiveCoupling`:\n",
    "\n",
    "- **`__init__`** wraps every scalar/array through `Param.init`. `Param.init(float)`\n",
    "  returns a non-trainable `Const`, so defaults add *no* trainable state; pass an\n",
    "  explicit `Param(..., fit=True)` to make `k` or `p` learnable.\n",
    "- **`init_state`** is decorated `@brainstate.nn.call_order(2)` (it must run *after*\n",
    "  the node allocates its states) and calls `init_maybe_prefetch` on each read to\n",
    "  size the delay buffers.\n",
    "- **`update`** reads the current `Param` values and calls the kernel. `Module` is\n",
    "  callable, so `coupling()` dispatches to `update`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "50be6306",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-19T09:16:24.388463Z",
     "iopub.status.busy": "2026-06-19T09:16:24.388257Z",
     "iopub.status.idle": "2026-06-19T09:16:24.393382Z",
     "shell.execute_reply": "2026-06-19T09:16:24.392421Z"
    }
   },
   "outputs": [],
   "source": [
    "from brainstate.nn import Module, init_maybe_prefetch\n",
    "\n",
    "class PowerLawCoupling(Module):\n",
    "    \"\"\"Power-law diffusive coupling (mirrors brainmass.DiffusiveCoupling).\"\"\"\n",
    "\n",
    "    def __init__(self, x, y, conn, k=1.0, p=1.0):\n",
    "        super().__init__()\n",
    "        self.x = x          # source prefetch_delay -> (..., N, N)\n",
    "        self.y = y          # target prefetch        -> (..., N)\n",
    "        self.k = Param.init(k)\n",
    "        self.p = Param.init(p)\n",
    "        self.conn = Param.init(conn)\n",
    "\n",
    "    @brainstate.nn.call_order(2)\n",
    "    def init_state(self, *args, **kwargs):\n",
    "        init_maybe_prefetch(self.x)\n",
    "        init_maybe_prefetch(self.y)\n",
    "\n",
    "    def update(self, *args, **kwargs):\n",
    "        return powerlaw_coupling(\n",
    "            self.x, self.y, self.conn.value(), self.k.value(), self.p.value()\n",
    "        )"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d258fcec",
   "metadata": {},
   "source": [
    "## Step 3 -- wire it into a network\n",
    "\n",
    "`brainmass.Network` dispatches a *fixed* set of coupling names, so to use a brand\n",
    "new coupling we assemble the network the way `Network` does internally: build the\n",
    "prefetch reads on the node, hand them to the coupling, and feed the coupling\n",
    "current back as the node's first input. This is the same\n",
    "`current = coupling(); node(current)` idiom, made explicit.\n",
    "\n",
    "`delay_index(N)` builds the source-index matrix; `brainstate.environ` must already\n",
    "hold `dt` (the setup cell sets it) because `prefetch_delay` sizes its buffer at\n",
    "construction time."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "69905e02",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-19T09:16:24.395217Z",
     "iopub.status.busy": "2026-06-19T09:16:24.394990Z",
     "iopub.status.idle": "2026-06-19T09:16:25.012645Z",
     "shell.execute_reply": "2026-06-19T09:16:25.011662Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "trajectory shape : (400, 4)\n",
      "all finite       : True\n"
     ]
    }
   ],
   "source": [
    "from brainmass import delay_index\n",
    "\n",
    "class PowerLawNetwork(brainstate.nn.Module):\n",
    "    \"\"\"Minimal whole-brain network using the custom coupling.\"\"\"\n",
    "\n",
    "    def __init__(self, node, conn, *, coupled_var, k=1.0, p=1.0,\n",
    "                 delay_time=None, delay_init=braintools.init.Constant(0.0)):\n",
    "        super().__init__()\n",
    "        self.node = node\n",
    "        n = node.varshape[0]\n",
    "        conn = np.asarray(conn) * (1.0 - np.eye(n))         # zero the diagonal\n",
    "        if delay_time is None:\n",
    "            delay_time = u.math.zeros((n, n)) * u.ms        # instantaneous\n",
    "        idx = delay_index(n)\n",
    "        src = node.prefetch_delay(coupled_var, (delay_time, idx), init=delay_init)\n",
    "        tgt = node.prefetch(coupled_var)\n",
    "        self.coupling = PowerLawCoupling(src, tgt, conn, k=k, p=p)\n",
    "\n",
    "    def update(self, *node_inputs):\n",
    "        current = self.coupling()\n",
    "        return self.node(current, *node_inputs)\n",
    "\n",
    "\n",
    "N = 4\n",
    "conn = np.ones((N, N)) * 0.1\n",
    "node = brainmass.HopfStep(N, a=0.2, w=0.3)\n",
    "net = PowerLawNetwork(node, conn, coupled_var='x', k=0.5, p=1.5)\n",
    "\n",
    "sim = brainmass.Simulator(net, dt=0.1 * u.ms)\n",
    "res = sim.run(40. * u.ms, monitors=lambda m: m.node.x.value)\n",
    "print('trajectory shape :', res['output'].shape)\n",
    "print('all finite       :', bool(jnp.isfinite(res['output']).all()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "2d040b0a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-19T09:16:25.014853Z",
     "iopub.status.busy": "2026-06-19T09:16:25.014615Z",
     "iopub.status.idle": "2026-06-19T09:16:25.110104Z",
     "shell.execute_reply": "2026-06-19T09:16:25.109028Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnYAAAHWCAYAAAD6oMSKAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAQvNJREFUeJzt3XlclOX+//H3AAIqAqII4opmrqTlgpjbSY7YKqmlZkctSzPX1Eo95VKn6NSxzCyX6mi7pqWdzEwzt5JcUFs0Sc0tFXAJcAWE6/eHP+6vI8gmOHL7ej4e89C57uue+Vxz3TO8uee+bxzGGCMAAACUem6uLgAAAADFg2AHAABgEwQ7AAAAmyDYAQAA2ATBDgAAwCYIdgAAADZBsAMAALAJgh0AAIBNEOwAAABsgmAHW+vYsaM6duzo6jJcJjExUT169FClSpXkcDg0depUV5fkUqtXr5bD4dDChQtdXQpKIYfDoUmTJrm6jGvavn375HA4NHfuXKtt0qRJcjgcrivqOkOwwxV54YUX5HA41KRJE1eXck3JL0D0799fPj4+JV7HE088oW+++Ubjxo3TBx98oC5dupT4c2bbsWOHJk2apH379l2157werF+/XpMmTVJycrKrSwFwDSLYocj+/PNPvfjiiypfvryrS7ms5cuXa/ny5a4uw2W+++47de3aVWPGjNGDDz6oBg0aXLXn3rFjhyZPnkywK2br16/X5MmTCXYoNZ555hmdPXvW1WVcNzxcXQBKrzFjxqh169bKzMzUsWPHivw458+fV1ZWljw9PYuxugtK4jFLk6SkJPn7+7u6DJfL3saAvBhjdO7cOZUtW9bVpdiKh4eHPDyIG1cLe+xQJGvXrtXChQsLfcxW9vEX//nPfzR16lTVrVtXXl5e2rFjhyRp586d6tGjhwICAuTt7a0WLVrof//7X47H+fnnn9WhQweVLVtW1atX17/+9S/NmTNHDofDaQ9RbsfYJSUlacCAAQoKCpK3t7eaNm2q995777J1zp4926qzZcuW2rRpU6HGXBhvvfWWGjduLC8vL4WEhGjIkCE59sx07NhRTZo0UVxcnNq0aaOyZcsqNDRUM2fOtPrMnTtXDodDxhi9+eabcjgceR7jUtjx5jdPc+fO1X333SdJ+tvf/mY9/+rVqzVq1ChVqlRJxhir/7Bhw+RwODRt2jSrLTExUQ6HQzNmzLDaCjt3uW1jl0pLS9Ndd90lPz8/rV+//rKvUV6+/vprdejQQRUqVJCvr69atmypjz/+2Fpeu3Zt9e/fP8d6uW2fb7zxhho3bqxy5cqpYsWKatGihfVYkyZN0pNPPilJCg0NtV7X7G3+/Pnzev75560x165dW+PHj1daWprTc9SuXVt33XWXVq9erRYtWqhs2bIKCwvT6tWrJUmff/65wsLC5O3trebNm2vr1q35vgbZ29zatWs1aNAgVapUSb6+vurbt6/++uuvHP3z29anTZsmd3d3p7YpU6bI4XBo1KhRVltmZqYqVKigp59+2mrLysrS1KlT1bhxY3l7eysoKEiDBg3KUUf26/DNN99Yr8OsWbPyHWu2/fv36/HHH1f9+vVVtmxZVapUSffdd5/TZ1BycrLc3d2dtu1jx47Jzc0tx/tg8ODBCg4Ozvd5Dx06pAEDBigkJEReXl4KDQ3V4MGDlZ6ebvX5448/dN999ykgIEDlypVT69at9dVXXzk9TvacXbpXPftQkuztQSrY587l5HaMncPh0NChQ7V48WI1adJEXl5eaty4sZYtW5Zj/ezt1NvbW3Xr1tWsWbM4bi8PRGgUWmZmpoYNG6ZHHnlEYWFhRXqMOXPm6Ny5cxo4cKC8vLwUEBCg7du369Zbb1W1atU0duxYlS9fXp9++qmio6P12Wef6d5775V04UMtOyyMGzdO5cuX1zvvvCMvL698n/fs2bPq2LGjdu/eraFDhyo0NFQLFixQ//79lZycrBEjRjj1//jjj3Xy5EkNGjRIDodDL7/8srp166Y//vhDZcqUyff5Tp48mevezEt/0EoXPvwmT56syMhIDR48WPHx8ZoxY4Y2bdqkH374wen5/vrrL91xxx26//771bt3b3366acaPHiwPD099fDDD6t9+/b64IMP9I9//EN///vf1bdv33xrLeh4CzJP7du31/DhwzVt2jSNHz9eDRs2lCQ1bNhQf/31l1577TVt377dOjZz3bp1cnNz07p16zR8+HCrTZLat28vqfBzl9s2dmlIPnv2rLp27arNmzfr22+/VcuWLQv0Ol1s7ty5evjhh9W4cWONGzdO/v7+2rp1q5YtW6YHHnigUI/19ttva/jw4erRo4dGjBihc+fO6eeff9aGDRv0wAMPqFu3bvr999/1ySef6LXXXlPlypUl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WVnatWuXtWdGunB2cXJy8lXbtnft2qW//e1v1v1Tp07pyJEjuuOOO6w6pYJt661atZKnp6fWrVundevWWWcEt2/fXm+//bZWrlxp3c9Wt25dffvtt7r11luLdNmSwnx2LVy4UP369dOUKVOstnPnzuU61+3atdPatWsVGhqqZs2aqUKFCmratKn8/Py0bNkybdmyJd/wHhgYKF9fX/3666959qtVq5b1te/FLn2vZO+Rv7Tey+3Ru9qfO1WqVJG3t3eRP++vV3wViwJr0qSJFi1alOPWuHFj1axZU4sWLdKAAQMkSc2bN3f66rFRo0Z5PnaVKlXUsWNHzZo1S0eOHMmx/OjRo9b/o6KiFBsbq23btlltJ06cuOxv6Be74447lJCQoPnz51tt58+f1xtvvCEfHx916NAh38coCZGRkfL09NS0adOcflt99913lZKSojvvvNOp//nz550uy5Cenq5Zs2YpMDDQKQwVt8LMU/aHf24/5EJDQ1WtWjW99tprysjI0K233irpwg+/PXv2aOHChWrdurXTta+Ke+769u2radOmaebMmU6XyiiMzp07q0KFCoqJidG5c+ecll08j3Xr1tWPP/7odDmKJUuW6ODBg07rHD9+3Om+p6enGjVqJGOMMjIyJF3+dc0OTpdegujVV1+VpBzbUEmZPXu2VaskzZgxQ+fPn7eOiSrMtu7t7a2WLVvqk08+0YEDB5z22J09e1bTpk1T3bp1nQ5tuP/++5WZmannn38+R23nz5/P98LOhfnscnd3z7F36Y033sh1j1e7du20b98+zZ8/3xpH9nGEr776qjIyMvLdQ+3m5qbo6Gh9+eWX2rx5c47l2bXccccd2rhxo3UcoiSdPn1as2fPVu3ata0xZf9ikn0co3Rhb93s2bNzff6r/bnj7u6uyMhILV682Olbot27d+vrr78u9uezC/bYocAqV66s6OjoHO3ZP0hyW1YYb775ptq2bauwsDA9+uijqlOnjhITExUbG6s///xTP/30kyTpqaee0ocffqi///3vGjZsmHW5k5o1a+rEiRN57ikaOHCgZs2apf79+ysuLk61a9fWwoUL9cMPP2jq1KmqUKHCFY2hqAIDAzVu3DhNnjxZXbp00T333KP4+Hi99dZbatmypR588EGn/iEhIfr3v/+tffv26cYbb9T8+fO1bds2zZ49u0CXYbkSBZ2nZs2ayd3dXf/+97+VkpIiLy8v3XbbbapSpYqkCz/o5s2bp7CwMGvPwS233KLy5cvr999/z3GpkJKYu6FDhyo1NVX//Oc/5efnp/Hjx1vLHA6HOnTo4HQtr0v5+vrqtdde0yOPPKKWLVvqgQceUMWKFfXTTz/pzJkz1jX2HnnkES1cuFBdunTR/fffrz179ujDDz90OnBcuhAUg4ODdeuttyooKEi//fabpk+frjvvvNMaX/YP0H/+85/q1auXypQpo7vvvltNmzZVv379NHv2bCUnJ6tDhw7auHGj3nvvPUVHRzvtRStJ6enp6tSpk3WJlLfeektt27bVPffcI6nw23q7du300ksvyc/Pz7q8UpUqVVS/fn3Fx8fnuD5ghw4dNGjQIMXExGjbtm3q3LmzypQpo127dmnBggV6/fXX1aNHj2IZ61133aUPPvhAfn5+atSokWJjY/Xtt99al+q4dBzShT2VL774otXevn17ff3119Z1I/Pz4osvavny5erQoYMGDhyohg0b6siRI1qwYIG+//57+fv7a+zYsfrkk090++23a/jw4QoICNB7772nvXv36rPPPrP2eDdu3FitW7fWuHHjdOLECQUEBGjevHk6f/58rs/tis+dSZMmafny5br11ls1ePBgZWZmavr06WrSpInTL/e4iAvOxIXNFOVyJ6+88kquy/fs2WP69u1rgoODTZkyZUy1atXMXXfdZRYuXOjUb+vWraZdu3bGy8vLVK9e3cTExJhp06YZSSYhIcGptosvJ2GMMYmJieahhx4ylStXNp6eniYsLMzp1Pz86tQllzzIzcWX1chNv379nC53km369OmmQYMGpkyZMiYoKMgMHjzYuozDxWNq3Lix2bx5s4mIiDDe3t6mVq1aZvr06bnWWpjLnRR0vAWdp7ffftvUqVPHuLu757h8wptvvmkkmcGDBzutExkZaSSZlStX5qjlSufucvPy1FNPGUnWa3jy5EkjyfTq1SvHY+Tmf//7n2nTpo0pW7as8fX1Na1atTKffPKJU58pU6aYatWqGS8vL3PrrbeazZs359g+Z82aZdq3b28qVapkvLy8TN26dc2TTz5pUlJSnB7r+eefN9WqVTNubm5Ol6vIyMgwkydPNqGhoaZMmTKmRo0aZty4cU6XpjHmwmU+7rzzzhzjyG17ye89my370hlr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s6suXbqoW7duCg0N1Z49ezR+/Hjdfvvtio2Nlbu7u6vLuyJZWVkaOXKkbr31VjVp0kTShbn29PSUv7+/U187zXVu45akBx54QLVq1VJISIh+/vlnPf3004qPj9fnn3/uwmqvzC+//KKIiAidO3dOPj4+WrRokRo1aqRt27bZdp4vN2bJnnMsSfPmzdOWLVu0adOmHMuupfc0wQ7XjNtvv936/0033aTw8HDVqlVLn376qQYMGODCylDSevXqZf0/LCxMN910k+rWravVq1erU6dOLqzsyg0ZMkS//vqr7Y4Xzc/lxj1w4EDr/2FhYapatao6deqkPXv2qG7dule7zGJRv359bdu2TSkpKVq4cKH69eunNWvWuLqsEnW5MTdq1MiWc3zw4EGNGDFCK1askLe3t6vLyRNfxV6DKleuLHd39xxn0yQmJio4ONhFVV19/v7+uvHGG7V7925Xl3JVZM/t9T7vklSnTh1Vrly51M/90KFDtWTJEq1atUrVq1e32oODg5Wenq7k5GSn/naZ68uNOzfh4eGSVKrn2tPTUzfccIOaN2+umJgYNW3aVK+//rqt5/lyY86NHeY4Li5OSUlJuuWWW+Th4SEPDw+tWbNG06ZNk4eHh4KCgq6ZuSbYXYM8PT3VvHlzrVy50mrLysrSypUrnY5hsLtTp05pz549qlq1qqtLuSpCQ0MVHBzsNO+pqanasGHDdTXvkvTnn3/q+PHjpXbujTEaOnSoFi1apO+++06hoaFOy5s3b64yZco4zXV8fLwOHDhQquc6v3HnZtu2bZJUauc6N1lZWUpLS7PtPOcme8y5scMcd+rUSb/88ou2bdtm3Vq0aKE+ffpY/79m5vqqnqqBAps3b57x8vIyc+fONTt27DADBw40/v7+JiEhwdWllZjRo0eb1atXm71795offvjBREZGmsqVK5ukpCRXl1ZsTp48abZu3Wq2bt1qJJlXX33VbN261ezfv98YY8xLL71k/P39zRdffGF+/vln07VrVxMaGmrOnj3r4sqvTF7jPnnypBkzZoyJjY01e/fuNd9++6255ZZbTL169cy5c+dcXXqRDB482Pj5+ZnVq1ebI0eOWLczZ85YfR577DFTs2ZN891335nNmzebiIgIExER4cKqr1x+4969e7d57rnnzObNm83evXvNF198YerUqWPat2/v4sqLbuzYsWbNmjVm79695ueffzZjx441DofDLF++3Bhjz3nOa8x2nOPLufTs32tlrgl217A33njD1KxZ03h6eppWrVqZH3/80dUllaiePXuaqlWrGk9PT1OtWjXTs2dPs3v3bleXVaxWrVplJOW49evXzxhz4ZInzz77rAkKCjJeXl6mU6dOJj4+3rVFF4O8xn3mzBnTuXNnExgYaMqUKWNq1aplHn300VL9S0xuY5Vk5syZY/U5e/asefzxx03FihVNuXLlzL333muOHDniuqKLQX7jPnDggGnfvr0JCAgwXl5e5oYbbjBPPvmkSUlJcW3hV+Dhhx82tWrVMp6eniYwMNB06tTJCnXG2HOe8xqzHef4ci4NdtfKXDuMMebq7R8EAABASeEYOwAAAJsg2AEAANgEwQ4AAMAmCHYAAAA2QbADAACwCYIdAACATRDsAAAAbIJgBwAAYBMEOwAoJqtXr5bD4cjxh8AB4GrhL08AQBF17NhRzZo109SpUyVJ6enpOnHihIKCguRwOFxbHIDrkoerCwAAu/D09FRwcLCrywBwHeOrWAAogv79+2vNmjV6/fXX5XA45HA4NHfuXKevYufOnSt/f38tWbJE9evXV7ly5dSjRw+dOXNG7733nmrXrq2KFStq+PDhyszMtB47LS1NY8aMUbVq1VS+fHmFh4dr9erVrhkogFKFPXYAUASvv/66fv/9dzVp0kTPPfecJGn79u05+p05c0bTpk3TvHnzdPLkSXXr1k333nuv/P39tXTpUv3xxx/q3r27br31VvXs2VOSNHToUO3YsUPz5s1TSEiIFi1apC5duuiXX35RvXr1ruo4AZQuBDsAKAI/Pz95enqqXLly1tevO3fuzNEvIyNDM2bMUN26dSVJPXr00AcffKDExET5+PioUaNG+tvf/qZVq1apZ8+eOnDggObMmaMDBw4oJCREkjRmzBgtW7ZMc+bM0Ysvvnj1Bgmg1CHYAUAJKleunBXqJCkoKEi1a9eWj4+PU1tSUpIk6ZdfflFmZqZuvPFGp8dJS0tTpUqVrk7RAEotgh0AlKAyZco43Xc4HLm2ZWVlSZJOnTold3d3xcXFyd3d3anfxWEQAHJDsAOAIvL09HQ66aE43HzzzcrMzFRSUpLatWtXrI8NwP44KxYAiqh27drasGGD9u3bp2PHjll73a7EjTfeqD59+qhv3776/PPPtXfvXm3cuFExMTH66quviqFqAHZGsAOAIhozZozc3d3VqFEjBQYG6sCBA8XyuHPmzFHfvn01evRo1a9fX9HR0dq0aZNq1qxZLI8PwL74yxMAAAA2wR47AAAAmyDYAQAA2ATBDgAAwCYIdgAAADZBsAMAALAJgh0AAIBNEOwAAABsgmAHAABgEwQ7AAAAmyDYAQAA2ATBDgAAwCYIdgAAADbx/wDZnKeyghCxDQAAAABJRU5ErkJggg==",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "brainmass.viz.plot_timeseries(res['output'], ts=res['ts'])\n",
    "plt.title('4-region Hopf network, custom power-law coupling')\n",
    "plt.tight_layout()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e691fd90",
   "metadata": {},
   "source": [
    "## It composes like the built-ins\n",
    "\n",
    "Because the coupling exposes its parameters as `Param`s, a `Param(..., fit=True)`\n",
    "makes `k` or `p` a trainable knob the `Fitter` / a hand-written loop can optimise\n",
    "(see {doc}`building_a_data_driven_workflow`). And because it consumes prefetch\n",
    "reads rather than raw state, conduction **delays** come for free -- pass a\n",
    "non-zero `delay_time` matrix and the source read is delayed per connection, no\n",
    "change to the coupling code."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "5fa678e9",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-19T09:16:25.111963Z",
     "iopub.status.busy": "2026-06-19T09:16:25.111806Z",
     "iopub.status.idle": "2026-06-19T09:16:25.528756Z",
     "shell.execute_reply": "2026-06-19T09:16:25.527796Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "delayed run finite: True\n"
     ]
    }
   ],
   "source": [
    "# Same coupling, now with conduction delays (distance / speed).\n",
    "dist = np.full((N, N), 10.0)\n",
    "speed = 10.0                       # mm / ms -> 1 ms delays\n",
    "delay_time = (dist / speed) * (1.0 - np.eye(N)) * u.ms\n",
    "node_d = brainmass.HopfStep(N, a=0.2, w=0.3)\n",
    "net_d = PowerLawNetwork(node_d, conn, coupled_var='x', k=0.5, p=1.5,\n",
    "                        delay_time=delay_time,\n",
    "                        delay_init=braintools.init.Uniform(0., 0.05))\n",
    "res_d = brainmass.Simulator(net_d, dt=0.1 * u.ms).run(\n",
    "    40. * u.ms, monitors=lambda m: m.node.x.value)\n",
    "print('delayed run finite:', bool(jnp.isfinite(res_d['output']).all()))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b1ae532b",
   "metadata": {},
   "source": [
    "## Checklist for a new coupling\n",
    "\n",
    "- [ ] a **pure kernel** function: source `(.., N, N)`, target `(.., N)`, conn,\n",
    "      strength -> current `(.., N)`; strip units before any nonlinearity.\n",
    "- [ ] a **`Module`** holding `Param.init(...)` parameters and the prefetch reads.\n",
    "- [ ] `init_state` decorated `@brainstate.nn.call_order(2)` calling\n",
    "      `init_maybe_prefetch` on each read.\n",
    "- [ ] `update` reads `.value()` of each `Param` and calls the kernel.\n",
    "- [ ] (optional) a `@set_module_as('brainmass')` decorator and a `__all__` entry if\n",
    "      you are contributing it upstream, plus a branch in `Network`'s `coupling=`\n",
    "      dispatch.\n",
    "\n",
    "## See Also\n",
    "\n",
    "- {doc}`/howto/custom_coupling` -- using the built-in couplings via `Network`.\n",
    "- {doc}`/concepts/coupling_and_delays` -- the theory of coupling and delays.\n",
    "- {doc}`creating_models` -- the node contract a coupling reads from."
   ]
  }
 ],
 "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.13.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
