Cookbook 325: Offline Clinical Pediatric Ophthalmology Amblyopia, AAPOS ARF & Patching Engine

This cookbook details how to deploy a localized, containerized pediatric ophthalmology, strabismus, and vision screening decision-support engine for pediatric eye clinics, community vision screening programs, and optometry centers to ingest cycloplegic refractive errors, visual acuity ($VA$) measurements, corneal light reflex displacement measurements, and fixation preference telemetry, evaluate AAPOS 2021 Amblyopia Risk Factors ($\text{ARFs}$), quantify ocular alignment using the Hirschberg / Krimsky Reflex Geometry ($1\text{ mm} \approx 14 - 15\Delta\text{ Prism Diopters}$), classify amblyopia into Anisometropic, Strabismic, Isoametropic, and Deprivation Etiologies, stage severity (Mild, Moderate, Severe), and generate Pediatric Eye Disease Investigator Group ($\text{PEDIG}$)-compliant occlusion patching ($2\text{h/day}$ vs $6\text{h/day}$) and Weekend Atropine $1\%$ Pharmacological Penalization protocols with automated sound-eye reverse amblyopia safety sentinels according to AAPOS, American Academy of Ophthalmology ($\text{AAO}$), and American Academy of Pediatrics ($\text{AAP}$) guidelines without external cloud API reliance.


1. Clinical Background & Pediatric Vision Architecture

Amblyopia (“lazy eye”) is the leading cause of preventable monocular vision impairment in children, affecting $2 - 5\%$ of the population. It arises from abnormal visual experience during the critical period of visual cortex neurodevelopment ($0 - 7\text{ years}$):


2. Pipeline & Workflow Architecture

[Patient Telemetry: Age, Refraction OD/OS, VA OD/OS, Corneal Reflex mm, Media Opacity]
                                         │
                                         ▼
      [AAPOS 2021 ARF Classifier: Anisometropia, Isoametropia, Strabismus, Deprivation]
                                         │
                                         ▼
     [Hirschberg / Krimsky Alignment Engine: Prism Diopters & Tropia Quantification]
                                         │
                                         ▼
    [Amblyopia Staging: Mild (20/25-30) vs Moderate (20/40-80) vs Severe (20/100-400)]
                                         │
                                         ▼
    [PEDIG Protocol Engine: Optical Adaptation -> 2h Patching vs Weekend Atropine 1% vs 6h]
                                         │
                                         ▼
    [Sound-Eye Reverse Amblyopia Safety Sentinel & Age-Adjusted Follow-Up Gatekeeper]

3. Environment & Prerequisites

Install required scientific Python and pediatric ophthalmology modeling packages:

pip install numpy scipy pandas torch torchvision matplotlib

4. Complete Offline Python / PyTorch Implementation

"""
Cookbook 325: Offline Pediatric Ophthalmology Amblyopia, AAPOS ARF & Patching Engine
OpenPHR Clinical AI Working Group (https://openphr.org)
"""

import math
import numpy as np
import pandas as pd
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass

@dataclass
class PediatricOphthalmicTelemetry:
    patient_id: str
    age_months: int = 42 # 3.5 years old
    # Visual Acuity (Snellen Denominator: e.g. 20/20 -> 20; 20/60 -> 60)
    va_right_eye_od_denominator: int = 20 # 20/20 (Sound Eye)
    va_left_eye_os_denominator: int = 60 # 20/60 (Amblyopic Eye)
    # Cycloplegic Refraction Telemetry
    od_sphere_diopters: float = +1.50 # OD Sphere (D)
    od_cylinder_diopters: float = +0.50 # OD Cylinder (D)
    os_sphere_diopters: float = +4.50 # OS Sphere (D) (Hyperopic Anisometropia: 3.0 D diff)
    os_cylinder_diopters: float = +1.00 # OS Cylinder (D)
    # Ocular Alignment & Strabismus Telemetry
    hirschberg_corneal_reflex_displacement_mm: float = 2.5 # mm displacement
    displacement_direction: str = "Temporal" # "Temporal" (Esotropia), "Nasal" (Exotropia), "Central" (Orthophoria)
    fixation_preference_os: str = "Unsteady / Not Maintained" # "Central Steady Maintained", "Unsteady / Not Maintained"
    # Media Opacity & Structural Telemetry
    media_opacity_present: bool = False # Cataract, corneal scar, vitreous hemorrhage
    significant_ptosis_present: bool = False # Covering > 1 mm of pupil
    # Treatment History
    current_glasses_adaptation_duration_weeks: int = 18 # >= 16-18 weeks allows patching initiation

