Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| from datetime import datetime, timedelta | |
| """Sundew Diabetes Commons – holistic, open Streamlit experience.""" | |
| import json | |
| import logging | |
| import math | |
| import time | |
| from dataclasses import dataclass | |
| from typing import Any, Dict, List, Optional, Tuple | |
| import numpy as np | |
| import pandas as pd | |
| import streamlit as st | |
| from sklearn.linear_model import LogisticRegression | |
| from sklearn.pipeline import Pipeline | |
| from sklearn.preprocessing import StandardScaler | |
| try: | |
| from sundew import SundewAlgorithm # type: ignore[attr-defined] | |
| from sundew.config import SundewConfig | |
| from sundew.config_presets import get_preset | |
| _HAS_SUNDEW = True | |
| except Exception: # fallback when package is unavailable | |
| SundewAlgorithm = None # type: ignore | |
| SundewConfig = object # type: ignore | |
| def get_preset(_: str) -> Any: # type: ignore | |
| return None | |
| _HAS_SUNDEW = False | |
| LOGGER = logging.getLogger("sundew.diabetes.commons") | |
| class SundewGateConfig: | |
| target_activation: float = 0.22 | |
| temperature: float = 0.08 | |
| mode: str = "tuned_v2" | |
| use_native: bool = True | |
| def _build_sundew_runtime(config: SundewGateConfig) -> Optional[SundewAlgorithm]: | |
| if not (config.use_native and _HAS_SUNDEW and SundewAlgorithm is not None): | |
| return None | |
| try: | |
| preset = get_preset(config.mode) | |
| except Exception: | |
| preset = SundewConfig() # type: ignore | |
| for attr, value in ( | |
| ("target_activation_rate", config.target_activation), | |
| ("gate_temperature", config.temperature), | |
| ): | |
| try: | |
| setattr(preset, attr, value) | |
| except Exception: | |
| pass | |
| for constructor in ( | |
| lambda: SundewAlgorithm(preset), # type: ignore[arg-type] | |
| lambda: SundewAlgorithm(config=preset), # type: ignore[arg-type] | |
| lambda: SundewAlgorithm(), | |
| ): | |
| try: | |
| return constructor() | |
| except Exception: | |
| continue | |
| return None | |
| class AdaptiveGate: | |
| """Adapter that hides Sundew/Fallback branching.""" | |
| def __init__(self, config: SundewGateConfig) -> None: | |
| self.config = config | |
| self._ema = 0.0 | |
| self._tau = float(np.clip(config.target_activation, 0.05, 0.95)) | |
| self._alpha = 0.05 | |
| self.sundew: Optional[SundewAlgorithm] = _build_sundew_runtime(config) | |
| def decide(self, score: float) -> bool: | |
| if self.sundew is not None: | |
| for attr in ("decide", "step", "open"): | |
| fn = getattr(self.sundew, attr, None) | |
| if callable(fn): | |
| try: | |
| return bool(fn(score)) | |
| except Exception: | |
| continue | |
| normalized = float(np.clip(score / 1.4, 0.0, 1.0)) | |
| temperature = max(self.config.temperature, 0.02) | |
| probability = 1.0 / (1.0 + math.exp(-(normalized - self._tau) / temperature)) | |
| fired = bool(np.random.rand() < probability) | |
| self._ema = (1 - self._alpha) * self._ema + self._alpha * ( | |
| 1.0 if fired else 0.0 | |
| ) | |
| self._tau += 0.05 * (self.config.target_activation - self._ema) | |
| self._tau = float(np.clip(self._tau, 0.05, 0.95)) | |
| return fired | |
| def load_example_dataset(n_rows: int = 720) -> pd.DataFrame: | |
| rng = np.random.default_rng(17) | |
| t0 = pd.Timestamp.utcnow().floor("5min") - pd.Timedelta(minutes=5 * n_rows) | |
| timestamps = [t0 + pd.Timedelta(minutes=5 * i) for i in range(n_rows)] | |
| base = 118 + 28 * np.sin(np.linspace(0, 7 * math.pi, n_rows)) | |
| noise = rng.normal(0, 12, n_rows) | |
| meals = (rng.random(n_rows) < 0.05).astype(float) * rng.normal(50, 18, n_rows).clip( | |
| 0, 150 | |
| ) | |
| insulin = (rng.random(n_rows) < 0.03).astype(float) * rng.normal( | |
| 4.2, 1.5, n_rows | |
| ).clip(0, 10) | |
| steps = rng.integers(0, 200, size=n_rows) | |
| heart_rate = 68 + (steps > 90) * rng.integers(20, 45, size=n_rows) | |
| sleep_flag = (rng.random(n_rows) < 0.12).astype(float) | |
| stress_index = rng.uniform(0, 1, n_rows) | |
| glucose = base + noise | |
| for i in range(n_rows): | |
| if i >= 6: | |
| glucose[i] += 0.4 * meals[i - 6 : i].sum() / 6 | |
| if i >= 4: | |
| glucose[i] -= 1.2 * insulin[i - 4 : i].sum() / 4 | |
| if steps[i] > 100: | |
| glucose[i] -= 15 | |
| glucose[180:200] = rng.normal(62, 5, 20) | |
| glucose[350:365] = rng.normal(210, 10, 15) | |
| return pd.DataFrame( | |
| { | |
| "timestamp": timestamps, | |
| "glucose_mgdl": np.round(np.clip(glucose, 40, 350), 1), | |
| "carbs_g": np.round(meals, 1), | |
| "insulin_units": np.round(insulin, 1), | |
| "steps": steps.astype(int), | |
| "hr": (heart_rate + rng.normal(0, 5, n_rows)).round().astype(int), | |
| "sleep_flag": sleep_flag, | |
| "stress_index": stress_index, | |
| } | |
| ) | |
| def compute_features(df: pd.DataFrame) -> pd.DataFrame: | |
| df = df.copy().sort_values("timestamp").reset_index(drop=True) | |
| df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True) | |
| df["glucose_prev"] = df["glucose_mgdl"].shift(1) | |
| dt = ( | |
| df["timestamp"].astype("int64") - df["timestamp"].shift(1).astype("int64") | |
| ) / 60e9 | |
| df["roc_mgdl_min"] = (df["glucose_mgdl"] - df["glucose_prev"]) / dt | |
| df["roc_mgdl_min"] = df["roc_mgdl_min"].replace([np.inf, -np.inf], 0.0).fillna(0.0) | |
| ema = df["glucose_mgdl"].ewm(span=48, adjust=False).mean() | |
| df["deviation"] = (df["glucose_mgdl"] - ema).fillna(0.0) | |
| df["iob_proxy"] = df["insulin_units"].rolling(12, min_periods=1).sum() / 12.0 | |
| df["cob_proxy"] = df["carbs_g"].rolling(12, min_periods=1).sum() / 12.0 | |
| df["variability"] = df["glucose_mgdl"].rolling(24, min_periods=2).std().fillna(0.0) | |
| df["activity_factor"] = (df["steps"] / 200.0 + df["hr"] / 160.0).clip(0, 1) | |
| df["sleep_flag"] = df["sleep_flag"].fillna(0.0) if "sleep_flag" in df else 0.0 | |
| df["stress_index"] = df["stress_index"].fillna(0.5) if "stress_index" in df else 0.5 | |
| return df[ | |
| [ | |
| "timestamp", | |
| "glucose_mgdl", | |
| "roc_mgdl_min", | |
| "deviation", | |
| "iob_proxy", | |
| "cob_proxy", | |
| "variability", | |
| "activity_factor", | |
| "sleep_flag", | |
| "stress_index", | |
| ] | |
| ].copy() | |
| def lightweight_score(row: pd.Series) -> float: | |
| glucose = row["glucose_mgdl"] | |
| roc = row["roc_mgdl_min"] | |
| deviation = row["deviation"] | |
| iob = row["iob_proxy"] | |
| cob = row["cob_proxy"] | |
| stress = row["stress_index"] | |
| score = 0.0 | |
| score += max(0.0, (glucose - 180) / 80) | |
| score += max(0.0, (70 - glucose) / 30) | |
| score += abs(roc) / 6.0 | |
| score += abs(deviation) / 100.0 | |
| score += stress * 0.4 | |
| score += max(0.0, (cob - iob) * 0.04) | |
| return float(np.clip(score, 0.0, 1.4)) | |
| def train_simple_model(df: pd.DataFrame): | |
| features = df[ | |
| [ | |
| "glucose_mgdl", | |
| "roc_mgdl_min", | |
| "iob_proxy", | |
| "cob_proxy", | |
| "activity_factor", | |
| "variability", | |
| ] | |
| ] | |
| labels = (df["glucose_mgdl"] > 180).astype(int) | |
| model = Pipeline( | |
| [ | |
| ("scaler", StandardScaler()), | |
| ("clf", LogisticRegression(max_iter=400, class_weight="balanced")), | |
| ] | |
| ) | |
| try: | |
| model.fit(features, labels) | |
| return model | |
| except Exception: | |
| return None | |
| def render_overview( | |
| results: pd.DataFrame, | |
| alerts: List[Dict[str, Any]], | |
| gate_config: SundewGateConfig, | |
| ) -> None: | |
| total = len(results) | |
| activations = int(results["activated"].sum()) | |
| activation_rate = activations / max(total, 1) | |
| energy_savings = max(0.0, 1.0 - activation_rate) | |
| col_a, col_b, col_c, col_d = st.columns(4) | |
| col_a.metric("Events", f"{total}") | |
| col_b.metric("Heavy activations", f"{activations} ({activation_rate:.1%})") | |
| col_c.metric("Estimated energy saved", f"{energy_savings:.1%}") | |
| col_d.metric("Alerts", f"{len(alerts)}") | |
| if gate_config.use_native and _HAS_SUNDEW: | |
| st.caption( | |
| "Energy savings follow 1 − activation rate. With native Sundew gating we target " | |
| f"≈{gate_config.target_activation:.0%} activations, so savings approach " | |
| f"{1 - gate_config.target_activation:.0%}." | |
| ) | |
| else: | |
| st.warning( | |
| "Fallback gate active – heavy inference runs frequently, so savings mirror the observed activation rate." | |
| ) | |
| with st.expander("Recent alerts", expanded=False): | |
| if alerts: | |
| st.table(pd.DataFrame(alerts).tail(10)) | |
| else: | |
| st.info("No high-risk alerts in this window.") | |
| st.area_chart(results.set_index("timestamp")["glucose_mgdl"], height=220) | |
| def render_treatment_plan(medications: Dict[str, Any], next_visit: str) -> None: | |
| """Display medication plan guidance within the treatment tab.""" | |
| st.subheader("Full-cycle treatment support") | |
| st.write( | |
| "Upload or edit medication schedules, insulin titration guidance, and clinician notes." | |
| ) | |
| st.json(medications, expanded=False) | |
| st.caption(f"Next scheduled review: {next_visit}") | |
| def render_lifestyle_support(results: pd.DataFrame) -> None: | |
| st.subheader("Lifestyle & wellbeing") | |
| recent = results.tail(96).copy() | |
| avg_glucose = recent["glucose_mgdl"].mean() | |
| active_minutes = int((recent["activity_factor"] > 0.4).sum() * 5) | |
| col1, col2 = st.columns(2) | |
| col1.metric("Average glucose (8h)", f"{avg_glucose:.1f} mg/dL") | |
| col2.metric("Active minutes", f"{active_minutes} min") | |
| st.markdown( | |
| """ | |
| - Aim for gentle movement every hour you are awake. | |
| - Pair carbohydrates with protein/fiber to smooth spikes. | |
| - Sleep flagged recently? Try 10-minute breathing before bed. | |
| - Journal one gratitude moment—stress strongly shapes risk. | |
| """ | |
| ) | |
| def render_community_actions() -> Dict[str, List[str]]: | |
| st.subheader("Community impact") | |
| st.write( | |
| "Invite families, caregivers, and clinics to the commons. Set up alerts, shared logs, and outreach." | |
| ) | |
| contact_list = [ | |
| "SMS: +233-200-000-111", | |
| "WhatsApp: Care Circle Group", | |
| "Clinic portal: sundew.health/community", | |
| ] | |
| st.table(pd.DataFrame({"Support channel": contact_list})) | |
| return { | |
| "Desired partners": ["Rural clinics", "Youth ambassadors", "Nutrition co-ops"], | |
| "Needs": ["Smartphone grants", "Solar charging kits", "Translation volunteers"], | |
| } | |
| def render_telemetry(results: pd.DataFrame, telemetry: List[Dict[str, Any]]) -> None: | |
| """Allow operators to export telemetry and inspect recent events.""" | |
| st.subheader("Telemetry & export") | |
| st.write( | |
| "Download event-level telemetry for validation, research, or regulatory reporting." | |
| ) | |
| st.caption( | |
| "Energy savings are computed as 1 minus the observed activation rate. When the gate stays mostly open, savings naturally trend toward zero." | |
| ) | |
| json_payload = json.dumps(telemetry, default=str, indent=2) | |
| st.download_button( | |
| label="Download telemetry (JSON)", | |
| data=json_payload, | |
| file_name="sundew_diabetes_telemetry.json", | |
| mime="application/json", | |
| ) | |
| st.dataframe(results.tail(100), use_container_width=True) | |
| def main() -> None: | |
| """Streamlit entry point for the Sundew diabetes commons demo.""" | |
| st.set_page_config( | |
| page_title="Sundew Diabetes Commons", | |
| layout="wide", | |
| page_icon="🕊", | |
| ) | |
| st.title("Sundew Diabetes Commons") | |
| st.caption( | |
| "Open, compassionate diabetes care—monitoring, treatment, lifestyle, community." | |
| ) | |
| st.sidebar.header("Load data") | |
| uploaded = st.sidebar.file_uploader("CGM / diary CSV", type=["csv"]) | |
| use_example = st.sidebar.checkbox("Use synthetic example", value=True) | |
| st.sidebar.header("Sundew configuration") | |
| use_native = st.sidebar.checkbox( | |
| "Use native Sundew gating", | |
| value=_HAS_SUNDEW, | |
| help="Disable to demo the lightweight fallback gate only.", | |
| ) | |
| target_activation = st.sidebar.slider("Target activation", 0.05, 0.90, 0.22, 0.01) | |
| temperature = st.sidebar.slider("Gate temperature", 0.02, 0.50, 0.08, 0.01) | |
| mode = st.sidebar.selectbox( | |
| "Preset", ["tuned_v2", "conservative", "aggressive", "auto_tuned"], index=0 | |
| ) | |
| if uploaded is not None: | |
| df = pd.read_csv(uploaded) | |
| elif use_example: | |
| df = load_example_dataset() | |
| else: | |
| st.info("Upload a CSV file or enable the synthetic example to continue.") | |
| st.stop() | |
| features = compute_features(df) | |
| model = train_simple_model(features) | |
| gate_config = SundewGateConfig( | |
| target_activation=target_activation, | |
| temperature=temperature, | |
| mode=mode, | |
| use_native=use_native, | |
| ) | |
| gate = AdaptiveGate(gate_config) | |
| telemetry: List[Dict[str, Any]] = [] | |
| records: List[Dict[str, Any]] = [] | |
| alerts: List[Dict[str, Any]] = [] | |
| total_events = len(features) | |
| progress = st.progress(0.0) | |
| status = st.empty() | |
| for idx, row in enumerate(features.itertuples(index=False), start=1): | |
| event = row._asdict() | |
| score = lightweight_score(pd.Series(event)) | |
| should_run = gate.decide(score) | |
| risk_proba: Optional[float] = None | |
| if should_run and model is not None: | |
| sample_df = pd.DataFrame( | |
| [ | |
| [ | |
| event["glucose_mgdl"], | |
| event["roc_mgdl_min"], | |
| event["iob_proxy"], | |
| event["cob_proxy"], | |
| event["activity_factor"], | |
| event["variability"], | |
| ] | |
| ], | |
| columns=[ | |
| "glucose_mgdl", | |
| "roc_mgdl_min", | |
| "iob_proxy", | |
| "cob_proxy", | |
| "activity_factor", | |
| "variability", | |
| ], | |
| ) | |
| try: | |
| risk_proba = float(model.predict_proba(sample_df)[0, 1]) # type: ignore[index] | |
| except Exception as exc: | |
| LOGGER.debug("Risk model inference failed: %s", exc) | |
| risk_proba = None | |
| if risk_proba is not None and risk_proba >= 0.6: | |
| alerts.append( | |
| { | |
| "timestamp": event["timestamp"], | |
| "glucose": event["glucose_mgdl"], | |
| "risk": risk_proba, | |
| "message": "Check CGM, hydrate, plan balanced snack/insulin", | |
| } | |
| ) | |
| records.append( | |
| { | |
| "timestamp": event["timestamp"], | |
| "glucose_mgdl": event["glucose_mgdl"], | |
| "roc_mgdl_min": event["roc_mgdl_min"], | |
| "deviation": event["deviation"], | |
| "iob_proxy": event["iob_proxy"], | |
| "cob_proxy": event["cob_proxy"], | |
| "variability": event["variability"], | |
| "activity_factor": event["activity_factor"], | |
| "score": score, | |
| "activated": should_run, | |
| "risk_proba": risk_proba, | |
| } | |
| ) | |
| telemetry.append( | |
| { | |
| "timestamp": str(event["timestamp"]), | |
| "score": score, | |
| "activated": should_run, | |
| "risk_proba": risk_proba, | |
| } | |
| ) | |
| progress.progress(idx / max(total_events, 1)) | |
| status.text(f"Processing event {idx}/{total_events}") | |
| progress.empty() | |
| status.empty() | |
| results = pd.DataFrame(records) | |
| tabs = st.tabs(["Overview", "Treatment", "Lifestyle", "Community", "Telemetry"]) | |
| with tabs[0]: | |
| render_overview(results, alerts, gate_config) | |
| with tabs[1]: | |
| default_plan = { | |
| "Insulin": { | |
| "Basal": "14u glargine at 21:00", | |
| "Bolus": "1u per 10g carbs + correction 1u per 40 mg/dL over 140", | |
| }, | |
| "Oral medications": { | |
| "Metformin": "500mg breakfast + 500mg dinner", | |
| "Empagliflozin": "10mg once daily (if eGFR > 45)", | |
| }, | |
| "Monitoring": [ | |
| "CGM sensor change every 10 days", | |
| "Morning fasted CGM calibration", | |
| "Weekly telehealth coaching", | |
| "Quarterly in-person clinician review", | |
| ], | |
| "Safety plan": [ | |
| "Carry glucose tabs + glucagon kit", | |
| "Emergency contact: +233-200-000-888", | |
| ], | |
| "Lifestyle": [ | |
| "30 min brisk walk 5x/week", | |
| "Bedtime snack if glucose < 110 mg/dL", | |
| "Hydrate 2L water daily unless contraindicated", | |
| ], | |
| } | |
| st.caption( | |
| "Upload or edit schedules, medication titration guidance, and clinician notes." | |
| ) | |
| uploaded_plan = st.file_uploader( | |
| "Optional plan JSON", type=["json"], key="plan_uploader" | |
| ) | |
| plan_text = st.text_area( | |
| "Edit plan JSON", | |
| json.dumps(default_plan, indent=2), | |
| height=240, | |
| key="plan_editor", | |
| ) | |
| plan_data = default_plan | |
| if uploaded_plan is not None: | |
| try: | |
| plan_data = json.load(uploaded_plan) | |
| except Exception as exc: | |
| st.error(f"Could not parse uploaded plan JSON: {exc}") | |
| plan_data = default_plan | |
| else: | |
| try: | |
| plan_data = json.loads(plan_text) | |
| except Exception as exc: | |
| st.warning( | |
| f"Using default plan because text could not be parsed: {exc}" | |
| ) | |
| plan_data = default_plan | |
| next_visit = (datetime.utcnow() + timedelta(days=30)).strftime( | |
| "%Y-%m-%d (telehealth)" | |
| ) | |
| render_treatment_plan(plan_data, next_visit=next_visit) | |
| with tabs[2]: | |
| render_lifestyle_support(results) | |
| with tabs[3]: | |
| community_items = render_community_actions() | |
| st.json(community_items, expanded=False) | |
| with tabs[4]: | |
| render_telemetry(results, telemetry) | |
| st.sidebar.markdown("---") | |
| status_text = ( | |
| "native gating" | |
| if gate_config.use_native and gate.sundew is not None | |
| else "fallback gate" | |
| ) | |
| st.sidebar.caption(f"Sundew status: {status_text}") | |
| if __name__ == "__main__": | |
| main() | |