diff --git a/Working_Code/vmed_sans_generator.py b/Working_Code/vmed_sans_generator.py new file mode 100644 index 00000000..1a19fa0c --- /dev/null +++ b/Working_Code/vmed_sans_generator.py @@ -0,0 +1,85 @@ +import numpy as np +import pandas as pd +import uuid + +# 1. Parametros del estandar CDSS +MISSION_TIMELINES = ["L-30", "L", "L+30", "L+90", "L+120"] + +CLINICAL_RANGES = { + "rnfl_thickness_um": {"normal": (85.0, 105.0), "edema_risk": 115.0}, + "choroidal_thickness_um": {"normal": (250.0, 320.0), "congestion_risk": 360.0}, + "intraocular_pressure_mmhg": {"normal": (12.0, 20.0), "high_risk": 22.0}, + "spherical_equivalent_diopters": {"normal": (-0.5, 0.5), "hyperopic_shift": 1.25} +} + +class AstronautProfileGenerator: + def __init__(self, seed: int = 42): + np.random.seed(seed) + + def generate_patient(self, astronaut_id: str, trajectory_type: str = "normal") -> pd.DataFrame: + age = np.random.randint(35, 56) + sex = np.random.choice(["Male", "Female"], p=[0.7, 0.3]) + base_rnfl = np.random.normal(95.0, 4.0) + base_choroid = np.random.normal(285.0, 15.0) + base_iop = np.random.normal(15.0, 2.0) + base_refraction = np.random.normal(0.0, 0.25) + + records = [] + for t_idx, timepoint in enumerate(MISSION_TIMELINES): + flight_factor = 0.0 if timepoint == "L-30" else (t_idx * 1.5) + if trajectory_type == "progressive_sans": + rnfl = base_rnfl + (flight_factor * 4.2) + np.random.normal(0, 0.8) + choroid = base_choroid + (flight_factor * 12.5) + np.random.normal(0, 2.0) + iop = base_iop + (flight_factor * 1.1) + np.random.normal(0, 0.5) + refraction = base_refraction + (flight_factor * 0.2) + np.random.normal(0, 0.05) + clinical_flag = "At_Risk_SANS" if rnfl > CLINICAL_RANGES["rnfl_thickness_um"]["edema_risk"] else "Monitoring" + else: + rnfl = base_rnfl + np.random.normal(0, 0.9) + choroid = base_choroid + np.random.normal(0, 3.0) + iop = base_iop + np.random.normal(0, 0.6) + refraction = base_refraction + np.random.normal(0, 0.05) + clinical_flag = "Normal" + + records.append({ + "patient_id": astronaut_id, + "age": age, + "sex": sex, + "timepoint": timepoint, + "rnfl_thickness_um": round(float(rnfl), 2), + "choroidal_thickness_um": round(float(choroid), 2), + "iop_mmhg": round(float(iop), 2), + "refraction_diopters": round(float(refraction), 2), + "trajectory_profile": trajectory_type, + "clinical_status": clinical_flag, + "data_integrity_hash": str(uuid.uuid5(uuid.NAMESPACE_DNS, f"{astronaut_id}_{timepoint}"))[:8] + }) + return pd.DataFrame(records) + +def audit_cohort(df: pd.DataFrame) -> pd.DataFrame: + audit_summary = [] + for patient_id, group in df.groupby("patient_id"): + baseline = group[group["timepoint"] == "L-30"].iloc[0] + final = group[group["timepoint"] == "L+120"].iloc[0] + rnfl_delta = final["rnfl_thickness_um"] - baseline["rnfl_thickness_um"] + choroid_delta = final["choroidal_thickness_um"] - baseline["choroidal_thickness_um"] + status = "ALERT: Progressive SANS" if rnfl_delta > 15.0 or choroid_delta > 40.0 else "PASS: Stable" + audit_summary.append({ + "patient_id": patient_id, + "profile": baseline["trajectory_profile"], + "rnfl_delta_um": round(rnfl_delta, 2), + "choroid_delta_um": round(choroid_delta, 2), + "audit_verdict": status + }) + return pd.DataFrame(audit_summary) + +if __name__ == "__main__": + generator = AstronautProfileGenerator(seed=101) + cohort = [] + for i in range(1, 6): + cohort.append(generator.generate_patient(f"ASTRO-NORM-{i:02d}", "normal")) + cohort.append(generator.generate_patient(f"ASTRO-SANS-{i:02d}", "progressive_sans")) + dataset = pd.concat(cohort, ignore_index=True) + dataset.to_csv("cdss_sans_synthetic_cohort.csv", index=False) + report = audit_cohort(dataset) + report.to_csv("cdss_audit_report.csv", index=False) + print("Execution complete: Synthetic dataset and audit report generated.") diff --git a/sans_validation_curves.png b/sans_validation_curves.png new file mode 100644 index 00000000..6d616f4d Binary files /dev/null and b/sans_validation_curves.png differ diff --git a/synthetic_sans_cohort.csv b/synthetic_sans_cohort.csv new file mode 100644 index 00000000..1694ea1f --- /dev/null +++ b/synthetic_sans_cohort.csv @@ -0,0 +1,101 @@ +astronaut_id,cohort,timepoint,rnfl_um,choroid_um,iop_mmhg,refraction_diopters +AST_01,SANS,L-30,93.58,270.76,14.63,-0.36 +AST_01,SANS,L+30,105.95,288.62,14.31,0.07 +AST_01,SANS,L+60,116.12,300.93,17.34,0.31 +AST_01,SANS,L+90,129.31,302.99,16.89,0.47 +AST_01,SANS,L+120,141.65,317.35,17.97,0.86 +AST_02,Control,L-30,94.87,260.74,14.95,-0.25 +AST_02,Control,L+30,96.49,280.58,14.61,-0.29 +AST_02,Control,L+60,95.71,264.76,14.05,-0.27 +AST_02,Control,L+90,93.97,275.26,13.27,-0.2 +AST_02,Control,L+120,97.83,270.78,16.14,-0.08 +AST_03,SANS,L-30,94.99,264.13,15.65,-0.18 +AST_03,SANS,L+30,104.19,286.5,15.39,0.02 +AST_03,SANS,L+60,113.36,295.13,17.69,0.41 +AST_03,SANS,L+90,132.09,306.29,17.61,0.37 +AST_03,SANS,L+120,140.36,323.91,18.97,0.7 +AST_04,Control,L-30,98.51,268.1,14.7,-0.18 +AST_04,Control,L+30,93.95,268.35,14.67,-0.3 +AST_04,Control,L+60,93.54,274.15,15.84,-0.21 +AST_04,Control,L+90,94.25,267.03,15.84,-0.27 +AST_04,Control,L+120,93.34,271.62,14.84,-0.29 +AST_05,SANS,L-30,96.59,271.22,14.77,-0.35 +AST_05,SANS,L+30,101.66,285.66,16.49,0.0 +AST_05,SANS,L+60,114.05,301.55,17.82,0.32 +AST_05,SANS,L+90,130.26,315.15,16.05,0.57 +AST_05,SANS,L+120,139.91,314.7,18.51,0.69 +AST_06,Control,L-30,92.43,268.27,14.72,-0.26 +AST_06,Control,L+30,101.32,267.67,15.02,-0.31 +AST_06,Control,L+60,95.86,265.14,15.93,-0.36 +AST_06,Control,L+90,93.14,272.76,15.27,-0.16 +AST_06,Control,L+120,95.9,267.12,14.63,-0.15 +AST_07,SANS,L-30,94.1,275.9,15.06,-0.24 +AST_07,SANS,L+30,103.21,292.16,16.65,-0.12 +AST_07,SANS,L+60,112.53,289.42,16.47,0.17 +AST_07,SANS,L+90,129.12,310.4,15.05,0.43 +AST_07,SANS,L+120,140.97,318.09,16.31,0.75 +AST_08,Control,L-30,96.46,279.12,14.72,-0.14 +AST_08,Control,L+30,100.42,273.33,15.25,-0.12 +AST_08,Control,L+60,93.27,274.24,15.89,-0.22 +AST_08,Control,L+90,91.63,268.57,14.96,-0.31 +AST_08,Control,L+120,89.72,268.82,14.23,-0.12 +AST_09,SANS,L-30,96.86,266.49,14.52,-0.27 +AST_09,SANS,L+30,104.4,278.48,18.58,0.1 +AST_09,SANS,L+60,113.27,300.49,16.38,0.23 +AST_09,SANS,L+90,131.32,306.36,17.14,0.48 +AST_09,SANS,L+120,135.86,313.65,16.48,0.86 +AST_10,Control,L-30,98.54,277.66,14.14,-0.14 +AST_10,Control,L+30,95.71,267.35,15.49,-0.27 +AST_10,Control,L+60,94.95,266.53,15.65,-0.02 +AST_10,Control,L+90,94.93,270.32,13.85,-0.34 +AST_10,Control,L+120,95.34,263.33,15.42,-0.12 +AST_11,SANS,L-30,94.65,273.54,14.45,-0.18 +AST_11,SANS,L+30,101.38,282.01,15.88,0.05 +AST_11,SANS,L+60,112.41,292.35,15.55,0.18 +AST_11,SANS,L+90,129.43,306.93,18.16,0.33 +AST_11,SANS,L+120,139.74,316.91,17.14,0.8 +AST_12,Control,L-30,94.23,259.51,14.79,-0.39 +AST_12,Control,L+30,91.25,258.96,16.83,-0.26 +AST_12,Control,L+60,97.44,271.4,14.45,-0.19 +AST_12,Control,L+90,94.82,261.44,15.95,-0.4 +AST_12,Control,L+120,94.6,270.61,13.4,-0.2 +AST_13,SANS,L-30,93.75,265.17,14.91,-0.08 +AST_13,SANS,L+30,101.69,283.67,16.35,-0.18 +AST_13,SANS,L+60,113.54,296.33,17.36,0.29 +AST_13,SANS,L+90,128.11,303.91,18.84,0.41 +AST_13,SANS,L+120,139.27,315.87,18.26,0.88 +AST_14,Control,L-30,92.67,267.51,14.77,-0.32 +AST_14,Control,L+30,98.29,267.59,16.13,-0.39 +AST_14,Control,L+60,95.98,274.24,14.14,-0.38 +AST_14,Control,L+90,94.48,269.96,13.92,-0.21 +AST_14,Control,L+120,97.56,266.72,13.23,-0.25 +AST_15,SANS,L-30,94.04,267.86,13.85,-0.28 +AST_15,SANS,L+30,106.81,278.9,16.13,-0.05 +AST_15,SANS,L+60,116.57,293.98,17.15,0.29 +AST_15,SANS,L+90,132.92,303.66,17.38,0.6 +AST_15,SANS,L+120,136.49,309.79,17.15,0.67 +AST_16,Control,L-30,94.44,264.93,14.56,-0.35 +AST_16,Control,L+30,93.81,269.26,15.71,-0.38 +AST_16,Control,L+60,95.7,271.47,15.05,-0.15 +AST_16,Control,L+90,95.75,265.28,12.66,-0.13 +AST_16,Control,L+120,97.14,274.03,16.1,-0.23 +AST_17,SANS,L-30,94.75,265.53,16.07,-0.21 +AST_17,SANS,L+30,105.06,285.17,16.09,0.03 +AST_17,SANS,L+60,115.65,299.23,18.03,0.21 +AST_17,SANS,L+90,129.2,314.69,17.7,0.56 +AST_17,SANS,L+120,141.53,313.09,19.09,0.62 +AST_18,Control,L-30,94.54,262.36,14.01,-0.28 +AST_18,Control,L+30,93.02,277.11,15.8,-0.12 +AST_18,Control,L+60,99.12,262.62,16.99,-0.3 +AST_18,Control,L+90,96.91,275.24,15.05,-0.11 +AST_18,Control,L+120,94.04,272.79,14.86,-0.22 +AST_19,SANS,L-30,94.82,269.75,16.34,-0.27 +AST_19,SANS,L+30,104.35,288.59,17.17,-0.2 +AST_19,SANS,L+60,110.12,305.57,15.38,0.2 +AST_19,SANS,L+90,128.96,308.3,17.46,0.39 +AST_19,SANS,L+120,137.45,304.89,17.92,0.61 +AST_20,Control,L-30,95.5,271.64,14.0,-0.15 +AST_20,Control,L+30,94.42,270.95,13.4,-0.3 +AST_20,Control,L+60,97.23,268.49,15.12,-0.29 +AST_20,Control,L+90,95.27,279.73,14.58,-0.25 +AST_20,Control,L+120,94.05,276.42,15.12,-0.34 diff --git a/vmed_sans_generator.py b/vmed_sans_generator.py new file mode 100644 index 00000000..6a707001 --- /dev/null +++ b/vmed_sans_generator.py @@ -0,0 +1,30 @@ +import numpy as np +import pandas as pd + +TIMEPOINTS = ["L-30", "L+30", "L+60", "L+90", "L+120"] +DELTAS = {"L-30":[0,0,0,0], "L+30":[8,15,1.5,0.25], "L+60":[20,28,2.0,0.5], "L+90":[35,38,2.2,0.75], "L+120":[45,45,2.5,1.0]} + +def generate_cohort(n=20, sans_ratio=0.5): + records = [] + for i in range(n): + aid = f"AST_{i+1:02d}" + sans = (i % 2 == 0) + grp = "SANS" if sans else "Control" + b_rnfl, b_cho, b_iop, b_ref = np.random.normal(95, 8), np.random.normal(270, 40), np.random.normal(15, 2.5), np.random.normal(-0.25, 0.75) + for t in TIMEPOINTS: + d = DELTAS[t] if sans else [0, 0, 0, 0] + records.append({"astronaut_id": aid, "cohort": grp, "timepoint": t, "rnfl_um": round(b_rnfl + d[0] + float(np.random.normal(0, 2)), 2), "choroid_um": round(b_cho + d[1] + float(np.random.normal(0, 5)), 2), "iop_mmhg": round(b_iop + d[2] + float(np.random.normal(0, 1)), 2), "refraction_diopters": round(b_ref + d[3] + float(np.random.normal(0, 0.1)), 2)}) + return pd.DataFrame(records) + +def audit_cohort(df): + p = df.pivot(index=["astronaut_id", "cohort"], columns="timepoint", values="rnfl_um") + deltas = p["L+120"] - p["L-30"] + sans_err = deltas[(p.index.get_level_values("cohort") == "SANS") & (deltas < 15.0)] + ctrl_err = deltas[(p.index.get_level_values("cohort") == "Control") & (deltas > 10.0)] + return len(sans_err) == 0 and len(ctrl_err) == 0 + +if __name__ == "__main__": + cohort = generate_cohort(20) + cohort.to_csv("synthetic_sans_cohort.csv", index=False) + passed = audit_cohort(cohort) + print(f"vmed_sans_generator: OK (Audit Passed: {passed})")