{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a1b2c3d4",
   "metadata": {},
   "source": [
    "# Operators for Online Learning\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b2c3d4e5",
   "metadata": {},
   "source": [
    "ETP operators are the first layer of an online-learning-ready network. They mark the parameterized computations that the compiler should connect to temporal hidden state. This chapter introduces the five public operators, then verifies their physical-unit and JAX-transformation contracts.\n",
    "\n",
    "See the complete [ETP Operators API](../apis/concepts.rst) for signatures and generated reference pages.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "c3d4e5f6",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T05:33:29.896943Z",
     "iopub.status.busy": "2026-07-09T05:33:29.896770Z",
     "iopub.status.idle": "2026-07-09T05:33:32.318106Z",
     "shell.execute_reply": "2026-07-09T05:33:32.317189Z"
    }
   },
   "outputs": [],
   "source": [
    "import brainstate\n",
    "import brainunit as u\n",
    "import jax\n",
    "import jax.numpy as jnp\n",
    "\n",
    "import braintrace\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d4e5f6a7",
   "metadata": {},
   "source": [
    "## Function reference\n",
    "\n",
    "`braintrace` provides five user-facing ETP operator functions:\n",
    "\n",
    "| Function | Underlying primitives | Purpose |\n",
    "|---|---|---|\n",
    "| {func}`braintrace.matmul` | `etp_mm_p` (batched) / `etp_mv_p` (unbatched) | Dense matrix multiplication |\n",
    "| {func}`braintrace.element_wise` | `etp_elemwise_p` | Element-wise (diagonal) weight ops |\n",
    "| {func}`braintrace.conv` | `etp_conv_p` | Convolution |\n",
    "| {func}`braintrace.sparse_matmul` | `etp_sp_mm_p` / `etp_sp_mv_p` | Sparse matrix multiplication |\n",
    "| {func}`braintrace.lora_matmul` | `etp_lora_mm_p` / `etp_lora_mv_p` | LoRA (Low-Rank Adaptation) matmul |\n",
    "\n",
    "Each function auto-dispatches between batched and unbatched variants based on input dimensionality. The generated pages above are indexed together in the [ETP Operators API](../apis/concepts.rst).\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e5f6a7b8",
   "metadata": {},
   "source": [
    "### 1. `braintrace.matmul(x, weight, bias=None)` -- Dense Matrix Multiplication\n",
    "\n",
    "Computes $y = x \\, @ \\, w \\; (+ b)$.\n",
    "\n",
    "Auto-dispatches based on `x.ndim`:\n",
    "- `x.ndim >= 2` --> `etp_mm_p` (batched): expects `x` of shape `(batch, in_features)`\n",
    "- `x.ndim == 1` --> `etp_mv_p` (unbatched): expects `x` of shape `(in_features,)`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "f6a7b8c9",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T05:33:32.320214Z",
     "iopub.status.busy": "2026-07-09T05:33:32.319961Z",
     "iopub.status.idle": "2026-07-09T05:33:32.525704Z",
     "shell.execute_reply": "2026-07-09T05:33:32.524759Z"
    }
   },
   "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"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Batched output shape: (4, 5)\n",
      "Unbatched output shape: (5,)\n"
     ]
    }
   ],
   "source": [
    "# Batched matmul: x has shape (batch, in_features)\n",
    "x_batched = jnp.ones((4, 3))    # batch=4, in_features=3\n",
    "w = jnp.ones((3, 5))            # in_features=3, out_features=5\n",
    "\n",
    "y_batched = braintrace.matmul(x_batched, w)\n",
    "print(\"Batched output shape:\", y_batched.shape)   # (4, 5)\n",
    "\n",
    "# Unbatched matmul: x has shape (in_features,)\n",
    "x_single = jnp.ones((3,))       # in_features=3\n",
    "\n",
    "y_single = braintrace.matmul(x_single, w)\n",
    "print(\"Unbatched output shape:\", y_single.shape)   # (5,)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "a7b8c9d0",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T05:33:32.528326Z",
     "iopub.status.busy": "2026-07-09T05:33:32.528149Z",
     "iopub.status.idle": "2026-07-09T05:33:32.582644Z",
     "shell.execute_reply": "2026-07-09T05:33:32.581735Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "With bias: (4, 5)\n"
     ]
    }
   ],
   "source": [
    "# With bias\n",
    "b = jnp.zeros((5,))\n",
    "\n",
    "y_with_bias = braintrace.matmul(x_batched, w, bias=b)\n",
    "print(\"With bias:\", y_with_bias.shape)              # (4, 5)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b8c9d0e1",
   "metadata": {},
   "source": [
    "### 2. `braintrace.element_wise(weight, *, weight_fn=None)` — Element-wise Operation\n",
    "\n",
    "Applies `weight_fn` to the weight and passes the result through a marker primitive. The operation is treated as *diagonal* in the hidden-state space.\n",
    "\n",
    "$$y = \\texttt{weight\\_fn}(w)$$\n",
    "\n",
    "`weight_fn` defaults to `None` (identity); supply any JAX-differentiable function when you want a non-trivial transformation.\n",
    "\n",
    "Common use cases:\n",
    "- Gating mechanisms in RNNs (learnable gate biases)\n",
    "- Learnable time constants or thresholds in spiking neural networks\n",
    "- Any parameter that enters the computation element-wise\n",
    "\n",
    "Note: `etp_elemwise_p` is the only primitive registered with `gradient_enabled=True`. The compiler *descends into* it when walking ``y -> h``, so it does not act as a tail boundary for upstream ETP weights. See the *gradient_enabled Flag* section below for details."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "c9d0e1f2",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T05:33:32.584737Z",
     "iopub.status.busy": "2026-07-09T05:33:32.584433Z",
     "iopub.status.idle": "2026-07-09T05:33:32.651206Z",
     "shell.execute_reply": "2026-07-09T05:33:32.650196Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Identity: [ 0.5 -0.3  0.8  0.1]\n",
      "Sigmoid: [0.62245935 0.4255575  0.6899745  0.5249792 ]\n",
      "Abs: [0.5 0.3 0.8 0.1]\n"
     ]
    }
   ],
   "source": [
    "# Identity (default weight_fn): just marks the weight for ETP\n",
    "w_gate = jnp.array([0.5, -0.3, 0.8, 0.1])\n",
    "\n",
    "y_identity = braintrace.element_wise(w_gate)\n",
    "print(\"Identity:\", y_identity)\n",
    "\n",
    "# With a transformation function\n",
    "y_sigmoid = braintrace.element_wise(w_gate, weight_fn=jax.nn.sigmoid)\n",
    "print(\"Sigmoid:\", y_sigmoid)\n",
    "\n",
    "# With absolute value (e.g., enforcing positive time constants)\n",
    "y_abs = braintrace.element_wise(w_gate, weight_fn=jnp.abs)\n",
    "print(\"Abs:\", y_abs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d0e1f2a3",
   "metadata": {},
   "source": [
    "### 3. `braintrace.conv(x, kernel, bias=None, *, strides, padding, ...)` -- Convolution\n",
    "\n",
    "ETP-aware convolution that wraps `jax.lax.conv_general_dilated`. Computes:\n",
    "\n",
    "$$y = \\text{conv}(x, \\text{kernel}) \\; (+ b)$$\n",
    "\n",
    "**Important**: Always expects a batch dimension on `x`.\n",
    "\n",
    "Supports all parameters of `jax.lax.conv_general_dilated`: `strides`, `padding`, `lhs_dilation`, `rhs_dilation`, `feature_group_count`, `batch_group_count`, and `dimension_numbers`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "e1f2a3b4",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T05:33:32.653207Z",
     "iopub.status.busy": "2026-07-09T05:33:32.653040Z",
     "iopub.status.idle": "2026-07-09T05:33:32.741742Z",
     "shell.execute_reply": "2026-07-09T05:33:32.740889Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Conv1D output shape: (2, 16, 8)\n"
     ]
    }
   ],
   "source": [
    "# 1D convolution example\n",
    "# x: (batch, spatial, channels) with dimension_numbers\n",
    "x_1d = jnp.ones((2, 16, 3))         # batch=2, length=16, in_channels=3\n",
    "kernel_1d = jnp.ones((4, 3, 8))     # kernel_size=4, in_channels=3, out_channels=8\n",
    "\n",
    "y_conv = braintrace.conv(\n",
    "    x_1d, kernel_1d,\n",
    "    strides=(1,),\n",
    "    padding='SAME',\n",
    "    dimension_numbers=('NWC', 'WIO', 'NWC'),\n",
    ")\n",
    "print(\"Conv1D output shape:\", y_conv.shape)  # (2, 16, 8)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "f2a3b4c5",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T05:33:32.743904Z",
     "iopub.status.busy": "2026-07-09T05:33:32.743708Z",
     "iopub.status.idle": "2026-07-09T05:33:32.833682Z",
     "shell.execute_reply": "2026-07-09T05:33:32.832764Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Conv2D output shape: (2, 32, 32, 16)\n"
     ]
    }
   ],
   "source": [
    "# 2D convolution example\n",
    "x_2d = jnp.ones((2, 32, 32, 3))          # batch=2, H=32, W=32, in_channels=3\n",
    "kernel_2d = jnp.ones((3, 3, 3, 16))      # kH=3, kW=3, in_channels=3, out_channels=16\n",
    "\n",
    "y_conv2d = braintrace.conv(\n",
    "    x_2d, kernel_2d,\n",
    "    strides=(1, 1),\n",
    "    padding='SAME',\n",
    "    dimension_numbers=('NHWC', 'HWIO', 'NHWC'),\n",
    ")\n",
    "print(\"Conv2D output shape:\", y_conv2d.shape)  # (2, 32, 32, 16)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a3b4c5d6",
   "metadata": {},
   "source": [
    "### 4. `braintrace.sparse_matmul(x, weight_data, *, sparse_mat, bias=None)` -- Sparse Matmul\n",
    "\n",
    "ETP-aware sparse matrix multiplication. Computes:\n",
    "\n",
    "$$y = x \\, @ \\, \\text{sparse}(w) \\; (+ b)$$\n",
    "\n",
    "The `sparse_mat` argument provides the sparse structure (indices), while `weight_data` contains only the non-zero values. This is useful for models with sparse connectivity patterns, such as biologically plausible neural networks or graph neural networks."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b4c5d6e7",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T05:33:32.835474Z",
     "iopub.status.busy": "2026-07-09T05:33:32.835337Z",
     "iopub.status.idle": "2026-07-09T05:33:35.770728Z",
     "shell.execute_reply": "2026-07-09T05:33:35.769886Z"
    }
   },
   "outputs": [],
   "source": [
    "import brainevent\n",
    "\n",
    "# Create a reproducible sparse connectivity matrix\n",
    "brainstate.random.seed(13)\n",
    "dense_w = jnp.where(\n",
    "    brainstate.random.uniform(size=(50, 50)) < 0.1,\n",
    "    brainstate.random.normal(size=(50, 50)),\n",
    "    0.0\n",
    ")\n",
    "# sparse_mat must be a brainevent.DataRepresentation (e.g. brainevent.CSR),\n",
    "# which implements the with_data / dt2t_transposed / dt2t protocol.\n",
    "sparse_mat = brainevent.CSR.fromdense(dense_w)\n",
    "\n",
    "# The learnable parameter is just the non-zero data\n",
    "weight_data = sparse_mat.data\n",
    "\n",
    "x_sp = jnp.ones((4, 50))  # batch=4, features=50\n",
    "y_sp = braintrace.sparse_matmul(x_sp, weight_data, sparse_mat=sparse_mat)\n",
    "print(\"Sparse matmul output shape:\", y_sp.shape)  # (4, 50)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c5d6e7f8",
   "metadata": {},
   "source": [
    "### 5. `braintrace.lora_matmul(x, B, A, *, alpha=1.0, bias=None)` -- LoRA Matmul\n",
    "\n",
    "Low-Rank Adaptation matmul. Computes:\n",
    "\n",
    "$$y = \\alpha \\cdot x \\, @ \\, B \\, @ \\, A \\; (+ b)$$\n",
    "\n",
    "where $B \\in \\mathbb{R}^{\\text{in} \\times \\text{rank}}$ and $A \\in \\mathbb{R}^{\\text{rank} \\times \\text{out}}$ are low-rank factors. This is useful for parameter-efficient fine-tuning of large models, where only the low-rank factors are trained."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d6e7f8a9",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T05:33:35.773052Z",
     "iopub.status.busy": "2026-07-09T05:33:35.772887Z",
     "iopub.status.idle": "2026-07-09T05:33:36.321372Z",
     "shell.execute_reply": "2026-07-09T05:33:36.320482Z"
    }
   },
   "outputs": [],
   "source": [
    "in_features, out_features, rank = 64, 32, 4\n",
    "\n",
    "brainstate.random.seed(17)\n",
    "B = brainstate.random.normal(size=(in_features, rank)) * 0.01\n",
    "A = brainstate.random.normal(size=(rank, out_features)) * 0.01\n",
    "\n",
    "x_lora = jnp.ones((8, in_features))  # batch=8\n",
    "\n",
    "y_lora = braintrace.lora_matmul(x_lora, B, A, alpha=2.0)\n",
    "print(\"LoRA output shape:\", y_lora.shape)  # (8, 32)\n",
    "print(\"LoRA output (first sample):\", y_lora[0])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4edae675",
   "metadata": {},
   "source": [
    "## Physical Units (`brainunit` / Quantity) Support\n",
    "\n",
    "Every user-facing ETP function accepts `brainunit.Quantity` inputs. The wrapper separates mantissas and units, binds the JAX primitive to plain arrays, and recombines the result with `u.maybe_decimal`. Bias values must be dimensionally compatible with the combined input and weight unit.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "1b2c54cf",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T05:33:36.323365Z",
     "iopub.status.busy": "2026-07-09T05:33:36.323086Z",
     "iopub.status.idle": "2026-07-09T05:33:36.359191Z",
     "shell.execute_reply": "2026-07-09T05:33:36.358273Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Output: [[3. 3. 3. 3. 3.]\n",
      " [3. 3. 3. 3. 3.]\n",
      " [3. 3. 3. 3. 3.]\n",
      " [3. 3. 3. 3. 3.]] A\n",
      "Unit : A\n"
     ]
    }
   ],
   "source": [
    "# Quantity-valued inputs pass through unchanged.\n",
    "x_q = jnp.ones((4, 3)) * u.volt          # shape (4, 3), unit = V\n",
    "w_q = jnp.ones((3, 5)) * u.siemens       # shape (3, 5), unit = S\n",
    "b_q = jnp.zeros((5,)) * u.amp             # must match V * S = A\n",
    "\n",
    "y_q = braintrace.matmul(x_q, w_q, bias=b_q)\n",
    "print(\"Output:\", y_q)\n",
    "print(\"Unit :\", u.get_unit(y_q))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e7f8a9b0",
   "metadata": {},
   "source": [
    "## JAX Compatibility\n",
    "\n",
    "ETP operators participate in standard JAX transformations such as `jit`, `grad`, `vmap`, and `jvp`. Numerical compatibility does not by itself guarantee ETP compiler recognition: a transformation may decompose a marker primitive into ordinary JAX operations. When a transformed function will be compiled for online learning, inspect the compiled graph and prefer the direct batched ETP operator where necessary.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "f8a9b0c1",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T05:33:36.361884Z",
     "iopub.status.busy": "2026-07-09T05:33:36.361676Z",
     "iopub.status.idle": "2026-07-09T05:33:36.385708Z",
     "shell.execute_reply": "2026-07-09T05:33:36.384820Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "JIT output shape: (4, 5)\n"
     ]
    }
   ],
   "source": [
    "x = jnp.ones((4, 3))\n",
    "w = jnp.ones((3, 5))\n",
    "\n",
    "# ---- JIT compilation ----\n",
    "y_jit = jax.jit(braintrace.matmul)(x, w)\n",
    "print(\"JIT output shape:\", y_jit.shape)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "a9b0c1d2",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T05:33:36.387510Z",
     "iopub.status.busy": "2026-07-09T05:33:36.387355Z",
     "iopub.status.idle": "2026-07-09T05:33:36.575134Z",
     "shell.execute_reply": "2026-07-09T05:33:36.574189Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Gradient shape: (3, 5)\n",
      "Gradient values:\n",
      " [[4. 4. 4. 4. 4.]\n",
      " [4. 4. 4. 4. 4.]\n",
      " [4. 4. 4. 4. 4.]]\n"
     ]
    }
   ],
   "source": [
    "# ---- Gradient computation ----\n",
    "grad_fn = jax.grad(lambda w: jnp.sum(braintrace.matmul(x, w)))\n",
    "dw = grad_fn(w)\n",
    "print(\"Gradient shape:\", dw.shape)\n",
    "print(\"Gradient values:\\n\", dw)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "b0c1d2e3",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T05:33:36.578476Z",
     "iopub.status.busy": "2026-07-09T05:33:36.578199Z",
     "iopub.status.idle": "2026-07-09T05:33:36.651220Z",
     "shell.execute_reply": "2026-07-09T05:33:36.650466Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "vmap output shape: (8, 4, 5)\n"
     ]
    }
   ],
   "source": [
    "# ---- Vectorized mapping (vmap) ----\n",
    "# vmap over a batch of inputs, each of shape (4, 3)\n",
    "xs = jnp.ones((8, 4, 3))  # 8 different batches\n",
    "vmap_fn = jax.vmap(lambda x_i: braintrace.matmul(x_i, w))\n",
    "ys = vmap_fn(xs)\n",
    "print(\"vmap output shape:\", ys.shape)  # (8, 4, 5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "c1d2e3f4",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T05:33:36.653251Z",
     "iopub.status.busy": "2026-07-09T05:33:36.653079Z",
     "iopub.status.idle": "2026-07-09T05:33:36.707484Z",
     "shell.execute_reply": "2026-07-09T05:33:36.706614Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "JVP primal shape: (4, 5)\n",
      "JVP tangent shape: (4, 5)\n"
     ]
    }
   ],
   "source": [
    "# ---- JVP (forward-mode differentiation) ----\n",
    "primals = (x, w)\n",
    "tangents = (jnp.ones_like(x), jnp.ones_like(w))\n",
    "\n",
    "y_primal, y_tangent = jax.jvp(braintrace.matmul, primals, tangents)\n",
    "print(\"JVP primal shape:\", y_primal.shape)\n",
    "print(\"JVP tangent shape:\", y_tangent.shape)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "d2e3f4a5",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-07-09T05:33:36.709443Z",
     "iopub.status.busy": "2026-07-09T05:33:36.709247Z",
     "iopub.status.idle": "2026-07-09T05:33:36.759603Z",
     "shell.execute_reply": "2026-07-09T05:33:36.758480Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Per-sample gradients shape: (8, 3, 5)\n"
     ]
    }
   ],
   "source": [
    "# ---- Composability: JIT + grad + vmap ----\n",
    "@jax.jit\n",
    "def batched_grad(xs, w):\n",
    "    \"\"\"Compute per-sample gradients w.r.t. the weight.\"\"\"\n",
    "    def single_grad(x_i):\n",
    "        return jax.grad(lambda w_: jnp.sum(braintrace.matmul(x_i, w_)))(w)\n",
    "    return jax.vmap(single_grad)(xs)\n",
    "\n",
    "xs = jnp.ones((8, 4, 3))\n",
    "per_sample_grads = batched_grad(xs, w)\n",
    "print(\"Per-sample gradients shape:\", per_sample_grads.shape)  # (8, 3, 5)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "five-primitives-next",
   "metadata": {},
   "source": [
    "## Next steps\n",
    "\n",
    "Continue with [Neural Network Layers for Online Learning](neural_network_layers.ipynb) to compose these operators into reusable models, then read [Hidden States for Online Learning](hidden_states.ipynb) to make those models temporal. Extension authors can continue to [Creating Custom ETP Primitives](../advanced/etp_primitives.ipynb).\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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
}