@dataclass
class AmblyopiaEvaluationReport:
    patient_id: str
    aapos_arf_status: str # "AAPOS Amblyopia Risk Factors Present", "No ARFs"
    arf_criteria_triggered: List[str]
    strabismus_quantification: str # e.g. "Left Esotropia ~35-37.5 Prism Diopters"
    amblyopia_etiology_and_severity: str # "Moderate Anisometropic & Strabismic Amblyopia (20/60 OS)"
    pedig_treatment_prescription: List[str]
    sound_eye_safety_sentinel: str
    clinical_aapos_aao_directive: str

class PediatricAmblyopiaDecisionEngine:
    """
    Offline clinical engine for AAPOS 2021 Amblyopia Risk Factor (ARF) evaluation,
    Hirschberg/Krimsky strabismus triangulation, and PEDIG occlusion/atropine dosing.
    """

    def evaluate_aapos_arfs(self, d: PediatricOphthalmicTelemetry) -> Tuple[str, List[str]]:
        arfs = []

        # 1. Anisometropia Calculation
        sphere_diff = abs(d.od_sphere_diopters - d.os_sphere_diopters)
        cyl_diff = abs(d.od_cylinder_diopters - d.os_cylinder_diopters)

        if sphere_diff >= 1.50:
            arfs.append(f"Anisometropia (Spherical): {sphere_diff:.2f} D inter-eye difference (AAPOS threshold >= 1.50 D).")
        if cyl_diff >= 1.50:
            arfs.append(f"Anisometropia (Astigmatic): {cyl_diff:.2f} D cylinder difference (AAPOS threshold >= 1.50 D).")

        # 2. Isoametropia (High Bilateral Refraction)
        if d.od_sphere_diopters >= 4.50 and d.os_sphere_diopters >= 4.50:
            arfs.append(f"Bilateral High Hyperopia: OD +{d.od_sphere_diopters:.2f} D / OS +{d.os_sphere_diopters:.2f} D (Threshold >= +4.50 D).")
        if d.od_cylinder_diopters >= 2.00 and d.os_cylinder_diopters >= 2.00:
            arfs.append(f"Bilateral High Astigmatism: OD +{d.od_cylinder_diopters:.2f} D / OS +{d.os_cylinder_diopters:.2f} D (Threshold >= +2.00 D).")

        # 3. Strabismus
        if d.displacement_direction != "Central" and d.hirschberg_corneal_reflex_displacement_mm > 0.5:
            arfs.append(f"Manifest Strabismus: {d.displacement_direction} displacement ({d.hirschberg_corneal_reflex_displacement_mm:.1f} mm).")

        # 4. Deprivation
        if d.media_opacity_present or d.significant_ptosis_present:
            arfs.append("Visual Axis Obstruction: Media opacity or ptosis covering pupillary axis.")

        status = "🚨 AAPOS Amblyopia Risk Factors Present (High Amblyopia Risk)" if len(arfs) > 0 else "No Significant AAPOS ARFs Detected"
        return status, arfs

    def quantify_strabismus(self, d: PediatricOphthalmicTelemetry) -> str:
        if d.displacement_direction == "Central" or d.hirschberg_corneal_reflex_displacement_mm <= 0.5:
            return "Orthophoria / Orthotropia (Normal central corneal light reflexes)."

        # 1 mm displacement approx 7 degrees approx 14 - 15 Prism Diopters (PD)
        pd_low = round(d.hirschberg_corneal_reflex_displacement_mm * 14.0, 1)
        pd_high = round(d.hirschberg_corneal_reflex_displacement_mm * 15.0, 1)

        tropia_type = "Esotropia (Inward Deviation)" if d.displacement_direction == "Temporal" else "Exotropia (Outward Deviation)"
        return f"{tropia_type}: {d.hirschberg_corneal_reflex_displacement_mm:.1f} mm displacement ≈ {pd_low} - {pd_high} Δ Prism Diopters."

    def stage_amblyopia(self, d: PediatricOphthalmicTelemetry) -> Tuple[str, str]:
        # Identify worse eye
        worse_va = max(d.va_right_eye_od_denominator, d.va_left_eye_os_denominator)
        better_va = min(d.va_right_eye_od_denominator, d.va_left_eye_os_denominator)
        affected_eye = "OS" if d.va_left_eye_os_denominator > d.va_right_eye_od_denominator else "OD"

        # Inter-eye difference (>= 2 lines difference defines amblyopia)
        if worse_va == better_va or (worse_va <= 25 and better_va <= 20):
            return "No Clinically Significant Amblyopia", "Symmetric normal visual acuity."

        # Severity staging
        if worse_va <= 30:
            severity = "Mild Amblyopia"
        elif 40 <= worse_va <= 80:
            severity = "Moderate Amblyopia"
        else:
            severity = "Severe Amblyopia"

        # Etiology determination
        has_aniso = abs(d.od_sphere_diopters - d.os_sphere_diopters) >= 1.50
        has_strab = d.displacement_direction != "Central" and d.hirschberg_corneal_reflex_displacement_mm > 0.5
        has_depriv = d.media_opacity_present or d.significant_ptosis_present

        etiologies = []
        if has_aniso: etiologies.append("Anisometropic")
        if has_strab: etiologies.append("Strabismic")
        if has_depriv: etiologies.append("Deprivation")
        if not etiologies: etiologies.append("Isoametropic / Unspecified")

        summary = f"{severity} ({'/'.join(etiologies)}) - 20/{worse_va} {affected_eye} vs 20/{better_va} Sound Eye"
        return severity, summary

    def generate_pedig_treatment_plan(self, severity: str, d: PediatricOphthalmicTelemetry) -> Tuple[List[str], str]:
        plan = []
        sound_eye = "OD (Right Eye)" if d.va_left_eye_os_denominator > d.va_right_eye_od_denominator else "OS (Left Eye)"
        amblyopic_eye = "OS (Left Eye)" if d.va_left_eye_os_denominator > d.va_right_eye_od_denominator else "OD (Right Eye)"

        # Step 1: Optical Correction
        plan.append(f"1. OPTICAL ADAPTATION: Prescribe full cycloplegic refraction spectacles (OD: +{d.od_sphere_diopters:.2f} +{d.od_cylinder_diopters:.2f}; OS: +{d.os_sphere_diopters:.2f} +{d.os_cylinder_diopters:.2f}). Wear full-time during all waking hours.")

        if d.current_glasses_adaptation_duration_weeks < 16:
            plan.append(f"   ℹ️ Refractive Adaptation Active ({d.current_glasses_adaptation_duration_weeks}/16 weeks). Re-evaluate visual acuity in 8-12 weeks before initiating active patching.")
            safety_interval = f"Re-evaluate in 8-12 weeks for optical adaptation."
            return plan, safety_interval

        # Step 2: Occlusion vs Penalization based on PEDIG protocols
        if severity == "Moderate Amblyopia":
            plan.append(f"2. FIRST-LINE OCCLUSION (PEDIG ATS 2A): Adhesive patch over sound {sound_eye} for 2 HOURS PER DAY.")
            plan.append("   • Prescribe 1 hour of active near visual engagement (drawing, reading, tracing, tablet apps) during patching.")
            plan.append(f"3. PHARMACOLOGICAL PENALIZATION ALTERNATIVE (PEDIG ATS 1): Atropine 1% ophthalmic drops — 1 drop in sound {sound_eye} on SATURDAY AND SUNDAY ONLY (Proven equal efficacy to 2h daily patching).")
        elif severity == "Severe Amblyopia":
            plan.append(f"2. HIGH-INTENSITY OCCLUSION (PEDIG ATS 2B): Adhesive patch over sound {sound_eye} for 6 HOURS PER DAY.")
            plan.append("   • Engage in near activities for at least 1-2 hours daily during patching.")
            plan.append("   • Full-day patching is not required and does not improve visual acuity gains compared to 6 hours.")
        else: # Mild
            plan.append(f"2. MILD AMBLYOPIA: Full-time spectacle wear with close observation or part-time patching (1-2 hours/day) if no progression after 16 weeks.")

        # Safety Sentinel: Reverse Amblyopia Monitoring
        # Rule of Thumb: Follow-up interval = 1 week per year of child's age
        child_age_years = max(int(d.age_months / 12), 1)
        followup_weeks = min(child_age_years + 1, 6)
        safety_sentinel = f"REVERSE AMBLYOPIA MONITORING: Re-assess visual acuity in sound {sound_eye} in {followup_weeks} WEEKS (1 week per year of age). If sound eye visual acuity drops >= 2 lines, suspend patching immediately."

        return plan, safety_sentinel

    def evaluate_case(self, data: PediatricOphthalmicTelemetry) -> AmblyopiaEvaluationReport:
        arf_status, arf_list = self.evaluate_aapos_arfs(data)
        strab_desc = self.quantification_strab = self.quantify_strabismus(data)
        severity, amblyopia_summary = self.stage_amblyopia(data)
        treatment_plan, safety_alert = self.generate_pedig_treatment_plan(severity, data)

        sentinels = []
        if data.media_opacity_present:
            sentinels.append("URGENT SURGICAL SENTINEL: Deprivation amblyopia from media opacity requires urgent pediatric ophthalmology surgical clearance within days to avoid permanent visual cortex synaptic loss.")

        directives = []
        directives.append(f"ARF STATUS: {arf_status}.")
        directives.append(f"ALIGNMENT: {strab_desc}")
        directives.append(f"AMBLYOPIA: {amblyopia_summary}.")
        directives.append(f"SAFETY: {safety_alert}")

        return AmblyopiaEvaluationReport(
            patient_id=data.patient_id,
            aapos_arf_status=arf_status,
            arf_criteria_triggered=arf_list,
            strabismus_quantification=strab_desc,
            amblyopia_etiology_and_severity=amblyopia_summary,
            pedig_treatment_prescription=treatment_plan,
            sound_eye_safety_sentinel=safety_alert,
            clinical_aapos_aao_directive=" ".join(directives)
        )

