Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,9 @@
- fully additive; existing grounding and profile behavior is unchanged

# 0.4.0
- added Mermaid-based visualization for output-definition catalogues and projected bundles, and an optional interactive terminal explorer built on them
- added Mermaid-based visualization for output-definition catalogues and projected bundles, and an optional interactive terminal explorer built on them

# 0.5.0
- brachy therapy
- excluded_parent_concepts support on OmopGroup
- new groups: cancer_indicating_surgery_parent_concepts, cancer_indicating_surgery_point_concepts
8 changes: 7 additions & 1 deletion docs/valuesets.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ All labelled-concept types (`RuntimeEnum`, `RuntimeGroup`) expose:
| `.labels` | sorted `list[str]` of labels |
| `.mapper()` | `dict[str, int]` label → concept_id |

`RuntimeGroup` can also define excluded parent concepts for rules shaped as one
hierarchy closure minus another. Excluded labels are still addressable as
attributes, while `.ids` and `.mapper()` remain the included anchors only. Use
`.excluded_ids` or `.excluded_mapper()` for the excluded closure.

`RuntimeSemanticUnit` additionally exposes `.enums`, `.groups`, and `.concepts` as dictionaries for direct access to the underlying objects.

## What value sets are available
Expand All @@ -53,7 +58,8 @@ The shipped value sets are defined in `instances/valuesets.yaml`. Current top-le
| `treatment_modifiers` | Treatment intent, modality, and modifier values |
| `condition_modifiers` | Condition modifier values, tumour grade, numeric modifiers, condition status |
| `nlp` | Document type, encoding, and language |
| `cancer_procedures` | Consult types, provider specialties, procedure types, location |
| `cancer_procedures` | Consult types, provider specialties, broad procedure types, cancer-indicating surgery anchors, diagnostic/staging procedure anchors, location |
| `sact` | SACT drug inclusion and exclusion anchors |
| `measurements_numeric` | Body size units and measurements, lab values, smoking, PROMs, performance status |
| `staging` | T, N, M, and group stage concepts plus stage edition |
| `visits` | Visit modalities |
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "omop-semantics"
version = "0.4.0"
version = "0.5.0"
description = "Define, validate, and use schema-backed semantic conventions for OMOP CDM"
readme = "README.md"
authors = [
Expand Down
19 changes: 18 additions & 1 deletion src/omop_semantics/runtime/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,15 @@ def render_semantic_object(obj: OmopSemanticObject | None) -> Html:
for p in as_list(obj.parent_concepts)
if p.concept_id is not None
)
exclusions = ", ".join(
f"{h(p.concept_id)} ({h(p.label)})"
for p in as_list(obj.excluded_parent_concepts)
if p.concept_id is not None
)
return Html(
f"<b>Group</b>: {h(obj.name)}<br/>"
f"<small>Anchors: {parents or '—'}</small>"
+ (f"<br/><small>Excluded anchors: {exclusions}</small>" if exclusions else "")
)

if isinstance(obj, OmopEnum):
Expand Down Expand Up @@ -184,9 +190,20 @@ def render_profile_object(obj: dict) -> Html:
for a in anchors
if isinstance(a, dict)
)
exclusions = as_list(obj.get("excluded_parent_concepts"))
exclusions_str = ", ".join(
f"{a.get('concept_id')} ({a.get('label')})"
for a in exclusions
if isinstance(a, dict)
)
return Html(
f"<b>Group</b>: {h(obj.get('name'))}"
+ (f"<br/><small>Anchors: {h(anchors_str)}</small>" if anchors_str else "")
+ (
f"<br/><small>Excluded anchors: {h(exclusions_str)}</small>"
if exclusions_str
else ""
)
)

