Skip to content

Commit ff55062

Browse files
authored
Merge branch 'main' into add_imro_tests
2 parents c2d2fc5 + bd8b55c commit ff55062

6 files changed

+115
-57
lines changed

probeinterface/io.py

+36-21
Original file line numberDiff line numberDiff line change
@@ -920,7 +920,7 @@ def write_csv(file, probe):
920920
}
921921

922922

923-
# Map imDatPrb_pn to imDatPrb_type when the latter is missing
923+
# Map imDatPrb_pn (probe number) to imDatPrb_type (probe type) when the latter is missing
924924
probe_number_to_probe_type = {
925925
"PRB_1_4_0480_1": 0,
926926
"PRB_1_4_0480_1_C": 0,
@@ -957,41 +957,56 @@ def read_imro(file_path: Union[str, Path]) -> Probe:
957957

958958
def _read_imro_string(imro_str: str, imDatPrb_pn: str = None) -> Probe:
959959
"""
960-
Low-level function to parse the imro table when presented as a string
961-
962-
See this doc https://billkarsh.github.io/SpikeGLX/help/imroTables/
963-
960+
Parse the IMRO table when presented as a string and create a Probe object.
961+
962+
Parameters
963+
----------
964+
imro_str : str
965+
IMRO table as a string.
966+
imDatPrb_pn : str, optional
967+
Probe number, by default None.
968+
969+
Returns
970+
-------
971+
Probe
972+
A Probe object built from the parsed IMRO table data.
973+
974+
See Also
975+
--------
976+
https://billkarsh.github.io/SpikeGLX/help/imroTables/
977+
964978
"""
965-
headers, *parts, _ = imro_str.strip().split(")")
979+
imro_table_header_str, *imro_table_values_list, _ = imro_str.strip().split(")")
966980

967-
header = tuple(map(int, headers[1:].split(',')))
968-
if len(header) == 3:
981+
imro_table_header = tuple(map(int, imro_table_header_str[1:].split(',')))
982+
if len(imro_table_header) == 3:
969983
# In older versions of neuropixel arrays (phase 3A), imro tables were structured differently.
970-
probe_serial_number, probe_option, num_contact = header
984+
probe_serial_number, probe_option, num_contact = imro_table_header
971985
imDatPrb_type = 'Phase3a'
972-
elif len(header) == 2:
973-
imDatPrb_type, num_contact = header
986+
elif len(imro_table_header) == 2:
987+
imDatPrb_type, num_contact = imro_table_header
974988
else:
975-
raise ValueError(f'read_imro error, the header has a strange shape: {header}')
989+
raise RuntimeError(f'read_imro error, the header has a strange length: {imro_table_header}')
990+
976991

977992
if imDatPrb_type in [0, None]:
978993
imDatPrb_type = probe_number_to_probe_type[imDatPrb_pn]
979994

980995
probe_description = npx_probe[imDatPrb_type]
981996
fields = probe_description["fields_in_imro_table"]
982-
contact_info = {k: [] for k in fields}
983-
for i, part in enumerate(parts):
984-
values = tuple(map(int, part[1:].split(' ')))
985-
for k, v in zip(fields, values):
986-
contact_info[k].append(v)
997+
contact_info = {k: [] for k in fields}
998+
for field_values_str in imro_table_values_list: # Imro table values look like '(value, value, value, ... '
999+
values = tuple(map(int, field_values_str[1:].split(' ')))
1000+
# Split them by space to get (int('value'), int('value'), int('value'), ...)
1001+
for field, field_value in zip(fields, values):
1002+
contact_info[field].append(field_value)
9871003

9881004
channel_ids = np.array(contact_info['channel_ids'])
989-
probe_types_without_elec_ids_in_their_imro_table = (0, 1015, 1022, 1030, 1031, 1032, "Phase3a", 1100)
990-
if imDatPrb_type in probe_types_without_elec_ids_in_their_imro_table:
1005+
if "elect_ids" in contact_info:
1006+
elec_ids = np.array(contact_info['elect_ids'])
1007+
else:
9911008
banks = np.array(contact_info['banks'])
9921009
elec_ids = banks * 384 + channel_ids
993-
else:
994-
elec_ids = np.array(contact_info['elec_ids'])
9951010

9961011
# compute position
9971012
y_idx, x_idx = np.divmod(elec_ids, probe_description["ncol"])

tests/test_io/test_spikeglx.py

+79-36
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,32 @@
1111

1212
data_path = Path(__file__).absolute().parent.parent / "data" / "spikeglx"
1313

14+
def test_parse_meta():
15+
for meta_file in [
16+
"doppio-checkerboard_t0.imec0.ap.meta",
17+
"NP1_saved_only_subset_of_channels.meta",
18+
"allan-longcol_g0_t0.imec0.ap.meta",
19+
]:
20+
meta = parse_spikeglx_meta(data_path / meta_file)
21+
22+
def test_get_saved_channel_indices_from_spikeglx_meta():
23+
# all channel saved + 1 synchro
24+
chan_inds = get_saved_channel_indices_from_spikeglx_meta(
25+
data_path / "Noise_g0_t0.imec0.ap.meta"
26+
)
27+
assert chan_inds.size == 385
28+
29+
# example by Pierre Yger NP1.0 with 384 but only 151 channels are saved + 1 synchro
30+
chan_inds = get_saved_channel_indices_from_spikeglx_meta(
31+
data_path / "NP1_saved_only_subset_of_channels.meta"
32+
)
33+
assert chan_inds.size == 152
1434

1535
def test_NP1():
1636
probe = read_spikeglx(data_path / "Noise_g0_t0.imec0.ap.meta")
1737
assert "1.0" in probe.annotations["name"]
1838

1939

20-
def test_NP2_4_shanks():
21-
probe = read_spikeglx(data_path / "TEST_20210920_0_g0_t0.imec0.ap.meta")
22-
assert probe.get_shank_count() == 4
23-
assert "2.0" in probe.annotations["name"]
24-
25-
2640
def test_NP2_1_shanks():
2741
probe = read_spikeglx(data_path / "p2_g0_t0.imec0.ap.meta")
2842
assert "2.0" in probe.annotations["name"]
@@ -31,19 +45,72 @@ def test_NP2_1_shanks():
3145

3246
def test_NP_phase3A():
3347
# Data provided by rtraghavan
34-
probe = read_spikeglx(data_path / "NeuropixelPhase3A_file_g0_t0.imec.ap.meta")
35-
assert "Phase3a" in probe.annotations["name"]
48+
probe = read_spikeglx(data_path / "phase3a.imec.ap.meta")
49+
50+
assert probe.annotations["name"] == "Phase3a"
51+
assert probe.annotations["manufacturer"] == "IMEC"
52+
assert probe.annotations["probe_type"] == "Phase3a"
53+
54+
assert probe.ndim == 2
3655
assert probe.get_shank_count() == 1
56+
assert probe.get_contact_count() == 384
57+
58+
# Test contact geometry
59+
contact_width = 12.0
60+
contact_shape = "square"
3761

62+
assert np.all(probe.contact_shape_params == {"width": contact_width})
63+
assert np.all(probe.contact_shapes == contact_shape)
3864

3965
def test_NP2_4_shanks():
66+
probe = read_spikeglx(data_path / "NP2_4_shanks.imec0.ap.meta")
67+
68+
assert probe.annotations["name"] == "Neuropixels 2.0 - Four Shank"
69+
assert probe.annotations["manufacturer"] == "IMEC"
70+
assert probe.annotations["probe_type"] == 24
71+
72+
assert probe.ndim == 2
73+
assert probe.get_shank_count() == 4
74+
assert probe.get_contact_count() == 384
75+
76+
# Test contact geometry
77+
contact_width = 12.0
78+
contact_shape = "square"
79+
80+
assert np.all(probe.contact_shape_params == {"width": contact_width})
81+
assert np.all(probe.contact_shapes == contact_shape)
82+
83+
# This file does not save the channnels from 0 as the one above (NP2_4_shanks_g0_t0.imec0.ap.meta)
84+
ypos = probe.contact_positions[:, 1]
85+
assert np.min(ypos) == pytest.approx(0)
86+
87+
88+
def test_NP2_4_shanks_with_different_electrodes_saved():
4089
# Data provided by Jennifer Colonell
41-
probe = read_spikeglx(data_path / "NP24_g0_t0.imec0.ap.meta")
42-
assert "2.0" in probe.annotations["name"]
90+
probe = read_spikeglx(data_path / "NP2_4_shanks_save_different_electrodes.imec0.ap.meta")
91+
92+
assert probe.annotations["name"] == "Neuropixels 2.0 - Four Shank"
93+
assert probe.annotations["manufacturer"] == "IMEC"
94+
assert probe.annotations["probe_type"] == 24
95+
96+
assert probe.ndim == 2
4397
assert probe.get_shank_count() == 4
98+
assert probe.get_contact_count() == 384
99+
100+
# Test contact geometry
101+
contact_width = 12.0
102+
contact_shape = "square"
44103

104+
assert np.all(probe.contact_shape_params == {"width": contact_width})
105+
assert np.all(probe.contact_shapes == contact_shape)
106+
107+
# This file does not save the channnels from 0 as the one above (NP2_4_shanks_g0_t0.imec0.ap.meta)
108+
ypos = probe.contact_positions[:, 1]
109+
assert np.min(ypos) == pytest.approx(2880.0)
110+
assert np.max(ypos) == pytest.approx(5745.0)
45111

46-
def test_NP1_large_depth_sapn():
112+
113+
def test_NP1_large_depth_span():
47114
# Data provided by Tom Bugnon NP1 with large Depth span
48115
probe = read_spikeglx(data_path / "allan-longcol_g0_t0.imec0.ap.meta")
49116
assert "1.0" in probe.annotations["name"]
@@ -64,35 +131,11 @@ def test_NP1_other_example():
64131

65132
def tes_NP1_384_channels():
66133
# example by Pierre Yger NP1.0 with 384 but only 151 channels are saved
67-
probe = read_spikeglx(data_path / "Day_3_g0_t0.imec1.ap.meta")
134+
probe = read_spikeglx(data_path / "NP1_saved_only_subset_of_channels.meta")
68135
assert probe.get_shank_count() == 1
69136
assert probe.get_contact_count() == 151
70137
assert 152 not in probe.contact_annotations["channel_ids"]
71138

72-
73-
def test_parse_meta():
74-
for meta_file in [
75-
"doppio-checkerboard_t0.imec0.ap.meta",
76-
"Day_3_g0_t0.imec1.ap.meta",
77-
"allan-longcol_g0_t0.imec0.ap.meta",
78-
]:
79-
meta = parse_spikeglx_meta(data_path / meta_file)
80-
81-
82-
def test_get_saved_channel_indices_from_spikeglx_meta():
83-
# all channel saved + 1 synchro
84-
chan_inds = get_saved_channel_indices_from_spikeglx_meta(
85-
data_path / "Noise_g0_t0.imec0.ap.meta"
86-
)
87-
assert chan_inds.size == 385
88-
89-
# example by Pierre Yger NP1.0 with 384 but only 151 channels are saved + 1 synchro
90-
chan_inds = get_saved_channel_indices_from_spikeglx_meta(
91-
data_path / "Day_3_g0_t0.imec1.ap.meta"
92-
)
93-
assert chan_inds.size == 152
94-
95-
96139
def test_NPH_long_staggered():
97140
# Data provided by Nate Dolensek
98141
probe = read_spikeglx(data_path / "non_human_primate_long_staggered.imec0.ap.meta")

0 commit comments

Comments
 (0)