{ "cells": [ { "cell_type": "markdown", "id": "40ca96fb-84d5-4ddd-b8c3-08f934800c5f", "metadata": {}, "source": [ "# Extending Guideline\n", "\n", "This notebook shows examples of adding supporting on another probe family.\n", "\n", "* For extending on another electrode selecting method, please see [Selecting](Selecting.ipynb)\n", "* For extending on UI components, please see [Extending_View](Extending_View.ipynb)\n" ] }, { "cell_type": "markdown", "id": "0008d91f-5fc8-4f73-8b28-54590050da4e", "metadata": {}, "source": [ "## Provide supporting for another probe type" ] }, { "cell_type": "markdown", "id": "3be05ee9-639f-431b-8a6a-8a603af0fe18", "metadata": {}, "source": [ "Create a new file `probe_mynpx.py`. Due to `ProbeDesp` is an abstract class, you need to implement all abstract methods in `MyProbeDesp`. \n", "Here use `NpxProbeDesp` to explan each abstract method." ] }, { "cell_type": "code", "execution_count": null, "id": "74c4b768-e567-42f6-ba5f-a999cfe655bf", "metadata": {}, "outputs": [], "source": [ "from neurocarto.probe import ProbeDesp, ElectrodeDesp\n", "\n", "class MyElectrodeDesp(ElectrodeDesp):\n", " ... # extra information\n", " \n", "class MyProbeDesp(ProbeDesp):\n", " ... # implement all abstract methods here\n", " " ] }, { "cell_type": "markdown", "id": "89978a7f-b373-47df-bca9-cfe09c9c7be2", "metadata": {}, "source": [ "You can put the file `probe_mynpx.py` under directory `src/neurocarto/`, then the program (`neurocarto.probe.get_probe_desp`) should be able to find the implementation. You can use command line:\n", "\n", " neurocarto --probe=mynpx\n", " \n", "If you put the file outside the NeuroCarto source root, you need to provide the full moulde path in command line:\n", "\n", " neurocarto --probe=PATH:probe_mynpx:MyProbeDesp" ] }, { "cell_type": "markdown", "id": "55fb29ca-636f-47f9-b0a1-655841815878", "metadata": {}, "source": [ "### ElectrodeDesp\n", "\n", "It is a simple class that only carry the necessary information for each electrode." ] }, { "cell_type": "code", "execution_count": 10, "id": "1859e149-8053-47cb-bd92-bc47178f2a50", "metadata": {}, "outputs": [], "source": [ "from typing import Any, Hashable, ClassVar\n", "from typing_extensions import Self # for Python < 3.11\n", "\n", "class ElectrodeDesp:\n", " \"\"\"An electrode interface for GUI interaction between different electrode implementations.\"\"\"\n", "\n", " x: float # x position in um\n", " y: float # y position in um\n", " electrode: Hashable # for identify\n", " channel: Any # for display in hover\n", " state: int = 0\n", " category: int = 0\n", "\n", " def copy(self, r: ElectrodeDesp, **kwargs) -> Self: ...\n", " def __hash__(self): ...\n", " def __eq__(self, other): ...\n", " def __str__(self): ...\n", " def __repr__(self): ..." ] }, { "cell_type": "markdown", "id": "efe0e56d-74fa-4987-ae11-68ba289bca19", "metadata": {}, "source": [ "You don't need to modify it much, actually, unless you create a new UI component that tries to provide more information for each electrode.\n", "\n", "In `NpxElectrodeDesp`, we only re-define the actual type for some attributes." ] }, { "cell_type": "code", "execution_count": null, "id": "7d85523f-f883-45d4-8a17-9600f149fafd", "metadata": {}, "outputs": [], "source": [ "class NpxElectrodeDesp(ElectrodeDesp):\n", " electrode: tuple[int, int, int] # (shank, column, row)\n", " channel: int" ] }, { "cell_type": "markdown", "id": "987cdd3b-6613-4876-8c11-5f0899ca6a7f", "metadata": {}, "source": [ "For the 3D probe that electrodes are located in 3D space, attribute `x` and `y` should be the projected coordinated, so it can be shown on the screen, without chaning too much code in GUI part." ] }, { "cell_type": "markdown", "id": "55973bfe-8edd-4519-8457-3c47e9c9eeaf", "metadata": {}, "source": [ "### ProbeDesp" ] }, { "cell_type": "markdown", "id": "1c51403f-f60c-498c-b1ab-68d30079c9f8", "metadata": {}, "source": [ "#### Class Declaration\n", "\n", "The class `ProbeDesp[M, E]` is a generic class that carries two type variables: `M` and `E`, \n", "where `M` indicates the type of channelmap, and `E` indicates the type of ElectrodeDesp subclass.\n", "For a particular `ProbeDesp[M, E]` implementation, you need to specify these two type variables when declaring.\n", "\n", "**Note**: The following code blocks use `NpxProbeDesp` as an example, but all `M` and `E` are kept for demonstrating. \n", "In actual implementation, they should be replaced with the actual types." ] }, { "cell_type": "code", "execution_count": 16, "id": "1c8503db-14a7-4068-97bc-4b108eebbfcb", "metadata": {}, "outputs": [], "source": [ "class NpxProbeDesp(ProbeDesp[ChannelMap, NpxElectrodeDesp]):\n", " ... # skip below" ] }, { "cell_type": "markdown", "id": "93a788b4-a8c4-41c4-bfc5-c0a6e0620e88", "metadata": {}, "source": [ "#### Supporting types, electrode states, and categories\n", "\n", "The following three properties provide information on what sub-types of supporting probe type, possible electrode state (selected, unselected, or disabled), and supporting categories. \n", "The GUI will read the returned dict to generate the corresponding UI controls.\n", "\n", "**Predefined states**\n", "\n", "* `STATE_UNUSED`: electrode is not used, and it is selectable.\n", "* `STATE_USED`: electrode is selected.\n", "* `STATE_FORBIDDEN`: electrode is not used, but it is not selectable.\n", "\n", "**Note** : `STATE_FORBIDDEN` is a valid electrode state, but it is handled by the program instead of users, so it does't need to\n", "present in `possible_states`.\n", "\n", "**Predefined categories**\n", "\n", "* `CATE_UNSET`: initial category value\n", "* `CATE_SET`: pre-selected category\n", "* `CATE_FORBIDDEN`: never be selected\n", "* `CATE_LOW`: random selected, less priority" ] }, { "cell_type": "code", "execution_count": 17, "id": "76649bef-5cd7-46fd-bdfe-abdc50cb060b", "metadata": {}, "outputs": [], "source": [ "class NpxProbeDesp:\n", " ... # continue from above\n", " \n", " # specific categories for this selecting method.\n", " CATE_FULL: ClassVar = 11 # full-density category\n", " CATE_HALF: ClassVar = 12 # half-density category\n", " CATE_QUARTER: ClassVar = 13 # quarter-density category\n", " \n", " @property\n", " def supported_type(self) -> dict[str, int]:\n", " return {'Probe description': probe_code} # where probe_code will be used in new_channelmap(probe_type)\n", " @property\n", " def possible_states(self) -> dict[str, int]:\n", " return {'electrode state description': state_code} # where state_code is either STATE_UNUSED, STATE_USED, or STATE_* etc.\n", " @property\n", " def possible_categories(self) -> dict[str, int]:\n", " return {'electrode category description': category_code} # where category_code is either CATE_UNSET, CATE_SET, or CATE_* etc.\n", "\n", " # not abstract methods\n", " \n", " def type_description(self, code: int | None) -> str | None: ...\n", " def state_description(self, state: int) -> str | None: ...\n", " def category_description(self, code: int) -> str | None: ...\n", " \n", " @classmethod\n", " def all_possible_states(cls) -> dict[str, int]: ...\n", " \n", " @classmethod\n", " def all_possible_categories(cls) -> dict[str, int]: ...\n", " \n", " ... # skip below" ] }, { "cell_type": "markdown", "id": "b3073247-321a-44e1-8e8e-f017ff69f4b0", "metadata": {}, "source": [ "#### File IO\n", "\n", "The following property and methods define what files are look at and how to read/write them from/to disk. " ] }, { "cell_type": "code", "execution_count": null, "id": "26f6002b-40a2-449d-aed1-69f0ce3c6c49", "metadata": {}, "outputs": [], "source": [ "class NpxProbeDesp:\n", " ... # continue from above\n", "\n", " # channelmap file\n", " @property\n", " def channelmap_file_suffix(self) -> list[str]:\n", " return ['.imro']\n", " def load_from_file(self, file: Path) -> M: ...\n", " def save_to_file(self, chmap: M, file: Path): ...\n", "\n", " # electrode blueprint\n", " def save_blueprint(self, s: list[E]) -> NDArray[np.int_]: ...\n", " def load_blueprint(self, a: str | Path | NDArray[np.int_], chmap: int | M | list[E]) -> list[E]: ...\n", " \n", " ... # skip below" ] }, { "cell_type": "markdown", "id": "3470a176-54cf-4e43-9f20-42324340c948", "metadata": {}, "source": [ "#### Channelmap editing" ] }, { "cell_type": "code", "execution_count": null, "id": "5d4501f5-063b-49b0-89b4-ae8f63158c85", "metadata": {}, "outputs": [], "source": [ "class NpxProbeDesp:\n", " ... # continue from above\n", "\n", " def channelmap_code(self, chmap: Any | None) -> int | None: ...\n", " def new_channelmap(self, chmap: int | M) -> M: ...\n", " def copy_channelmap(self, chmap: M) -> M: ...\n", " def channelmap_desp(self, chmap: M | None) -> str: ...\n", " def all_electrodes(self, chmap: int | M) -> list[E]: ...\n", " def all_channels(self, chmap: M, electrodes: Iterable[E] = None) -> list[E]: ...\n", " def add_electrode(self, chmap: M, e: E, *, overwrite=False): ...\n", " def del_electrode(self, chmap: M, e: E): ...\n", "\n", " # not abstract methods\n", "\n", " def get_electrode(self, electrodes: Iterable[E], e: Hashable | E) -> E | None: ...\n", " def copy_electrode(self, electrodes: Sequence[E]) -> list[E]: ...\n", " \n", " ... # skip below" ] }, { "cell_type": "markdown", "id": "e2d6e459-43c8-4e46-ad2a-807f50f82ddd", "metadata": {}, "source": [ "#### Probe restriction rules\n", "\n", "Probe restriction rules are defined in the following two methods. \n", "\n", "**Note**: These two methods should be pure methods that do not contain side effects. \n", "For example, `probe_rule` doesn't give different results for the same electrodes `e1`, `e2` inputs.\n", "However, if a probe restriction is context-depend, which means the electrode selecting order makes the side effect of `probe_rule`,\n", "there are some ways to do it:\n", "\n", "1. record the electrode selecting order in `M`, then `probe_rule` becomes a pure method that its return depends on the `M`. (ignore what `probe_rule`'s document said about `M`)\n", "2. write other methods to support `select_electrodes` correctly." ] }, { "cell_type": "code", "execution_count": null, "id": "fdde27f0-e569-45a9-8e79-2d8e7aa11758", "metadata": {}, "outputs": [], "source": [ "class NpxProbeDesp:\n", " ... # continue from above\n", "\n", " def is_valid(self, chmap: M) -> bool: ...\n", " def probe_rule(self, chmap: M, e1: E, e2: E) -> bool: ...\n", "\n", " # not abstract methods\n", "\n", " def invalid_electrodes(self, chmap: M, e: E | Iterable[E], electrodes: Iterable[E]) -> list[E]: ...\n", " \n", " ... # skip below" ] }, { "cell_type": "markdown", "id": "92d7d995-f84c-4323-b587-1c04cc185d6e", "metadata": {}, "source": [ "#### Electrode selection\n", "\n", "**Note**: we keep `kwargs` in the `select_electrodes` signature to provide a way to give extra parameters during electrode selection. \n", "It can be given from the GUI via `ProbeView.selecting_parameters` attribute (or `CartoApp.probe_view.selecting_parameters`)." ] }, { "cell_type": "code", "execution_count": null, "id": "f050d4c1-e6eb-4219-a542-972f1343b66f", "metadata": {}, "outputs": [], "source": [ "class NpxProbeDesp:\n", " ... # continue from above\n", "\n", " def select_electrodes(self, chmap: M, blueprint: list[E], **kwargs) -> M: ...\n", " \n", " ... # skip below" ] }, { "cell_type": "markdown", "id": "e32d9407", "metadata": {}, "source": [ "#### Custom UI components\n", "\n", "You can provide probe-specific UI components. \n", "`NpxProbeDesp` provides, for example, `NpxReferenceControl` for setting the Neuropixels probe's reference electrode.\n", "\n", "For custom UI components, please check [Provide another Bokeh UI component](#Provide-another-Bokeh-UI-component) section." ] }, { "cell_type": "code", "execution_count": 18, "id": "62692e8c", "metadata": {}, "outputs": [], "source": [ "from neurocarto.config import CartoConfig\n", "from neurocarto.views.base import ViewBase\n", "\n", "class NpxProbeDesp:\n", " ... # continue from above\n", "\n", " def extra_controls(self, config: CartoConfig) -> list[type[ViewBase]]: \n", " from .views import NpxReferenceControl\n", " return [NpxReferenceControl]\n", " \n", " ... # skip below" ] }, { "cell_type": "markdown", "id": "31f3cf37", "metadata": {}, "source": [ "#### UI extension\n", "\n", "For some UI components, they may require probe's detail informations, or probe-specific functions. For example, `ElectrodeDensityDataView` require special function to calculate the density along the probe. In NeuroCarto, we use Protocol classes to declare what UI components want. All protocol methods are named starts with `view_ext_`. Once `NpxProbeDesp` declare a method with matched name and signature, then it can be used by the corresponding UI components.\n", "\n", "Use `ElectrodeDensityDataView` for following demostrate. The protocol class `ProbeElectrodeDensityProtocol` declare the wanted function. `NpxProbeDesp` can just copy the function declaration and give implement the code without inheriting the Protocol class.\n", "\n", "When the application is initializing, `ElectrodeDensityDataView` will check whether the probe implement the protocol function, and enable itself only when it does." ] }, { "cell_type": "code", "execution_count": null, "id": "ff003601", "metadata": {}, "outputs": [], "source": [ "class NpxProbeDesp:\n", " ... # continue from above\n", " \n", " def view_ext_electrode_density(self, chmap: M) -> NDArray[np.float_]:\n", " from .stat import npx_electrode_density\n", " return npx_electrode_density(chmap)\n", " \n", " ... # skip below" ] } ], "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.10.13" } }, "nbformat": 4, "nbformat_minor": 5 }