Spaces:
Runtime error
Runtime error
| """Rule-based pattern detection / differential-diagnosis hints. | |
| Looks at the pattern of regional z-scores (and asymmetry) and flags | |
| clinically recognized signatures. This is NOT a diagnosis engine — it | |
| surfaces pattern hints that a radiologist should consider. | |
| Each rule returns: | |
| { "pattern": short name, | |
| "confidence": low | moderate | high, | |
| "ddx_consider": list of possible diagnoses to rule in/out, | |
| "supporting_findings": list of (region, z) that triggered the rule } | |
| Patterns encoded (initial set — extensible): | |
| - Medial temporal atrophy (AD, TLE) | |
| - Ventriculomegaly + cortex OK (normal-pressure hydrocephalus / hydro) | |
| - Global GM atrophy, WM OK (primary GM degeneration) | |
| - Bilateral frontal reduction (frontotemporal pattern) | |
| - Asymmetric focal pattern (tumor / stroke / focal pathology) | |
| - Accelerated WM myelination (pediatric developmental anomaly) | |
| - Posterior atrophy dominant (posterior cortical atrophy) | |
| """ | |
| from __future__ import annotations | |
| from typing import Iterable | |
| def _find(regions: Iterable[dict], name_frag: str, | |
| min_abs_z: float = 0.0, | |
| direction: str | None = None) -> list[dict]: | |
| out = [] | |
| for r in regions: | |
| if name_frag.lower() not in r.get("name", "").lower(): | |
| continue | |
| z = r.get("z_score") | |
| if z is None: | |
| continue | |
| if abs(z) < min_abs_z: | |
| continue | |
| if direction == "low" and z >= 0: | |
| continue | |
| if direction == "high" and z <= 0: | |
| continue | |
| out.append(r) | |
| return out | |
| def detect_patterns(regions: list[dict], asymmetry: list[dict], | |
| tissue: dict) -> list[dict]: | |
| hits = [] | |
| # ---- 1. Medial temporal atrophy (AD, TLE) --------------------------- | |
| hipp = _find(regions, "hippocamp", min_abs_z=1.5, direction="low") | |
| phipp = _find(regions, "parahippocamp", min_abs_z=1.0, direction="low") | |
| if len(hipp) >= 1 and len(phipp) >= 1: | |
| bilateral = any(r["hemi"] == "L" for r in hipp) and \ | |
| any(r["hemi"] == "R" for r in hipp) | |
| conf = "high" if bilateral else "moderate" | |
| hits.append({ | |
| "pattern": "Medial temporal atrophy", | |
| "confidence": conf, | |
| "ddx_consider": ["Alzheimer's disease (early)", | |
| "Temporal lobe epilepsy (mesial sclerosis)", | |
| "Limbic encephalitis"], | |
| "supporting_findings": [f"{r['name']} z={r['z_score']:+.1f}" | |
| for r in hipp + phipp], | |
| }) | |
| # ---- 2. Ventriculomegaly with preserved cortex --------------------- | |
| vent = _find(regions, "lateral ventricle", min_abs_z=1.5, direction="high") | |
| cort = _find(regions, "cerebral cortex", direction="low") | |
| if vent and (not cort or max(abs(r["z_score"]) for r in cort) < 1.0): | |
| hits.append({ | |
| "pattern": "Ventriculomegaly with cortical preservation", | |
| "confidence": "moderate", | |
| "ddx_consider": ["Normal pressure hydrocephalus", | |
| "Obstructive hydrocephalus", | |
| "Ex-vacuo (if secondary to WM loss)"], | |
| "supporting_findings": [f"{r['name']} z={r['z_score']:+.1f}" | |
| for r in vent], | |
| }) | |
| # ---- 3. Global GM atrophy with WM preserved ------------------------ | |
| gm_lo = [r for r in regions | |
| if r.get("group") == "Cortical" | |
| and r.get("z_score") is not None | |
| and r["z_score"] < -1.0] | |
| wm = _find(regions, "cerebral white matter") | |
| wm_ok = all(abs(r.get("z_score", 0)) < 1.0 for r in wm) if wm else True | |
| if len(gm_lo) >= 10 and wm_ok: | |
| hits.append({ | |
| "pattern": "Diffuse grey matter volume loss (white matter preserved)", | |
| "confidence": "moderate", | |
| "ddx_consider": ["Primary GM degeneration", | |
| "Early neurodegenerative process"], | |
| "supporting_findings": [f"{len(gm_lo)} cortical regions with z<-1"], | |
| }) | |
| # ---- 4. Bilateral frontal reduction -------------------------------- | |
| front_lo = [r for r in regions | |
| if "frontal" in r.get("name", "").lower() | |
| and r.get("z_score") is not None | |
| and r["z_score"] < -1.5] | |
| if len(front_lo) >= 3: | |
| hits.append({ | |
| "pattern": "Frontal-predominant volume reduction", | |
| "confidence": "moderate" if len(front_lo) >= 5 else "low", | |
| "ddx_consider": ["Frontotemporal dementia (behavioral variant)", | |
| "Chronic traumatic encephalopathy", | |
| "Prefrontal developmental delay (pediatric)"], | |
| "supporting_findings": [f"{r['name']} z={r['z_score']:+.1f}" | |
| for r in front_lo[:5]], | |
| }) | |
| # ---- 5. Asymmetric focal pattern ----------------------------------- | |
| sig_asym = [a for a in asymmetry if a.get("significant") and | |
| abs(a.get("asymmetry_pct", 0)) >= 10.0] | |
| if len(sig_asym) >= 2: | |
| hits.append({ | |
| "pattern": "Asymmetric focal volume pattern", | |
| "confidence": "low" if len(sig_asym) < 4 else "moderate", | |
| "ddx_consider": ["Focal mass lesion (tumor)", | |
| "Chronic infarct", | |
| "Focal cortical dysplasia (if pediatric)", | |
| "Mesial temporal sclerosis (if medial temporal)"], | |
| "supporting_findings": [ | |
| f"{a['region']} AI {a['asymmetry_pct']:+.1f}%" | |
| for a in sig_asym[:5]], | |
| }) | |
| # ---- 6. Posterior-dominant atrophy --------------------------------- | |
| occ = [r for r in regions if r.get("lobe") == "Occipital" | |
| and (r.get("z_score") or 0) < -1.0] | |
| par = [r for r in regions if r.get("lobe") == "Parietal" | |
| and (r.get("z_score") or 0) < -1.0] | |
| front_ok = [r for r in regions if r.get("lobe") == "Frontal" | |
| and abs(r.get("z_score") or 0) < 1.0] | |
| if (len(occ) + len(par)) >= 4 and len(front_ok) >= 5: | |
| hits.append({ | |
| "pattern": "Posterior cortical volume loss", | |
| "confidence": "moderate", | |
| "ddx_consider": ["Posterior cortical atrophy (PCA)", | |
| "Lewy body disease variant", | |
| "Late-stage Alzheimer's"], | |
| "supporting_findings": [f"occipital regions: {len(occ)}, " | |
| f"parietal: {len(par)} with z<-1"], | |
| }) | |
| return hits | |