Unify UMLS semantic-type to Biolink-class mappings into one registry#804
Open
gaurav wants to merge 5 commits into
Open
Unify UMLS semantic-type to Biolink-class mappings into one registry#804gaurav wants to merge 5 commits into
gaurav wants to merge 5 commits into
Conversation
Move src/datahandlers/umls.py to src/datahandlers/umls/__init__.py with no content changes, so that UMLS semantic-type logic can live in its own module (src/datahandlers/umls/semantic_types.py, added next). All existing `from src.datahandlers import umls` imports keep working unchanged. The rest of __init__.py should be split into focused subfiles later; tracked in #802. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce src/datahandlers/umls/semantic_types.py as the single source of truth for mapping UMLS semantic types to Biolink classes, keyed by the UMLS semantic-type tree number (the canonical key across MeSH/UMLS partitioning): - SEMANTIC_NETWORK: the full 127-entry UMLS Semantic Network (TUI <-> tree number <-> name), hardcoded so the module is usable offline, with helpers to translate between TUIs and tree numbers. - UMLS_TYPE_MAP: one UMLSTypeAssignment per tree number recording the Biolink class Babel assigns, the owning compendium, an optional proposed_biolink_type (what we argue the Biolink Model itself should map the type to) and tracking issue. Validated at import (one owner per tree number, known categories constants, issue required for every disagreement). - category_map_for(): derives each compendium's partition map. - umls_tree_number_to_biolink_type()/resolve_biolink_types(): resolve types for the leftover catch-all, registry-first with the Biolink Model as fallback. Add biolink:Phenomenon and biolink:ClinicalFinding to src/categories.py for use as proposed mappings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the seven hardcoded UMLS semantic-type maps in createcompendia with calls to umls.semantic_types.category_map_for(); per-compendium blocklists stay local to each module (they are exclusion logic, not type assignments). leftover_umls now resolves residual CUIs via resolve_biolink_types() (registry first, Biolink Model fallback) instead of an inline nested function. This is behavior-preserving: the derived partition maps are byte-identical to the previous hardcoded maps. The registry records three tracked disagreements with the Biolink Model (#111 Neoplastic Process, #569 Finding and Laboratory or Test Result) via proposed_biolink_type, but none are applied yet, so build output is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tests/datahandlers/test_umls_semantic_types.py: - unit: registry reproduces the legacy per-compendium partition maps (golden snapshot), TUI<->tree-number translation, the leftover resolver prefers the registry over the Biolink Model, and validation rejects malformed entries. - network: each tracked disagreement (proposed_biolink_type) fails or xfails once the live Biolink Model adopts it, signalling the override is redundant. tests/pipeline/test_umls.py: - pipeline: SEMANTIC_NETWORK must still match the live MRSTY.RRF (drift detection at UMLS upgrade time). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add docs/UMLSSemanticTypes.md (registry shape, how to change an assignment with the no-CUI-moves check, the disagreement -> track -> retire workflow), a "UMLS semantic-type mappings" subsection in CLAUDE.md, and entries in tests/README.md for the new test files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gaurav
added a commit
that referenced
this pull request
Jun 4, 2026
…erage report (#816) ## Context Several issues report that "leftover" UMLS concepts get the wrong Biolink type ([#569](#569), umbrella [#410](#410), [#579](#579)). The correct long-term fix is in the Biolink Model, but it's hard to predict how a Biolink change lands in real Babel data. PR [#804](#804) is a comprehensive UMLS consolidation that's too large to review safely before the next release. This is the small, reviewable alternative: keep the existing leftover-UMLS flow but correct the typing with declarative override tables, guard them with a test, and add a coverage report. ## What changed ### Typing overrides (`src/createcompendia/leftover_umls.py`) Two declarative tables at the top of the module replace the inline `if`-chain: - **`STY_OVERRIDES: dict[str, str | None]`** — overrides the Biolink type bmt assigns to a single UMLS semantic type (`None` = deliberately reject). Each entry cites its motivating issue: - `T033` "Finding" → `biolink:Phenomenon` (#569; no STY mapping in biolink 4.4.0, so these were dropped as unmapped). - `T034` "Laboratory or Test Result" → `biolink:ClinicalFinding` (#569; bmt maps it to the broader `biolink:Phenomenon`). - `T058` "Health Care Activity" → `biolink:ClinicalIntervention` (#90; bmt assigns the generic `biolink:Activity`). - `T045` "Genetic Function" → `biolink:BiologicalProcess` (#421; no STY mapping). - `T021` "Fully Formed Anatomical Structure" → `biolink:GrossAnatomicalStructure` (#421; no STY mapping). - `T120` "Chemical Viewed Functionally" → `biolink:ChemicalEntity` (#421; no STY mapping). - `T122` "Biomedical or Dental Material" → `biolink:ChemicalEntity` (#421; no STY mapping). - `T168` "Food" → `biolink:Food` (#421; no STY mapping). With `T121` "Pharmacologic Substance" this resolves to `biolink:Food` via the `{Drug, Food} → Food` entry in `TYPE_COMBO_OVERRIDES`. - `T090`/`T091`/`T097` (occupation/discipline) → `biolink:PopulationOfIndividualOrganisms` as a placeholder (#817; ~8,881 CUIs previously dropped). Imprecise pending a proper Biolink fix. - **`TYPE_COMBO_OVERRIDES: dict[frozenset[str], str]`** — disambiguates a concept that resolves to multiple Biolink types (migrated verbatim from the inline `if`-chain). The resolution logic now classifies each semantic type as **mapped / rejected / unmapped** and reports the skip reason accordingly (`NO_UMLS_TYPE`, `REJECTED`, `MULTIPLE_UMLS_TYPES`). The conservative behavior is unchanged: a concept with any unmapped semantic type is still skipped entirely. bmt lookups are now memoized per TUI. `tui_to_biolink_type()` deliberately does **not** apply the overrides — it returns the raw Biolink answer so the test can compare against it. ### Drift test (`tests/createcompendia/test_leftover_umls.py`, `network`-marked) Records the current Biolink `STY:<code>` mapping for each override in `RECORDED_STY_BASELINE` and, on each run: - **hard-fails** when the live mapping diverges from the recorded baseline (Biolink changed → re-review the override), and - **warns** when the live mapping has come to equal the override (Biolink now agrees → override is redundant). Also checks that every override has a recorded baseline and that every type named in `TYPE_COMBO_OVERRIDES` is a real class in the pinned model. Run it when bumping `biolink_version`. ### Coverage report (consolidated under `babel_outputs/reports/umls/`) The rule's `report` output moved from `reports/umls.txt` to `reports/umls/log.txt`, and all UMLS reports now live in that directory. `log.txt` is the complete per-CURIE record; the CSVs are aggregate counts plus up to 5 sample `CURIE=label`s per bucket: - `compendium-coverage.csv` — per input compendium: total UMLS CURIEs and single-UMLS-clique count. - `types-coverage.csv` — per Biolink type of the leftover cliques: count + samples. - `unmapped-types.csv` — per semantic type that was unmapped or rejected: status, affected CUI count, samples. - `multi-type-curies.csv` — CURIEs still resolving to multiple Biolink types after `TYPE_COMBO_OVERRIDES`: the type combo, count, samples. - `tui-sty.tsv` — raw STY→semantic-type-name dump (was `reports/umls-types.tsv`). ### Supporting changes - **New constants** in `src/categories.py`: `CLINICAL_FINDING`, `CLINICAL_INTERVENTION`, `COHORT`, `PHENOMENON`, `POPULATION_OF_INDIVIDUAL_ORGANISMS`. - **Docs** — a Leftover UMLS section in `CLAUDE.md` and a new `docs/sources/UMLS/Leftover.md` (indexed in `docs/sources/README.md`), covering the override tables, the drift test, and the counts-vs-samples design of the CSVs. ## Issues **Closed by the overrides here** (they fire for concepts that fall through to the leftover sweep): Closes #90 Closes #421 **Related #410 children not addressed here** — the mis-typing originates outside the leftover-UMLS path (in `diseasephenotype.py`, EFO/NCIT typing, or LitCoin NER), so an `STY_OVERRIDES` entry can't reach those concepts: #111, #255, #269, #324, #392. **Handled separately:** - **#239** (toxic waste, `T131`) — already fixed upstream: `STY:T131` now maps to `biolink:ChemicalEntity` in biolink 4.4.0. No override needed. - **#817** — `T090`/`T091`/`T097` mapped to `biolink:PopulationOfIndividualOrganisms` as a placeholder; a proper Biolink fix is tracked there. - **#569** (the `T033`/`T034` seed) — closed by the stacked follow-up **#818**, not here. Most T033/T034 CUIs are still claimed upstream by `diseasephenotype.py` (as `PhenotypicFeature`) and never reach the leftover sweep; #818 stops that so these overrides can take effect. ## Testing - `ruff`, `snakefmt`, `rumdl` clean; unit + network tests pass (the drift test confirms the live biolink 4.4.0 baseline for every override). - Synthetic smoke test confirms overrides apply, unmapped/rejected types are reported, already-claimed CUIs are skipped, and all CSVs are correct. - **Not yet run against a full build** — @gaurav will validate override effects and the new `reports/umls/*.csv` in the next full pipeline run. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Babel assigns a Biolink Model class to every UMLS concept from its UMLS semantic type, but the logic was scattered: seven hardcoded tree-number maps across
createcompendia/*.py, plus a separate Biolink-Model lookup inleftover_umls.py. This PR consolidates all of it into one tree-number-keyed registry,src/datahandlers/umls/semantic_types.py, and adds a mechanism to record (and test) the places where Babel deliberately disagrees with the Biolink Model.This is the UMLS slice of #735 (consolidate per-vocabulary tree-partition registries so filters don't overlap). Scope here is UMLS only and behavior-preserving — see "Deliberately out of scope" below.
Changes
src/datahandlers/umls.py->src/datahandlers/umls/__init__.py(no content change; all imports unchanged). Tracking issue to split the rest of__init__.pyinto subfiles: Reorganize src/datahandlers/umls into subfiles #802.semantic_types.py):SEMANTIC_NETWORK— full 127-entry UMLS Semantic Network (TUI <-> tree number <-> name), hardcoded for offline use, with translation helpers.UMLS_TYPE_MAP— oneUMLSTypeAssignmentper tree number: the Biolink class Babel assigns, the owning compendium, an optionalproposed_biolink_type+ trackingissuefor disagreements with the Biolink Model. Validated at import.category_map_for()derives each partition map;umls_tree_number_to_biolink_type()/resolve_biolink_types()power the leftover catch-all (registry-first, Biolink Model fallback).category_map-stylewrite_umls_ids()sites andleftover_umls.pynow derive from the registry. Per-compendium blocklists stay local (exclusion logic, not type assignments).gene.py(bespoke MRCONSO path) and the RxNorm path are intentionally left alone.unit); a network test that fails/xfails once the Biolink Model adopts a proposed mapping; a pipeline drift test that checksSEMANTIC_NETWORKagainst the liveMRSTY.RRF.docs/UMLSSemanticTypes.md, a CLAUDE.md subsection, andtests/README.mdentries.Behavior impact
The partition maps are byte-identical to the previous hardcoded maps (verified by the golden-snapshot test and a direct before/after diff on the live
MRSTY.RRF— same CUI set, same counts). Three disagreements are recorded but not applied (proposed_biolink_typeonly), so build output is unchanged.One intended difference in
leftover_umls: residual CUIs whose semantic type Babel partitions are now typed from the registry (registry-first) rather than from the Biolink Model — e.g. a protein-tree CUI that was blocklisted into the leftover compendium now resolves to Protein instead of Biolink's Polypeptide. This never moves a CUI between compendia; it only makes leftover typing consistent with the partition decisions.Deliberately out of scope (follow-ups)
B2.2.1.1.4.1is mapped to BiologicalProcess by the process partition; noted in Apply and quantify the tracked UMLS -> Biolink semantic-type disagreements #803 for confirmation/closure.Testing
Closes part of #735 (UMLS portion).
🤖 Generated with Claude Code