mgbam commited on
Commit
4f0e90f
·
verified ·
1 Parent(s): 6c96ca1

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +1362 -498
app.py CHANGED
@@ -1,498 +1,1362 @@
1
- from __future__ import annotations
2
- from datetime import datetime, timedelta
3
-
4
- """Sundew Diabetes Commons – holistic, open Streamlit experience."""
5
-
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
16
- import streamlit as st
17
- 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
-
31
- def get_preset(_: str) -> Any: # type: ignore
32
- return None
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),
57
- ):
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]
65
- lambda: 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:
86
- for attr in ("decide", "step", "open"):
87
- fn = getattr(self.sundew, attr, None)
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 * (
98
- 1.0 if fired else 0.0
99
- )
100
- self._tau += 0.05 * (self.config.target_activation - self._ema)
101
- self._tau = float(np.clip(self._tau, 0.05, 0.95))
102
- return fired
103
-
104
-
105
- def load_example_dataset(n_rows: int = 720) -> pd.DataFrame:
106
- rng = np.random.default_rng(17)
107
- t0 = pd.Timestamp.utcnow().floor("5min") - pd.Timedelta(minutes=5 * n_rows)
108
- timestamps = [t0 + pd.Timedelta(minutes=5 * i) for i in range(n_rows)]
109
- base = 118 + 28 * np.sin(np.linspace(0, 7 * math.pi, n_rows))
110
- noise = rng.normal(0, 12, n_rows)
111
- meals = (rng.random(n_rows) < 0.05).astype(float) * rng.normal(50, 18, n_rows).clip(
112
- 0, 150
113
- )
114
- insulin = (rng.random(n_rows) < 0.03).astype(float) * rng.normal(
115
- 4.2, 1.5, n_rows
116
- ).clip(0, 10)
117
- steps = rng.integers(0, 200, size=n_rows)
118
- heart_rate = 68 + (steps > 90) * rng.integers(20, 45, size=n_rows)
119
- sleep_flag = (rng.random(n_rows) < 0.12).astype(float)
120
- stress_index = rng.uniform(0, 1, n_rows)
121
- glucose = base + noise
122
- for i in range(n_rows):
123
- if i >= 6:
124
- glucose[i] += 0.4 * meals[i - 6 : i].sum() / 6
125
- if i >= 4:
126
- glucose[i] -= 1.2 * insulin[i - 4 : i].sum() / 4
127
- if steps[i] > 100:
128
- glucose[i] -= 15
129
- glucose[180:200] = rng.normal(62, 5, 20)
130
- glucose[350:365] = rng.normal(210, 10, 15)
131
- return pd.DataFrame(
132
- {
133
- "timestamp": timestamps,
134
- "glucose_mgdl": np.round(np.clip(glucose, 40, 350), 1),
135
- "carbs_g": np.round(meals, 1),
136
- "insulin_units": np.round(insulin, 1),
137
- "steps": steps.astype(int),
138
- "hr": (heart_rate + rng.normal(0, 5, n_rows)).round().astype(int),
139
- "sleep_flag": sleep_flag,
140
- "stress_index": stress_index,
141
- }
142
- )
143
-
144
-
145
- def compute_features(df: pd.DataFrame) -> pd.DataFrame:
146
- df = df.copy().sort_values("timestamp").reset_index(drop=True)
147
- df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True)
148
- df["glucose_prev"] = df["glucose_mgdl"].shift(1)
149
- dt = (
150
- df["timestamp"].astype("int64") - df["timestamp"].shift(1).astype("int64")
151
- ) / 60e9
152
- df["roc_mgdl_min"] = (df["glucose_mgdl"] - df["glucose_prev"]) / dt
153
- df["roc_mgdl_min"] = df["roc_mgdl_min"].replace([np.inf, -np.inf], 0.0).fillna(0.0)
154
- ema = df["glucose_mgdl"].ewm(span=48, adjust=False).mean()
155
- df["deviation"] = (df["glucose_mgdl"] - ema).fillna(0.0)
156
- df["iob_proxy"] = df["insulin_units"].rolling(12, min_periods=1).sum() / 12.0
157
- df["cob_proxy"] = df["carbs_g"].rolling(12, min_periods=1).sum() / 12.0
158
- df["variability"] = df["glucose_mgdl"].rolling(24, min_periods=2).std().fillna(0.0)
159
- df["activity_factor"] = (df["steps"] / 200.0 + df["hr"] / 160.0).clip(0, 1)
160
- df["sleep_flag"] = df["sleep_flag"].fillna(0.0) if "sleep_flag" in df else 0.0
161
- df["stress_index"] = df["stress_index"].fillna(0.5) if "stress_index" in df else 0.5
162
- return df[
163
- [
164
- "timestamp",
165
- "glucose_mgdl",
166
- "roc_mgdl_min",
167
- "deviation",
168
- "iob_proxy",
169
- "cob_proxy",
170
- "variability",
171
- "activity_factor",
172
- "sleep_flag",
173
- "stress_index",
174
- ]
175
- ].copy()
176
-
177
-
178
- def lightweight_score(row: pd.Series) -> float:
179
- glucose = row["glucose_mgdl"]
180
- roc = row["roc_mgdl_min"]
181
- deviation = row["deviation"]
182
- iob = row["iob_proxy"]
183
- cob = row["cob_proxy"]
184
- stress = row["stress_index"]
185
- score = 0.0
186
- score += max(0.0, (glucose - 180) / 80)
187
- score += max(0.0, (70 - glucose) / 30)
188
- score += abs(roc) / 6.0
189
- score += abs(deviation) / 100.0
190
- score += stress * 0.4
191
- score += max(0.0, (cob - iob) * 0.04)
192
- return float(np.clip(score, 0.0, 1.4))
193
-
194
-
195
- def train_simple_model(df: pd.DataFrame):
196
- features = df[
197
- [
198
- "glucose_mgdl",
199
- "roc_mgdl_min",
200
- "iob_proxy",
201
- "cob_proxy",
202
- "activity_factor",
203
- "variability",
204
- ]
205
- ]
206
- labels = (df["glucose_mgdl"] > 180).astype(int)
207
- model = Pipeline(
208
- [
209
- ("scaler", StandardScaler()),
210
- ("clf", LogisticRegression(max_iter=400, class_weight="balanced")),
211
- ]
212
- )
213
- try:
214
- model.fit(features, labels)
215
- return model
216
- except Exception:
217
- return None
218
-
219
-
220
- def render_overview(
221
- results: pd.DataFrame,
222
- alerts: List[Dict[str, Any]],
223
- gate_config: SundewGateConfig,
224
- ) -> None:
225
- total = len(results)
226
- activations = int(results["activated"].sum())
227
- activation_rate = activations / max(total, 1)
228
- energy_savings = max(0.0, 1.0 - activation_rate)
229
- col_a, col_b, col_c, col_d = st.columns(4)
230
- col_a.metric("Events", f"{total}")
231
- col_b.metric("Heavy activations", f"{activations} ({activation_rate:.1%})")
232
- col_c.metric("Estimated energy saved", f"{energy_savings:.1%}")
233
- col_d.metric("Alerts", f"{len(alerts)}")
234
- if gate_config.use_native and _HAS_SUNDEW:
235
- st.caption(
236
- "Energy savings follow 1 − activation rate. With native Sundew gating we target "
237
- f"≈{gate_config.target_activation:.0%} activations, so savings approach "
238
- f"{1 - gate_config.target_activation:.0%}."
239
- )
240
- else:
241
- st.warning(
242
- "Fallback gate active – heavy inference runs frequently, so savings mirror the observed activation rate."
243
- )
244
- with st.expander("Recent alerts", expanded=False):
245
- if alerts:
246
- st.table(pd.DataFrame(alerts).tail(10))
247
- else:
248
- st.info("No high-risk alerts in this window.")
249
- st.area_chart(results.set_index("timestamp")["glucose_mgdl"], height=220)
250
-
251
-
252
- def render_treatment_plan(medications: Dict[str, Any], next_visit: str) -> None:
253
- st.subheader("Full-cycle treatment support")
254
- st.write(
255
- "Upload or edit medication schedules, insulin titration guidance, and clinician notes."
256
- )
257
- st.json(medications, expanded=False)
258
- st.caption(f"Next scheduled review: {next_visit}")
259
-
260
-
261
- def render_lifestyle_support(results: pd.DataFrame) -> None:
262
- st.subheader("Lifestyle & wellbeing")
263
- recent = results.tail(96).copy()
264
- avg_glucose = recent["glucose_mgdl"].mean()
265
- active_minutes = int((recent["activity_factor"] > 0.4).sum() * 5)
266
- col1, col2 = st.columns(2)
267
- col1.metric("Average glucose (8h)", f"{avg_glucose:.1f} mg/dL")
268
- col2.metric("Active minutes", f"{active_minutes} min")
269
- st.markdown(
270
- """
271
- - Aim for gentle movement every hour you are awake.
272
- - Pair carbohydrates with protein/fiber to smooth spikes.
273
- - Sleep flagged recently? Try 10-minute breathing before bed.
274
- - Journal one gratitude moment—stress strongly shapes risk.
275
- """
276
- )
277
-
278
-
279
- def render_community_actions() -> Dict[str, List[str]]:
280
- st.subheader("Community impact")
281
- st.write(
282
- "Invite families, caregivers, and clinics to the commons. Set up alerts, shared logs, and outreach."
283
- )
284
- contact_list = [
285
- "SMS: +233-200-000-111",
286
- "WhatsApp: Care Circle Group",
287
- "Clinic portal: sundew.health/community",
288
- ]
289
- st.table(pd.DataFrame({"Support channel": contact_list}))
290
- return {
291
- "Desired partners": ["Rural clinics", "Youth ambassadors", "Nutrition co-ops"],
292
- "Needs": ["Smartphone grants", "Solar charging kits", "Translation volunteers"],
293
- }
294
-
295
-
296
- def render_telemetry(results: pd.DataFrame, telemetry: List[Dict[str, Any]]) -> None:
297
- st.subheader("Telemetry & export")
298
- st.write(
299
- "Download event-level telemetry for validation, research, or regulatory reporting."
300
- )
301
- st.caption(
302
- "Energy savings are computed as 1 minus the observed activation rate. When the gate stays mostly open, savings naturally trend toward zero."
303
- )
304
- json_payload = json.dumps(telemetry, default=str, indent=2)
305
- st.download_button(
306
- label="Download telemetry (JSON)",
307
- data=json_payload,
308
- file_name="sundew_diabetes_telemetry.json",
309
- mime="application/json",
310
- )
311
- st.dataframe(results.tail(100), use_container_width=True)
312
-
313
-
314
- def main() -> None:
315
- st.set_page_config(
316
- page_title="Sundew Diabetes Commons", layout="wide", page_icon="🕊"
317
- )
318
- st.title("Sundew Diabetes Commons")
319
- st.caption(
320
- "Open, compassionate diabetes care—monitoring, treatment, lifestyle, community."
321
- )
322
-
323
- st.sidebar.header("Load data")
324
- uploaded = st.sidebar.file_uploader("CGM / diary CSV", type=["csv"])
325
- use_example = st.sidebar.checkbox("Use synthetic example", True)
326
-
327
- st.sidebar.header("Sundew configuration")
328
- use_native = st.sidebar.checkbox(
329
- "Use native Sundew gating",
330
- value=_HAS_SUNDEW,
331
- help="Disable to demo the lightweight fallback gate only.",
332
- )
333
- target_activation = st.sidebar.slider("Target activation", 0.05, 0.90, 0.22, 0.01)
334
- temperature = st.sidebar.slider("Gate temperature", 0.02, 0.50, 0.08, 0.01)
335
- mode = st.sidebar.selectbox(
336
- "Preset", ["tuned_v2", "conservative", "aggressive", "auto_tuned"], index=0
337
- )
338
-
339
- if uploaded is not None:
340
- df = pd.read_csv(uploaded)
341
- elif use_example:
342
- df = load_example_dataset()
343
- else:
344
- st.stop()
345
-
346
- features = compute_features(df)
347
- model = train_simple_model(features)
348
- gate_config = SundewGateConfig(
349
- target_activation=target_activation,
350
- temperature=temperature,
351
- mode=mode,
352
- use_native=use_native,
353
- )
354
- gate = AdaptiveGate(gate_config)
355
-
356
- telemetry: List[Dict[str, Any]] = []
357
- records: List[Dict[str, Any]] = []
358
- alerts: List[Dict[str, Any]] = []
359
-
360
- progress = st.progress(0)
361
- status = st.empty()
362
- for idx, row in enumerate(features.itertuples(index=False), start=1):
363
- score = lightweight_score(pd.Series(row._asdict()))
364
- should_run = gate.decide(score)
365
- risk_proba = None
366
- if should_run and model is not None:
367
- sample = np.array(
368
- [
369
- [
370
- row.glucose_mgdl,
371
- row.roc_mgdl_min,
372
- row.iob_proxy,
373
- row.cob_proxy,
374
- row.activity_factor,
375
- row.variability,
376
- ]
377
- ]
378
- )
379
- try:
380
- risk_proba = float(model.predict_proba(sample)[0, 1]) # type: ignore[index]
381
- except Exception:
382
- risk_proba = None
383
- if risk_proba is not None and risk_proba >= 0.6:
384
- alerts.append(
385
- {
386
- "timestamp": row.timestamp,
387
- "glucose": row.glucose_mgdl,
388
- "risk": risk_proba,
389
- "message": "Check CGM, hydrate, plan balanced snack/insulin",
390
- }
391
- )
392
- records.append(
393
- {
394
- "timestamp": row.timestamp,
395
- "glucose_mgdl": row.glucose_mgdl,
396
- "roc_mgdl_min": row.roc_mgdl_min,
397
- "deviation": row.deviation,
398
- "iob_proxy": row.iob_proxy,
399
- "cob_proxy": row.cob_proxy,
400
- "variability": row.variability,
401
- "activity_factor": row.activity_factor,
402
- "score": score,
403
- "activated": should_run,
404
- "risk_proba": risk_proba,
405
- }
406
- )
407
- telemetry.append(
408
- {
409
- "timestamp": str(row.timestamp),
410
- "score": score,
411
- "activated": should_run,
412
- "risk_proba": risk_proba,
413
- }
414
- )
415
- progress.progress(idx / len(features))
416
- status.text(f"Processing event {idx}/{len(features)}")
417
- progress.empty()
418
- status.empty()
419
-
420
- results = pd.DataFrame(records)
421
-
422
-
423
- tabs = st.tabs(["Overview", "Treatment", "Lifestyle", "Community", "Telemetry"])
424
- with tabs[0]:
425
- render_overview(results, alerts, gate_config)
426
- with tabs[1]:
427
- st.subheader("Full-cycle treatment support")
428
- default_plan = {
429
- "Insulin": {
430
- "Basal": "14u glargine at 21:00",
431
- "Bolus": "1u per 10g carbs + correction 1u per 40 mg/dL over 140",
432
- },
433
- "Oral medications": {
434
- "Metformin": "500mg breakfast + 500mg dinner",
435
- "Empagliflozin": "10mg once daily (if eGFR > 45)",
436
- },
437
- "Monitoring": [
438
- "CGM sensor change every 10 days",
439
- "Morning fasted CGM calibration",
440
- "Weekly telehealth coaching",
441
- "Quarterly in-person clinician review",
442
- ],
443
- "Safety plan": [
444
- "Carry glucose tabs + glucagon kit",
445
- "Emergency contact: +233-200-000-888",
446
- ],
447
- "Lifestyle": [
448
- "30 min brisk walk 5x/week",
449
- "Bedtime snack if glucose < 110 mg/dL",
450
- "Hydrate 2L water daily unless contraindicated",
451
- ],
452
- }
453
- st.caption(
454
- "Upload or edit schedules, medication titration guidance, and clinician notes."
455
- )
456
- uploaded_plan = st.file_uploader(
457
- "Optional plan JSON", type=["json"], key="plan_uploader"
458
- )
459
- plan_text = st.text_area(
460
- "Edit plan JSON",
461
- json.dumps(default_plan, indent=2),
462
- height=240,
463
- )
464
- plan_data = default_plan
465
- if uploaded_plan is not None:
466
- try:
467
- plan_data = json.load(uploaded_plan)
468
- except Exception as exc:
469
- st.error(f"Could not parse uploaded plan JSON: {exc}")
470
- plan_data = default_plan
471
- else:
472
- try:
473
- plan_data = json.loads(plan_text)
474
- except Exception as exc:
475
- st.warning(f"Using default plan because text could not be parsed: {exc}")
476
- plan_data = default_plan
477
- next_visit = (datetime.utcnow() + timedelta(days=30)).strftime(
478
- "%Y-%m-%d (telehealth)"
479
- )
480
- render_treatment_plan(plan_data, next_visit=next_visit)
481
- with tabs[2]:
482
- render_lifestyle_support(results)
483
- with tabs[3]:
484
- community_items = render_community_actions()
485
- st.json(community_items, expanded=False)
486
- with tabs[4]:
487
- render_telemetry(results, telemetry)
488
- st.sidebar.markdown("---")
489
- status_text = (
490
- "native gating"
491
- if gate_config.use_native and gate.sundew is not None
492
- else "fallback gate"
493
- )
494
- st.sidebar.caption(f"Sundew status: {status_text}")
495
-
496
-
497
- if __name__ == "__main__":
498
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+
4
+ from datetime import datetime, timedelta
5
+
6
+
7
+
8
+
9
+
10
+ """Sundew Diabetes Commons – holistic, open Streamlit experience."""
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+ import json
20
+
21
+
22
+ import logging
23
+
24
+
25
+ import math
26
+
27
+
28
+ import time
29
+
30
+
31
+ from dataclasses import dataclass
32
+
33
+
34
+ from typing import Any, Dict, List, Optional, Tuple
35
+
36
+
37
+
38
+
39
+
40
+ import numpy as np
41
+
42
+
43
+ import pandas as pd
44
+
45
+
46
+ import streamlit as st
47
+
48
+
49
+ from sklearn.linear_model import LogisticRegression
50
+
51
+
52
+ from sklearn.pipeline import Pipeline
53
+
54
+
55
+ from sklearn.preprocessing import StandardScaler
56
+
57
+
58
+
59
+
60
+
61
+ try:
62
+
63
+
64
+ from sundew import SundewAlgorithm # type: ignore[attr-defined]
65
+
66
+
67
+ from sundew.config import SundewConfig
68
+
69
+
70
+ from sundew.config_presets import get_preset
71
+
72
+
73
+
74
+
75
+
76
+ _HAS_SUNDEW = True
77
+
78
+
79
+ except Exception: # fallback when package is unavailable
80
+
81
+
82
+ SundewAlgorithm = None # type: ignore
83
+
84
+
85
+ SundewConfig = object # type: ignore
86
+
87
+
88
+
89
+
90
+
91
+ def get_preset(_: str) -> Any: # type: ignore
92
+
93
+
94
+ return None
95
+
96
+
97
+
98
+
99
+
100
+ _HAS_SUNDEW = False
101
+
102
+
103
+
104
+
105
+
106
+ LOGGER = logging.getLogger("sundew.diabetes.commons")
107
+
108
+
109
+
110
+
111
+
112
+
113
+
114
+
115
+ @dataclass
116
+
117
+
118
+ class SundewGateConfig:
119
+
120
+
121
+ target_activation: float = 0.22
122
+
123
+
124
+ temperature: float = 0.08
125
+
126
+
127
+ mode: str = "tuned_v2"
128
+
129
+
130
+ use_native: bool = True
131
+
132
+
133
+
134
+
135
+
136
+
137
+
138
+
139
+ def _build_sundew_runtime(config: SundewGateConfig) -> Optional[SundewAlgorithm]:
140
+
141
+
142
+ if not (config.use_native and _HAS_SUNDEW and SundewAlgorithm is not None):
143
+
144
+
145
+ return None
146
+
147
+
148
+ try:
149
+
150
+
151
+ preset = get_preset(config.mode)
152
+
153
+
154
+ except Exception:
155
+
156
+
157
+ preset = SundewConfig() # type: ignore
158
+
159
+
160
+ for attr, value in (
161
+
162
+
163
+ ("target_activation_rate", config.target_activation),
164
+
165
+
166
+ ("gate_temperature", config.temperature),
167
+
168
+
169
+ ):
170
+
171
+
172
+ try:
173
+
174
+
175
+ setattr(preset, attr, value)
176
+
177
+
178
+ except Exception:
179
+
180
+
181
+ pass
182
+
183
+
184
+ for constructor in (
185
+
186
+
187
+ lambda: SundewAlgorithm(preset), # type: ignore[arg-type]
188
+
189
+
190
+ lambda: SundewAlgorithm(config=preset), # type: ignore[arg-type]
191
+
192
+
193
+ lambda: SundewAlgorithm(),
194
+
195
+
196
+ ):
197
+
198
+
199
+ try:
200
+
201
+
202
+ return constructor()
203
+
204
+
205
+ except Exception:
206
+
207
+
208
+ continue
209
+
210
+
211
+ return None
212
+
213
+
214
+
215
+
216
+
217
+
218
+
219
+
220
+ class AdaptiveGate:
221
+
222
+
223
+ """Adapter that hides Sundew/Fallback branching."""
224
+
225
+
226
+
227
+
228
+
229
+ def __init__(self, config: SundewGateConfig) -> None:
230
+
231
+
232
+ self.config = config
233
+
234
+
235
+ self._ema = 0.0
236
+
237
+
238
+ self._tau = float(np.clip(config.target_activation, 0.05, 0.95))
239
+
240
+
241
+ self._alpha = 0.05
242
+
243
+
244
+ self.sundew: Optional[SundewAlgorithm] = _build_sundew_runtime(config)
245
+
246
+
247
+
248
+
249
+
250
+ def decide(self, score: float) -> bool:
251
+
252
+
253
+ if self.sundew is not None:
254
+
255
+
256
+ for attr in ("decide", "step", "open"):
257
+
258
+
259
+ fn = getattr(self.sundew, attr, None)
260
+
261
+
262
+ if callable(fn):
263
+
264
+
265
+ try:
266
+
267
+
268
+ return bool(fn(score))
269
+
270
+
271
+ except Exception:
272
+
273
+
274
+ continue
275
+
276
+
277
+ normalized = float(np.clip(score / 1.4, 0.0, 1.0))
278
+
279
+
280
+ temperature = max(self.config.temperature, 0.02)
281
+
282
+
283
+ probability = 1.0 / (1.0 + math.exp(-(normalized - self._tau) / temperature))
284
+
285
+
286
+ fired = bool(np.random.rand() < probability)
287
+
288
+
289
+ self._ema = (1 - self._alpha) * self._ema + self._alpha * (
290
+
291
+
292
+ 1.0 if fired else 0.0
293
+
294
+
295
+ )
296
+
297
+
298
+ self._tau += 0.05 * (self.config.target_activation - self._ema)
299
+
300
+
301
+ self._tau = float(np.clip(self._tau, 0.05, 0.95))
302
+
303
+
304
+ return fired
305
+
306
+
307
+
308
+
309
+
310
+
311
+
312
+
313
+ def load_example_dataset(n_rows: int = 720) -> pd.DataFrame:
314
+
315
+
316
+ rng = np.random.default_rng(17)
317
+
318
+
319
+ t0 = pd.Timestamp.utcnow().floor("5min") - pd.Timedelta(minutes=5 * n_rows)
320
+
321
+
322
+ timestamps = [t0 + pd.Timedelta(minutes=5 * i) for i in range(n_rows)]
323
+
324
+
325
+ base = 118 + 28 * np.sin(np.linspace(0, 7 * math.pi, n_rows))
326
+
327
+
328
+ noise = rng.normal(0, 12, n_rows)
329
+
330
+
331
+ meals = (rng.random(n_rows) < 0.05).astype(float) * rng.normal(50, 18, n_rows).clip(
332
+
333
+
334
+ 0, 150
335
+
336
+
337
+ )
338
+
339
+
340
+ insulin = (rng.random(n_rows) < 0.03).astype(float) * rng.normal(
341
+
342
+
343
+ 4.2, 1.5, n_rows
344
+
345
+
346
+ ).clip(0, 10)
347
+
348
+
349
+ steps = rng.integers(0, 200, size=n_rows)
350
+
351
+
352
+ heart_rate = 68 + (steps > 90) * rng.integers(20, 45, size=n_rows)
353
+
354
+
355
+ sleep_flag = (rng.random(n_rows) < 0.12).astype(float)
356
+
357
+
358
+ stress_index = rng.uniform(0, 1, n_rows)
359
+
360
+
361
+ glucose = base + noise
362
+
363
+
364
+ for i in range(n_rows):
365
+
366
+
367
+ if i >= 6:
368
+
369
+
370
+ glucose[i] += 0.4 * meals[i - 6 : i].sum() / 6
371
+
372
+
373
+ if i >= 4:
374
+
375
+
376
+ glucose[i] -= 1.2 * insulin[i - 4 : i].sum() / 4
377
+
378
+
379
+ if steps[i] > 100:
380
+
381
+
382
+ glucose[i] -= 15
383
+
384
+
385
+ glucose[180:200] = rng.normal(62, 5, 20)
386
+
387
+
388
+ glucose[350:365] = rng.normal(210, 10, 15)
389
+
390
+
391
+ return pd.DataFrame(
392
+
393
+
394
+ {
395
+
396
+
397
+ "timestamp": timestamps,
398
+
399
+
400
+ "glucose_mgdl": np.round(np.clip(glucose, 40, 350), 1),
401
+
402
+
403
+ "carbs_g": np.round(meals, 1),
404
+
405
+
406
+ "insulin_units": np.round(insulin, 1),
407
+
408
+
409
+ "steps": steps.astype(int),
410
+
411
+
412
+ "hr": (heart_rate + rng.normal(0, 5, n_rows)).round().astype(int),
413
+
414
+
415
+ "sleep_flag": sleep_flag,
416
+
417
+
418
+ "stress_index": stress_index,
419
+
420
+
421
+ }
422
+
423
+
424
+ )
425
+
426
+
427
+
428
+
429
+
430
+
431
+
432
+
433
+ def compute_features(df: pd.DataFrame) -> pd.DataFrame:
434
+
435
+
436
+ df = df.copy().sort_values("timestamp").reset_index(drop=True)
437
+
438
+
439
+ df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True)
440
+
441
+
442
+ df["glucose_prev"] = df["glucose_mgdl"].shift(1)
443
+
444
+
445
+ dt = (
446
+
447
+
448
+ df["timestamp"].astype("int64") - df["timestamp"].shift(1).astype("int64")
449
+
450
+
451
+ ) / 60e9
452
+
453
+
454
+ df["roc_mgdl_min"] = (df["glucose_mgdl"] - df["glucose_prev"]) / dt
455
+
456
+
457
+ df["roc_mgdl_min"] = df["roc_mgdl_min"].replace([np.inf, -np.inf], 0.0).fillna(0.0)
458
+
459
+
460
+ ema = df["glucose_mgdl"].ewm(span=48, adjust=False).mean()
461
+
462
+
463
+ df["deviation"] = (df["glucose_mgdl"] - ema).fillna(0.0)
464
+
465
+
466
+ df["iob_proxy"] = df["insulin_units"].rolling(12, min_periods=1).sum() / 12.0
467
+
468
+
469
+ df["cob_proxy"] = df["carbs_g"].rolling(12, min_periods=1).sum() / 12.0
470
+
471
+
472
+ df["variability"] = df["glucose_mgdl"].rolling(24, min_periods=2).std().fillna(0.0)
473
+
474
+
475
+ df["activity_factor"] = (df["steps"] / 200.0 + df["hr"] / 160.0).clip(0, 1)
476
+
477
+
478
+ df["sleep_flag"] = df["sleep_flag"].fillna(0.0) if "sleep_flag" in df else 0.0
479
+
480
+
481
+ df["stress_index"] = df["stress_index"].fillna(0.5) if "stress_index" in df else 0.5
482
+
483
+
484
+ return df[
485
+
486
+
487
+ [
488
+
489
+
490
+ "timestamp",
491
+
492
+
493
+ "glucose_mgdl",
494
+
495
+
496
+ "roc_mgdl_min",
497
+
498
+
499
+ "deviation",
500
+
501
+
502
+ "iob_proxy",
503
+
504
+
505
+ "cob_proxy",
506
+
507
+
508
+ "variability",
509
+
510
+
511
+ "activity_factor",
512
+
513
+
514
+ "sleep_flag",
515
+
516
+
517
+ "stress_index",
518
+
519
+
520
+ ]
521
+
522
+
523
+ ].copy()
524
+
525
+
526
+
527
+
528
+
529
+
530
+
531
+
532
+ def lightweight_score(row: pd.Series) -> float:
533
+
534
+
535
+ glucose = row["glucose_mgdl"]
536
+
537
+
538
+ roc = row["roc_mgdl_min"]
539
+
540
+
541
+ deviation = row["deviation"]
542
+
543
+
544
+ iob = row["iob_proxy"]
545
+
546
+
547
+ cob = row["cob_proxy"]
548
+
549
+
550
+ stress = row["stress_index"]
551
+
552
+
553
+ score = 0.0
554
+
555
+
556
+ score += max(0.0, (glucose - 180) / 80)
557
+
558
+
559
+ score += max(0.0, (70 - glucose) / 30)
560
+
561
+
562
+ score += abs(roc) / 6.0
563
+
564
+
565
+ score += abs(deviation) / 100.0
566
+
567
+
568
+ score += stress * 0.4
569
+
570
+
571
+ score += max(0.0, (cob - iob) * 0.04)
572
+
573
+
574
+ return float(np.clip(score, 0.0, 1.4))
575
+
576
+
577
+
578
+
579
+
580
+
581
+
582
+
583
+ def train_simple_model(df: pd.DataFrame):
584
+
585
+
586
+ features = df[
587
+
588
+
589
+ [
590
+
591
+
592
+ "glucose_mgdl",
593
+
594
+
595
+ "roc_mgdl_min",
596
+
597
+
598
+ "iob_proxy",
599
+
600
+
601
+ "cob_proxy",
602
+
603
+
604
+ "activity_factor",
605
+
606
+
607
+ "variability",
608
+
609
+
610
+ ]
611
+
612
+
613
+ ]
614
+
615
+
616
+ labels = (df["glucose_mgdl"] > 180).astype(int)
617
+
618
+
619
+ model = Pipeline(
620
+
621
+
622
+ [
623
+
624
+
625
+ ("scaler", StandardScaler()),
626
+
627
+
628
+ ("clf", LogisticRegression(max_iter=400, class_weight="balanced")),
629
+
630
+
631
+ ]
632
+
633
+
634
+ )
635
+
636
+
637
+ try:
638
+
639
+
640
+ model.fit(features, labels)
641
+
642
+
643
+ return model
644
+
645
+
646
+ except Exception:
647
+
648
+
649
+ return None
650
+
651
+
652
+
653
+
654
+
655
+
656
+
657
+
658
+ def render_overview(
659
+
660
+
661
+ results: pd.DataFrame,
662
+
663
+
664
+ alerts: List[Dict[str, Any]],
665
+
666
+
667
+ gate_config: SundewGateConfig,
668
+
669
+
670
+ ) -> None:
671
+
672
+
673
+ total = len(results)
674
+
675
+
676
+ activations = int(results["activated"].sum())
677
+
678
+
679
+ activation_rate = activations / max(total, 1)
680
+
681
+
682
+ energy_savings = max(0.0, 1.0 - activation_rate)
683
+
684
+
685
+ col_a, col_b, col_c, col_d = st.columns(4)
686
+
687
+
688
+ col_a.metric("Events", f"{total}")
689
+
690
+
691
+ col_b.metric("Heavy activations", f"{activations} ({activation_rate:.1%})")
692
+
693
+
694
+ col_c.metric("Estimated energy saved", f"{energy_savings:.1%}")
695
+
696
+
697
+ col_d.metric("Alerts", f"{len(alerts)}")
698
+
699
+
700
+ if gate_config.use_native and _HAS_SUNDEW:
701
+
702
+
703
+ st.caption(
704
+
705
+
706
+ "Energy savings follow 1 − activation rate. With native Sundew gating we target "
707
+
708
+
709
+ f"≈{gate_config.target_activation:.0%} activations, so savings approach "
710
+
711
+
712
+ f"{1 - gate_config.target_activation:.0%}."
713
+
714
+
715
+ )
716
+
717
+
718
+ else:
719
+
720
+
721
+ st.warning(
722
+
723
+
724
+ "Fallback gate active – heavy inference runs frequently, so savings mirror the observed activation rate."
725
+
726
+
727
+ )
728
+
729
+
730
+ with st.expander("Recent alerts", expanded=False):
731
+
732
+
733
+ if alerts:
734
+
735
+
736
+ st.table(pd.DataFrame(alerts).tail(10))
737
+
738
+
739
+ else:
740
+
741
+
742
+ st.info("No high-risk alerts in this window.")
743
+
744
+
745
+ st.area_chart(results.set_index("timestamp")["glucose_mgdl"], height=220)
746
+
747
+
748
+
749
+
750
+
751
+
752
+
753
+
754
+ def render_treatment_plan(medications: Dict[str, Any], next_visit: str) -> None:
755
+
756
+
757
+ st.subheader("Full-cycle treatment support")
758
+
759
+
760
+ st.write(
761
+
762
+
763
+ "Upload or edit medication schedules, insulin titration guidance, and clinician notes."
764
+
765
+
766
+ )
767
+
768
+
769
+ st.json(medications, expanded=False)
770
+
771
+
772
+ st.caption(f"Next scheduled review: {next_visit}")
773
+
774
+
775
+
776
+
777
+
778
+
779
+
780
+
781
+ def render_lifestyle_support(results: pd.DataFrame) -> None:
782
+
783
+
784
+ st.subheader("Lifestyle & wellbeing")
785
+
786
+
787
+ recent = results.tail(96).copy()
788
+
789
+
790
+ avg_glucose = recent["glucose_mgdl"].mean()
791
+
792
+
793
+ active_minutes = int((recent["activity_factor"] > 0.4).sum() * 5)
794
+
795
+
796
+ col1, col2 = st.columns(2)
797
+
798
+
799
+ col1.metric("Average glucose (8h)", f"{avg_glucose:.1f} mg/dL")
800
+
801
+
802
+ col2.metric("Active minutes", f"{active_minutes} min")
803
+
804
+
805
+ st.markdown(
806
+
807
+
808
+ """
809
+
810
+
811
+ - Aim for gentle movement every hour you are awake.
812
+
813
+
814
+ - Pair carbohydrates with protein/fiber to smooth spikes.
815
+
816
+
817
+ - Sleep flagged recently? Try 10-minute breathing before bed.
818
+
819
+
820
+ - Journal one gratitude moment—stress strongly shapes risk.
821
+
822
+
823
+ """
824
+
825
+
826
+ )
827
+
828
+
829
+
830
+
831
+
832
+
833
+
834
+
835
+ def render_community_actions() -> Dict[str, List[str]]:
836
+
837
+
838
+ st.subheader("Community impact")
839
+
840
+
841
+ st.write(
842
+
843
+
844
+ "Invite families, caregivers, and clinics to the commons. Set up alerts, shared logs, and outreach."
845
+
846
+
847
+ )
848
+
849
+
850
+ contact_list = [
851
+
852
+
853
+ "SMS: +233-200-000-111",
854
+
855
+
856
+ "WhatsApp: Care Circle Group",
857
+
858
+
859
+ "Clinic portal: sundew.health/community",
860
+
861
+
862
+ ]
863
+
864
+
865
+ st.table(pd.DataFrame({"Support channel": contact_list}))
866
+
867
+
868
+ return {
869
+
870
+
871
+ "Desired partners": ["Rural clinics", "Youth ambassadors", "Nutrition co-ops"],
872
+
873
+
874
+ "Needs": ["Smartphone grants", "Solar charging kits", "Translation volunteers"],
875
+
876
+
877
+ }
878
+
879
+
880
+
881
+
882
+
883
+
884
+
885
+
886
+ def render_telemetry(results: pd.DataFrame, telemetry: List[Dict[str, Any]]) -> None:
887
+
888
+
889
+ st.subheader("Telemetry & export")
890
+
891
+
892
+ st.write(
893
+
894
+
895
+ "Download event-level telemetry for validation, research, or regulatory reporting."
896
+
897
+
898
+ )
899
+
900
+
901
+ st.caption(
902
+
903
+
904
+ "Energy savings are computed as 1 minus the observed activation rate. When the gate stays mostly open, savings naturally trend toward zero."
905
+
906
+
907
+ )
908
+
909
+
910
+ json_payload = json.dumps(telemetry, default=str, indent=2)
911
+
912
+
913
+ st.download_button(
914
+
915
+
916
+ label="Download telemetry (JSON)",
917
+
918
+
919
+ data=json_payload,
920
+
921
+
922
+ file_name="sundew_diabetes_telemetry.json",
923
+
924
+
925
+ mime="application/json",
926
+
927
+
928
+ )
929
+
930
+
931
+ st.dataframe(results.tail(100), use_container_width=True)
932
+
933
+
934
+
935
+
936
+
937
+
938
+
939
+
940
+ def main() -> None:
941
+
942
+
943
+ st.set_page_config(
944
+
945
+
946
+ page_title="Sundew Diabetes Commons", layout="wide", page_icon="🕊"
947
+
948
+
949
+ )
950
+
951
+
952
+ st.title("Sundew Diabetes Commons")
953
+
954
+
955
+ st.caption(
956
+
957
+
958
+ "Open, compassionate diabetes care—monitoring, treatment, lifestyle, community."
959
+
960
+
961
+ )
962
+
963
+
964
+
965
+
966
+
967
+ st.sidebar.header("Load data")
968
+
969
+
970
+ uploaded = st.sidebar.file_uploader("CGM / diary CSV", type=["csv"])
971
+
972
+
973
+ use_example = st.sidebar.checkbox("Use synthetic example", True)
974
+
975
+
976
+
977
+
978
+
979
+ st.sidebar.header("Sundew configuration")
980
+
981
+
982
+ use_native = st.sidebar.checkbox(
983
+
984
+
985
+ "Use native Sundew gating",
986
+
987
+
988
+ value=_HAS_SUNDEW,
989
+
990
+
991
+ help="Disable to demo the lightweight fallback gate only.",
992
+
993
+
994
+ )
995
+
996
+
997
+ target_activation = st.sidebar.slider("Target activation", 0.05, 0.90, 0.22, 0.01)
998
+
999
+
1000
+ temperature = st.sidebar.slider("Gate temperature", 0.02, 0.50, 0.08, 0.01)
1001
+
1002
+
1003
+ mode = st.sidebar.selectbox(
1004
+
1005
+
1006
+ "Preset", ["tuned_v2", "conservative", "aggressive", "auto_tuned"], index=0
1007
+
1008
+
1009
+ )
1010
+
1011
+
1012
+
1013
+
1014
+
1015
+ if uploaded is not None:
1016
+
1017
+
1018
+ df = pd.read_csv(uploaded)
1019
+
1020
+
1021
+ elif use_example:
1022
+
1023
+
1024
+ df = load_example_dataset()
1025
+
1026
+
1027
+ else:
1028
+
1029
+
1030
+ st.stop()
1031
+
1032
+
1033
+
1034
+
1035
+
1036
+ features = compute_features(df)
1037
+
1038
+
1039
+ model = train_simple_model(features)
1040
+
1041
+
1042
+ gate_config = SundewGateConfig(
1043
+
1044
+
1045
+ target_activation=target_activation,
1046
+
1047
+
1048
+ temperature=temperature,
1049
+
1050
+
1051
+ mode=mode,
1052
+
1053
+
1054
+ use_native=use_native,
1055
+
1056
+
1057
+ )
1058
+
1059
+
1060
+ gate = AdaptiveGate(gate_config)
1061
+
1062
+
1063
+
1064
+
1065
+
1066
+ telemetry: List[Dict[str, Any]] = []
1067
+
1068
+
1069
+ records: List[Dict[str, Any]] = []
1070
+
1071
+
1072
+ alerts: List[Dict[str, Any]] = []
1073
+
1074
+
1075
+
1076
+
1077
+
1078
+ progress = st.progress(0)
1079
+
1080
+
1081
+ status = st.empty()
1082
+
1083
+
1084
+ for idx, row in enumerate(features.itertuples(index=False), start=1):
1085
+
1086
+
1087
+ score = lightweight_score(pd.Series(row._asdict()))
1088
+
1089
+
1090
+ should_run = gate.decide(score)
1091
+
1092
+
1093
+ risk_proba = None
1094
+
1095
+
1096
+ if should_run and model is not None:
1097
+
1098
+
1099
+ sample = np.array(
1100
+
1101
+
1102
+ [
1103
+
1104
+
1105
+ [
1106
+
1107
+
1108
+ row.glucose_mgdl,
1109
+
1110
+
1111
+ row.roc_mgdl_min,
1112
+
1113
+
1114
+ row.iob_proxy,
1115
+
1116
+
1117
+ row.cob_proxy,
1118
+
1119
+
1120
+ row.activity_factor,
1121
+
1122
+
1123
+ row.variability,
1124
+
1125
+
1126
+ ]
1127
+
1128
+
1129
+ ]
1130
+
1131
+
1132
+ )
1133
+
1134
+
1135
+ try:
1136
+
1137
+
1138
+ risk_proba = float(model.predict_proba(sample)[0, 1]) # type: ignore[index]
1139
+
1140
+
1141
+ except Exception:
1142
+
1143
+
1144
+ risk_proba = None
1145
+
1146
+
1147
+ if risk_proba is not None and risk_proba >= 0.6:
1148
+
1149
+
1150
+ alerts.append(
1151
+
1152
+
1153
+ {
1154
+
1155
+
1156
+ "timestamp": row.timestamp,
1157
+
1158
+
1159
+ "glucose": row.glucose_mgdl,
1160
+
1161
+
1162
+ "risk": risk_proba,
1163
+
1164
+
1165
+ "message": "Check CGM, hydrate, plan balanced snack/insulin",
1166
+
1167
+
1168
+ }
1169
+
1170
+
1171
+ )
1172
+
1173
+
1174
+ records.append(
1175
+
1176
+
1177
+ {
1178
+
1179
+
1180
+ "timestamp": row.timestamp,
1181
+
1182
+
1183
+ "glucose_mgdl": row.glucose_mgdl,
1184
+
1185
+
1186
+ "roc_mgdl_min": row.roc_mgdl_min,
1187
+
1188
+
1189
+ "deviation": row.deviation,
1190
+
1191
+
1192
+ "iob_proxy": row.iob_proxy,
1193
+
1194
+
1195
+ "cob_proxy": row.cob_proxy,
1196
+
1197
+
1198
+ "variability": row.variability,
1199
+
1200
+
1201
+ "activity_factor": row.activity_factor,
1202
+
1203
+
1204
+ "score": score,
1205
+
1206
+
1207
+ "activated": should_run,
1208
+
1209
+
1210
+ "risk_proba": risk_proba,
1211
+
1212
+
1213
+ }
1214
+
1215
+
1216
+ )
1217
+
1218
+
1219
+ telemetry.append(
1220
+
1221
+
1222
+ {
1223
+
1224
+
1225
+ "timestamp": str(row.timestamp),
1226
+
1227
+
1228
+ "score": score,
1229
+
1230
+
1231
+ "activated": should_run,
1232
+
1233
+
1234
+ "risk_proba": risk_proba,
1235
+
1236
+
1237
+ }
1238
+
1239
+
1240
+ )
1241
+
1242
+
1243
+ progress.progress(idx / len(features))
1244
+
1245
+
1246
+ status.text(f"Processing event {idx}/{len(features)}")
1247
+
1248
+
1249
+ progress.empty()
1250
+
1251
+
1252
+ status.empty()
1253
+
1254
+
1255
+
1256
+
1257
+
1258
+ results = pd.DataFrame(records)
1259
+
1260
+
1261
+
1262
+
1263
+
1264
+
1265
+ tabs = st.tabs(["Overview", "Treatment", "Lifestyle", "Community", "Telemetry"])
1266
+ with tabs[0]:
1267
+ render_overview(results, alerts, gate_config)
1268
+ with tabs[1]:
1269
+ st.subheader("Full-cycle treatment support")
1270
+ default_plan = {
1271
+ "Insulin": {
1272
+ "Basal": "14u glargine at 21:00",
1273
+ "Bolus": "1u per 10g carbs + correction 1u per 40 mg/dL over 140",
1274
+ },
1275
+ "Oral medications": {
1276
+ "Metformin": "500mg breakfast + 500mg dinner",
1277
+ "Empagliflozin": "10mg once daily (if eGFR > 45)",
1278
+ },
1279
+ "Monitoring": [
1280
+ "CGM sensor change every 10 days",
1281
+ "Morning fasted CGM calibration",
1282
+ "Weekly telehealth coaching",
1283
+ "Quarterly in-person clinician review",
1284
+ ],
1285
+ "Safety plan": [
1286
+ "Carry glucose tabs + glucagon kit",
1287
+ "Emergency contact: +233-200-000-888",
1288
+ ],
1289
+ "Lifestyle": [
1290
+ "30 min brisk walk 5x/week",
1291
+ "Bedtime snack if glucose < 110 mg/dL",
1292
+ "Hydrate 2L water daily unless contraindicated",
1293
+ ],
1294
+ }
1295
+ st.caption("Upload or edit schedules, medication titration guidance, and clinician notes.")
1296
+ uploaded_plan = st.file_uploader(
1297
+ "Optional plan JSON", type=["json"], key="plan_uploader"
1298
+ )
1299
+ plan_text = st.text_area(
1300
+ "Edit plan JSON",
1301
+ json.dumps(default_plan, indent=2),
1302
+ height=240,
1303
+ )
1304
+ plan_data = default_plan
1305
+ if uploaded_plan is not None:
1306
+ try:
1307
+ plan_data = json.load(uploaded_plan)
1308
+ except Exception as exc:
1309
+ st.error(f"Could not parse uploaded plan JSON: {exc}")
1310
+ plan_data = default_plan
1311
+ else:
1312
+ try:
1313
+ plan_data = json.loads(plan_text)
1314
+ except Exception as exc:
1315
+ st.warning(
1316
+ f"Using default plan because text could not be parsed: {exc}"
1317
+ )
1318
+ plan_data = default_plan
1319
+ next_visit = (datetime.utcnow() + timedelta(days=30)).strftime(
1320
+ "%Y-%m-%d (telehealth)"
1321
+ )
1322
+ render_treatment_plan(plan_data, next_visit=next_visit)
1323
+ with tabs[2]:
1324
+ render_lifestyle_support(results)
1325
+ with tabs[3]:
1326
+ community_items = render_community_actions()
1327
+ st.json(community_items, expanded=False)
1328
+ with tabs[4]:
1329
+ render_telemetry(results, telemetry)
1330
+ st.sidebar.markdown("---")
1331
+
1332
+
1333
+ status_text = (
1334
+
1335
+
1336
+ "native gating"
1337
+
1338
+
1339
+ if gate_config.use_native and gate.sundew is not None
1340
+
1341
+
1342
+ else "fallback gate"
1343
+
1344
+
1345
+ )
1346
+
1347
+
1348
+ st.sidebar.caption(f"Sundew status: {status_text}")
1349
+
1350
+
1351
+
1352
+
1353
+
1354
+
1355
+
1356
+
1357
+ if __name__ == "__main__":
1358
+
1359
+
1360
+ main()
1361
+
1362
+