Skip to content

Commit

Permalink
Merge branch 'dev' into add_io_can_read
Browse files Browse the repository at this point in the history
  • Loading branch information
h-mayorquin authored Nov 9, 2024
2 parents 0035bb2 + ad04661 commit fdd89f1
Show file tree
Hide file tree
Showing 17 changed files with 143 additions and 37 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@
### Enhancements and minor changes
* Added `NWBHDF5IO.read_nwb` convenience method to simplify reading an NWB file. @h-mayorquin [#1979](https://github.com/NeurodataWithoutBorders/pynwb/pull/1979)

### Documentation and tutorial enhancements
- Added documentation example for `SpikeEventSeries`. @stephprince [#1983](https://github.com/NeurodataWithoutBorders/pynwb/pull/1983)

### Performance
- Cache global type map to speed import 3X. @sneakers-the-rat [#1931](https://github.com/NeurodataWithoutBorders/pynwb/pull/1931)

### Bug fixes
- Fixed bug in how `ElectrodeGroup.__init__` validates its `position` argument. @oruebel [#1770](https://github.com/NeurodataWithoutBorders/pynwb/pull/1770)
- Changed `SpatialSeries.reference_frame` from required to optional as specified in the schema. @rly [#1986](https://github.com/NeurodataWithoutBorders/pynwb/pull/1986)

### Enhancements and minor changes
- Made gain an optional argument for PatchClampSeries to match the schema. @stephprince [#1975](https://github.com/NeurodataWithoutBorders/pynwb/pull/1975)
- Added warning when writing files with `NWBHDF5IO` without the `.nwb` extension. @stephprince [#1978](https://github.com/NeurodataWithoutBorders/pynwb/pull/1978)

## PyNWB 2.8.2 (September 9, 2024)

Expand Down
3 changes: 3 additions & 0 deletions docs/gallery/advanced_io/plot_editing.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
First, let's create an NWB file with data:
"""

# sphinx_gallery_thumbnail_path = "figures/gallery_thumbnails_editing.png"

from pynwb import NWBHDF5IO, NWBFile, TimeSeries
from datetime import datetime
from dateutil.tz import tzlocal
Expand Down
41 changes: 32 additions & 9 deletions docs/gallery/domain/ecephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from dateutil.tz import tzlocal

from pynwb import NWBHDF5IO, NWBFile
from pynwb.ecephys import LFP, ElectricalSeries
from pynwb.ecephys import LFP, ElectricalSeries, SpikeEventSeries

#######################
# Creating and Writing NWB files
Expand Down Expand Up @@ -244,8 +244,8 @@
####################
# .. _units_electrode:
#
# Spike Times
# ^^^^^^^^^^^
# Sorted spike times
# ^^^^^^^^^^^^^^^^^^
#
# Spike times are stored in the :py:class:`~pynwb.misc.Units` table, which is a subclass of
# :py:class:`~hdmf.common.table.DynamicTable`. Adding columns to the :py:class:`~pynwb.misc.Units` table is analogous
Expand All @@ -272,6 +272,29 @@

nwbfile.units.to_dataframe()

####################
# Unsorted spike times
# ^^^^^^^^^^^^^^^^^^^^
#
# While the :py:class:`~pynwb.misc.Units` table is used to store spike times and waveform data for
# spike-sorted, single-unit activity, you may also want to store spike times and waveform snippets of
# unsorted spiking activity (e.g., multi-unit activity detected via threshold crossings during data acquisition).
# This information can be stored using :py:class:`~pynwb.ecephys.SpikeEventSeries` objects.

spike_snippets = np.random.rand(20, 3, 40) # 20 events, 3 channels, 40 samples per event
shank0 = nwbfile.create_electrode_table_region(
region=[0, 1, 2],
description="shank0",
)


spike_events = SpikeEventSeries(name='SpikeEvents_Shank0',
description="events detected with 100uV threshold",
data=spike_snippets,
timestamps=np.arange(20),
electrodes=shank0)
nwbfile.add_acquisition(spike_events)

#######################
# Designating electrophysiology data
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand All @@ -283,17 +306,17 @@
# :py:mod:`API documentation <pynwb.ecephys>` and :ref:`basics` for more details on
# using these objects.
#
# For storing spike data, there are two options. Which one you choose depends on what data you have available.
# If you need to store the complete, continuous raw voltage traces, you should store the traces with
# For storing unsorted spiking data, there are two options. Which one you choose depends on what data you
# have available. If you need to store the complete, continuous raw voltage traces, you should store the traces with
# :py:class:`~pynwb.ecephys.ElectricalSeries` objects as :ref:`acquisition <basic_timeseries>` data, and use
# the :py:class:`~pynwb.ecephys.EventDetection` class for identifying the spike events in your raw traces.
# If you do not want to store the raw voltage traces and only the waveform 'snippets' surrounding spike events,
# you should use the :py:class:`~pynwb.ecephys.EventWaveform` class, which can store one or more
# :py:class:`~pynwb.ecephys.SpikeEventSeries` objects.
# you should use :py:class:`~pynwb.ecephys.SpikeEventSeries` objects.
#
# The results of spike sorting (or clustering) should be stored in the top-level :py:class:`~pynwb.misc.Units` table.
# Note that it is not required to store spike waveforms in order to store spike events or mean waveforms--if you only
# want to store the spike times of clustered units you can use only the Units table.
# The :py:class:`~pynwb.misc.Units` table can contain simply the spike times of sorted units, or you can also include
# individual and mean waveform information in some of the optional, predefined :py:class:`~pynwb.misc.Units` table
# columns: ``waveform_mean``, ``waveform_sd``, or ``waveforms``.
#
# For local field potential data, there are two options. Again, which one you choose depends on what data you
# have available. With both options, you should store your traces with :py:class:`~pynwb.ecephys.ElectricalSeries`
Expand Down
10 changes: 6 additions & 4 deletions docs/gallery/general/plot_configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@
Introduction
-------------
Users will create a configuration YAML file that outlines the fields (within a neurodata type)
they want to be validated against a set of allowed terms.
they want to be validated against a set of allowed terms.
After creating the configuration file, users will need to load the
configuration file with the :py:func:`~pynwb.load_type_config` method.
With the configuration loaded, every instance of the neurodata
types defined in the configuration file will have the respective fields wrapped with a
:py:class:`~hdmf.term_set.TermSetWrapper`.
This automatic wrapping is what provides the term validation for the field value.
For greater control on which datasets and attributes are validated
against which sets of allowed terms, use the
against which sets of allowed terms, use the
:py:class:`~hdmf.term_set.TermSetWrapper` on individual datasets and attributes instead.
You can follow the
`TermSet tutorial in the HDMF documentation
You can follow the
`TermSet tutorial in the HDMF documentation
<https://hdmf.readthedocs.io/en/stable/tutorials/plot_term_set.html#sphx-glr-tutorials-plot-term-set-py>`_
for more information.
Expand All @@ -42,6 +42,8 @@
3. Each data type will have a list of fields associated with a :py:class:`~hdmf.term_set.TermSet`.
The user can use the same or unique TermSet instances for each field.
"""
# sphinx_gallery_thumbnail_path = 'figures/gallery_thumbnails_configurator.png'

try:
import linkml_runtime # noqa: F401
except ImportError as e:
Expand Down
Binary file modified docs/source/figures/gallery_thumbnails.pptx
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/pynwb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,10 @@ def __init__(self, **kwargs):
if mode in io_modes_that_create_file or manager is not None or extensions is not None:
load_namespaces = False

if mode in io_modes_that_create_file and not str(path).endswith('.nwb'):
warn(f"The file path provided: {path} does not end in '.nwb'. "
"It is recommended that NWB files using the HDF5 backend use the '.nwb' extension.", UserWarning)

if load_namespaces:
tm = get_type_map()
super().load_namespaces(tm, path, file=file_obj, driver=driver, aws_region=aws_region)
Expand Down
4 changes: 2 additions & 2 deletions src/pynwb/behavior.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ class SpatialSeries(TimeSeries):
'or 3 columns, which represent x, y, and z.')},
{'name': 'bounds', 'type': list, 'shape': ((1, 2), (2, 2), (3, 2)), 'default': None,
'doc': 'The boundary range (min, max) for each dimension of data.'},
{'name': 'reference_frame', 'type': str, # required
'doc': 'description defining what the zero-position is'},
{'name': 'reference_frame', 'type': str,
'doc': 'description defining what the zero-position is', 'default': None},
{'name': 'unit', 'type': str, 'doc': 'The base unit of measurement (should be SI unit)',
'default': 'meters'},
*get_docval(TimeSeries.__init__, 'conversion', 'resolution', 'timestamps', 'starting_time', 'rate',
Expand Down
33 changes: 17 additions & 16 deletions src/pynwb/icephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ class PatchClampSeries(TimeSeries):
'name': 'gain',
'type': float,
'doc': 'Units: Volt/Amp (v-clamp) or Volt/Volt (c-clamp)',
}, # required
'default': None,
},
{
'name': 'stimulus_description',
'type': str,
Expand Down Expand Up @@ -164,7 +165,7 @@ class CurrentClampSeries(PatchClampSeries):
'capacitance_compensation')

@docval(*get_docval(PatchClampSeries.__init__, 'name', 'data', 'electrode'), # required
{'name': 'gain', 'type': float, 'doc': 'Units - Volt/Volt'},
{'name': 'gain', 'type': float, 'doc': 'Units - Volt/Volt', 'default': None},
*get_docval(PatchClampSeries.__init__, 'stimulus_description'),
{'name': 'bias_current', 'type': float, 'doc': 'Unit - Amp', 'default': None},
{'name': 'bridge_balance', 'type': float, 'doc': 'Unit - Ohm', 'default': None},
Expand Down Expand Up @@ -196,7 +197,7 @@ class IZeroClampSeries(CurrentClampSeries):
__nwbfields__ = ()

@docval(*get_docval(CurrentClampSeries.__init__, 'name', 'data', 'electrode'), # required
{'name': 'gain', 'type': float, 'doc': 'Units: Volt/Volt'}, # required
{'name': 'gain', 'type': float, 'doc': 'Units: Volt/Volt', 'default': None},
{'name': 'stimulus_description', 'type': str,
'doc': ('The stimulus name/protocol. Setting this to a value other than "N/A" is deprecated as of '
'NWB 2.3.0.'),
Expand Down Expand Up @@ -238,16 +239,16 @@ class CurrentClampStimulusSeries(PatchClampSeries):

__nwbfields__ = ()

@docval(*get_docval(PatchClampSeries.__init__, 'name', 'data', 'electrode', 'gain'), # required
*get_docval(PatchClampSeries.__init__, 'stimulus_description', 'resolution', 'conversion', 'timestamps',
'starting_time', 'rate', 'comments', 'description', 'control', 'control_description',
'sweep_number', 'offset'),
@docval(*get_docval(PatchClampSeries.__init__, 'name', 'data', 'electrode'), # required
*get_docval(PatchClampSeries.__init__, 'gain', 'stimulus_description', 'resolution', 'conversion',
'timestamps', 'starting_time', 'rate', 'comments', 'description', 'control',
'control_description', 'sweep_number', 'offset'),
{'name': 'unit', 'type': str, 'doc': "The base unit of measurement (must be 'amperes')",
'default': 'amperes'})
def __init__(self, **kwargs):
name, data, unit, electrode, gain = popargs('name', 'data', 'unit', 'electrode', 'gain', kwargs)
name, data, unit, electrode = popargs('name', 'data', 'unit', 'electrode', kwargs)
unit = ensure_unit(self, name, unit, 'amperes', '2.1.0')
super().__init__(name, data, unit, electrode, gain, **kwargs)
super().__init__(name, data, unit, electrode, **kwargs)


@register_class('VoltageClampSeries', CORE_NAMESPACE)
Expand All @@ -267,7 +268,7 @@ class VoltageClampSeries(PatchClampSeries):
'whole_cell_series_resistance_comp')

@docval(*get_docval(PatchClampSeries.__init__, 'name', 'data', 'electrode'), # required
{'name': 'gain', 'type': float, 'doc': 'Units - Volt/Amp'}, # required
{'name': 'gain', 'type': float, 'doc': 'Units - Volt/Amp', 'default': None},
*get_docval(PatchClampSeries.__init__, 'stimulus_description'),
{'name': 'capacitance_fast', 'type': float, 'doc': 'Unit - Farad', 'default': None},
{'name': 'capacitance_slow', 'type': float, 'doc': 'Unit - Farad', 'default': None},
Expand Down Expand Up @@ -307,16 +308,16 @@ class VoltageClampStimulusSeries(PatchClampSeries):

__nwbfields__ = ()

@docval(*get_docval(PatchClampSeries.__init__, 'name', 'data', 'electrode', 'gain'), # required
*get_docval(PatchClampSeries.__init__, 'stimulus_description', 'resolution', 'conversion', 'timestamps',
'starting_time', 'rate', 'comments', 'description', 'control', 'control_description',
'sweep_number', 'offset'),
@docval(*get_docval(PatchClampSeries.__init__, 'name', 'data', 'electrode'), # required
*get_docval(PatchClampSeries.__init__, 'gain', 'stimulus_description', 'resolution', 'conversion',
'timestamps', 'starting_time', 'rate', 'comments', 'description', 'control',
'control_description', 'sweep_number', 'offset'),
{'name': 'unit', 'type': str, 'doc': "The base unit of measurement (must be 'volts')",
'default': 'volts'})
def __init__(self, **kwargs):
name, data, unit, electrode, gain = popargs('name', 'data', 'unit', 'electrode', 'gain', kwargs)
name, data, unit, electrode = popargs('name', 'data', 'unit', 'electrode', kwargs)
unit = ensure_unit(self, name, unit, 'volts', '2.1.0')
super().__init__(name, data, unit, electrode, gain, **kwargs)
super().__init__(name, data, unit, electrode, **kwargs)


@register_class('SweepTable', CORE_NAMESPACE)
Expand Down
11 changes: 11 additions & 0 deletions tests/integration/hdf5/test_behavior.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,14 @@ def setUpContainer(self):
reference_frame='reference_frame',
timestamps=[1., 2., 3.]
)


class TestSpatialSeriesMinIO(AcquisitionH5IOMixin, TestCase):

def setUpContainer(self):
""" Return the test TimeSeries to read/write """
return SpatialSeries(
name='test_sS',
data=np.ones((3, 2)),
timestamps=[1., 2., 3.]
)
4 changes: 2 additions & 2 deletions tests/integration/hdf5/test_file_copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
class TestFileCopy(TestCase):

def setUp(self):
self.path1 = "test_a.h5"
self.path2 = "test_b.h5"
self.path1 = "test_a.nwb"
self.path2 = "test_b.nwb"

def tearDown(self):
if os.path.exists(self.path1):
Expand Down
21 changes: 21 additions & 0 deletions tests/integration/hdf5/test_icephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,27 @@ def addContainer(self, nwbfile):
nwbfile.add_device(self.device)
super().addContainer(nwbfile)

class TestPatchClampSeriesMin(AcquisitionH5IOMixin, TestCase):
""" Test a PatchClampSeries with minimum required args to read/write """

def setUpElectrode(self):
""" Set up the test IntracellularElectrode """
self.device = Device(name='device_name')
self.elec = IntracellularElectrode(name="elec0", description='a fake electrode object',
device=self.device)

def setUpContainer(self):
self.setUpElectrode()
return PatchClampSeries(name="pcs", data=[1, 2, 3, 4, 5], unit='A',
starting_time=123.6, rate=10e3, electrode=self.elec)

def addContainer(self, nwbfile):
"""
Add the test PatchClampSeries as an acquisition and IntracellularElectrode and Device to the given NWBFile
"""
nwbfile.add_icephys_electrode(self.elec)
nwbfile.add_device(self.device)
super().addContainer(nwbfile)

class TestCurrentClampStimulusSeries(TestPatchClampSeries):

Expand Down
21 changes: 19 additions & 2 deletions tests/integration/hdf5/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ def setUp(self):
self.nwbfile = NWBFile(session_description='a',
identifier='b',
session_start_time=datetime(1970, 1, 1, 12, tzinfo=tzutc()))
self.path = "test_pynwb_io_hdf5_h5dataIO.h5"
self.path = "test_pynwb_io_hdf5_h5dataIO.nwb"

def tearDown(self):
remove_test_file(self.path)
Expand Down Expand Up @@ -429,7 +429,7 @@ def setUp(self):
self.nwbfile = NWBFile(session_description='a test NWB File',
identifier='TEST123',
session_start_time=datetime(1970, 1, 1, 12, tzinfo=tzutc()))
self.path = "test_pynwb_io_nwbhdf5.h5"
self.path = "test_pynwb_io_nwbhdf5.nwb"

def tearDown(self):
remove_test_file(self.path)
Expand Down Expand Up @@ -533,6 +533,23 @@ def test_round_trip_with_pathlib_path(self):
read_file = io.read()
self.assertContainerEqual(read_file, self.nwbfile)

def test_warn_for_nwb_extension(self):
"""Creating a file with an extension other than .nwb should raise a warning"""
pathlib_path = Path(self.path).with_suffix('.h5')

with self.assertWarns(UserWarning):
with NWBHDF5IO(pathlib_path, 'w') as io:
io.write(self.nwbfile)
with self.assertWarns(UserWarning):
with NWBHDF5IO(str(pathlib_path), 'w') as io:
io.write(self.nwbfile)

# should not warn on read or append
with NWBHDF5IO(str(pathlib_path), 'r') as io:
io.read()
with NWBHDF5IO(str(pathlib_path), 'a') as io:
io.read()

def test_can_read_current_nwb_file(self):
with NWBHDF5IO(self.path, 'w') as io:
io.write(self.nwbfile)
Expand Down
9 changes: 9 additions & 0 deletions tests/unit/test_behavior.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ def test_init(self):
self.assertEqual(sS.bounds, [(-1,1),(-1,1),(-1,1)])
self.assertEqual(sS.reference_frame, 'reference_frame')

def test_init_minimum(self):
sS = SpatialSeries(
name='test_sS',
data=np.ones((3, 2)),
timestamps=[1., 2., 3.]
)
assert sS.bounds is None
assert sS.reference_frame is None

def test_set_unit(self):
sS = SpatialSeries(
name='test_sS',
Expand Down
7 changes: 7 additions & 0 deletions tests/unit/test_icephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ def test_default(self):
self.assertEqual(pCS.electrode, electrode_name)
self.assertEqual(pCS.gain, 1.0)

def test_gain_optional(self):
electrode_name = GetElectrode()

pCS = PatchClampSeries('test_pCS', list(), 'unit',
electrode_name, timestamps=list())
self.assertIsNone(pCS.gain)

def test_sweepNumber_valid(self):
electrode_name = GetElectrode()

Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_icephys_metadata_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def setUp(self):
sweep_number=np.uint64(15)
)
self.nwbfile.add_acquisition(self.response)
self.path = 'test_icephys_meta_intracellularrecording.h5'
self.path = 'test_icephys_meta_intracellularrecording.nwb'

def tearDown(self):
remove_test_file(self.path)
Expand Down Expand Up @@ -1037,7 +1037,7 @@ class NWBFileTests(TestCase):
"""
def setUp(self):
warnings.simplefilter("always") # Trigger all warnings
self.path = 'test_icephys_meta_intracellularrecording.h5'
self.path = 'test_icephys_meta_intracellularrecording.nwb'

def tearDown(self):
remove_test_file(self.path)
Expand Down

0 comments on commit fdd89f1

Please sign in to comment.