Skip to content

Commit 250af1c

Browse files
Merge pull request #325 from catalystneuro/add_check_for_timestamps_are_not_negative
Add inspector check for negative timestamps
2 parents bead03d + f84bb14 commit 250af1c

File tree

5 files changed

+129
-0
lines changed

5 files changed

+129
-0
lines changed

docs/best_practices/nwbfile_metadata.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,18 @@ the ``timestamp_reference_time`` used across all of the NWBFiles may be set sepa
2222
All time-related data in the NWBFile should be synchronized to the ``timestamps_reference_time`` so that future users
2323
are able to understand the timing of all events contained within the NWBFile.
2424

25+
The ``timestamps_reference_time`` should also be the earliest timestamp in the file, giving all other time references
26+
a positive value relative to that. There should be no time references which are negative.
27+
2528
Given the importance of this field within an :ref:`nwb-schema:sec-NWBFile`, is it critical that it be set to a proper
2629
value. Default values should generally not be used for this field. If the true date is unknown, use your
2730
best guess. If the exact start time is unknown, then it is fine to simply set it to midnight on that date.
2831

2932

3033
Check functions: :py:meth:`~nwbinspector.checks.nwbfile_metadata.check_session_start_time_old_date`,
3134
:py:meth:`~nwbinspector.checks.nwbfile_metadata.check_session_start_time_future_date`,
35+
:py:meth:`~nwbinspector.checks.time_series.check_timestamp_of_the_first_sample_is_not_negative`
36+
:py:meth:`~nwbinspector.checks.tables.check_table_time_columns_are_not_negative`
3237

3338

3439

src/nwbinspector/checks/tables.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,24 @@ def check_ids_unique(table: DynamicTable, nelems: Optional[int] = NELEMS):
235235
data = table.id[:nelems]
236236
if len(set(data)) != len(data):
237237
return InspectorMessage(message="This table has ids that are not unique.")
238+
239+
240+
@register_check(importance=Importance.BEST_PRACTICE_SUGGESTION, neurodata_type=DynamicTable)
241+
def check_table_time_columns_are_not_negative(table: DynamicTable):
242+
"""
243+
Check that time columns are not negative.
244+
245+
Best Practice: :ref:`best_practice_global_time_reference`
246+
247+
Parameters
248+
----------
249+
table: DynamicTable
250+
"""
251+
for column_name in table.colnames:
252+
if column_name.endswith("_time"):
253+
first_timestamp = table[column_name][0]
254+
if first_timestamp < 0:
255+
yield InspectorMessage(
256+
message=f"Timestamps in column {column_name} should not be negative."
257+
" It is recommended to align the `session_start_time` or `timestamps_reference_time` to be the earliest time value that occurs in the data, and shift all other signals accordingly."
258+
)

src/nwbinspector/checks/time_series.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,22 @@ def check_timestamps_ascending(time_series: TimeSeries, nelems=200):
8888
return InspectorMessage(f"{time_series.name} timestamps are not ascending.")
8989

9090

