mgbam's picture
Upload 4 files
74ec90b verified
raw
history blame
8.24 kB
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 ----------------
@dataclass
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)"))