Extending Guideline

This notebook shows examples of adding supporting on another probe family.

  • For extending on another electrode selecting method, please see Selecting

  • For extending on UI components, please see Extending_View

Provide supporting for another probe type

Create a new file probe_mynpx.py. Due to ProbeDesp is an abstract class, you need to implement all abstract methods in MyProbeDesp. Here use NpxProbeDesp to explan each abstract method.

[ ]:
from neurocarto.probe import ProbeDesp, ElectrodeDesp

class MyElectrodeDesp(ElectrodeDesp):
    ... # extra information

class MyProbeDesp(ProbeDesp):
    ... # implement all abstract methods here

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:

neurocarto --probe=mynpx

If you put the file outside the NeuroCarto source root, you need to provide the full moulde path in command line:

neurocarto --probe=PATH:probe_mynpx:MyProbeDesp

ElectrodeDesp

It is a simple class that only carry the necessary information for each electrode.

[10]:
from typing import Any, Hashable, ClassVar
from typing_extensions import Self # for Python < 3.11

class ElectrodeDesp:
    """An electrode interface for GUI interaction between different electrode implementations."""

    x: float  # x position in um
    y: float  # y position in um
    electrode: Hashable  # for identify
    channel: Any  # for display in hover
    state: int = 0
    category: int = 0

    def copy(self, r: ElectrodeDesp, **kwargs) -> Self: ...
    def __hash__(self): ...
    def __eq__(self, other): ...
    def __str__(self): ...
    def __repr__(self): ...

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.

In NpxElectrodeDesp, we only re-define the actual type for some attributes.

[ ]:
class NpxElectrodeDesp(ElectrodeDesp):
    electrode: tuple[int, int, int]  # (shank, column, row)
    channel: int

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.

ProbeDesp

Class Declaration

The class ProbeDesp[M, E] is a generic class that carries two type variables: M and E, where M indicates the type of channelmap, and E indicates the type of ElectrodeDesp subclass. For a particular ProbeDesp[M, E] implementation, you need to specify these two type variables when declaring.

Note: The following code blocks use NpxProbeDesp as an example, but all M and E are kept for demonstrating. In actual implementation, they should be replaced with the actual types.

[16]:
class NpxProbeDesp(ProbeDesp[ChannelMap, NpxElectrodeDesp]):
    ... # skip below

Supporting types, electrode states, and categories

The following three properties provide information on what sub-types of supporting probe type, possible electrode state (selected, unselected, or disabled), and supporting categories. The GUI will read the returned dict to generate the corresponding UI controls.

Predefined states

  • STATE_UNUSED: electrode is not used, and it is selectable.

  • STATE_USED: electrode is selected.

  • STATE_FORBIDDEN: electrode is not used, but it is not selectable.

Note : STATE_FORBIDDEN is a valid electrode state, but it is handled by the program instead of users, so it does’t need to present in possible_states.

Predefined categories

  • CATE_UNSET: initial category value

  • CATE_SET: pre-selected category

  • CATE_FORBIDDEN: never be selected

  • CATE_LOW: random selected, less priority

[17]:
class NpxProbeDesp:
    ... # continue from above

    # specific categories for this selecting method.
    CATE_FULL: ClassVar = 11 # full-density category
    CATE_HALF: ClassVar = 12 # half-density category
    CATE_QUARTER: ClassVar = 13 # quarter-density category

    @property
    def supported_type(self) -> dict[str, int]:
        return {'Probe description': probe_code} # where probe_code will be used in new_channelmap(probe_type)
    @property
    def possible_states(self) -> dict[str, int]:
        return {'electrode state description': state_code} # where state_code is either STATE_UNUSED, STATE_USED, or STATE_* etc.
    @property
    def possible_categories(self) -> dict[str, int]:
        return {'electrode category description': category_code} # where category_code is either CATE_UNSET, CATE_SET, or CATE_* etc.

    # not abstract methods

    def type_description(self, code: int | None) -> str | None: ...
    def state_description(self, state: int) -> str | None: ...
    def category_description(self, code: int) -> str | None: ...

    @classmethod
    def all_possible_states(cls) -> dict[str, int]: ...

    @classmethod
    def all_possible_categories(cls) -> dict[str, int]: ...

    ... # skip below

