diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e907976..9770856 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black - rev: 23.10.0 + rev: 23.10.1 hooks: - id: black files: ^src/ diff --git a/README.md b/README.md index d911ddc..8c8432f 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Please [Star](https://github.com/SpikeInterface/probeinterface/stargazers) the p ProbeInterface aims to provide a common framework to handle probe information across neuroscience experiments. -ProbeInterface is used by the [SpikeInterface](https://github.com/SpikeInterface/spikeinterface) package to attach a probe information to a recordng object. +ProbeInterface is used by the [SpikeInterface](https://github.com/SpikeInterface/spikeinterface) package to attach probe information to a recording object. You can find detailed documentation in the [SpikeInterface documentation](https://spikeinterface.readthedocs.io/en/latest/modules/core.html#handling-probes). In practice, ProbeInterface is a lightweight package to handle: diff --git a/doc/generate_format_example.py b/doc/generate_format_example.py index df68a39..d247bf6 100644 --- a/doc/generate_format_example.py +++ b/doc/generate_format_example.py @@ -33,7 +33,7 @@ print(d.keys()) fig, ax = plt.subplots(figsize=(8, 8)) -plot_probe(probe, with_channel_index=True, ax=ax) +plot_probe(probe, ax=ax) ax.set_xlim(-50, 200) ax.set_ylim(-150, 120) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 7aa9052..a67b4de 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -6,6 +6,7 @@ Release notes .. toctree:: :maxdepth: 1 + releases/0.2.19.rst releases/0.2.18.rst releases/0.2.17.rst releases/0.2.16.rst diff --git a/doc/releases/0.2.18.rst b/doc/releases/0.2.18.rst index d30ed03..1191149 100644 --- a/doc/releases/0.2.18.rst +++ b/doc/releases/0.2.18.rst @@ -9,6 +9,9 @@ Features * Extend probe constructor (name, serial_number, manufacturer, model_name) (#206) * Extend available NP2 probe types to commercial types (20** series) (#217) +* Remove :code:`with_channel_index` argument from :code:`plot_probe` (#229) +* Remove checker for unique contact ids in probe group (#229) +* Unify usage of "contact" and remove "channel" notation (except for "device_channel_index") (#229) Bug fixes diff --git a/doc/releases/0.2.19.rst b/doc/releases/0.2.19.rst new file mode 100644 index 0000000..b9b830b --- /dev/null +++ b/doc/releases/0.2.19.rst @@ -0,0 +1,10 @@ +probeinterface 0.2.19 +--------------------- + +Nov, 2nd 2023 + + +Features +^^^^^^^^ + +* Unify NP reading with probe part number (#232) diff --git a/examples/ex_03_generate_probe_group.py b/examples/ex_03_generate_probe_group.py index 0fa214d..cb68bbf 100644 --- a/examples/ex_03_generate_probe_group.py +++ b/examples/ex_03_generate_probe_group.py @@ -34,7 +34,7 @@ print('probe0.get_contact_count()', probe0.get_contact_count()) print('probe1.get_contact_count()', probe1.get_contact_count()) -print('probegroup.get_channel_count()', probegroup.get_channel_count()) +print('probegroup.get_contact_count()', probegroup.get_contact_count()) ############################################################################## #  We can now plot all probes in the same axis: @@ -44,6 +44,6 @@ ############################################################################## #  or in separate axes: -plot_probe_group(probegroup, same_axes=False, with_channel_index=True) +plot_probe_group(probegroup, same_axes=False, with_contact_id=True) plt.show() diff --git a/examples/ex_05_device_channel_indices.py b/examples/ex_05_device_channel_indices.py index 96534c7..f928c80 100644 --- a/examples/ex_05_device_channel_indices.py +++ b/examples/ex_05_device_channel_indices.py @@ -29,7 +29,7 @@ xpitch=75, ypitch=75, y_shift_per_column=[0, -37.5, 0], contact_shapes='circle', contact_shape_params={'radius': 12}) -plot_probe(probe, with_channel_index=True) +plot_probe(probe, with_contact_id=True) ############################################################################## # The Probe is not connected to any device yet: @@ -51,7 +51,7 @@ # * the prbXX is the contact index ordered from 0 to N # * the devXX is the channel index on the device (with the second half reversed) -plot_probe(probe, with_channel_index=True, with_device_index=True) +plot_probe(probe, with_contact_id=True, with_device_index=True) ############################################################################## # Very often we have several probes on the device and this can lead to even @@ -85,6 +85,6 @@ # The indices of the probe group can also be plotted: fig, ax = plt.subplots() -plot_probe_group(probegroup, with_channel_index=True, same_axes=True, ax=ax) +plot_probe_group(probegroup, with_contact_id=True, same_axes=True, ax=ax) plt.show() diff --git a/examples/ex_06_import_export_to_file.py b/examples/ex_06_import_export_to_file.py index fb7d5b8..6d34b15 100644 --- a/examples/ex_06_import_export_to_file.py +++ b/examples/ex_06_import_export_to_file.py @@ -98,6 +98,6 @@ f.write(prb_two_tetrodes) two_tetrode = read_prb('two_tetrodes.prb') -plot_probe_group(two_tetrode, same_axes=False, with_channel_index=True) +plot_probe_group(two_tetrode, same_axes=False, with_contact_id=True) plt.show() diff --git a/examples/ex_07_probe_generator.py b/examples/ex_07_probe_generator.py index 82964a5..1978164 100644 --- a/examples/ex_07_probe_generator.py +++ b/examples/ex_07_probe_generator.py @@ -35,7 +35,7 @@ df = probegroup.to_dataframe() df -plot_probe_group(probegroup, with_channel_index=True, same_axes=True) +plot_probe_group(probegroup, with_contact_id=True, same_axes=True) ############################################################################## # Generate a linear probe: @@ -44,7 +44,7 @@ from probeinterface import generate_linear_probe linear_probe = generate_linear_probe(num_elec=16, ypitch=20) -plot_probe(linear_probe, with_channel_index=True) +plot_probe(linear_probe, with_contact_id=True) ############################################################################## # Generate a multi-column probe: @@ -57,7 +57,7 @@ xpitch=22, ypitch=20, y_shift_per_column=[0, -10, 0], contact_shapes='square', contact_shape_params={'width': 12}) -plot_probe(multi_columns, with_channel_index=True, ) +plot_probe(multi_columns, with_contact_id=True, ) ############################################################################## # Generate a square probe: diff --git a/examples/ex_10_get_probe_from_library.py b/examples/ex_10_get_probe_from_library.py index 4f18833..dcf6e35 100644 --- a/examples/ex_10_get_probe_from_library.py +++ b/examples/ex_10_get_probe_from_library.py @@ -39,7 +39,7 @@ # When plotting, the channel indices are automatically displayed with # one-based notation (even if internally everything is still zero based): -plot_probe(probe, with_channel_index=True) +plot_probe(probe, with_contact_id=True) ############################################################################## diff --git a/examples/ex_11_automatic_wiring.py b/examples/ex_11_automatic_wiring.py index 5293a17..b120110 100644 --- a/examples/ex_11_automatic_wiring.py +++ b/examples/ex_11_automatic_wiring.py @@ -52,7 +52,7 @@ # * the lower "devXX" is the channel on the Intan device (zero-based) fig, ax = plt.subplots(figsize=(5, 15)) -plot_probe(probe, with_channel_index=True, with_device_index=True, ax=ax) +plot_probe(probe, with_contact_id=True, with_device_index=True, ax=ax) plt.show() diff --git a/resources/generate_cambridgeneurotech_libray.py b/resources/generate_cambridgeneurotech_libray.py index 5974993..8d96baa 100644 --- a/resources/generate_cambridgeneurotech_libray.py +++ b/resources/generate_cambridgeneurotech_libray.py @@ -18,6 +18,10 @@ 2023-06-14 generate new library +2023-10-30 +Generate new library with some fixes + + Derive probes to be used with SpikeInterface base on Cambridgeneurotech databases Probe library to match and add on https://gin.g-node.org/spikeinterface/probeinterface_library/src/master/cambridgeneurotech @@ -36,16 +40,26 @@ from pathlib import Path +import json +import shutil + # work_dir = r"C:\Users\Windows\Dropbox (Scripps Research)\2021-01-SpikeInterface_CambridgeNeurotech" # work_dir = '.' # work_dir = '/home/samuel/Documents/SpikeInterface/2021-03-01-probeinterface_CambridgeNeurotech/' # work_dir = '/home/samuel/Documents/SpikeInterface/2022-05-20-probeinterface_CambridgeNeurotech/' # work_dir = '/home/samuel/Documents/SpikeInterface/2022-10-18-probeinterface_CambridgeNeurotech/' -work_dir = '/home/samuel/OwnCloudCNRS/probeinterface/2023-06-14-probeinterface-CambridgeNeurotech/' +# work_dir = '/home/samuel/OwnCloudCNRS/probeinterface/2023-06-14-probeinterface-CambridgeNeurotech/' +work_dir = '/home/samuel/OwnCloudCNRS/probeinterface/2023-10-30-probeinterface-CambridgeNeurotech/' + + +library_folder = '/home/samuel/Documents/SpikeInterface/probeinterface_library/cambridgeneurotech/' + +library_folder = Path(library_folder) + work_dir = Path(work_dir).absolute() -export_folder = work_dir / 'export_2023_06_14' +export_folder = work_dir / 'export_2023_10_30' probe_map_file = work_dir / 'ProbeMaps_Final2023.xlsx' probe_info_table_file = work_dir / 'ProbesDataBase_Final2023.csv' @@ -74,7 +88,7 @@ def convert_contact_shape(listCoord): listCoord = [float(s) for s in listCoord.split(' ')] return listCoord -def get_channel_index(connector, probe_type): +def get_contact_order(connector, probe_type): """ Get the channel index given a connector and a probe_type. This will help to re-order the probe contact later on. @@ -179,7 +193,7 @@ def create_CN_figure(probe_name, probe): plot_probe(probe, ax=ax, contacts_colors = ['#5bc5f2'] * n, # made change to default color probe_shape_kwargs = dict(facecolor='#6f6f6e', edgecolor='k', lw=0.5, alpha=0.3), # made change to default color - with_channel_index=True) + with_contact_id=True) ax.set_xlabel(u'Width (\u03bcm)') #modif to legend ax.set_ylabel(u'Height (\u03bcm)') #modif to legend @@ -244,22 +258,57 @@ def generate_all_probes(): #~ continue print(' ', probe_name) - channelIndex = get_channel_index(connector = connector, probe_type = probe_info['part']) + contact_order = get_contact_order(connector = connector, probe_type = probe_info['part']) - order = np.argsort(channelIndex) - probe = probe_unordered.get_slice(order) + sorted_indices = np.argsort(contact_order) + probe = probe_unordered.get_slice(sorted_indices) - probe.annotate(name=probe_name, - manufacturer='cambridgeneurotech', - first_index=1) + probe.annotate(name=probe_name, manufacturer='cambridgeneurotech') # one based in cambridge neurotech - contact_ids = np.arange(order.size) + 1 - contact_ids =contact_ids.astype(str) + contact_ids = np.arange(sorted_indices.size) + 1 + contact_ids = contact_ids.astype(str) probe.set_contact_ids(contact_ids) export_one_probe(probe_name, probe) +def synchronize_library(): + + for source_probe_file in export_folder.glob('**/*.json'): + # print() + print(source_probe_file.stem) + target_probe_file = library_folder / source_probe_file.parent.stem / source_probe_file.name + # print(target_probe_file) + with open(source_probe_file, mode='r')as source: + source_dict = json.load(source) + + with open(target_probe_file, mode='r')as target: + target_dict = json.load(target) + + source_dict.pop('version') + + target_dict.pop('version') + + # this was needed between version 0.2.17 > 0.2.18 + # target_dict["probes"][0]["annotations"].pop("first_index") + + same = source_dict == target_dict + + # copy the json + shutil.copyfile(source_probe_file, target_probe_file) + if not same: + # copy the png + shutil.copyfile(source_probe_file.parent / (source_probe_file.stem + '.png'), + target_probe_file.parent / (target_probe_file.stem + '.png') ) + + + + + + + # library_folder + if __name__ == '__main__': - generate_all_probes() + # generate_all_probes() + synchronize_library() diff --git a/src/probeinterface/generator.py b/src/probeinterface/generator.py index 5dca013..578ad1a 100644 --- a/src/probeinterface/generator.py +++ b/src/probeinterface/generator.py @@ -148,6 +148,7 @@ def generate_multi_columns_probe( probe = Probe(ndim=2, si_units="um") probe.set_contacts(positions=positions, shapes=contact_shapes, shape_params=contact_shape_params) probe.create_auto_shape(probe_type="tip", margin=25) + probe.set_contact_ids(np.arange(positions.shape[0]).astype("str")) return probe diff --git a/src/probeinterface/io.py b/src/probeinterface/io.py index 78b3cdb..147d36a 100644 --- a/src/probeinterface/io.py +++ b/src/probeinterface/io.py @@ -711,8 +711,8 @@ def write_csv(file, probe): npx_probe = { # Neuropixels 1.0 # This probably should be None or something else because NOT ONLY the neuropixels 1.0 have that imDatPrb_type - 0: { - "probe_name": "Neuropixels 1.0", + "0": { + "model_name": "Neuropixels 1.0", "x_pitch": 32, "y_pitch": 20, "contact_width": 12, @@ -729,10 +729,11 @@ def write_csv(file, probe): "lf_gains", "ap_hp_filters", ), + "x_shift": -11, }, # Neuropixels 2.0 - Single Shank - Prototype - 21: { - "probe_name": "Neuropixels 2.0 - Single Shank - Prototype", + "21": { + "model_name": "Neuropixels 2.0 - Single Shank - Prototype", "x_pitch": 32, "y_pitch": 15, "contact_width": 12, @@ -742,10 +743,11 @@ def write_csv(file, probe): "ncol": 2, "polygon": polygon_description["default"], "fields_in_imro_table": ("channel_ids", "banks", "references", "elec_ids"), + "x_shift": -8, }, # Neuropixels 2.0 - Four Shank - Prototype - 24: { - "probe_name": "Neuropixels 2.0 - Four Shank - Prototype", + "24": { + "model_name": "Neuropixels 2.0 - Four Shank - Prototype", "x_pitch": 32, "y_pitch": 15, "contact_width": 12, @@ -761,10 +763,11 @@ def write_csv(file, probe): "references", "elec_ids", ), + "x_shift": -8, }, # Neuropixels 2.0 - Single Shank - Commercial without metal cap - 2003: { - "probe_name": "Neuropixels 2.0 - Single Shank", + "2003": { + "model_name": "Neuropixels 2.0 - Single Shank", "x_pitch": 32, "y_pitch": 15, "contact_width": 12, @@ -774,10 +777,11 @@ def write_csv(file, probe): "ncol": 2, "polygon": polygon_description["default"], "fields_in_imro_table": ("channel_ids", "banks", "references", "elec_ids"), + "x_shift": -8, }, # Neuropixels 2.0 - Single Shank - Commercial with metal cap - 2004: { - "probe_name": "Neuropixels 2.0 - Single Shank", + "2004": { + "model_name": "Neuropixels 2.0 - Single Shank", "x_pitch": 32, "y_pitch": 15, "contact_width": 12, @@ -787,10 +791,11 @@ def write_csv(file, probe): "ncol": 2, "polygon": polygon_description["default"], "fields_in_imro_table": ("channel_ids", "banks", "references", "elec_ids"), + "x_shift": -8, }, # Neuropixels 2.0 - Four Shank - Commercial without metal cap - 2013: { - "probe_name": "Neuropixels 2.0 - Four Shank", + "2013": { + "model_name": "Neuropixels 2.0 - Four Shank", "x_pitch": 32, "y_pitch": 15, "contact_width": 12, @@ -806,10 +811,11 @@ def write_csv(file, probe): "references", "elec_ids", ), + "x_shift": -8, }, # Neuropixels 2.0 - Four Shank - Commercial with metal cap - 2014: { - "probe_name": "Neuropixels 2.0 - Four Shank", + "2014": { + "model_name": "Neuropixels 2.0 - Four Shank", "x_pitch": 32, "y_pitch": 15, "contact_width": 12, @@ -825,10 +831,11 @@ def write_csv(file, probe): "references", "elec_ids", ), + "x_shift": -8, }, # Experimental probes previous to 1.0 "Phase3a": { - "probe_name": "Phase3a", + "model_name": "Phase3a", "x_pitch": 32, "y_pitch": 20, "contact_width": 12, @@ -844,10 +851,11 @@ def write_csv(file, probe): "ap_gains", "lf_gains", ), + "x_shift": -11, }, # Neuropixels 1.0-NHP Short (10mm) - 1015: { - "probe_name": "Neuropixels 1.0-NHP - short", + "1015": { + "model_name": "Neuropixels 1.0-NHP - short", "x_pitch": 32, "y_pitch": 20, "contact_width": 12, @@ -864,10 +872,11 @@ def write_csv(file, probe): "lf_gains", "ap_hp_filters", ), + "x_shift": -11, }, # Neuropixels 1.0-NHP Medium (25mm) - 1022: { - "probe_name": "Neuropixels 1.0-NHP - medium", + "1022": { + "model_name": "Neuropixels 1.0-NHP - medium", "x_pitch": 103, "y_pitch": 20, "contact_width": 12, @@ -884,10 +893,11 @@ def write_csv(file, probe): "lf_gains", "ap_hp_filters", ), + "x_shift": -11, }, # Neuropixels 1.0-NHP 45mm SOI90 - NHP long 90um wide, staggered contacts - 1030: { - "probe_name": "Neuropixels 1.0-NHP - long SOI90 staggered", + "1030": { + "model_name": "Neuropixels 1.0-NHP - long SOI90 staggered", "x_pitch": 56, "y_pitch": 20, "stagger": 12, @@ -904,10 +914,11 @@ def write_csv(file, probe): "lf_gains", "ap_hp_filters", ), + "x_shift": -11, }, # Neuropixels 1.0-NHP 45mm SOI125 - NHP long 125um wide, staggered contacts - 1031: { - "probe_name": "Neuropixels 1.0-NHP - long SOI125 staggered", + "1031": { + "model_name": "Neuropixels 1.0-NHP - long SOI125 staggered", "x_pitch": 91, "y_pitch": 20, "contact_width": 12, @@ -924,10 +935,11 @@ def write_csv(file, probe): "lf_gains", "ap_hp_filters", ), + "x_shift": -11, }, # 1.0-NHP 45mm SOI115 / 125 linear - NHP long 125um wide, linear contacts - 1032: { - "probe_name": "Neuropixels 1.0-NHP - long SOI125 linear", + "1032": { + "model_name": "Neuropixels 1.0-NHP - long SOI125 linear", "x_pitch": 103, "y_pitch": 20, "contact_width": 12, @@ -944,10 +956,11 @@ def write_csv(file, probe): "lf_gains", "ap_hp_filters", ), + "x_shift": -11, }, # Ultra probe - 1100: { - "probe_name": "Ultra probe", + "1100": { + "model_name": "Neuropixels Ultra", "x_pitch": 6, "y_pitch": 6, "contact_width": 5, @@ -964,21 +977,59 @@ def write_csv(file, probe): "lf_gains", "ap_hp_filters", ), + "x_shift": -8, + }, + # NP-Opto + "1300": { + "model_name": "Neuropixels Opto", + "x_pitch": 48, + "y_pitch": 20, + "contact_width": 12, + "stagger": 0.0, + "shank_pitch": 0, + "shank_number": 1, + "ncol": 2, + "polygon": polygon_description["default"], + "fields_in_imro_table": ( + "channel_ids", + "banks", + "references", + "ap_gains", + "lf_gains", + "ap_hp_filters", + ), + "x_shift": -11, }, } +# TODO: unify implementation with https://github.com/jenniferColonell/SGLXMetaToCoords/blob/main/SGLXMetaToCoords.py + # Map imDatPrb_pn (probe number) to imDatPrb_type (probe type) when the latter is missing -probe_number_to_probe_type = { - "PRB_1_4_0480_1": 0, - "PRB_1_4_0480_1_C": 0, - "NP1010": 0, - "NP1015": 1015, - "NP1022": 1022, - "NP1030": 1030, - "NP1031": 1031, - "NP1032": 1032, - None: 0, +probe_part_number_to_probe_type = { + # NP1.0 + "PRB_1_4_0480_1": "0", + "PRB_1_4_0480_1_C": "0", + "NP1010": "0", + None: "0", # for old version without a probe number we assume 1.0 + # NHP probes + "NP1015": "1015", + "NP1022": "1022", + "NP1030": "1030", + "NP1031": "1031", + "NP1032": "1032", + # NP2.0 + "NP2000": "21", + "NP2010": "24", + "NP2013": "2013", + "NP2014": "2014", + "NP2003": "2003", + "NP2004": "2004", + "PRB2_1_2_0640_0": "21", + # Other probes + "NP1100": "1100", # Ultra probe - 1 bank + "NP1110": "1100", # Ultra probe - 16 banks + "NP1300": "1300", # Opto probe } @@ -1026,22 +1077,25 @@ def _read_imro_string(imro_str: str, imDatPrb_pn: Optional[str] = None) -> Probe """ imro_table_header_str, *imro_table_values_list, _ = imro_str.strip().split(")") - imro_table_header = tuple(map(int, imro_table_header_str[1:].split(","))) - if len(imro_table_header) == 3: - # In older versions of neuropixel arrays (phase 3A), imro tables were structured differently. - probe_serial_number, probe_option, num_contact = imro_table_header - imDatPrb_type = "Phase3a" - elif len(imro_table_header) == 2: - imDatPrb_type, num_contact = imro_table_header - else: - raise ValueError(f"read_imro error, the header has a strange length: {imro_table_header}") - if imDatPrb_type in [0, None]: - imDatPrb_type = probe_number_to_probe_type[imDatPrb_pn] + if imDatPrb_pn is None: + if len(imro_table_header) == 3: + # In older versions of neuropixel arrays (phase 3A), imro tables were structured differently. + probe_serial_number, probe_option, num_contact = imro_table_header + imDatPrb_type = "Phase3a" + elif len(imro_table_header) == 2: + imDatPrb_type, num_contact = imro_table_header + else: + raise ValueError(f"read_imro error, the header has a strange length: {imro_table_header}") + imDatPrb_type = str(imDatPrb_type) + else: + if imDatPrb_pn not in probe_part_number_to_probe_type: + raise NotImplementedError(f"Probe part number {imDatPrb_pn} is not supported yet") + imDatPrb_type = probe_part_number_to_probe_type[imDatPrb_pn] probe_description = npx_probe[imDatPrb_type] - probe_name = probe_description["probe_name"] + model_name = probe_description["model_name"] fields = probe_description["fields_in_imro_table"] contact_info = {k: [] for k in fields} @@ -1067,7 +1121,7 @@ def _read_imro_string(imro_str: str, imDatPrb_pn: Optional[str] = None) -> Probe x_pos = x_idx * x_pitch + stagger y_pos = y_idx * y_pitch - if imDatPrb_type == 24: + if probe_description["shank_number"] > 1: shank_ids = np.array(contact_info["shank_id"]) shank_pitch = probe_description["shank_pitch"] contact_ids = [f"s{shank_id}e{elec_id}" for shank_id, elec_id in zip(shank_ids, elec_ids)] @@ -1079,7 +1133,7 @@ def _read_imro_string(imro_str: str, imDatPrb_pn: Optional[str] = None) -> Probe positions = np.stack((x_pos, y_pos), axis=1) # construct Probe object - probe = Probe(ndim=2, si_units="um", model_name=probe_name, manufacturer="IMEC") + probe = Probe(ndim=2, si_units="um", model_name=model_name, manufacturer="IMEC") probe.set_contacts( positions=positions, shapes="square", @@ -1134,21 +1188,21 @@ def write_imro(file: str | Path, probe: Probe): annotations = probe.contact_annotations ret = [f"({probe_type},{len(data)})"] - if probe_type == 0: + if probe_type == "0": for ch in range(len(data)): ret.append( f"({ch} 0 {annotations['references'][ch]} {annotations['ap_gains'][ch]} " f"{annotations['lf_gains'][ch]} {annotations['ap_hp_filters'][ch]})" ) - elif probe_type == 21: + elif probe_type in ("21", "2003", "2004"): for ch in range(len(data)): ret.append( f"({data['device_channel_indices'][ch]} {annotations['banks'][ch]} " f"{annotations['references'][ch]} {data['contact_ids'][ch][1:]})" ) - elif probe_type == 24: + elif probe_type in ("24", "2013", "2014"): for ch in range(len(data)): ret.append( f"({data['device_channel_indices'][ch]} {data['shank_ids'][ch]} {annotations['banks'][ch]} " @@ -1435,26 +1489,11 @@ def read_openephys( ypos = np.array([float(electrode_ypos.attrib[ch]) for ch in channel_names]) positions = np.array([xpos, ypos]).T - contact_ids = [] - pname = np_probe.attrib["probe_name"] - if "2.0" in pname: - x_shift = -8 - if "Multishank" in pname: - ptype = 24 - else: - ptype = 21 - elif "NHP" in pname: - ptype = 0 - x_shift = -11 - elif "1.0" in pname: - ptype = 0 - x_shift = -11 - elif "Ultra" in pname: - ptype = 1100 - x_shift = -8 - else: # Probe type unknown - ptype = None - x_shift = 0 + probe_part_number = np_probe.get("probe_part_number", None) + if probe_part_number not in probe_part_number_to_probe_type: + raise NotImplementedError(f"Probe part number {probe_part_number} is not supported yet") + ptype = probe_part_number_to_probe_type[probe_part_number] + x_shift = npx_probe[ptype]["x_shift"] if ptype is not None else 0 if fix_x_position_for_oe_5 and oe_version < parse("0.6.0") and shank_ids is not None: positions[:, 1] = positions[:, 1] - npx_probe[ptype]["shank_pitch"] * shank_ids @@ -1462,25 +1501,27 @@ def read_openephys( # x offset positions[:, 0] += x_shift + contact_ids = [] for i, pos in enumerate(positions): if ptype is None: contact_ids = None break stagger = np.mod(pos[1] / npx_probe[ptype]["y_pitch"] + 1, 2) * npx_probe[ptype]["stagger"] - shank_id = shank_ids[0] if ptype == 24 else 0 + shank_id = shank_ids[i] if npx_probe[ptype]["shank_number"] > 1 else 0 contact_id = int( (pos[0] - stagger - npx_probe[ptype]["shank_pitch"] * shank_id) / npx_probe[ptype]["x_pitch"] + npx_probe[ptype]["ncol"] * pos[1] / npx_probe[ptype]["y_pitch"] ) - if ptype == 24: + if npx_probe[ptype]["shank_number"] > 1: contact_ids.append(f"s{shank_id}e{contact_id}") else: contact_ids.append(f"e{contact_id}") + model_name = npx_probe[ptype]["model_name"] if ptype is not None else "Unknown" np_probe_dict = { - "model_name": pname, + "model_name": model_name, "shank_ids": shank_ids, "contact_ids": contact_ids, "positions": positions, diff --git a/src/probeinterface/plotting.py b/src/probeinterface/plotting.py index b9aaeeb..78be9f7 100644 --- a/src/probeinterface/plotting.py +++ b/src/probeinterface/plotting.py @@ -13,11 +13,9 @@ def plot_probe( probe, ax=None, contacts_colors=None, - with_channel_index=False, with_contact_id=False, with_device_index=False, text_on_contact=None, - first_index="auto", contacts_values=None, cmap="viridis", title=True, @@ -39,16 +37,12 @@ def plot_probe( The axis to plot the probe on. If None, an axis is created, by default None contacts_colors : matplotlib color, optional The color of the contacts, by default None - with_channel_index : bool, optional - If True, channel indices are displayed on top of the channels, by default False with_contact_id : bool, optional If True, channel ids are displayed on top of the channels, by default False with_device_index : bool, optional If True, device channel indices are displayed on top of the channels, by default False text_on_contact: None or list or numpy.array Addintional text to plot on each contact - first_index : str, optional - The first index of the contacts, by default 'auto' (taken from channel ids) contacts_values : np.array, optional Values to color the contacts with, by default None cmap : str, optional @@ -92,16 +86,6 @@ def plot_probe( else: fig = ax.get_figure() - if first_index == "auto": - if "first_index" in probe.annotations: - first_index = probe.annotations["first_index"] - elif probe.annotations.get("manufacturer", None) == "neuronexus": - # neuronexus is one based indexing - first_index = 1 - else: - first_index = 0 - assert first_index in (0, 1) - _probe_shape_kwargs = dict(facecolor="green", edgecolor="k", lw=0.5, alpha=0.3) _probe_shape_kwargs.update(probe_shape_kwargs) @@ -154,13 +138,11 @@ def on_press(event): text_on_contact = np.asarray(text_on_contact) assert text_on_contact.size == probe.get_contact_count() - if with_channel_index or with_contact_id or with_device_index or text_on_contact is not None: + if with_contact_id or with_device_index or text_on_contact is not None: if probe.ndim == 3: raise NotImplementedError("Channel index is 2d only") for i in range(n): txt = [] - if with_channel_index: - txt.append(f"{i + first_index}") if with_contact_id and probe.contact_ids is not None: contact_id = probe.contact_ids[i] txt.append(f"id{contact_id}") diff --git a/src/probeinterface/probe.py b/src/probeinterface/probe.py index 5a7a9ba..645e5ef 100644 --- a/src/probeinterface/probe.py +++ b/src/probeinterface/probe.py @@ -221,7 +221,9 @@ def get_shank_count(self) -> int: n = len(np.unique(self.shank_ids)) return n - def set_contacts(self, positions, shapes="circle", shape_params={"radius": 10}, plane_axes=None, shank_ids=None): + def set_contacts( + self, positions, shapes="circle", shape_params={"radius": 10}, plane_axes=None, contact_ids=None, shank_ids=None + ): """Sets contacts to a Probe. This sets four attributes of the probe: @@ -241,6 +243,8 @@ def set_contacts(self, positions, shapes="circle", shape_params={"radius": 10}, plane_axes : np.array (num_contacts, 2, ndim) Defines the two axes of the contact plane for each electrode. The third dimension corresponds to the probe `ndim` (2d or 3d). + contact_ids: None or array of str + Defines the contact ids for the contacts. If None, contact ids are not assigned. shank_ids : None or array of str Defines the shank ids for the contacts. If None, then these are assigned to a unique Shank. @@ -264,6 +268,9 @@ def set_contacts(self, positions, shapes="circle", shape_params={"radius": 10}, plane_axes = np.array(plane_axes) self._contact_plane_axes = plane_axes + if contact_ids is not None: + self.set_contact_ids(contact_ids) + if shank_ids is None: self._shank_ids = np.zeros(n, dtype=str) else: @@ -402,9 +409,14 @@ def set_contact_ids(self, contact_ids: np.array | list): """ contact_ids = np.asarray(contact_ids) + if np.all([c == "" for c in contact_ids]): + self._contact_ids = None + return + + assert np.unique(contact_ids).size == contact_ids.size, "Contact ids have to be unique within a Probe" if contact_ids.size != self.get_contact_count(): - ValueError(f"channel_indices do not have the same size as number of contacts") + ValueError(f"contact_ids do not have the same size as number of contacts") if contact_ids.dtype.kind != "U": contact_ids = contact_ids.astype("U") diff --git a/src/probeinterface/probegroup.py b/src/probeinterface/probegroup.py index 2912201..5aebe0b 100644 --- a/src/probeinterface/probegroup.py +++ b/src/probeinterface/probegroup.py @@ -24,7 +24,9 @@ def add_probe(self, probe): def _check_compatible(self, probe): if probe._probe_group is not None: - raise ValueError("This probe is already attached to another ProbeGroup") + raise ValueError( + "This probe is already attached to another ProbeGroup. Use probe.copy() to attach it to another ProbeGroup" + ) if probe.ndim != self.probes[-1].ndim: raise ValueError("ndim are not compatible") @@ -38,7 +40,7 @@ def _check_compatible(self, probe): def ndim(self): return self.probes[0].ndim - def get_channel_count(self): + def get_contact_count(self): """ Total number of channels. """ @@ -144,7 +146,7 @@ def get_global_device_channel_indices(self): Note: channel -1 means not connected """ - total_chan = self.get_channel_count() + total_chan = self.get_contact_count() channels = np.zeros(total_chan, dtype=[("probe_index", "int64"), ("device_channel_indices", "int64")]) arr = self.to_numpy(complete=True) channels["probe_index"] = arr["probe_index"] @@ -156,7 +158,7 @@ def set_global_device_channel_indices(self, channels): Set global indices for all probes """ channels = np.asarray(channels) - if channels.size != self.get_channel_count(): + if channels.size != self.get_contact_count(): raise ValueError("Wrong channels size") # first reset previsous indices @@ -187,14 +189,6 @@ def check_global_device_wiring_and_ids(self): if valid_chans.size != np.unique(valid_chans).size: raise ValueError("channel device index are not unique across probes") - # check unique ids for != '' - all_ids = self.get_global_contact_ids() - keep = [e != "" for e in all_ids] - valid_ids = all_ids[keep] - - if valid_ids.size != np.unique(valid_ids).size: - raise ValueError("contact_ids are not unique across probes") - def auto_generate_probe_ids(self, *args, **kwargs): """ Annotate all probes with unique probe_id values. @@ -230,13 +224,10 @@ def auto_generate_contact_ids(self, *args, **kwargs): `probeinterface.utils.generate_unique_ids` """ - if any(p.contact_ids is not None for p in self.probes): - raise ValueError("Some contacts already have contact ids " "assigned.") - if not args: args = 1e7, 1e8 # 3rd argument has to be the number of probes - args = args[:2] + (self.get_channel_count(),) + args = args[:2] + (self.get_contact_count(),) contact_ids = generate_unique_ids(*args, **kwargs).astype(str) diff --git a/tests/data/openephys/OE_Neuropix-PXI-NP2-4shank/settings.xml b/tests/data/openephys/OE_Neuropix-PXI-NP2-4shank/settings.xml new file mode 100644 index 0000000..5e08ec2 --- /dev/null +++ b/tests/data/openephys/OE_Neuropix-PXI-NP2-4shank/settings.xml @@ -0,0 +1,324 @@ + + + + + 0.6.5 + 8 + 3 Oct 2023 18:57:09 + Windows 10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + diff --git a/tests/test_generator.py b/tests/test_generator.py index 15ebae5..2836e1c 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -27,7 +27,7 @@ def test_generate(): #~ from probeinterface.plotting import plot_probe_group, plot_probe #~ import matplotlib.pyplot as plt - #~ plot_probe(multi_shank, with_channel_index=True,) + #~ plot_probe(multi_shank, with_contact_id=True,) #~ plt.show() if __name__ == '__main__': diff --git a/tests/test_io/test_imro.py b/tests/test_io/test_imro.py index 58d0670..9e6e269 100644 --- a/tests/test_io/test_imro.py +++ b/tests/test_io/test_imro.py @@ -50,3 +50,7 @@ def test_raising_error_when_writing_with_wrong_type(tmp_path): def test_non_standard_file(): with pytest.raises(ValueError): probe = read_imro(data_path / "test_non_standard.imro") + + +if __name__ == "__main__": + test_reading_old_imro(Path("tmp")) diff --git a/tests/test_io/test_io.py b/tests/test_io/test_io.py index 662a341..dd701fe 100644 --- a/tests/test_io/test_io.py +++ b/tests/test_io/test_io.py @@ -39,8 +39,8 @@ def test_probeinterface_format(tmp_path): # ~ from probeinterface.plotting import plot_probe_group # ~ import matplotlib.pyplot as plt - # ~ plot_probe_group(probegroup, with_channel_index=True, same_axes=False) - # ~ plot_probe_group(probegroup2, with_channel_index=True, same_axes=False) + # ~ plot_probe_group(probegroup, with_contact_id=True, same_axes=False) + # ~ plot_probe_group(probegroup2, with_contact_id=True, same_axes=False) # ~ plt.show() def test_writeprobeinterface(tmp_path): @@ -210,7 +210,7 @@ def test_prb(tmp_path): # ~ from probeinterface.plotting import plot_probe_group # ~ import matplotlib.pyplot as plt - # ~ plot_probe_group(probegroup, with_channel_index=True, same_axes=False) + # ~ plot_probe_group(probegroup, with_contact_id=True, same_axes=False) # ~ plt.show() # from probeinterface.plotting import plot_probe diff --git a/tests/test_io/test_openephys.py b/tests/test_io/test_openephys.py index edc3b46..761ff21 100644 --- a/tests/test_io/test_openephys.py +++ b/tests/test_io/test_openephys.py @@ -16,6 +16,14 @@ def test_NP2(): assert "2.0 - Single Shank" in probe.model_name +def test_NP2_four_shank(): + # NP2 + probe = read_openephys(data_path / "OE_Neuropix-PXI-NP2-4shank" / "settings.xml") + # on this case, only shanks 2-3 are used + assert probe.get_shank_count() == 2 + assert "2.0 - Four Shank" in probe.model_name + + def test_NP1_subset(): # NP1 - 200 channels selected by recording_state in Record Node probe_ap = read_openephys( @@ -89,13 +97,13 @@ def test_multiple_probes(): ) assert probeB2.get_shank_count() == 1 - assert "2.0 - Multishank" in probeB2.model_name + assert "2.0 - Four Shank" in probeB2.model_name ypos = probeB2.contact_positions[:, 1] assert np.min(ypos) >= 0 -def test_np_otpo_with_sync(): +def test_np_opto_with_sync(): probe = read_openephys(data_path / "OE_Neuropix-PXI-opto-with-sync" / "settings.xml") assert probe.model_name == "Neuropixels Opto" assert probe.get_shank_count() == 1 @@ -111,11 +119,11 @@ def test_older_than_06_format(): ) assert probe.get_shank_count() == 4 - assert "2.0 - Multishank" in probe.model_name + assert "2.0 - Four Shank" in probe.model_name ypos = probe.contact_positions[:, 1] assert np.min(ypos) >= 0 if __name__ == "__main__": - test_multiple_probes() + # test_multiple_probes() test_older_than_06_format() diff --git a/tests/test_io/test_spikeglx.py b/tests/test_io/test_spikeglx.py index e22cc55..48281ba 100644 --- a/tests/test_io/test_spikeglx.py +++ b/tests/test_io/test_spikeglx.py @@ -68,7 +68,7 @@ def test_NP2_4_shanks(): assert probe.model_name == "Neuropixels 2.0 - Four Shank - Prototype" assert probe.manufacturer == "IMEC" - assert probe.annotations["probe_type"] == 24 + assert probe.annotations["probe_type"] == "24" assert probe.ndim == 2 assert probe.get_shank_count() == 4 @@ -92,11 +92,11 @@ def test_NP2_2013_all(): assert probe.model_name == "Neuropixels 2.0 - Four Shank" assert probe.manufacturer == "IMEC" - assert probe.annotations["probe_type"] == 2013 + assert probe.annotations["probe_type"] == "2013" assert probe.ndim == 2 # all channels are from the first shank - assert probe.get_shank_count() == 1 + assert probe.get_shank_count() == 4 assert probe.get_contact_count() == 384 # Test contact geometry @@ -117,11 +117,11 @@ def test_NP2_2013_subset(): assert probe.model_name == "Neuropixels 2.0 - Four Shank" assert probe.manufacturer == "IMEC" - assert probe.annotations["probe_type"] == 2013 + assert probe.annotations["probe_type"] == "2013" assert probe.ndim == 2 # all channels are from the first shank - assert probe.get_shank_count() == 1 + assert probe.get_shank_count() == 4 assert probe.get_contact_count() == 120 # Test contact geometry @@ -142,7 +142,7 @@ def test_NP2_4_shanks_with_different_electrodes_saved(): assert probe.model_name == "Neuropixels 2.0 - Four Shank - Prototype" assert probe.manufacturer == "IMEC" - assert probe.annotations["probe_type"] == 24 + assert probe.annotations["probe_type"] == "24" assert probe.ndim == 2 assert probe.get_shank_count() == 4 @@ -193,7 +193,7 @@ def test_NPH_long_staggered(): assert probe.model_name == "Neuropixels 1.0-NHP - long SOI90 staggered" assert probe.manufacturer == "IMEC" - assert probe.annotations["probe_type"] == 1030 + assert probe.annotations["probe_type"] == "1030" assert probe.ndim == 2 assert probe.get_shank_count() == 1 @@ -248,7 +248,7 @@ def test_NPH_short_linear_probe_type_0(): assert probe.model_name == "Neuropixels 1.0-NHP - short" assert probe.manufacturer == "IMEC" - assert probe.annotations["probe_type"] == 1015 + assert probe.annotations["probe_type"] == "1015" assert probe.ndim == 2 assert probe.get_shank_count() == 1 @@ -297,9 +297,9 @@ def test_ultra_probe(): # Data provided by Alessio probe = read_spikeglx(data_path / "npUltra.meta") - assert probe.model_name == "Ultra probe" + assert probe.model_name == "Neuropixels Ultra" assert probe.manufacturer == "IMEC" - assert probe.annotations["probe_type"] == 1100 + assert probe.annotations["probe_type"] == "1100" # Test contact geometry contact_width = 5.0 @@ -326,5 +326,4 @@ def test_CatGT_NP1(): if __name__ == "__main__": - test_NP2_2013_all() - test_NP2_2013_subset() + test_NP2_1_shanks() diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 696d311..e79fa42 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -11,7 +11,6 @@ def test_plot_probe(): probe = generate_dummy_probe() plot_probe(probe) - plot_probe(probe, with_channel_index=True) plot_probe(probe, with_contact_id=True) plot_probe(probe, with_device_index=True) plot_probe(probe, text_on_contact=['abcde'[i%5] for i in range(probe.get_contact_count())]) @@ -33,7 +32,7 @@ def test_plot_probe(): def test_plot_probe_group(): probegroup = generate_dummy_probe_group() - plot_probe_group(probegroup, same_axes=True, with_channel_index=True) + plot_probe_group(probegroup, same_axes=True, with_contact_id=True) plot_probe_group(probegroup, same_axes=False) # 3d diff --git a/tests/test_wiring.py b/tests/test_wiring.py index 9cf5da0..bfcae1e 100644 --- a/tests/test_wiring.py +++ b/tests/test_wiring.py @@ -15,7 +15,7 @@ def test_wire_probe(): probe.wiring_to_device('H32>RHD2132') - plot_probe(probe, with_channel_index=True) + plot_probe(probe, with_contact_id=True) manufacturer = 'cambridgeneurotech' probe_name = 'ASSY-156-P-1' @@ -23,7 +23,7 @@ def test_wire_probe(): probe.wiring_to_device('ASSY-156>RHD2164') - plot_probe(probe, with_channel_index=True) + plot_probe(probe, with_contact_id=True) if __name__ == '__main__':