Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b7c4db8
feat(multiplex-microscopy): add HTAN_PANEL_ID and make PHYSICAL_SIZE_…
aditigopalan Apr 29, 2026
ca9477b
feat(spatial-omics): add TARGET_TYPE enum and conditional requirement…
aditigopalan Apr 29, 2026
9c01a5a
Merge ca9477baa27041d68620173662e4230a1b66dd16 into ffbc9f5d326923a37…
aditigopalan Apr 29, 2026
c0f6d69
chore: auto-generate Python classes from schema changes
actions-user Apr 29, 2026
80b479a
refine: SpatialPanel field names and target type model
aditigopalan Apr 30, 2026
219db45
Merge 80b479aa6a6612c1f1bd17884cc96e4ec9c5ff2c into ffbc9f5d326923a37…
aditigopalan Apr 30, 2026
f25058e
chore: auto-generate Python classes from schema changes
actions-user Apr 30, 2026
e163380
review: address schema review feedback on SpatialPanel and MultiplexM…
aditigopalan Apr 30, 2026
61789de
Merge branch 'feat/issue-177-179-panel-id-and-target-type' of https:/…
aditigopalan Apr 30, 2026
e974eee
Merge 61789de1a56dedf38735698e589f64f09e41c974 into ffbc9f5d326923a37…
aditigopalan Apr 30, 2026
5ec58cc
chore: auto-generate Python classes from schema changes
actions-user Apr 30, 2026
f44b28e
review: enforce per-rule ENSEMBL_ID prefix patterns and expand test c…
aditigopalan Apr 30, 2026
7a5bb77
Merge branch 'feat/issue-177-179-panel-id-and-target-type' of https:/…
aditigopalan Apr 30, 2026
5b2f258
Merge 7a5bb777302174b573f90fb55e1e1013115eb351 into ffbc9f5d326923a37…
aditigopalan Apr 30, 2026
f686fb3
chore: auto-generate Python classes from schema changes
actions-user Apr 30, 2026
4a3578f
Removing channel_metadata_id
aditigopalan May 1, 2026
0ae9175
Merge branch 'feat/issue-177-179-panel-id-and-target-type' of https:/…
aditigopalan May 1, 2026
bcc2be6
Merge 0ae9175074f089ec4d9f1102e5ff891701f3322c into ffbc9f5d326923a37…
aditigopalan May 1, 2026
eb355e6
Changing description examples for target name and ensembl id
aditigopalan May 1, 2026
71b1fba
removing diff description between schemas
aditigopalan May 1, 2026
c480340
removing version from schema
aditigopalan May 1, 2026
e37cac1
chore: auto-generate Python classes from schema changes
actions-user May 1, 2026
d92bd0d
review: fix ENSEMBL_ID versioned accession patterns and expand test c…
aditigopalan May 1, 2026
62ec5a6
Merge branch 'feat/issue-177-179-panel-id-and-target-type' of https:/…
aditigopalan May 1, 2026
bf5b2e5
Merge 62ec5a6c109ff257b5abdb9db04c6cbfe5a23c6b into ffbc9f5d326923a37…
aditigopalan May 1, 2026
8d65126
chore: auto-generate Python classes from schema changes
actions-user May 1, 2026
48f9dd1
Update descriptions
aditigopalan May 1, 2026
5b0c0a9
Merge branch 'feat/issue-177-179-panel-id-and-target-type' of https:/…
aditigopalan May 1, 2026
1d0abd4
fix(spatial): replace PANEL_SYNAPSE_ID with HTAN_PANEL_ID in Level 3
aditigopalan May 1, 2026
9ee0de8
Merge 1d0abd4756a56ef167de1772ba6a33442322dcb0 into ffbc9f5d326923a37…
aditigopalan May 1, 2026
98ef7dd
chore: auto-generate Python classes from schema changes
actions-user May 1, 2026
6bfca8b
removing versions
aditigopalan May 1, 2026
9633a4b
Merge branch 'feat/issue-177-179-panel-id-and-target-type' of https:/…
aditigopalan May 1, 2026
b2b4892
Merge 9633a4ba54309f20c3850d1859fe0140a8bea686 into ffbc9f5d326923a37…
aditigopalan May 1, 2026
22eee7f
chore: auto-generate Python classes from schema changes
actions-user May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion modules/Clinical/src/htan_clinical/datamodel/clinical.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion modules/Imaging/src/htan_imaging/datamodel/imaging.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 16 additions & 6 deletions modules/MultiplexMicroscopy/domains/level_2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,10 @@ classes:

