From ae5a1770fb037b8226fac0062ea1838e050bf199 Mon Sep 17 00:00:00 2001 From: Dan Birman Date: Thu, 16 Apr 2026 11:11:29 -0700 Subject: [PATCH 1/6] refactor: only allow Injections in Procedures.subject_procedures when wrapped with an InjectionProcedure --- src/aind_data_schema/components/subject_procedures.py | 9 +++++++++ src/aind_data_schema/core/procedures.py | 6 ++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/aind_data_schema/components/subject_procedures.py b/src/aind_data_schema/components/subject_procedures.py index 3aef85071..678ba57f6 100644 --- a/src/aind_data_schema/components/subject_procedures.py +++ b/src/aind_data_schema/components/subject_procedures.py @@ -39,6 +39,15 @@ class GenericSubjectProcedure(DataModel): notes: Optional[str] = Field(default=None, title="Notes") +class InjectionProcedure(DataModel): + """Injection procedure, which may include one or more injections at different locations/depths""" + + start_date: date = Field(..., title="Start date") + injections: DiscriminatedList[Injection | BrainInjection] = Field(..., title="Injections", min_length=1) + anaesthesia: Optional[Anaesthetic] = Field(default=None, title="Anaesthesia") + notes: Optional[str] = Field(default=None, title="Notes") + + class TrainingProtocol(DataModel): """Description of an animal training protocol""" diff --git a/src/aind_data_schema/core/procedures.py b/src/aind_data_schema/core/procedures.py index 58b7ea3e4..016951c73 100644 --- a/src/aind_data_schema/core/procedures.py +++ b/src/aind_data_schema/core/procedures.py @@ -1,10 +1,11 @@ """schema for various Procedures""" +from datetime import date from typing import List, Literal, Optional from pydantic import Field, SkipValidation, model_validator -from aind_data_schema.base import DataCoreModel, DiscriminatedList +from aind_data_schema.base import DataCoreModel, DataModel, DiscriminatedList from aind_data_schema.components.coordinates import CoordinateSystem from aind_data_schema.components.injection_procedures import Injection from aind_data_schema.components.specimen_procedures import SpecimenProcedure @@ -13,6 +14,7 @@ Surgery, TrainingProtocol, WaterRestriction, + InjectionProcedure, ) from aind_data_schema.utils.merge import merge_notes, merge_coordinate_systems from aind_data_schema.utils.validators import subject_specimen_id_compatibility @@ -31,7 +33,7 @@ class Procedures(DataCoreModel): title="Subject ID", ) subject_procedures: DiscriminatedList[ - Surgery | Injection | TrainingProtocol | WaterRestriction | GenericSubjectProcedure + Surgery | InjectionProcedure | TrainingProtocol | WaterRestriction | GenericSubjectProcedure ] = Field(default=[], title="Subject Procedures", description="Procedures performed on a live subject") specimen_procedures: List[SpecimenProcedure] = Field( default=[], title="Specimen Procedures", description="Procedures performed on tissue extracted after perfusion" From 48136b78ff70897ea8ebcdc7fac941b538e1deff Mon Sep 17 00:00:00 2001 From: Dan Birman Date: Thu, 16 Apr 2026 11:13:56 -0700 Subject: [PATCH 2/6] chore: add ethics_review_id/protocol_id --- src/aind_data_schema/components/subject_procedures.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/aind_data_schema/components/subject_procedures.py b/src/aind_data_schema/components/subject_procedures.py index 678ba57f6..448acdc20 100644 --- a/src/aind_data_schema/components/subject_procedures.py +++ b/src/aind_data_schema/components/subject_procedures.py @@ -43,6 +43,8 @@ class InjectionProcedure(DataModel): """Injection procedure, which may include one or more injections at different locations/depths""" start_date: date = Field(..., title="Start date") + ethics_review_id: str = Field(..., title="Ethics review ID") + protocol_id: Optional[str] = Field(default=None, title="Protocol ID", description="DOI for protocols.io") injections: DiscriminatedList[Injection | BrainInjection] = Field(..., title="Injections", min_length=1) anaesthesia: Optional[Anaesthetic] = Field(default=None, title="Anaesthesia") notes: Optional[str] = Field(default=None, title="Notes") From ce46e6d5b2df159d84a33e1fae34849264bcace3 Mon Sep 17 00:00:00 2001 From: Dan Birman Date: Thu, 16 Apr 2026 11:14:49 -0700 Subject: [PATCH 3/6] fix: only allow Injection, BrainInjection requires a full CS from Surgery --- src/aind_data_schema/components/subject_procedures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aind_data_schema/components/subject_procedures.py b/src/aind_data_schema/components/subject_procedures.py index 448acdc20..62becf7b9 100644 --- a/src/aind_data_schema/components/subject_procedures.py +++ b/src/aind_data_schema/components/subject_procedures.py @@ -45,7 +45,7 @@ class InjectionProcedure(DataModel): start_date: date = Field(..., title="Start date") ethics_review_id: str = Field(..., title="Ethics review ID") protocol_id: Optional[str] = Field(default=None, title="Protocol ID", description="DOI for protocols.io") - injections: DiscriminatedList[Injection | BrainInjection] = Field(..., title="Injections", min_length=1) + injections: List[Injection] = Field(..., title="Injections", min_length=1) anaesthesia: Optional[Anaesthetic] = Field(default=None, title="Anaesthesia") notes: Optional[str] = Field(default=None, title="Notes") From bb44e69fc06ade0b583797e7c961a62ee3924d33 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:16:22 +0000 Subject: [PATCH 4/6] update docs [skip actions] --- docs/source/components/subject_procedures.md | 14 ++++++++++++++ docs/source/procedures.md | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/source/components/subject_procedures.md b/docs/source/components/subject_procedures.md index 7f630b4b2..85668033a 100644 --- a/docs/source/components/subject_procedures.md +++ b/docs/source/components/subject_procedures.md @@ -16,6 +16,20 @@ Description of a non-surgical procedure performed on a subject | `notes` | `Optional[str]` | Notes | +### InjectionProcedure + +Injection procedure, which may include one or more injections at different locations/depths + +| Field | Type | Title (Description) | +|-------|------|-------------| +| `start_date` | `datetime.date` | Start date | +| `ethics_review_id` | `str` | Ethics review ID | +| `protocol_id` | `Optional[str]` | Protocol ID (DOI for protocols.io) | +| `injections` | List[[Injection](injection_procedures.md#injection)] | Injections | +| `anaesthesia` | Optional[[Anaesthetic](surgery_procedures.md#anaesthetic)] | Anaesthesia | +| `notes` | `Optional[str]` | Notes | + + ### Surgery Description of subject procedures performed at one time diff --git a/docs/source/procedures.md b/docs/source/procedures.md index 98a5969c2..72a011198 100644 --- a/docs/source/procedures.md +++ b/docs/source/procedures.md @@ -27,7 +27,7 @@ Description of all procedures performed on a subject, including surgeries, injec | Field | Type | Title (Description) | |-------|------|-------------| | `subject_id` | `str` | Subject ID (Unique identifier for the subject of data acquisition) | -| `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) | +| `subject_procedures` | List[[Surgery](components/subject_procedures.md#surgery) or [InjectionProcedure](components/subject_procedures.md#injectionprocedure) 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) | | `specimen_procedures` | List[[SpecimenProcedure](components/specimen_procedures.md#specimenprocedure)] | Specimen Procedures (Procedures performed on tissue extracted after perfusion) | | `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) | | `notes` | `Optional[str]` | Notes | From e6c2281f1d5b432e55d5acd815b324a174edbcf6 Mon Sep 17 00:00:00 2001 From: Dan Birman Date: Wed, 22 Apr 2026 15:04:50 -0700 Subject: [PATCH 5/6] refactor: NonSurgicalInjection --- docs/source/components/subject_procedures.md | 2 +- docs/source/procedures.md | 2 +- .../components/injection_procedures.py | 7 +++++- .../components/subject_procedures.py | 7 +++--- src/aind_data_schema/core/procedures.py | 24 +++++++++++++++---- tests/test_procedures.py | 23 ++++++++++++++++++ 6 files changed, 55 insertions(+), 10 deletions(-) diff --git a/docs/source/components/subject_procedures.md b/docs/source/components/subject_procedures.md index 85668033a..b3d3a932f 100644 --- a/docs/source/components/subject_procedures.md +++ b/docs/source/components/subject_procedures.md @@ -16,7 +16,7 @@ Description of a non-surgical procedure performed on a subject | `notes` | `Optional[str]` | Notes | -### InjectionProcedure +### NonSurgicalInjection Injection procedure, which may include one or more injections at different locations/depths diff --git a/docs/source/procedures.md b/docs/source/procedures.md index 72a011198..9d09d768a 100644 --- a/docs/source/procedures.md +++ b/docs/source/procedures.md @@ -27,7 +27,7 @@ Description of all procedures performed on a subject, including surgeries, injec | Field | Type | Title (Description) | |-------|------|-------------| | `subject_id` | `str` | Subject ID (Unique identifier for the subject of data acquisition) | -| `subject_procedures` | List[[Surgery](components/subject_procedures.md#surgery) or [InjectionProcedure](components/subject_procedures.md#injectionprocedure) 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) | +| `subject_procedures` | List[[Surgery](components/subject_procedures.md#surgery) 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) | | `specimen_procedures` | List[[SpecimenProcedure](components/specimen_procedures.md#specimenprocedure)] | Specimen Procedures (Procedures performed on tissue extracted after perfusion) | | `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) | | `notes` | `Optional[str]` | Notes | diff --git a/src/aind_data_schema/components/injection_procedures.py b/src/aind_data_schema/components/injection_procedures.py index 4117fe291..039893d3e 100644 --- a/src/aind_data_schema/components/injection_procedures.py +++ b/src/aind_data_schema/components/injection_procedures.py @@ -116,4 +116,9 @@ class Injection(DataModel): dynamics: List[InjectionDynamics] = Field( ..., title="Injection dynamics", description="List of injection events, one per location/depth" ) - protocol_id: Optional[str] = Field(default=None, title="Protocol ID", description="DOI for protocols.io") + protocol_id: Optional[str] = Field( + default=None, + title="Protocol ID", + description="DOI for protocols.io", + deprecated="Use protocol_id in Surgery or NonSurgicalInjection instead", + ) diff --git a/src/aind_data_schema/components/subject_procedures.py b/src/aind_data_schema/components/subject_procedures.py index 62becf7b9..93850c061 100644 --- a/src/aind_data_schema/components/subject_procedures.py +++ b/src/aind_data_schema/components/subject_procedures.py @@ -39,14 +39,15 @@ class GenericSubjectProcedure(DataModel): notes: Optional[str] = Field(default=None, title="Notes") -class InjectionProcedure(DataModel): - """Injection procedure, which may include one or more injections at different locations/depths""" +class NonSurgicalInjection(DataModel): + """Injection procedure performed outside of surgery, + which may include one or more injections at different locations/depths + """ start_date: date = Field(..., title="Start date") ethics_review_id: str = Field(..., title="Ethics review ID") protocol_id: Optional[str] = Field(default=None, title="Protocol ID", description="DOI for protocols.io") injections: List[Injection] = Field(..., title="Injections", min_length=1) - anaesthesia: Optional[Anaesthetic] = Field(default=None, title="Anaesthesia") notes: Optional[str] = Field(default=None, title="Notes") diff --git a/src/aind_data_schema/core/procedures.py b/src/aind_data_schema/core/procedures.py index 016951c73..a09d23d66 100644 --- a/src/aind_data_schema/core/procedures.py +++ b/src/aind_data_schema/core/procedures.py @@ -1,11 +1,11 @@ """schema for various Procedures""" -from datetime import date +import warnings from typing import List, Literal, Optional from pydantic import Field, SkipValidation, model_validator -from aind_data_schema.base import DataCoreModel, DataModel, DiscriminatedList +from aind_data_schema.base import DataCoreModel, DiscriminatedList from aind_data_schema.components.coordinates import CoordinateSystem from aind_data_schema.components.injection_procedures import Injection from aind_data_schema.components.specimen_procedures import SpecimenProcedure @@ -14,7 +14,7 @@ Surgery, TrainingProtocol, WaterRestriction, - InjectionProcedure, + NonSurgicalInjection, ) from aind_data_schema.utils.merge import merge_notes, merge_coordinate_systems from aind_data_schema.utils.validators import subject_specimen_id_compatibility @@ -33,7 +33,7 @@ class Procedures(DataCoreModel): title="Subject ID", ) subject_procedures: DiscriminatedList[ - Surgery | InjectionProcedure | TrainingProtocol | WaterRestriction | GenericSubjectProcedure + Surgery | Injection | NonSurgicalInjection | TrainingProtocol | WaterRestriction | GenericSubjectProcedure ] = Field(default=[], title="Subject Procedures", description="Procedures performed on a live subject") specimen_procedures: List[SpecimenProcedure] = Field( default=[], title="Specimen Procedures", description="Procedures performed on tissue extracted after perfusion" @@ -74,6 +74,22 @@ def get_device_names(self) -> List[str]: return list(device_names) + @model_validator(mode="after") + def reject_injections(self): + """Raise a warning for injections since they should now be wrapped + in a Surgery or NonSurgicalInjection procedure + """ + + for procedure in self.subject_procedures: + if isinstance(procedure, Injection): + warnings.warn( + "Injection procedures should be wrapped in a Surgery or NonSurgicalInjection procedure.", + UserWarning, + stacklevel=2, + ) + + return self + @model_validator(mode="after") def validate_subject_specimen_ids(self): """Validate that the subject_id and specimen_id match""" diff --git a/tests/test_procedures.py b/tests/test_procedures.py index d808d6c0d..4554c4c8a 100644 --- a/tests/test_procedures.py +++ b/tests/test_procedures.py @@ -53,6 +53,29 @@ def test_required_field_validation_check(self): p = Procedures(subject_id="12345") self.assertEqual("12345", p.subject_id) + @patch("aind_data_schema_models.mouse_anatomy.get_emapa_id") + def test_unwrapped_injection_warns(self, mock_get_emapa_id): + """Unwrapped Injection in subject_procedures should emit a UserWarning""" + mock_get_emapa_id.return_value = "123456" + with self.assertWarns(UserWarning): + Procedures( + subject_id="12345", + subject_procedures=[ + Injection( + injection_materials=[NonViralMaterial(name="saline", source=Organization.OTHER)], + dynamics=[ + InjectionDynamics( + volume=1, + volume_unit=VolumeUnit.UL, + duration=1, + duration_unit=TimeUnit.S, + profile=InjectionProfile.BOLUS, + ) + ], + ) + ], + ) + @patch("aind_data_schema_models.mouse_anatomy.get_emapa_id") def test_injection_material_check(self, mock_get_emapa_id): """Check for validation error when injection_materials is empty""" From 5296708cf5d21395b862c2abee8e123d384cd2ff Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:05:47 +0000 Subject: [PATCH 6/6] update docs --- docs/source/aind_data_schema_models/harp_types.md | 5 ++++- docs/source/components/injection_procedures.md | 2 +- docs/source/components/subject_procedures.md | 4 ++-- docs/source/components/surgery_procedures.md | 2 +- docs/source/procedures.md | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/source/aind_data_schema_models/harp_types.md b/docs/source/aind_data_schema_models/harp_types.md index fff533f6d..f89b1bad1 100644 --- a/docs/source/aind_data_schema_models/harp_types.md +++ b/docs/source/aind_data_schema_models/harp_types.md @@ -10,10 +10,12 @@ Harp device types |------|------|------| | `ANALOGINPUT` | `AnalogInput` | `1236` | | `ARCHIMEDES` | `Archimedes` | `1136` | +| `AUDIOSWITCH` | `AudioSwitch` | `1248` | | `BEHAVIOR` | `Behavior` | `1216` | | `CAMERACONTROLLER` | `CameraController` | `1168` | | `CAMERACONTROLLERGEN2` | `CameraControllerGen2` | `1170` | | `CLOCKSYNCHRONIZER` | `ClockSynchronizer` | `1152` | +| `CURRENTDRIVER` | `CurrentDriver` | `1282` | | `CUTTLEFISH` | `cuTTLefish` | `1403` | | `CUTTLEFISHFIP` | `cuTTLefishFip` | `1407` | | `DRIVER12VOLTS` | `Driver12Volts` | `1072` | @@ -22,6 +24,7 @@ Harp device types | `HOBGOBLIN` | `Hobgoblin` | `123` | | `IBL_BEHAVIOR_CONTROL` | `Ibl_behavior_control` | `2080` | | `INPUTEXPANDER` | `InputExpander` | `1106` | +| `LASERDRIVERCONTROLLER` | `LaserDriverController` | `1298` | | `LEDCONTROLLER` | `LedController` | `1088` | | `LICKETYSPLIT` | `LicketySplit` | `1400` | | `LOADCELLS` | `LoadCells` | `1232` | @@ -33,7 +36,7 @@ Harp device types | `POKE` | `Poke` | `1024` | | `PYCONTROLADAPTER` | `PyControlAdapter` | `1184` | | `RFIDREADER` | `RfidReader` | `2094` | -| `RGBARRAY` | `RgbArray` | `1248` | +| `RGBARRAY` | `RgbArray` | `1264` | | `SIMPLEANALOGGENERATOR` | `SimpleAnalogGenerator` | `1121` | | `SNIFFDETECTOR` | `SniffDetector` | `1401` | | `SOUNDCARD` | `SoundCard` | `1280` | diff --git a/docs/source/components/injection_procedures.md b/docs/source/components/injection_procedures.md index 8a92b07a2..633ab1827 100644 --- a/docs/source/components/injection_procedures.md +++ b/docs/source/components/injection_procedures.md @@ -12,7 +12,7 @@ Description of an injection procedure | `targeted_structure` | Optional[[MouseAnatomyModel](../aind_data_schema_models/external.md#mouseanatomymodel)] | Injection target (Use InjectionTargets) | | `relative_position` | Optional[List[[AnatomicalRelative](../aind_data_schema_models/coordinates.md#anatomicalrelative)]] | Relative position | | `dynamics` | List[[InjectionDynamics](#injectiondynamics)] | Injection dynamics (List of injection events, one per location/depth) | -| `protocol_id` | `Optional[str]` | Protocol ID (DOI for protocols.io) | +| `protocol_id` | `Optional[str]` | **[DEPRECATED]** Use protocol_id in Surgery or NonSurgicalInjection instead. Protocol ID (DOI for protocols.io) | ### InjectionDynamics diff --git a/docs/source/components/subject_procedures.md b/docs/source/components/subject_procedures.md index b3d3a932f..9a1ac14f6 100644 --- a/docs/source/components/subject_procedures.md +++ b/docs/source/components/subject_procedures.md @@ -18,7 +18,8 @@ Description of a non-surgical procedure performed on a subject ### NonSurgicalInjection -Injection procedure, which may include one or more injections at different locations/depths +Injection procedure performed outside of surgery, +which may include one or more injections at different locations/depths | Field | Type | Title (Description) | |-------|------|-------------| @@ -26,7 +27,6 @@ Injection procedure, which may include one or more injections at different locat | `ethics_review_id` | `str` | Ethics review ID | | `protocol_id` | `Optional[str]` | Protocol ID (DOI for protocols.io) | | `injections` | List[[Injection](injection_procedures.md#injection)] | Injections | -| `anaesthesia` | Optional[[Anaesthetic](surgery_procedures.md#anaesthetic)] | Anaesthesia | | `notes` | `Optional[str]` | Notes | diff --git a/docs/source/components/surgery_procedures.md b/docs/source/components/surgery_procedures.md index 0248979f1..fe51b6f17 100644 --- a/docs/source/components/surgery_procedures.md +++ b/docs/source/components/surgery_procedures.md @@ -26,7 +26,7 @@ Description of an injection procedure into a brain | `injection_materials` | List[[ViralMaterial](injection_procedures.md#viralmaterial) or [NonViralMaterial](injection_procedures.md#nonviralmaterial)] | Injection material | | `relative_position` | Optional[List[[AnatomicalRelative](../aind_data_schema_models/coordinates.md#anatomicalrelative)]] | Relative position | | `dynamics` | List[[InjectionDynamics](injection_procedures.md#injectiondynamics)] | Injection dynamics (List of injection events, one per location/depth) | -| `protocol_id` | `Optional[str]` | Protocol ID (DOI for protocols.io) | +| `protocol_id` | `Optional[str]` | **[DEPRECATED]** Use protocol_id in Surgery or NonSurgicalInjection instead. Protocol ID (DOI for protocols.io) | ### CatheterImplant diff --git a/docs/source/procedures.md b/docs/source/procedures.md index 9d09d768a..029b65a29 100644 --- a/docs/source/procedures.md +++ b/docs/source/procedures.md @@ -27,7 +27,7 @@ Description of all procedures performed on a subject, including surgeries, injec | Field | Type | Title (Description) | |-------|------|-------------| | `subject_id` | `str` | Subject ID (Unique identifier for the subject of data acquisition) | -| `subject_procedures` | List[[Surgery](components/subject_procedures.md#surgery) 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) | +| `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) | | `specimen_procedures` | List[[SpecimenProcedure](components/specimen_procedures.md#specimenprocedure)] | Specimen Procedures (Procedures performed on tissue extracted after perfusion) | | `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) | | `notes` | `Optional[str]` | Notes |