Skip to content

Commit 377dcf5

Browse files
bendichterclaude
andcommitted
Add check_subject_age_reference validation (supersedes #250)
Port the age.reference support from the 2022 WIP PR #250 onto the current restructured codebase and complete its open TODOs (best-practice docs + tests). Adds check_subject_age_reference, which flags Subject.age__reference values outside {"birth", "gestational"}. PyNWB now enforces this constraint at construction time, so the check is defensive against files written by other tools (e.g. MatNWB) and read back. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 95d62d6 commit 377dcf5

5 files changed

Lines changed: 69 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# v0.7.3 (Upcoming)
22

33
### New Checks
4+
* Added `check_subject_age_reference` to validate that `Subject.age__reference`, when present, is one of the supported values (`"birth"` or `"gestational"`). This catches invalid references in files written by tools that do not enforce the schema constraint. [#250](https://github.com/NeurodataWithoutBorders/nwbinspector/pull/250)
45

56
### Improvements
67
* Raised the minimum required PyNWB to `>=4.0` to track the latest PyNWB release. [#708](https://github.com/NeurodataWithoutBorders/nwbinspector/pull/708)

docs/best_practices/nwbfile_metadata.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,13 @@ If the precise age is unknown, an age range can be given by "[lower bound]/[uppe
277277
that the age is in between 10 and 20 days. If only the lower bound is known, then including only the slash after that lower bound can be used to indicate a
278278
missing bound. For instance, "P90Y/" would indicate that the age is 90 years or older.
279279

280-
Check function: :py:meth:`~nwbinspector.checks._nwbfile_metadata.check_subject_age`
280+
The ``age`` is measured relative to a reference point, controlled by the ``age__reference`` field of the
281+
:ref:`nwb-schema:sec-Subject`. The only supported values are ``"birth"`` (the default) and ``"gestational"``. Use
282+
``"gestational"`` when the age is measured from conception rather than from birth, as is common for embryonic or
283+
prenatal preparations.
284+
285+
Check functions: :py:meth:`~nwbinspector.checks._nwbfile_metadata.check_subject_age` and
286+
:py:meth:`~nwbinspector.checks._nwbfile_metadata.check_subject_age_reference`
281287

282288

283289

src/nwbinspector/checks/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
check_session_start_time_future_date,
5959
check_session_start_time_old_date,
6060
check_subject_age,
61+
check_subject_age_reference,
6162
check_subject_exists,
6263
check_subject_id_exists,
6364
check_subject_id_no_slashes,
@@ -131,6 +132,7 @@
131132
"check_keywords",
132133
"check_institution",
133134
"check_subject_age",
135+
"check_subject_age_reference",
134136
"check_subject_sex",
135137
"check_subject_exists",
136138
"check_doi_publications",

src/nwbinspector/checks/_nwbfile_metadata.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,25 @@ def check_subject_proper_age_range(subject: Subject) -> Optional[InspectorMessag
234234
return None
235235

236236

237+
@register_check(importance=Importance.BEST_PRACTICE_SUGGESTION, neurodata_type=Subject)
238+
def check_subject_age_reference(subject: Subject) -> Optional[InspectorMessage]:
239+
"""
240+
Check if the Subject age reference, when specified, is one of the supported options.
241+
242+
Best Practice: :ref:`best_practice_subject_age`
243+
"""
244+
valid_options = ["birth", "gestational"]
245+
if subject.age__reference is not None and subject.age__reference not in valid_options:
246+
return InspectorMessage(
247+
message=(
248+
f"Subject age reference, '{subject.age__reference}', is not one of the valid options "
249+
f"({valid_options})."
250+
)
251+
)
252+
253+
return None
254+
255+
237256
@register_check(importance=Importance.CRITICAL, neurodata_type=Subject)
238257
def check_subject_id_exists(subject: Subject) -> Optional[InspectorMessage]:
239258
"""

tests/unit_tests/test_nwbfile_metadata.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
check_session_start_time_future_date,
2828
check_session_start_time_old_date,
2929
check_subject_age,
30+
check_subject_age_reference,
3031
check_subject_exists,
3132
check_subject_id_exists,
3233
check_subject_id_no_slashes,
@@ -474,6 +475,45 @@ def test_check_subject_age_with_years_fail():
474475
)
475476

476477

478+
def test_check_subject_age_reference_default_pass():
479+
subject = Subject(subject_id="001", age="P1D")
480+
assert check_subject_age_reference(subject) is None
481+
482+
483+
def test_check_subject_age_reference_birth_pass():
484+
subject = Subject(subject_id="001", age="P1D", age__reference="birth")
485+
assert check_subject_age_reference(subject) is None
486+
487+
488+
def test_check_subject_age_reference_gestational_pass():
489+
subject = Subject(subject_id="001", age="P1D", age__reference="gestational")
490+
assert check_subject_age_reference(subject) is None
491+
492+
493+
def test_check_subject_age_reference_none_pass():
494+
# Files written before age__reference existed (or by other tools) may have no reference set.
495+
subject = Subject(subject_id="001", age="P1D")
496+
subject.fields["age__reference"] = None
497+
assert check_subject_age_reference(subject) is None
498+
499+
500+
def test_check_subject_age_reference_fail():
501+
# PyNWB rejects invalid references at construction time, so emulate a file written by another
502+
# tool with an unsupported value by overriding the field after construction.
503+
subject = Subject(subject_id="001", age="P1D")
504+
subject.fields["age__reference"] = "conception"
505+
assert check_subject_age_reference(subject) == InspectorMessage(
506+
message=(
507+
"Subject age reference, 'conception', is not one of the valid options (['birth', 'gestational'])."
508+
),
509+
importance=Importance.BEST_PRACTICE_SUGGESTION,
510+
check_function_name="check_subject_age_reference",
511+
object_type="Subject",
512+
object_name="subject",
513+
location="/general/subject",
514+
)
515+
516+
477517
def test_pass_check_subject_species_exists():
478518
subject = Subject(subject_id="001", species="Homo sapiens")
479519
assert check_subject_species_exists(subject) is None

0 commit comments

Comments
 (0)