Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

529 Make pynwb optional dependency #530

Merged
merged 7 commits into from
Jun 29, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .github/workflows/cov_test_workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
poetry run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
poetry run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Install optional dependency pynwb
run: |
poetry add pynwb
- name: Test with pytest and get coverage
run: |
cd syncopy/tests
Expand Down
939 changes: 430 additions & 509 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ tqdm = ">=4.31"
natsort = "^8.1.0"
psutil = ">=5.9"
fooof = ">=1.0"
pynwb = "^2.3.2"
bokeh = "^3.1.1"

[tool.poetry.group.dev.dependencies]
Expand Down
1 change: 0 additions & 1 deletion syncopy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ dependencies:
- psutil
- tqdm >= 4.31
- fooof >= 1.0
- pynwb >= 2.3
- bokeh >= 3.1.1

# Optional packages required for running the test-suite and building the HTML docs
Expand Down
5 changes: 5 additions & 0 deletions syncopy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ def startup_print_once(message, force=False):
except ImportError:
__plt__ = False

try:
import pynwb
__pynwb__ = True
except ImportError:
__pynwb__ = False

# Set package-wide temp directory
csHome = "/cs/home/{}".format(getpass.getuser())
Expand Down
19 changes: 17 additions & 2 deletions syncopy/datatype/continuous_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@
from .methods.definetrial import definetrial
from .base_data import BaseData
from syncopy.shared.parsers import scalar_parser, array_parser
from syncopy.shared.errors import SPYValueError
from syncopy.shared.errors import SPYValueError, SPYError
from syncopy.shared.tools import best_match
from syncopy.plotting import sp_plotting, mp_plotting
from syncopy.io.nwb import _analog_timelocked_to_nwbfile
from .util import TimeIndexer
from pynwb import NWBHDF5IO


from syncopy import __pynwb__

if __pynwb__: # pragma: no cover
from pynwb import NWBHDF5IO



