Skip to content

Better Neuroscope support. #3862

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
61 changes: 61 additions & 0 deletions src/spikeinterface/extractors/neoextractors/neuroscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import warnings
from pathlib import Path
from typing import Union, Optional
from xml.etree import ElementTree as Etree

import numpy as np

Expand Down Expand Up @@ -64,6 +65,8 @@ def __init__(
if xml_file_path is not None:
xml_file_path = str(Path(xml_file_path).absolute())
self._kwargs.update(dict(file_path=str(Path(file_path).absolute()), xml_file_path=xml_file_path))
self.xml_file_path = xml_file_path if xml_file_path is not None else Path(file_path).with_suffix(".xml")
self._set_groups()

@classmethod
def map_to_neo_kwargs(cls, file_path, xml_file_path=None):
Expand All @@ -78,6 +81,64 @@ def map_to_neo_kwargs(cls, file_path, xml_file_path=None):

return neo_kwargs

def _parse_xml_file(self, xml_file_path):
"""
Comes from NeuroPhy package by Diba Lab
"""
tree = Etree.parse(xml_file_path)
myroot = tree.getroot()

for sf in myroot.findall("acquisitionSystem"):
n_channels = int(sf.find("nChannels").text)

channel_groups, skipped_channels, anatomycolors = [], [], {}
for x in myroot.findall("anatomicalDescription"):
for y in x.findall("channelGroups"):
for z in y.findall("group"):
chan_group = []
for chan in z.findall("channel"):
if int(chan.attrib["skip"]) == 1:
skipped_channels.append(int(chan.text))

chan_group.append(int(chan.text))
if chan_group:
channel_groups.append(np.array(chan_group))

for x in myroot.findall("neuroscope"):
for y in x.findall("channels"):
for i, z in enumerate(y.findall("channelColors")):
try:
channel_id = str(z.find("channel").text)
color = z.find("color").text

except AttributeError:
channel_id = i
color = "#0080ff"
anatomycolors[channel_id] = color

discarded_channels = [ch for ch in range(n_channels) if all(ch not in group for group in channel_groups)]
kept_channels = [ch for ch in range(n_channels) if ch not in skipped_channels and ch not in discarded_channels]

return channel_groups, kept_channels, discarded_channels, anatomycolors

def _set_groups(self):
"""
Set the group ids and colors based on the xml file.
These group ids are usually different brain/body anatomical areas, or shanks from multi-shank probes.
The group ids are set as a property of the recording extractor.
"""
n = self.get_num_channels()
group_ids = np.full(n, -1, dtype=int) # Initialize all positions to -1

channel_groups, kept_channels, discarded_channels, colors = self._parse_xml_file(self.xml_file_path)
for group_id, numbers in enumerate(channel_groups):
group_ids[numbers] = group_id # Assign group_id to the positions in `numbers`
self.set_property("group", group_ids)
discarded_ppty = np.full(n, False, dtype=bool)
discarded_ppty[discarded_channels] = True
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does discarded channel mean?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it just means it wont be shown in the final viewer and was probably considered as noise/unplugged channel on the eib

self.set_property("discarded_channels", discarded_ppty)
self.set_property("colors", values=list(colors.values()), ids=list(colors.keys()))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure about adding colors here but I think that @samuelgarcia and @alejoe91 are more familiar with viewers so I defer to them.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's mostly a gimmick for reproducibility of data visualisation with my lab ahah. I use it later in a custom ephyviewer script (not shown here, but maybe i should share it as well ?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we just need to understand is this something all users should think about or is this super specific. If colors are always generated in a consistent (at least to the user) way then it might be worth adding (again with Heberto's final suggestion to fit this in with all_annotations for a future PR).

Basically we want properties set here to be truly meaningful to all users. Other properties can be set on a per-user basis with our property setting machinery.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! In neuroscope it's always generated in a consistent manner, hardcoded in the paired xml file.



class NeuroScopeSortingExtractor(BaseSorting):
"""
Expand Down