91+
@register_check(importance=Importance.BEST_PRACTICE_SUGGESTION, neurodata_type=TimeSeries)
92+
def check_timestamp_of_the_first_sample_is_not_negative(time_series: TimeSeries):
93+
"""
94+
Check that the timestamp of the first sample is not negative.
95+
96+
Best Practice: :ref:`best_practice_global_time_reference`
97+
"""
98+
99+
first_timestamp = time_series.starting_time if time_series.starting_time is not None else time_series.timestamps[0]
100+
if first_timestamp < 0:
101+
return InspectorMessage(
102+
message="Timestamps should not be negative."
103+
" It is recommended to align the `session_start_time` or `timestamps_reference_time` to be the earliest time value that occurs in the data, and shift all other signals accordingly."
104+
)
105+
106+
91107
@register_check(importance=Importance.BEST_PRACTICE_VIOLATION, neurodata_type=TimeSeries)
92108
def check_missing_unit(time_series: TimeSeries):
93109
"""

tests/unit_tests/test_tables.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
check_table_values_for_dict,
2020
check_col_not_nan,
2121
check_ids_unique,
22+
check_table_time_columns_are_not_negative,
2223
)
2324
from nwbinspector.utils import get_package_version
2425

@@ -416,3 +417,40 @@ def test_fail_check_ids_unique():
416417
def test_pass_check_ids_unique():
417418
dt = DynamicTable(name="test_table", description="test", id=[0, 1])
418419
assert check_ids_unique(dt) is None
420+
421+
422+
def test_table_time_columns_are_not_negative_fail():
423+
test_table = DynamicTable(name="test_table", description="test")
424+
test_table.add_column(name="test_time", description="")
425+
test_table.add_column(name="start_time", description="")
426+
test_table.add_column(name="stop_time", description="")
427+
test_table.add_row(test_time=-2.0, start_time=-1.0, stop_time=3.0)
428+
429+
assert check_table_time_columns_are_not_negative(test_table) == [
430+
InspectorMessage(
431+
message=f"Timestamps in column test_time should not be negative."
432+
" It is recommended to align the `session_start_time` or `timestamps_reference_time` to be the earliest time value that occurs in the data, and shift all other signals accordingly.",
433+
importance=Importance.BEST_PRACTICE_SUGGESTION,
434+
check_function_name="check_table_time_columns_are_not_negative",
435+
object_type="DynamicTable",
436+
object_name="test_table",
437+
location="/",
438+
),
439+
InspectorMessage(
440+
message=f"Timestamps in column start_time should not be negative."
441+
" It is recommended to align the `session_start_time` or `timestamps_reference_time` to be the earliest time value that occurs in the data, and shift all other signals accordingly.",
442+
importance=Importance.BEST_PRACTICE_SUGGESTION,
443+
check_function_name="check_table_time_columns_are_not_negative",
444+
object_type="DynamicTable",
445+
object_name="test_table",
446+
location="/",
447+
),
448+
]
449+
450+
451+
def test_table_time_columns_are_not_negative_pass():
452+
test_table = DynamicTable(name="test_table", description="test")
453+
test_table.add_column(name="test_time", description="")
454+
test_table.add_row(test_time=1.0)
455+
456+
assert check_table_time_columns_are_not_negative(test_table) is None

tests/unit_tests/test_time_series.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
check_timestamps_ascending,
1515
check_missing_unit,
1616
check_resolution,
17+
check_timestamp_of_the_first_sample_is_not_negative,
1718
)
1819
from nwbinspector.tools import make_minimal_nwbfile
1920
from nwbinspector.testing import check_streaming_tests_enabled
@@ -215,6 +216,54 @@ def test_check_timestamps_ascending_fail():
215216
)
216217

217218

219+
def test_check_timestamp_of_the_first_sample_is_not_negative_with_timestamps_fail():
220+
time_series = pynwb.TimeSeries(name="test_time_series", unit="test_units", data=[1, 2, 3], timestamps=[-1, 0, 1])
221+
message = (
222+
"Timestamps should not be negative."
223+
" It is recommended to align the `session_start_time` or `timestamps_reference_time` "
224+
"to be the earliest time value that occurs in the data, and shift all other signals accordingly."
225+
)
226+
assert check_timestamp_of_the_first_sample_is_not_negative(time_series) == InspectorMessage(
227+
message=message,
228+
importance=Importance.BEST_PRACTICE_SUGGESTION,
229+
check_function_name="check_timestamp_of_the_first_sample_is_not_negative",
230+
object_type="TimeSeries",
231+
object_name="test_time_series",
232+
location="/",
233+
)
234+
235+
236+
def test_check_timestamp_of_the_first_sample_is_not_negative_with_timestamps_pass():
237+
time_series = pynwb.TimeSeries(name="test_time_series", unit="test_units", data=[1, 2, 3], timestamps=[0, 1, 2])
238+
assert check_timestamp_of_the_first_sample_is_not_negative(time_series) is None
239+
240+
241+
def test_check_timestamp_of_the_first_sample_is_not_negative_with_starting_time_fail():
242+
time_series = pynwb.TimeSeries(
243+
name="test_time_series", unit="test_units", data=[1, 2, 3], starting_time=-1.0, rate=30.0
244+
)
245+
message = (
246+
"Timestamps should not be negative."
247+
" It is recommended to align the `session_start_time` or `timestamps_reference_time` "
248+
"to be the earliest time value that occurs in the data, and shift all other signals accordingly."
249+
)
250+
assert check_timestamp_of_the_first_sample_is_not_negative(time_series) == InspectorMessage(
251+
message=message,
252+
importance=Importance.BEST_PRACTICE_SUGGESTION,
253+
check_function_name="check_timestamp_of_the_first_sample_is_not_negative",
254+
object_type="TimeSeries",
255+
object_name="test_time_series",
256+
location="/",
257+
)
258+
259+
260+
def test_check_timestamp_of_the_first_sample_is_not_negative_with_starting_time_pass():
261+
time_series = pynwb.TimeSeries(
262+
name="test_time_series", unit="test_units", data=[1, 2, 3], starting_time=0.0, rate=30.0
263+
)
264+
assert check_timestamp_of_the_first_sample_is_not_negative(time_series) is None
265+
266+
218267
def test_check_missing_unit_pass():
219268
time_series = pynwb.TimeSeries(name="test_time_series", unit="test_units", data=[1, 2, 3], timestamps=[1, 2, 3])
220269
assert check_missing_unit(time_series) is None

0 commit comments

Comments
 (0)