mgbam commited on
Commit
afba4df
·
verified ·
1 Parent(s): 286eea9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +164 -109
app.py CHANGED
@@ -1,15 +1,26 @@
1
  from __future__ import annotations
2
 
3
- from datetime import datetime, timedelta
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
- """Sundew Diabetes Commons – holistic, open Streamlit experience."""
 
 
6
 
7
  import json
8
  import logging
9
  import math
10
- import time
11
- from dataclasses import dataclass
12
- from typing import Any, Dict, List, Optional, Tuple
13
 
14
  import numpy as np
15
  import pandas as pd
@@ -18,13 +29,16 @@ from sklearn.linear_model import LogisticRegression
18
  from sklearn.pipeline import Pipeline
19
  from sklearn.preprocessing import StandardScaler
20
 
 
 
 
21
  try:
22
  from sundew import SundewAlgorithm # type: ignore[attr-defined]
23
  from sundew.config import SundewConfig
24
  from sundew.config_presets import get_preset
25
 
26
  _HAS_SUNDEW = True
27
- except Exception: # fallback when package is unavailable
28
  SundewAlgorithm = None # type: ignore
29
  SundewConfig = object # type: ignore
30
 
@@ -33,24 +47,34 @@ except Exception: # fallback when package is unavailable
33
 
34
  _HAS_SUNDEW = False
35
 
 
36
  LOGGER = logging.getLogger("sundew.diabetes.commons")
 
 
37
 
38
 
 
 
 
39
  @dataclass
40
  class SundewGateConfig:
41
  target_activation: float = 0.22
42
  temperature: float = 0.08
43
  mode: str = "tuned_v2"
44
  use_native: bool = True
 
45
 
46
 
47
- def _build_sundew_runtime(config: SundewGateConfig) -> Optional[SundewAlgorithm]:
 
48
  if not (config.use_native and _HAS_SUNDEW and SundewAlgorithm is not None):
49
  return None
50
  try:
51
  preset = get_preset(config.mode)
52
  except Exception:
 
53
  preset = SundewConfig() # type: ignore
 
