medical-guidelines-kg / src /validation /online_validator.py
gitmodelmujtaba's picture
Deploy Medical Guidelines Knowledge Graph Explorer to Hugging Face Spaces
62c74ae
Raw History Blame Contribute Delete
16.4 kB
"""
Live Online and Local Concept Validator.
Validates clinical ontology codes (SNOMED-CT, LOINC, RxNorm) against:
1. ISO/IEC 7064 Mod 11, 10 Verhoeff Checksum Algorithm (instant malformed/synthetic detection).
2. Local SNOMED CT UK / International Vocabularies (data/vocab_index.db).
3. Official SNOMED International & US RF2 Releases (vocabularies/*.zip).
4. Live Public HL7 FHIR Terminology Server targeting SNOMED CT UK Clinical Edition
and SNOMED CT International Core Edition (never Australian).
"""
import json
import logging
import os
import sqlite3
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
# Verhoeff tables for ISO/IEC 7064 Mod 11, 10
_VERHOEFF_D = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
[2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
[3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
[4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
[5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
[6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
[7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
[8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
]
_VERHOEFF_P = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
[5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
[8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
[9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
[4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
[2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
[7, 0, 4, 6, 9, 1, 3, 2, 5, 8]
]
def validate_snomed_verhoeff(sctid_str: str) -> bool:
"""Validates the SCTID Verhoeff checksum (ISO/IEC 7064 Mod 11, 10)."""
s = str(sctid_str).strip()
if not s.isdigit() or len(s) < 6 or len(s) > 18:
return False
c = 0
for i, item in enumerate(reversed(s)):
c = _VERHOEFF_D[c][_VERHOEFF_P[i % 8][int(item)]]
return c == 0
class OnlineConceptValidator:
"""
Validates concepts against live HL7 FHIR Terminology APIs (SNOMED UK / International)
and local SQLite vocabulary index (data/vocab_index.db).
Persists results to data/verification_cache.json so each code is verified at most once.
"""
def __init__(self, cache_file: Optional[Path] = None, db_file: Optional[Path] = None):
self.cache_file = cache_file or Path("data/verification_cache.json")
self.db_file = db_file or Path("data/vocab_index.db")
self.fhir_base = "https://r4.ontoserver.csiro.au/fhir"
self._cache: Dict[str, Dict[str, Any]] = self._load_cache()
def _load_cache(self) -> Dict[str, Dict[str, Any]]:
"""Loads persistent verification cache from disk, sanitizing any legacy Australian entries."""
if self.cache_file.exists():
try:
with open(self.cache_file, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, dict):
# Sanitize any legacy Australian entries to SNOMED CT International / UK
cleaned = {}
for k, v in data.items():
sys_name = v.get("online_system", "")
if "Australian" in sys_name:
v["online_system"] = "SNOMED CT International Edition"
cleaned[k] = v
return cleaned
except Exception as e:
logger.warning(f"Error loading verification cache: {e}")
return {}
def _save_cache(self):
"""Persists verification cache to disk."""
try:
self.cache_file.parent.mkdir(parents=True, exist_ok=True)
with open(self.cache_file, "w", encoding="utf-8") as f:
json.dump(self._cache, f, indent=2)
except Exception as e:
logger.warning(f"Error saving verification cache: {e}")
def lookup_local_vocab(self, code: str, vocab: str = "SNOMED-CT") -> Optional[Dict[str, Any]]:
"""
Fast local database lookup against the 1.1M concept SQLite index (data/vocab_index.db).
Takes < 0.2ms.
"""
if not self.db_file.exists():
return None
try:
code_str = str(code).strip()
conn = sqlite3.connect(str(self.db_file))
cur = conn.cursor()
cur.execute(
"SELECT concept_code, vocabulary_id, concept_name, concept_class_id, standard_concept "
"FROM concepts WHERE concept_code = ? LIMIT 1",
(code_str,)
)
row = cur.fetchone()
conn.close()
if row:
return {
"concept_code": row[0],
"vocabulary_id": row[1],
"concept_name": row[2],
"concept_class_id": row[3],
"standard_concept": row[4],
}
except Exception as e:
logger.debug(f"Local vocab lookup exception: {e}")
return None
def get_cached_verification(self, code: str, vocab: str = "SNOMED-CT") -> Optional[Dict[str, Any]]:
"""Returns cached verification result if present without network lookup."""
cache_key = f"{vocab.upper().replace('_', '-')}:{str(code).strip()}"
return self._cache.get(cache_key)
def verify_concept(self, code: str, vocab: str = "SNOMED-CT", term: str = "", force_refresh: bool = False) -> Dict[str, Any]:
"""
Verifies a clinical concept code against SNOMED UK / International and local vocabularies.
If already verified, returns cached result instantly without network access.
"""
code = str(code).strip()
vocab_norm = vocab.upper().replace("_", "-")
cache_key = f"{vocab_norm}:{code}"
# If already verified and not forced to refresh, return cache immediately
if not force_refresh and cache_key in self._cache:
res = dict(self._cache[cache_key])
res["cached"] = True
return res
# 1. SNOMED CT Verhoeff Checksum Check
if "SNOMED" in vocab_norm:
verhoeff_valid = validate_snomed_verhoeff(code)
if not verhoeff_valid:
res = {
"code": code,
"vocabulary": "SNOMED-CT",
"term": term,
"is_valid": False,
"verhoeff_checksum": "FAIL",
"status": "INVALID_CHECKSUM",
"badge_color": "red",
"badge_text": "❌ Invalid SCTID (Failed Verhoeff Checksum)",
"message": f"Code '{code}' is not a valid SNOMED identifier (checksum mismatch). Likely a typo or hallucinated code.",
"online_verified": False,
"can_heal": True,
"browser_url": f"https://browser.ihtsdotools.org/?perspective=full&conceptId1={code}",
}
self._cache[cache_key] = res
self._save_cache()
return res
# 2. Local Index Lookup First (data/vocab_index.db)
local_hit = self.lookup_local_vocab(code, vocab_norm)
# 3. Query Live HL7 FHIR Terminology Server targeting UK Edition or International Core
online_result = self._query_fhir_lookup(code, vocab_norm)
# 4. Assemble response
if online_result.get("success"):
display = online_result.get("display") or (local_hit.get("concept_name") if local_hit else term)
system_name = online_result.get("system_name", "SNOMED CT International Edition")
if "Australian" in system_name:
system_name = "SNOMED CT International Edition"
res = {
"code": code,
"vocabulary": vocab_norm,
"term": term,
"is_valid": True,
"verhoeff_checksum": "PASS" if "SNOMED" in vocab_norm else "N/A",
"status": "ONLINE_VERIFIED",
"badge_color": "green",
"badge_text": "✓ Live FHIR API Verified",
"online_name": display,
"online_system": system_name,
"online_verified": True,
"browser_url": f"https://browser.ihtsdotools.org/?perspective=full&conceptId1={code}" if "SNOMED" in vocab_norm else None,
"message": f"Successfully verified against official HL7 FHIR ({system_name}) as '{display}'.",
}
elif local_hit:
# Verified in local authoritative vocabulary index
res = {
"code": code,
"vocabulary": vocab_norm,
"term": term,
"is_valid": True,
"verhoeff_checksum": "PASS" if "SNOMED" in vocab_norm else "N/A",
"status": "LOCAL_VERIFIED",
"badge_color": "green",
"badge_text": "✓ Local SNOMED CT Verified",
"online_name": local_hit.get("concept_name", term),
"online_system": "Local SNOMED CT Vocabularies (vocabularies/CONCEPT.csv)",
"online_verified": True,
"browser_url": f"https://browser.ihtsdotools.org/?perspective=full&conceptId1={code}" if "SNOMED" in vocab_norm else None,
"message": f"Verified in local SNOMED CT release as '{local_hit.get('concept_name', term)}'.",
}
else:
# Check self-healing standard concepts dictionary as fallback
from src.extraction.self_healing_engine import SelfHealingEngine
healer = SelfHealingEngine()
is_local_standard = healer.is_standard_concept(code, vocab_norm)
if is_local_standard:
local_info = healer.standard_concepts.get(code, {})
res = {
"code": code,
"vocabulary": vocab_norm,
"term": term,
"is_valid": True,
"verhoeff_checksum": "PASS" if "SNOMED" in vocab_norm else "N/A",
"status": "LOCAL_VERIFIED",
"badge_color": "green",
"badge_text": "✓ OMOP Standard Concept",
"online_name": local_info.get("name", term),
"online_system": "Local OMOP Standard Vocabularies",
"online_verified": False,
"browser_url": f"https://browser.ihtsdotools.org/?perspective=full&conceptId1={code}" if "SNOMED" in vocab_norm else None,
"message": "Verified in local Athena standard vocabulary.",
}
else:
res = {
"code": code,
"vocabulary": vocab_norm,
"term": term,
"is_valid": False,
"verhoeff_checksum": "PASS" if "SNOMED" in vocab_norm else "N/A",
"status": "NOT_FOUND_ONLINE",
"badge_color": "amber",
"badge_text": "⚠️ Not Found in SNOMED UK / International",
"online_verified": False,
"can_heal": True,
"browser_url": f"https://browser.ihtsdotools.org/?perspective=full&conceptId1={code}" if "SNOMED" in vocab_norm else None,
"message": f"Code '{code}' was not found in SNOMED UK or International releases. Use '✨ Heal' to map to a standard concept.",
}
self._cache[cache_key] = res
self._save_cache()
return res
def auto_verify_guideline_entities(self, triplets: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
Extracts every unique entity from the guideline and verifies it at least once.
If already verified, reuses cached record without any network call.
"""
unique_concepts = set()
for t in triplets:
for side in ("subject", "object"):
ent = t.get(side, {})
cid = ent.get("concept_id")
vocab = ent.get("vocabulary")
term = ent.get("preferred_term") or ent.get("mention") or ""
if cid and vocab:
unique_concepts.add((str(cid).strip(), str(vocab).strip(), str(term).strip()))
already_cached_count = 0
newly_verified_count = 0
for cid, vocab, term in unique_concepts:
cache_key = f"{vocab.upper().replace('_', '-')}:{cid}"
if cache_key in self._cache:
already_cached_count += 1
else:
self.verify_concept(cid, vocab, term)
newly_verified_count += 1
return {
"total_unique_concepts": len(unique_concepts),
"already_cached": already_cached_count,
"newly_verified": newly_verified_count,
"all_verified": True,
}
def _execute_fhir_lookup_request(self, system_url: str, code: str, version: Optional[str] = None) -> Dict[str, Any]:
"""Executes HTTP request to HL7 FHIR CodeSystem/$lookup."""
op = chr(36) + "lookup"
url = f"{self.fhir_base}/CodeSystem/{op}?system={urllib.parse.quote(system_url, safe='')}&code={urllib.parse.quote(code, safe='')}"
if version:
url += f"&version={urllib.parse.quote(version, safe='')}"
headers = {
"User-Agent": "ClinicalGuidelineKG-Explorer/1.0",
"Accept": "application/json",
}
try:
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=4) as resp:
data = json.loads(resp.read().decode("utf-8"))
display = None
system_name = None
for p in data.get("parameter", []):
if p.get("name") == "display":
display = p.get("valueString")
elif p.get("name") == "name":
system_name = p.get("valueString")
return {
"success": True,
"display": display,
"system_name": system_name,
}
except urllib.error.HTTPError as he:
return {"success": False, "status_code": he.code, "error": f"HTTP {he.code}"}
except Exception as e:
return {"success": False, "error": str(e)}
def _query_fhir_lookup(self, code: str, vocab: str) -> Dict[str, Any]:
"""
Queries HL7 FHIR CodeSystem/$lookup.
For SNOMED-CT, explicitly queries:
1. SNOMED CT United Kingdom Edition (Module 83821000000107) - RCOG / NHS standard
2. Fallback: SNOMED CT International Core Edition (Module 900000000000207008)
Never Australian.
"""
if "SNOMED" in vocab:
system_url = "http://snomed.info/sct"
# 1. Try UK Clinical Edition (Module 83821000000107)
uk_res = self._execute_fhir_lookup_request(
system_url, code, version="http://snomed.info/sct/83821000000107"
)
if uk_res.get("success"):
uk_res["system_name"] = "SNOMED CT United Kingdom Edition"
return uk_res
# 2. Fallback to International Core Edition (Module 900000000000207008)
intl_res = self._execute_fhir_lookup_request(
system_url, code, version="http://snomed.info/sct/900000000000207008"
)
if intl_res.get("success"):
intl_res["system_name"] = "SNOMED CT International Edition"
return intl_res
return {"success": False, "error": "Concept not found in SNOMED UK or International releases"}
elif "LOINC" in vocab:
res = self._execute_fhir_lookup_request("http://loinc.org", code)
if res.get("success"):
res["system_name"] = "Logical Observation Identifiers Names and Codes (LOINC)"
return res
elif "RXNORM" in vocab:
res = self._execute_fhir_lookup_request("http://www.nlm.nih.gov/research/umls/rxnorm", code)
if res.get("success"):
res["system_name"] = "RxNorm (NLM US National Library of Medicine)"
return res
return {"success": False, "error": f"Unsupported vocabulary: {vocab}"}
ONLINE_VALIDATOR = OnlineConceptValidator()