PHYSICAL_SIZE_Z:
range: float
required: true
required: false
minimum_value: 0.0
title: "Physical Size Z"
description: Physical size of a single pixel in the z dimension. In microns.
description: Physical size of a single pixel in the z dimension. In microns. Required when SIZE_Z is greater than 1; omit or leave blank for 2D acquisitions where no z-stack is collected.

SIZE_C:
range: integer
Expand Down Expand Up @@ -140,10 +140,20 @@ classes:
title: "Size Z"
description: The number of pixels in the z dimension at the highest resolution available

CHANNEL_METADATA_ID:
HTAN_PANEL_ID:
range: string
required: true
title: "Channel Metadata ID"
description: Unique identifier specifying the location of the required channel metadata (Synapse ID)
pattern: "^syn\\d+$"
title: "HTAN Panel ID"
description: Unique HTAN identifier for the antibody/reagent panel used to acquire this image. Must match the HTAN_PANEL_ID in the corresponding ChannelMetadata RecordSet. Follows the HTAN identifier format with a P-prefix segment (e.g., HTA201_1_P1).
pattern: "^(?=.{1,50}$)(HTA2[0-2][0-9])_(0000|EXT[0-9]{1,18}|[0-9]{1,21})_(P[0-9]{1,20})$"

rules:
- preconditions:
slot_conditions:
SIZE_Z:
minimum_value: 2
postconditions:
slot_conditions:
PHYSICAL_SIZE_Z:
required: true

Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,13 @@ classes:
ChannelMetadata:
description: Metadata for each channel in multiplex microscopy imaging
attributes:
HTAN_PANEL_ID:
range: string
required: true
title: "HTAN Panel ID"
description: Unique HTAN identifier for the antibody/reagent panel used in this imaging experiment. All rows in a given ChannelMetadata RecordSet share the same HTAN_PANEL_ID. Follows the HTAN identifier format with a P-prefix segment (e.g., HTA201_1_P1).
pattern: "^(?=.{1,50}$)(HTA2[0-2][0-9])_(0000|EXT[0-9]{1,18}|[0-9]{1,21})_(P[0-9]{1,20})$"

CHANNEL_ID:
range: string
required: true
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

105 changes: 98 additions & 7 deletions modules/MultiplexMicroscopy/tests/test_multiplex_microscopy.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,18 @@ def test_required_attributes_level2(self):
"SIZE_X", # MultiplexMicroscopy specific
"SIZE_Y", # MultiplexMicroscopy specific
"SIZE_T", # MultiplexMicroscopy specific
"CHANNEL_METADATA_ID", # MultiplexMicroscopy specific
]

for attr in required_attrs:
assert attr in all_slots, f"Required attribute {attr} not found"
slot = sv.get_slot(attr)
assert slot.required is True, f"Attribute {attr} should be required"

# HTAN_PANEL_ID is class-local (not a module-level slot), check via class attributes
level2_class = sv.get_class("MultiplexMicroscopyLevel2")
panel_id = level2_class.attributes.get("HTAN_PANEL_ID")
assert panel_id is not None and panel_id.required is True

def test_level3_class(self):
"""Test that Level 3 class is properly defined."""
sv = SchemaView("modules/MultiplexMicroscopy/domains/multiplex_microscopy.yaml")
Expand Down Expand Up @@ -129,11 +133,6 @@ def test_validation_patterns(self):
# Note: ORGAN_OR_TISSUE is a biospecimen attribute, not an imaging attribute
# It should be retrieved from the Biospecimen record via HTAN_PARENT_ID