File IO

The following property and methods define what files are look at and how to read/write them from/to disk.

[ ]:
class NpxProbeDesp:
    ... # continue from above

    # channelmap file
    @property
    def channelmap_file_suffix(self) -> list[str]:
        return ['.imro']
    def load_from_file(self, file: Path) -> M: ...
    def save_to_file(self, chmap: M, file: Path): ...

    # electrode blueprint
    def save_blueprint(self, s: list[E]) -> NDArray[np.int_]: ...
    def load_blueprint(self, a: str | Path | NDArray[np.int_], chmap: int | M | list[E]) -> list[E]: ...

    ... # skip below

Channelmap editing

[ ]:
class NpxProbeDesp:
    ... # continue from above

    def channelmap_code(self, chmap: Any | None) -> int | None: ...
    def new_channelmap(self, chmap: int | M) -> M: ...
    def copy_channelmap(self, chmap: M) -> M: ...
    def channelmap_desp(self, chmap: M | None) -> str: ...
    def all_electrodes(self, chmap: int | M) -> list[E]: ...
    def all_channels(self, chmap: M, electrodes: Iterable[E] = None) -> list[E]: ...
    def add_electrode(self, chmap: M, e: E, *, overwrite=False): ...
    def del_electrode(self, chmap: M, e: E): ...

    # not abstract methods

    def get_electrode(self, electrodes: Iterable[E], e: Hashable | E) -> E | None: ...
    def copy_electrode(self, electrodes: Sequence[E]) -> list[E]: ...

    ... # skip below

Probe restriction rules

Probe restriction rules are defined in the following two methods.

Note: These two methods should be pure methods that do not contain side effects. For example, probe_rule doesn’t give different results for the same electrodes e1, e2 inputs. However, if a probe restriction is context-depend, which means the electrode selecting order makes the side effect of probe_rule, there are some ways to do it:

  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)

  2. write other methods to support select_electrodes correctly.

[ ]:
class NpxProbeDesp:
    ... # continue from above

    def is_valid(self, chmap: M) -> bool: ...
    def probe_rule(self, chmap: M, e1: E, e2: E) -> bool: ...

    # not abstract methods

    def invalid_electrodes(self, chmap: M, e: E | Iterable[E], electrodes: Iterable[E]) -> list[E]: ...

    ... # skip below

Electrode selection

Note: we keep kwargs in the select_electrodes signature to provide a way to give extra parameters during electrode selection. It can be given from the GUI via ProbeView.selecting_parameters attribute (or CartoApp.probe_view.selecting_parameters).

[ ]:
class NpxProbeDesp:
    ... # continue from above

    def select_electrodes(self, chmap: M, blueprint: list[E], **kwargs) -> M: ...

    ... # skip below

Custom UI components

You can provide probe-specific UI components. NpxProbeDesp provides, for example, NpxReferenceControl for setting the Neuropixels probe’s reference electrode.

For custom UI components, please check Provide another Bokeh UI component section.

[18]:
from neurocarto.config import CartoConfig
from neurocarto.views.base import ViewBase

class NpxProbeDesp:
    ... # continue from above

    def extra_controls(self, config: CartoConfig) -> list[type[ViewBase]]:
        from .views import NpxReferenceControl
        return [NpxReferenceControl]

    ... # skip below

UI extension

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.

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.

When the application is initializing, ElectrodeDensityDataView will check whether the probe implement the protocol function, and enable itself only when it does.

[ ]:
class NpxProbeDesp:
    ... # continue from above

    def view_ext_electrode_density(self, chmap: M) -> NDArray[np.float_]:
        from .stat import npx_electrode_density
        return npx_electrode_density(chmap)

    ... # skip below