# Example Execution & Verification
if __name__ == "__main__":
    engine = PediatricAmblyopiaDecisionEngine()

    print("=" * 80)
    print("OpenPHR Clinical Pediatric Ophthalmology Amblyopia & Patching Engine")
    print("=" * 80)

    # Test Case 1: 3.5-year-old child (42 months) with Moderate Anisometropic & Strabismic Amblyopia
    # Refraction: OD +1.50 +0.50; OS +4.50 +1.00 (3.0 D hyperopic anisometropia).
    # Alignment: 2.5 mm temporal displacement on Hirschberg = Left Esotropia (~35-37.5 Prism Diopters).
    # Visual Acuity: OD 20/20 (Sound), OS 20/60 (Amblyopic). Completed 18 weeks of glasses.
    # Triage: Moderate Amblyopia -> PEDIG 2h/day Patching OR Weekend Atropine 1% + 4-week follow-up!
    peds1 = PediatricOphthalmicTelemetry(
        patient_id="PEDS-OPH-6602",
        age_months=42,
        va_right_eye_od_denominator=20,
        va_left_eye_os_denominator=60,
        od_sphere_diopters=+1.50,
        od_cylinder_diopters=+0.50,
        os_sphere_diopters=+4.50,
        os_cylinder_diopters=+1.00,
        hirschberg_corneal_reflex_displacement_mm=2.5,
        displacement_direction="Temporal",
        fixation_preference_os="Unsteady / Not Maintained",
        current_glasses_adaptation_duration_weeks=18
    )

    rep1 = engine.evaluate_case(peds1)

    print(f"\n[Patient {rep1.patient_id} - Pediatric Eye Assessment]")
    print(f"AAPOS Risk Status: {rep1.aapos_arf_status}")
    print("Risk Factors Triggered:")
    for arf in rep1.arf_criteria_triggered:
        print(f"  • {arf}")
    print(f"\nOcular Alignment: {rep1.strabismus_quantification}")
    print(f"Amblyopia Diagnosis: {rep1.amblyopia_etiology_and_severity}")
    print("\nPEDIG Treatment Prescription:")
    for tx in rep1.pedig_treatment_prescription:
        print(f"  {tx}")
    print(f"\nSafety Sentinel:\n  🚨 {rep1.sound_eye_safety_sentinel}")
    print(f"\nAAPOS / AAO Consensus Directive:\n{rep1.clinical_aapos_aao_directive}")

5. Clinical Verification & Guideline Conformance


6. References