From fc6ae2648d4015228ddc4edd7145b3485b422d81 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Thu, 8 Aug 2024 06:38:06 -0300 Subject: [PATCH] Add plexon 2 support (#918) Co-authored-by: Ben Dichter Co-authored-by: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> --- .github/actions/install-wine/action.yml | 30 +++++++++++ .github/workflows/dev-testing.yml | 11 ++-- .github/workflows/doctests.yml | 7 ++- .../formatwise-installation-testing.yml | 2 +- .github/workflows/testing.yml | 7 ++- CHANGELOG.md | 4 +- docs/conversion_examples_gallery/index.rst | 1 + .../recording/plexon2.rst | 35 ++++++++++++ src/neuroconv/datainterfaces/__init__.py | 2 + .../ecephys/plexon/plexondatainterface.py | 53 +++++++++++++++++++ .../ecephys/plexon/requirements.txt | 1 + .../datainterfaces/ecephys/requirements.txt | 2 +- .../datainterfaces/icephys/requirements.txt | 2 +- .../test_gin_ecephys/test_raw_recordings.py | 7 +++ .../test_metadata/test_plexon_metadata.py | 14 +++++ 15 files changed, 167 insertions(+), 11 deletions(-) create mode 100644 .github/actions/install-wine/action.yml create mode 100644 docs/conversion_examples_gallery/recording/plexon2.rst create mode 100644 src/neuroconv/datainterfaces/ecephys/plexon/requirements.txt create mode 100644 tests/test_on_data/test_metadata/test_plexon_metadata.py diff --git a/.github/actions/install-wine/action.yml b/.github/actions/install-wine/action.yml new file mode 100644 index 000000000..85e70b471 --- /dev/null +++ b/.github/actions/install-wine/action.yml @@ -0,0 +1,30 @@ +name: Install packages +description: This action installs the package and its dependencies for testing + +inputs: + os: + description: 'Operating system to set up' + required: true + +runs: + using: "composite" + steps: + - name: Install wine on Linux + if: runner.os == 'Linux' + run: | + sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list + sudo dpkg --add-architecture i386 + sudo apt-get update -qq + sudo apt-get install -yqq --allow-downgrades libc6:i386 libgcc-s1:i386 libstdc++6:i386 wine + shell: bash + - name: Install wine on macOS + if: runner.os == 'macOS' + run: | + brew install --cask xquartz + brew install --cask wine-stable + shell: bash + + - name: Skip installation on Windows + if: ${{ inputs.os == 'Windows' }} + run: echo "Skipping Wine installation on Windows. Not necessary." + shell: bash diff --git a/.github/workflows/dev-testing.yml b/.github/workflows/dev-testing.yml index ada090501..65be5bffd 100644 --- a/.github/workflows/dev-testing.yml +++ b/.github/workflows/dev-testing.yml @@ -38,6 +38,11 @@ jobs: git config --global user.email "CI@example.com" git config --global user.name "CI Almighty" + - name: Install Wine (For Plexon2 Tests) + uses: ./.github/actions/install-wine + with: + os: ${{ runner.os }} + - name: Install full requirements run: pip install --no-cache-dir .[full,test] @@ -70,7 +75,7 @@ jobs: id: cache-ephys-datasets with: path: ./ephy_testing_data - key: ephys-datasets-2024-03-27-${{ matrix.os }}-${{ steps.ephys.outputs.HASH_EPHY_DATASET }} + key: ephys-datasets-2024-06-21-ubuntu-latest-${{ steps.ephys.outputs.HASH_EPHY_DATASET }} - name: Get ophys_testing_data current head hash id: ophys run: echo "::set-output name=HASH_OPHYS_DATASET::$(git ls-remote https://gin.g-node.org/CatalystNeuro/ophys_testing_data.git HEAD | cut -f1)" @@ -79,7 +84,7 @@ jobs: id: cache-ophys-datasets with: path: ./ophys_testing_data - key: ophys-datasets-2022-08-18-${{ matrix.os }}-${{ steps.ophys.outputs.HASH_OPHYS_DATASET }} + key: ophys-datasets-2022-08-18-ubuntu-latest-${{ steps.ophys.outputs.HASH_OPHYS_DATASET }} - name: Get behavior_testing_data current head hash id: behavior run: echo "::set-output name=HASH_BEHAVIOR_DATASET::$(git ls-remote https://gin.g-node.org/CatalystNeuro/behavior_testing_data.git HEAD | cut -f1)" @@ -88,7 +93,7 @@ jobs: id: cache-behavior-datasets with: path: ./behavior_testing_data - key: behavior-datasets-2023-07-26-${{ matrix.os }}-${{ steps.behavior.outputs.HASH_behavior_DATASET }} + key: behavior-datasets-2023-07-26-ubuntu-latest-${{ steps.behavior.outputs.HASH_behavior_DATASET }} diff --git a/.github/workflows/doctests.yml b/.github/workflows/doctests.yml index ec1bb7199..c3e467fe0 100644 --- a/.github/workflows/doctests.yml +++ b/.github/workflows/doctests.yml @@ -29,7 +29,10 @@ jobs: - name: Install neuroconv with minimal requirements run: pip install .[full,test] - + - name: Install Wine (For Plexon2 Tests) + uses: ./.github/actions/install-wine + with: + os: ${{ runner.os }} - name: Get ephy_testing_data current head hash id: ephys @@ -39,7 +42,7 @@ jobs: id: cache-ephys-datasets with: path: ./ephy_testing_data - key: ephys-datasets-2024-03-27-${{ matrix.os }}-${{ steps.ephys.outputs.HASH_EPHY_DATASET }} + key: ephys-datasets-2024-08-07-${{ matrix.os }}-${{ steps.ephys.outputs.HASH_EPHY_DATASET }} - name: Get ophys_testing_data current head hash id: ophys run: echo "::set-output name=HASH_OPHYS_DATASET::$(git ls-remote https://gin.g-node.org/CatalystNeuro/ophys_testing_data.git HEAD | cut -f1)" diff --git a/.github/workflows/formatwise-installation-testing.yml b/.github/workflows/formatwise-installation-testing.yml index 60118d7f6..ea6af41c9 100644 --- a/.github/workflows/formatwise-installation-testing.yml +++ b/.github/workflows/formatwise-installation-testing.yml @@ -44,7 +44,7 @@ jobs: id: cache-ephys-datasets with: path: ./ephy_testing_data - key: ephys-datasets-2024-03-27-${{ matrix.os }}-${{ steps.ephys.outputs.HASH_EPHY_DATASET }} + key: ephys-datasets-2024-08-27-${{ matrix.os }}-${{ steps.ephys.outputs.HASH_EPHY_DATASET }} - name: Get ophys_testing_data current head hash id: ophys run: echo "::set-output name=HASH_OPHYS_DATASET::$(git ls-remote https://gin.g-node.org/CatalystNeuro/ophys_testing_data.git HEAD | cut -f1)" diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 72f0e54f0..f6b4acc49 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -38,7 +38,10 @@ jobs: run: pip install . - name: Test initial import of all non-lazy dependencies run: python -c "import neuroconv" - + - name: Install Wine (For Plexon2 Tests) + uses: ./.github/actions/install-wine + with: + os: ${{ runner.os }} - name: Install NeuroConv with testing requirements run: pip install .[test] - name: Run import tests @@ -88,7 +91,7 @@ jobs: id: cache-ephys-datasets with: path: ./ephy_testing_data - key: ephys-datasets-2024-03-27-${{ matrix.os }}-${{ steps.ephys.outputs.HASH_EPHY_DATASET }} + key: ephys-datasets-2024-08-07-${{ matrix.os }}-${{ steps.ephys.outputs.HASH_EPHY_DATASET }} - name: Get ophys_testing_data current head hash id: ophys run: echo "::set-output name=HASH_OPHYS_DATASET::$(git ls-remote https://gin.g-node.org/CatalystNeuro/ophys_testing_data.git HEAD | cut -f1)" diff --git a/CHANGELOG.md b/CHANGELOG.md index c0e46c9ac..fa01f17bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ### Features * Support `SortingAnalyzer` in the `SpikeGLXConverterPipe`. [PR #821](https://github.com/catalystneuro/neuroconv/pull/821) +* Add Plexon2 support [PR #918](https://github.com/catalystneuro/neuroconv/pull/918) +* Converter working with multiple VideoInterface instances [PR 914](https://github.com/catalystneuro/neuroconv/pull/914) ### Bug fixes * Fixed the default naming of multiple electrical series in the `SpikeGLXConverterPipe`. [PR #957](https://github.com/catalystneuro/neuroconv/pull/957) @@ -58,7 +60,7 @@ ### Improvements * Propagated `photon_series_type` to `BaseImagingExtractorInterface` init instead of passing it as an argument of `get_metadata()` and `get_metadata_schema()`. [PR #847](https://github.com/catalystneuro/neuroconv/pull/847) -* Converter working with multiple VideoInterface instances [PR 914](https://github.com/catalystneuro/neuroconv/pull/914) + diff --git a/docs/conversion_examples_gallery/index.rst b/docs/conversion_examples_gallery/index.rst index 298dd28a3..15cc48ab7 100644 --- a/docs/conversion_examples_gallery/index.rst +++ b/docs/conversion_examples_gallery/index.rst @@ -28,6 +28,7 @@ Recording NeuroScope OpenEphys Plexon + Plexon2 Spike2 Spikegadgets SpikeGLX diff --git a/docs/conversion_examples_gallery/recording/plexon2.rst b/docs/conversion_examples_gallery/recording/plexon2.rst new file mode 100644 index 000000000..e07cfe6ed --- /dev/null +++ b/docs/conversion_examples_gallery/recording/plexon2.rst @@ -0,0 +1,35 @@ +Plexon2 Recording Conversion +---------------------------- + +Install NeuroConv with the additional dependencies necessary for reading Plexon acquisition data. + +.. code-block:: bash + + pip install neuroconv[plexon] + +.. warning:: + When running plexon2 conversion on platforms other than Windows, you also need to install `wine `_. + +Convert Plexon2 recording data to NWB using :py:class:`~neuroconv.datainterfaces.ecephys.plexon.plexondatainterface.Plexon2RecordingInterface`. + +.. code-block:: python + + >>> from datetime import datetime + >>> from zoneinfo import ZoneInfo + >>> from pathlib import Path + >>> from neuroconv.datainterfaces import Plexon2RecordingInterface + >>> + >>> file_path = f"{ECEPHY_DATA_PATH}/plexon/4chDemoPL2.pl2" + >>> # Change the file_path to the location in your system + >>> interface = Plexon2RecordingInterface(file_path=file_path, verbose=False) + >>> + >>> # Extract what metadata we can from the source files + >>> metadata = interface.get_metadata() + >>> # For data provenance we add the time zone information to the conversion + >>> tzinfo = ZoneInfo("US/Pacific") + >>> session_start_time = metadata["NWBFile"]["session_start_time"] + >>> metadata["NWBFile"].update(session_start_time=session_start_time.replace(tzinfo=tzinfo)) + >>> + >>> # Choose a path for saving the nwb file and run the conversion + >>> nwbfile_path = f"{path_to_save_nwbfile}" + >>> interface.run_conversion(nwbfile_path=nwbfile_path, metadata=metadata) diff --git a/src/neuroconv/datainterfaces/__init__.py b/src/neuroconv/datainterfaces/__init__.py index ac603a4ac..9028c1d42 100644 --- a/src/neuroconv/datainterfaces/__init__.py +++ b/src/neuroconv/datainterfaces/__init__.py @@ -54,6 +54,7 @@ from .ecephys.openephys.openephyssortingdatainterface import OpenEphysSortingInterface from .ecephys.phy.phydatainterface import PhySortingInterface from .ecephys.plexon.plexondatainterface import ( + Plexon2RecordingInterface, PlexonRecordingInterface, PlexonSortingInterface, ) @@ -124,6 +125,7 @@ EDFRecordingInterface, TdtRecordingInterface, PlexonRecordingInterface, + Plexon2RecordingInterface, PlexonSortingInterface, BiocamRecordingInterface, AlphaOmegaRecordingInterface, diff --git a/src/neuroconv/datainterfaces/ecephys/plexon/plexondatainterface.py b/src/neuroconv/datainterfaces/ecephys/plexon/plexondatainterface.py index b13c738dd..cf10d5f0b 100644 --- a/src/neuroconv/datainterfaces/ecephys/plexon/plexondatainterface.py +++ b/src/neuroconv/datainterfaces/ecephys/plexon/plexondatainterface.py @@ -1,3 +1,5 @@ +from pathlib import Path + from ..baserecordingextractorinterface import BaseRecordingExtractorInterface from ..basesortingextractorinterface import BaseSortingExtractorInterface from ....utils import DeepDict @@ -48,6 +50,57 @@ def get_metadata(self) -> DeepDict: return metadata +class Plexon2RecordingInterface(BaseRecordingExtractorInterface): + """ + Primary data interface class for converting Plexon2 data. + + Uses the :py:class:`~spikeinterface.extractors.Plexon2RecordingExtractor`. + """ + + display_name = "Plexon2 Recording" + associated_suffixes = (".pl2",) + info = "Interface for Plexon2 recording data." + + @classmethod + def get_source_schema(cls) -> dict: + source_schema = super().get_source_schema() + source_schema["properties"]["file_path"]["description"] = "Path to the .pl2 file." + return source_schema + + def __init__(self, file_path: FilePathType, verbose: bool = True, es_key: str = "ElectricalSeries"): + """ + Load and prepare data for Plexon. + + Parameters + ---------- + file_path : str or Path + Path to the .plx file. + verbose : bool, default: True + Allows verbosity. + es_key : str, default: "ElectricalSeries" + """ + stream_id = "3" + assert Path(file_path).is_file(), f"Plexon file not found in: {file_path}" + super().__init__( + file_path=file_path, + verbose=verbose, + es_key=es_key, + stream_id=stream_id, + all_annotations=True, + ) + + def get_metadata(self) -> DeepDict: + metadata = super().get_metadata() + + neo_reader = self.recording_extractor.neo_reader + + block_ind = self.recording_extractor.block_index + neo_metadata = neo_reader.raw_annotations["blocks"][block_ind] + metadata["NWBFile"].update(session_start_time=neo_metadata["m_CreatorDateTime"]) + + return metadata + + class PlexonSortingInterface(BaseSortingExtractorInterface): """ Primary data interface class for converting Plexon spiking data. diff --git a/src/neuroconv/datainterfaces/ecephys/plexon/requirements.txt b/src/neuroconv/datainterfaces/ecephys/plexon/requirements.txt new file mode 100644 index 000000000..1f3b3018c --- /dev/null +++ b/src/neuroconv/datainterfaces/ecephys/plexon/requirements.txt @@ -0,0 +1 @@ +zugbruecke >= 0.2.1; platform_system != "Windows" diff --git a/src/neuroconv/datainterfaces/ecephys/requirements.txt b/src/neuroconv/datainterfaces/ecephys/requirements.txt index baf8b33bc..938056d7a 100644 --- a/src/neuroconv/datainterfaces/ecephys/requirements.txt +++ b/src/neuroconv/datainterfaces/ecephys/requirements.txt @@ -1,2 +1,2 @@ spikeinterface>=0.101.0 -neo>=0.13.1 +neo>=0.13.2 diff --git a/src/neuroconv/datainterfaces/icephys/requirements.txt b/src/neuroconv/datainterfaces/icephys/requirements.txt index 41dc6814a..7c4979c99 100644 --- a/src/neuroconv/datainterfaces/icephys/requirements.txt +++ b/src/neuroconv/datainterfaces/icephys/requirements.txt @@ -1 +1 @@ -neo>=0.9.0 +neo>=0.13.2 diff --git a/tests/test_on_data/test_gin_ecephys/test_raw_recordings.py b/tests/test_on_data/test_gin_ecephys/test_raw_recordings.py index f75bd1f7b..92b23f554 100644 --- a/tests/test_on_data/test_gin_ecephys/test_raw_recordings.py +++ b/tests/test_on_data/test_gin_ecephys/test_raw_recordings.py @@ -26,6 +26,7 @@ NeuroScopeRecordingInterface, OpenEphysBinaryRecordingInterface, OpenEphysLegacyRecordingInterface, + Plexon2RecordingInterface, PlexonRecordingInterface, SpikeGadgetsRecordingInterface, SpikeGLXRecordingInterface, @@ -95,6 +96,12 @@ class TestEcephysRawRecordingsNwbConversions(unittest.TestCase): ), case_name="plexon_recording", ), + param( + data_interface=Plexon2RecordingInterface, + interface_kwargs=dict( + file_path=str(DATA_PATH / "plexon" / "4chDemoPL2.pl2"), + ), + ), param( data_interface=BiocamRecordingInterface, interface_kwargs=dict(file_path=str(DATA_PATH / "biocam" / "biocam_hw3.0_fw1.6.brw")), diff --git a/tests/test_on_data/test_metadata/test_plexon_metadata.py b/tests/test_on_data/test_metadata/test_plexon_metadata.py new file mode 100644 index 000000000..07e9105e6 --- /dev/null +++ b/tests/test_on_data/test_metadata/test_plexon_metadata.py @@ -0,0 +1,14 @@ +import datetime + +from neuroconv.datainterfaces import Plexon2RecordingInterface + +from ..setup_paths import ECEPHY_DATA_PATH + + +def test_session_start_time(): + file_path = f"{ECEPHY_DATA_PATH}/plexon/4chDemoPL2.pl2" + interface = Plexon2RecordingInterface(file_path=file_path) + metadata = interface.get_metadata() + session_start_time = metadata["NWBFile"]["session_start_time"] + + assert session_start_time == datetime.datetime(2013, 11, 20, 15, 59, 39)