Skip to content

Commit 4c0a6a3

Browse files
authored
feat: Section and Sectioning (#1695)
* feat: Section and Sectioning * chore: lint * test: coverage on all warnings/validators * docs: build docs (including missing timezone docs) * test: full coverage for PlanarSection * chore: docstring * docs: point to PlanarSection for slices
1 parent cd30435 commit 4c0a6a3

File tree

5 files changed

+155
-33
lines changed

5 files changed

+155
-33
lines changed

docs/source/acquisition.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ while the StimulusEpoch represents all stimuli being presented.
7979
|-------|------|-------------|
8080
| `subject_id` | `str` | Subject ID (Unique identifier for the subject) |
8181
| `specimen_id` | `Optional[str]` | Specimen ID (Specimen ID is required for in vitro imaging modalities) |
82-
| `acquisition_start_time` | `datetime (timezone-aware)` | Acquisition start time |
82+
| `acquisition_start_time` | `datetime (timezone-aware)` | Acquisition start time (During validation, timezone information will be moved into the acquisition_start_tz field.) |
83+
| `acquisition_start_tz` | `Optional[pydantic_extra_types.timezone_name.TimeZoneName]` | Acquisition start timezone (Automatically populated by a validator based on acquisition_start_time.) |
8384
| `acquisition_end_time` | `datetime (timezone-aware)` | Acquisition end time |
8485
| `experimenters` | `List[str]` | experimenter(s) |
8586
| `protocol_id` | `Optional[List[str]]` | Protocol ID (DOI for protocols.io) |

docs/source/components/identifiers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Code or script identifier
1212
| `name` | `Optional[str]` | Name |
1313
| `version` | `Optional[str]` | Code version |
1414
| `container` | Optional[[Container](#container)] | Container |
15-
| `run_script` | `Optional[pathlib._local.Path]` | Run script (Path to run script) |
15+
| `run_script` | `Optional[pathlib.Path]` | Run script (Path to run script) |
1616
| `language` | `Optional[str]` | Programming language (Programming language used) |
1717
| `language_version` | `Optional[str]` | Programming language version |
1818
| `input_data` | Optional[List[[DataAsset](#dataasset) or [CombinedData](#combineddata)]] | Input data (Input data used in the code or script) |

docs/source/components/specimen_procedures.md

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,31 +29,49 @@ Description of an HCR staining round
2929
| `probe_concentration_unit` | `str` | Probe concentration unit |
3030

3131

32+
### PlanarSection
33+
34+
Description of a single planar section of brain tissue
35+
36+
| Field | Type | Title (Description) |
37+
|-------|------|-------------|
38+
| `coordinate_system_name` | `str` | Coordinate system name |
39+
| `start_coordinate` | [Translation](coordinates.md#translation) | Start coordinate |
40+
| `end_coordinate` | Optional[[Translation](coordinates.md#translation)] | End coordinate |
41+
| `thickness` | `Optional[float]` | Slice thickness |
42+
| `thickness_unit` | Optional[[SizeUnit](../aind_data_schema_models/units.md#sizeunit)] | Slice thickness unit |
43+
| `partial_slice` | Optional[List[[AnatomicalRelative](../aind_data_schema_models/coordinates.md#anatomicalrelative)]] | Partial slice (If sectioning does not include the entire slice, indicate which part of the slice is retained.) |
44+
| `output_specimen_id` | `str` | Specimen ID |
45+
| `targeted_structure` | Optional[[BrainAtlas](../aind_data_schema_models/brain_atlas.md#ccfv3)] | Targeted structure |
46+
| `includes_surrounding_tissue` | `Optional[bool]` | Includes surrounding tissue (Whether the section includes additional tissue surrounding the targeted structure.) |
47+
48+
3249
### PlanarSectioning
3350

3451
Description of a sectioning procedure performed on the coronal, sagittal, or transverse/axial plane
3552

3653
| Field | Type | Title (Description) |
3754
|-------|------|-------------|
38-
| `coordinate_system` | Optional[[CoordinateSystem](coordinates.md#coordinatesystem)] | Sectioning coordinate system (Only required if different from the Procedures.coordinate_system) |
39-
| `sections` | List[[Section](#section)] | Sections |
55+
| `coordinate_system` | [CoordinateSystem](coordinates.md#coordinatesystem) or [Atlas](coordinates.md#atlas) or NoneType | Sectioning coordinate system (Only required if different from the Procedures.coordinate_system) |
56+
| `sections` | List[[Section](#section) or [PlanarSection](#planarsection)] | Planar sections (Use PlanarSection for new implementations) |
4057
| `section_orientation` | [SectionOrientation](#sectionorientation) | Sectioning orientation |
4158

4259

4360
### Section
4461

45-
Description of a slice of brain tissue
62+
Description of a single section of brain tissue. Slices should use PlanarSection.
4663

4764
| Field | Type | Title (Description) |
4865
|-------|------|-------------|
4966
| `output_specimen_id` | `str` | Specimen ID |
5067
| `targeted_structure` | Optional[[BrainAtlas](../aind_data_schema_models/brain_atlas.md#ccfv3)] | Targeted structure |
51-
| `coordinate_system_name` | `str` | Coordinate system name |
52-
| `start_coordinate` | [Translation](coordinates.md#translation) | Start coordinate |
53-
| `end_coordinate` | Optional[[Translation](coordinates.md#translation)] | End coordinate |
54-
| `thickness` | `Optional[float]` | Slice thickness |
55-
| `thickness_unit` | Optional[[SizeUnit](../aind_data_schema_models/units.md#sizeunit)] | Slice thickness unit |
56-
| `partial_slice` | Optional[List[[AnatomicalRelative](../aind_data_schema_models/coordinates.md#anatomicalrelative)]] | Partial slice (If sectioning does not include the entire slice, indicate which part of the slice is retained.) |
68+
| `includes_surrounding_tissue` | `Optional[bool]` | Includes surrounding tissue (Whether the section includes additional tissue surrounding the targeted structure.) |
69+
| <del>`coordinate_system_name`</del> | `Optional[str]` | **[DEPRECATED]** Use PlanarSection instead. Coordinate system name |
70+
| <del>`start_coordinate`</del> | Optional[[Translation](coordinates.md#translation)] | **[DEPRECATED]** Use PlanarSection instead. Start coordinate |
71+
| <del>`end_coordinate`</del> | Optional[[Translation](coordinates.md#translation)] | **[DEPRECATED]** Use PlanarSection instead. End coordinate |
72+
| <del>`thickness`</del> | `Optional[float]` | **[DEPRECATED]** Use PlanarSection instead. Slice thickness |
73+
| <del>`thickness_unit`</del> | Optional[[SizeUnit](../aind_data_schema_models/units.md#sizeunit)] | **[DEPRECATED]** Use PlanarSection instead. Slice thickness unit |
74+
| <del>`partial_slice`</del> | Optional[List[[AnatomicalRelative](../aind_data_schema_models/coordinates.md#anatomicalrelative)]] | **[DEPRECATED]** Use PlanarSection instead. Partial slice (If sectioning does not include the entire slice, indicate which part of the slice is retained.) |
5775

5876

5977
### SectionOrientation
@@ -67,6 +85,15 @@ Orientation of sectioning
6785
| `TRANSVERSE` | `Transverse` |
6886

6987

88+
### Sectioning
89+
90+
Description of a sectioning procedure targeting a specific structure
91+
92+
| Field | Type | Title (Description) |
93+
|-------|------|-------------|
94+
| `sections` | List[[Section](#section)] | Sections |
95+
96+
7097
### SpecimenProcedure
7198

7299
Description of surgical or other procedure performed on a specimen
@@ -75,13 +102,13 @@ Description of surgical or other procedure performed on a specimen
75102
|-------|------|-------------|
76103
| `procedure_type` | [SpecimenProcedureType](../aind_data_schema_models/specimen_procedure_types.md#specimenproceduretype) | Procedure type |
77104
| `procedure_name` | `Optional[str]` | Procedure name |
78-
| `specimen_id` | `str` | Specimen ID |
105+
| `specimen_id` | `str or List[str]` | Specimen ID(s) |
79106
| `start_date` | `datetime.date` | Start date |
80107
| `end_date` | `datetime.date` | End date |
81108
| `experimenters` | `List[str]` | experimenter(s) |
82109
| `protocol_id` | `Optional[List[str]]` | Protocol ID (DOI for protocols.io) |
83110
| `protocol_parameters` | `Optional[Dict[str, str]]` | Protocol parameters (Parameters defined in the protocol and their value during this procedure) |
84-
| `procedure_details` | List[[HCRSeries](#hcrseries) or [FluorescentStain](reagent.md#fluorescentstain) or [PlanarSectioning](#planarsectioning) or [ProbeReagent](reagent.md#probereagent) or [Reagent](reagent.md#reagent) or [GeneProbeSet](reagent.md#geneprobeset)] | Procedure details (Details of the procedures, including reagents and sectioning information.) |
111+
| `procedure_details` | List[[HCRSeries](#hcrseries) or [FluorescentStain](reagent.md#fluorescentstain) or [Sectioning](#sectioning) or [PlanarSectioning](#planarsectioning) or [ProbeReagent](reagent.md#probereagent) or [Reagent](reagent.md#reagent) or [GeneProbeSet](reagent.md#geneprobeset)] | Procedure details (Details of the procedures, including reagents and sectioning information.) |
85112
| `notes` | `Optional[str]` | Notes |
86113

87114

src/aind_data_schema/components/specimen_procedures.py

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"""Specimen procedures module for AIND data schema."""
22

3+
import warnings
34
from datetime import date
45
from enum import Enum
5-
from typing import Dict, List, Optional
6+
from typing import Dict, List, Optional, Union
67

78
from aind_data_schema_models.brain_atlas import BrainStructureModel
89
from aind_data_schema_models.coordinates import AnatomicalRelative
@@ -30,10 +31,67 @@ class SectionOrientation(str, Enum):
3031

3132

3233
class Section(DataModel):
33-
"""Description of a slice of brain tissue"""
34+
"""Description of a single section of brain tissue. Slices should use PlanarSection."""
3435

3536
output_specimen_id: str = Field(..., title="Specimen ID")
3637
targeted_structure: Optional[BrainStructureModel] = Field(default=None, title="Targeted structure")
38+
includes_surrounding_tissue: Optional[bool] = Field(
39+
default=None,
40+
title="Includes surrounding tissue",
41+
description="Whether the section includes additional tissue surrounding the targeted structure.",
42+
)
43+
44+
coordinate_system_name: Optional[str] = Field(
45+
default=None, title="Coordinate system name", deprecated="Use PlanarSection instead"
46+
)
47+
start_coordinate: Optional[Translation] = Field(
48+
default=None, title="Start coordinate", deprecated="Use PlanarSection instead"
49+
)
50+
end_coordinate: Optional[Translation] = Field(
51+
default=None, title="End coordinate", deprecated="Use PlanarSection instead"
52+
)
53+
thickness: Optional[float] = Field(default=None, title="Slice thickness", deprecated="Use PlanarSection instead")
54+
thickness_unit: Optional[SizeUnit] = Field(
55+
default=None, title="Slice thickness unit", deprecated="Use PlanarSection instead"
56+
)
57+
partial_slice: Optional[List[AnatomicalRelative]] = Field(
58+
default=None,
59+
title="Partial slice",
60+
description="If sectioning does not include the entire slice, indicate which part of the slice is retained.",
61+
deprecated="Use PlanarSection instead",
62+
)
63+
64+
@model_validator(mode="after")
65+
def deprecated_coordinate_fields(self):
66+
"""Warn if deprecated coordinate fields are used"""
67+
deprecated_fields = []
68+
if self.coordinate_system_name is not None:
69+
deprecated_fields.append("coordinate_system_name")
70+
if self.start_coordinate is not None:
71+
deprecated_fields.append("start_coordinate")
72+
if self.end_coordinate is not None:
73+
deprecated_fields.append("end_coordinate")
74+
if self.thickness is not None:
75+
deprecated_fields.append("thickness")
76+
if self.thickness_unit is not None:
77+
deprecated_fields.append("thickness_unit")
78+
if self.partial_slice is not None:
79+
deprecated_fields.append("partial_slice")
80+
81+
if deprecated_fields:
82+
warnings.warn(
83+
(
84+
f"Section fields {deprecated_fields} are deprecated. "
85+
"Use PlanarSection for sections with coordinate data."
86+
),
87+
DeprecationWarning,
88+
)
89+
90+
return self
91+
92+
93+
class PlanarSection(Section):
94+
"""Description of a single planar section of brain tissue"""
3795

3896
# Coordinates
3997
coordinate_system_name: str = Field(..., title="Coordinate system name")
@@ -54,22 +112,31 @@ def check_one_of_end_thickness(self):
54112

55113
if not self.end_coordinate and not self.thickness:
56114
raise OneOfError(
57-
"Section",
115+
"PlanarError",
58116
["end_coordinate", "thickness"],
59117
)
60118
return self
61119

62120

63-
class PlanarSectioning(DataModel):
121+
class Sectioning(DataModel):
122+
"""Description of a sectioning procedure targeting a specific structure"""
123+
124+
sections: List[Section] = Field(..., title="Sections")
125+
126+
127+
class PlanarSectioning(Sectioning):
64128
"""Description of a sectioning procedure performed on the coronal, sagittal, or transverse/axial plane"""
65129

66130
coordinate_system: Optional[CoordinateSystem | Atlas] = Field(
67131
default=None,
68132
title="Sectioning coordinate system",
69133
description="Only required if different from the Procedures.coordinate_system",
70-
) # note: exact field name is used by a validator
134+
)
135+
136+
sections: List[Union[Section, PlanarSection]] = Field(
137+
..., title="Planar sections", description="Use PlanarSection for new implementations"
138+
)
71139

72-
sections: List[Section] = Field(..., title="Sections")
73140
section_orientation: SectionOrientation = Field(..., title="Sectioning orientation")
74141

75142

@@ -99,7 +166,7 @@ class SpecimenProcedure(DataModel):
99166

100167
procedure_type: SpecimenProcedureType = Field(..., title="Procedure type")
101168
procedure_name: Optional[str] = Field(default=None, title="Procedure name")
102-
specimen_id: str = Field(..., title="Specimen ID")
169+
specimen_id: Union[str, List[str]] = Field(..., title="Specimen ID(s)")
103170
start_date: date = Field(..., title="Start date")
104171
end_date: date = Field(..., title="End date")
105172
experimenters: List[str] = Field(
@@ -114,7 +181,7 @@ class SpecimenProcedure(DataModel):
114181
)
115182

116183
procedure_details: DiscriminatedList[
117-
HCRSeries | FluorescentStain | PlanarSectioning | ProbeReagent | Reagent | GeneProbeSet
184+
HCRSeries | FluorescentStain | Sectioning | PlanarSectioning | ProbeReagent | Reagent | GeneProbeSet
118185
] = Field(
119186
default=[],
120187
title="Procedure details",

tests/test_procedures.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
""" test Procedures """
22

33
import unittest
4+
import warnings
45
from datetime import date
56
from unittest.mock import patch
67

@@ -24,6 +25,7 @@
2425
)
2526
from aind_data_schema.components.specimen_procedures import (
2627
HCRSeries,
28+
PlanarSection,
2729
PlanarSectioning,
2830
Section,
2931
SectionOrientation,
@@ -326,6 +328,26 @@ def test_validate_procedure_type_multiple(self):
326328
)
327329
self.assertIn("SpecimenProcedure.procedure_details should only contain one type of model", repr(e.exception))
328330

331+
def test_section_deprecated_coordinate_fields(self):
332+
"""Test that using deprecated coordinate fields in Section raises deprecation warnings"""
333+
with warnings.catch_warnings(record=True) as w:
334+
warnings.simplefilter("always")
335+
section = Section(
336+
output_specimen_id="section1",
337+
coordinate_system_name="CCFv3",
338+
start_coordinate=Translation(translation=[0.5, 1.0, 0.0, 1.0]),
339+
thickness=100.0,
340+
thickness_unit=SizeUnit.UM,
341+
partial_slice=[AnatomicalRelative.LEFT],
342+
)
343+
deprecation_warnings = [warning for warning in w if issubclass(warning.category, DeprecationWarning)]
344+
self.assertGreaterEqual(len(deprecation_warnings), 1)
345+
section_warnings = [warning for warning in deprecation_warnings if "Section fields" in str(warning.message)]
346+
self.assertEqual(len(section_warnings), 1)
347+
self.assertIn("partial_slice", str(section_warnings[0].message))
348+
self.assertIn("PlanarSection", str(section_warnings[0].message))
349+
self.assertEqual(section.output_specimen_id, "section1")
350+
329351
def test_coordinate_volume_validator(self):
330352
"""Test validator for list lengths on BrainInjection"""
331353

@@ -452,20 +474,25 @@ def test_sectioning(self):
452474
)
453475
self.assertIsNotNone(sectioning_procedure)
454476

477+
valid_section = PlanarSection(
478+
output_specimen_id="123456_001",
479+
coordinate_system_name="BREGMA_ARI",
480+
start_coordinate=Translation(
481+
translation=[0.3, 0, 0, 0],
482+
),
483+
thickness=100.0,
484+
thickness_unit=SizeUnit.UM,
485+
)
486+
self.assertIsNotNone(valid_section)
487+
455488
# Raise error if neither end_coordinate nor thickness is provided
456489
with self.assertRaises(OneOfError):
457-
PlanarSectioning(
458-
coordinate_system=CoordinateSystemLibrary.BREGMA_ARI,
459-
sections=[
460-
Section(
461-
output_specimen_id="123456_001",
462-
coordinate_system_name="BREGMA_ARI",
463-
start_coordinate=Translation(
464-
translation=[0.3, 0, 0],
465-
),
466-
),
467-
],
468-
section_orientation=SectionOrientation.CORONAL,
490+
PlanarSection(
491+
output_specimen_id="123456_001",
492+
coordinate_system_name="BREGMA_ARI",
493+
start_coordinate=Translation(
494+
translation=[0.3, 0, 0],
495+
),
469496
)
470497

471498
def test_validate_subject_specimen_ids(self):

0 commit comments

Comments
 (0)