|
11 | 11 |
|
12 | 12 | from pathlib import Path |
13 | 13 | import re |
14 | | -import warnings |
15 | 14 | import json |
16 | 15 | from collections import OrderedDict |
17 | 16 | from packaging.version import parse |
18 | 17 | import numpy as np |
19 | | -from xml.etree import ElementTree |
20 | | - |
21 | 18 | from . import __version__ |
22 | 19 | from .probe import Probe |
23 | 20 | from .probegroup import ProbeGroup |
24 | | -from .neuropixels_tools import build_neuropixels_probe, _annotate_probe_with_adc_sampling_info |
25 | 21 | from .utils import import_safely |
26 | 22 |
|
27 | 23 |
|
@@ -733,193 +729,6 @@ def write_csv(file, probe): |
733 | 729 | raise NotImplementedError |
734 | 730 |
|
735 | 731 |
|
736 | | -def read_spikegadgets_neuropixels(file: str | Path, raise_error: bool = True) -> ProbeGroup: |
737 | | - """ |
738 | | - Find active channels of the given Neuropixels probe from a SpikeGadgets .rec file. |
739 | | - SpikeGadgets headstages support up to three Neuropixels 1.0 probes (as of March 28, 2024), |
740 | | - and information for all probes will be returned in a ProbeGroup object. |
741 | | -
|
742 | | - This function only supports Neuropixels probes recorded with SpikeGadgets |
743 | | - headstages (``HardwareConfiguration`` entries with ``name == "NeuroPixels1"``). |
744 | | - It does not handle tetrodes or other probe types that SpikeGadgets can |
745 | | - record. Use :func:`has_spikegadgets_neuropixels_probes` to check whether a |
746 | | - ``.rec`` file contains Neuropixels probe geometry before calling this reader. |
747 | | -
|
748 | | - Parameters |
749 | | - ---------- |
750 | | - file : Path or str |
751 | | - The .rec file path |
752 | | -
|
753 | | - Returns |
754 | | - ------- |
755 | | - probe_group : ProbeGroup object |
756 | | -
|
757 | | - """ |
758 | | - # The SpikeGadgets .rec XML does not include a probe part number. The NP1.0 |
759 | | - # catalogue variants (NP1000, NP1001, PRB_1_2_0480_2, PRB_1_4_0480_1, |
760 | | - # PRB_1_4_0480_1_C) share identical 2D geometry in the probeinterface |
761 | | - # catalogue (contact positions, pitch, stagger, shank width), differing only |
762 | | - # in metadata that probeinterface does not consume (ADC resolution, databus |
763 | | - # phase, gain, on-shank reference, shank thickness). So hardcoding NP1000 |
764 | | - # produces correct geometry; `model_name` and `description` are cleared on |
765 | | - # the sliced probe to avoid claiming a specific variant. |
766 | | - PART_NUMBER = "NP1000" |
767 | | - |
768 | | - header_txt = parse_spikegadgets_header(file) |
769 | | - root = ElementTree.fromstring(header_txt) |
770 | | - hconf = root.find("HardwareConfiguration") |
771 | | - sconf = root.find("SpikeConfiguration") |
772 | | - |
773 | | - probe_configs = [d for d in hconf if d.attrib.get("name") == "NeuroPixels1"] |
774 | | - n_probes = len(probe_configs) |
775 | | - |
776 | | - if n_probes == 0: |
777 | | - if raise_error: |
778 | | - raise Exception("No Neuropixels 1.0 probes found") |
779 | | - return None |
780 | | - |
781 | | - # NeuroPixels1 SourceOptions blocks carry the per-probe AP/LF gain settings. |
782 | | - # They appear in the same order as the SpikeNTrode probe digits (1, 2, 3). |
783 | | - source_options_blocks = [s for s in hconf.findall("SourceOptions") if s.attrib.get("name") == "NeuroPixels1"] |
784 | | - |
785 | | - probe_group = ProbeGroup() |
786 | | - |
787 | | - for curr_probe in range(1, n_probes + 1): |
788 | | - # SpikeNTrode elements are the authoritative list of recorded electrodes. |
789 | | - # Each id is "<probe_digit><1-based electrode number>" for up to 960 |
790 | | - # electrodes on NP1.0; the catalogue uses 0-based indices, so |
791 | | - # catalogue_index = electrode_number - 1. The probe number is assumed |
792 | | - # to be a single digit (1, 2, or 3), matching the documented |
793 | | - # SpikeGadgets limit of three simultaneous Neuropixels probes. |
794 | | - electrode_to_hwchan = {} |
795 | | - for ntrode in sconf: |
796 | | - electrode_id = ntrode.attrib["id"] |
797 | | - if int(electrode_id[0]) == curr_probe: |
798 | | - catalogue_index = int(electrode_id[1:]) - 1 |
799 | | - hw_chan = int(ntrode[0].attrib["hwChan"]) |
800 | | - electrode_to_hwchan[catalogue_index] = hw_chan |
801 | | - |
802 | | - active_indices = np.array(sorted(electrode_to_hwchan.keys())) |
803 | | - |
804 | | - full_probe = build_neuropixels_probe(PART_NUMBER) |
805 | | - probe = full_probe.get_slice(active_indices) |
806 | | - |
807 | | - # Clear part-number-specific metadata since we don't know the actual part number. |
808 | | - probe.model_name = "" |
809 | | - probe.description = "" |
810 | | - |
811 | | - device_channels = np.array([electrode_to_hwchan[idx] for idx in active_indices]) |
812 | | - probe.set_device_channel_indices(device_channels) |
813 | | - |
814 | | - # Per-contact ADC group and sample order from the catalogue MUX table plus |
815 | | - # the hwChan mapping (which is the readout-channel index for each contact). |
816 | | - adc_sampling_table = probe.annotations.get("adc_sampling_table") |
817 | | - _annotate_probe_with_adc_sampling_info(probe, adc_sampling_table) |
818 | | - |
819 | | - # NP1.0 gain is programmable. Read APGainMode and LFPGainMode from the |
820 | | - # SourceOptions block matching this probe (blocks appear in probe order). |
821 | | - if "ap_gain" not in probe.annotations and curr_probe - 1 < len(source_options_blocks): |
822 | | - custom_options = { |
823 | | - opt.attrib["name"]: opt.attrib["data"].strip() |
824 | | - for opt in source_options_blocks[curr_probe - 1].findall("CustomOption") |
825 | | - } |
826 | | - ap_gain_str = custom_options.get("APGainMode") |
827 | | - if ap_gain_str: |
828 | | - probe.annotate(ap_gain=float(ap_gain_str)) |
829 | | - if probe.annotations.get("lf_sample_frequency_hz", 0) > 0: |
830 | | - lf_gain_str = custom_options.get("LFPGainMode") |
831 | | - if lf_gain_str: |
832 | | - probe.annotate(lf_gain=float(lf_gain_str)) |
833 | | - |
834 | | - # Shift multiple probes so they don't overlap when plotted |
835 | | - probe.move([250 * (curr_probe - 1), 0]) |
836 | | - |
837 | | - probe_group.add_probe(probe) |
838 | | - |
839 | | - return probe_group |
840 | | - |
841 | | - |
842 | | -def read_spikegadgets(*args, **kwargs) -> ProbeGroup: |
843 | | - """ |
844 | | - Deprecated alias for :func:`read_spikegadgets_neuropixels`. |
845 | | -
|
846 | | - The name ``read_spikegadgets`` is misleading because the function only reads |
847 | | - Neuropixels probe geometry, not arbitrary SpikeGadgets ``.rec`` recordings. |
848 | | - Use :func:`read_spikegadgets_neuropixels` instead, and |
849 | | - :func:`has_spikegadgets_neuropixels_probes` to check whether a ``.rec`` file |
850 | | - has Neuropixels geometry before calling it. |
851 | | - """ |
852 | | - warnings.warn( |
853 | | - "read_spikegadgets is deprecated and will be removed in a future release. " |
854 | | - "Use read_spikegadgets_neuropixels instead.", |
855 | | - category=DeprecationWarning, |
856 | | - stacklevel=2, |
857 | | - ) |
858 | | - return read_spikegadgets_neuropixels(*args, **kwargs) |
859 | | - |
860 | | - |
861 | | -def has_spikegadgets_neuropixels_probes(file: str | Path) -> bool: |
862 | | - """ |
863 | | - Return True if the SpikeGadgets ``.rec`` file describes at least one |
864 | | - Neuropixels probe. |
865 | | -
|
866 | | - Detection scans the ``HardwareConfiguration`` block of the ``.rec`` XML |
867 | | - header for ``Device`` entries whose ``name`` attribute matches a known |
868 | | - Neuropixels source name (currently ``"NeuroPixels1"``). The presence of |
869 | | - any such entry is the ground-truth signal that the file contains |
870 | | - Neuropixels probe geometry, independent of what other hardware the |
871 | | - headstage is also streaming. |
872 | | -
|
873 | | - Intended use: callers that route heterogeneous SpikeGadgets recordings |
874 | | - (mixing tetrodes, Neuropixels, etc.) can gate the call to |
875 | | - :func:`read_spikegadgets_neuropixels` on this helper and skip probe |
876 | | - attachment for non-Neuropixels recordings. |
877 | | -
|
878 | | - Parameters |
879 | | - ---------- |
880 | | - file : str or Path |
881 | | - Path to the SpikeGadgets ``.rec`` file. |
882 | | -
|
883 | | - Returns |
884 | | - ------- |
885 | | - bool |
886 | | - """ |
887 | | - try: |
888 | | - header_txt = parse_spikegadgets_header(file) |
889 | | - root = ElementTree.fromstring(header_txt) |
890 | | - except Exception: |
891 | | - return False |
892 | | - |
893 | | - hconf = root.find("HardwareConfiguration") |
894 | | - if hconf is None: |
895 | | - return False |
896 | | - |
897 | | - for device in hconf: |
898 | | - if device.attrib.get("name") == "NeuroPixels1": |
899 | | - return True |
900 | | - return False |
901 | | - |
902 | | - |
903 | | -def parse_spikegadgets_header(file: str | Path) -> str: |
904 | | - """ |
905 | | - Parse file (SpikeGadgets .rec format) into a string until "</Configuration>", |
906 | | - which is the last tag of the header, after which the binary data begins. |
907 | | - """ |
908 | | - header_size = None |
909 | | - with open(file, mode="rb") as f: |
910 | | - while True: |
911 | | - line = f.readline() |
912 | | - if b"</Configuration>" in line: |
913 | | - header_size = f.tell() |
914 | | - break |
915 | | - |
916 | | - if header_size is None: |
917 | | - ValueError("SpikeGadgets: the xml header does not contain '</Configuration>'") |
918 | | - |
919 | | - f.seek(0) |
920 | | - return f.read(header_size).decode("utf8") |
921 | | - |
922 | | - |
923 | 732 | def read_mearec(file: str | Path) -> Probe: |
924 | 733 | """ |
925 | 734 | Read probe position, and contact shape from a MEArec file. |
|
0 commit comments