Expand Down Expand Up @@ -480,7 +485,12 @@ def save_nwb(self, outpath, nwbfile=None, with_trialdefinition=True, is_raw=True
before calling this function if you want to export a subset only.

The Syncopy NWB reader only supports the NWB raw data format.

This function requires the optional 'pynwb' dependency to be installed.
"""
if not __pynwb__:
raise SPYError("NWB support is not available. Please install the 'pynwb' package.")

nwbfile = _analog_timelocked_to_nwbfile(self, nwbfile=nwbfile, with_trialdefinition=with_trialdefinition, is_raw=is_raw)
# Write the file to disk.
with NWBHDF5IO(outpath, "w") as io:
Expand Down Expand Up @@ -900,7 +910,12 @@ def save_nwb(self, outpath, with_trialdefinition=True, is_raw=True):
open it using pynwb before using it with the target software.

Selections are ignored, the full data is exported. Create a new Syncopy data object before calling this function if you want to export a subset only.

This function requires the optional 'pynwb' dependency to be installed.
"""
if not __pynwb__:
raise SPYError("NWB support is not available. Please install the 'pynwb' package.")

nwbfile = _analog_timelocked_to_nwbfile(self, nwbfile=None, with_trialdefinition=with_trialdefinition, is_raw=is_raw)
# Write the file to disk.
with NWBHDF5IO(outpath, "w") as io:
Expand Down
11 changes: 9 additions & 2 deletions syncopy/datatype/discrete_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
from syncopy.plotting import spike_plotting

from syncopy.io.nwb import _spikedata_to_nwbfile
from pynwb import NWBHDF5IO

from syncopy import __pynwb__

if __pynwb__: # pragma: no cover
from pynwb import NWBHDF5IO


__all__ = ["SpikeData", "EventData"]
Expand Down Expand Up @@ -631,11 +635,14 @@ def save_nwb(self, outpath, with_trialdefinition=True):

Selections are ignored, the full data is exported. Create a new Syncopy data object before calling this function if you want to export a subset only.
"""
if not __pynwb__:
raise SPYError("NWB support is not available. Please install the 'pynwb' package.")

nwbfile = _spikedata_to_nwbfile(self, nwbfile=None, with_trialdefinition=with_trialdefinition)
# Write the file to disk.
with NWBHDF5IO(outpath, "w") as io:
io.write(nwbfile)

# implement plotting
def singlepanelplot(self, **show_kwargs):

Expand Down
7 changes: 6 additions & 1 deletion syncopy/io/load_nwb.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@
import subprocess
import numpy as np
from tqdm import tqdm
import pynwb

# Local imports
from syncopy.datatype.continuous_data import AnalogData
from syncopy.datatype.discrete_data import EventData, SpikeData
from syncopy.shared.errors import SPYError, SPYTypeError, SPYValueError, SPYWarning, SPYInfo
from syncopy.shared.parsers import io_parser, scalar_parser, filename_parser
from syncopy import __pynwb__

__all__ = ["load_nwb"]


if __pynwb__:
import pynwb

def _is_valid_nwb_file(filename):
try:
this_python = os.path.join(os.path.dirname(sys.executable), 'python')
Expand Down Expand Up @@ -58,6 +61,8 @@ def load_nwb(filename, memuse=3000, container=None, validate=False, default_spik
objects are returned as a dictionary whose keys are the base-names
(sans path) of the corresponding files.
"""
if not __pynwb__:
raise SPYError("NWB support is not available. Please install the 'pynwb' package.")

# Check if file exists
nwbPath, nwbBaseName = io_parser(filename, varname="filename", isfile=True, exists=True)
Expand Down
12 changes: 9 additions & 3 deletions syncopy/io/nwb.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,15 @@
import pytz
import os
import shutil
from pynwb import NWBFile
from pynwb.ecephys import LFP, ElectricalSeries
from pynwb.core import DynamicTableRegion

from syncopy import __pynwb__


if __pynwb__:
import pynwb
from pynwb import NWBFile
from pynwb.ecephys import LFP, ElectricalSeries
from pynwb.core import DynamicTableRegion

# Local imports

Expand Down
14 changes: 12 additions & 2 deletions syncopy/tests/test_spyio.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
from syncopy.synthdata.analog import white_noise
from syncopy.synthdata.spikes import poisson_noise
from syncopy.io.load_nwb import _is_valid_nwb_file
from syncopy import __pynwb__

skip_no_pynwb = pytest.mark.skipif(not __pynwb__, reason=f"This test requires the 'pynwb' package to be installed.")


# Decorator to detect if test data dir is available
on_esi = os.path.isdir('/cs/slurm/syncopy')
Expand Down Expand Up @@ -587,6 +591,7 @@ class TestNWBImporter:
nwb_filename = nwbfile
break

@skip_no_pynwb
def test_load_nwb_analog(self):
"""Test loading of an NWB file containing acquistion data into a Syncopy AnalogData object."""

Expand Down Expand Up @@ -632,6 +637,7 @@ class TestNWBExporter():

do_validate_NWB = True

@skip_no_pynwb
def test_save_nwb_analog_no_trialdef(self):
"""Test saving to NWB file and re-reading data for AnalogData, without trial definition."""

Expand All @@ -658,6 +664,7 @@ def test_save_nwb_analog_no_trialdef(self):
assert all(adata_reread.channel == adata.channel) # Check that channel names are saved and re-read correctly.
assert np.allclose(adata.data, adata_reread.data)

@skip_no_pynwb
def test_save_nwb_analog_with_trialdef(self):
"""Test saving to NWB file and re-reading data for AnalogData with a trial definition."""

Expand Down Expand Up @@ -686,6 +693,7 @@ def test_save_nwb_analog_with_trialdef(self):
assert all(adata_reread.channel == adata.channel) # Check that channel names are saved and re-read correctly.
assert np.allclose(adata.data, adata_reread.data)

@skip_no_pynwb
def test_save_nwb_analog_with_trialdef_as_LFP(self):
"""Test saving to NWB file and re-reading data for AnalogData with a trial definition. Saves as LFP, as opposed to raw data."""

Expand Down Expand Up @@ -714,6 +722,7 @@ def test_save_nwb_analog_with_trialdef_as_LFP(self):
assert all(adata_reread.channel == adata.channel) # Check that channel names are saved and re-read correctly.
assert np.allclose(adata.data, adata_reread.data)

@skip_no_pynwb
def test_save_nwb_analog_2(self):
"""Test saving to NWB file and re-reading data for 2x AnalogData."""

Expand Down Expand Up @@ -746,7 +755,7 @@ def test_save_nwb_analog_2(self):
assert np.allclose(adata.data, adata_reread.data)
assert np.allclose(adata2.data, adata2_reread.data)


@skip_no_pynwb
def test_save_nwb_timelock_with_trialdef(self):
"""Test saving to NWB file and re-reading data for TimeLockData with a trial definition.
Currently, when the file is bering re-read, it results in an AnalogData object, not a TimeLockData object.
Expand Down Expand Up @@ -784,7 +793,7 @@ def test_save_nwb_timelock_with_trialdef(self):
assert all(adata_reread.channel == adata.channel) # Check that channel names are saved and re-read correctly.
assert np.allclose(adata.data, adata_reread.data)


@skip_no_pynwb
def test_save_nwb_spikedata(self):
"""Test exporting SpikeData to NWB format.

Expand Down Expand Up @@ -826,6 +835,7 @@ def test_save_nwb_spikedata(self):
assert np.allclose(spdata.data[:, 0], spdata_reread.data[:, 0])

@skip_no_pynapple
@skip_no_pynwb
def test_load_exported_nwb_spikes_pynapple(self, plot_spikes=True):
"""Test loading exported SpikeData in pynapple.

Expand Down