54
  for attr, value in (
55
  ("target_activation_rate", config.target_activation),
56
  ("gate_temperature", config.temperature),
@@ -58,7 +82,8 @@ def _build_sundew_runtime(config: SundewGateConfig) -> Optional[SundewAlgorithm]
58
  try:
59
  setattr(preset, attr, value)
60
  except Exception:
61
- pass
 
62
  for constructor in (
63
  lambda: SundewAlgorithm(preset), # type: ignore[arg-type]
64
  lambda: SundewAlgorithm(config=preset), # type: ignore[arg-type]
@@ -66,20 +91,26 @@ def _build_sundew_runtime(config: SundewGateConfig) -> Optional[SundewAlgorithm]
66
  ):
67
  try:
68
  return constructor()
69
- except Exception:
 
70
  continue
71
  return None
72
 
73
 
74
  class AdaptiveGate:
75
- """Adapter that hides Sundew/Fallback branching."""
 
 
 
 
76
 
77
  def __init__(self, config: SundewGateConfig) -> None:
78
  self.config = config
79
  self._ema = 0.0
80
  self._tau = float(np.clip(config.target_activation, 0.05, 0.95))
81
  self._alpha = 0.05
82
- self.sundew: Optional[SundewAlgorithm] = _build_sundew_runtime(config)
 
83
 
84
  def decide(self, score: float) -> bool:
85
  if self.sundew is not None:
@@ -88,18 +119,25 @@ class AdaptiveGate:
88
  if callable(fn):
89
  try:
90
  return bool(fn(score))
91
- except Exception:
 
92
  continue
 
93
  normalized = float(np.clip(score / 1.4, 0.0, 1.0))
94
  temperature = max(self.config.temperature, 0.02)
95
  probability = 1.0 / (1.0 + math.exp(-(normalized - self._tau) / temperature))
96
- fired = bool(np.random.rand() < probability)
 
97
  self._ema = (1 - self._alpha) * self._ema + self._alpha * (1.0 if fired else 0.0)
98
  self._tau += 0.05 * (self.config.target_activation - self._ema)
99
  self._tau = float(np.clip(self._tau, 0.05, 0.95))
100
  return fired
101
 
102
 
 
 
 
 
103
  def load_example_dataset(n_rows: int = 720) -> pd.DataFrame:
104
  rng = np.random.default_rng(17)
105
  t0 = pd.Timestamp.utcnow().floor("5min") - pd.Timedelta(minutes=5 * n_rows)
@@ -112,6 +150,7 @@ def load_example_dataset(n_rows: int = 720) -> pd.DataFrame:
112
  heart_rate = 68 + (steps > 90) * rng.integers(20, 45, size=n_rows)
113
  sleep_flag = (rng.random(n_rows) < 0.12).astype(float)
114
  stress_index = rng.uniform(0, 1, n_rows)
 
115
  glucose = base + noise
116
  for i in range(n_rows):
117
  if i >= 6:
@@ -122,6 +161,7 @@ def load_example_dataset(n_rows: int = 720) -> pd.DataFrame:
122
  glucose[i] -= 15
123
  glucose[180:200] = rng.normal(62, 5, 20)
124
  glucose[350:365] = rng.normal(210, 10, 15)
 
125
  return pd.DataFrame(
126
  {
127
  "timestamp": timestamps,
@@ -139,18 +179,26 @@ def load_example_dataset(n_rows: int = 720) -> pd.DataFrame:
139
  def compute_features(df: pd.DataFrame) -> pd.DataFrame:
140
  df = df.copy().sort_values("timestamp").reset_index(drop=True)
141
  df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True)
142
- df["glucose_prev"] = df["glucose_mgdl"].shift(1)
143
- dt = (df["timestamp"].astype("int64") - df["timestamp"].shift(1).astype("int64")) / 60e9
144
- df["roc_mgdl_min"] = (df["glucose_mgdl"] - df["glucose_prev"]) / dt
 
 
 
 
145
  df["roc_mgdl_min"] = df["roc_mgdl_min"].replace([np.inf, -np.inf], 0.0).fillna(0.0)
 
146
  ema = df["glucose_mgdl"].ewm(span=48, adjust=False).mean()
147
  df["deviation"] = (df["glucose_mgdl"] - ema).fillna(0.0)
 
148
  df["iob_proxy"] = df["insulin_units"].rolling(12, min_periods=1).sum() / 12.0
149
  df["cob_proxy"] = df["carbs_g"].rolling(12, min_periods=1).sum() / 12.0
150
  df["variability"] = df["glucose_mgdl"].rolling(24, min_periods=2).std().fillna(0.0)
151
- df["activity_factor"] = (df["steps"] / 200.0 + df["hr"] / 160.0).clip(0, 1)
152
- df["sleep_flag"] = df["sleep_flag"].fillna(0.0) if "sleep_flag" in df else 0.0
153
- df["stress_index"] = df["stress_index"].fillna(0.5) if "stress_index" in df else 0.5
 
 
154
  return df[
155
  [
156
  "timestamp",
@@ -167,6 +215,10 @@ def compute_features(df: pd.DataFrame) -> pd.DataFrame:
167
  ].copy()
168
 
169
 
 
 
 
 
170
  def lightweight_score(row: pd.Series) -> float:
171
  glucose = row["glucose_mgdl"]
172
  roc = row["roc_mgdl_min"]
@@ -174,6 +226,7 @@ def lightweight_score(row: pd.Series) -> float:
174
  iob = row["iob_proxy"]
175
  cob = row["cob_proxy"]
176
  stress = row["stress_index"]
 
177
  score = 0.0
178
  score += max(0.0, (glucose - 180) / 80)
179
  score += max(0.0, (70 - glucose) / 30)
@@ -181,22 +234,22 @@ def lightweight_score(row: pd.Series) -> float:
181
  score += abs(deviation) / 100.0
182
  score += stress * 0.4
183
  score += max(0.0, (cob - iob) * 0.04)
 
184
  return float(np.clip(score, 0.0, 1.4))
185
 
186
 
187
- def train_simple_model(df: pd.DataFrame):
188
- features = df[
189
- [
190
- "glucose_mgdl",
191
- "roc_mgdl_min",
192
- "iob_proxy",
193
- "cob_proxy",
194
- "activity_factor",
195
- "variability",
196
- ]
197
- ]
198
  labels = (df["glucose_mgdl"] > 180).astype(int)
199
- model = Pipeline(
 
200
  [
201
  ("scaler", StandardScaler()),
202
  ("clf", LogisticRegression(max_iter=400, class_weight="balanced")),
@@ -205,10 +258,15 @@ def train_simple_model(df: pd.DataFrame):
205
  try:
206
  model.fit(features, labels)
207
  return model
208
- except Exception:
 
209
  return None
210
 
211
 
 
 
 
 
212
  def render_overview(
213
  results: pd.DataFrame,
214
  alerts: List[Dict[str, Any]],
@@ -218,26 +276,30 @@ def render_overview(
218
  activations = int(results["activated"].sum())
219
  activation_rate = activations / max(total, 1)
220
  energy_savings = max(0.0, 1.0 - activation_rate)
 
221
  col_a, col_b, col_c, col_d = st.columns(4)
222
  col_a.metric("Events", f"{total}")
223
  col_b.metric("Heavy activations", f"{activations} ({activation_rate:.1%})")
224
  col_c.metric("Estimated energy saved", f"{energy_savings:.1%}")
225
  col_d.metric("Alerts", f"{len(alerts)}")
 
226
  if gate_config.use_native and _HAS_SUNDEW:
227
  st.caption(
228
- "Energy savings follow 1 − activation rate. With native Sundew gating we target "
229
- f"≈{gate_config.target_activation:.0%} activations, so savings approach "
230
  f"{1 - gate_config.target_activation:.0%}."
231
  )
232
  else:
233
  st.warning(
234
- "Fallback gate active – heavy inference runs frequently, so savings mirror the observed activation rate."
235
  )
 
236
  with st.expander("Recent alerts", expanded=False):
237
  if alerts:
238
  st.table(pd.DataFrame(alerts).tail(10))
239
  else:
240
  st.info("No high-risk alerts in this window.")
 
241
  st.area_chart(results.set_index("timestamp")["glucose_mgdl"], height=220)
242
 
243
 
@@ -255,15 +317,17 @@ def render_lifestyle_support(results: pd.DataFrame) -> None:
255
  recent = results.tail(96).copy()
256
  avg_glucose = recent["glucose_mgdl"].mean()
257
  active_minutes = int((recent["activity_factor"] > 0.4).sum() * 5)
 
258
  col1, col2 = st.columns(2)
259
  col1.metric("Average glucose (8h)", f"{avg_glucose:.1f} mg/dL")
260
  col2.metric("Active minutes", f"{active_minutes} min")
 
261
  st.markdown(
262
  """
263
  - Aim for gentle movement every hour you are awake.
264
  - Pair carbohydrates with protein/fiber to smooth spikes.
265
- - Sleep flagged recently? Try 10-minute breathing before bed.
266
- - Journal one gratitude moment—stress strongly shapes risk.
267
  """
268
  )
269
 
@@ -303,19 +367,21 @@ def render_telemetry(results: pd.DataFrame, telemetry: List[Dict[str, Any]]) ->
303
  st.dataframe(results.tail(100), use_container_width=True)
304
 
305
 
 
 
 
 
306
  def main() -> None:
307
- st.set_page_config(
308
- page_title="Sundew Diabetes Commons", layout="wide", page_icon="🕊"
309
- )
310
  st.title("Sundew Diabetes Commons")
311
- st.caption(
312
- "Open, compassionate diabetes care—monitoring, treatment, lifestyle, community."
313
- )
314
 
 
315
  st.sidebar.header("Load data")
316
  uploaded = st.sidebar.file_uploader("CGM / diary CSV", type=["csv"])
317
  use_example = st.sidebar.checkbox("Use synthetic example", True)
318
 
 
319
  st.sidebar.header("Sundew configuration")
320
  use_native = st.sidebar.checkbox(
321
  "Use native Sundew gating",
@@ -324,10 +390,9 @@ def main() -> None:
324
  )
325
  target_activation = st.sidebar.slider("Target activation", 0.05, 0.90, 0.22, 0.01)
326
  temperature = st.sidebar.slider("Gate temperature", 0.02, 0.50, 0.08, 0.01)
327
- mode = st.sidebar.selectbox(
328
- "Preset", ["tuned_v2", "conservative", "aggressive", "auto_tuned"], index=0
329
- )
330
 
 
331
  if uploaded is not None:
332
  df = pd.read_csv(uploaded)
333
  elif use_example:
@@ -337,6 +402,7 @@ def main() -> None:
337
 
338
  features = compute_features(df)
339
  model = train_simple_model(features)
 
340
  gate_config = SundewGateConfig(
341
  target_activation=target_activation,
342
  temperature=temperature,
@@ -351,61 +417,60 @@ def main() -> None:
351
 
352
  progress = st.progress(0)
353
  status = st.empty()
 
354
  for idx, row in enumerate(features.itertuples(index=False), start=1):
355
- score = lightweight_score(pd.Series(row._asdict()))
 
356
  should_run = gate.decide(score)
357
- risk_proba = None
 
358
  if should_run and model is not None:
359
- sample = np.array(
360
- [
361
- [
362
- row.glucose_mgdl,
363
- row.roc_mgdl_min,
364
- row.iob_proxy,
365
- row.cob_proxy,
366
- row.activity_factor,
367
- row.variability,
368
- ]
369
- ]
370
- )
371
  try:
372
  risk_proba = float(model.predict_proba(sample)[0, 1]) # type: ignore[index]
373
- except Exception:
 
374
  risk_proba = None
375
- if risk_proba is not None and risk_proba >= 0.6:
376
- alerts.append(
377
- {
378
- "timestamp": row.timestamp,
379
- "glucose": row.glucose_mgdl,
380
- "risk": risk_proba,
381
- "message": "Check CGM, hydrate, plan balanced snack/insulin",
382
- }
383
- )
384
- records.append(
385
- {
386
  "timestamp": row.timestamp,
387
- "glucose_mgdl": row.glucose_mgdl,
388
- "roc_mgdl_min": row.roc_mgdl_min,
389
- "deviation": row.deviation,
390
- "iob_proxy": row.iob_proxy,
391
- "cob_proxy": row.cob_proxy,
392
- "variability": row.variability,
393
- "activity_factor": row.activity_factor,
394
- "score": score,
395
- "activated": should_run,
396
- "risk_proba": risk_proba,
397
- }
398
- )
399
- telemetry.append(
400
- {
401
- "timestamp": str(row.timestamp),
402
- "score": score,
403
- "activated": should_run,
404
- "risk_proba": risk_proba,
405
- }
406
- )
 
 
 
 
 
 
407
  progress.progress(idx / len(features))
408
  status.text(f"Processing event {idx}/{len(features)}")
 
409
  progress.empty()
410
  status.empty()
411
 
@@ -444,14 +509,9 @@ def main() -> None:
444
  ],
445
  }
446
  st.caption("Upload or edit schedules, medication titration guidance, and clinician notes.")
447
- uploaded_plan = st.file_uploader(
448
- "Optional plan JSON", type=["json"], key="plan_uploader"
449
- )
450
- plan_text = st.text_area(
451
- "Edit plan JSON",
452
- json.dumps(default_plan, indent=2),
453
- height=240,
454
- )
455
  plan_data = default_plan
456
  if uploaded_plan is not None:
457
  try:
@@ -463,13 +523,10 @@ def main() -> None:
463
  try:
464
  plan_data = json.loads(plan_text)
465
  except Exception as exc:
466
- st.warning(
467
- f"Using default plan because text could not be parsed: {exc}"
468
- )
469
  plan_data = default_plan
470
- next_visit = (datetime.utcnow() + timedelta(days=30)).strftime(
471
- "%Y-%m-%d (telehealth)"
472
- )
473
  render_treatment_plan(plan_data, next_visit=next_visit)
474
 
475
  with tabs[2]:
@@ -484,9 +541,7 @@ def main() -> None:
484
 
485
  st.sidebar.markdown("---")
486
  status_text = (
487
- "native gating"
488
- if gate_config.use_native and gate.sundew is not None
489
- else "fallback gate"
490
  )
491
  st.sidebar.caption(f"Sundew status: {status_text}")
492
 
 
1
  from __future__ import annotations
2
 
3
+ """Sundew Diabetes Commons – holistic, open Streamlit experience.
4
+
5
+ This app demonstrates a lightweight gating pipeline with an optional native
6
+ Sundew integration, feature engineering over CGM-like time series, a simple
7
+ logistic baseline, and a compact UI for overview, treatment, lifestyle, and
8
+ telemetry.
9
+
10
+ ⚠️ Medical disclaimer: This software is for research & educational purposes only
11
+ and does *not* provide medical advice. Always consult qualified clinicians.
12
+
13
+ Copyright (c) 2025 The Sundew Diabetes Commons authors
14
+ SPDX-License-Identifier: Apache-2.0
15
+ """
16
 
17
+ from dataclasses import dataclass
18
+ from datetime import datetime, timedelta
19
+ from typing import Any, Dict, List, Optional, Tuple
20
 
21
  import json
22
  import logging
23
  import math
 
 
 
24
 
25
  import numpy as np
26
  import pandas as pd
 
29
  from sklearn.pipeline import Pipeline
30
  from sklearn.preprocessing import StandardScaler
31
 
32
+ # -----------------------------------------------------------------------------
33
+ # Optional Sundew dependency (kept import-safe for open source distribution)
34
+ # -----------------------------------------------------------------------------
35
  try:
36
  from sundew import SundewAlgorithm # type: ignore[attr-defined]
37
  from sundew.config import SundewConfig
38
  from sundew.config_presets import get_preset
39
 
40
  _HAS_SUNDEW = True
41
+ except Exception: # pragma: no cover - fallback when package is unavailable
42
  SundewAlgorithm = None # type: ignore
43
  SundewConfig = object # type: ignore
44
 
 
47
 
48
  _HAS_SUNDEW = False
49
 
50
+
51
  LOGGER = logging.getLogger("sundew.diabetes.commons")
52
+ if not LOGGER.handlers:
53
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
54
 
55
 
56
+ # -----------------------------------------------------------------------------
57
+ # Config & Gate
58
+ # -----------------------------------------------------------------------------
59
  @dataclass
60
  class SundewGateConfig:
61
  target_activation: float = 0.22
62
  temperature: float = 0.08
63
  mode: str = "tuned_v2"
64
  use_native: bool = True
65
+ rng_seed: Optional[int] = 17
66
 
67
 
68
+ def _build_sundew_runtime(config: SundewGateConfig) -> Optional["SundewAlgorithm"]:
69
+ """Try multiple Sundew constructor forms; fall back to None if unavailable."""
70
  if not (config.use_native and _HAS_SUNDEW and SundewAlgorithm is not None):
71
  return None
72
  try:
73
  preset = get_preset(config.mode)
74
  except Exception:
75
+ LOGGER.warning("Could not load preset %s; using bare SundewConfig", config.mode)
76
  preset = SundewConfig() # type: ignore
77
+ # best-effort attribute binding
78
  for attr, value in (
79
  ("target_activation_rate", config.target_activation),
80
  ("gate_temperature", config.temperature),
 
82
  try:
83
  setattr(preset, attr, value)
84
  except Exception:
85
+ LOGGER.debug("Preset missing attribute %s", attr)
86
+ # try common constructor signatures
87
  for constructor in (
88
  lambda: SundewAlgorithm(preset), # type: ignore[arg-type]
89
  lambda: SundewAlgorithm(config=preset), # type: ignore[arg-type]
 
91
  ):
92
  try:
93
  return constructor()
94
+ except Exception as exc:
95
+ LOGGER.debug("Sundew constructor failed: %s", exc)
96
  continue
97
  return None
98
 
99
 
100
  class AdaptiveGate:
101
+ """Adapter that hides Sundew/Fallback branching.
102
+
103
+ If native Sundew is not present, uses a simple logistic gate whose threshold
104
+ self-adjusts via a moving target activation rate.
105
+ """
106
 
107
  def __init__(self, config: SundewGateConfig) -> None:
108
  self.config = config
109
  self._ema = 0.0
110
  self._tau = float(np.clip(config.target_activation, 0.05, 0.95))
111
  self._alpha = 0.05
112
+ self._rng = np.random.default_rng(config.rng_seed)
113
+ self.sundew: Optional["SundewAlgorithm"] = _build_sundew_runtime(config)
114
 
115
  def decide(self, score: float) -> bool:
116
  if self.sundew is not None:
 
119
  if callable(fn):
120
  try:
121
  return bool(fn(score))
122
+ except Exception as exc:
123
+ LOGGER.debug("Sundew.%s failed: %s", attr, exc)
124
  continue
125
+ # Fallback: temperatured logistic on a normalized score
126
  normalized = float(np.clip(score / 1.4, 0.0, 1.0))
127
  temperature = max(self.config.temperature, 0.02)
128
  probability = 1.0 / (1.0 + math.exp(-(normalized - self._tau) / temperature))
129
+ fired = bool(self._rng.random() < probability)
130
+ # EMA of activations and threshold nudging toward target rate
131
  self._ema = (1 - self._alpha) * self._ema + self._alpha * (1.0 if fired else 0.0)
132
  self._tau += 0.05 * (self.config.target_activation - self._ema)
133
  self._tau = float(np.clip(self._tau, 0.05, 0.95))
134
  return fired
135
 
136
 
137
+ # -----------------------------------------------------------------------------
138
+ # Data utilities
139
+ # -----------------------------------------------------------------------------
140
+
141
  def load_example_dataset(n_rows: int = 720) -> pd.DataFrame:
142
  rng = np.random.default_rng(17)
143
  t0 = pd.Timestamp.utcnow().floor("5min") - pd.Timedelta(minutes=5 * n_rows)
 
150
  heart_rate = 68 + (steps > 90) * rng.integers(20, 45, size=n_rows)
151
  sleep_flag = (rng.random(n_rows) < 0.12).astype(float)
152
  stress_index = rng.uniform(0, 1, n_rows)
153
+
154
  glucose = base + noise
155
  for i in range(n_rows):
156
  if i >= 6:
 
161
  glucose[i] -= 15
162
  glucose[180:200] = rng.normal(62, 5, 20)
163
  glucose[350:365] = rng.normal(210, 10, 15)
164
+
165
  return pd.DataFrame(
166
  {
167
  "timestamp": timestamps,
 
179
  def compute_features(df: pd.DataFrame) -> pd.DataFrame:
180
  df = df.copy().sort_values("timestamp").reset_index(drop=True)
181
  df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True)
182
+
183
+ # Time delta in minutes (robust vs. dtype casting)
184
+ dt_min = df["timestamp"].diff().dt.total_seconds().div(60).fillna(5.0)
185
+
186
+ # Rate of change and smoothed baseline deviation
187
+ glucose_prev = df["glucose_mgdl"].shift(1)
188
+ df["roc_mgdl_min"] = (df["glucose_mgdl"] - glucose_prev).div(dt_min)
189
  df["roc_mgdl_min"] = df["roc_mgdl_min"].replace([np.inf, -np.inf], 0.0).fillna(0.0)
190
+
191
  ema = df["glucose_mgdl"].ewm(span=48, adjust=False).mean()
192
  df["deviation"] = (df["glucose_mgdl"] - ema).fillna(0.0)
193
+
194
  df["iob_proxy"] = df["insulin_units"].rolling(12, min_periods=1).sum() / 12.0
195
  df["cob_proxy"] = df["carbs_g"].rolling(12, min_periods=1).sum() / 12.0
196
  df["variability"] = df["glucose_mgdl"].rolling(24, min_periods=2).std().fillna(0.0)
197
+
198
+ df["activity_factor"] = (df["steps"].div(200.0) + df["hr"].div(160.0)).clip(0, 1)
199
+ df["sleep_flag"] = df.get("sleep_flag", 0.0)
200
+ df["stress_index"] = df.get("stress_index", 0.5)
201
+
202
  return df[
203
  [
204
  "timestamp",
 
215
  ].copy()
216
 
217
 
218
+ # -----------------------------------------------------------------------------
219
+ # Scoring & Modeling
220
+ # -----------------------------------------------------------------------------
221
+
222
  def lightweight_score(row: pd.Series) -> float:
223
  glucose = row["glucose_mgdl"]
224
  roc = row["roc_mgdl_min"]
 
226
  iob = row["iob_proxy"]
227
  cob = row["cob_proxy"]
228
  stress = row["stress_index"]
229
+
230
  score = 0.0
231
  score += max(0.0, (glucose - 180) / 80)
232
  score += max(0.0, (70 - glucose) / 30)
 
234
  score += abs(deviation) / 100.0
235
  score += stress * 0.4
236
  score += max(0.0, (cob - iob) * 0.04)
237
+
238
  return float(np.clip(score, 0.0, 1.4))
239
 
240
 
241
+ def train_simple_model(df: pd.DataFrame) -> Optional[Pipeline]:
242
+ features = df[[
243
+ "glucose_mgdl",
244
+ "roc_mgdl_min",
245
+ "iob_proxy",
246
+ "cob_proxy",
247
+ "activity_factor",
248
+ "variability",
249
+ ]]
 
 
250
  labels = (df["glucose_mgdl"] > 180).astype(int)
251
+
252
+ model: Pipeline = Pipeline(
253
  [
254
  ("scaler", StandardScaler()),
255
  ("clf", LogisticRegression(max_iter=400, class_weight="balanced")),
 
258
  try:
259
  model.fit(features, labels)
260
  return model
261
+ except Exception as exc:
262
+ LOGGER.warning("Model training failed: %s", exc)
263
  return None
264
 
265
 
266
+ # -----------------------------------------------------------------------------
267
+ # UI rendering
268
+ # -----------------------------------------------------------------------------
269
+
270
  def render_overview(
271
  results: pd.DataFrame,
272
  alerts: List[Dict[str, Any]],
 
276
  activations = int(results["activated"].sum())
277
  activation_rate = activations / max(total, 1)
278
  energy_savings = max(0.0, 1.0 - activation_rate)
279
+
280
  col_a, col_b, col_c, col_d = st.columns(4)
281
  col_a.metric("Events", f"{total}")
282
  col_b.metric("Heavy activations", f"{activations} ({activation_rate:.1%})")
283
  col_c.metric("Estimated energy saved", f"{energy_savings:.1%}")
284
  col_d.metric("Alerts", f"{len(alerts)}")
285
+
286
  if gate_config.use_native and _HAS_SUNDEW:
287
  st.caption(
288
+ "Energy savings follow 1 activation rate. With native Sundew gating we target "
289
+ f"{gate_config.target_activation:.0%} activations, so savings approach "
290
  f"{1 - gate_config.target_activation:.0%}."
291
  )
292
  else:
293
  st.warning(
294
+ "Fallback gate active heavy inference runs frequently, so savings mirror the observed activation rate."
295
  )
296
+
297
  with st.expander("Recent alerts", expanded=False):
298
  if alerts:
299
  st.table(pd.DataFrame(alerts).tail(10))
300
  else:
301
  st.info("No high-risk alerts in this window.")
302
+
303
  st.area_chart(results.set_index("timestamp")["glucose_mgdl"], height=220)
304
 
305
 
 
317
  recent = results.tail(96).copy()
318
  avg_glucose = recent["glucose_mgdl"].mean()
319
  active_minutes = int((recent["activity_factor"] > 0.4).sum() * 5)
320
+
321
  col1, col2 = st.columns(2)
322
  col1.metric("Average glucose (8h)", f"{avg_glucose:.1f} mg/dL")
323
  col2.metric("Active minutes", f"{active_minutes} min")
324
+
325
  st.markdown(
326
  """
327
  - Aim for gentle movement every hour you are awake.
328
  - Pair carbohydrates with protein/fiber to smooth spikes.
329
+ - Sleep flagged recently? Try 10minute breathing before bed.
330
+ - Journal one gratitude momentstress strongly shapes risk.
331
  """
332
  )
333
 
 
367
  st.dataframe(results.tail(100), use_container_width=True)
368
 
369
 
370
+ # -----------------------------------------------------------------------------
371
+ # App
372
+ # -----------------------------------------------------------------------------
373
+
374
  def main() -> None:
375
+ st.set_page_config(page_title="Sundew Diabetes Commons", layout="wide", page_icon="🩺")
 
 
376
  st.title("Sundew Diabetes Commons")
377
+ st.caption("Open, compassionate diabetes care — monitoring, treatment, lifestyle, community.")
 
 
378
 
379
+ # Sidebar – data
380
  st.sidebar.header("Load data")
381
  uploaded = st.sidebar.file_uploader("CGM / diary CSV", type=["csv"])
382
  use_example = st.sidebar.checkbox("Use synthetic example", True)
383
 
384
+ # Sidebar – config
385
  st.sidebar.header("Sundew configuration")
386
  use_native = st.sidebar.checkbox(
387
  "Use native Sundew gating",
 
390
  )
391
  target_activation = st.sidebar.slider("Target activation", 0.05, 0.90, 0.22, 0.01)
392
  temperature = st.sidebar.slider("Gate temperature", 0.02, 0.50, 0.08, 0.01)
393
+ mode = st.sidebar.selectbox("Preset", ["tuned_v2", "conservative", "aggressive", "auto_tuned"], index=0)
 
 
394
 
395
+ # Data source
396
  if uploaded is not None:
397
  df = pd.read_csv(uploaded)
398
  elif use_example:
 
402
 
403
  features = compute_features(df)
404
  model = train_simple_model(features)
405
+
406
  gate_config = SundewGateConfig(
407
  target_activation=target_activation,
408
  temperature=temperature,
 
417
 
418
  progress = st.progress(0)
419
  status = st.empty()
420
+
421
  for idx, row in enumerate(features.itertuples(index=False), start=1):
422
+ row_s = pd.Series(row._asdict())
423
+ score = lightweight_score(row_s)
424
  should_run = gate.decide(score)
425
+ risk_proba: Optional[float] = None
426
+
427
  if should_run and model is not None:
428
+ sample = np.array([[
429
+ row.glucose_mgdl,
430
+ row.roc_mgdl_min,
431
+ row.iob_proxy,
432
+ row.cob_proxy,
433
+ row.activity_factor,
434
+ row.variability,
435
+ ]])
 
 
 
 
436
  try:
437
  risk_proba = float(model.predict_proba(sample)[0, 1]) # type: ignore[index]
438
+ except Exception as exc:
439
+ LOGGER.debug("predict_proba failed: %s", exc)
440
  risk_proba = None
441
+
442
+ if (risk_proba is not None) and (risk_proba >= 0.6):
443
+ alerts.append({
 
 
 
 
 
 
 
 
444
  "timestamp": row.timestamp,
445
+ "glucose": row.glucose_mgdl,
446
+ "risk": risk_proba,
447
+ "message": "Check CGM, hydrate, plan balanced snack/insulin",
448
+ })
449
+
450
+ records.append({
451
+ "timestamp": row.timestamp,
452
+ "glucose_mgdl": row.glucose_mgdl,
453
+ "roc_mgdl_min": row.roc_mgdl_min,
454
+ "deviation": row.deviation,
455
+ "iob_proxy": row.iob_proxy,
456
+ "cob_proxy": row.cob_proxy,
457
+ "variability": row.variability,
458
+ "activity_factor": row.activity_factor,
459
+ "score": score,
460
+ "activated": should_run,
461
+ "risk_proba": risk_proba,
462
+ })
463
+
464
+ telemetry.append({
465
+ "timestamp": str(row.timestamp),
466
+ "score": score,
467
+ "activated": should_run,
468
+ "risk_proba": risk_proba,
469
+ })
470
+
471
  progress.progress(idx / len(features))
472
  status.text(f"Processing event {idx}/{len(features)}")
473
+
474
  progress.empty()
475
  status.empty()
476
 
 
509
  ],
510
  }
511
  st.caption("Upload or edit schedules, medication titration guidance, and clinician notes.")
512
+ uploaded_plan = st.file_uploader("Optional plan JSON", type=["json"], key="plan_uploader")
513
+ plan_text = st.text_area("Edit plan JSON", json.dumps(default_plan, indent=2), height=240)
514
+
 
 
 
 
 
515
  plan_data = default_plan
516
  if uploaded_plan is not None:
517
  try:
 
523
  try:
524
  plan_data = json.loads(plan_text)
525
  except Exception as exc:
526
+ st.warning(f"Using default plan because text could not be parsed: {exc}")
 
 
527
  plan_data = default_plan
528
+
529
+ next_visit = (datetime.utcnow() + timedelta(days=30)).strftime("%Y-%m-%d (telehealth)")
 
530
  render_treatment_plan(plan_data, next_visit=next_visit)
531
 
532
  with tabs[2]:
 
541
 
542
  st.sidebar.markdown("---")
543
  status_text = (
544
+ "native gating" if gate_config.use_native and gate.sundew is not None else "fallback gate"
 
 
545
  )
546
  st.sidebar.caption(f"Sundew status: {status_text}")
547