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.
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}$):
[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]
Install required scientific Python and pediatric ophthalmology modeling packages:
pip install numpy scipy pandas torch torchvision matplotlib
"""
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}")