From 36a46b37339198d3b52ca95a9a8fe7ae4d8f8b5d Mon Sep 17 00:00:00 2001 From: Ben Dichter Date: Thu, 18 Jun 2020 15:17:23 -0400 Subject: [PATCH 01/14] add strain optional arg to Subject (#1241) * remove validate_core_schema. You can check the core schema with e.g. * Update Subject unit test * Fix Subject unit test, add check for date_of_birth Co-authored-by: Ryan Ly --- src/pynwb/file.py | 7 +++++-- tests/integration/hdf5/test_nwbfile.py | 5 +++-- tests/unit/test_file.py | 9 ++++++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/pynwb/file.py b/src/pynwb/file.py index bea4eeb40..d37c09f1f 100644 --- a/src/pynwb/file.py +++ b/src/pynwb/file.py @@ -44,7 +44,8 @@ class Subject(NWBContainer): 'species', 'subject_id', 'weight', - 'date_of_birth' + 'date_of_birth', + 'strain' ) @docval({'name': 'age', 'type': str, 'doc': 'the age of the subject', 'default': None}, @@ -55,7 +56,8 @@ class Subject(NWBContainer): {'name': 'subject_id', 'type': str, 'doc': 'a unique identifier for the subject', 'default': None}, {'name': 'weight', 'type': str, 'doc': 'the weight of the subject', 'default': None}, {'name': 'date_of_birth', 'type': datetime, 'default': None, - 'doc': 'datetime of date of birth. May be supplied instead of age.'}) + 'doc': 'datetime of date of birth. May be supplied instead of age.'}, + {'name': 'strain', 'type': str, 'doc': 'the strain of the subject', 'default': None}) def __init__(self, **kwargs): kwargs['name'] = 'subject' call_docval_func(super(Subject, self).__init__, kwargs) @@ -66,6 +68,7 @@ def __init__(self, **kwargs): self.species = getargs('species', kwargs) self.subject_id = getargs('subject_id', kwargs) self.weight = getargs('weight', kwargs) + self.strain = getargs('strain', kwargs) date_of_birth = getargs('date_of_birth', kwargs) if date_of_birth and date_of_birth.tzinfo is None: self.date_of_birth = _add_missing_timezone(date_of_birth) diff --git a/tests/integration/hdf5/test_nwbfile.py b/tests/integration/hdf5/test_nwbfile.py index f9168b642..7e7657589 100644 --- a/tests/integration/hdf5/test_nwbfile.py +++ b/tests/integration/hdf5/test_nwbfile.py @@ -203,14 +203,15 @@ class TestSubjectIO(NWBH5IOMixin, TestCase): def setUpContainer(self): """ Return the test Subject """ - return Subject(age='12 mo', + return Subject(age='P90D', description='An unfortunate rat', genotype='WT', sex='M', species='Rattus norvegicus', subject_id='RAT123', weight='2 lbs', - date_of_birth=datetime(1970, 1, 1, 12, tzinfo=tzutc())) + date_of_birth=datetime(1970, 1, 1, 12, tzinfo=tzutc()), + strain='my_strain') def addContainer(self, nwbfile): """ Add the test Subject to the given NWBFile """ diff --git a/tests/unit/test_file.py b/tests/unit/test_file.py index 8476914f5..e29cb67ec 100644 --- a/tests/unit/test_file.py +++ b/tests/unit/test_file.py @@ -417,14 +417,15 @@ def test_multi_publications(self): class SubjectTest(TestCase): def setUp(self): - self.subject = Subject(age='12 mo', + self.subject = Subject(age='P90D', description='An unfortunate rat', genotype='WT', sex='M', species='Rattus norvegicus', subject_id='RAT123', weight='2 lbs', - date_of_birth=datetime(2017, 5, 1, 12, tzinfo=tzlocal())) + date_of_birth=datetime(2017, 5, 1, 12, tzinfo=tzlocal()), + strain='my_strain') self.start = datetime(2017, 5, 1, 12, tzinfo=tzlocal()) self.path = 'nwbfile_test.h5' self.nwbfile = NWBFile('a test session description for a test NWBFile', @@ -438,13 +439,15 @@ def setUp(self): subject=self.subject) def test_constructor(self): - self.assertEqual(self.subject.age, '12 mo') + self.assertEqual(self.subject.age, 'P90D') self.assertEqual(self.subject.description, 'An unfortunate rat') self.assertEqual(self.subject.genotype, 'WT') self.assertEqual(self.subject.sex, 'M') self.assertEqual(self.subject.species, 'Rattus norvegicus') self.assertEqual(self.subject.subject_id, 'RAT123') self.assertEqual(self.subject.weight, '2 lbs') + self.assertEqual(self.subject.date_of_birth, datetime(2017, 5, 1, 12, tzinfo=tzlocal())) + self.assertEqual(self.subject.strain, 'my_strain') def test_nwbfile_constructor(self): self.assertIs(self.nwbfile.subject, self.subject) From 80366d4feda86b054da21f6f61294fa9d725ba8d Mon Sep 17 00:00:00 2001 From: "!git for-each-ref --format='%(refname:short)' `git symbolic-ref HEAD`" Date: Thu, 9 Jul 2020 19:36:41 -0400 Subject: [PATCH 02/14] add source channels to DecompSeries --- src/pynwb/nwb-schema | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pynwb/nwb-schema b/src/pynwb/nwb-schema index aa7702e1c..2fc379e09 160000 --- a/src/pynwb/nwb-schema +++ b/src/pynwb/nwb-schema @@ -1 +1 @@ -Subproject commit aa7702e1cc5be2b85f19206e0d1d50bc4bd7e296 +Subproject commit 2fc379e09f66ddbde6c06947fa1b94dae1189990 From 7e40410a597da72abb0f92d48d620b139eb0603a Mon Sep 17 00:00:00 2001 From: Ben Dichter Date: Wed, 22 Jul 2020 14:07:17 -0400 Subject: [PATCH 03/14] add option to include source_channels for DecompositionSeries (#1258) * add option to include source_channels for DecompositionSeries and include round trip test * Use latest schema * Add unit test --- src/pynwb/misc.py | 16 ++++++--- tests/integration/hdf5/test_misc.py | 55 ++++++++++++++++++++++++++++- tests/unit/test_misc.py | 33 +++++++++++++++-- 3 files changed, 96 insertions(+), 8 deletions(-) diff --git a/src/pynwb/misc.py b/src/pynwb/misc.py index 18222bbd8..6124b18e3 100644 --- a/src/pynwb/misc.py +++ b/src/pynwb/misc.py @@ -7,7 +7,7 @@ from . import register_class, CORE_NAMESPACE from .base import TimeSeries -from hdmf.common import DynamicTable +from hdmf.common import DynamicTable, DynamicTableRegion @register_class('AnnotationSeries', CORE_NAMESPACE) @@ -251,6 +251,7 @@ class DecompositionSeries(TimeSeries): __nwbfields__ = ('metric', {'name': 'source_timeseries', 'child': False, 'doc': 'the input TimeSeries from this analysis'}, + {'name': 'source_channels', 'child': True, 'doc': 'the channels that provided the source data'}, {'name': 'bands', 'doc': 'the bands that the signal is decomposed into', 'child': True}) @@ -267,15 +268,20 @@ class DecompositionSeries(TimeSeries): 'doc': 'a table for describing the frequency bands that the signal was decomposed into', 'default': None}, {'name': 'source_timeseries', 'type': TimeSeries, 'doc': 'the input TimeSeries from this analysis', 'default': None}, + {'name': 'source_channels', 'type': DynamicTableRegion, 'doc': 'the channels that provided the source data', + 'default': None}, *get_docval(TimeSeries.__init__, 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate', 'comments', 'control', 'control_description')) def __init__(self, **kwargs): - metric, source_timeseries, bands = popargs('metric', 'source_timeseries', 'bands', kwargs) + metric, source_timeseries, bands, source_channels = popargs('metric', 'source_timeseries', 'bands', + 'source_channels', kwargs) super(DecompositionSeries, self).__init__(**kwargs) self.source_timeseries = source_timeseries - if self.source_timeseries is None: - warnings.warn("It is best practice to set `source_timeseries` if it is present to document " - "where the DecompositionSeries was derived from. (Optional)") + self.source_channels = source_channels + if self.source_timeseries is None and self.source_channels is None: + warnings.warn("Neither source_timeseries nor source_channels is present in DecompositionSeries. It is " + "recommended to indicate the source timeseries if it is present, or else to link to the " + "corresponding source_channels. (Optional)") self.metric = metric if bands is None: bands = DynamicTable("bands", "data about the frequency bands that the signal was decomposed into") diff --git a/tests/integration/hdf5/test_misc.py b/tests/integration/hdf5/test_misc.py index 396985993..abe9e9ef1 100644 --- a/tests/integration/hdf5/test_misc.py +++ b/tests/integration/hdf5/test_misc.py @@ -1,9 +1,12 @@ import numpy as np -from hdmf.common import DynamicTable, VectorData +from hdmf.common import DynamicTable, VectorData, DynamicTableRegion from pynwb import TimeSeries from pynwb.misc import Units, DecompositionSeries from pynwb.testing import NWBH5IOMixin, AcquisitionH5IOMixin, TestCase +from pynwb.ecephys import ElectrodeGroup +from pynwb.device import Device +from pynwb.file import ElectrodeTable as get_electrode_table class TestUnitsIO(AcquisitionH5IOMixin, TestCase): @@ -134,3 +137,53 @@ def addContainer(self, nwbfile): def getContainer(self, nwbfile): """ Return the test DecompositionSeries from the given NWBFile """ return nwbfile.processing['test_mod']['LFPSpectralAnalysis'] + + +class TestDecompositionSeriesWithSourceChannelsIO(AcquisitionH5IOMixin, TestCase): + + @staticmethod + def make_electrode_table(self): + """ Make an electrode table, electrode group, and device """ + self.table = get_electrode_table() + self.dev1 = Device(name='dev1') + self.group = ElectrodeGroup(name='tetrode1', + description='tetrode description', + location='tetrode location', + device=self.dev1) + for i in range(4): + self.table.add_row(x=i, y=2.0, z=3.0, imp=-1.0, location='CA1', filtering='none', group=self.group, + group_name='tetrode1') + + def setUpContainer(self): + """ Return the test ElectricalSeries to read/write """ + self.make_electrode_table(self) + region = DynamicTableRegion(name='source_channels', + data=[0, 2], + description='the first and third electrodes', + table=self.table) + data = np.random.randn(100, 2, 30) + timestamps = np.arange(100)/100 + ds = DecompositionSeries(name='test_DS', + data=data, + source_channels=region, + timestamps=timestamps, + metric='amplitude') + return ds + + def addContainer(self, nwbfile): + """ Add the test ElectricalSeries and related objects to the given NWBFile """ + nwbfile.add_device(self.dev1) + nwbfile.add_electrode_group(self.group) + nwbfile.set_electrode_table(self.table) + nwbfile.add_acquisition(self.container) + + def test_eg_ref(self): + """ + Test that the electrode DynamicTableRegion references of the read ElectricalSeries have a group that + correctly resolves to ElectrodeGroup instances. + """ + read = self.roundtripContainer() + row1 = read.source_channels[0] + row2 = read.source_channels[1] + self.assertIsInstance(row1.iloc[0]['group'], ElectrodeGroup) + self.assertIsInstance(row2.iloc[0]['group'], ElectrodeGroup) diff --git a/tests/unit/test_misc.py b/tests/unit/test_misc.py index 3417af97e..4412063ce 100644 --- a/tests/unit/test_misc.py +++ b/tests/unit/test_misc.py @@ -1,9 +1,9 @@ import numpy as np -from hdmf.common import DynamicTable, VectorData +from hdmf.common import DynamicTable, VectorData, DynamicTableRegion from pynwb.misc import AnnotationSeries, AbstractFeatureSeries, IntervalSeries, Units, DecompositionSeries -from pynwb.file import TimeSeries +from pynwb.file import TimeSeries, ElectrodeTable as get_electrode_table from pynwb.device import Device from pynwb.ecephys import ElectrodeGroup from pynwb.testing import TestCase @@ -74,6 +74,35 @@ def test_init_delayed_bands(self): self.assertEqual(spec_anal.source_timeseries, timeseries) self.assertEqual(spec_anal.metric, 'amplitude') + @staticmethod + def make_electrode_table(self): + """ Make an electrode table, electrode group, and device """ + self.table = get_electrode_table() + self.dev1 = Device(name='dev1') + self.group = ElectrodeGroup(name='tetrode1', + description='tetrode description', + location='tetrode location', + device=self.dev1) + for i in range(4): + self.table.add_row(x=i, y=2.0, z=3.0, imp=-1.0, location='CA1', filtering='none', group=self.group, + group_name='tetrode1') + + def test_init_with_source_channels(self): + self.make_electrode_table(self) + region = DynamicTableRegion(name='source_channels', + data=[0, 2], + description='the first and third electrodes', + table=self.table) + data = np.random.randn(100, 2, 30) + timestamps = np.arange(100)/100 + ds = DecompositionSeries(name='test_DS', + data=data, + source_channels=region, + timestamps=timestamps, + metric='amplitude') + + self.assertIs(ds.source_channels, region) + class IntervalSeriesConstructor(TestCase): def test_init(self): From 714715026398c993b4522ec270c608255fe1e507 Mon Sep 17 00:00:00 2001 From: Ryan Ly Date: Tue, 28 Jul 2020 11:43:49 -0700 Subject: [PATCH 04/14] Add optional link to Device in ImageSeries (#1265) * Add optional link to Device in ImageSeries * Add support for device in ImageSeries subclasses * Update ImageMaskSeries docstring * Update schema --- src/pynwb/image.py | 21 ++++++++---- src/pynwb/ophys.py | 2 +- tests/integration/hdf5/test_image.py | 49 ++++++++++++++++++++++------ tests/unit/test_image.py | 18 +++++++--- 4 files changed, 69 insertions(+), 21 deletions(-) diff --git a/src/pynwb/image.py b/src/pynwb/image.py index ad7af3761..666177296 100644 --- a/src/pynwb/image.py +++ b/src/pynwb/image.py @@ -5,6 +5,7 @@ from . import register_class, CORE_NAMESPACE from .base import TimeSeries, Image +from .device import Device @register_class('ImageSeries', CORE_NAMESPACE) @@ -17,7 +18,8 @@ class ImageSeries(TimeSeries): __nwbfields__ = ('dimension', 'external_file', 'starting_frame', - 'format') + 'format', + 'device') @docval(*get_docval(TimeSeries.__init__, 'name'), # required {'name': 'data', 'type': ('array_data', 'data', TimeSeries), 'shape': ([None] * 3, [None] * 4), @@ -40,10 +42,12 @@ class ImageSeries(TimeSeries): {'name': 'dimension', 'type': Iterable, 'doc': 'Number of pixels on x, y, (and z) axes.', 'default': None}, *get_docval(TimeSeries.__init__, 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate', - 'comments', 'description', 'control', 'control_description')) + 'comments', 'description', 'control', 'control_description'), + {'name': 'device', 'type': Device, + 'doc': 'Device used to capture the images/video.', 'default': None},) def __init__(self, **kwargs): - bits_per_pixel, dimension, external_file, starting_frame, format = popargs( - 'bits_per_pixel', 'dimension', 'external_file', 'starting_frame', 'format', kwargs) + bits_per_pixel, dimension, external_file, starting_frame, format, device = popargs( + 'bits_per_pixel', 'dimension', 'external_file', 'starting_frame', 'format', 'device', kwargs) call_docval_func(super(ImageSeries, self).__init__, kwargs) if external_file is None and self.data is None: raise ValueError("Must supply either external_file or data to %s '%s'." @@ -56,6 +60,7 @@ def __init__(self, **kwargs): else: self.starting_frame = None self.format = format + self.device = device @property def bits_per_pixel(self): @@ -111,7 +116,11 @@ class ImageMaskSeries(ImageSeries): 'doc': 'Link to ImageSeries that mask is applied to.'}, *get_docval(ImageSeries.__init__, 'format', 'external_file', 'starting_frame', 'bits_per_pixel', 'dimension', 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate', 'comments', - 'description', 'control', 'control_description')) + 'description', 'control', 'control_description'), + {'name': 'device', 'type': Device, + 'doc': ('Device used to capture the mask data. This field will likely not be needed. ' + 'The device used to capture the masked ImageSeries data should be stored in the ImageSeries.'), + 'default': None},) def __init__(self, **kwargs): masked_imageseries = popargs('masked_imageseries', kwargs) super(ImageMaskSeries, self).__init__(**kwargs) @@ -146,7 +155,7 @@ class OpticalSeries(ImageSeries): 'Must also specify frame of reference.'}, *get_docval(ImageSeries.__init__, 'external_file', 'starting_frame', 'bits_per_pixel', 'dimension', 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate', 'comments', - 'description', 'control', 'control_description')) + 'description', 'control', 'control_description', 'device')) def __init__(self, **kwargs): distance, field_of_view, orientation = popargs('distance', 'field_of_view', 'orientation', kwargs) super(OpticalSeries, self).__init__(**kwargs) diff --git a/src/pynwb/ophys.py b/src/pynwb/ophys.py index 2dd329749..06059ed75 100644 --- a/src/pynwb/ophys.py +++ b/src/pynwb/ophys.py @@ -142,7 +142,7 @@ class TwoPhotonSeries(ImageSeries): 'default': None}, *get_docval(ImageSeries.__init__, 'external_file', 'starting_frame', 'bits_per_pixel', 'dimension', 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate', - 'comments', 'description', 'control', 'control_description')) + 'comments', 'description', 'control', 'control_description', 'device')) def __init__(self, **kwargs): field_of_view, imaging_plane, pmt_gain, scan_line_rate = popargs( 'field_of_view', 'imaging_plane', 'pmt_gain', 'scan_line_rate', kwargs) diff --git a/tests/integration/hdf5/test_image.py b/tests/integration/hdf5/test_image.py index 558fb3c98..d10770f83 100644 --- a/tests/integration/hdf5/test_image.py +++ b/tests/integration/hdf5/test_image.py @@ -1,25 +1,54 @@ import numpy as np -from pynwb.image import OpticalSeries -from pynwb.testing import NWBH5IOMixin, TestCase +from pynwb.device import Device +from pynwb.image import ImageSeries, OpticalSeries +from pynwb.testing import AcquisitionH5IOMixin, NWBH5IOMixin, TestCase + + +class TestImageSeriesIO(AcquisitionH5IOMixin, TestCase): + + def setUpContainer(self): + """ Return a test ImageSeries to read/write """ + self.dev1 = Device('dev1') + iS = ImageSeries( + name='test_iS', + data=np.ones((3, 3, 3)), + unit='unit', + external_file=['external_file'], + starting_frame=[1, 2, 3], + format='tiff', + timestamps=list(), + device=self.dev1, + ) + return iS + + def addContainer(self, nwbfile): + """ Add the test ElectrodeGroup to the given NWBFile """ + nwbfile.add_device(self.dev1) + super().addContainer(nwbfile) class TestOpticalSeriesIO(NWBH5IOMixin, TestCase): def setUpContainer(self): """ Return a test OpticalSeries to read/write """ - self.optical_series = OpticalSeries(name='OpticalSeries', - distance=8., - field_of_view=(4., 5.), - orientation='upper left', - data=np.ones((10, 3, 3)), - unit='m', - format='raw', - timestamps=np.arange(10.)) + self.dev1 = Device('dev1') + self.optical_series = OpticalSeries( + name='OpticalSeries', + distance=8., + field_of_view=(4., 5.), + orientation='upper left', + data=np.ones((10, 3, 3)), + unit='m', + format='raw', + timestamps=np.arange(10.), + device=self.dev1, + ) return self.optical_series def addContainer(self, nwbfile): """ Add the test OpticalSeries to the given NWBFile """ + nwbfile.add_device(self.dev1) nwbfile.add_stimulus(self.optical_series) def getContainer(self, nwbfile): diff --git a/tests/unit/test_image.py b/tests/unit/test_image.py index 063cbea9e..c79a7b0d5 100644 --- a/tests/unit/test_image.py +++ b/tests/unit/test_image.py @@ -1,21 +1,31 @@ import numpy as np from pynwb import TimeSeries -from pynwb.image import ImageSeries, IndexSeries, ImageMaskSeries, OpticalSeries, \ - GrayscaleImage, RGBImage, RGBAImage +from pynwb.device import Device +from pynwb.image import ImageSeries, IndexSeries, ImageMaskSeries, OpticalSeries, GrayscaleImage, RGBImage, RGBAImage from pynwb.testing import TestCase class ImageSeriesConstructor(TestCase): def test_init(self): - iS = ImageSeries(name='test_iS', data=np.ones((3, 3, 3)), unit='unit', - external_file=['external_file'], starting_frame=[1, 2, 3], format='tiff', timestamps=list()) + dev = Device('test_device') + iS = ImageSeries( + name='test_iS', + data=np.ones((3, 3, 3)), + unit='unit', + external_file=['external_file'], + starting_frame=[1, 2, 3], + format='tiff', + timestamps=list(), + device=dev, + ) self.assertEqual(iS.name, 'test_iS') self.assertEqual(iS.unit, 'unit') self.assertEqual(iS.external_file, ['external_file']) self.assertEqual(iS.starting_frame, [1, 2, 3]) self.assertEqual(iS.format, 'tiff') + self.assertIs(iS.device, dev) # self.assertEqual(iS.bits_per_pixel, np.nan) def test_no_data_no_file(self): From 0a5717e572d6eb12d4d966db983a263dd5d6b70b Mon Sep 17 00:00:00 2001 From: Ben Dichter Date: Thu, 30 Jul 2020 14:16:32 -0400 Subject: [PATCH 05/14] add continuity optional arg to TimeSeries.__init__ (#1226) * add continuity optional arg to TimeSeries.__init__ * use new enum feature of hdmf to check against controlled vocabulary * Fix flake8 * Add integration test * Fix flake8 * Add correct mapping for data.continuity Co-authored-by: Ryan Ly --- src/pynwb/base.py | 17 ++++++++++++++--- src/pynwb/io/base.py | 1 + tests/integration/hdf5/test_base.py | 3 ++- tests/unit/test_base.py | 12 ++++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/pynwb/base.py b/src/pynwb/base.py index fceb6776e..bc9fcd9df 100644 --- a/src/pynwb/base.py +++ b/src/pynwb/base.py @@ -92,7 +92,8 @@ class TimeSeries(NWBDataInterface): "rate", "starting_time_unit", "control", - "control_description") + "control_description", + "continuity") __time_unit = "seconds" @@ -119,10 +120,19 @@ class TimeSeries(NWBDataInterface): {'name': 'control', 'type': Iterable, 'doc': 'Numerical labels that apply to each element in data', 'default': None}, {'name': 'control_description', 'type': Iterable, 'doc': 'Description of each control value', - 'default': None}) + 'default': None}, + {'name': 'continuity', 'type': str, 'default': None, 'enum': ["continuous", "instantaneous", "step"], + 'doc': 'Optionally describe the continuity of the data. Can be "continuous", "instantaneous", or' + '"step". For example, a voltage trace would be "continuous", because samples are recorded from a ' + 'continuous process. An array of lick times would be "instantaneous", because the data represents ' + 'distinct moments in time. Times of image presentations would be "step" because the picture ' + 'remains the same until the next time-point. This field is optional, but is useful in providing ' + 'information about the underlying data. It may inform the way this data is interpreted, the way it ' + 'is visualized, and what analysis methods are applicable.'}) def __init__(self, **kwargs): """Create a TimeSeries object """ + call_docval_func(super(TimeSeries, self).__init__, kwargs) keys = ("resolution", "comments", @@ -130,7 +140,8 @@ def __init__(self, **kwargs): "conversion", "unit", "control", - "control_description") + "control_description", + "continuity") for key in keys: val = kwargs.get(key) if val is not None: diff --git a/src/pynwb/io/base.py b/src/pynwb/io/base.py index dd5f499d2..b1aaa940b 100644 --- a/src/pynwb/io/base.py +++ b/src/pynwb/io/base.py @@ -29,6 +29,7 @@ def __init__(self, spec): self.map_spec('unit', data_spec.get_attribute('unit')) self.map_spec('resolution', data_spec.get_attribute('resolution')) self.map_spec('conversion', data_spec.get_attribute('conversion')) + self.map_spec('continuity', data_spec.get_attribute('continuity')) timestamps_spec = self.spec.get_dataset('timestamps') self.map_spec('timestamps_unit', timestamps_spec.get_attribute('unit')) diff --git a/tests/integration/hdf5/test_base.py b/tests/integration/hdf5/test_base.py index 3a7c06221..a13ffe7ff 100644 --- a/tests/integration/hdf5/test_base.py +++ b/tests/integration/hdf5/test_base.py @@ -15,7 +15,8 @@ def setUpContainer(self): data=list(range(1000)), unit='SIunit', timestamps=np.arange(1000.), - resolution=0.1 + resolution=0.1, + continuity='continuous', ) diff --git a/tests/unit/test_base.py b/tests/unit/test_base.py index 99dfce1d1..c15af44ec 100644 --- a/tests/unit/test_base.py +++ b/tests/unit/test_base.py @@ -124,6 +124,18 @@ def test_timestamps_timeseries(self): 'grams', timestamps=ts1) self.assertEqual(ts2.timestamps, [0.0, 0.1, 0.2, 0.3, 0.4, 0.5]) + def test_good_continuity_timeseries(self): + ts1 = TimeSeries('test_ts1', [0, 1, 2, 3, 4, 5], + 'grams', timestamps=[0.0, 0.1, 0.2, 0.3, 0.4, 0.5], + continuity='continuous') + self.assertEqual(ts1.continuity, 'continuous') + + def test_bad_continuity_timeseries(self): + with self.assertRaises(ValueError): + TimeSeries('test_ts1', [0, 1, 2, 3, 4, 5], + 'grams', timestamps=[0.0, 0.1, 0.2, 0.3, 0.4, 0.5], + continuity='wrong') + def test_nodata(self): ts1 = TimeSeries('test_ts1', starting_time=0.0, rate=0.1) with self.assertWarns(UserWarning): From 1ed36d02e05362897dca046c967dd4558bc4dc02 Mon Sep 17 00:00:00 2001 From: Ryan Ly Date: Thu, 30 Jul 2020 13:25:26 -0700 Subject: [PATCH 06/14] Add support for ElectricalSeries.filtering dataset (#1270) --- src/pynwb/ecephys.py | 13 +++++++++++-- tests/integration/hdf5/test_ecephys.py | 15 ++++++++++----- tests/unit/test_ecephys.py | 14 +++++++++++++- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/pynwb/ecephys.py b/src/pynwb/ecephys.py index bb9ee7344..91160753d 100644 --- a/src/pynwb/ecephys.py +++ b/src/pynwb/ecephys.py @@ -49,7 +49,8 @@ class ElectricalSeries(TimeSeries): __nwbfields__ = ({'name': 'electrodes', 'required_name': 'electrodes', 'doc': 'the electrodes that generated this electrical series', 'child': True}, - 'channel_conversion') + 'channel_conversion', + 'filtering') @docval(*get_docval(TimeSeries.__init__, 'name'), # required {'name': 'data', 'type': ('array_data', 'data', TimeSeries), # required @@ -66,13 +67,21 @@ class ElectricalSeries(TimeSeries): "to support the storage of electrical recordings as native values generated by data acquisition systems. " "If this dataset is not present, then there is no channel-specific conversion factor, i.e. it is 1 for all" " channels.", 'default': None}, + {'name': 'filtering', 'type': str, 'doc': + "Filtering applied to all channels of the data. For example, if this ElectricalSeries represents " + "high-pass-filtered data (also known as AP Band), then this value could be 'High-pass 4-pole Bessel " + "filter at 500 Hz'. If this ElectricalSeries represents low-pass-filtered LFP data and the type of " + "filter is unknown, then this value could be 'Low-pass filter at 300 Hz'. If a non-standard filter " + "type is used, provide as much detail about the filter properties as possible.", 'default': None}, *get_docval(TimeSeries.__init__, 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate', 'comments', 'description', 'control', 'control_description')) def __init__(self, **kwargs): - name, electrodes, data, channel_conversion = popargs('name', 'electrodes', 'data', 'channel_conversion', kwargs) + name, electrodes, data, channel_conversion, filtering = popargs('name', 'electrodes', 'data', + 'channel_conversion', 'filtering', kwargs) super(ElectricalSeries, self).__init__(name, data, 'volts', **kwargs) self.electrodes = electrodes self.channel_conversion = channel_conversion + self.filtering = filtering @register_class('SpikeEventSeries', CORE_NAMESPACE) diff --git a/tests/integration/hdf5/test_ecephys.py b/tests/integration/hdf5/test_ecephys.py index 392f9375c..dfd1376c6 100644 --- a/tests/integration/hdf5/test_ecephys.py +++ b/tests/integration/hdf5/test_ecephys.py @@ -52,11 +52,16 @@ def setUpContainer(self): table=self.table) data = list(zip(range(10), range(10, 20))) timestamps = list(map(lambda x: x/10., range(10))) - es = ElectricalSeries(name='test_eS', - data=data, - electrodes=region, - channel_conversion=[4., .4], - timestamps=timestamps) + channel_conversion = [1., 2., 3., 4.] + filtering = 'Low-pass filter at 300 Hz' + es = ElectricalSeries( + name='test_eS', + data=data, + electrodes=region, + channel_conversion=channel_conversion, + filtering=filtering, + timestamps=timestamps + ) return es def addContainer(self, nwbfile): diff --git a/tests/unit/test_ecephys.py b/tests/unit/test_ecephys.py index d8b2682b3..e9048da6c 100644 --- a/tests/unit/test_ecephys.py +++ b/tests/unit/test_ecephys.py @@ -29,12 +29,24 @@ class ElectricalSeriesConstructor(TestCase): def test_init(self): data = list(range(10)) ts = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] + channel_conversion = [2., 6.3] + filtering = 'Low-pass filter at 300 Hz' table = make_electrode_table() region = DynamicTableRegion('electrodes', [0, 2], 'the first and third electrodes', table) - eS = ElectricalSeries('test_eS', data, region, channel_conversion=[2., 6.3], timestamps=ts) + eS = ElectricalSeries( + name='test_eS', + data=data, + electrodes=region, + channel_conversion=channel_conversion, + filtering=filtering, + timestamps=ts + ) self.assertEqual(eS.name, 'test_eS') self.assertEqual(eS.data, data) + self.assertEqual(eS.electrodes, region) self.assertEqual(eS.timestamps, ts) + self.assertEqual(eS.channel_conversion, [2., 6.3]) + self.assertEqual(eS.filtering, filtering) def test_link(self): table = make_electrode_table() From fb79c95222a0abf71f9f592f70ee0e20d9c9a8af Mon Sep 17 00:00:00 2001 From: Ryan Ly Date: Tue, 11 May 2021 18:29:38 -0700 Subject: [PATCH 07/14] Set fixed value for IZeroClampSeries stimulus description --- src/pynwb/icephys.py | 18 +++++++++++++++++- tests/unit/test_icephys.py | 11 +++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/pynwb/icephys.py b/src/pynwb/icephys.py index 156c97c04..d3b91e84a 100644 --- a/src/pynwb/icephys.py +++ b/src/pynwb/icephys.py @@ -71,7 +71,7 @@ class PatchClampSeries(TimeSeries): 'doc': 'IntracellularElectrode group that describes the electrode that was used to apply ' 'or record this data.'}, {'name': 'gain', 'type': 'float', 'doc': 'Units: Volt/Amp (v-clamp) or Volt/Volt (c-clamp)'}, # required - {'name': 'stimulus_description', 'type': str, 'doc': 'the stimulus name/protocol', 'default': "NA"}, + {'name': 'stimulus_description', 'type': str, 'doc': 'the stimulus name/protocol', 'default': "N/A"}, *get_docval(TimeSeries.__init__, 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate', 'comments', 'description', 'control', 'control_description'), {'name': 'sweep_number', 'type': (int, 'uint32', 'uint64'), @@ -146,9 +146,25 @@ class IZeroClampSeries(CurrentClampSeries): def __init__(self, **kwargs): name, data, electrode, gain = popargs('name', 'data', 'electrode', 'gain', kwargs) bias_current, bridge_balance, capacitance_compensation = (0.0, 0.0, 0.0) + stimulus_description = popargs('stimulus_description', kwargs) + stimulus_description = self._ensure_stimulus_description(name, stimulus_description, 'N/A', '2.3.0') + kwargs['stimulus_description'] = stimulus_description super().__init__(name, data, electrode, gain, bias_current, bridge_balance, capacitance_compensation, **kwargs) + def _ensure_stimulus_description(self, name, current_stim_desc, stim_desc, nwb_version): + """A helper to ensure correct stimulus_description used. + + Issues a warning with details if `current_stim_desc` is to be ignored, and + `stim_desc` to be used instead. + """ + if current_stim_desc != stim_desc: + warnings.warn( + "Stimulus description '%s' for %s '%s' is ignored and will be set to '%s' " + "as per NWB %s." + % (current_stim_desc, self.__class__.__name__, name, stim_desc, nwb_version)) + return stim_desc + @register_class('CurrentClampStimulusSeries', CORE_NAMESPACE) class CurrentClampStimulusSeries(PatchClampSeries): diff --git a/tests/unit/test_icephys.py b/tests/unit/test_icephys.py index 5610a613e..2c1930491 100644 --- a/tests/unit/test_icephys.py +++ b/tests/unit/test_icephys.py @@ -225,6 +225,7 @@ def test_init(self): self.assertEqual(iZCS.bias_current, 0.0) self.assertEqual(iZCS.bridge_balance, 0.0) self.assertEqual(iZCS.capacitance_compensation, 0.0) + self.assertEqual(iZCS.stimulus_description, 'N/A') def test_unit_warning(self): electrode_name = GetElectrode() @@ -234,6 +235,16 @@ def test_unit_warning(self): iZCS = IZeroClampSeries('test_iZCS', list(), electrode_name, 1.0, timestamps=list(), unit='unit') self.assertEqual(iZCS.unit, 'volts') + def test_stim_desc_warning(self): + electrode_name = GetElectrode() + + msg = ("Stimulus_description 'desc' for IZeroClampSeries 'test_iZCS' is ignored and will be set to 'N/A' " + "as per NWB 2.3.0.") + with self.assertWarnsWith(UserWarning, msg): + iZCS = IZeroClampSeries('test_iZCS', list(), electrode_name, 1.0, timestamps=list(), + stimulus_description='desc') + self.assertEqual(iZCS.stimulus_description, 'N/A') + class CurrentClampStimulusSeriesConstructor(TestCase): From 908028920974d7fb193e60ad0793fc10bab1e35c Mon Sep 17 00:00:00 2001 From: Ryan Ly Date: Wed, 12 May 2021 13:06:15 -0700 Subject: [PATCH 08/14] Fix test --- tests/unit/test_icephys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_icephys.py b/tests/unit/test_icephys.py index 2c1930491..428b9c6ef 100644 --- a/tests/unit/test_icephys.py +++ b/tests/unit/test_icephys.py @@ -238,7 +238,7 @@ def test_unit_warning(self): def test_stim_desc_warning(self): electrode_name = GetElectrode() - msg = ("Stimulus_description 'desc' for IZeroClampSeries 'test_iZCS' is ignored and will be set to 'N/A' " + msg = ("Stimulus description 'desc' for IZeroClampSeries 'test_iZCS' is ignored and will be set to 'N/A' " "as per NWB 2.3.0.") with self.assertWarnsWith(UserWarning, msg): iZCS = IZeroClampSeries('test_iZCS', list(), electrode_name, 1.0, timestamps=list(), From a6fa4d457d5758cfe3a43ba2afc259b7829adc25 Mon Sep 17 00:00:00 2001 From: Ryan Ly Date: Wed, 12 May 2021 16:55:07 -0700 Subject: [PATCH 09/14] Update changelog --- CHANGELOG.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 724aac770..171e0eb2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # PyNWB Changelog -## PyNWB 1.5.0 (April 23, 2021) +## PyNWB 1.5.0 (May 17, 2021) ### New features: - `NWBFile.add_scratch(...)` and `ScratchData.__init__(...)` now accept scalar data in addition to the currently @@ -8,7 +8,21 @@ - Support `pathlib.Path` paths when opening files with `NWBHDF5IO`. @dsleiter (#1314) - Use HDMF 2.5.1. See the [HDMF release notes](https://github.com/hdmf-dev/hdmf/releases/tag/2.5.1) for details. - Support `driver='ros3'` in `NWBHDF5IO` for streaming NWB files directly from s3. @bendichter (#1331) -........ TODO +- Update documentation, CI GitHub processes. @oruebel @yarikoptic, @bendichter, @TomDonoghue, @rly + (#1311, #1336, #1351, #1352, #1345, #1340, #1327) +- Set default `neurodata_type_inc` for `NWBGroupSpec`, `NWBDatasetSpec`. @rly (#1295) +- Add support for nwb-schema 2.3.0. @rly (#1245, #1330) + - Add optional `waveforms` column to the `Units` table. + - Add optional `strain` field to `Subject`. + - Add to `DecompositionSeries` an optional `DynamicTableRegion` called `source_channels`. + - Add to `ImageSeries` an optional link to `Device`. + - Add optional `continuity` field to `TimeSeries`. + - Add optional `filtering` attribute to `ElectricalSeries`. + - Clarify documentation for electrode impedance and filtering. + - Set the `stimulus_description` for `IZeroCurrentClamp` to have the fixed value "N/A". + - See https://nwb-schema.readthedocs.io/en/latest/format_release_notes.html for full schema release notes. +- Add support for HDMF 2.5.3. @rly @ajtritt (#1325, #1355, #1360, #1245, #1287) + - See https://github.com/hdmf-dev/hdmf/releases for full HDMF release notes. ## PyNWB 1.4.0 (August 12, 2020) From 2d90d9f31655e6f4727efe0bf3639041acbb878e Mon Sep 17 00:00:00 2001 From: Ryan Ly Date: Wed, 12 May 2021 17:06:17 -0700 Subject: [PATCH 10/14] Add note about allensdk py3.8 incompatibility --- requirements-doc.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-doc.txt b/requirements-doc.txt index e31310e0a..8a61a78c5 100644 --- a/requirements-doc.txt +++ b/requirements-doc.txt @@ -2,4 +2,4 @@ sphinx matplotlib sphinx_rtd_theme sphinx-gallery -allensdk \ No newline at end of file +allensdk # ntoe that as of allensdk 2.10.0, python 3.8 is not supported From 1e316ed99d02b48731370191182e38afcede6e56 Mon Sep 17 00:00:00 2001 From: Ryan Ly Date: Wed, 12 May 2021 18:40:22 -0700 Subject: [PATCH 11/14] Add workaround for validation against cached hdmf-experimental --- src/pynwb/validate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pynwb/validate.py b/src/pynwb/validate.py index 2e62eecfb..b4c323f1d 100644 --- a/src/pynwb/validate.py +++ b/src/pynwb/validate.py @@ -73,6 +73,9 @@ def main(): s = set(ns_deps.keys()) # determine which namespaces are the most for k in ns_deps: # specific (i.e. extensions) and validate s -= ns_deps[k].keys() # against those + # TODO remove this workaround for issue https://github.com/NeurodataWithoutBorders/pynwb/issues/1357 + if 'hdmf-experimental' in s: + s.remove('hdmf-experimental') # remove validation of hdmf-experimental for now namespaces = list(sorted(s)) if len(namespaces) > 0: tm = TypeMap(catalog) From e27d5c4cb2dbce8cfab9968efd6113f81c33088d Mon Sep 17 00:00:00 2001 From: Ryan Ly Date: Mon, 17 May 2021 17:11:43 -0700 Subject: [PATCH 12/14] Update CI to avoid click8 incompatibility --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3f0575d44..fd9e00fcc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -354,7 +354,7 @@ jobs: command: | python -m venv ../venv . ../venv/bin/activate - pip install githubrelease + pip install "click<8" githubrelease githubrelease release $CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME \ create $CIRCLE_TAG --name $CIRCLE_TAG \ --publish ./dist/* From def36e931111afcddb8183dc7deb7b79f44712fe Mon Sep 17 00:00:00 2001 From: Ryan Ly Date: Wed, 12 May 2021 15:59:10 -0700 Subject: [PATCH 13/14] Use HDMF 2.5.5 --- requirements-min.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-min.txt b/requirements-min.txt index 85531eaf5..fcd921f3e 100644 --- a/requirements-min.txt +++ b/requirements-min.txt @@ -1,7 +1,7 @@ # package dependencies and their minimum versions for installing PyNWB # the requirements here specify '==' for testing; setup.py replaces '==' with '>=' h5py==2.9,<3 # support for setting attrs to lists of utf-8 added in 2.9 -hdmf==2.5.2,<3 +hdmf==2.5.5,<3 numpy==1.16,<1.21 pandas==0.23,<2 python-dateutil==2.7,<3 diff --git a/requirements.txt b/requirements.txt index bd11df87c..f8c374bda 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ h5py==2.10.0 -hdmf==2.5.2 +hdmf==2.5.5 numpy==1.18.5 pandas==0.25.3 python-dateutil==2.8.1 From 50eb5b9dc53a090b37acee45817e2da4a5659b03 Mon Sep 17 00:00:00 2001 From: Ryan Ly Date: Mon, 17 May 2021 22:45:03 -0700 Subject: [PATCH 14/14] Address comments, improve doc --- requirements-doc.txt | 2 +- src/pynwb/file.py | 36 +++++++++++++++++++------- src/pynwb/icephys.py | 6 ++++- src/pynwb/misc.py | 5 +++- tests/integration/hdf5/test_nwbfile.py | 2 +- tests/unit/test_file.py | 11 ++++++-- 6 files changed, 46 insertions(+), 16 deletions(-) diff --git a/requirements-doc.txt b/requirements-doc.txt index 8a61a78c5..c311a0bb4 100644 --- a/requirements-doc.txt +++ b/requirements-doc.txt @@ -2,4 +2,4 @@ sphinx matplotlib sphinx_rtd_theme sphinx-gallery -allensdk # ntoe that as of allensdk 2.10.0, python 3.8 is not supported +allensdk # note that as of allensdk 2.10.0, python 3.8 is not supported diff --git a/src/pynwb/file.py b/src/pynwb/file.py index d37c09f1f..b31270137 100644 --- a/src/pynwb/file.py +++ b/src/pynwb/file.py @@ -48,16 +48,29 @@ class Subject(NWBContainer): 'strain' ) - @docval({'name': 'age', 'type': str, 'doc': 'the age of the subject', 'default': None}, - {'name': 'description', 'type': str, 'doc': 'a description of the subject', 'default': None}, - {'name': 'genotype', 'type': str, 'doc': 'the genotype of the subject', 'default': None}, - {'name': 'sex', 'type': str, 'doc': 'the sex of the subject', 'default': None}, - {'name': 'species', 'type': str, 'doc': 'the species of the subject', 'default': None}, - {'name': 'subject_id', 'type': str, 'doc': 'a unique identifier for the subject', 'default': None}, - {'name': 'weight', 'type': str, 'doc': 'the weight of the subject', 'default': None}, + @docval({'name': 'age', 'type': str, + 'doc': ('The age of the subject. The ISO 8601 Duration format is recommended, e.g., "P90D" for ' + '90 days old.'), 'default': None}, + {'name': 'description', 'type': str, + 'doc': 'A description of the subject, e.g., "mouse A10".', 'default': None}, + {'name': 'genotype', 'type': str, + 'doc': 'The genotype of the subject, e.g., "Sst-IRES-Cre/wt;Ai32(RCL-ChR2(H134R)_EYFP)/wt".', + 'default': None}, + {'name': 'sex', 'type': str, + 'doc': ('The sex of the subject. Using "F" (female), "M" (male), "U" (unknown), or "O" (other) ' + 'is recommended.'), 'default': None}, + {'name': 'species', 'type': str, + 'doc': ('The species of the subject. The formal latin binomal name is recommended, e.g., "Mus musculus"'), + 'default': None}, + {'name': 'subject_id', 'type': str, 'doc': 'A unique identifier for the subject, e.g., "A10"', + 'default': None}, + {'name': 'weight', 'type': (float, str), + 'doc': ('The weight of the subject, including units. Using kilograms is recommended. e.g., "0.02 kg". ' + 'If a float is provided, then the weight will be stored as "[value] kg".'), + 'default': None}, {'name': 'date_of_birth', 'type': datetime, 'default': None, - 'doc': 'datetime of date of birth. May be supplied instead of age.'}, - {'name': 'strain', 'type': str, 'doc': 'the strain of the subject', 'default': None}) + 'doc': 'The datetime of the date of birth. May be supplied instead of age.'}, + {'name': 'strain', 'type': str, 'doc': 'The strain of the subject, e.g., "C57BL/6J"', 'default': None}) def __init__(self, **kwargs): kwargs['name'] = 'subject' call_docval_func(super(Subject, self).__init__, kwargs) @@ -67,7 +80,10 @@ def __init__(self, **kwargs): self.sex = getargs('sex', kwargs) self.species = getargs('species', kwargs) self.subject_id = getargs('subject_id', kwargs) - self.weight = getargs('weight', kwargs) + weight = getargs('weight', kwargs) + if isinstance(weight, float): + weight = str(weight) + ' kg' + self.weight = weight self.strain = getargs('strain', kwargs) date_of_birth = getargs('date_of_birth', kwargs) if date_of_birth and date_of_birth.tzinfo is None: diff --git a/src/pynwb/icephys.py b/src/pynwb/icephys.py index d3b91e84a..bcceaa5ac 100644 --- a/src/pynwb/icephys.py +++ b/src/pynwb/icephys.py @@ -138,7 +138,11 @@ class IZeroClampSeries(CurrentClampSeries): @docval(*get_docval(CurrentClampSeries.__init__, 'name', 'data', 'electrode'), # required {'name': 'gain', 'type': 'float', 'doc': 'Units: Volt/Volt'}, # required - *get_docval(CurrentClampSeries.__init__, 'stimulus_description', 'resolution', 'conversion', 'timestamps', + {'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.'), + 'default': 'N/A'}, + *get_docval(CurrentClampSeries.__init__, 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate', 'comments', 'description', 'control', 'control_description', 'sweep_number'), {'name': 'unit', 'type': str, 'doc': "The base unit of measurement (must be 'volts')", diff --git a/src/pynwb/misc.py b/src/pynwb/misc.py index 6124b18e3..31303edf1 100644 --- a/src/pynwb/misc.py +++ b/src/pynwb/misc.py @@ -268,7 +268,10 @@ class DecompositionSeries(TimeSeries): 'doc': 'a table for describing the frequency bands that the signal was decomposed into', 'default': None}, {'name': 'source_timeseries', 'type': TimeSeries, 'doc': 'the input TimeSeries from this analysis', 'default': None}, - {'name': 'source_channels', 'type': DynamicTableRegion, 'doc': 'the channels that provided the source data', + {'name': 'source_channels', 'type': DynamicTableRegion, + 'doc': ('The channels that provided the source data. In the case of electrical recordings this is ' + 'typically a DynamicTableRegion pointing to the electrodes table at NWBFile.electrodes, ' + 'similar to ElectricalSeries.electrodes.'), 'default': None}, *get_docval(TimeSeries.__init__, 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate', 'comments', 'control', 'control_description')) diff --git a/tests/integration/hdf5/test_nwbfile.py b/tests/integration/hdf5/test_nwbfile.py index 7e7657589..70909c029 100644 --- a/tests/integration/hdf5/test_nwbfile.py +++ b/tests/integration/hdf5/test_nwbfile.py @@ -209,7 +209,7 @@ def setUpContainer(self): sex='M', species='Rattus norvegicus', subject_id='RAT123', - weight='2 lbs', + weight='2 kg', date_of_birth=datetime(1970, 1, 1, 12, tzinfo=tzutc()), strain='my_strain') diff --git a/tests/unit/test_file.py b/tests/unit/test_file.py index e29cb67ec..d3afa8155 100644 --- a/tests/unit/test_file.py +++ b/tests/unit/test_file.py @@ -423,7 +423,7 @@ def setUp(self): sex='M', species='Rattus norvegicus', subject_id='RAT123', - weight='2 lbs', + weight='2 kg', date_of_birth=datetime(2017, 5, 1, 12, tzinfo=tzlocal()), strain='my_strain') self.start = datetime(2017, 5, 1, 12, tzinfo=tzlocal()) @@ -445,13 +445,20 @@ def test_constructor(self): self.assertEqual(self.subject.sex, 'M') self.assertEqual(self.subject.species, 'Rattus norvegicus') self.assertEqual(self.subject.subject_id, 'RAT123') - self.assertEqual(self.subject.weight, '2 lbs') + self.assertEqual(self.subject.weight, '2 kg') self.assertEqual(self.subject.date_of_birth, datetime(2017, 5, 1, 12, tzinfo=tzlocal())) self.assertEqual(self.subject.strain, 'my_strain') def test_nwbfile_constructor(self): self.assertIs(self.nwbfile.subject, self.subject) + def test_weight_float(self): + subject = Subject( + subject_id='RAT123', + weight=2.3, + ) + self.assertEqual(subject.weight, '2.3 kg') + class TestCacheSpec(TestCase):