Skip to content

Commit 2934fb0

Browse files
rlybendichter
andauthored
Merge pull request #1245 from NeurodataWithoutBorders/schema_2.3.0
Co-authored-by: Ryan Ly <[email protected]> Co-authored-by: Ben Dichter <[email protected]>
2 parents 130665a + 50eb5b9 commit 2934fb0

26 files changed

+336
-68
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ jobs:
354354
command: |
355355
python -m venv ../venv
356356
. ../venv/bin/activate
357-
pip install githubrelease
357+
pip install "click<8" githubrelease
358358
githubrelease release $CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME \
359359
create $CIRCLE_TAG --name $CIRCLE_TAG \
360360
--publish ./dist/*

CHANGELOG.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
11
# PyNWB Changelog
22

3-
## PyNWB 1.5.0 (April 23, 2021)
3+
## PyNWB 1.5.0 (May 17, 2021)
44

55
### New features:
66
- `NWBFile.add_scratch(...)` and `ScratchData.__init__(...)` now accept scalar data in addition to the currently
77
accepted types. @rly (#1309)
88
- Support `pathlib.Path` paths when opening files with `NWBHDF5IO`. @dsleiter (#1314)
99
- Use HDMF 2.5.1. See the [HDMF release notes](https://github.com/hdmf-dev/hdmf/releases/tag/2.5.1) for details.
1010
- Support `driver='ros3'` in `NWBHDF5IO` for streaming NWB files directly from s3. @bendichter (#1331)
11-
........ TODO
11+
- Update documentation, CI GitHub processes. @oruebel @yarikoptic, @bendichter, @TomDonoghue, @rly
12+
(#1311, #1336, #1351, #1352, #1345, #1340, #1327)
13+
- Set default `neurodata_type_inc` for `NWBGroupSpec`, `NWBDatasetSpec`. @rly (#1295)
14+
- Add support for nwb-schema 2.3.0. @rly (#1245, #1330)
15+
- Add optional `waveforms` column to the `Units` table.
16+
- Add optional `strain` field to `Subject`.
17+
- Add to `DecompositionSeries` an optional `DynamicTableRegion` called `source_channels`.
18+
- Add to `ImageSeries` an optional link to `Device`.
19+
- Add optional `continuity` field to `TimeSeries`.
20+
- Add optional `filtering` attribute to `ElectricalSeries`.
21+
- Clarify documentation for electrode impedance and filtering.
22+
- Set the `stimulus_description` for `IZeroCurrentClamp` to have the fixed value "N/A".
23+
- See https://nwb-schema.readthedocs.io/en/latest/format_release_notes.html for full schema release notes.
24+
- Add support for HDMF 2.5.3. @rly @ajtritt (#1325, #1355, #1360, #1245, #1287)
25+
- See https://github.com/hdmf-dev/hdmf/releases for full HDMF release notes.
1226

1327
## PyNWB 1.4.0 (August 12, 2020)
1428

requirements-doc.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ sphinx
22
matplotlib
33
sphinx_rtd_theme
44
sphinx-gallery
5-
allensdk
5+
allensdk # note that as of allensdk 2.10.0, python 3.8 is not supported

requirements-min.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# package dependencies and their minimum versions for installing PyNWB
22
# the requirements here specify '==' for testing; setup.py replaces '==' with '>='
33
h5py==2.9,<3 # support for setting attrs to lists of utf-8 added in 2.9
4-
hdmf==2.5.2,<3
4+
hdmf==2.5.5,<3
55
numpy==1.16,<1.21
66
pandas==0.23,<2
77
python-dateutil==2.7,<3

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
h5py==2.10.0
2-
hdmf==2.5.2
2+
hdmf==2.5.5
33
numpy==1.18.5
44
pandas==0.25.3
55
python-dateutil==2.8.1

src/pynwb/base.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ class TimeSeries(NWBDataInterface):
9292
"rate",
9393
"starting_time_unit",
9494
"control",
95-
"control_description")
95+
"control_description",
96+
"continuity")
9697

9798
__time_unit = "seconds"
9899

@@ -119,18 +120,28 @@ class TimeSeries(NWBDataInterface):
119120
{'name': 'control', 'type': Iterable, 'doc': 'Numerical labels that apply to each element in data',
120121
'default': None},
121122
{'name': 'control_description', 'type': Iterable, 'doc': 'Description of each control value',
122-
'default': None})
123+
'default': None},
124+
{'name': 'continuity', 'type': str, 'default': None, 'enum': ["continuous", "instantaneous", "step"],
125+
'doc': 'Optionally describe the continuity of the data. Can be "continuous", "instantaneous", or'
126+
'"step". For example, a voltage trace would be "continuous", because samples are recorded from a '
127+
'continuous process. An array of lick times would be "instantaneous", because the data represents '
128+
'distinct moments in time. Times of image presentations would be "step" because the picture '
129+
'remains the same until the next time-point. This field is optional, but is useful in providing '
130+
'information about the underlying data. It may inform the way this data is interpreted, the way it '
131+
'is visualized, and what analysis methods are applicable.'})
123132
def __init__(self, **kwargs):
124133
"""Create a TimeSeries object
125134
"""
135+
126136
call_docval_func(super(TimeSeries, self).__init__, kwargs)
127137
keys = ("resolution",
128138
"comments",
129139
"description",
130140
"conversion",
131141
"unit",
132142
"control",
133-
"control_description")
143+
"control_description",
144+
"continuity")
134145
for key in keys:
135146
val = kwargs.get(key)
136147
if val is not None:

src/pynwb/ecephys.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ class ElectricalSeries(TimeSeries):
4949

5050
__nwbfields__ = ({'name': 'electrodes', 'required_name': 'electrodes',
5151
'doc': 'the electrodes that generated this electrical series', 'child': True},
52-
'channel_conversion')
52+
'channel_conversion',
53+
'filtering')
5354

5455
@docval(*get_docval(TimeSeries.__init__, 'name'), # required
5556
{'name': 'data', 'type': ('array_data', 'data', TimeSeries), # required
@@ -66,13 +67,21 @@ class ElectricalSeries(TimeSeries):
6667
"to support the storage of electrical recordings as native values generated by data acquisition systems. "
6768
"If this dataset is not present, then there is no channel-specific conversion factor, i.e. it is 1 for all"
6869
" channels.", 'default': None},
70+
{'name': 'filtering', 'type': str, 'doc':
71+
"Filtering applied to all channels of the data. For example, if this ElectricalSeries represents "
72+
"high-pass-filtered data (also known as AP Band), then this value could be 'High-pass 4-pole Bessel "
73+
"filter at 500 Hz'. If this ElectricalSeries represents low-pass-filtered LFP data and the type of "
74+
"filter is unknown, then this value could be 'Low-pass filter at 300 Hz'. If a non-standard filter "
75+
"type is used, provide as much detail about the filter properties as possible.", 'default': None},
6976
*get_docval(TimeSeries.__init__, 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate',
7077
'comments', 'description', 'control', 'control_description'))
7178
def __init__(self, **kwargs):
72-
name, electrodes, data, channel_conversion = popargs('name', 'electrodes', 'data', 'channel_conversion', kwargs)
79+
name, electrodes, data, channel_conversion, filtering = popargs('name', 'electrodes', 'data',
80+
'channel_conversion', 'filtering', kwargs)
7381
super(ElectricalSeries, self).__init__(name, data, 'volts', **kwargs)
7482
self.electrodes = electrodes
7583
self.channel_conversion = channel_conversion
84+
self.filtering = filtering
7685

7786

7887
@register_class('SpikeEventSeries', CORE_NAMESPACE)

src/pynwb/file.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,33 @@ class Subject(NWBContainer):
4444
'species',
4545
'subject_id',
4646
'weight',
47-
'date_of_birth'
47+
'date_of_birth',
48+
'strain'
4849
)
4950

50-
@docval({'name': 'age', 'type': str, 'doc': 'the age of the subject', 'default': None},
51-
{'name': 'description', 'type': str, 'doc': 'a description of the subject', 'default': None},
52-
{'name': 'genotype', 'type': str, 'doc': 'the genotype of the subject', 'default': None},
53-
{'name': 'sex', 'type': str, 'doc': 'the sex of the subject', 'default': None},
54-
{'name': 'species', 'type': str, 'doc': 'the species of the subject', 'default': None},
55-
{'name': 'subject_id', 'type': str, 'doc': 'a unique identifier for the subject', 'default': None},
56-
{'name': 'weight', 'type': str, 'doc': 'the weight of the subject', 'default': None},
51+
@docval({'name': 'age', 'type': str,
52+
'doc': ('The age of the subject. The ISO 8601 Duration format is recommended, e.g., "P90D" for '
53+
'90 days old.'), 'default': None},
54+
{'name': 'description', 'type': str,
55+
'doc': 'A description of the subject, e.g., "mouse A10".', 'default': None},
56+
{'name': 'genotype', 'type': str,
57+
'doc': 'The genotype of the subject, e.g., "Sst-IRES-Cre/wt;Ai32(RCL-ChR2(H134R)_EYFP)/wt".',
58+
'default': None},
59+
{'name': 'sex', 'type': str,
60+
'doc': ('The sex of the subject. Using "F" (female), "M" (male), "U" (unknown), or "O" (other) '
61+
'is recommended.'), 'default': None},
62+
{'name': 'species', 'type': str,
63+
'doc': ('The species of the subject. The formal latin binomal name is recommended, e.g., "Mus musculus"'),
64+
'default': None},
65+
{'name': 'subject_id', 'type': str, 'doc': 'A unique identifier for the subject, e.g., "A10"',
66+
'default': None},
67+
{'name': 'weight', 'type': (float, str),
68+
'doc': ('The weight of the subject, including units. Using kilograms is recommended. e.g., "0.02 kg". '
69+
'If a float is provided, then the weight will be stored as "[value] kg".'),
70+
'default': None},
5771
{'name': 'date_of_birth', 'type': datetime, 'default': None,
58-
'doc': 'datetime of date of birth. May be supplied instead of age.'})
72+
'doc': 'The datetime of the date of birth. May be supplied instead of age.'},
73+
{'name': 'strain', 'type': str, 'doc': 'The strain of the subject, e.g., "C57BL/6J"', 'default': None})
5974
def __init__(self, **kwargs):
6075
kwargs['name'] = 'subject'
6176
call_docval_func(super(Subject, self).__init__, kwargs)
@@ -65,7 +80,11 @@ def __init__(self, **kwargs):
6580
self.sex = getargs('sex', kwargs)
6681
self.species = getargs('species', kwargs)
6782
self.subject_id = getargs('subject_id', kwargs)
68-
self.weight = getargs('weight', kwargs)
83+
weight = getargs('weight', kwargs)
84+
if isinstance(weight, float):
85+
weight = str(weight) + ' kg'
86+
self.weight = weight
87+
self.strain = getargs('strain', kwargs)
6988
date_of_birth = getargs('date_of_birth', kwargs)
7089
if date_of_birth and date_of_birth.tzinfo is None:
7190
self.date_of_birth = _add_missing_timezone(date_of_birth)

src/pynwb/icephys.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class PatchClampSeries(TimeSeries):
7171
'doc': 'IntracellularElectrode group that describes the electrode that was used to apply '
7272
'or record this data.'},
7373
{'name': 'gain', 'type': 'float', 'doc': 'Units: Volt/Amp (v-clamp) or Volt/Volt (c-clamp)'}, # required
74-
{'name': 'stimulus_description', 'type': str, 'doc': 'the stimulus name/protocol', 'default': "NA"},
74+
{'name': 'stimulus_description', 'type': str, 'doc': 'the stimulus name/protocol', 'default': "N/A"},
7575
*get_docval(TimeSeries.__init__, 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate',
7676
'comments', 'description', 'control', 'control_description'),
7777
{'name': 'sweep_number', 'type': (int, 'uint32', 'uint64'),
@@ -138,17 +138,37 @@ class IZeroClampSeries(CurrentClampSeries):
138138

139139
@docval(*get_docval(CurrentClampSeries.__init__, 'name', 'data', 'electrode'), # required
140140
{'name': 'gain', 'type': 'float', 'doc': 'Units: Volt/Volt'}, # required
141-
*get_docval(CurrentClampSeries.__init__, 'stimulus_description', 'resolution', 'conversion', 'timestamps',
141+
{'name': 'stimulus_description', 'type': str,
142+
'doc': ('The stimulus name/protocol. Setting this to a value other than "N/A" is deprecated as of '
143+
'NWB 2.3.0.'),
144+
'default': 'N/A'},
145+
*get_docval(CurrentClampSeries.__init__, 'resolution', 'conversion', 'timestamps',
142146
'starting_time', 'rate', 'comments', 'description', 'control', 'control_description',
143147
'sweep_number'),
144148
{'name': 'unit', 'type': str, 'doc': "The base unit of measurement (must be 'volts')",
145149
'default': 'volts'})
146150
def __init__(self, **kwargs):
147151
name, data, electrode, gain = popargs('name', 'data', 'electrode', 'gain', kwargs)
148152
bias_current, bridge_balance, capacitance_compensation = (0.0, 0.0, 0.0)
153+
stimulus_description = popargs('stimulus_description', kwargs)
154+
stimulus_description = self._ensure_stimulus_description(name, stimulus_description, 'N/A', '2.3.0')
155+
kwargs['stimulus_description'] = stimulus_description
149156
super().__init__(name, data, electrode, gain, bias_current, bridge_balance, capacitance_compensation,
150157
**kwargs)
151158

159+
def _ensure_stimulus_description(self, name, current_stim_desc, stim_desc, nwb_version):
160+
"""A helper to ensure correct stimulus_description used.
161+
162+
Issues a warning with details if `current_stim_desc` is to be ignored, and
163+
`stim_desc` to be used instead.
164+
"""
165+
if current_stim_desc != stim_desc:
166+
warnings.warn(
167+
"Stimulus description '%s' for %s '%s' is ignored and will be set to '%s' "
168+
"as per NWB %s."
169+
% (current_stim_desc, self.__class__.__name__, name, stim_desc, nwb_version))
170+
return stim_desc
171+
152172

153173
@register_class('CurrentClampStimulusSeries', CORE_NAMESPACE)
154174
class CurrentClampStimulusSeries(PatchClampSeries):

src/pynwb/image.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from . import register_class, CORE_NAMESPACE
77
from .base import TimeSeries, Image
8+
from .device import Device
89

910

1011
@register_class('ImageSeries', CORE_NAMESPACE)
@@ -17,7 +18,8 @@ class ImageSeries(TimeSeries):
1718
__nwbfields__ = ('dimension',
1819
'external_file',
1920
'starting_frame',
20-
'format')
21+
'format',
22+
'device')
2123

2224
@docval(*get_docval(TimeSeries.__init__, 'name'), # required
2325
{'name': 'data', 'type': ('array_data', 'data', TimeSeries), 'shape': ([None] * 3, [None] * 4),
@@ -40,10 +42,12 @@ class ImageSeries(TimeSeries):
4042
{'name': 'dimension', 'type': Iterable,
4143
'doc': 'Number of pixels on x, y, (and z) axes.', 'default': None},
4244
*get_docval(TimeSeries.__init__, 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate',
43-
'comments', 'description', 'control', 'control_description'))
45+
'comments', 'description', 'control', 'control_description'),
46+
{'name': 'device', 'type': Device,
47+
'doc': 'Device used to capture the images/video.', 'default': None},)
4448
def __init__(self, **kwargs):
45-
bits_per_pixel, dimension, external_file, starting_frame, format = popargs(
46-
'bits_per_pixel', 'dimension', 'external_file', 'starting_frame', 'format', kwargs)
49+
bits_per_pixel, dimension, external_file, starting_frame, format, device = popargs(
50+
'bits_per_pixel', 'dimension', 'external_file', 'starting_frame', 'format', 'device', kwargs)
4751
call_docval_func(super(ImageSeries, self).__init__, kwargs)
4852
if external_file is None and self.data is None:
4953
raise ValueError("Must supply either external_file or data to %s '%s'."
@@ -56,6 +60,7 @@ def __init__(self, **kwargs):
5660
else:
5761
self.starting_frame = None
5862
self.format = format
63+
self.device = device
5964

6065
@property
6166
def bits_per_pixel(self):
@@ -111,7 +116,11 @@ class ImageMaskSeries(ImageSeries):
111116
'doc': 'Link to ImageSeries that mask is applied to.'},
112117
*get_docval(ImageSeries.__init__, 'format', 'external_file', 'starting_frame', 'bits_per_pixel',
113118
'dimension', 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate', 'comments',
114-
'description', 'control', 'control_description'))
119+
'description', 'control', 'control_description'),
120+
{'name': 'device', 'type': Device,
121+
'doc': ('Device used to capture the mask data. This field will likely not be needed. '
122+
'The device used to capture the masked ImageSeries data should be stored in the ImageSeries.'),
123+
'default': None},)
115124
def __init__(self, **kwargs):
116125
masked_imageseries = popargs('masked_imageseries', kwargs)
117126
super(ImageMaskSeries, self).__init__(**kwargs)
@@ -146,7 +155,7 @@ class OpticalSeries(ImageSeries):
146155
'Must also specify frame of reference.'},
147156
*get_docval(ImageSeries.__init__, 'external_file', 'starting_frame', 'bits_per_pixel',
148157
'dimension', 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate', 'comments',
149-
'description', 'control', 'control_description'))
158+
'description', 'control', 'control_description', 'device'))
150159
def __init__(self, **kwargs):
151160
distance, field_of_view, orientation = popargs('distance', 'field_of_view', 'orientation', kwargs)
152161
super(OpticalSeries, self).__init__(**kwargs)

0 commit comments

Comments
 (0)