"""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