Spaces:
Running
Running
| """Knowledge graph: click a code -> its parent/child ontology as nodes + edges. | |
| Every builder returns one uniform shape the frontend renders as a node-edge tree: | |
| {available, kind, title, subtitle, | |
| nodes: [{id, label, sub, tier, current}], edges: [[parentId, childId], ...], | |
| note} | |
| Graph relations use CIPHER, VA CDW, RxNorm, and LOINC data: | |
| - Diagnosis (ICD-9/10) & ICD-9-Proc: dotted-code family (category stem -> the | |
| codes sharing it), descriptions from the VA dictionary that backs code search. | |
| ICD diagnosis graphs also carry the code's phecode assignments (v1.2 and | |
| phecodeX) as annotation. | |
| - Procedures (ICD-10-PCS): 7-char positional codes -> the 3-char "table" family. | |
| - Procedures (CPT/HCPCS): CMS RBCS group (family, else subcategory) -> the | |
| index codes sharing it. | |
| - Medication: RxNorm ingredient(s) -> the drug -> sibling products. | |
| - Lab (LOINC): COMPONENT -> active LOINC terms sharing it (loinc_terms). | |
| VA-local LabChemTestSIDs enter that graph through the tiered lab mapping. | |
| Mapping lookups go through the shared MappingStore, which owns the usability | |
| rules. Routing resolves the clicked code to a CodeSystem once, then dispatches; | |
| the resolution order is load-bearing (PCS and ICD-9-Proc types also contain | |
| "ICD"/"CPT" substrings) and is pinned by tests. Codes without supported | |
| relations return ``available: false``. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from collections import defaultdict | |
| from enum import Enum | |
| from pathlib import Path | |
| from . import paths | |
| from . import provenance | |
| from .mappings import MappingStore, get_store | |
| from .rxnav import RxNavClient | |
| MEMBER_CAP = 26 # sibling codes shown per family (windowed around the clicked code; rest -> "+N more") | |
| NDC_NODE_CAP = 4 # NDC package codes drawn on a medication graph; rest -> "+N more" | |
| class CodeSystem(Enum): | |
| PCS = "pcs" | |
| ICD9_PROC = "icd9_proc" | |
| CPT = "cpt" | |
| ICD = "icd" | |
| LOINC = "loinc" | |
| VA_LAB = "va_lab" | |
| MEDICATION = "medication" | |
| def _dotted_cat(code: str) -> str: | |
| """Category stem for a dotted code (E11.9 -> E11, 345.01 -> 345).""" | |
| return code.split(".")[0].strip() | |
| def _pcs_cat(code: str) -> str: | |
| """ICD-10-PCS 'table' = first 3 positional chars (0SGK44Z -> 0SG).""" | |
| return code[:3] | |
| def _load_jsonl(path: Path): | |
| if path.exists(): | |
| with path.open(encoding="utf-8") as handle: | |
| for line in handle: | |
| if line.strip(): | |
| yield json.loads(line) | |
| class KnowledgeGraph: | |
| def __init__(self, diagnosis_records=None, procedure_records=None, | |
| rxnav_client: RxNavClient | None = None, lab_loinc_records=None, | |
| loinc_records=None, store: MappingStore | None = None): | |
| if store is None: | |
| store = (MappingStore(lab_loinc_records=lab_loinc_records) | |
| if lab_loinc_records is not None else get_store()) | |
| self.store = store | |
| self.rxnav = rxnav_client or RxNavClient() | |
| # -- lab (LOINC): term table + analyte(component) -> codes ------- | |
| self.loinc: dict[str, dict] = {} | |
| self.loinc_by_comp: dict[str, list[str]] = defaultdict(list) | |
| for r in (loinc_records if loinc_records is not None else _load_jsonl(paths.LOINC_TERMS)): | |
| self.loinc[r["loinc"]] = r | |
| self.loinc_by_comp[r["component"].lower()].append(r["loinc"]) | |
| # -- ICD diagnosis (dotted) -------------------------------------- | |
| self.icd, self.icd_cat = self._index_codes( | |
| diagnosis_records, | |
| keep="ICD", cat_fn=_dotted_cat) | |
| # -- procedures: ICD-10-PCS (3-char) + ICD-9-Proc (dotted) ------- | |
| self.pcs, self.pcs_cat = {}, defaultdict(list) | |
| self.icd9p, self.icd9p_cat = {}, defaultdict(list) | |
| # -- procedures: CPT/HCPCS grouped by CMS RBCS ------------------- | |
| self.cpt, self.cpt_group = {}, defaultdict(list) | |
| for r in procedure_records: | |
| ct = str(r.get("code_type", "")) | |
| desc = r.get("description", "") | |
| for code in str(r.get("code", "")).split(","): | |
| code = code.strip() | |
| if not code: | |
| continue | |
| if "PCS" in ct and code not in self.pcs: | |
| self.pcs[code] = {"code_type": "ICD-10-PCS", "description": desc} | |
| self.pcs_cat[_pcs_cat(code)].append(code) | |
| elif "ICD-9" in ct and "." in code and code not in self.icd9p: | |
| self.icd9p[code] = {"code_type": "ICD-9-Proc", "description": desc} | |
| self.icd9p_cat[_dotted_cat(code)].append(code) | |
| elif "CPT" in ct and code not in self.cpt: | |
| group = self._rbcs_group(code) | |
| if group: | |
| self.cpt[code] = {"code_type": "CPT", "description": desc, | |
| "group": group} | |
| self.cpt_group[group].append(code) | |
| self._routes = { | |
| CodeSystem.PCS: self._route_pcs, | |
| CodeSystem.ICD9_PROC: self._route_icd9_proc, | |
| CodeSystem.CPT: self._route_cpt, | |
| CodeSystem.ICD: self._route_icd, | |
| CodeSystem.LOINC: self._route_loinc, | |
| CodeSystem.VA_LAB: self._route_va_lab, | |
| CodeSystem.MEDICATION: self._route_medication, | |
| } | |
| def _rbcs_group(self, code: str) -> str | None: | |
| """Grouping label for a CPT/HCPCS code: RBCS family, else subcategory.""" | |
| entry = self.store.rbcs_validated_for(code.upper()) | |
| if not entry: | |
| return None | |
| return entry.get("family") or entry.get("subcategory") or None | |
| def _index_codes(records, keep: str, cat_fn): | |
| table: dict[str, dict] = {} | |
| buckets: dict[str, list[str]] = defaultdict(list) | |
| for r in records: | |
| ct = str(r.get("code_type", "")) | |
| if keep not in ct: | |
| continue | |
| desc = r.get("description", "") | |
| for code in str(r.get("code", "")).split(","): # split merged rows | |
| code = code.strip() | |
| if code and code not in table: | |
| table[code] = {"code_type": ct, "description": desc} | |
| buckets[cat_fn(code)].append(code) | |
| return table, buckets | |
| # -- routing ----------------------------------------------------------- | |
| def code_system(self, code: str, code_type: str | None = None) -> CodeSystem: | |
| """Which family a clicked code belongs to. Order is load-bearing: | |
| PCS and ICD-9-Proc must resolve before the bare "CPT"/"ICD" substring | |
| tests, and an embedding-matched CPT code wins regardless of the | |
| declared type (it has no other graph). MEDICATION is also the terminal | |
| bucket for unknowns; its handler ends with the no-hierarchy answer.""" | |
| ct = (code_type or "").upper() | |
| first = code.split(",")[0].strip() | |
| if "PCS" in ct or (not ct and code in self.pcs): | |
| return CodeSystem.PCS | |
| if ("ICD" in ct and "PROC" in ct) or ("ICD-9" in ct and code in self.icd9p): | |
| return CodeSystem.ICD9_PROC | |
| if "CPT" in ct or (not ct and first.upper() in self.cpt) \ | |
| or self.store.has_cpt_embedding(first.upper()): | |
| return CodeSystem.CPT | |
| if "ICD" in ct or (not ct and (code in self.icd or _dotted_cat(code) in self.icd_cat)): | |
| return CodeSystem.ICD | |
| if "LOINC" in ct or first in self.loinc: | |
| return CodeSystem.LOINC | |
| if "LABCHEM" in ct or "VA LAB" in ct or ( | |
| not ct and self.store.has_lab_sid(code.split(","))): | |
| return CodeSystem.VA_LAB | |
| return CodeSystem.MEDICATION | |
| def neighbors(self, code: str, code_type: str | None = None, | |
| drug_name: str | None = None, cap: int | None = None) -> dict: | |
| """cap: sibling window size per family; None = MEMBER_CAP, 0 = no cap.""" | |
| code = (code or "").strip() | |
| if not code: | |
| return self._none(code, "No code given.") | |
| cap = MEMBER_CAP if cap is None else max(0, cap) | |
| return self._routes[self.code_system(code, code_type)](code, code_type, drug_name, cap) | |
| def _route_pcs(self, code: str, code_type: str | None, drug_name: str | None, cap: int) -> dict: | |
| return self._family(code.split(",")[0].strip(), self.pcs, self.pcs_cat, _pcs_cat, | |
| "procedure", "ICD-10-PCS procedure table — codes sharing this 3-character root.", cap) | |
| def _route_icd9_proc(self, code: str, code_type: str | None, drug_name: str | None, cap: int) -> dict: | |
| return self._family(code.split(",")[0].strip(), self.icd9p, self.icd9p_cat, _dotted_cat, | |
| "procedure", "ICD-9 procedure family — codes in this category.", cap) | |
| def _route_cpt(self, code: str, code_type: str | None, drug_name: str | None, cap: int) -> dict: | |
| return self._rbcs_family(code.split(",")[0].strip().upper(), cap) | |
| def _route_icd(self, code: str, code_type: str | None, drug_name: str | None, cap: int) -> dict: | |
| graph = self._family(code.split(",")[0].strip(), self.icd, self.icd_cat, _dotted_cat, | |
| "icd", "Diagnosis code family — the codes in this ICD category.", cap) | |
| self._attach_phecodes(graph, code.split(",")[0].strip(), (code_type or "").upper()) | |
| return graph | |
| def _route_loinc(self, code: str, code_type: str | None, drug_name: str | None, cap: int) -> dict: | |
| return self._loinc(code.split(",")[0].strip(), cap) | |
| def _route_va_lab(self, code: str, code_type: str | None, drug_name: str | None, cap: int) -> dict: | |
| return self._local_lab(code, drug_name, cap) | |
| def _route_medication(self, code: str, code_type: str | None, drug_name: str | None, cap: int) -> dict: | |
| # The NDC and VA drug indexes pack every package/local code of a product | |
| # into one comma-joined field (the longest is 52,506 characters), so the | |
| # first entry is the one to resolve — the rest are the same product. | |
| first = code.split(",")[0].strip() | |
| mapping = self.store.ingredients_for(first) | |
| if mapping: | |
| return self._med(first, mapping, cap=cap) | |
| if self.store.ingredients_by_name(code) or self.store.ingredients_by_rxcui(code): | |
| return self._med(code, cap=cap) | |
| if self.store.ingredients_by_name(first) or self.store.ingredients_by_rxcui(first): | |
| return self._med(first, cap=cap) | |
| if self.rxnav.supports(code, code_type): | |
| mapping = self.rxnav.resolve(code, code_type, drug_name) | |
| if mapping: | |
| # Display the code that actually resolved, never the whole | |
| # packed field, and record what it is so the drug node names | |
| # its own vocabulary. | |
| up = (code_type or "").upper() | |
| kind = ("ndc" if mapping.get("matched_ndc") | |
| else "rxcui" if "RXNORM" in up else None) | |
| return self._med(mapping.get("matched_ndc") or first, | |
| {**mapping, "kind": kind}, cap=cap) | |
| return self._none( | |
| code, | |
| f"No unique RxNorm match found for {drug_name or code}.", | |
| "medication", | |
| ) | |
| return self._none(code, "No hierarchy is available for this code.") | |
| # -- dotted / positional code family (nodes + edges) ------------------- | |
| def _family(self, code, table, buckets, cat_fn, kind, subtitle, cap: int = MEMBER_CAP) -> dict: | |
| cat = cat_fn(code) | |
| members = sorted(buckets.get(cat, [])) | |
| if not members: | |
| return self._none(code, f"No {kind} family found for {code}.", kind) | |
| sel = self._window(members, code, cap) # siblings around the clicked code | |
| root_desc = (table.get(cat) or {}).get("description") | |
| nodes = [{"id": cat, "label": cat, "tier": 0, "current": cat == code, "path": True, | |
| "sub": root_desc or f"{table.get(code, {}).get('code_type', 'ICD')} category {cat}"}] | |
| edges, path = [], [cat] | |
| for c in sel: | |
| if c == cat: | |
| continue | |
| is_cur = c == code | |
| nodes.append({"id": c, "label": c, "sub": table[c]["description"], "tier": 1, | |
| "current": is_cur, "path": is_cur, | |
| "nav": {"code": c, "code_type": table[c]["code_type"]}}) | |
| edges.append([cat, c]) | |
| if is_cur: | |
| path.append(c) | |
| if len(members) > len(sel): | |
| nodes.append({"id": f"more:{cat}", "label": f"+{len(members) - len(sel)} more", | |
| "sub": "", "tier": 1, "more": True}) | |
| edges.append([cat, f"more:{cat}"]) | |
| return {"available": True, "kind": kind, "code": code, "title": code, "subtitle": subtitle, | |
| "nodes": nodes, "edges": edges, "path": path, "note": None} | |
| def _window(items: list[str], focus: str, cap: int) -> list[str]: | |
| """Up to `cap` items centred on `focus`; cap 0 = no windowing.""" | |
| if cap <= 0 or len(items) <= cap: | |
| return items | |
| if focus in items: | |
| idx = items.index(focus) | |
| start = max(0, min(idx - cap // 2, len(items) - cap)) | |
| return items[start:start + cap] | |
| return items[:cap] | |
| def _attach_phecodes(self, graph: dict, code: str, code_type: str) -> None: | |
| """Annotate an ICD family graph with the code's phecode assignments.""" | |
| if not graph.get("available"): | |
| return | |
| versions = [v.strip().upper() for v in code_type.split("|") if v.strip()] | |
| entry = self.store.phecodes_for(code, versions or ["ICD10", "ICD9"]) | |
| if entry: | |
| graph["phecodes"] = {**entry, "provenance": provenance.phecode_line(entry)} | |
| # -- CPT/HCPCS: CMS RBCS group -> index codes sharing it --------------- | |
| def _rbcs_family(self, code: str, cap: int = MEMBER_CAP) -> dict: | |
| entry = self.cpt.get(code) | |
| derived_entry = None if entry else self.store.cpt_embedding_for(code) | |
| if entry: | |
| rbcs = self.store.rbcs_validated_for(code) or {} | |
| group = entry["group"] | |
| sub = " · ".join(x for x in ["RBCS " + rbcs.get("category", ""), | |
| rbcs.get("subcategory", "")] if x.strip()) | |
| mapping = rbcs | |
| elif derived_entry: | |
| group = derived_entry["group"] | |
| sub = ("RBCS " + derived_entry.get("category", "")).strip() | |
| mapping = derived_entry | |
| else: | |
| return self._none(code, f"No RBCS group is available for {code}.", "procedure") | |
| members = sorted(self.cpt_group.get(group, []) or [code]) | |
| sel = self._window(members, code, cap) | |
| group_id = "rbcs:" + group | |
| nodes = [{"id": group_id, "label": group, "tier": 0, "current": False, | |
| "path": True, "sub": sub}] | |
| edges, path = [], [group_id] | |
| if derived_entry and code not in sel: | |
| # The clicked code inherited this group by embedding match; it is | |
| # not a validated member, so it joins the display explicitly. | |
| sel = [code] + (sel[:max(0, cap - 1)] if cap > 0 else sel) | |
| for c in sel: | |
| is_cur = c == code | |
| desc = (self.cpt.get(c) or derived_entry or {}).get("description", "") | |
| nodes.append({"id": c, "label": c, "sub": desc, | |
| "tier": 1, "current": is_cur, "path": is_cur, | |
| "nav": {"code": c, "code_type": "CPT"}}) | |
| edges.append([group_id, c]) | |
| if is_cur: | |
| path.append(c) | |
| if len(members) > len(sel): | |
| nodes.append({"id": "more:" + group_id, "label": f"+{len(members) - len(sel)} more", | |
| "sub": "", "tier": 1, "more": True}) | |
| edges.append([group_id, "more:" + group_id]) | |
| # Say how much of the family is on screen. The window is centred on the | |
| # clicked code, so two codes in one family legitimately show different | |
| # neighbours; without the count that looks like the group is unstable. | |
| shown = (f"Showing {len(sel)} of {len(members)}. " | |
| if len(members) > len(sel) else "") | |
| return {"available": True, "kind": "procedure", "code": code, "title": code, | |
| "subtitle": f"CPT/HCPCS codes sharing the RBCS group: {group}. " | |
| f"{shown}Codes in one family share this name.", | |
| "nodes": nodes, "edges": edges, "path": path, | |
| "derived": bool(derived_entry), | |
| "match_tier": "embedding_match" if derived_entry else None, | |
| "note": "Grouped by the CMS Restructured BETOS Classification System.", | |
| "source": provenance.line(mapping.get("source", "cms_rbcs_ry2025"), | |
| derived=bool(derived_entry), | |
| match=mapping.get("match"))} | |
| def _local_lab(self, code: str, display_name: str | None = None, cap: int = MEMBER_CAP) -> dict: | |
| """Resolve VA LabChemTestSID mappings before drawing the LOINC graph.""" | |
| entries = self.store.loinc_for(code.split(",")) | |
| if not entries: | |
| return self._none( | |
| code, | |
| "No LOINC mapping found for this VA lab code.", | |
| "lab", | |
| ) | |
| targets = {entry["loinc"] for entry in entries} | |
| if len(targets) != 1: | |
| return self._none( | |
| code, | |
| "These VA lab codes map to multiple LOINC terms.", | |
| "lab", | |
| ) | |
| entry = entries[0] | |
| loinc = entry["loinc"] | |
| graph = self._loinc(loinc, cap) | |
| if not graph.get("available"): | |
| return graph | |
| local_label = display_name or entry.get("lab_name") or code | |
| graph.update({ | |
| "title": f"{local_label} → LOINC {loinc}", | |
| "derived": bool(entry.get("derived")), | |
| "match_tier": entry.get("match"), | |
| "source": provenance.line(entry.get("source"), | |
| derived=bool(entry.get("derived")), | |
| match=entry.get("match"), | |
| target=entry.get("loinc_version", "LOINC")), | |
| "mapped_from": { | |
| "system": "VA Lab List (LabChemTestSID)", | |
| "code": code, | |
| "name": local_label, | |
| }, | |
| "mapped_loinc": loinc, | |
| "note": ( | |
| f"VA Lab List {code} → LOINC {loinc}. " | |
| "Related terms share its LOINC COMPONENT." | |
| ), | |
| }) | |
| return graph | |
| # -- LOINC lab: COMPONENT -> active terms sharing that component ------- | |
| def _loinc(self, code, cap: int = MEMBER_CAP) -> dict: | |
| term = self.loinc.get(code) | |
| if not term: | |
| return self._none(code, f"{code} is not in the LOINC table.", "lab") | |
| comp = term["component"] | |
| members = sorted(self.loinc_by_comp.get(comp.lower(), [code])) | |
| sel = self._window(members, code, cap) | |
| comp_id = "comp:" + comp | |
| nodes = [{"id": comp_id, "label": comp, "tier": 0, "current": False, "path": True, | |
| "sub": ("LOINC component · " + term.get("class", "")).strip(" ·")}] | |
| edges, path = [], [comp_id] | |
| for c in sel: | |
| t = self.loinc.get(c, {}) | |
| sub = " · ".join(x for x in [t.get("system"), t.get("name")] if x) | |
| is_cur = c == code | |
| nodes.append({"id": c, "label": c, "sub": sub, "tier": 1, "current": is_cur, | |
| "path": is_cur, "nav": {"code": c, "code_type": "LOINC"}}) | |
| edges.append([comp_id, c]) | |
| if is_cur: | |
| path.append(c) | |
| if len(members) > len(sel): | |
| nodes.append({"id": "more:" + comp_id, "label": f"+{len(members) - len(sel)} more", | |
| "sub": "", "tier": 1, "more": True}) | |
| edges.append([comp_id, "more:" + comp_id]) | |
| return {"available": True, "kind": "lab", "code": code, "title": code, | |
| "subtitle": f"Active LOINC terms sharing COMPONENT: {comp}.", | |
| "nodes": nodes, "edges": edges, "path": path, | |
| "note": "Grouped by LOINC COMPONENT.", | |
| "source": provenance.line("loinc_2_82")} | |
| # -- medication: ingredient(s) -> drug -> sibling products ------------ | |
| def _med(self, code, mapping=None, cap: int | None = None) -> dict: | |
| e = (mapping or self.store.ingredients_by_name(code) | |
| or self.store.ingredients_by_rxcui(code)) | |
| if not e: | |
| return self._none(code, f"No RxNorm ingredient mapping for {code}.", "medication") | |
| label = e.get("name") or code | |
| ings = e.get("ingredients", []) | |
| if not ings: | |
| return self._none(code, f"No RxNorm ingredient is mapped for {code}.", "medication") | |
| nodes, edges, seen = [], [], set() | |
| drug_id = "drug:" + str(code) | |
| path = [drug_id] | |
| for ing in ings: | |
| iid = "ing:" + str(ing.get("rxcui") or ing["name"]) | |
| # The RXCUI is the portable half of this graph and the only part a | |
| # reader can take elsewhere, so it travels as its own field rather | |
| # than staying encoded in the node id. Absent when the ingredient | |
| # was matched by name and carries no RXCUI of its own. | |
| nodes.append({"id": iid, "label": ing["name"], "sub": "RxNorm ingredient", | |
| "code": ing.get("rxcui"), "code_label": "RXCUI", | |
| "tier": 0, "current": False, "path": True}) | |
| edges.append([iid, drug_id]) | |
| path.append(iid) | |
| # The drug node names the vocabulary of the code it displays. NDC and | |
| # RXCUI keyed mappings were labelled VA Drug List ID before, which | |
| # asserted a vocabulary the code is not in. A name-keyed mapping has | |
| # no code to show, so the chip is left off entirely. | |
| kind = e.get("kind") | |
| code_label = {"ndc": "NDC", "rxcui": "RXCUI", | |
| "drug_name": None}.get(kind, "VA Drug List ID") | |
| nodes.append({"id": drug_id, "label": label, "sub": "this medication", | |
| "code": str(code) if code_label else None, | |
| "code_label": code_label, | |
| "tier": 1, "current": True, "path": True}) | |
| n_prod = 0 | |
| for ing in ings: | |
| iid = "ing:" + str(ing.get("rxcui") or ing["name"]) | |
| for prod in (e.get("child_drugs", {}) or {}).get(ing["name"], []): | |
| if prod.upper() == label.upper() or prod in seen: | |
| continue | |
| seen.add(prod) | |
| pid = "prod:" + prod | |
| nodes.append({"id": pid, "label": prod, "sub": "", "tier": 2, "current": False}) | |
| edges.append([iid, pid]) | |
| n_prod += 1 | |
| # NDC package codes of this product. The mapping's own packages win; | |
| # a mapping that carries none falls back to the NDCs RxNorm lists for | |
| # the matched concept. The displayed code is excluded by digit | |
| # comparison, since keys are dashed and stored packages are 11 digit | |
| # strings. cap follows the family convention: 0 draws them all. | |
| digits = lambda s: "".join(ch for ch in str(s) if ch.isdigit()) | |
| shown_digits = digits(code) | |
| ndcs = [n for n in (e.get("ndcs") or []) if digits(n) != shown_digits] | |
| if not ndcs: | |
| rxcuis = e.get("matched_rxcuis") or [] | |
| if rxcuis: | |
| ndcs = [n for n in self.rxnav.ndcs_for_rxcui(str(rxcuis[0])) | |
| if digits(n) != shown_digits] | |
| # One NDC hub in the middle tier, package codes hanging off it: bare | |
| # 11-digit strings interleaved with product names read as noise, and | |
| # the hub also gives the codes a labelled parent the way ingredients | |
| # have. No `code` on the hub, so Select all never collects it. | |
| limit = len(ndcs) if cap == 0 else NDC_NODE_CAP | |
| if ndcs: | |
| n_pkg = len(ndcs) | |
| nodes.append({"id": "ndcgrp", "label": "NDC", | |
| "sub": f"{n_pkg} package code{'' if n_pkg == 1 else 's'}", | |
| "vocab": "NDC", "tier": 1, "current": False}) | |
| edges.append([drug_id, "ndcgrp"]) | |
| for n in ndcs[:limit]: | |
| nid = f"ndc:{n}" | |
| nodes.append({"id": nid, "label": n, "sub": "NDC package code", | |
| "code": n, "code_label": "NDC", | |
| "vocab": "NDC", "tier": 2, "current": False}) | |
| edges.append(["ndcgrp", nid]) | |
| if len(ndcs) > limit: | |
| nodes.append({"id": "more:ndc", "label": f"+{len(ndcs) - limit} more", | |
| "sub": "", "tier": 2, "more": True}) | |
| edges.append(["ndcgrp", "more:ndc"]) | |
| source = provenance.line(e.get("source"), derived=bool(e.get("derived")), | |
| match=e.get("match")) | |
| total_products = e.get("related_product_count", n_prod) | |
| shown_products = e.get("related_products_shown", n_prod) | |
| note = f"{total_products} related products." | |
| if total_products > shown_products: | |
| note += f" Showing the first {shown_products}." | |
| return {"available": True, "kind": "medication", "code": code, | |
| "title": label, "subtitle": "RxNorm ingredient(s) → this drug → related products and NDC packages.", | |
| "nodes": nodes, "edges": edges, "path": path, | |
| "derived": bool(e.get("derived")), | |
| "match_tier": e.get("match") if e.get("derived") else None, | |
| "note": (note if n_prod else None), "source": source, | |
| "mapped_rxcuis": e.get("matched_rxcuis", [])} | |
| def _none(code, reason, kind=None) -> dict: | |
| return {"available": False, "code": code, "kind": kind, "reason": reason} | |