diff --git a/CHANGELOG.md b/CHANGELOG.md index 282156763..9c59cf953 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # PyNWB Changelog +## PyNWB 3.1.0 (Upcoming) +### Enhancements and minor changes +- Added HERD to to `general` within the the `NWBFile`. @mavaylon1 [#2111](https://github.com/NeurodataWithoutBorders/pynwb/pull/2111) + ## PyNWB 3.1.0 (July 8, 2025) ### Breaking changes diff --git a/src/pynwb/file.py b/src/pynwb/file.py index c2fd317ca..d98c4604d 100644 --- a/src/pynwb/file.py +++ b/src/pynwb/file.py @@ -339,6 +339,7 @@ class NWBFile(MultiContainerInterface, HERDManager): {'name': 'keywords', 'type': 'array_data', 'doc': 'Terms to search over', 'default': None}, {'name': 'notes', 'type': str, 'doc': 'Notes about the experiment.', 'default': None}, + {'name': 'external_resources', 'child': True, 'required_name': 'external_resources'}, {'name': 'pharmacology', 'type': str, 'doc': 'Description of drugs used, including how and when they were administered. ' 'Anesthesia(s), painkiller(s), etc., plus dosage, concentration, etc.', 'default': None}, @@ -483,9 +484,13 @@ def __init__(self, **kwargs): 'icephys_experimental_conditions' ] args_to_set = popargs_to_dict(keys_to_set, kwargs) + args_to_set['internal_herd'] = popargs('external_resources', kwargs) kwargs['name'] = 'root' super().__init__(**kwargs) + self.reset_herd = False + self.external_herd = None + # add timezone to session_start_time if missing session_start_time = args_to_set['session_start_time'] if session_start_time.tzinfo is None: @@ -570,6 +575,29 @@ def all_children(self): stack.append(c) return ret + def link_resources(self, herd): + """ + This method is to set an external HERD file as the external resources for this file. + This will not persist on export. # TODO: This could change in the future with further development. + """ + self.external_herd = herd + self.reset_herd = True + + @property + def external_resources(self): + if self.reset_herd: + return self.external_herd + else: + return self.internal_herd + + @external_resources.setter + def external_resources(self, herd): + """ + This is here to set HERD for the file if the user did not do so using __init__. + """ + self.internal_herd = herd + self.internal_herd.parent = self + @property def objects(self): if self.__obj is None: @@ -1152,4 +1180,4 @@ def ElectrodeTable(name='electrodes', description='metadata about extracellular electrodes'): warn("The ElectrodeTable convenience function is deprecated. Please create a new instance of " "the ElectrodesTable class instead.", DeprecationWarning) - return ElectrodesTable() \ No newline at end of file + return ElectrodesTable() diff --git a/src/pynwb/io/file.py b/src/pynwb/io/file.py index d74c66be1..a3233a01d 100644 --- a/src/pynwb/io/file.py +++ b/src/pynwb/io/file.py @@ -110,6 +110,8 @@ def __init__(self, spec): self.map_spec('subject', general_spec.get_group('subject')) + self.map_spec('external_resources', general_spec.get_group('external_resources')) + device_spec = general_spec.get_group('devices') self.unmap(device_spec) self.map_spec('devices', device_spec.get_neurodata_type('Device')) diff --git a/src/pynwb/nwb-schema b/src/pynwb/nwb-schema index ade50ef33..39aaaec2b 160000 --- a/src/pynwb/nwb-schema +++ b/src/pynwb/nwb-schema @@ -1 +1 @@ -Subproject commit ade50ef33446beb3c7df4c6f1072ae0e821b5115 +Subproject commit 39aaaec2b199f7509c60da4a6287c5df68c96259 diff --git a/tests/unit/test_resources.py b/tests/unit/test_resources.py index 108a7fd84..85d9279f0 100644 --- a/tests/unit/test_resources.py +++ b/tests/unit/test_resources.py @@ -1,10 +1,27 @@ import warnings +from datetime import datetime +from uuid import uuid4 +import os +import numpy as np + +from dateutil import tz from pynwb.resources import HERD +from pynwb.file import Subject +from pynwb import NWBHDF5IO, NWBFile from pynwb.testing import TestCase class TestNWBContainer(TestCase): + def setUp(self): + self.path = "resources_file.nwb" + self.export_path = "export_file.nwb" + + def tearDown(self): + for path in [self.path, self.export_path]: + if os.path.isfile(path): + os.remove(path) + def test_constructor(self): """ Test constructor @@ -17,3 +34,178 @@ def test_constructor(self): ) er = HERD() self.assertIsInstance(er, HERD) + + def test_nwbfile_init_herd(self): + session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific")) + herd = HERD() + nwbfile = NWBFile( + session_description="A Person undergoing brain pokes.", + identifier=str(uuid4()), + session_start_time=session_start_time, + external_resources=herd + ) + self.assertTrue(isinstance(nwbfile.external_resources, HERD)) + + def test_nwbfile_set_herd(self): + session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific")) + herd = HERD() + nwbfile = NWBFile( + session_description="A Person undergoing brain pokes.", + identifier=str(uuid4()), + session_start_time=session_start_time, + ) + nwbfile.external_resources = herd + self.assertTrue(isinstance(nwbfile.external_resources, HERD)) + self.assertEqual(nwbfile.external_resources.parent, nwbfile) + + def test_resources_roundtrip(self): + session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific")) + + nwbfile = NWBFile( + session_description="A Person undergoing brain pokes.", + identifier=str(uuid4()), + session_start_time=session_start_time, + ) + subject = Subject( + subject_id="001", + age="26", + description="human 5", + species='Homo sapiens', + sex="M", + ) + + nwbfile.subject = subject + herd = HERD() + nwbfile.external_resources = herd + + nwbfile.external_resources.add_ref(container=nwbfile.subject, + key=nwbfile.subject.species, + entity_id="NCBI_TAXON:9606", + entity_uri='https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=Info&id=9606') + + with NWBHDF5IO(self.path, "w") as io: + io.write(nwbfile) + + with NWBHDF5IO(self.path, "r") as io: + read_nwbfile = io.read() + self.assertEqual( + read_nwbfile.external_resources.keys[:], + np.array( + [[(b'Homo sapiens',)]], + dtype=[('key', 'O')] + ) + ) + + self.assertEqual( + read_nwbfile.external_resources.entities[:], + np.array( + [ + ('NCBI_TAXON:9606', + 'https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=Info&id=9606') + ], + dtype=[('entity_id', 'O'), ('entity_uri', 'O')] + ) + ) + + self.assertEqual( + read_nwbfile.external_resources.objects[:], + np.array( + [ + (0, + subject.object_id, + 'Subject', + '', + '') + ], + dtype=[ + ('files_idx', '