Spaces:
Sleeping
Sleeping
| import math | |
| from dataclasses import dataclass | |
| from typing import Optional | |
| import numpy as np | |
| import pandas as pd | |
| import streamlit as st | |
| # ---- Try Sundew; fallback if missing ---- | |
| try: | |
| from sundew import SundewAlgorithm # provided by sundew-algorithms | |
| _HAS_SUNDEW = True | |
| except Exception: | |
| SundewAlgorithm = None # type: ignore | |
| _HAS_SUNDEW = False | |
| st.set_page_config(page_title="Sundew Diabetes Watch", layout="wide") | |
| st.title("🌿 Sundew Diabetes Watch") | |
| st.caption("Energy-aware selective activation for diabetes monitoring — research demo (not medical advice).") | |
| # ---------------- Sundew Gate wrapper ---------------- | |
| class SundewGate: | |
| target_activation: float = 0.25 | |
| temperature: float = 0.08 | |
| mode: str = "tuned_v2" | |
| def __post_init__(self): | |
| if _HAS_SUNDEW and SundewAlgorithm is not None: | |
| try: | |
| self.sd = SundewAlgorithm( | |
| target_activation=self.target_activation, | |
| temperature=self.temperature, | |
| mode=self.mode, | |
| ) | |
| except TypeError: | |
| self.sd = SundewAlgorithm() | |
| else: | |
| self.sd = None | |
| # fallback state | |
| self._tau = 0.5 | |
| self._ema = 0.0 | |
| self._alpha = 0.02 | |
| def decide(self, score: float) -> bool: | |
| score = float(max(0.0, min(1.0, score))) | |
| if self.sd is not None: | |
| for name in ("decide", "step", "open"): | |
| if hasattr(self.sd, name): | |
| try: | |
| return bool(getattr(self.sd, name)(score)) | |
| except Exception: | |
| pass | |
| # stochastic logistic fallback | |
| p_open = 1 / (1 + math.exp(-(score - self._tau) / max(1e-6, self.temperature))) | |
| fired = np.random.rand() < p_open | |
| self._ema = (1 - self._alpha) * self._ema + self._alpha * (1.0 if fired else 0.0) | |
| self._tau += 0.01 * (self.target_activation - self._ema) | |
| self._tau = min(0.95, max(0.05, self._tau)) | |
| return fired | |
| # ---------------- Lightweight risk scoring ---------------- | |
| def compute_lightweight_score(row: pd.Series) -> float: | |
| g = float(row.get("glucose_mgdl", np.nan)) | |
| roc = float(row.get("roc_mgdl_min", 0.0)) | |
| insulin = float(row.get("insulin_units", 0.0)) | |
| carbs = float(row.get("carbs_g", 0.0)) | |
| hr = float(row.get("hr", 0.0)) | |
| low_gap = max(0.0, 80 - g) | |
| high_gap = max(0.0, g - 140) | |
| base = (low_gap + high_gap) / 120.0 | |
| roc_term = min(1.0, abs(roc) / 3.0) | |
| insulin_term = min(1.0, insulin / 6.0) * (1.0 if roc < -0.5 else 0.3) | |
| carbs_term = min(1.0, carbs / 50.0) * (1.0 if roc > 0.5 else 0.3) | |
| activity_term = min(1.0, max(0.0, hr - 100) / 60.0) * (1.0 if insulin > 0.5 else 0.2) | |
| score = base + 0.7 * roc_term + 0.5 * insulin_term + 0.4 * carbs_term + 0.3 * activity_term | |
| return float(max(0.0, min(1.0, score))) | |
| # ---------------- Heavy model (simple logreg) ---------------- | |
| from sklearn.linear_model import LogisticRegression | |
| from sklearn.preprocessing import StandardScaler | |
| from sklearn.pipeline import Pipeline | |
| def make_heavy_model() -> Pipeline: | |
| return Pipeline([("scaler", StandardScaler()), ("clf", LogisticRegression(max_iter=1000))]) | |
| def train_heavy_model(df: pd.DataFrame) -> Pipeline: | |
| df = df.copy() | |
| # predict 30-min ahead risk: hypo<70 or hyper>180 | |
| df["future_glucose"] = df["glucose_mgdl"].shift(-6) # assuming 5-min cadence | |
| df["label"] = ((df["future_glucose"] < 70) | (df["future_glucose"] > 180)).astype(int) | |
| df = df.dropna(subset=["label"]).copy() | |
| X = df[["glucose_mgdl", "roc_mgdl_min", "insulin_units", "carbs_g", "hr"]].fillna(0.0).values | |
| y = df["label"].values | |
| if len(np.unique(y)) < 2: | |
| # ensure fit works even with monotone data | |
| y = np.array([0, 1] * (len(X) // 2 + 1))[: len(X)] | |
| model = make_heavy_model() | |
| model.fit(X, y) | |
| return model | |
| # ---------------- UI ---------------- | |
| left, right = st.columns([2, 1]) | |
| with left: | |
| uploaded = st.file_uploader("Upload CGM CSV (timestamp, glucose_mgdl, carbs_g, insulin_units, steps, hr)", type=["csv"]) | |
| use_synth = st.checkbox("Use synthetic example if no file uploaded", value=True) | |
| with right: | |
| target_activation = st.slider("Target heavy-activation rate", 0.05, 0.9, 0.25, 0.01) | |
| temperature = st.slider("Gate temperature", 0.02, 0.5, 0.08, 0.01) | |
| mode = st.selectbox("Sundew mode", ["tuned_v2", "conservative", "aggressive", "auto_tuned"], index=0) | |
| # ---------------- Load or synthesize data ---------------- | |
| if uploaded is not None: | |
| df = pd.read_csv(uploaded) | |
| else: | |
| if not use_synth: | |
| st.stop() | |
| rng = np.random.default_rng(7) | |
| n = 600 | |
| t0 = pd.Timestamp.utcnow().floor("min") | |
| times = [t0 + pd.Timedelta(minutes=5 * i) for i in range(n)] | |
| base = 120 + 25 * np.sin(np.linspace(0, 10 * np.pi, n)) | |
| noise = rng.normal(0, 10, n) | |
| meals = (rng.random(n) < 0.04).astype(float) * rng.normal(45, 15, n).clip(0, 120) | |
| insulin = (rng.random(n) < 0.03).astype(float) * rng.normal(4, 1.2, n).clip(0, 8) | |
| steps = rng.integers(0, 150, size=n) | |
| hr = 70 + (steps > 80) * rng.integers(30, 60, size=n) | |
| glucose = base + noise + 0.3 * meals - 0.8 * insulin | |
| df = pd.DataFrame({ | |
| "timestamp": times, | |
| "glucose_mgdl": np.round(glucose, 1), | |
| "carbs_g": np.round(meals, 1), | |
| "insulin_units": np.round(insulin, 1), | |
| "steps": steps, | |
| "hr": hr, | |
| }) | |
| # ---- Robust timestamp parsing (handles tz-aware) ---- | |
| from pandas.api.types import is_datetime64_any_dtype | |
| if "timestamp" not in df.columns: | |
| st.error("CSV must include a 'timestamp' column.") | |
| st.stop() | |
| if not is_datetime64_any_dtype(df["timestamp"]): | |
| df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True, errors="coerce") | |
| # localize to UTC if naive | |
| if getattr(df["timestamp"].dt, "tz", None) is None: | |
| df["timestamp"] = df["timestamp"].dt.tz_localize("UTC") | |
| df = df.sort_values("timestamp").reset_index(drop=True) | |
| df["dt_min"] = df["timestamp"].diff().dt.total_seconds() / 60.0 | |
| df["glucose_prev"] = df["glucose_mgdl"].shift(1) | |
| df["roc_mgdl_min"] = (df["glucose_mgdl"] - df["glucose_prev"]) / df["dt_min"] | |
| df["roc_mgdl_min"] = df["roc_mgdl_min"].replace([np.inf, -np.inf], 0.0).fillna(0.0) | |
| # Train model | |
| model = train_heavy_model(df) | |
| # Run stream | |
| gate = SundewGate(target_activation=target_activation, temperature=temperature, mode=mode) | |
| records = [] | |
| alerts = [] | |
| for _, row in df.iterrows(): | |
| score = compute_lightweight_score(row) | |
| open_gate = gate.decide(score) | |
| decision = "SKIP" | |
| proba = None | |
| if open_gate: | |
| X = np.array([[row.get("glucose_mgdl", 0.0), row.get("roc_mgdl_min", 0.0), | |
| row.get("insulin_units", 0.0), row.get("carbs_g", 0.0), row.get("hr", 0.0)]]) | |
| try: | |
| proba = float(model.predict_proba(X)[0, 1]) | |
| except Exception: | |
| proba = float(model.predict(X)[0]) | |
| decision = "RUN" | |
| if proba >= 0.6: | |
| alerts.append({ | |
| "timestamp": row["timestamp"], | |
| "glucose": row["glucose_mgdl"], | |
| "risk_proba": proba, | |
| "note": "⚠ Elevated 30-min risk — please check CGM and plan carbs/insulin." | |
| }) | |
| records.append({ | |
| "timestamp": row["timestamp"], "glucose": row["glucose_mgdl"], "roc": row["roc_mgdl_min"], | |
| "score": score, "gate": decision, "risk_proba": proba | |
| }) | |
| out = pd.DataFrame(records) | |
| events = len(out) | |
| activations = int((out["gate"] == "RUN").sum()) | |
| rate = activations / max(events, 1) | |
| c1, c2, c3 = st.columns(3) | |
| c1.metric("Events", f"{events}") | |
| c2.metric("Heavy activations", f"{activations}") | |
| c3.metric("Activation rate", f"{rate:.2%}") | |
| st.line_chart(out.set_index("timestamp")["glucose"], height=220) | |
| st.line_chart(out.set_index("timestamp")["score"], height=220) | |
| st.subheader("Decisions (tail)") | |
| st.dataframe(out.tail(50)) | |
| st.subheader("Alerts") | |
| if alerts: | |
| st.dataframe(pd.DataFrame(alerts)) | |
| else: | |
| st.info("No high-risk alerts triggered in this window.") | |
| st.caption("Engine: {}" | |
| .format("sundew-algorithms active" if _HAS_SUNDEW else "fallback gate (install sundew-algorithms)")) | |