Spaces:
Sleeping
Sleeping
File size: 8,238 Bytes
74ec90b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
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)"))
|