Skip to content
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 40 additions & 19 deletions src/probeinterface/neuropixels_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,7 @@ def read_openephys(
oe_version = parse(info_chain.find("VERSION").text)
neuropix_pxi_processor = None
onebox_processor = None
onix_processor = None
for signal_chain in root.findall("SIGNALCHAIN"):
for processor in signal_chain:
if "PROCESSOR" == processor.tag:
Expand All @@ -752,8 +753,10 @@ def read_openephys(
neuropix_pxi_processor = processor
if "OneBox" in name:
onebox_processor = processor
if "ONIX" in name:
onix_processor = processor

if neuropix_pxi_processor is None and onebox_processor is None:
if neuropix_pxi_processor is None and onebox_processor is None and onix_processor is None:
if raise_error:
raise Exception("Open Ephys can only be read when the Neuropix-PXI or the " "OneBox plugin is used.")
return None
Expand All @@ -769,6 +772,8 @@ def read_openephys(
if onebox_processor is not None:
assert neuropix_pxi_processor is None, "Only one processor should be present"
processor = onebox_processor
if onix_processor is not None:
processor = onix_processor

if "NodeId" in processor.attrib:
node_id = processor.attrib["NodeId"]
Expand Down Expand Up @@ -797,14 +802,20 @@ def read_openephys(
has_streams = False
probe_names_used = None

Copy link
Member Author

Choose a reason for hiding this comment

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

The list of probe names for the test file are:

probe_names_used=['BreakoutBoard-DigitalIO', 'BreakoutBoard-AnalogIO-AnalogInput', 'BreakoutBoard-MemoryMonitor-PercentUsed', 'PortB-Neuropixels1.0eHeadstage-Probe', 'PortB-Neuropixels1.0eHeadstage-BNO055']

I think PortB-Neuropixels1.0eHeadstage-Probe is the one we're interested int? What are the others??

Copy link

Choose a reason for hiding this comment

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

That is correct, the PortB-Neuropixels1.0eHeadstage-Probe is the Neuropixels probe. The other streams are for other devices not related to Neuropixels. For reference, streams that start with BreakoutBoard are devices on the ONIX Breakout Board, and can be ignored for the purposes of SpikeInterface parsing. The BNO055 is a device on the Neuropixels headstage, and can also be ignored in this PR.

if onix_processor is not None:
probe_names_used = [probe_name for probe_name in probe_names_used if "Probe" in probe_name]

# for Open Ephys version < 1.0 np_probes is in the EDITOR field.
# for Open Ephys version >= 1.0 np_probes is in the CUSTOM_PARAMETERS field.
editor = processor.find("EDITOR")
if oe_version < parse("0.9.0"):
np_probes = editor.findall("NP_PROBE")
else:
custom_parameters = editor.find("CUSTOM_PARAMETERS")
np_probes = custom_parameters.findall("NP_PROBE")
if onix_processor is not None:
np_probes = custom_parameters.findall("NEUROPIXELSV1E")
Copy link
Member Author

Choose a reason for hiding this comment

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

Will NEUROPIXELSV1E always be the name of the probe?

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 it might be V1 or V2 depeneding on Neuropixels1.0/2.0. @bparks13 could you confirm?

Copy link

Choose a reason for hiding this comment

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

Currently the available options are NEUROPIXELSV1E, NEUROPIXELSV1F, and NEUROPIXELSV2E. The V1E/V1F are both Neuropixels 1.0 probes, and can be used interchangeably at this stage, the E/F distinction is differentiating the other ONIX hardware/headstage.

else:
np_probes = custom_parameters.findall("NP_PROBE")

if len(np_probes) == 0:
if raise_error:
Expand Down Expand Up @@ -839,13 +850,21 @@ def read_openephys(
# now load probe info from NP_PROBE fields
np_probes_info = []
for probe_idx, np_probe in enumerate(np_probes):
slot = np_probe.attrib["slot"]
port = np_probe.attrib["port"]
dock = np_probe.attrib["dock"]
probe_part_number = np_probe.attrib["probe_part_number"]
probe_serial_number = np_probe.attrib["probe_serial_number"]
# read channels
channels = np_probe.find("CHANNELS")
if onix_processor is not None:
slot, port, dock = None, None, None
probe_part_number, probe_serial_number = (
np_probe.attrib["probePartNumber"],
np_probe.attrib["probeSerialNumber"],
)
channels = np_probe.find("SELECTED_CHANNELS")
Copy link
Member Author

Choose a reason for hiding this comment

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

Here, the names of several settings are slightly different than the other cases. e.g. ONIX uses probePartNumber instead of probe_part_number.
Also, I don't think ONIX stores the slot, port and dock? Or maybe PortB in the probe_names_used says which port is used?

Copy link

Choose a reason for hiding this comment

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

In terms of naming, some of these elements have been introduced in the draft PR here, which gives us some flexibility with changing the names. I could modify the name to be probe_part_number if that makes more sense to be consistent.

Since we are recording from Open Ephys hardware, and not IMEC hardware, we do not have a slot/port/dock to report. The Port in this instance is specifying which port on the Breakout Board the headstage is connected to.

else:
slot = np_probe.attrib["slot"]
port = np_probe.attrib["port"]
dock = np_probe.attrib["dock"]
probe_part_number = np_probe.attrib["probe_part_number"]
probe_serial_number = np_probe.attrib["probe_serial_number"]
channels = np_probe.find("CHANNELS")

channel_names = np.array(list(channels.attrib.keys()))
channel_ids = np.array([int(ch[2:]) for ch in channel_names])
channel_order = np.argsort(channel_ids)
Expand All @@ -862,18 +881,20 @@ def read_openephys(
else:
shank_ids = None

electrode_xpos = np_probe.find("ELECTRODE_XPOS")
electrode_ypos = np_probe.find("ELECTRODE_YPOS")
if onix_processor is None:
electrode_xpos = np_probe.find("ELECTRODE_XPOS")
electrode_ypos = np_probe.find("ELECTRODE_YPOS")

if electrode_xpos is None or electrode_ypos is None:
if raise_error:
raise Exception("ELECTRODE_XPOS or ELECTRODE_YPOS is not available in settings!")
return None
xpos = np.array([float(electrode_xpos.attrib[ch]) for ch in channel_names])
ypos = np.array([float(electrode_ypos.attrib[ch]) for ch in channel_names])
positions = np.array([xpos, ypos]).T
if electrode_xpos is None or electrode_ypos is None:
if raise_error:
raise Exception("ELECTRODE_XPOS or ELECTRODE_YPOS is not available in settings!")
return None
xpos = np.array([float(electrode_xpos.attrib[ch]) for ch in channel_names])
ypos = np.array([float(electrode_ypos.attrib[ch]) for ch in channel_names])
positions = np.array([xpos, ypos]).T
else:
Copy link
Member Author

Choose a reason for hiding this comment

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

This seems like the hardest thing: electrode positions are not saved with ONIX, but they are for the other formats.

Copy link
Member

Choose a reason for hiding this comment

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

yep, I think that the easiest would be to store the electrode x/y positions, or the selected electrods. In the latter case, we could instantiate a full NP probe from the probe part number and slice it accordingly.

Copy link

Choose a reason for hiding this comment

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

We are currently storing the selected electrodes in the settings.xml file, under the SELECTED_CHANNELS node. The indices are the global electrode index. I would be happy to talk through how it is currently set up, and we can determine if that information is enough or I can add in more details as needed.

positions = np.reshape(np.arange(0, 384 * 2 * 20, 20), shape=(384, 2))

probe_part_number = np_probe.get("probe_part_number", None)
pt_metadata, _, mux_info = get_probe_metadata_from_probe_features(probe_features, probe_part_number)

shank_pitch = pt_metadata["shank_pitch_um"]
Expand Down