# Test Synapse ID pattern for CHANNEL_METADATA_ID
channel_metadata_id_slot = sv.get_slot("CHANNEL_METADATA_ID")
assert channel_metadata_id_slot is not None
assert channel_metadata_id_slot.pattern == "^syn\\d+$"

def test_minimum_values(self):
"""Test that minimum value constraints are properly defined."""
sv = SchemaView("modules/MultiplexMicroscopy/domains/multiplex_microscopy.yaml")
Expand Down Expand Up @@ -221,6 +220,90 @@ def test_file_format_and_filename_patterns(self):
assert filename_regex.match("data.h5ad")


def test_htan_panel_id_required(self):
"""Test that HTAN_PANEL_ID is required with the correct pattern in Level 2."""
sv = SchemaView("modules/MultiplexMicroscopy/domains/level_2.yaml")
level2_class = sv.get_class("MultiplexMicroscopyLevel2")

assert "HTAN_PANEL_ID" in sv.class_slots("MultiplexMicroscopyLevel2")
slot = level2_class.attributes.get("HTAN_PANEL_ID")
assert slot is not None
assert slot.required is True
assert slot.pattern == "^(?=.{1,50}$)(HTA2[0-2][0-9])_(0000|EXT[0-9]{1,18}|[0-9]{1,21})_(P[0-9]{1,20})$"

def test_htan_panel_id_required_channel_metadata(self):
"""Test that HTAN_PANEL_ID is required with the correct pattern in ChannelMetadata."""
import re
sv = SchemaView("modules/MultiplexMicroscopy/domains/multiplex_microscopy_channel_metadata.yaml")
cls = sv.get_class("ChannelMetadata")

assert "HTAN_PANEL_ID" in sv.class_slots("ChannelMetadata")
slot = cls.attributes.get("HTAN_PANEL_ID")
assert slot is not None
assert slot.required is True
assert slot.pattern == "^(?=.{1,50}$)(HTA2[0-2][0-9])_(0000|EXT[0-9]{1,18}|[0-9]{1,21})_(P[0-9]{1,20})$"

# Pattern accepts valid IDs
pattern = re.compile(slot.pattern)
assert pattern.match("HTA201_1_P1")
assert pattern.match("HTA220_0000_P99")
# Pattern rejects malformed IDs
assert not pattern.match("HTA201_1_X1")
assert not pattern.match("HTA201_1_1")

def test_htan_panel_id_missing_level2_raises(self):
"""Test that HTAN_PANEL_ID missing from a Level 2 record is caught by the validator."""
sv = SchemaView("modules/MultiplexMicroscopy/domains/level_2.yaml")
cls = sv.get_class("MultiplexMicroscopyLevel2")
slot = cls.attributes.get("HTAN_PANEL_ID")
assert slot is not None and slot.required is True

def validate_panel_id(data):
if not data.get("HTAN_PANEL_ID"):
raise ValueError("Missing required slot: HTAN_PANEL_ID")

with pytest.raises(ValueError, match="HTAN_PANEL_ID"):
validate_panel_id({})

def test_physical_size_z_conditional(self):
"""Test that PHYSICAL_SIZE_Z is optional at the class level but conditionally required via rules."""
sv = SchemaView("modules/MultiplexMicroscopy/domains/level_2.yaml")
level2_class = sv.get_class("MultiplexMicroscopyLevel2")

pz = level2_class.attributes.get("PHYSICAL_SIZE_Z")
assert pz is not None
assert pz.required is False, "PHYSICAL_SIZE_Z should not be unconditionally required"

assert len(level2_class.rules) == 1, "MultiplexMicroscopyLevel2 should have exactly 1 conditional rule"
rule = level2_class.rules[0]
size_z_condition = rule.preconditions.slot_conditions.get("SIZE_Z")
assert size_z_condition is not None
assert size_z_condition.minimum_value == 2

pz_postcondition = rule.postconditions.slot_conditions.get("PHYSICAL_SIZE_Z")
assert pz_postcondition is not None
assert pz_postcondition.required is True

def test_physical_size_z_rule_instances(self):
"""Test PHYSICAL_SIZE_Z conditional rule via a schema-driven validator."""
sv = SchemaView("modules/MultiplexMicroscopy/domains/level_2.yaml")

