Skip to content

Commit ecf4b9e

Browse files
refactor: only allow wrapped NonSurgeryInjections in Procedures.subject_procedures (#1802)
* refactor: only allow Injections in Procedures.subject_procedures when wrapped with an InjectionProcedure * chore: add ethics_review_id/protocol_id * fix: only allow Injection, BrainInjection requires a full CS from Surgery * update docs [skip actions] * refactor: NonSurgicalInjection * update docs --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 92b8741 commit ecf4b9e

8 files changed

Lines changed: 77 additions & 5 deletions

File tree

docs/source/components/injection_procedures.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Description of an injection procedure
1212
| `targeted_structure` | Optional[[MouseAnatomyModel](../aind_data_schema_models/external.md#mouseanatomymodel)] | Injection target (Use InjectionTargets) |
1313
| `relative_position` | Optional[List[[AnatomicalRelative](../aind_data_schema_models/coordinates.md#anatomicalrelative)]] | Relative position |
1414
| `dynamics` | List[[InjectionDynamics](#injectiondynamics)] | Injection dynamics (List of injection events, one per location/depth) |
15-
| `protocol_id` | `Optional[str]` | Protocol ID (DOI for protocols.io) |
15+
| <del>`protocol_id`</del> | `Optional[str]` | **[DEPRECATED]** Use protocol_id in Surgery or NonSurgicalInjection instead. Protocol ID (DOI for protocols.io) |
1616

1717

1818
### InjectionDynamics

docs/source/components/subject_procedures.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ Description of a non-surgical procedure performed on a subject
1616
| `notes` | `Optional[str]` | Notes |
1717

1818

19+
### NonSurgicalInjection
20+
21+
Injection procedure performed outside of surgery,
22+
which may include one or more injections at different locations/depths
23+
24+
| Field | Type | Title (Description) |
25+
|-------|------|-------------|
26+
| `start_date` | `datetime.date` | Start date |
27+
| `ethics_review_id` | `str` | Ethics review ID |
28+
| `protocol_id` | `Optional[str]` | Protocol ID (DOI for protocols.io) |
29+
| `injections` | List[[Injection](injection_procedures.md#injection)] | Injections |
30+
| `notes` | `Optional[str]` | Notes |
31+
32+
1933
### Surgery
2034

2135
Description of subject procedures performed at one time

docs/source/components/surgery_procedures.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Description of an injection procedure into a brain
2626
| `injection_materials` | List[[ViralMaterial](injection_procedures.md#viralmaterial) or [NonViralMaterial](injection_procedures.md#nonviralmaterial)] | Injection material |
2727
| `relative_position` | Optional[List[[AnatomicalRelative](../aind_data_schema_models/coordinates.md#anatomicalrelative)]] | Relative position |
2828
| `dynamics` | List[[InjectionDynamics](injection_procedures.md#injectiondynamics)] | Injection dynamics (List of injection events, one per location/depth) |
29-
| `protocol_id` | `Optional[str]` | Protocol ID (DOI for protocols.io) |
29+
| <del>`protocol_id`</del> | `Optional[str]` | **[DEPRECATED]** Use protocol_id in Surgery or NonSurgicalInjection instead. Protocol ID (DOI for protocols.io) |
3030

3131

3232
### CatheterImplant

docs/source/procedures.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Description of all procedures performed on a subject, including surgeries, injec
2727
| Field | Type | Title (Description) |
2828
|-------|------|-------------|
2929
| `subject_id` | `str` | Subject ID (Unique identifier for the subject of data acquisition) |
30-
| `subject_procedures` | List[[Surgery](components/subject_procedures.md#surgery) or [Injection](components/injection_procedures.md#injection) or [TrainingProtocol](components/subject_procedures.md#trainingprotocol) or [WaterRestriction](components/subject_procedures.md#waterrestriction) or [GenericSubjectProcedure](components/subject_procedures.md#genericsubjectprocedure)] | Subject Procedures (Procedures performed on a live subject) |
30+
| `subject_procedures` | List[[Surgery](components/subject_procedures.md#surgery) or [Injection](components/injection_procedures.md#injection) or [NonSurgicalInjection](components/subject_procedures.md#nonsurgicalinjection) or [TrainingProtocol](components/subject_procedures.md#trainingprotocol) or [WaterRestriction](components/subject_procedures.md#waterrestriction) or [GenericSubjectProcedure](components/subject_procedures.md#genericsubjectprocedure)] | Subject Procedures (Procedures performed on a live subject) |
3131
| `specimen_procedures` | List[[SpecimenProcedure](components/specimen_procedures.md#specimenprocedure)] | Specimen Procedures (Procedures performed on tissue extracted after perfusion) |
3232
| `coordinate_system` | Optional[[CoordinateSystem](components/coordinates.md#coordinatesystem)] | Coordinate System (Origin and axis definitions for determining the configured position of devices implanted during procedures. Required when coordinates are provided within the Procedures) |
3333
| `notes` | `Optional[str]` | Notes |

src/aind_data_schema/components/injection_procedures.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,9 @@ class Injection(DataModel):
116116
dynamics: List[InjectionDynamics] = Field(
117117
..., title="Injection dynamics", description="List of injection events, one per location/depth"
118118
)
119-
protocol_id: Optional[str] = Field(default=None, title="Protocol ID", description="DOI for protocols.io")
119+
protocol_id: Optional[str] = Field(
120+
default=None,
121+
title="Protocol ID",
122+
description="DOI for protocols.io",
123+
deprecated="Use protocol_id in Surgery or NonSurgicalInjection instead",
124+
)

src/aind_data_schema/components/subject_procedures.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ class GenericSubjectProcedure(DataModel):
3939
notes: Optional[str] = Field(default=None, title="Notes")
4040

4141

42+
class NonSurgicalInjection(DataModel):
43+
"""Injection procedure performed outside of surgery,
44+
which may include one or more injections at different locations/depths
45+
"""
46+
47+
start_date: date = Field(..., title="Start date")
48+
ethics_review_id: str = Field(..., title="Ethics review ID")
49+
protocol_id: Optional[str] = Field(default=None, title="Protocol ID", description="DOI for protocols.io")
50+
injections: List[Injection] = Field(..., title="Injections", min_length=1)
51+
notes: Optional[str] = Field(default=None, title="Notes")
52+
53+
4254
class TrainingProtocol(DataModel):
4355
"""Description of an animal training protocol"""
4456

src/aind_data_schema/core/procedures.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""schema for various Procedures"""
22

3+
import warnings
34
from typing import List, Literal, Optional
45

56
from pydantic import Field, SkipValidation, model_validator
@@ -13,6 +14,7 @@
1314
Surgery,
1415
TrainingProtocol,
1516
WaterRestriction,
17+
NonSurgicalInjection,
1618
)
1719
from aind_data_schema.utils.merge import merge_coordinate_systems, merge_notes
1820
from aind_data_schema.utils.validators import subject_specimen_id_compatibility
@@ -31,7 +33,7 @@ class Procedures(DataCoreModel):
3133
title="Subject ID",
3234
)
3335
subject_procedures: DiscriminatedList[
34-
Surgery | Injection | TrainingProtocol | WaterRestriction | GenericSubjectProcedure
36+
Surgery | Injection | NonSurgicalInjection | TrainingProtocol | WaterRestriction | GenericSubjectProcedure
3537
] = Field(default=[], title="Subject Procedures", description="Procedures performed on a live subject")
3638
specimen_procedures: List[SpecimenProcedure] = Field(
3739
default=[], title="Specimen Procedures", description="Procedures performed on tissue extracted after perfusion"
@@ -72,6 +74,22 @@ def get_device_names(self) -> List[str]:
7274

7375
return list(device_names)
7476

77+
@model_validator(mode="after")
78+
def reject_injections(self):
79+
"""Raise a warning for injections since they should now be wrapped
80+
in a Surgery or NonSurgicalInjection procedure
81+
"""
82+
83+
for procedure in self.subject_procedures:
84+
if isinstance(procedure, Injection):
85+
warnings.warn(
86+
"Injection procedures should be wrapped in a Surgery or NonSurgicalInjection procedure.",
87+
UserWarning,
88+
stacklevel=2,
89+
)
90+
91+
return self
92+
7593
@model_validator(mode="after")
7694
def validate_subject_specimen_ids(self):
7795
"""Validate that the subject_id and specimen_id match"""

tests/test_procedures.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,29 @@ def test_required_field_validation_check(self):
5353
p = Procedures(subject_id="12345")
5454
self.assertEqual("12345", p.subject_id)
5555

56+
@patch("aind_data_schema_models.mouse_anatomy.get_emapa_id")
57+
def test_unwrapped_injection_warns(self, mock_get_emapa_id):
58+
"""Unwrapped Injection in subject_procedures should emit a UserWarning"""
59+
mock_get_emapa_id.return_value = "123456"
60+
with self.assertWarns(UserWarning):
61+
Procedures(
62+
subject_id="12345",
63+
subject_procedures=[
64+
Injection(
65+
injection_materials=[NonViralMaterial(name="saline", source=Organization.OTHER)],
66+
dynamics=[
67+
InjectionDynamics(
68+
volume=1,
69+
volume_unit=VolumeUnit.UL,
70+
duration=1,
71+
duration_unit=TimeUnit.S,
72+
profile=InjectionProfile.BOLUS,
73+
)
74+
],
75+
)
76+
],
77+
)
78+
5679
@patch("aind_data_schema_models.mouse_anatomy.get_emapa_id")
5780
def test_injection_material_check(self, mock_get_emapa_id):
5881
"""Check for validation error when injection_materials is empty"""

0 commit comments

Comments
 (0)