diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bcd996f3..3b7b926a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -# Upcoming +# Upcoming (0.5.0) + + +### New Checks + +* Add: `check_session_start_time_contains_time_zone` [#458](https://github.com/NeurodataWithoutBorders/nwbinspector/pull/458). ### Fixes diff --git a/docs/best_practices/nwbfile_metadata.rst b/docs/best_practices/nwbfile_metadata.rst index 8cdbf5679..ddc388621 100644 --- a/docs/best_practices/nwbfile_metadata.rst +++ b/docs/best_practices/nwbfile_metadata.rst @@ -19,21 +19,26 @@ objects in the :ref:`nwb-schema:sec-NWBFile` is the ``timestamps_reference_time` ``session_start_time``, but when writing multiple NWBFiles that are all designed to align to the same time reference, the ``timestamp_reference_time`` used across all of the NWBFiles may be set separately from the ``session_start_time``. -All time-related data in the NWBFile should be synchronized to the ``timestamps_reference_time`` so that future users -are able to understand the timing of all events contained within the NWBFile. +All time-related data in the ``NWBFile`` should be synchronized to the ``timestamps_reference_time`` so that future users +are able to understand the timing of all events contained within the ``NWBFile``. The ``timestamps_reference_time`` should also be the earliest timestamp in the file, giving all other time references a positive value relative to that. There should be no time references which are negative. Given the importance of this field within an :ref:`nwb-schema:sec-NWBFile`, is it critical that it be set to a proper value. Default values should generally not be used for this field. If the true date is unknown, use your -best guess. If the exact start time is unknown, then it is fine to simply set it to midnight on that date. +best guess. If working with human participants and such data is protected information, use the date 1900-01-01. + +**New in PyNWB 2.7.0, released May 2, 2024:** + +The ``session_start_time`` is no longer required to have a timezone offset. This information is now optional, but recommended. If the timezone offset is not provided, it should **not** be assumed to be UTC. Check functions: :py:meth:`~nwbinspector.checks.nwbfile_metadata.check_session_start_time_old_date`, :py:meth:`~nwbinspector.checks.nwbfile_metadata.check_session_start_time_future_date`, :py:meth:`~nwbinspector.checks.time_series.check_timestamp_of_the_first_sample_is_not_negative` -:py:meth:`~nwbinspector.checks.tables.check_table_time_columns_are_not_negative` +:py:meth:`~nwbinspector.checks.tables.check_table_time_columns_are_not_negative`, +:py:meth:`~nwbinspector.checks.nwbfile_metadata.check_session_start_time_contains_time_zone`, diff --git a/src/nwbinspector/checks/nwbfile_metadata.py b/src/nwbinspector/checks/nwbfile_metadata.py index a5fbae66e..bdebbdfe8 100644 --- a/src/nwbinspector/checks/nwbfile_metadata.py +++ b/src/nwbinspector/checks/nwbfile_metadata.py @@ -1,7 +1,7 @@ """Check functions that examine general NWBFile metadata.""" import re -from datetime import datetime +from datetime import datetime, date from isodate import parse_duration, Duration from pynwb import NWBFile, ProcessingModule @@ -36,6 +36,22 @@ def check_session_start_time_old_date(nwbfile: NWBFile): ) +@register_check(importance=Importance.BEST_PRACTICE_SUGGESTION, neurodata_type=NWBFile) +def check_session_start_time_contains_time_zone(nwbfile: NWBFile): + """ + Check if the session_start_time contains a time zone. + + Best Practice: :ref:`best_practice_global_time_reference` + """ + session_start_time = nwbfile.session_start_time + if isinstance(session_start_time, date): + return + if session_start_time.tzinfo is None: + return InspectorMessage( + message=(f"The session_start_time ({session_start_time}) does not contain a time zone.") + ) + + @register_check(importance=Importance.CRITICAL, neurodata_type=NWBFile) def check_session_start_time_future_date(nwbfile: NWBFile): """ @@ -44,8 +60,19 @@ def check_session_start_time_future_date(nwbfile: NWBFile): Best Practice: :ref:`best_practice_global_time_reference` """ session_start_time = nwbfile.session_start_time + + if isinstance(session_start_time, date): + current_date = date.today() + if session_start_time > current_date: + return InspectorMessage( + message=( + f"The session_start_time ({session_start_time}) is set to a future date. " + "Please ensure that the session_start_time is set to the correct date and time." + ) + ) + current_time = datetime.now() - if session_start_time.tzinfo is not None: + if isinstance(session_start_time, datetime) and session_start_time.tzinfo is not None: current_time = current_time.astimezone() if session_start_time >= current_time: return InspectorMessage( diff --git a/tests/unit_tests/test_nwbfile_metadata.py b/tests/unit_tests/test_nwbfile_metadata.py index d86140965..9a7d72c2e 100644 --- a/tests/unit_tests/test_nwbfile_metadata.py +++ b/tests/unit_tests/test_nwbfile_metadata.py @@ -1,6 +1,8 @@ from uuid import uuid4 -from datetime import datetime, timezone +from datetime import datetime, timezone, date +import pynwb +import pytest from pynwb import NWBFile, ProcessingModule from pynwb.file import Subject @@ -23,6 +25,7 @@ check_processing_module_name, check_session_start_time_old_date, check_session_start_time_future_date, + check_session_start_time_contains_time_zone, PROCESSING_MODULE_CONFIG, ) from nwbinspector.tools import make_minimal_nwbfile @@ -56,6 +59,40 @@ def test_check_session_start_time_future_date_pass(): assert check_session_start_time_future_date(nwbfile) is None +@pytest.mark.skipif(pynwb.__version__ < "2.7.0", reason="Feature not supported in pynwb < 2.7.0") +def test_check_session_start_time_contains_time_zone_pass(): + nwbfile = NWBFile( + session_description="", + identifier=str(uuid4()), + session_start_time=datetime(2010, 1, 1, 0, 0, 0, 0, timezone.utc), + ) + assert check_session_start_time_contains_time_zone(nwbfile) is None + + +@pytest.mark.skipif(pynwb.__version__ < "2.7.0", reason="Feature not supported in pynwb < 2.7.0") +def test_check_session_start_time_is_date_contains_time_zone_pass(): + nwbfile = NWBFile( + session_description="", + identifier=str(uuid4()), + session_start_time=date(2010, 1, 1), + ) + assert check_session_start_time_contains_time_zone(nwbfile) is None + + +@pytest.mark.skipif(pynwb.__version__ < "2.7.0", reason="Feature not supported in pynwb < 2.7.0") +def test_check_session_start_time_contains_time_zone_fail(): + session_start_time = datetime(2010, 1, 1, 0, 0, 0, 0) + nwbfile = NWBFile(session_description="", identifier=str(uuid4()), session_start_time=session_start_time) + assert check_session_start_time_contains_time_zone(nwbfile) == InspectorMessage( + message=f"The session_start_time ({session_start_time}) does not contain a time zone.", + importance=Importance.BEST_PRACTICE_SUGGESTION, + check_function_name="check_session_start_time_contains_time_zone", + object_type="NWBFile", + object_name="root", + location="/", + ) + + def test_check_session_start_time_future_date_fail(): nwbfile = NWBFile( session_description="",