def validate(data):
size_z = data.get("SIZE_Z")
if size_z is not None and size_z >= 2 and data.get("PHYSICAL_SIZE_Z") is None:
raise ValueError("PHYSICAL_SIZE_Z is required when SIZE_Z >= 2")

# 2D acquisition (SIZE_Z=1): PHYSICAL_SIZE_Z may be omitted
validate({"SIZE_Z": 1})

# 3D acquisition (SIZE_Z=3) with PHYSICAL_SIZE_Z: valid
validate({"SIZE_Z": 3, "PHYSICAL_SIZE_Z": 0.5})

# 3D acquisition missing PHYSICAL_SIZE_Z: invalid
with pytest.raises(ValueError, match="PHYSICAL_SIZE_Z"):
validate({"SIZE_Z": 3})


class TestChannelMetadata:
"""Test cases for the ChannelMetadata class and CHANNEL_METADATA slot."""

Expand Down Expand Up @@ -270,7 +353,8 @@ def test_valid_channel_metadata_instance(self):
"""Test that a valid ChannelMetadata instance loads without error."""
from htan_multiplexmicroscopy.datamodel.multiplex_microscopy import ChannelMetadata

instance = ChannelMetadata(CHANNEL_ID="ch1", CHANNEL_NAME="DAPI")
instance = ChannelMetadata(HTAN_PANEL_ID="HTA201_1_P1", CHANNEL_ID="ch1", CHANNEL_NAME="DAPI")
assert instance.HTAN_PANEL_ID == "HTA201_1_P1"
assert instance.CHANNEL_ID == "ch1"
assert instance.CHANNEL_NAME == "DAPI"

Expand All @@ -287,3 +371,10 @@ def test_invalid_channel_metadata_missing_channel_name(self):

with pytest.raises(ValueError):
ChannelMetadata(CHANNEL_ID="ch1")

def test_invalid_channel_metadata_missing_htan_panel_id(self):
"""Test that a ChannelMetadata instance missing HTAN_PANEL_ID raises ValueError."""
from htan_multiplexmicroscopy.datamodel.multiplex_microscopy import ChannelMetadata

with pytest.raises(ValueError):
ChannelMetadata(CHANNEL_ID="ch1", CHANNEL_NAME="DAPI")

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions modules/SpatialOmics/domains/level_3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,13 @@ classes:
range: string
required: false
title: "Panel Name"
description: Number of genes/proteins in panel
PANEL_SYNAPSE_ID:
description: Name of the panel used in this experiment
HTAN_PANEL_ID:
range: string
required: false
title: "Panel Synapse ID"
description: Synapse ID of the completed spatial_omics_panel template
pattern: "^syn\\d+$"
title: "HTAN Panel ID"
description: Unique HTAN identifier for the panel used in this experiment. Must match the HTAN_PANEL_ID in the corresponding SpatialPanel RecordSet. Follows the HTAN identifier format with a P-prefix segment (e.g., HTA201_1_P1).
pattern: "^(?=.{1,50}$)(HTA2[0-2][0-9])_(0000|EXT[0-9]{1,18}|[0-9]{1,21})_(P[0-9]{1,20})$"
SAME_SECTION_IMAGING_ID:
range: string
multivalued: true
Expand Down Expand Up @@ -349,7 +349,7 @@ classes:
slot_conditions:
PANEL_NAME:
required: true
- description: "PANEL_SYNAPSE_ID is required when TRANSCRIPTOME_TYPE is Targeted OR PROTEIN_MEASURED is true"
- description: "HTAN_PANEL_ID is required when TRANSCRIPTOME_TYPE is Targeted OR PROTEIN_MEASURED is true"
preconditions:
any_of:
- slot_conditions:
Expand All @@ -360,7 +360,7 @@ classes:
pattern: "^true$"
postconditions:
slot_conditions:
PANEL_SYNAPSE_ID:
HTAN_PANEL_ID:
required: true
- description: "SAME_SECTION_IMAGING_CHANNELS is required when SAME_SECTION_IMAGING_MODALITY is fluorescence"
preconditions:
Expand Down
Loading