Resolving clinical context
NER finds that a condition was mentioned; it does not tell you whether the
patient has it. "Patient denies chest pain," "history of MI," and "rule out
PE" all surface entities that must not be recorded as active, present
findings. OpenMed's openmed.clinical ConText layer assigns three deterministic
axes to each span — negation, temporality, uncertainty — turning raw
mentions into clinically faithful assertions before they reach a problem list or
FHIR Condition.
When to use
- Immediately after
extracting-clinical-entities, before grounding,
problem-list building, or analytics.
- The user asks for assertion status, negation handling, "is this affirmed?",
family-history vs. patient, historical vs. active, or hedged/uncertain findings.
- You are about to map entities to FHIR
verificationStatus /clinicalStatus
and need the upstream signal.
Quick start
import openmed
from openmed.clinical import (
resolve_span_context, assert_context_axes,
NEGATED, HISTORICAL, HYPOTHETICAL, UNCERTAIN,
)
note = "Patient denies chest pain. History of MI. Concern for PE; rule out DVT."
# 1) Extract entities (registry key, HF id, or local path).
ents = openmed.analyze_text(note, model_name="disease_detection_superclinical",
output_format="dict")
# 2) Assign ConText axes per entity. Pass the span text plus a window of cues.
for e in ents:
span = e["word"] # entity surface text
window = note # full sentence/note as modifier context
ctx = resolve_span_context(span, window)
print(span, "->", ctx.negation, ctx.temporality, ctx.certainty)
# "chest pain" -> negated recent certain (do NOT record as present)
# "MI" -> affirmed historical certain (past, not active)
# "PE" -> affirmed recent uncertain (hedged; flag, don't drop)
resolve_span_context returns a ClinicalContextResult(negation, temporality, certainty). For a downstream-grounding-shaped record use assert_context_axes,
which returns a ClinicalAssertion with a .to_dict() that omits unset axes.
Workflow
- Get entities and their context window. From
analyze_text, take each
entity's surface text and the surrounding sentence (or the whole short note)
as the modifier window. The ConText helpers accept a string, a span mapping
with a text-like key, or any object exposing .text, plus optional
modifier_hits.
- Resolve negation with
resolve_negation(span, window) → AFFIRMED or
NEGATED. It uses a NegEx/ConText cue lexicon ("denies," "no evidence of,"
"without," "negative for"), masks pseudo-negation ("not ruled out,"
"cannot be excluded") so those don't refute the concept, and counts true cues
with even/odd parity so double-negation is deterministic.
- Resolve temporality with
resolve_temporality(span, window) → RECENT
(default), HISTORICAL ("history of," "h/o," "s/p," "resolved," "PMH"), or
HYPOTHETICAL ("if," "should," "in case of"). A conditional span is treated
as hypothetical even if a historical cue is also present.
- Resolve uncertainty with
resolve_uncertainty(span, window) → CERTAIN
or UNCERTAIN ("concern for," "suspicious for," "rule out," "probable,"
"vs," "r/o"). Uncertain spans are flagged, not dropped.
- Apply the axes downstream. Drop or refute
NEGATED spans; route
HISTORICAL to inactive/resolved status; do not record HYPOTHETICAL spans
as present; mark UNCERTAIN spans provisional. Use the constants, not string
literals, so a vocabulary change doesn't silently break comparisons.
Hand-off to / from OpenMed
- From
extracting-clinical-entities: this skill consumes analyze_text
Disease/Finding entities. Without context resolution, every mention — including
negated and historical ones — would be (wrongly) treated as present.
- OpenMed calls:
from openmed.clinical import resolve_negation, resolve_temporality, resolve_uncertainty, resolve_span_context, assert_context_axes, ClinicalAssertion and the NEGATED/AFFIRMED,
HISTORICAL/RECENT/HYPOTHETICAL, CERTAIN/UNCERTAIN constants.
- To
reconciling-problem-lists: feed each entity plus its
ClinicalContextResult so active vs. resolved vs. historical is decided
correctly and negated mentions are excluded.
- To FHIR grounding:
negation=negated → verificationStatus=refuted;
temporality=historical → inactive/resolved clinicalStatus;
certainty=uncertain → verificationStatus=provisional. The layer emits the
axis; it does not build the FHIR record.
Edge cases & gotchas
- Window scoping matters. Pass a sentence-sized window, not the whole
document — a negation cue three sentences away should not flip an affirmed
finding. Segment first (
segmenting-clinical-sections) for long notes.
- Pseudo-negation is handled, double-check anyway. "Cannot exclude PE" is
affirmed-but-uncertain, not negated. The negation layer masks these cues; the
uncertainty layer is what flags the hedge.
- Experiencer (family history) is a separate axis. These helpers cover
negation/temporality/uncertainty; "mother with breast cancer" being about a
relative is the experiencer axis and is out of scope here — handle it before
attributing the finding to the patient.
- Deterministic, not ML. ConText is a rule layer: fast, transparent,
auditable — but cue-list bound. Novel phrasings may need lexicon tuning; it
will not infer assertion from semantics the way a model might.
- Advisory only. Outputs are annotations for review and downstream grounding,
not autonomous clinical decisions.
Standards & references
1---2name: resolving-clinical-context3description: Assign negation, temporality, and uncertainty (the ConText axes) to clinical entities extracted by OpenMed, so "denies chest pain" is not counted as chest pain and "history of MI" is not counted as an active MI. Use after NER when the user needs assertion status, negation detection, family-history / hypothetical / historical flags, or ConText/NegEx-style classification before grounding entities to FHIR or a problem list. Covers openmed.clinical.resolve_negation / resolve_temporality / resolve_uncertainty / resolve_span_context / assert_context_axes, ClinicalAssertion, and the AFFIRMED/NEGATED, RECENT/HISTORICAL/HYPOTHETICAL, CERTAIN/UNCERTAIN constants. Pairs after extracting-clinical-entities.4license: Apache-2.05---67# Resolving clinical context89NER finds *that* a condition was mentioned; it does not tell you whether the10patient **has** it. "Patient denies chest pain," "history of MI," and "rule out11PE" all surface entities that must **not** be recorded as active, present12findings. OpenMed's `openmed.clinical` ConText layer assigns three deterministic13axes to each span — **negation**, **temporality**, **uncertainty** — turning raw14mentions into clinically faithful assertions before they reach a problem list or15FHIR Condition.1617## When to use1819- Immediately after `extracting-clinical-entities`, before grounding,20 problem-list building, or analytics.21- The user asks for assertion status, negation handling, "is this affirmed?",22 family-history vs. patient, historical vs. active, or hedged/uncertain findings.23- You are about to map entities to FHIR `verificationStatus` /`clinicalStatus`24 and need the upstream signal.2526## Quick start2728```python29import openmed30from openmed.clinical import (31 resolve_span_context, assert_context_axes,32 NEGATED, HISTORICAL, HYPOTHETICAL, UNCERTAIN,33)3435note = "Patient denies chest pain. History of MI. Concern for PE; rule out DVT."3637# 1) Extract entities (registry key, HF id, or local path).38ents = openmed.analyze_text(note, model_name="disease_detection_superclinical",39 output_format="dict")4041# 2) Assign ConText axes per entity. Pass the span text plus a window of cues.42for e in ents:43 span = e["word"] # entity surface text44 window = note # full sentence/note as modifier context45 ctx = resolve_span_context(span, window)46 print(span, "->", ctx.negation, ctx.temporality, ctx.certainty)4748# "chest pain" -> negated recent certain (do NOT record as present)49# "MI" -> affirmed historical certain (past, not active)50# "PE" -> affirmed recent uncertain (hedged; flag, don't drop)51```5253`resolve_span_context` returns a `ClinicalContextResult(negation, temporality,54certainty)`. For a downstream-grounding-shaped record use `assert_context_axes`,55which returns a `ClinicalAssertion` with a `.to_dict()` that omits unset axes.5657## Workflow58591. **Get entities and their context window.** From `analyze_text`, take each60 entity's surface text and the surrounding sentence (or the whole short note)61 as the modifier window. The ConText helpers accept a string, a span mapping62 with a `text`-like key, or any object exposing `.text`, plus optional63 `modifier_hits`.642. **Resolve negation** with `resolve_negation(span, window)` → `AFFIRMED` or65 `NEGATED`. It uses a NegEx/ConText cue lexicon ("denies," "no evidence of,"66 "without," "negative for"), masks **pseudo-negation** ("not ruled out,"67 "cannot be excluded") so those don't refute the concept, and counts true cues68 with even/odd parity so double-negation is deterministic.693. **Resolve temporality** with `resolve_temporality(span, window)` → `RECENT`70 (default), `HISTORICAL` ("history of," "h/o," "s/p," "resolved," "PMH"), or71 `HYPOTHETICAL` ("if," "should," "in case of"). A conditional span is treated72 as hypothetical even if a historical cue is also present.734. **Resolve uncertainty** with `resolve_uncertainty(span, window)` → `CERTAIN`74 or `UNCERTAIN` ("concern for," "suspicious for," "rule out," "probable,"75 "vs," "r/o"). Uncertain spans are **flagged, not dropped**.765. **Apply the axes downstream.** Drop or refute `NEGATED` spans; route77 `HISTORICAL` to inactive/resolved status; do not record `HYPOTHETICAL` spans78 as present; mark `UNCERTAIN` spans provisional. Use the constants, not string79 literals, so a vocabulary change doesn't silently break comparisons.8081## Hand-off to / from OpenMed8283- **From** `extracting-clinical-entities`: this skill consumes `analyze_text`84 Disease/Finding entities. Without context resolution, every mention — including85 negated and historical ones — would be (wrongly) treated as present.86- **OpenMed calls:** `from openmed.clinical import resolve_negation,87 resolve_temporality, resolve_uncertainty, resolve_span_context,88 assert_context_axes, ClinicalAssertion` and the `NEGATED/AFFIRMED`,89 `HISTORICAL/RECENT/HYPOTHETICAL`, `CERTAIN/UNCERTAIN` constants.90- **To** `reconciling-problem-lists`: feed each entity plus its91 `ClinicalContextResult` so active vs. resolved vs. historical is decided92 correctly and negated mentions are excluded.93- **To FHIR grounding:** `negation=negated` → `verificationStatus=refuted`;94 `temporality=historical` → inactive/resolved `clinicalStatus`;95 `certainty=uncertain` → `verificationStatus=provisional`. The layer emits the96 axis; it does not build the FHIR record.9798## Edge cases & gotchas99100- **Window scoping matters.** Pass a sentence-sized window, not the whole101 document — a negation cue three sentences away should not flip an affirmed102 finding. Segment first (`segmenting-clinical-sections`) for long notes.103- **Pseudo-negation is handled, double-check anyway.** "Cannot exclude PE" is104 affirmed-but-uncertain, not negated. The negation layer masks these cues; the105 uncertainty layer is what flags the hedge.106- **Experiencer (family history) is a separate axis.** These helpers cover107 negation/temporality/uncertainty; "mother with breast cancer" being about a108 relative is the experiencer axis and is out of scope here — handle it before109 attributing the finding to the patient.110- **Deterministic, not ML.** ConText is a rule layer: fast, transparent,111 auditable — but cue-list bound. Novel phrasings may need lexicon tuning; it112 will not infer assertion from semantics the way a model might.113- **Advisory only.** Outputs are annotations for review and downstream grounding,114 not autonomous clinical decisions.115116## Standards & references117118- Chapman et al., *NegEx* — A simple algorithm for negation in discharge119 summaries (2001): https://doi.org/10.1006/jbin.2001.1029120- Harkema et al., *ConText* — negation, experiencer, temporality, certainty121 (2009): https://doi.org/10.1016/j.jbi.2009.05.002122- HL7 FHIR R4 Condition — `clinicalStatus` / `verificationStatus`:123 https://hl7.org/fhir/R4/condition.html