Mapping OpenMed spans to SNOMED CT
Ground clinical concept spans that OpenMed extracts — disorders, findings,
procedures, body structures, substances — to SNOMED CT, the comprehensive
clinical reference terminology. The atom is the SCTID (a SNOMED CT concept
identifier), organized into a description-logic hierarchy you can query with
ECL (Expression Constraint Language).
Hard licensing boundary — read first. SNOMED CT is license-restricted.
OpenMed and this skill never bundle, ship, cache, or redistribute any
SNOMED CT content. All mapping happens out-of-process against a terminology
server the user supplies and is licensed for — their own Ontoserver,
Snowstorm, the NLM's UTS/UMLS FHIR endpoint, or a national release
server. SNOMED International requires an Affiliate License (free in member
territories like the US via the NLM; check your country). Your code receives a
base URL + credentials from the user; it must work with any compliant
FHIR terminology server and store nothing but the returned codes.
When to use
- You need rich, hierarchy-aware clinical codes (more granular than ICD-10) for
problems, procedures, or body sites.
- You want to translate an existing code (ICD-10-CM, local code) to SNOMED CT
via a
ConceptMap/$translate.
- You need subsumption/ECL queries ("is this a descendant of Diabetes
mellitus?") for cohorting or decision support.
For billing codes use coding-icd10; for drugs normalizing-rxnorm; for labs
mapping-loinc. SNOMED CT is the clinical-meaning layer.
Quick start (user-supplied FHIR terminology server)
Configuration is injected, never hardcoded. The operations are standard FHIR R4.
import os, requests
# Provided by the USER — their licensed server. Nothing bundled.
TX = os.environ["FHIR_TX_URL"] # e.g. https://snowstorm.example.org/fhir
TOKEN = os.environ.get("FHIR_TX_TOKEN") # if the server requires auth
SNOMED = "http://snomed.info/sct"
HDRS = {"Accept": "application/fhir+json"}
if TOKEN:
HDRS["Authorization"] = f"Bearer {TOKEN}"
def lookup(code: str) -> dict:
"""$lookup: fully specified name + properties for an SCTID."""
r = requests.get(f"{TX}/CodeSystem/$lookup",
params={"system": SNOMED, "code": code},
headers=HDRS, timeout=15)
r.raise_for_status()
return r.json()
def find_concepts(text: str, ecl: str = "<<404684003", count: int = 10):
"""Text search constrained by ECL (default: descendants of Clinical finding)."""
vs = f"{SNOMED}?fhir_vs=ecl/{ecl}"
r = requests.get(f"{TX}/ValueSet/$expand",
params={"url": vs, "filter": text, "count": count},
headers=HDRS, timeout=20)
r.raise_for_status()
return r.json().get("expansion", {}).get("contains", [])
def translate(code: str, source_system: str, conceptmap_url: str):
"""$translate an existing code to SNOMED CT via a ConceptMap."""
r = requests.get(f"{TX}/ConceptMap/$translate",
params={"url": conceptmap_url, "system": source_system,
"code": code, "targetsystem": SNOMED},
headers=HDRS, timeout=20)
r.raise_for_status()
return r.json()
# ECL examples: 64572001=disease, 71388002=procedure, 123037004=body structure
print(find_concepts("type 2 diabetes", ecl="<<64572001"))
Workflow
- Extract spans with OpenMed (Disease, Anatomy, Pharmaceutical models).
- Pick a semantic constraint (ECL) from the OpenMed label so you search the
right hierarchy: disorder span →
<<64572001; anatomy span → <<123037004;
substance/drug → <<105590001; procedure → <<71388002.
- Search with
ValueSet/$expand?filter=<span> under that ECL.
- Rank & disambiguate by display match and confidence; prefer the most
specific concept whose meaning is fully entailed by the text (do not over-code).
- Validate with
$validate-code; $lookup to capture the FSN and any
needed properties.
- Translate instead of searching when you already hold an ICD-10/local code
and the user's server has the relevant
ConceptMap.
- Emit
{system: "http://snomed.info/sct", code, display} — the SCTID plus
the OpenMed source offsets for traceability.
Hand-off from OpenMed
openmed.analyze_text(..., output_format="dict") returns entities, each a dict
with text, label, confidence, start, end. Route each label to an ECL
hierarchy and map out-of-process:
import openmed
note = "Assessment: type 2 diabetes mellitus with diabetic nephropathy."
result = openmed.analyze_text(
note,
model_name="disease_detection_superclinical", # Disease category
output_format="dict",
)
ECL_FOR_LABEL = {
"DISEASE": "<<64572001", # | Disease |
"CONDITION": "<<64572001",
"PATHOLOGY": "<<64572001",
"ANATOMY": "<<123037004", # | Body structure |
"ORGAN": "<<123037004",
}
for ent in result["entities"]:
ecl = ECL_FOR_LABEL.get(ent["label"], "<<404684003") # fallback: Clinical finding
candidates = find_concepts(ent["text"], ecl=ecl, count=5)
print(ent["text"], ent["start"], ent["end"], "->",
[(c["code"], c["display"]) for c in candidates[:3]])
Carry OpenMed's start/end offsets next to each SCTID so every code is
auditable back to its span. Persist codes and offsets only — never the raw note,
and never a local copy of SNOMED content.
Edge cases & gotchas
- Never bundle SNOMED CT. Do not vendor a release, embed an export, or cache
descriptions to disk for reuse. If you find yourself shipping SNOMED data, stop
— the design must call the user's licensed server live, out-of-process.
- Affiliate licensing. Confirm the user holds (or their territory grants) a
SNOMED International Affiliate License. In the US it is free via the NLM/UMLS;
elsewhere it varies. Surface this requirement; do not assume entitlement.
- Pre- vs post-coordination. Some clinical meanings need a post-coordinated
expression (e.g. finding + body site + severity). Prefer a single
pre-coordinated concept when one exists; only post-coordinate when your server
and downstream systems support SNOMED CT expressions.
- Edition/version drift. SCTIDs are stable but content differs across
editions (International vs US vs UK) and monthly releases. Record the edition
the server reports; do not mix codes across editions silently.
- Negation/uncertainty stays in OpenMed. A span "no evidence of pneumonia"
must not be coded as present pneumonia. Resolve assertion/negation with
OpenMed's clinical-context layer before mapping.
- Don't over-specify. Map to the concept actually supported by the text;
inventing severity or laterality the note never stated is a coding error.
- Local-first. OpenMed NER runs on-device; only de-identified concept
strings reach the terminology server. No PHI over the wire.
Standards & references
1---2name: mapping-to-snomed3description: Maps clinical concept spans extracted by OpenMed to SNOMED CT concepts through a USER-SUPPLIED terminology server (the user's own Ontoserver, Snowstorm, or UMLS/UTS), never a bundled vocabulary. Use when the user wants to code findings, disorders, procedures, body structures, or substances to SNOMED CT, run an ECL query, translate via a ConceptMap, or resolve a span to a concept id with FHIR $lookup/$translate/$validate-code. Trigger keywords: SNOMED CT, SNOMED concept id, ECL, ConceptMap, $translate, $lookup, Ontoserver, Snowstorm, SCTID, post-coordination, terminology server. Pairs after OpenMed NER: consume Disease/Anatomy/Pharmaceutical entities from openmed.analyze_text and map each span out-of-process. SNOMED CT is license-restricted — it is NEVER bundled; the user calls their own affiliate-licensed server.4license: Apache-2.05---67# Mapping OpenMed spans to SNOMED CT89Ground clinical concept spans that OpenMed extracts — disorders, findings,10procedures, body structures, substances — to **SNOMED CT**, the comprehensive11clinical reference terminology. The atom is the **SCTID** (a SNOMED CT concept12identifier), organized into a description-logic hierarchy you can query with13**ECL** (Expression Constraint Language).1415> **Hard licensing boundary — read first.** SNOMED CT is **license-restricted**.16> OpenMed and this skill **never bundle, ship, cache, or redistribute** any17> SNOMED CT content. All mapping happens **out-of-process against a terminology18> server the user supplies and is licensed for** — their own **Ontoserver**,19> **Snowstorm**, the NLM's **UTS/UMLS** FHIR endpoint, or a national release20> server. SNOMED International requires an Affiliate License (free in member21> territories like the US via the NLM; check your country). Your code receives a22> **base URL + credentials from the user**; it must work with *any* compliant23> FHIR terminology server and store nothing but the returned codes.2425## When to use2627- You need rich, hierarchy-aware clinical codes (more granular than ICD-10) for28 problems, procedures, or body sites.29- You want to **translate** an existing code (ICD-10-CM, local code) to SNOMED CT30 via a `ConceptMap`/`$translate`.31- You need subsumption/ECL queries ("is this a descendant of *Diabetes32 mellitus*?") for cohorting or decision support.3334For billing codes use `coding-icd10`; for drugs `normalizing-rxnorm`; for labs35`mapping-loinc`. SNOMED CT is the clinical-meaning layer.3637## Quick start (user-supplied FHIR terminology server)3839Configuration is injected, never hardcoded. The operations are standard FHIR R4.4041```python42import os, requests4344# Provided by the USER — their licensed server. Nothing bundled.45TX = os.environ["FHIR_TX_URL"] # e.g. https://snowstorm.example.org/fhir46TOKEN = os.environ.get("FHIR_TX_TOKEN") # if the server requires auth47SNOMED = "http://snomed.info/sct"48HDRS = {"Accept": "application/fhir+json"}49if TOKEN:50 HDRS["Authorization"] = f"Bearer {TOKEN}"5152def lookup(code: str) -> dict:53 """$lookup: fully specified name + properties for an SCTID."""54 r = requests.get(f"{TX}/CodeSystem/$lookup",55 params={"system": SNOMED, "code": code},56 headers=HDRS, timeout=15)57 r.raise_for_status()58 return r.json()5960def find_concepts(text: str, ecl: str = "<<404684003", count: int = 10):61 """Text search constrained by ECL (default: descendants of Clinical finding)."""62 vs = f"{SNOMED}?fhir_vs=ecl/{ecl}"63 r = requests.get(f"{TX}/ValueSet/$expand",64 params={"url": vs, "filter": text, "count": count},65 headers=HDRS, timeout=20)66 r.raise_for_status()67 return r.json().get("expansion", {}).get("contains", [])6869def translate(code: str, source_system: str, conceptmap_url: str):70 """$translate an existing code to SNOMED CT via a ConceptMap."""71 r = requests.get(f"{TX}/ConceptMap/$translate",72 params={"url": conceptmap_url, "system": source_system,73 "code": code, "targetsystem": SNOMED},74 headers=HDRS, timeout=20)75 r.raise_for_status()76 return r.json()7778# ECL examples: 64572001=disease, 71388002=procedure, 123037004=body structure79print(find_concepts("type 2 diabetes", ecl="<<64572001"))80```8182## Workflow83841. **Extract** spans with OpenMed (Disease, Anatomy, Pharmaceutical models).852. **Pick a semantic constraint (ECL)** from the OpenMed label so you search the86 right hierarchy: disorder span → `<<64572001`; anatomy span → `<<123037004`;87 substance/drug → `<<105590001`; procedure → `<<71388002`.883. **Search** with `ValueSet/$expand?filter=<span>` under that ECL.894. **Rank & disambiguate** by display match and confidence; prefer the most90 specific concept whose meaning is fully entailed by the text (do not over-code).915. **Validate** with `$validate-code`; `$lookup` to capture the FSN and any92 needed properties.936. **Translate** instead of searching when you already hold an ICD-10/local code94 and the user's server has the relevant `ConceptMap`.957. **Emit** `{system: "http://snomed.info/sct", code, display}` — the SCTID plus96 the OpenMed source offsets for traceability.9798## Hand-off from OpenMed99100`openmed.analyze_text(..., output_format="dict")` returns `entities`, each a dict101with `text`, `label`, `confidence`, `start`, `end`. Route each label to an ECL102hierarchy and map out-of-process:103104```python105import openmed106107note = "Assessment: type 2 diabetes mellitus with diabetic nephropathy."108result = openmed.analyze_text(109 note,110 model_name="disease_detection_superclinical", # Disease category111 output_format="dict",112)113114ECL_FOR_LABEL = {115 "DISEASE": "<<64572001", # | Disease |116 "CONDITION": "<<64572001",117 "PATHOLOGY": "<<64572001",118 "ANATOMY": "<<123037004", # | Body structure |119 "ORGAN": "<<123037004",120}121122for ent in result["entities"]:123 ecl = ECL_FOR_LABEL.get(ent["label"], "<<404684003") # fallback: Clinical finding124 candidates = find_concepts(ent["text"], ecl=ecl, count=5)125 print(ent["text"], ent["start"], ent["end"], "->",126 [(c["code"], c["display"]) for c in candidates[:3]])127```128129Carry OpenMed's `start`/`end` offsets next to each SCTID so every code is130auditable back to its span. Persist codes and offsets only — never the raw note,131and never a local copy of SNOMED content.132133## Edge cases & gotchas134135- **Never bundle SNOMED CT.** Do not vendor a release, embed an export, or cache136 descriptions to disk for reuse. If you find yourself shipping SNOMED data, stop137 — the design must call the user's licensed server live, out-of-process.138- **Affiliate licensing.** Confirm the user holds (or their territory grants) a139 SNOMED International Affiliate License. In the US it is free via the NLM/UMLS;140 elsewhere it varies. Surface this requirement; do not assume entitlement.141- **Pre- vs post-coordination.** Some clinical meanings need a post-coordinated142 expression (e.g. finding + body site + severity). Prefer a single143 pre-coordinated concept when one exists; only post-coordinate when your server144 and downstream systems support SNOMED CT expressions.145- **Edition/version drift.** SCTIDs are stable but content differs across146 editions (International vs US vs UK) and monthly releases. Record the edition147 the server reports; do not mix codes across editions silently.148- **Negation/uncertainty stays in OpenMed.** A span "no evidence of pneumonia"149 must not be coded as present pneumonia. Resolve assertion/negation with150 OpenMed's clinical-context layer *before* mapping.151- **Don't over-specify.** Map to the concept actually supported by the text;152 inventing severity or laterality the note never stated is a coding error.153- **Local-first.** OpenMed NER runs on-device; only de-identified concept154 strings reach the terminology server. No PHI over the wire.155156## Standards & references157158- SNOMED CT (SNOMED International): https://www.snomed.org/159- SNOMED CT licensing & Affiliate program: https://www.snomed.org/get-snomed160- NLM SNOMED CT (US, via UMLS/UTS): https://www.nlm.nih.gov/healthit/snomedct/index.html161- Expression Constraint Language (ECL): https://confluence.ihtsdotools.org/display/DOCECL162- FHIR `$translate` / `$lookup` / `$validate-code`:163 https://hl7.org/fhir/terminology-service.html164- Snowstorm (reference terminology server): https://github.com/IHTSDO/snowstorm165- Ontoserver: https://ontoserver.csiro.au/