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 valueCATE_SET: pre-selected categoryCATE_FORBIDDEN: never be selectedCATE_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:
record the electrode selecting order in
M, thenprobe_rulebecomes a pure method that its return depends on theM. (ignore whatprobe_rule’s document said aboutM)write other methods to support
select_electrodescorrectly.
[ ]:
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