{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# Explore a small MaleCNS circuit\n",
        "\n",
        "Original dotSuper educational companion, 15 September 2026.\n",
        "\n",
        "**Not executed or validated by dotSuper.** This notebook queries real structural data but its dynamics are an intentionally artificial, all-positive propagation model. Its activity is not neural firing rate and its steps are not biological time. For a published neural simulation, use the Shiu/Brian2 route in the [article](https://dotsuper.net/feeds/applied-systems/how-to-build-fruit-fly-brain-simulation).\n",
        "\n",
        "Run cells in order in a fresh local Jupyter environment. You need your own neuPrint account token. No token is written to the result manifest. The selected model is capped at 200 neurons; initial neighbour query responses are not capped to that size.\n",
        "\n",
        "Sources: [MaleCNS](https://male-cns.janelia.org/download/), [neuPrint quick start](https://connectome-neuprint.github.io/neuprint-python/docs/quickstart.html), [query API](https://connectome-neuprint.github.io/neuprint-python/docs/queries.html).\n",
        "\n",
        "Data: follow the MaleCNS CC BY 4.0 attribution requirements. Original dotSuper notebook code is MIT licensed in the companion README. Third-party packages retain their own licenses."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 1. Dependencies\n",
        "\n",
        "This is a starting dependency list, not a validated lockfile. Use a separate environment. The final manifest records installed versions. Installation makes network requests to your configured Python package index; dataset queries contact neuprint.janelia.org."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "%pip install neuprint-python pandas numpy scipy matplotlib"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 2. Configure a small selection and authenticate\n",
        "\n",
        "DNge104 is a documented MaleCNS example, not a claim about a specific behavioural function. Keep the seed selection small. Use a neuPrint token from your own account. If authentication fails, consult the official guide; do not paste a token into public support messages."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "import os\n",
        "import json\n",
        "import hashlib\n",
        "import platform\n",
        "from datetime import datetime, timezone\n",
        "from pathlib import Path\n",
        "from getpass import getpass\n",
        "from importlib.metadata import version\n",
        "\n",
        "import numpy as np\n",
        "import pandas as pd\n",
        "import matplotlib.pyplot as plt\n",
        "from scipy.sparse import coo_matrix, diags\n",
        "from neuprint import Client, NeuronCriteria as NC\n",
        "from neuprint import fetch_neurons, fetch_simple_connections\n",
        "\n",
        "DATASET = \"male-cns:v1.0\"\n",
        "SEED_TYPE = \"DNge104\"\n",
        "MAX_NEURONS = 200\n",
        "MIN_WEIGHT = 5\n",
        "\n",
        "token = os.environ.get(\"NEUPRINT_APPLICATION_CREDENTIALS\") or getpass(\"Your neuPrint token: \")\n",
        "client = Client(\"https://neuprint.janelia.org\", dataset=DATASET, token=token)\n",
        "del token\n",
        "\n",
        "seed_neurons, _ = fetch_neurons(NC(type=SEED_TYPE), client=client)\n",
        "seed_ids = sorted(int(v) for v in seed_neurons[\"bodyId\"].unique())\n",
        "if not seed_ids:\n",
        "    raise ValueError(\"No matching seed cells. Inspect the type and release in the official browser.\")\n",
        "if len(seed_ids) > 10 or len(seed_ids) > MAX_NEURONS:\n",
        "    raise ValueError(\"Choose a smaller seed population, at most 10 cells for this teaching query.\")\n",
        "seed_neurons[[\"bodyId\", \"type\"]]\n",
        ""
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 3. Retrieve neighbours, then the selected internal graph\n",
        "\n",
        "First fetch connections incident to the seed cells. Rank non-seed neighbours by their total incident weight to the seeds. Keep the strongest neighbours within the model cap, then fetch all retained directed edges among the selected cells. This is a sampling rule, not a complete anatomical circuit. The weight cutoff is a teaching choice.\n",
        "\n",
        "Connection pairs returned by both incident queries must not be counted twice."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "outgoing = fetch_simple_connections(\n",
        "    NC(bodyId=seed_ids), None, min_weight=MIN_WEIGHT, properties=[], client=client\n",
        ")\n",
        "incoming = fetch_simple_connections(\n",
        "    None, NC(bodyId=seed_ids), min_weight=MIN_WEIGHT, properties=[], client=client\n",
        ")\n",
        "incident = pd.concat([outgoing, incoming], ignore_index=True)\n",
        "if incident.empty:\n",
        "    raise ValueError(\"No incident connections at this threshold. Inspect the selection before proceeding.\")\n",
        "incident = incident[[\"bodyId_pre\", \"bodyId_post\", \"weight\"]].drop_duplicates(\n",
        "    [\"bodyId_pre\", \"bodyId_post\"]\n",
        ")\n",
        "seed_set = set(seed_ids)\n",
        "scores = {}\n",
        "for pre, post, weight in incident.itertuples(index=False, name=None):\n",
        "    for body in (int(pre), int(post)):\n",
        "        if body not in seed_set:\n",
        "            scores[body] = scores.get(body, 0.0) + float(weight)\n",
        "ranked = sorted(scores, key=lambda body: (-scores[body], body))\n",
        "selected_ids = sorted(seed_ids + ranked[:MAX_NEURONS - len(seed_ids)])\n",
        "\n",
        "edges = fetch_simple_connections(\n",
        "    NC(bodyId=selected_ids), NC(bodyId=selected_ids),\n",
        "    min_weight=MIN_WEIGHT, properties=[], client=client\n",
        ")\n",
        "if edges.empty:\n",
        "    raise ValueError(\"The selected graph contains no retained connections.\")\n",
        "edges = edges[[\"bodyId_pre\", \"bodyId_post\", \"weight\"]].copy()\n",
        "for column in (\"bodyId_pre\", \"bodyId_post\"):\n",
        "    edges[column] = edges[column].map(int)\n",
        "edges = edges.sort_values([\"bodyId_pre\", \"bodyId_post\"]).reset_index(drop=True)\n",
        "neurons, _ = fetch_neurons(NC(bodyId=selected_ids), client=client)\n",
        "neurons = neurons[[\"bodyId\", \"type\"]].copy()\n",
        "neurons[\"bodyId\"] = neurons[\"bodyId\"].map(int)\n",
        "neurons = neurons.sort_values(\"bodyId\").reset_index(drop=True)\n",
        "print(f\"Selected {len(selected_ids)} nodes and {len(edges)} directed edges.\")\n",
        "edges.head()\n",
        ""
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 4. Build an explicitly non-biological propagation model\n",
        "\n",
        "Matrix orientation: rows are postsynaptic recipients, columns are presynaptic senders. Divide each row by its retained incoming weight sum. This destroys absolute synaptic strength information and ignores excluded inputs.\n",
        "\n",
        "The update is `x_next = (1-alpha)*x + alpha*tanh(gain*(W @ x) + input)`. We choose alpha=0.1 and gain=0.8 for a bounded teaching demonstration. These are not fitted physiological parameters. All links are positive; no transmitter or receptor inference is performed.\n",
        "\n",
        "A pulse goes to the seed nodes. The control intervention clamps one non-seed node to zero and removes its incoming/outgoing weights. We do not renormalize the remaining weights after the intervention. Selection by graph strength is an illustrative engineering rule, not a biological finding."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "index = {body: i for i, body in enumerate(selected_ids)}\n",
        "row = edges[\"bodyId_post\"].map(index).to_numpy(dtype=int)\n",
        "col = edges[\"bodyId_pre\"].map(index).to_numpy(dtype=int)\n",
        "weights = edges[\"weight\"].to_numpy(dtype=float)\n",
        "if not np.isfinite(weights).all() or (weights <= 0).any():\n",
        "    raise ValueError(\"This teaching model expects finite positive retained weights.\")\n",
        "\n",
        "n = len(selected_ids)\n",
        "raw = coo_matrix((weights, (row, col)), shape=(n, n)).tocsr()\n",
        "totals = np.asarray(raw.sum(axis=1)).ravel()\n",
        "inverse = np.divide(1.0, totals, out=np.zeros_like(totals), where=totals > 0)\n",
        "W = diags(inverse) @ raw\n",
        "seed_indices = [index[body] for body in seed_ids]\n",
        "non_seed = [body for body in selected_ids if body not in seed_set]\n",
        "if not non_seed:\n",
        "    raise ValueError(\"Need a non-seed node for this particular comparison.\")\n",
        "\n",
        "strength = np.asarray(raw.sum(axis=0)).ravel()\n",
        "ablated_body = max(non_seed, key=lambda body: (strength[index[body]], -body))\n",
        "ablated_index = index[ablated_body]\n",
        "keep = np.ones(n)\n",
        "keep[ablated_index] = 0.0\n",
        "W_ablated = diags(keep) @ W @ diags(keep)\n",
        "\n",
        "STEPS, PULSE_START, PULSE_END = 300, 20, 60\n",
        "ALPHA, GAIN, AMPLITUDE = 0.1, 0.8, 1.0\n",
        "\n",
        "def propagate(matrix, clamp=None):\n",
        "    state = np.zeros(n)\n",
        "    history = np.zeros((STEPS + 1, n))\n",
        "    for step in range(STEPS):\n",
        "        stimulus = np.zeros(n)\n",
        "        if PULSE_START <= step < PULSE_END:\n",
        "            stimulus[seed_indices] = AMPLITUDE\n",
        "        state = (1 - ALPHA) * state + ALPHA * np.tanh(\n",
        "            GAIN * (matrix @ state) + stimulus\n",
        "        )\n",
        "        if clamp is not None:\n",
        "            state[clamp] = 0.0\n",
        "        history[step + 1] = state\n",
        "    return history\n",
        "\n",
        "intact = propagate(W)\n",
        "ablated = propagate(W_ablated, clamp=ablated_index)\n",
        "readout = [index[body] for body in non_seed if body != ablated_body]\n",
        "if not readout:\n",
        "    raise ValueError(\"Need another non-seed node for the comparison readout.\")\n",
        "\n",
        "fig, ax = plt.subplots(figsize=(10, 4))\n",
        "ax.plot(intact[:, readout].mean(axis=1), label=\"Intact toy graph\")\n",
        "ax.plot(ablated[:, readout].mean(axis=1), label=\"One node ablated, toy graph\")\n",
        "ax.axvspan(PULSE_START, PULSE_END, alpha=0.12, color=\"black\", label=\"Input pulse\")\n",
        "ax.set(xlabel=\"Iteration, not biological time\",\n",
        "       ylabel=\"Mean toy activity, arbitrary units\",\n",
        "       title=\"Selected MaleCNS graph: uncalibrated positive propagation\")\n",
        "ax.legend()\n",
        "fig.tight_layout()\n",
        "plt.show()\n",
        ""
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 5. Save results and provenance without credentials\n",
        "\n",
        "A fresh uniquely named directory is created for each export. Keep the manifest with the CSVs. Identifiers in the manifest and exported tables are strings to discourage lossy numeric handling. This does not guarantee that spreadsheet software will preserve them if it auto-converts columns.\n",
        "\n",
        "Package versions are recorded explicitly. Do not replace this with a dump of environment variables, client internals or notebook credentials."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "stamp = datetime.now(timezone.utc).strftime(\"%Y%m%dT%H%M%S%fZ\")\n",
        "output = Path(\"results\") / f\"malecns-teaching-{stamp}\"\n",
        "output.mkdir(parents=True, exist_ok=False)\n",
        "\n",
        "saved_neurons = neurons.copy()\n",
        "saved_neurons[\"bodyId\"] = saved_neurons[\"bodyId\"].map(str)\n",
        "saved_neurons.to_csv(output / \"neurons.csv\", index=False)\n",
        "saved_edges = edges.copy()\n",
        "for column in (\"bodyId_pre\", \"bodyId_post\"):\n",
        "    saved_edges[column] = saved_edges[column].map(str)\n",
        "saved_edges.to_csv(output / \"edges.csv\", index=False)\n",
        "\n",
        "frames = []\n",
        "for condition, history in ((\"intact\", intact), (\"ablated\", ablated)):\n",
        "    frame = pd.DataFrame(history, columns=[str(body) for body in selected_ids])\n",
        "    frame.insert(0, \"iteration\", np.arange(STEPS + 1))\n",
        "    frame.insert(0, \"condition\", condition)\n",
        "    frames.append(frame)\n",
        "pd.concat(frames, ignore_index=True).to_csv(output / \"activity.csv\", index=False)\n",
        "fig.savefig(output / \"activity-comparison.png\", dpi=160)\n",
        "\n",
        "manifest = {\n",
        "    \"created_utc\": datetime.now(timezone.utc).isoformat(),\n",
        "    \"status\": \"Reader-generated educational output; not biological validation\",\n",
        "    \"dataset\": DATASET,\n",
        "    \"source\": \"https://male-cns.janelia.org/download/\",\n",
        "    \"seed_type\": SEED_TYPE,\n",
        "    \"seed_body_ids\": [str(body) for body in seed_ids],\n",
        "    \"selected_body_ids\": [str(body) for body in selected_ids],\n",
        "    \"selection\": \"Seed cells plus strongest incident-weight neighbours; internal edges retained\",\n",
        "    \"max_neurons\": MAX_NEURONS,\n",
        "    \"minimum_connection_weight\": MIN_WEIGHT,\n",
        "    \"model\": \"All-positive row-normalized leaky tanh propagation, arbitrary units\",\n",
        "    \"matrix_orientation\": \"row=post, column=pre\",\n",
        "    \"alpha\": ALPHA, \"gain\": GAIN, \"steps\": STEPS,\n",
        "    \"pulse\": {\"start\": PULSE_START, \"end_exclusive\": PULSE_END, \"amplitude\": AMPLITUDE},\n",
        "    \"ablated_body_id\": str(ablated_body),\n",
        "    \"ablation\": \"Zero incoming/outgoing weights and clamp state; no renormalization\",\n",
        "    \"readout_body_ids\": [str(selected_ids[i]) for i in readout],\n",
        "    \"randomness\": \"None in the toy update; selection tie-breaking uses bodyId\",\n",
        "    \"python\": platform.python_version(),\n",
        "    \"packages\": {name: version(name) for name in\n",
        "                 (\"neuprint-python\", \"numpy\", \"pandas\", \"scipy\", \"matplotlib\")},\n",
        "    \"sha256\": {name: hashlib.sha256((output / name).read_bytes()).hexdigest()\n",
        "               for name in (\"neurons.csv\", \"edges.csv\")},\n",
        "    \"limitations\": [\n",
        "        \"No transmitter signs, receptors, neural units, measured dynamics or physical body\",\n",
        "        \"Truncated graph omits inputs, outputs and feedback through excluded neurons\",\n",
        "        \"Activity differences are properties of this toy model, not biological conclusions\"\n",
        "    ]\n",
        "}\n",
        "(output / \"manifest.json\").write_text(json.dumps(manifest, indent=2) + \"\\n\", encoding=\"utf-8\")\n",
        "print(f\"Results written to {output}\")\n",
        ""
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 6. Interpret, do not overclaim\n",
        "\n",
        "Write down the selected circuit, the modelling assumptions and the observed comparison. No difference between curves is also a valid model outcome. This comparison does not prove that a real fly requires the ablated cell for any behaviour.\n",
        "\n",
        "For further work, separate structural sampling from physiological modelling. Add documented cell dynamics, explicit units, evidence-backed synaptic rules and evaluation against biological measurements. Use the published Brian2 model for a researched neural-simulation starting point.\n",
        "\n",
        "[Return to the full guide](https://dotsuper.net/feeds/applied-systems/how-to-build-fruit-fly-brain-simulation) | [Commands and license](https://dotsuper.net/guides/fruit-fly-simulation/README.md)"
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python"
    },
    "dotsuper": {
      "prepared": "2026-09-15",
      "execution_status": "not_executed",
      "purpose": "educational_graph_propagation_not_biological_simulation"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}

