mgbam commited on
Commit
1ee01d8
·
verified ·
1 Parent(s): ea7d209

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +405 -784
app.py CHANGED
@@ -1,784 +1,405 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- Sundew Diabetes Watch — ADVANCED EDITION
5
- Showcasing the full power of Sundew's bio-inspired adaptive algorithms.
6
-
7
- FEATURES:
8
- - PipelineRuntime with custom diabetes-specific SignificanceModel
9
- - Real-time energy tracking with visualization
10
- - PI control threshold adaptation with telemetry
11
- - Statistical validation with bootstrap confidence intervals
12
- - Comprehensive metrics dashboard (F1, precision, recall, energy efficiency)
13
- - Event-level monitoring with runtime listeners
14
- - Telemetry export for hardware validation
15
- - Multi-model ensemble with adaptive weighting
16
- - Adversarial robustness testing
17
- """
18
- from __future__ import annotations
19
-
20
- import json
21
- import math
22
- import os
23
- import time
24
- from collections import deque
25
- from dataclasses import dataclass, field
26
- from typing import Any, Callable, Dict, List, Optional, Tuple
27
-
28
- import numpy as np
29
- import pandas as pd
30
- import streamlit as st
31
-
32
- # ------------------------------ Sundew imports ------------------------------
33
- try:
34
- from sundew.config import SundewConfig
35
- from sundew.config_presets import get_preset
36
- from sundew.interfaces import (
37
- ControlState,
38
- GatingDecision,
39
- ProcessingContext,
40
- ProcessingResult,
41
- SignificanceModel,
42
- )
43
- from sundew.runtime import PipelineRuntime, RuntimeMetrics
44
-
45
- _HAS_SUNDEW = True
46
- except Exception as e:
47
- st.error(f"Sundew not available: {e}. Install with: pip install sundew-algorithms")
48
- _HAS_SUNDEW = False
49
- st.stop()
50
-
51
- # ------------------------------ Optional backends ------------------------------
52
- try:
53
- import xgboost as xgb
54
- _HAS_XGB = True
55
- except:
56
- _HAS_XGB = False
57
-
58
- try:
59
- import torch
60
- _HAS_TORCH = True
61
- except:
62
- _HAS_TORCH = False
63
-
64
- try:
65
- import onnxruntime as ort
66
- _HAS_ONNX = True
67
- except:
68
- _HAS_ONNX = False
69
-
70
- from sklearn.linear_model import LogisticRegression
71
- from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
72
- from sklearn.preprocessing import StandardScaler
73
- from sklearn.pipeline import Pipeline
74
- from sklearn.metrics import f1_score, precision_score, recall_score, roc_auc_score
75
-
76
- # ------------------------------ Custom Diabetes Significance Model ------------------------------
77
-
78
- class DiabetesSignificanceModel(SignificanceModel):
79
- """
80
- Advanced diabetes-specific significance model.
81
-
82
- Computes multi-factor risk score considering:
83
- - Glycemic variability and rate of change
84
- - Hypo/hyper proximity with non-linear penalties
85
- - Insulin-on-board (IOB) decay model
86
- - Carbohydrate absorption dynamics
87
- - Activity impact on glucose
88
- - Time-of-day circadian patterns
89
- - Recent history and trend analysis
90
- """
91
-
92
- def __init__(self, config: Dict[str, Any]):
93
- self.hypo_threshold = config.get("hypo_threshold", 70.0)
94
- self.hyper_threshold = config.get("hyper_threshold", 180.0)
95
- self.target_glucose = config.get("target_glucose", 100.0)
96
- self.roc_critical = config.get("roc_critical", 3.0) # mg/dL/min
97
- self.insulin_half_life = config.get("insulin_half_life", 60.0) # minutes
98
- self.carb_absorption_time = config.get("carb_absorption_time", 180.0) # minutes
99
- self.activity_glucose_impact = config.get("activity_glucose_impact", 0.5)
100
-
101
- # Adaptive weights (learned from data)
102
- self.weights = {
103
- "glycemic_deviation": 0.35,
104
- "velocity_risk": 0.25,
105
- "iob_risk": 0.15,
106
- "cob_risk": 0.10,
107
- "activity_risk": 0.05,
108
- "variability": 0.10,
109
- }
110
-
111
- # History for trend analysis
112
- self.glucose_history: deque = deque(maxlen=12) # Last hour (5-min samples)
113
- self.significance_ema = 0.5
114
- self.ema_alpha = 0.15
115
-
116
- def compute_significance(self, context: ProcessingContext) -> Tuple[float, Dict[str, Any]]:
117
- """Compute diabetes-specific significance score."""
118
- # Features is a dict attribute of context
119
- features = context.features if hasattr(context, 'features') else {}
120
-
121
- # Extract features safely with proper dict access
122
- glucose = float(features.get("glucose_mgdl", 120.0)) if isinstance(features, dict) else 120.0
123
- roc = float(features.get("roc_mgdl_min", 0.0)) if isinstance(features, dict) else 0.0
124
- insulin = float(features.get("insulin_units", 0.0)) if isinstance(features, dict) else 0.0
125
- carbs = float(features.get("carbs_g", 0.0)) if isinstance(features, dict) else 0.0
126
- hr = float(features.get("hr", 70.0)) if isinstance(features, dict) else 70.0
127
- steps = float(features.get("steps", 0)) if isinstance(features, dict) else 0
128
- time_min = float(features.get("time_min", 0.0)) if isinstance(features, dict) else 0.0
129
-
130
- # Update history
131
- self.glucose_history.append(glucose)
132
-
133
- # 1. Glycemic deviation (non-linear penalty for extremes)
134
- if glucose < self.hypo_threshold:
135
- hypo_gap = self.hypo_threshold - glucose
136
- glycemic_score = min(1.0, (hypo_gap / 40.0) ** 1.5) # Aggressive penalty
137
- elif glucose > self.hyper_threshold:
138
- hyper_gap = glucose - self.hyper_threshold
139
- glycemic_score = min(1.0, (hyper_gap / 100.0) ** 1.2)
140
- else:
141
- # In range - low significance
142
- deviation = abs(glucose - self.target_glucose)
143
- glycemic_score = min(0.3, deviation / 100.0)
144
-
145
- # 2. Velocity risk (rate of change)
146
- velocity_magnitude = abs(roc)
147
- velocity_score = min(1.0, velocity_magnitude / self.roc_critical)
148
-
149
- # Directional penalty (falling with hypo, rising with hyper)
150
- if glucose < 80 and roc < -0.5:
151
- velocity_score *= 1.5 # Amplify falling hypo risk
152
- elif glucose > 160 and roc > 0.5:
153
- velocity_score *= 1.3 # Amplify rising hyper risk
154
- velocity_score = min(1.0, velocity_score)
155
-
156
- # 3. Insulin-on-board risk (exponential decay model)
157
- if insulin > 0:
158
- # Simplified IOB: recent insulin decays exponentially
159
- iob_fraction = 1.0 # Assume all insulin still active (simplified)
160
- iob_risk = min(1.0, insulin / 6.0) * iob_fraction
161
-
162
- # Higher risk if glucose dropping with IOB
163
- if roc < -0.5:
164
- iob_risk *= 1.4
165
- else:
166
- iob_risk = 0.0
167
-
168
- # 4. Carbs-on-board risk (absorption curve)
169
- if carbs > 0:
170
- # Simplified COB: recent carbs cause glucose spike risk
171
- cob_risk = min(1.0, carbs / 60.0)
172
-
173
- # Higher risk if glucose rising with COB
174
- if roc > 0.5:
175
- cob_risk *= 1.3
176
- else:
177
- cob_risk = 0.0
178
-
179
- # 5. Activity risk (exercise lowers glucose, HR proxy)
180
- activity_level = steps / 100.0 + max(0, hr - 100) / 60.0
181
- activity_risk = min(0.5, activity_level * self.activity_glucose_impact)
182
-
183
- # Amplify if exercising with insulin
184
- if activity_level > 0.3 and insulin > 1.0:
185
- activity_risk *= 1.6
186
- activity_risk = min(1.0, activity_risk)
187
-
188
- # 6. Glycemic variability (standard deviation of recent history)
189
- if len(self.glucose_history) >= 3:
190
- variability = float(np.std(list(self.glucose_history)))
191
- variability_score = min(1.0, variability / 40.0)
192
- else:
193
- variability_score = 0.0
194
-
195
- # Weighted combination
196
- significance = (
197
- self.weights["glycemic_deviation"] * glycemic_score +
198
- self.weights["velocity_risk"] * velocity_score +
199
- self.weights["iob_risk"] * iob_risk +
200
- self.weights["cob_risk"] * cob_risk +
201
- self.weights["activity_risk"] * activity_risk +
202
- self.weights["variability"] * variability_score
203
- )
204
-
205
- # EMA smoothing to reduce noise
206
- self.significance_ema = (1 - self.ema_alpha) * self.significance_ema + self.ema_alpha * significance
207
- significance_smoothed = self.significance_ema
208
-
209
- # Clamp to [0, 1]
210
- significance_smoothed = max(0.0, min(1.0, significance_smoothed))
211
-
212
- explanation = {
213
- "glucose": glucose,
214
- "roc": roc,
215
- "components": {
216
- "glycemic_deviation": glycemic_score,
217
- "velocity_risk": velocity_score,
218
- "iob_risk": iob_risk,
219
- "cob_risk": cob_risk,
220
- "activity_risk": activity_risk,
221
- "variability": variability_score,
222
- },
223
- "raw_significance": significance,
224
- "smoothed_significance": significance_smoothed,
225
- }
226
-
227
- return float(significance_smoothed), explanation
228
-
229
- def update(self, context: ProcessingContext, outcome: Optional[Dict[str, Any]]) -> None:
230
- """Adaptive weight learning based on outcomes."""
231
- if outcome is None:
232
- return
233
-
234
- # Simple gradient-based weight adjustment
235
- true_risk = outcome.get("true_risk", None)
236
- if true_risk is not None:
237
- predicted_sig = outcome.get("predicted_significance", 0.5)
238
- error = true_risk - predicted_sig
239
-
240
- # Adjust weights slightly
241
- lr = 0.001
242
- for key in self.weights:
243
- component_value = outcome.get("components", {}).get(key, 0.0)
244
- self.weights[key] += lr * error * component_value
245
-
246
- # Normalize weights
247
- total = sum(self.weights.values())
248
- if total > 0:
249
- for key in self.weights:
250
- self.weights[key] /= total
251
-
252
- def get_parameters(self) -> Dict[str, Any]:
253
- return {
254
- "weights": self.weights,
255
- "hypo_threshold": self.hypo_threshold,
256
- "hyper_threshold": self.hyper_threshold,
257
- "target_glucose": self.target_glucose,
258
- }
259
-
260
- def set_parameters(self, params: Dict[str, Any]) -> None:
261
- self.weights = params.get("weights", self.weights)
262
- self.hypo_threshold = params.get("hypo_threshold", self.hypo_threshold)
263
- self.hyper_threshold = params.get("hyper_threshold", self.hyper_threshold)
264
- self.target_glucose = params.get("target_glucose", self.target_glucose)
265
-
266
-
267
- # ------------------------------ Telemetry & Monitoring ------------------------------
268
-
269
- @dataclass
270
- class TelemetryEvent:
271
- """Single telemetry event for export."""
272
- timestamp: float
273
- event_id: int
274
- glucose: float
275
- roc: float
276
- significance: float
277
- threshold: float
278
- activated: bool
279
- energy_level: float
280
- risk_proba: Optional[float]
281
- processing_time_ms: float
282
- components: Dict[str, float] = field(default_factory=dict)
283
-
284
-
285
- class RuntimeMonitor:
286
- """Real-time monitoring with event listeners."""
287
-
288
- def __init__(self):
289
- self.events: List[TelemetryEvent] = []
290
- self.alerts: List[Dict[str, Any]] = []
291
-
292
- def add_event(self, event: TelemetryEvent):
293
- self.events.append(event)
294
-
295
- # Check for alerts
296
- if event.risk_proba is not None and event.risk_proba >= 0.6:
297
- self.alerts.append({
298
- "timestamp": event.timestamp,
299
- "event_id": event.event_id,
300
- "glucose": event.glucose,
301
- "risk_proba": event.risk_proba,
302
- "significance": event.significance,
303
- "activated": event.activated,
304
- })
305
-
306
- def get_telemetry_df(self) -> pd.DataFrame:
307
- if not self.events:
308
- return pd.DataFrame()
309
-
310
- data = []
311
- for e in self.events:
312
- row = {
313
- "timestamp": e.timestamp,
314
- "event_id": e.event_id,
315
- "glucose": e.glucose,
316
- "roc": e.roc,
317
- "significance": e.significance,
318
- "threshold": e.threshold,
319
- "activated": e.activated,
320
- "energy_level": e.energy_level,
321
- "risk_proba": e.risk_proba,
322
- "processing_time_ms": e.processing_time_ms,
323
- }
324
- row.update({f"comp_{k}": v for k, v in e.components.items()})
325
- data.append(row)
326
-
327
- return pd.DataFrame(data)
328
-
329
- def export_json(self) -> str:
330
- """Export telemetry as JSON for hardware validation."""
331
- data = {
332
- "events": [
333
- {
334
- "timestamp": e.timestamp,
335
- "event_id": e.event_id,
336
- "glucose": e.glucose,
337
- "significance": e.significance,
338
- "threshold": e.threshold,
339
- "activated": e.activated,
340
- "energy_level": e.energy_level,
341
- "risk_proba": e.risk_proba,
342
- "processing_time_ms": e.processing_time_ms,
343
- }
344
- for e in self.events
345
- ],
346
- "alerts": self.alerts,
347
- "summary": {
348
- "total_events": len(self.events),
349
- "total_activations": sum(1 for e in self.events if e.activated),
350
- "activation_rate": sum(1 for e in self.events if e.activated) / max(len(self.events), 1),
351
- "total_alerts": len(self.alerts),
352
- }
353
- }
354
- return json.dumps(data, indent=2)
355
-
356
-
357
- # ------------------------------ Model backends ------------------------------
358
-
359
- def build_ensemble_model(df: pd.DataFrame):
360
- """Advanced ensemble with multiple classifiers."""
361
- # Prepare data
362
- tmp = df.copy()
363
- tmp["future_glucose"] = tmp["glucose_mgdl"].shift(-6)
364
- tmp["label"] = ((tmp["future_glucose"] < 70) | (tmp["future_glucose"] > 180)).astype(int)
365
- tmp = tmp.dropna(subset=["label"]).copy()
366
-
367
- X = tmp[["glucose_mgdl", "roc_mgdl_min", "insulin_units", "carbs_g", "hr"]].fillna(0.0).values
368
- y = tmp["label"].values
369
-
370
- if len(np.unique(y)) < 2:
371
- y = np.array([0, 1] * (len(X) // 2 + 1))[:len(X)]
372
-
373
- # Train ensemble
374
- scaler = StandardScaler()
375
- X_scaled = scaler.fit_transform(X)
376
-
377
- models = [
378
- ("logreg", LogisticRegression(max_iter=1000, C=0.1)),
379
- ("rf", RandomForestClassifier(n_estimators=50, max_depth=6, random_state=42)),
380
- ("gbm", GradientBoostingClassifier(n_estimators=50, max_depth=4, learning_rate=0.1, random_state=42)),
381
- ]
382
-
383
- trained_models = []
384
- for name, model in models:
385
- try:
386
- model.fit(X_scaled, y)
387
- trained_models.append((name, model))
388
- except:
389
- pass
390
-
391
- def _predict(Xarr: np.ndarray) -> float:
392
- X_s = scaler.transform(Xarr)
393
- predictions = []
394
- for name, model in trained_models:
395
- try:
396
- if hasattr(model, "predict_proba"):
397
- pred = model.predict_proba(X_s)[0, 1]
398
- else:
399
- pred = model.predict(X_s)[0]
400
- predictions.append(pred)
401
- except:
402
- pass
403
-
404
- if predictions:
405
- return float(np.mean(predictions))
406
- return 0.5
407
-
408
- return _predict
409
-
410
-
411
- # ------------------------------ Bootstrap Statistics ------------------------------
412
-
413
- def bootstrap_metric(y_true: np.ndarray, y_pred: np.ndarray, metric_fn: Callable, n_bootstrap: int = 1000) -> Tuple[float, float, float]:
414
- """Compute bootstrap confidence interval for a metric."""
415
- n = len(y_true)
416
- bootstrap_scores = []
417
-
418
- rng = np.random.default_rng(42)
419
- for _ in range(n_bootstrap):
420
- indices = rng.choice(n, size=n, replace=True)
421
- try:
422
- score = metric_fn(y_true[indices], y_pred[indices])
423
- bootstrap_scores.append(score)
424
- except:
425
- pass
426
-
427
- if not bootstrap_scores:
428
- return 0.0, 0.0, 0.0
429
-
430
- mean = float(np.mean(bootstrap_scores))
431
- ci_low = float(np.percentile(bootstrap_scores, 2.5))
432
- ci_high = float(np.percentile(bootstrap_scores, 97.5))
433
-
434
- return mean, ci_low, ci_high
435
-
436
-
437
- # ------------------------------ Streamlit UI ------------------------------
438
-
439
- st.set_page_config(page_title="Sundew Diabetes Watch - ADVANCED", layout="wide")
440
-
441
- st.title("🌿 Sundew Diabetes Watch — ADVANCED EDITION")
442
- st.caption("Bio-inspired adaptive gating showcasing the full power of Sundew algorithms")
443
-
444
- # Sidebar configuration
445
- with st.sidebar:
446
- st.header("⚙️ Sundew Configuration")
447
-
448
- preset_name = st.selectbox(
449
- "Preset",
450
- ["tuned_v2", "custom_health_hd82", "auto_tuned", "aggressive", "conservative", "energy_saver"],
451
- index=0,
452
- help="Use custom_health_hd82 for healthcare-optimized settings"
453
- )
454
-
455
- target_activation = st.slider("Target Activation Rate", 0.05, 0.50, 0.15, 0.01)
456
- energy_pressure = st.slider("Energy Pressure", 0.0, 0.3, 0.05, 0.01)
457
- gate_temperature = st.slider("Gate Temperature", 0.0, 0.3, 0.08, 0.01)
458
-
459
- st.header("🩺 Diabetes Parameters")
460
- hypo_threshold = st.number_input("Hypo Threshold (mg/dL)", 50.0, 90.0, 70.0)
461
- hyper_threshold = st.number_input("Hyper Threshold (mg/dL)", 140.0, 250.0, 180.0)
462
-
463
- st.header("📊 Analysis Options")
464
- show_bootstrap = st.checkbox("Show Bootstrap CI", value=True)
465
- show_energy_viz = st.checkbox("Show Energy Tracking", value=True)
466
- show_components = st.checkbox("Show Significance Components", value=True)
467
- export_telemetry = st.checkbox("Export Telemetry JSON", value=False)
468
-
469
- # File upload
470
- uploaded = st.file_uploader(
471
- "Upload CGM CSV (timestamp, glucose_mgdl, carbs_g, insulin_units, steps, hr)",
472
- type=["csv"],
473
- )
474
-
475
- use_synth = st.checkbox("Use synthetic example if no file uploaded", value=True)
476
-
477
- # Load data
478
- if uploaded is not None:
479
- df = pd.read_csv(uploaded)
480
- else:
481
- if not use_synth:
482
- st.stop()
483
-
484
- # Generate sophisticated synthetic data
485
- rng = np.random.default_rng(42)
486
- n = 600
487
- t0 = pd.Timestamp.utcnow().floor("min")
488
- times = [t0 + pd.Timedelta(minutes=5 * i) for i in range(n)]
489
-
490
- # Circadian pattern + meals + insulin + exercise
491
- circadian = 120 + 15 * np.sin(np.linspace(0, 8 * np.pi, n) - np.pi/2)
492
- noise = rng.normal(0, 8, n)
493
-
494
- # Meal events (3 per day)
495
- meals = np.zeros(n)
496
- meal_times = [60, 150, 270, 360, 450, 540]
497
- for mt in meal_times:
498
- if mt < n:
499
- meals[mt:min(mt+30, n)] += rng.normal(45, 10)
500
-
501
- # Insulin boluses (with meals)
502
- insulin = np.zeros(n)
503
- for mt in meal_times:
504
- if mt < n and mt > 2:
505
- insulin[mt-2] = rng.normal(4, 0.8)
506
-
507
- # Exercise periods
508
- steps = rng.integers(0, 120, size=n)
509
- exercise_periods = [[120, 150], [400, 430]]
510
- for start, end in exercise_periods:
511
- if start < n and end <= n:
512
- steps[start:end] = rng.integers(120, 180, size=end-start)
513
-
514
- hr = 70 + (steps > 100) * rng.integers(25, 50, size=n) + rng.normal(0, 5, n)
515
-
516
- # Glucose dynamics
517
- glucose = circadian + noise
518
- for i in range(n):
519
- # Meal absorption (delayed)
520
- if i >= 6:
521
- glucose[i] += 0.4 * meals[i-6:i].sum() / 6
522
- # Insulin effect (delayed, persistent)
523
- if i >= 4:
524
- glucose[i] -= 1.2 * insulin[i-4:i].sum() / 4
525
- # Exercise effect
526
- if steps[i] > 100:
527
- glucose[i] -= 15
528
-
529
- # Add some hypo/hyper episodes
530
- glucose[180:200] = rng.normal(62, 5, 20) # Hypo episode
531
- glucose[350:365] = rng.normal(210, 10, 15) # Hyper episode
532
-
533
- df = pd.DataFrame({
534
- "timestamp": times,
535
- "glucose_mgdl": np.round(np.clip(glucose, 40, 350), 1),
536
- "carbs_g": np.round(meals, 1),
537
- "insulin_units": np.round(insulin, 1),
538
- "steps": steps.astype(int),
539
- "hr": np.round(hr, 0).astype(int),
540
- })
541
-
542
- # Parse timestamps
543
- df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True, errors="coerce")
544
- if df["timestamp"].dt.tz is None:
545
- df["timestamp"] = df["timestamp"].dt.tz_localize("UTC")
546
- df = df.sort_values("timestamp").reset_index(drop=True)
547
-
548
- # Feature engineering
549
- df["dt_min"] = df["timestamp"].diff().dt.total_seconds() / 60.0
550
- df["glucose_prev"] = df["glucose_mgdl"].shift(1)
551
- df["roc_mgdl_min"] = (df["glucose_mgdl"] - df["glucose_prev"]) / df["dt_min"]
552
- df["roc_mgdl_min"] = df["roc_mgdl_min"].replace([np.inf, -np.inf], 0.0).fillna(0.0)
553
- df["time_min"] = (df["timestamp"] - df["timestamp"].iloc[0]).dt.total_seconds() / 60.0
554
-
555
- # Build heavy model
556
- with st.spinner("Training ensemble model..."):
557
- predict_proba = build_ensemble_model(df)
558
-
559
- st.success("✅ Ensemble model trained (LogReg + RandomForest + GBM)")
560
-
561
- # Initialize Sundew runtime
562
- with st.spinner("Initializing Sundew PipelineRuntime..."):
563
- config = get_preset(preset_name)
564
- config.target_activation_rate = target_activation
565
- config.energy_pressure = energy_pressure
566
- config.gate_temperature = gate_temperature
567
-
568
- # Custom significance model
569
- diabetes_config = {
570
- "hypo_threshold": hypo_threshold,
571
- "hyper_threshold": hyper_threshold,
572
- "target_glucose": 100.0,
573
- }
574
- significance_model = DiabetesSignificanceModel(diabetes_config)
575
-
576
- # Build pipeline runtime
577
- from sundew.runtime import PipelineRuntime, SimpleGatingStrategy, SimpleControlPolicy, SimpleEnergyModel
578
-
579
- runtime = PipelineRuntime(
580
- config=config,
581
- significance_model=significance_model,
582
- gating_strategy=SimpleGatingStrategy(config.hysteresis_gap),
583
- control_policy=SimpleControlPolicy(config),
584
- energy_model=SimpleEnergyModel(
585
- processing_cost=config.base_processing_cost,
586
- idle_cost=config.dormant_tick_cost,
587
- ),
588
- )
589
-
590
- st.success(f"✅ PipelineRuntime initialized with {preset_name} preset")
591
-
592
- # Runtime monitoring
593
- monitor = RuntimeMonitor()
594
-
595
- # Processing loop
596
- st.header("🔬 Processing Events")
597
- progress_bar = st.progress(0)
598
- status_text = st.empty()
599
-
600
- results = []
601
- ground_truth = []
602
-
603
- for idx, row in df.iterrows():
604
- progress_bar.progress((idx + 1) / len(df))
605
-
606
- # Create processing context
607
- context = ProcessingContext(
608
- timestamp=row["timestamp"].timestamp(),
609
- sequence_id=idx,
610
- features={
611
- "glucose_mgdl": row["glucose_mgdl"],
612
- "roc_mgdl_min": row["roc_mgdl_min"],
613
- "insulin_units": row["insulin_units"],
614
- "carbs_g": row["carbs_g"],
615
- "hr": row["hr"],
616
- "steps": row["steps"],
617
- "time_min": row["time_min"],
618
- },
619
- history=[],
620
- metadata={},
621
- )
622
-
623
- # Process with runtime
624
- t_start = time.perf_counter()
625
- result = runtime.process(context)
626
- t_elapsed = (time.perf_counter() - t_start) * 1000 # ms
627
-
628
- # Heavy model prediction if activated
629
- risk_proba = None
630
- if result.activated:
631
- X = np.array([[
632
- row["glucose_mgdl"],
633
- row["roc_mgdl_min"],
634
- row["insulin_units"],
635
- row["carbs_g"],
636
- row["hr"],
637
- ]])
638
- try:
639
- risk_proba = predict_proba(X)
640
- except:
641
- risk_proba = None
642
-
643
- # Ground truth (for evaluation)
644
- future_idx = min(idx + 6, len(df) - 1)
645
- future_glucose = df.iloc[future_idx]["glucose_mgdl"]
646
- true_risk = 1 if (future_glucose < hypo_threshold or future_glucose > hyper_threshold) else 0
647
- ground_truth.append(true_risk)
648
-
649
- # Record telemetry
650
- telemetry = TelemetryEvent(
651
- timestamp=context.timestamp,
652
- event_id=idx,
653
- glucose=row["glucose_mgdl"],
654
- roc=row["roc_mgdl_min"],
655
- significance=result.significance,
656
- threshold=result.threshold_used,
657
- activated=result.activated,
658
- energy_level=result.energy_consumed, # Use energy_consumed as proxy
659
- risk_proba=risk_proba,
660
- processing_time_ms=t_elapsed,
661
- components=result.explanation.get("feature_contributions", {}),
662
- )
663
- monitor.add_event(telemetry)
664
-
665
- results.append({
666
- "timestamp": row["timestamp"],
667
- "glucose": row["glucose_mgdl"],
668
- "roc": row["roc_mgdl_min"],
669
- "significance": result.significance,
670
- "threshold": result.threshold_used,
671
- "activated": result.activated,
672
- "energy_level": result.energy_consumed,
673
- "risk_proba": risk_proba,
674
- "true_risk": true_risk,
675
- })
676
-
677
- progress_bar.empty()
678
- status_text.empty()
679
-
680
- # Convert to DataFrame
681
- results_df = pd.DataFrame(results)
682
- telemetry_df = monitor.get_telemetry_df()
683
-
684
- # Compute metrics
685
- total_events = len(results_df)
686
- total_activations = int(results_df["activated"].sum())
687
- activation_rate = total_activations / total_events
688
- energy_savings = 1 - activation_rate
689
-
690
- # Statistical evaluation (on activated events)
691
- activated_results = results_df[results_df["activated"]].copy()
692
- if len(activated_results) > 10:
693
- y_true = activated_results["true_risk"].values
694
- y_pred = (activated_results["risk_proba"].fillna(0.5) >= 0.5).astype(int).values
695
-
696
- f1 = f1_score(y_true, y_pred, zero_division=0)
697
- precision = precision_score(y_true, y_pred, zero_division=0)
698
- recall = recall_score(y_true, y_pred, zero_division=0)
699
-
700
- if show_bootstrap:
701
- f1_mean, f1_low, f1_high = bootstrap_metric(y_true, y_pred, lambda yt, yp: f1_score(yt, yp, zero_division=0))
702
- prec_mean, prec_low, prec_high = bootstrap_metric(y_true, y_pred, lambda yt, yp: precision_score(yt, yp, zero_division=0))
703
- rec_mean, rec_low, rec_high = bootstrap_metric(y_true, y_pred, lambda yt, yp: recall_score(yt, yp, zero_division=0))
704
- else:
705
- f1 = precision = recall = 0.0
706
- f1_mean = prec_mean = rec_mean = 0.0
707
- f1_low = f1_high = prec_low = prec_high = rec_low = rec_high = 0.0
708
-
709
- # Dashboard
710
- st.header("📊 Performance Dashboard")
711
-
712
- col1, col2, col3, col4 = st.columns(4)
713
- col1.metric("Total Events", f"{total_events}")
714
- col2.metric("Activations", f"{total_activations} ({activation_rate:.1%})")
715
- col3.metric("Energy Savings", f"{energy_savings:.1%}")
716
- col4.metric("Alerts", f"{len(monitor.alerts)}")
717
-
718
- col1, col2, col3 = st.columns(3)
719
- if show_bootstrap and len(activated_results) > 10:
720
- col1.metric("F1 Score", f"{f1_mean:.3f}", help=f"95% CI: [{f1_low:.3f}, {f1_high:.3f}]")
721
- col2.metric("Precision", f"{prec_mean:.3f}", help=f"95% CI: [{prec_low:.3f}, {prec_high:.3f}]")
722
- col3.metric("Recall", f"{rec_mean:.3f}", help=f"95% CI: [{rec_low:.3f}, {rec_high:.3f}]")
723
- else:
724
- col1.metric("F1 Score", f"{f1:.3f}")
725
- col2.metric("Precision", f"{precision:.3f}")
726
- col3.metric("Recall", f"{recall:.3f}")
727
-
728
- # Visualizations
729
- st.header("📈 Real-Time Visualizations")
730
-
731
- # Glucose + Threshold
732
- fig_col1, fig_col2 = st.columns(2)
733
-
734
- with fig_col1:
735
- st.subheader("Glucose Levels")
736
- chart_data = results_df.set_index("timestamp")[["glucose"]]
737
- st.line_chart(chart_data, height=250)
738
-
739
- with fig_col2:
740
- st.subheader("Significance vs Threshold (Adaptive PI Control)")
741
- chart_data = results_df.set_index("timestamp")[["significance", "threshold"]]
742
- st.line_chart(chart_data, height=250)
743
-
744
- # Energy tracking
745
- if show_energy_viz:
746
- st.subheader("Energy Level (Bio-Inspired Regeneration)")
747
- chart_data = results_df.set_index("timestamp")[["energy_level"]]
748
- st.line_chart(chart_data, height=200)
749
-
750
- # Significance components
751
- if show_components and len(telemetry_df) > 0:
752
- comp_cols = [c for c in telemetry_df.columns if c.startswith("comp_")]
753
- if comp_cols:
754
- st.subheader("Significance Components (Diabetes-Specific Risk Factors)")
755
- chart_data = telemetry_df.set_index("timestamp")[comp_cols]
756
- st.line_chart(chart_data, height=200)
757
-
758
- # Alerts
759
- st.header("⚠️ Risk Alerts")
760
- if monitor.alerts:
761
- alerts_df = pd.DataFrame(monitor.alerts)
762
- st.dataframe(alerts_df, use_container_width=True)
763
- else:
764
- st.info("No high-risk alerts triggered in this window.")
765
-
766
- # Detailed telemetry
767
- with st.expander("🔍 Detailed Telemetry (Last 100 Events)"):
768
- st.dataframe(results_df.tail(100), use_container_width=True)
769
-
770
- # Export telemetry
771
- if export_telemetry:
772
- st.header("📥 Export Telemetry")
773
- json_data = monitor.export_json()
774
- st.download_button(
775
- label="Download Telemetry JSON",
776
- data=json_data,
777
- file_name="sundew_diabetes_telemetry.json",
778
- mime="application/json",
779
- )
780
- st.success("Telemetry ready for hardware validation workflows")
781
-
782
- # Footer
783
- st.divider()
784
- st.caption(f"🌿 Powered by Sundew Algorithms v0.7+ | PipelineRuntime with custom DiabetesSignificanceModel | Research prototype")
 
1
+ """Sundew Diabetes Commons – holistic, open Streamlit experience."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import math
8
+ import time
9
+ from dataclasses import dataclass
10
+ from typing import Any, Dict, List, Optional, Tuple
11
+
12
+ import numpy as np
13
+ import pandas as pd
14
+ import streamlit as st
15
+
16
+ try:
17
+ from sundew import SundewAlgorithm # type: ignore[attr-defined]
18
+
19
+ _HAS_SUNDEW = True
20
+ except Exception: # pragma: no cover - graceful fallback
21
+ SundewAlgorithm = None # type: ignore
22
+ _HAS_SUNDEW = False
23
+
24
+ LOGGER = logging.getLogger("sundew.diabetes.commons")
25
+
26
+
27
+ @dataclass
28
+ class SundewGateConfig:
29
+ target_activation: float = 0.22
30
+ temperature: float = 0.08
31
+ mode: str = "tuned_v2"
32
+
33
+
34
+ class AdaptiveGate:
35
+ """Adapter that hides Sundew/Fallback branching."""
36
+
37
+ def __init__(self, config: SundewGateConfig) -> None:
38
+ self.config = config
39
+ self._ema = 0.0
40
+ self._tau = 0.5
41
+ self._alpha = 0.02
42
+ if _HAS_SUNDEW and SundewAlgorithm is not None:
43
+ try:
44
+ self.sundew: Optional[SundewAlgorithm] = SundewAlgorithm(
45
+ target_activation=config.target_activation,
46
+ temperature=config.temperature,
47
+ mode=config.mode,
48
+ )
49
+ except TypeError: # older package versions
50
+ self.sundew = SundewAlgorithm()
51
+ else:
52
+ self.sundew = None
53
+
54
+ def decide(self, score: float) -> bool:
55
+ if self.sundew is not None:
56
+ for attr in ("decide", "step", "open"):
57
+ fn = getattr(self.sundew, attr, None)
58
+ if callable(fn):
59
+ try:
60
+ return bool(fn(score))
61
+ except Exception: # pragma: no cover - parity fallback
62
+ continue
63
+ # Fallback logistic gate
64
+ temperature = max(self.config.temperature, 1e-6)
65
+ probability = 1.0 / (1.0 + math.exp(-(score - self._tau) / temperature))
66
+ fired = np.random.rand() < probability
67
+ self._ema = (1 - self._alpha) * self._ema + self._alpha * (
68
+ 1.0 if fired else 0.0
69
+ )
70
+ self._tau += 0.01 * (self.config.target_activation - self._ema)
71
+ self._tau = min(0.95, max(0.05, self._tau))
72
+ return fired
73
+
74
+
75
+ def load_example_dataset(n_rows: int = 720) -> pd.DataFrame:
76
+ rng = np.random.default_rng(17)
77
+ t0 = pd.Timestamp.utcnow().floor("5min") - pd.Timedelta(minutes=5 * n_rows)
78
+ timestamps = [t0 + pd.Timedelta(minutes=5 * i) for i in range(n_rows)]
79
+ base = 118 + 28 * np.sin(np.linspace(0, 7 * np.pi, n_rows))
80
+ noise = rng.normal(0, 12, n_rows)
81
+ meals = (rng.random(n_rows) < 0.05).astype(float) * rng.normal(50, 18, n_rows).clip(
82
+ 0, 150
83
+ )
84
+ insulin = (rng.random(n_rows) < 0.03).astype(float) * rng.normal(
85
+ 4.2, 1.5, n_rows
86
+ ).clip(0, 10)
87
+ steps = rng.integers(0, 200, size=n_rows)
88
+ hr = 68 + (steps > 90) * rng.integers(20, 45, size=n_rows)
89
+ sleep = (rng.random(n_rows) < 0.12).astype(float)
90
+ stress = rng.uniform(0, 1, n_rows)
91
+ glucose = base + noise + 0.4 * meals - 0.7 * insulin
92
+ df = pd.DataFrame(
93
+ {
94
+ "timestamp": timestamps,
95
+ "glucose_mgdl": glucose.round(1),
96
+ "carbs_g": meals.round(1),
97
+ "insulin_units": insulin.round(1),
98
+ "steps": steps,
99
+ "hr": hr,
100
+ "sleep_flag": sleep,
101
+ "stress_index": stress,
102
+ }
103
+ )
104
+ return df
105
+
106
+
107
+ def compute_features(df: pd.DataFrame) -> pd.DataFrame:
108
+ df = df.copy()
109
+ df = df.sort_values("timestamp").reset_index(drop=True)
110
+ df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True)
111
+ df["glucose_prev"] = df["glucose_mgdl"].shift(1)
112
+ dt = (
113
+ df["timestamp"].astype("int64") - df["timestamp"].shift(1).astype("int64")
114
+ ) / 60e9
115
+ df["roc_mgdl_min"] = (df["glucose_mgdl"] - df["glucose_prev"]) / dt
116
+ df["roc_mgdl_min"].replace([np.inf, -np.inf], 0.0, inplace=True)
117
+ df["roc_mgdl_min"].fillna(0.0, inplace=True)
118
+ ema = df["glucose_mgdl"].ewm(span=48, adjust=False).mean()
119
+ df["deviation"] = (df["glucose_mgdl"] - ema).fillna(0.0)
120
+ df["iob_proxy"] = df["insulin_units"].rolling(12, min_periods=1).sum() / 12.0
121
+ df["cob_proxy"] = df["carbs_g"].rolling(12, min_periods=1).sum() / 12.0
122
+ df["variability"] = df["glucose_mgdl"].rolling(24, min_periods=2).std().fillna(0.0)
123
+ df["activity_factor"] = (df["steps"] / 200.0 + df["hr"] / 160.0).clip(0, 1)
124
+ df["sleep_flag"] = df.get("sleep_flag", 0.0).fillna(0.0)
125
+ df["stress_index"] = df.get("stress_index", 0.5).fillna(0.5)
126
+ features = df[
127
+ [
128
+ "timestamp",
129
+ "glucose_mgdl",
130
+ "roc_mgdl_min",
131
+ "deviation",
132
+ "iob_proxy",
133
+ "cob_proxy",
134
+ "variability",
135
+ "activity_factor",
136
+ "sleep_flag",
137
+ "stress_index",
138
+ ]
139
+ ].copy()
140
+ return features
141
+
142
+
143
+ def lightweight_score(row: pd.Series) -> float:
144
+ glucose = row["glucose_mgdl"]
145
+ roc = row["roc_mgdl_min"]
146
+ deviation = row["deviation"]
147
+ iob = row["iob_proxy"]
148
+ cob = row["cob_proxy"]
149
+ stress = row["stress_index"]
150
+ score = 0.0
151
+ score += max(0.0, (glucose - 180) / 80)
152
+ score += max(0.0, (70 - glucose) / 30)
153
+ score += abs(roc) / 6.0
154
+ score += abs(deviation) / 100.0
155
+ score += stress * 0.4
156
+ score += (cob - iob) * 0.05
157
+ return float(np.clip(score, 0.0, 1.5))
158
+
159
+
160
+ def train_simple_model(df: pd.DataFrame):
161
+ threshold = 180
162
+ features = df[
163
+ [
164
+ "glucose_mgdl",
165
+ "roc_mgdl_min",
166
+ "iob_proxy",
167
+ "cob_proxy",
168
+ "activity_factor",
169
+ "variability",
170
+ ]
171
+ ]
172
+ labels = (df["glucose_mgdl"] > threshold).astype(int)
173
+ model = Pipeline(
174
+ [
175
+ ("scaler", StandardScaler()),
176
+ (
177
+ "clf",
178
+ LogisticRegression(
179
+ max_iter=400,
180
+ class_weight="balanced",
181
+ ),
182
+ ),
183
+ ]
184
+ )
185
+ try:
186
+ model.fit(features, labels)
187
+ return model
188
+ except Exception: # pragma: no cover
189
+ return None
190
+
191
+
192
+ def render_overview(results: pd.DataFrame, alerts: List[Dict[str, Any]]) -> None:
193
+ total = len(results)
194
+ activations = int(results["activated"].sum())
195
+ activation_rate = activations / max(total, 1)
196
+ energy_savings = 1.0 - activation_rate
197
+
198
+ st.metric("Events", f"{total}")
199
+ st.metric("Heavy activations", f"{activations} ({activation_rate:.1%})")
200
+ st.metric("Estimated energy saved", f"{energy_savings:.1%}")
201
+ st.metric("Alerts", f"{len(alerts)}")
202
+
203
+ with st.expander("Recent alerts", expanded=False):
204
+ if alerts:
205
+ st.table(pd.DataFrame(alerts).tail(10))
206
+ else:
207
+ st.info("No high-risk alerts in this window.")
208
+
209
+ st.area_chart(results.set_index("timestamp")["glucose_mgdl"], height=220)
210
+
211
+
212
+ def render_treatment_plan(medications: Dict[str, Any], next_visit: str) -> None:
213
+ st.subheader("Full-cycle treatment support")
214
+ st.write(
215
+ "Upload or edit medication schedules, insulin titration guidance, and clinician notes."
216
+ )
217
+ st.json(medications, expanded=False)
218
+ st.caption(f"Next scheduled review: {next_visit}")
219
+
220
+
221
+ def render_lifestyle_support(results: pd.DataFrame) -> None:
222
+ st.subheader("Lifestyle & wellbeing")
223
+ recent = results.tail(96).copy() # last ~8 hours if 5min cadence
224
+ avg_glucose = recent["glucose_mgdl"].mean()
225
+ active_minutes = int((recent["activity_factor"] > 0.4).sum() * 5)
226
+ st.metric("Average glucose (8h)", f"{avg_glucose:.1f} mg/dL")
227
+ st.metric("Active minutes", f"{active_minutes} min")
228
+ st.markdown(
229
+ "- 🎯 Aim for gentle movement every hour you are awake.\n"
230
+ "- 🥗 Consider pairing carbs with protein/fiber to smooth spikes.\n"
231
+ "- 😴 Sleep flagged recently? Try 10-minute breathing before bed.\n"
232
+ "- 🤗 Journal one gratitude moment—stress index strongly shapes risk."
233
+ )
234
+
235
+
236
+ def render_community_actions() -> Dict[str, List[str]]:
237
+ st.subheader("Community impact")
238
+ st.write(
239
+ "Invite families, caregivers, and clinics to the commons. Set up alerts, shared logs, and outreach."
240
+ )
241
+ contact_list = [
242
+ "SMS: +233-200-000-111",
243
+ "WhatsApp: Care Circle Group",
244
+ "Clinic portal: sundew.health/community",
245
+ ]
246
+ st.table(pd.DataFrame({"Support channel": contact_list}))
247
+ callouts = {
248
+ "Desired partners": ["Rural clinics", "Youth ambassadors", "Nutrition co-ops"],
249
+ "Needs": ["Smartphone grants", "Solar charging kits", "Translation volunteers"],
250
+ }
251
+ return callouts
252
+
253
+
254
+ def render_telemetry(results: pd.DataFrame, telemetry: List[Dict[str, Any]]) -> None:
255
+ st.subheader("Telemetry & export")
256
+ st.write(
257
+ "Download event-level telemetry for validation, research, or regulatory reporting."
258
+ )
259
+ json_payload = json.dumps(telemetry, default=str, indent=2)
260
+ st.download_button(
261
+ label="Download telemetry (JSON)",
262
+ data=json_payload,
263
+ file_name="sundew_diabetes_telemetry.json",
264
+ mime="application/json",
265
+ )
266
+ st.dataframe(results.tail(100))
267
+
268
+
269
+ def main() -> None:
270
+ st.set_page_config(
271
+ page_title="Sundew Diabetes Commons",
272
+ layout="wide",
273
+ page_icon="🕊️",
274
+ )
275
+ st.title("🕊️ Sundew Diabetes Commons")
276
+ st.caption(
277
+ "Open, compassionate diabetes care—monitoring, treatment, lifestyle, community."
278
+ )
279
+
280
+ st.sidebar.header("Load data")
281
+ uploaded = st.sidebar.file_uploader("CGM / diary CSV", type=["csv"])
282
+ use_example = st.sidebar.checkbox("Use synthetic example", True)
283
+
284
+ st.sidebar.header("Sundew configuration")
285
+ target_activation = st.sidebar.slider("Target activation", 0.05, 0.9, 0.22, 0.01)
286
+ temperature = st.sidebar.slider("Gate temperature", 0.02, 0.5, 0.08, 0.01)
287
+ mode = st.sidebar.selectbox(
288
+ "Preset", ["tuned_v2", "conservative", "aggressive", "auto_tuned"], index=0
289
+ )
290
+
291
+ if uploaded is not None:
292
+ df = pd.read_csv(uploaded)
293
+ elif use_example:
294
+ df = load_example_dataset()
295
+ else:
296
+ st.stop()
297
+
298
+ features = compute_features(df)
299
+ model = train_simple_model(features)
300
+ gate = AdaptiveGate(SundewGateConfig(target_activation, temperature, mode))
301
+
302
+ telemetry: List[Dict[str, Any]] = []
303
+ records: List[Dict[str, Any]] = []
304
+ alerts: List[Dict[str, Any]] = []
305
+
306
+ progress = st.progress(0)
307
+ status = st.empty()
308
+
309
+ for idx, row in enumerate(features.itertuples(index=False), start=1):
310
+ score = lightweight_score(pd.Series(row._asdict()))
311
+ should_run = gate.decide(score)
312
+ risk_proba = None
313
+ if should_run and model is not None:
314
+ try:
315
+ sample = np.array(
316
+ [
317
+ [
318
+ row.glucose_mgdl,
319
+ row.roc_mgdl_min,
320
+ row.iob_proxy,
321
+ row.cob_proxy,
322
+ row.activity_factor,
323
+ row.variability,
324
+ ]
325
+ ]
326
+ )
327
+ risk_proba = float(model.predict_proba(sample)[0, 1]) # type: ignore[attr-defined]
328
+ except Exception:
329
+ pass
330
+ if risk_proba is not None and risk_proba >= 0.6:
331
+ alerts.append(
332
+ {
333
+ "timestamp": row.timestamp,
334
+ "glucose": row.glucose_mgdl,
335
+ "risk": risk_proba,
336
+ "message": "Check CGM, hydrate, plan balanced snack/insulin",
337
+ }
338
+ )
339
+ records.append(
340
+ {
341
+ "timestamp": row.timestamp,
342
+ "glucose_mgdl": row.glucose_mgdl,
343
+ "roc_mgdl_min": row.roc_mgdl_min,
344
+ "deviation": row.deviation,
345
+ "iob_proxy": row.iob_proxy,
346
+ "cob_proxy": row.cob_proxy,
347
+ "variability": row.variability,
348
+ "activity_factor": row.activity_factor,
349
+ "score": score,
350
+ "activated": should_run,
351
+ "risk_proba": risk_proba,
352
+ }
353
+ )
354
+ telemetry.append(
355
+ {
356
+ "timestamp": str(row.timestamp),
357
+ "score": score,
358
+ "activated": should_run,
359
+ "risk_proba": risk_proba,
360
+ }
361
+ )
362
+ progress.progress(idx / len(features))
363
+ status.text(f"Processing event {idx}/{len(features)}")
364
+
365
+ progress.empty()
366
+ status.empty()
367
+
368
+ results = pd.DataFrame(records)
369
+
370
+ tabs = st.tabs(
371
+ [
372
+ "Overview",
373
+ "Treatment",
374
+ "Lifestyle",
375
+ "Community",
376
+ "Telemetry",
377
+ ]
378
+ )
379
+
380
+ with tabs[0]:
381
+ render_overview(results, alerts)
382
+ with tabs[1]:
383
+ plan = {
384
+ "Insulin": {"Basal": "12u nightly", "Bolus": "1u per 12g carbs"},
385
+ "Metformin": "500mg twice daily",
386
+ "Check-ins": ["Morning CGM calibration", "Weekly telehealth"],
387
+ }
388
+ render_treatment_plan(plan, next_visit="2025-07-12 (virtual clinic)")
389
+ with tabs[2]:
390
+ render_lifestyle_support(results)
391
+ with tabs[3]:
392
+ community_items = render_community_actions()
393
+ st.json(community_items, expanded=False)
394
+ with tabs[4]:
395
+ render_telemetry(results, telemetry)
396
+
397
+ st.sidebar.markdown("---")
398
+ st.sidebar.caption(
399
+ "Sundew status: "
400
+ + ("✅ native gating" if _HAS_SUNDEW else "⚠️ fallback gate active")
401
+ )
402
+
403
+
404
+ if __name__ == "__main__":
405
+ main()