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)"))