if class_uri == "OmopEnum":
Expand Down Expand Up @@ -271,4 +288,4 @@ def render_profile_groups(profile: "SemanticProfileRuntime") -> Html:
return Html(table(
rows,
header=["Name", "Role", "Notes", "Members"],
))
))
48 changes: 42 additions & 6 deletions src/omop_semantics/runtime/value_sets.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,35 @@ class RuntimeGroup(_RuntimeLabelledConcepts):
def __init__(self, group: OmopGroup):
self._group = group
self._name = group.name or '[group]'
self._by_label = {
self._included_by_label = {
c.label: c.concept_id
for c in (group.parent_concepts or [])
if c and c.label and c.concept_id
}
self._excluded_by_label = {
c.label: c.concept_id
for c in (group.excluded_parent_concepts or [])
if c and c.label and c.concept_id
}
self._by_label = self._included_by_label | self._excluded_by_label

@property
def ids(self) -> set[int]:
return set(self._included_by_label.values())

@property
def excluded_ids(self) -> set[int]:
return set(self._excluded_by_label.values())

def mapper(self) -> dict[str, int]:
return dict(self._included_by_label)

def excluded_mapper(self) -> dict[str, int]:
return dict(self._excluded_by_label)

@property
def is_singleton(self) -> bool:
return len(self._by_label) == 1
return len(self._included_by_label) == 1

@property
def value(self) -> int:
Expand All @@ -120,14 +140,28 @@ def value(self) -> int:
raise AttributeError(
f"Group '{self._group.name}' has multiple parent concepts"
)
return next(iter(self._by_label.values()))
return next(iter(self._included_by_label.values()))

def __int__(self) -> int:
"""
Allow int(runtime.group) for singleton groups.
"""
return self.value

def _repr_html_(self) -> str:
rows = [
tr([label, cid, "included"])
for label, cid in sorted(self._included_by_label.items())
]
rows.extend(
tr([label, cid, "excluded"])
for label, cid in sorted(self._excluded_by_label.items())
)
return Html(
f"<h4>{h(self.kind_label)}: {h(self._name)}</h4>"
+ table(rows, header=["Label", "Concept ID", "Role"])
).raw

class RuntimeEnum(_RuntimeLabelledConcepts):

"""
Expand Down Expand Up @@ -236,7 +270,7 @@ def __getattr__(self, name: str):
for value in labelled_item.values():
try:
return getattr(value, name)
except KeyError:
except AttributeError:
pass

raise KeyError(name)
Expand All @@ -258,8 +292,10 @@ def _repr_html_(self) -> str:
for name in sorted(self.enums):
rows.append(tr(["Enum", name, ", ".join(self.enums[name]._by_label.keys())]))
for name, g in sorted(self.groups.items()):
if g._group.parent_concepts:
rows.append(tr(["Group", name, ", ".join(c.label for c in g._group.parent_concepts if c and c.label)]))
labels = list(g._included_by_label)
labels.extend(f"not {label}" for label in g._excluded_by_label)
if labels:
rows.append(tr(["Group", name, ", ".join(labels)]))
for name in sorted(self.concepts):
rows.append(tr(["Concept", name, ""]))

Expand Down
13 changes: 11 additions & 2 deletions src/omop_semantics/schema/configuration/core/omop_base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ classes:
T stage concepts.
slots:
- parent_concepts
- excluded_parent_concepts
slot_usage:
class_uri:
equals_string: OmopGroup
Expand Down Expand Up @@ -103,7 +104,16 @@ slots:
parent_concepts:
range: Concept
multivalued: true
description: Semantic parent concepts or grouping parents.
description: Semantic parent concepts or grouping parents.

excluded_parent_concepts:
range: Concept
multivalued: true
description: >
Parent concepts whose descendant closure is excluded from the positive
group membership. Use this when a governed value set is defined as one
hierarchy closure minus another, such as SACT drugs excluding supportive
medications.

enum_members:
range: Concept
Expand All @@ -121,4 +131,3 @@ slots:
class_uri:
range: string
required: true

2 changes: 2 additions & 0 deletions src/omop_semantics/schema/generated_models/omop_named_sets.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ class OmopGroup(OmopSemanticObject):
'name': 'class_uri'}}})

parent_concepts: Optional[list[Concept]] = Field(default=None, description="""Semantic parent concepts or grouping parents.""", json_schema_extra = { "linkml_meta": {'domain_of': ['OmopGroup']} })
excluded_parent_concepts: Optional[list[Concept]] = Field(default=None, description="""Parent concepts whose descendant closure is excluded from the positive group membership. Use this when a governed value set is defined as one hierarchy closure minus another, such as SACT drugs excluding supportive medications.
""", json_schema_extra = { "linkml_meta": {'domain_of': ['OmopGroup']} })
class_uri: Literal["OmopGroup"] = Field(default=..., json_schema_extra = { "linkml_meta": {'domain_of': ['OmopSemanticObject'], 'equals_string': 'OmopGroup'} })
name: Optional[str] = Field(default=None, json_schema_extra = { "linkml_meta": {'domain_of': ['OmopSemanticObject', 'CDMSemanticUnits']} })
notes: Optional[str] = Field(default=None, json_schema_extra = { "linkml_meta": {'domain_of': ['OmopSemanticObject']} })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ class OmopGroup(OmopSemanticObject):
'name': 'class_uri'}}})

parent_concepts: Optional[list[Concept]] = Field(default=None, description="""Semantic parent concepts or grouping parents.""", json_schema_extra = { "linkml_meta": {'domain_of': ['OmopGroup']} })
excluded_parent_concepts: Optional[list[Concept]] = Field(default=None, description="""Parent concepts whose descendant closure is excluded from the positive group membership. Use this when a governed value set is defined as one hierarchy closure minus another, such as SACT drugs excluding supportive medications.
""", json_schema_extra = { "linkml_meta": {'domain_of': ['OmopGroup']} })
class_uri: Literal["OmopGroup"] = Field(default=..., json_schema_extra = { "linkml_meta": {'domain_of': ['OmopSemanticObject'], 'equals_string': 'OmopGroup'} })
name: str = Field(default=..., json_schema_extra = { "linkml_meta": {'domain_of': ['OmopSemanticObject',
'OmopCdmProfile',
Expand Down
68 changes: 67 additions & 1 deletion src/omop_semantics/schema/instances/enumerators.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,75 @@ named_groups:
label: rn_procedure
- concept_id: 4141448
label: rt_externalbeam
- concept_id: 40317890
label: rt_brachytherapy
- concept_id: 37163499
label: rt_course
- name: cancer_indicating_surgery_parent_concepts
class_uri: OmopGroup
notes: >
More specific surgery anchors that can indicate cancer-directed surgery
in detailed procedure classification. These intentionally do not replace
the broad surgical_procedure anchor used when a construct needs all
surgery minus radiotherapy and radioisotope procedures.
parent_concepts:
- concept_id: 4000882
label: lung_excision
- concept_id: 4041977
label: gi_tract_excision
- concept_id: 4029565
label: large_intestine_excision
- concept_id: 4027426
label: kidney_excision
- concept_id: 4029571
label: urinary_bladder_excision
- concept_id: 4250917
label: prostate_operation
- concept_id: 4194253
label: breast_operation
- concept_id: 4171687
label: liver_operation
- concept_id: 4027422
label: endocrine_system_excision
- concept_id: 4238646
label: lymph_node_excision
- name: cancer_indicating_surgery_point_concepts
class_uri: OmopGroup
notes: >
Direct procedure concepts that can indicate cancer-directed surgery when
no useful OMOP ancestor captures the concept.
parent_concepts:
- concept_id: 4054047
label: lobectomy
- name: diagnostic_staging_procedure_parent_concepts
class_uri: OmopGroup
notes: >
Diagnostic and staging procedures are kept separate from treatment
surgery because they establish extent of disease rather than treating it.
parent_concepts:
- concept_id: 4228202
label: excisional_biopsy
- name: diagnostic_staging_procedure_point_concepts
class_uri: OmopGroup
notes: >
Direct diagnostic and staging procedure concepts that are not captured by
a useful OMOP ancestor.
parent_concepts:
- concept_id: 4120443
label: bone_marrow_sampling
- name: sact_drug_classification
class_uri: OmopGroup
notes: >
Systemic anti-cancer therapy drug classification is ATC L
(antineoplastic and immunomodulating agents) minus HemOnc supportive
medication. This concept-identity rule deliberately leaves
dual-purpose corticosteroids on the supportive side.
parent_concepts:
- concept_id: 21601386
label: atc_antineoplastic_and_immunomodulating
excluded_parent_concepts:
- concept_id: 35807271
label: hemonc_supportive_medication
- name: procedures_by_location
class_uri: OmopGroup
parent_concepts:
Expand Down Expand Up @@ -398,4 +465,3 @@ named_groups:
label: laterality
- concept_id: 36769180
label: metastatic_disease

9 changes: 8 additions & 1 deletion src/omop_semantics/schema/instances/valuesets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ valuesets:
- cancer_consult_types
- encounter_provider_specialty
- cancer_procedure_types
- cancer_indicating_surgery_parent_concepts
- cancer_indicating_surgery_point_concepts
- diagnostic_staging_procedure_parent_concepts
- diagnostic_staging_procedure_point_concepts
- procedures_by_location
- name: sact
semantic_units:
- sact_drug_classification
- name: measurements_numeric
semantic_units:
- body_size_units
Expand All @@ -58,4 +65,4 @@ valuesets:
- sact_concepts
- name: unknowns
semantic_units:
- unknowns
- unknowns
39 changes: 39 additions & 0 deletions tests/test_default_valuesets.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,42 @@ def test_default_valuesets_expose_id_sets_for_downstream_use() -> None:
assert 32533 in episode_types.ids
assert 32949 in episode_types.ids
assert "episode_of_care" in episode_types.labels


def test_cancer_procedure_groups_expose_governed_modality_anchors() -> None:
procedure_types = runtime.cancer_procedures.cancer_procedure_types
surgery_parents = runtime.cancer_procedures.cancer_indicating_surgery_parent_concepts
surgery_points = runtime.cancer_procedures.cancer_indicating_surgery_point_concepts
diagnostic_parents = runtime.cancer_procedures.diagnostic_staging_procedure_parent_concepts
diagnostic_points = runtime.cancer_procedures.diagnostic_staging_procedure_point_concepts

assert procedure_types.surgical_procedure == 4301351
assert procedure_types.rt_procedure == 1242725
assert procedure_types.rt_externalbeam == 4141448
assert procedure_types.rt_brachytherapy == 40317890

assert surgery_parents.mapper() == {
"lung_excision": 4000882,
"gi_tract_excision": 4041977,
"large_intestine_excision": 4029565,
"kidney_excision": 4027426,
"urinary_bladder_excision": 4029571,
"prostate_operation": 4250917,
"breast_operation": 4194253,
"liver_operation": 4171687,
"endocrine_system_excision": 4027422,
"lymph_node_excision": 4238646,
}
assert surgery_points.lobectomy == 4054047
assert diagnostic_parents.excisional_biopsy == 4228202
assert diagnostic_points.bone_marrow_sampling == 4120443


def test_sact_drug_classification_exposes_inclusion_and_exclusion_anchors() -> None:
sact = runtime.sact.sact_drug_classification

assert sact.atc_antineoplastic_and_immunomodulating == 21601386
assert sact.hemonc_supportive_medication == 35807271
assert sact.ids == {21601386}
assert sact.excluded_ids == {35807271}
assert sact.excluded_mapper() == {"hemonc_supportive_medication": 35807271}
2 changes: 1 addition & 1 deletion uv.lock

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

Loading