mgbam's picture
Update app.py
1ee01d8 verified
raw
history blame
13.7 kB
"""Sundew Diabetes Commons – holistic, open Streamlit experience."""
from __future__ import annotations
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
try:
from sundew import SundewAlgorithm # type: ignore[attr-defined]
_HAS_SUNDEW = True
except Exception: # pragma: no cover - graceful fallback
SundewAlgorithm = None # type: ignore
_HAS_SUNDEW = False
LOGGER = logging.getLogger("sundew.diabetes.commons")
@dataclass
class SundewGateConfig:
target_activation: float = 0.22
temperature: float = 0.08
mode: str = "tuned_v2"
class AdaptiveGate:
"""Adapter that hides Sundew/Fallback branching."""
def __init__(self, config: SundewGateConfig) -> None:
self.config = config
self._ema = 0.0
self._tau = 0.5
self._alpha = 0.02
if _HAS_SUNDEW and SundewAlgorithm is not None:
try:
self.sundew: Optional[SundewAlgorithm] = SundewAlgorithm(
target_activation=config.target_activation,
temperature=config.temperature,
mode=config.mode,
)
except TypeError: # older package versions
self.sundew = SundewAlgorithm()
else:
self.sundew = None
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: # pragma: no cover - parity fallback
continue
# Fallback logistic gate
temperature = max(self.config.temperature, 1e-6)
probability = 1.0 / (1.0 + math.exp(-(score - self._tau) / temperature))
fired = np.random.rand() < probability
self._ema = (1 - self._alpha) * self._ema + self._alpha * (
1.0 if fired else 0.0
)
self._tau += 0.01 * (self.config.target_activation - self._ema)
self._tau = min(0.95, max(0.05, self._tau))
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 * np.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)
hr = 68 + (steps > 90) * rng.integers(20, 45, size=n_rows)
sleep = (rng.random(n_rows) < 0.12).astype(float)
stress = rng.uniform(0, 1, n_rows)
glucose = base + noise + 0.4 * meals - 0.7 * insulin
df = pd.DataFrame(
{
"timestamp": timestamps,
"glucose_mgdl": glucose.round(1),
"carbs_g": meals.round(1),
"insulin_units": insulin.round(1),
"steps": steps,
"hr": hr,
"sleep_flag": sleep,
"stress_index": stress,
}
)
return df
def compute_features(df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
df = df.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"].replace([np.inf, -np.inf], 0.0, inplace=True)
df["roc_mgdl_min"].fillna(0.0, inplace=True)
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.get("sleep_flag", 0.0).fillna(0.0)
df["stress_index"] = df.get("stress_index", 0.5).fillna(0.5)
features = df[
[
"timestamp",
"glucose_mgdl",
"roc_mgdl_min",
"deviation",
"iob_proxy",
"cob_proxy",
"variability",
"activity_factor",
"sleep_flag",
"stress_index",
]
].copy()
return features
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 += (cob - iob) * 0.05
return float(np.clip(score, 0.0, 1.5))
def train_simple_model(df: pd.DataFrame):
threshold = 180
features = df[
[
"glucose_mgdl",
"roc_mgdl_min",
"iob_proxy",
"cob_proxy",
"activity_factor",
"variability",
]
]
labels = (df["glucose_mgdl"] > threshold).astype(int)
model = Pipeline(
[
("scaler", StandardScaler()),
(
"clf",
LogisticRegression(
max_iter=400,
class_weight="balanced",
),
),
]
)
try:
model.fit(features, labels)
return model
except Exception: # pragma: no cover
return None
def render_overview(results: pd.DataFrame, alerts: List[Dict[str, Any]]) -> None:
total = len(results)
activations = int(results["activated"].sum())
activation_rate = activations / max(total, 1)
energy_savings = 1.0 - activation_rate
st.metric("Events", f"{total}")
st.metric("Heavy activations", f"{activations} ({activation_rate:.1%})")
st.metric("Estimated energy saved", f"{energy_savings:.1%}")
st.metric("Alerts", f"{len(alerts)}")
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:
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() # last ~8 hours if 5min cadence
avg_glucose = recent["glucose_mgdl"].mean()
active_minutes = int((recent["activity_factor"] > 0.4).sum() * 5)
st.metric("Average glucose (8h)", f"{avg_glucose:.1f} mg/dL")
st.metric("Active minutes", f"{active_minutes} min")
st.markdown(
"- 🎯 Aim for gentle movement every hour you are awake.\n"
"- 🥗 Consider pairing carbs with protein/fiber to smooth spikes.\n"
"- 😴 Sleep flagged recently? Try 10-minute breathing before bed.\n"
"- 🤗 Journal one gratitude moment—stress index 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}))
callouts = {
"Desired partners": ["Rural clinics", "Youth ambassadors", "Nutrition co-ops"],
"Needs": ["Smartphone grants", "Solar charging kits", "Translation volunteers"],
}
return callouts
def render_telemetry(results: pd.DataFrame, telemetry: List[Dict[str, Any]]) -> None:
st.subheader("Telemetry & export")
st.write(
"Download event-level telemetry for validation, research, or regulatory reporting."
)
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))
def main() -> None:
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", True)
st.sidebar.header("Sundew configuration")
target_activation = st.sidebar.slider("Target activation", 0.05, 0.9, 0.22, 0.01)
temperature = st.sidebar.slider("Gate temperature", 0.02, 0.5, 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.stop()
features = compute_features(df)
model = train_simple_model(features)
gate = AdaptiveGate(SundewGateConfig(target_activation, temperature, mode))
telemetry: List[Dict[str, Any]] = []
records: List[Dict[str, Any]] = []
alerts: List[Dict[str, Any]] = []
progress = st.progress(0)
status = st.empty()
for idx, row in enumerate(features.itertuples(index=False), start=1):
score = lightweight_score(pd.Series(row._asdict()))
should_run = gate.decide(score)
risk_proba = None
if should_run and model is not None:
try:
sample = np.array(
[
[
row.glucose_mgdl,
row.roc_mgdl_min,
row.iob_proxy,
row.cob_proxy,
row.activity_factor,
row.variability,
]
]
)
risk_proba = float(model.predict_proba(sample)[0, 1]) # type: ignore[attr-defined]
except Exception:
pass
if risk_proba is not None and risk_proba >= 0.6:
alerts.append(
{
"timestamp": row.timestamp,
"glucose": row.glucose_mgdl,
"risk": risk_proba,
"message": "Check CGM, hydrate, plan balanced snack/insulin",
}
)
records.append(
{
"timestamp": row.timestamp,
"glucose_mgdl": row.glucose_mgdl,
"roc_mgdl_min": row.roc_mgdl_min,
"deviation": row.deviation,
"iob_proxy": row.iob_proxy,
"cob_proxy": row.cob_proxy,
"variability": row.variability,
"activity_factor": row.activity_factor,
"score": score,
"activated": should_run,
"risk_proba": risk_proba,
}
)
telemetry.append(
{
"timestamp": str(row.timestamp),
"score": score,
"activated": should_run,
"risk_proba": risk_proba,
}
)
progress.progress(idx / len(features))
status.text(f"Processing event {idx}/{len(features)}")
progress.empty()
status.empty()
results = pd.DataFrame(records)
tabs = st.tabs(
[
"Overview",
"Treatment",
"Lifestyle",
"Community",
"Telemetry",
]
)
with tabs[0]:
render_overview(results, alerts)
with tabs[1]:
plan = {
"Insulin": {"Basal": "12u nightly", "Bolus": "1u per 12g carbs"},
"Metformin": "500mg twice daily",
"Check-ins": ["Morning CGM calibration", "Weekly telehealth"],
}
render_treatment_plan(plan, next_visit="2025-07-12 (virtual clinic)")
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("---")
st.sidebar.caption(
"Sundew status: "
+ ("✅ native gating" if _HAS_SUNDEW else "⚠️ fallback gate active")
)
if __name__ == "__main__":
main()