Clinical Context and Extraction Depth¶
OpenMed's clinical context layer turns entity spans into reviewable assertion metadata. It composes deterministic ConText-style axes, section priors, scoped modifier hits, and lightweight normalization helpers so downstream exporters can keep clinical text extraction transparent.
Advisory annotations only
Context and extraction-depth outputs are advisory annotations for review, quality checks, and downstream processing. They must not automatically trigger diagnosis, triage, treatment, escalation, medication changes, or any other clinical decision. Validate the full workflow locally before using it in a clinical or regulated setting.
The layer is intentionally local and mechanical:
- A clinical NER or structured extractor proposes a target span.
- A scope scanner finds modifier cues that actually reach that target.
- Section metadata can add a prior, such as historical temporality in Past Medical History, when no stronger scoped cue is present.
- The axis resolvers produce temporality, certainty, and negation values.
ClinicalAssertioncarries the composed assertion axes for downstream grounding, tabular export, or FHIR resource construction.
For FHIR-specific resource shaping, see the FHIR interop helpers. For privacy-preserving examples and surrogate text handling, start with the de-identification cookbook and the copy/paste recipes.
Context Axes¶
resolve_temporality() classifies a span as:
| Temporality | Meaning | Example cue |
|---|---|---|
recent | Current or active by default. | acute |
historical | Belongs to the patient's past history. | history of, s/p |
hypothetical | Conditional or not asserted as present. | if, in case of |
resolve_uncertainty() classifies a span as:
| Certainty | Meaning | Example cue |
|---|---|---|
certain | Asserted without a hedging cue. | confirmed |
uncertain | Hedged, conditional, or possible. | possible, rule out |
resolve_negation() classifies polarity as:
| Negation | Meaning | Example cue |
|---|---|---|
affirmed | The span is not refuted. | pneumonia confirmed |
negated | The span is explicitly refuted. | no evidence of |
Pseudo-negation cues, such as not ruled out and no increase, are masked before true negation cues are counted. That keeps "pneumonia not ruled out" affirmed but uncertain, instead of incorrectly treating it as refuted.
from openmed.clinical import assert_context_axes, resolve_span_context
examples = {
"recent": ("acute pneumonia", []),
"historical": ("MI", ["history of"]),
"hypothetical": ("wheezing", ["if"]),
"uncertain": ("pneumonia", ["possible"]),
"negated": ("pneumonia", ["no evidence of"]),
}
for name, (span, modifiers) in examples.items():
context = resolve_span_context(span, modifiers)
assertion = assert_context_axes(span, modifiers)
print(name, context.temporality, context.certainty, context.negation)
print(assertion.to_dict())
Scope And Section Priors¶
Modifier hits should be scoped before they reach the axis resolvers. A cue only modifies a target when no sentence boundary or coordinating terminator blocks the path between cue and target. For example, history of should affect "asthma" in "history of asthma" but not "pneumonia" in "history of asthma but pneumonia is present".
Section priors are weaker than scoped cues. A Past Medical History section can seed a historical modifier for otherwise unmodified spans, while a direct hypothetical cue still wins over that prior.
from openmed.clinical import resolve_span_context
section_prior_hits = {
"historical": "history of",
}
target = "asthma"
modifier_hits = []
section_prior = "historical"
effective_hits = list(modifier_hits)
temporal_hits = {"history of", "if", "in case of"}
if section_prior and not any(hit in temporal_hits for hit in effective_hits):
effective_hits.append(section_prior_hits[section_prior])
context = resolve_span_context(target, effective_hits)
print(context.temporality, context.certainty, context.negation)
Family-history sections should also remain distinguishable from patient assertions. If an upstream extractor marks a span as family history, preserve that section or experiencer metadata and avoid materializing it as an active patient condition. The ClinicalAssertion.experiencer field is available for callers that already have an experiencer layer.
Assertion Records¶
assert_context_axes() returns a compact ClinicalAssertion for downstream grounding. It deliberately does not build FHIR, OMOP, or other clinical records by itself.
from openmed.clinical import assert_context_axes
assertion = assert_context_axes({"text": "possible pneumonia"})
print(assertion.to_dict())
Optional axes such as negation and experiencer can be carried on ClinicalAssertion when a caller has already resolved them:
from openmed.clinical import AFFIRMED, CERTAIN, ClinicalAssertion, RECENT
assertion = ClinicalAssertion(
temporality=RECENT,
certainty=CERTAIN,
negation=AFFIRMED,
experiencer="patient",
)
print(assertion.to_dict())
Axis To FHIR Mapping¶
The context layer emits axis values. A FHIR exporter decides whether and how to materialize a Condition, Observation, MedicationStatement, or related resource. Use this table as the documented default mapping for Condition-like assertions:
| Axis signal | FHIR field | Default mapping | Notes |
|---|---|---|---|
temporality=recent | clinicalStatus | active | Use when the span is asserted as a current patient condition. |
temporality=historical | clinicalStatus | inactive or resolved | Preserve onset, abatement, or provenance dates when available. |
temporality=hypothetical | clinicalStatus | no active condition | Keep as advisory metadata or a provisional planning note if retained. |
certainty=certain | verificationStatus | confirmed | Apply only when not negated. |
certainty=uncertain | verificationStatus | provisional | Do not drop the span; carry the uncertainty. |
negation=negated | verificationStatus | refuted | Refuted findings should not become active conditions. |
experiencer=family | resource choice | family-history representation | Do not turn family history into a patient active condition. |
from openmed.clinical import resolve_span_context
def condition_status_for_context(text: str, modifiers: list[str]) -> dict[str, str]:
context = resolve_span_context(text, modifiers)
status = {
"clinicalStatus": "active",
"verificationStatus": "confirmed",
}
if context.temporality == "historical":
status["clinicalStatus"] = "inactive"
if context.certainty == "uncertain":
status["verificationStatus"] = "provisional"
if context.negation == "negated":
status["verificationStatus"] = "refuted"
status["clinicalStatus"] = "not-materialized-as-active"
if context.temporality == "hypothetical":
status["clinicalStatus"] = "not-materialized-as-active"
return status
print(condition_status_for_context("pneumonia", ["possible"]))
print(condition_status_for_context("pneumonia", ["no evidence of"]))
Timeline, Relation, And Normalization Helpers¶
Timeline and relation helpers sit beside the assertion axes. A timeline layer should normalize dates and relative ordering while keeping offsets, provenance, and section metadata. A relation layer should connect already-extracted spans, such as medication-to-dose or finding-to-anatomy, without copying raw PHI into logs or diagnostics.
The flat-table exporter keeps these annotations easy to inspect. It copies only whitelisted fields into stable rows, including normalized_text, coding fields, context axes, offsets, and section labels.
from openmed.clinical import resolve_span_context
from openmed.clinical.exporters import flatten_clinical_entities
context = resolve_span_context("pneumonia", ["possible"])
rows = flatten_clinical_entities(
[
{
"label": "condition",
"text": "pneumonia",
"context": context,
"start": 24,
"end": 33,
"metadata": {"section": "Assessment"},
}
]
)
print(rows[0])
For laboratory values, the shipped helpers parse simple numeric reference ranges and derive advisory abnormal flags. They do not convert units and do not replace the originating laboratory's own formal flags.
from openmed.clinical import derive_abnormal_flag, parse_reference_range
reference_range = parse_reference_range("135-145")
print(reference_range)
print(derive_abnormal_flag(130, reference_range))
print(derive_abnormal_flag(140, "135-145", explicit_flag="N"))
Medication sig, problem status, family-history, and relation outputs should feed the same record shape: normalized text or coding, assertion axes, section or experiencer metadata, offsets, and provenance. Keep raw clinical text out of audit artifacts unless the caller explicitly owns that PHI boundary.