mgbam commited on
Commit
d648b19
ยท
verified ยท
1 Parent(s): 1af6258

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +2 -10
  2. app_advanced.py +792 -784
app.py CHANGED
@@ -209,14 +209,6 @@ class DiabetesSignificanceModel(SignificanceModel):
209
  # Clamp to [0, 1]
210
  significance_smoothed = max(0.0, min(1.0, significance_smoothed))
211
 
212
- # DEBUG: Print first 5 events and any high-risk events
213
- if len(self.glucose_history) <= 5 or glucose < 70 or glucose > 180:
214
- print(f"\n=== DEBUG Event {len(self.glucose_history)} ===")
215
- print(f" Glucose: {glucose:.1f} mg/dL, ROC: {roc:.2f} mg/dL/min")
216
- print(f" Insulin: {insulin:.1f}U, Carbs: {carbs:.1f}g, HR: {hr:.0f}, Steps: {steps:.0f}")
217
- print(f" Scores: glycemic={glycemic_score:.3f}, velocity={velocity_score:.3f}, iob={iob_risk:.3f}")
218
- print(f" Raw significance: {significance:.3f}, Smoothed: {significance_smoothed:.3f}")
219
-
220
  explanation = {
221
  "glucose": glucose,
222
  "roc": roc,
@@ -628,9 +620,9 @@ for idx, row in df.iterrows():
628
  metadata={},
629
  )
630
 
631
- # Process with runtime (pass features dict, not ProcessingContext)
632
  t_start = time.perf_counter()
633
- result = runtime.process(context.features)
634
  t_elapsed = (time.perf_counter() - t_start) * 1000 # ms
635
 
636
  # Heavy model prediction if activated
 
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,
 
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
app_advanced.py CHANGED
@@ -1,784 +1,792 @@
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
+ #!/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
+ # DEBUG: Print first 5 events and any high-risk events
213
+ if len(self.glucose_history) <= 5 or glucose < 70 or glucose > 180:
214
+ print(f"\n=== DEBUG Event {len(self.glucose_history)} ===")
215
+ print(f" Glucose: {glucose:.1f} mg/dL, ROC: {roc:.2f} mg/dL/min")
216
+ print(f" Insulin: {insulin:.1f}U, Carbs: {carbs:.1f}g, HR: {hr:.0f}, Steps: {steps:.0f}")
217
+ print(f" Scores: glycemic={glycemic_score:.3f}, velocity={velocity_score:.3f}, iob={iob_risk:.3f}")
218
+ print(f" Raw significance: {significance:.3f}, Smoothed: {significance_smoothed:.3f}")
219
+
220
+ explanation = {
221
+ "glucose": glucose,
222
+ "roc": roc,
223
+ "components": {
224
+ "glycemic_deviation": glycemic_score,
225
+ "velocity_risk": velocity_score,
226
+ "iob_risk": iob_risk,
227
+ "cob_risk": cob_risk,
228
+ "activity_risk": activity_risk,
229
+ "variability": variability_score,
230
+ },
231
+ "raw_significance": significance,
232
+ "smoothed_significance": significance_smoothed,
233
+ }
234
+
235
+ return float(significance_smoothed), explanation
236
+
237
+ def update(self, context: ProcessingContext, outcome: Optional[Dict[str, Any]]) -> None:
238
+ """Adaptive weight learning based on outcomes."""
239
+ if outcome is None:
240
+ return
241
+
242
+ # Simple gradient-based weight adjustment
243
+ true_risk = outcome.get("true_risk", None)
244
+ if true_risk is not None:
245
+ predicted_sig = outcome.get("predicted_significance", 0.5)
246
+ error = true_risk - predicted_sig
247
+
248
+ # Adjust weights slightly
249
+ lr = 0.001
250
+ for key in self.weights:
251
+ component_value = outcome.get("components", {}).get(key, 0.0)
252
+ self.weights[key] += lr * error * component_value
253
+
254
+ # Normalize weights
255
+ total = sum(self.weights.values())
256
+ if total > 0:
257
+ for key in self.weights:
258
+ self.weights[key] /= total
259
+
260
+ def get_parameters(self) -> Dict[str, Any]:
261
+ return {
262
+ "weights": self.weights,
263
+ "hypo_threshold": self.hypo_threshold,
264
+ "hyper_threshold": self.hyper_threshold,
265
+ "target_glucose": self.target_glucose,
266
+ }
267
+
268
+ def set_parameters(self, params: Dict[str, Any]) -> None:
269
+ self.weights = params.get("weights", self.weights)
270
+ self.hypo_threshold = params.get("hypo_threshold", self.hypo_threshold)
271
+ self.hyper_threshold = params.get("hyper_threshold", self.hyper_threshold)
272
+ self.target_glucose = params.get("target_glucose", self.target_glucose)
273
+
274
+
275
+ # ------------------------------ Telemetry & Monitoring ------------------------------
276
+
277
+ @dataclass
278
+ class TelemetryEvent:
279
+ """Single telemetry event for export."""
280
+ timestamp: float
281
+ event_id: int
282
+ glucose: float
283
+ roc: float
284
+ significance: float
285
+ threshold: float
286
+ activated: bool
287
+ energy_level: float
288
+ risk_proba: Optional[float]
289
+ processing_time_ms: float
290
+ components: Dict[str, float] = field(default_factory=dict)
291
+
292
+
293
+ class RuntimeMonitor:
294
+ """Real-time monitoring with event listeners."""
295
+
296
+ def __init__(self):
297
+ self.events: List[TelemetryEvent] = []
298
+ self.alerts: List[Dict[str, Any]] = []
299
+
300
+ def add_event(self, event: TelemetryEvent):
301
+ self.events.append(event)
302
+
303
+ # Check for alerts
304
+ if event.risk_proba is not None and event.risk_proba >= 0.6:
305
+ self.alerts.append({
306
+ "timestamp": event.timestamp,
307
+ "event_id": event.event_id,
308
+ "glucose": event.glucose,
309
+ "risk_proba": event.risk_proba,
310
+ "significance": event.significance,
311
+ "activated": event.activated,
312
+ })
313
+
314
+ def get_telemetry_df(self) -> pd.DataFrame:
315
+ if not self.events:
316
+ return pd.DataFrame()
317
+
318
+ data = []
319
+ for e in self.events:
320
+ row = {
321
+ "timestamp": e.timestamp,
322
+ "event_id": e.event_id,
323
+ "glucose": e.glucose,
324
+ "roc": e.roc,
325
+ "significance": e.significance,
326
+ "threshold": e.threshold,
327
+ "activated": e.activated,
328
+ "energy_level": e.energy_level,
329
+ "risk_proba": e.risk_proba,
330
+ "processing_time_ms": e.processing_time_ms,
331
+ }
332
+ row.update({f"comp_{k}": v for k, v in e.components.items()})
333
+ data.append(row)
334
+
335
+ return pd.DataFrame(data)
336
+
337
+ def export_json(self) -> str:
338
+ """Export telemetry as JSON for hardware validation."""
339
+ data = {
340
+ "events": [
341
+ {
342
+ "timestamp": e.timestamp,
343
+ "event_id": e.event_id,
344
+ "glucose": e.glucose,
345
+ "significance": e.significance,
346
+ "threshold": e.threshold,
347
+ "activated": e.activated,
348
+ "energy_level": e.energy_level,
349
+ "risk_proba": e.risk_proba,
350
+ "processing_time_ms": e.processing_time_ms,
351
+ }
352
+ for e in self.events
353
+ ],
354
+ "alerts": self.alerts,
355
+ "summary": {
356
+ "total_events": len(self.events),
357
+ "total_activations": sum(1 for e in self.events if e.activated),
358
+ "activation_rate": sum(1 for e in self.events if e.activated) / max(len(self.events), 1),
359
+ "total_alerts": len(self.alerts),
360
+ }
361
+ }
362
+ return json.dumps(data, indent=2)
363
+
364
+
365
+ # ------------------------------ Model backends ------------------------------
366
+
367
+ def build_ensemble_model(df: pd.DataFrame):
368
+ """Advanced ensemble with multiple classifiers."""
369
+ # Prepare data
370
+ tmp = df.copy()
371
+ tmp["future_glucose"] = tmp["glucose_mgdl"].shift(-6)
372
+ tmp["label"] = ((tmp["future_glucose"] < 70) | (tmp["future_glucose"] > 180)).astype(int)
373
+ tmp = tmp.dropna(subset=["label"]).copy()
374
+
375
+ X = tmp[["glucose_mgdl", "roc_mgdl_min", "insulin_units", "carbs_g", "hr"]].fillna(0.0).values
376
+ y = tmp["label"].values
377
+
378
+ if len(np.unique(y)) < 2:
379
+ y = np.array([0, 1] * (len(X) // 2 + 1))[:len(X)]
380
+
381
+ # Train ensemble
382
+ scaler = StandardScaler()
383
+ X_scaled = scaler.fit_transform(X)
384
+
385
+ models = [
386
+ ("logreg", LogisticRegression(max_iter=1000, C=0.1)),
387
+ ("rf", RandomForestClassifier(n_estimators=50, max_depth=6, random_state=42)),
388
+ ("gbm", GradientBoostingClassifier(n_estimators=50, max_depth=4, learning_rate=0.1, random_state=42)),
389
+ ]
390
+
391
+ trained_models = []
392
+ for name, model in models:
393
+ try:
394
+ model.fit(X_scaled, y)
395
+ trained_models.append((name, model))
396
+ except:
397
+ pass
398
+
399
+ def _predict(Xarr: np.ndarray) -> float:
400
+ X_s = scaler.transform(Xarr)
401
+ predictions = []
402
+ for name, model in trained_models:
403
+ try:
404
+ if hasattr(model, "predict_proba"):
405
+ pred = model.predict_proba(X_s)[0, 1]
406
+ else:
407
+ pred = model.predict(X_s)[0]
408
+ predictions.append(pred)
409
+ except:
410
+ pass
411
+
412
+ if predictions:
413
+ return float(np.mean(predictions))
414
+ return 0.5
415
+
416
+ return _predict
417
+
418
+
419
+ # ------------------------------ Bootstrap Statistics ------------------------------
420
+
421
+ def bootstrap_metric(y_true: np.ndarray, y_pred: np.ndarray, metric_fn: Callable, n_bootstrap: int = 1000) -> Tuple[float, float, float]:
422
+ """Compute bootstrap confidence interval for a metric."""
423
+ n = len(y_true)
424
+ bootstrap_scores = []
425
+
426
+ rng = np.random.default_rng(42)
427
+ for _ in range(n_bootstrap):
428
+ indices = rng.choice(n, size=n, replace=True)
429
+ try:
430
+ score = metric_fn(y_true[indices], y_pred[indices])
431
+ bootstrap_scores.append(score)
432
+ except:
433
+ pass
434
+
435
+ if not bootstrap_scores:
436
+ return 0.0, 0.0, 0.0
437
+
438
+ mean = float(np.mean(bootstrap_scores))
439
+ ci_low = float(np.percentile(bootstrap_scores, 2.5))
440
+ ci_high = float(np.percentile(bootstrap_scores, 97.5))
441
+
442
+ return mean, ci_low, ci_high
443
+
444
+
445
+ # ------------------------------ Streamlit UI ------------------------------
446
+
447
+ st.set_page_config(page_title="Sundew Diabetes Watch - ADVANCED", layout="wide")
448
+
449
+ st.title("๐ŸŒฟ Sundew Diabetes Watch โ€” ADVANCED EDITION")
450
+ st.caption("Bio-inspired adaptive gating showcasing the full power of Sundew algorithms")
451
+
452
+ # Sidebar configuration
453
+ with st.sidebar:
454
+ st.header("โš™๏ธ Sundew Configuration")
455
+
456
+ preset_name = st.selectbox(
457
+ "Preset",
458
+ ["tuned_v2", "custom_health_hd82", "auto_tuned", "aggressive", "conservative", "energy_saver"],
459
+ index=0,
460
+ help="Use custom_health_hd82 for healthcare-optimized settings"
461
+ )
462
+
463
+ target_activation = st.slider("Target Activation Rate", 0.05, 0.50, 0.15, 0.01)
464
+ energy_pressure = st.slider("Energy Pressure", 0.0, 0.3, 0.05, 0.01)
465
+ gate_temperature = st.slider("Gate Temperature", 0.0, 0.3, 0.08, 0.01)
466
+
467
+ st.header("๐Ÿฉบ Diabetes Parameters")
468
+ hypo_threshold = st.number_input("Hypo Threshold (mg/dL)", 50.0, 90.0, 70.0)
469
+ hyper_threshold = st.number_input("Hyper Threshold (mg/dL)", 140.0, 250.0, 180.0)
470
+
471
+ st.header("๐Ÿ“Š Analysis Options")
472
+ show_bootstrap = st.checkbox("Show Bootstrap CI", value=True)
473
+ show_energy_viz = st.checkbox("Show Energy Tracking", value=True)
474
+ show_components = st.checkbox("Show Significance Components", value=True)
475
+ export_telemetry = st.checkbox("Export Telemetry JSON", value=False)
476
+
477
+ # File upload
478
+ uploaded = st.file_uploader(
479
+ "Upload CGM CSV (timestamp, glucose_mgdl, carbs_g, insulin_units, steps, hr)",
480
+ type=["csv"],
481
+ )
482
+
483
+ use_synth = st.checkbox("Use synthetic example if no file uploaded", value=True)
484
+
485
+ # Load data
486
+ if uploaded is not None:
487
+ df = pd.read_csv(uploaded)
488
+ else:
489
+ if not use_synth:
490
+ st.stop()
491
+
492
+ # Generate sophisticated synthetic data
493
+ rng = np.random.default_rng(42)
494
+ n = 600
495
+ t0 = pd.Timestamp.utcnow().floor("min")
496
+ times = [t0 + pd.Timedelta(minutes=5 * i) for i in range(n)]
497
+
498
+ # Circadian pattern + meals + insulin + exercise
499
+ circadian = 120 + 15 * np.sin(np.linspace(0, 8 * np.pi, n) - np.pi/2)
500
+ noise = rng.normal(0, 8, n)
501
+
502
+ # Meal events (3 per day)
503
+ meals = np.zeros(n)
504
+ meal_times = [60, 150, 270, 360, 450, 540]
505
+ for mt in meal_times:
506
+ if mt < n:
507
+ meals[mt:min(mt+30, n)] += rng.normal(45, 10)
508
+
509
+ # Insulin boluses (with meals)
510
+ insulin = np.zeros(n)
511
+ for mt in meal_times:
512
+ if mt < n and mt > 2:
513
+ insulin[mt-2] = rng.normal(4, 0.8)
514
+
515
+ # Exercise periods
516
+ steps = rng.integers(0, 120, size=n)
517
+ exercise_periods = [[120, 150], [400, 430]]
518
+ for start, end in exercise_periods:
519
+ if start < n and end <= n:
520
+ steps[start:end] = rng.integers(120, 180, size=end-start)
521
+
522
+ hr = 70 + (steps > 100) * rng.integers(25, 50, size=n) + rng.normal(0, 5, n)
523
+
524
+ # Glucose dynamics
525
+ glucose = circadian + noise
526
+ for i in range(n):
527
+ # Meal absorption (delayed)
528
+ if i >= 6:
529
+ glucose[i] += 0.4 * meals[i-6:i].sum() / 6
530
+ # Insulin effect (delayed, persistent)
531
+ if i >= 4:
532
+ glucose[i] -= 1.2 * insulin[i-4:i].sum() / 4
533
+ # Exercise effect
534
+ if steps[i] > 100:
535
+ glucose[i] -= 15
536
+
537
+ # Add some hypo/hyper episodes
538
+ glucose[180:200] = rng.normal(62, 5, 20) # Hypo episode
539
+ glucose[350:365] = rng.normal(210, 10, 15) # Hyper episode
540
+
541
+ df = pd.DataFrame({
542
+ "timestamp": times,
543
+ "glucose_mgdl": np.round(np.clip(glucose, 40, 350), 1),
544
+ "carbs_g": np.round(meals, 1),
545
+ "insulin_units": np.round(insulin, 1),
546
+ "steps": steps.astype(int),
547
+ "hr": np.round(hr, 0).astype(int),
548
+ })
549
+
550
+ # Parse timestamps
551
+ df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True, errors="coerce")
552
+ if df["timestamp"].dt.tz is None:
553
+ df["timestamp"] = df["timestamp"].dt.tz_localize("UTC")
554
+ df = df.sort_values("timestamp").reset_index(drop=True)
555
+
556
+ # Feature engineering
557
+ df["dt_min"] = df["timestamp"].diff().dt.total_seconds() / 60.0
558
+ df["glucose_prev"] = df["glucose_mgdl"].shift(1)
559
+ df["roc_mgdl_min"] = (df["glucose_mgdl"] - df["glucose_prev"]) / df["dt_min"]
560
+ df["roc_mgdl_min"] = df["roc_mgdl_min"].replace([np.inf, -np.inf], 0.0).fillna(0.0)
561
+ df["time_min"] = (df["timestamp"] - df["timestamp"].iloc[0]).dt.total_seconds() / 60.0
562
+
563
+ # Build heavy model
564
+ with st.spinner("Training ensemble model..."):
565
+ predict_proba = build_ensemble_model(df)
566
+
567
+ st.success("โœ… Ensemble model trained (LogReg + RandomForest + GBM)")
568
+
569
+ # Initialize Sundew runtime
570
+ with st.spinner("Initializing Sundew PipelineRuntime..."):
571
+ config = get_preset(preset_name)
572
+ config.target_activation_rate = target_activation
573
+ config.energy_pressure = energy_pressure
574
+ config.gate_temperature = gate_temperature
575
+
576
+ # Custom significance model
577
+ diabetes_config = {
578
+ "hypo_threshold": hypo_threshold,
579
+ "hyper_threshold": hyper_threshold,
580
+ "target_glucose": 100.0,
581
+ }
582
+ significance_model = DiabetesSignificanceModel(diabetes_config)
583
+
584
+ # Build pipeline runtime
585
+ from sundew.runtime import PipelineRuntime, SimpleGatingStrategy, SimpleControlPolicy, SimpleEnergyModel
586
+
587
+ runtime = PipelineRuntime(
588
+ config=config,
589
+ significance_model=significance_model,
590
+ gating_strategy=SimpleGatingStrategy(config.hysteresis_gap),
591
+ control_policy=SimpleControlPolicy(config),
592
+ energy_model=SimpleEnergyModel(
593
+ processing_cost=config.base_processing_cost,
594
+ idle_cost=config.dormant_tick_cost,
595
+ ),
596
+ )
597
+
598
+ st.success(f"โœ… PipelineRuntime initialized with {preset_name} preset")
599
+
600
+ # Runtime monitoring
601
+ monitor = RuntimeMonitor()
602
+
603
+ # Processing loop
604
+ st.header("๐Ÿ”ฌ Processing Events")
605
+ progress_bar = st.progress(0)
606
+ status_text = st.empty()
607
+
608
+ results = []
609
+ ground_truth = []
610
+
611
+ for idx, row in df.iterrows():
612
+ progress_bar.progress((idx + 1) / len(df))
613
+
614
+ # Create processing context
615
+ context = ProcessingContext(
616
+ timestamp=row["timestamp"].timestamp(),
617
+ sequence_id=idx,
618
+ features={
619
+ "glucose_mgdl": row["glucose_mgdl"],
620
+ "roc_mgdl_min": row["roc_mgdl_min"],
621
+ "insulin_units": row["insulin_units"],
622
+ "carbs_g": row["carbs_g"],
623
+ "hr": row["hr"],
624
+ "steps": row["steps"],
625
+ "time_min": row["time_min"],
626
+ },
627
+ history=[],
628
+ metadata={},
629
+ )
630
+
631
+ # Process with runtime (pass features dict, not ProcessingContext)
632
+ t_start = time.perf_counter()
633
+ result = runtime.process(context.features)
634
+ t_elapsed = (time.perf_counter() - t_start) * 1000 # ms
635
+
636
+ # Heavy model prediction if activated
637
+ risk_proba = None
638
+ if result.activated:
639
+ X = np.array([[
640
+ row["glucose_mgdl"],
641
+ row["roc_mgdl_min"],
642
+ row["insulin_units"],
643
+ row["carbs_g"],
644
+ row["hr"],
645
+ ]])
646
+ try:
647
+ risk_proba = predict_proba(X)
648
+ except:
649
+ risk_proba = None
650
+
651
+ # Ground truth (for evaluation)
652
+ future_idx = min(idx + 6, len(df) - 1)
653
+ future_glucose = df.iloc[future_idx]["glucose_mgdl"]
654
+ true_risk = 1 if (future_glucose < hypo_threshold or future_glucose > hyper_threshold) else 0
655
+ ground_truth.append(true_risk)
656
+
657
+ # Record telemetry
658
+ telemetry = TelemetryEvent(
659
+ timestamp=context.timestamp,
660
+ event_id=idx,
661
+ glucose=row["glucose_mgdl"],
662
+ roc=row["roc_mgdl_min"],
663
+ significance=result.significance,
664
+ threshold=result.threshold_used,
665
+ activated=result.activated,
666
+ energy_level=result.energy_consumed, # Use energy_consumed as proxy
667
+ risk_proba=risk_proba,
668
+ processing_time_ms=t_elapsed,
669
+ components=result.explanation.get("feature_contributions", {}),
670
+ )
671
+ monitor.add_event(telemetry)
672
+
673
+ results.append({
674
+ "timestamp": row["timestamp"],
675
+ "glucose": row["glucose_mgdl"],
676
+ "roc": row["roc_mgdl_min"],
677
+ "significance": result.significance,
678
+ "threshold": result.threshold_used,
679
+ "activated": result.activated,
680
+ "energy_level": result.energy_consumed,
681
+ "risk_proba": risk_proba,
682
+ "true_risk": true_risk,
683
+ })
684
+
685
+ progress_bar.empty()
686
+ status_text.empty()
687
+
688
+ # Convert to DataFrame
689
+ results_df = pd.DataFrame(results)
690
+ telemetry_df = monitor.get_telemetry_df()
691
+
692
+ # Compute metrics
693
+ total_events = len(results_df)
694
+ total_activations = int(results_df["activated"].sum())
695
+ activation_rate = total_activations / total_events
696
+ energy_savings = 1 - activation_rate
697
+
698
+ # Statistical evaluation (on activated events)
699
+ activated_results = results_df[results_df["activated"]].copy()
700
+ if len(activated_results) > 10:
701
+ y_true = activated_results["true_risk"].values
702
+ y_pred = (activated_results["risk_proba"].fillna(0.5) >= 0.5).astype(int).values
703
+
704
+ f1 = f1_score(y_true, y_pred, zero_division=0)
705
+ precision = precision_score(y_true, y_pred, zero_division=0)
706
+ recall = recall_score(y_true, y_pred, zero_division=0)
707
+
708
+ if show_bootstrap:
709
+ f1_mean, f1_low, f1_high = bootstrap_metric(y_true, y_pred, lambda yt, yp: f1_score(yt, yp, zero_division=0))
710
+ prec_mean, prec_low, prec_high = bootstrap_metric(y_true, y_pred, lambda yt, yp: precision_score(yt, yp, zero_division=0))
711
+ rec_mean, rec_low, rec_high = bootstrap_metric(y_true, y_pred, lambda yt, yp: recall_score(yt, yp, zero_division=0))
712
+ else:
713
+ f1 = precision = recall = 0.0
714
+ f1_mean = prec_mean = rec_mean = 0.0
715
+ f1_low = f1_high = prec_low = prec_high = rec_low = rec_high = 0.0
716
+
717
+ # Dashboard
718
+ st.header("๐Ÿ“Š Performance Dashboard")
719
+
720
+ col1, col2, col3, col4 = st.columns(4)
721
+ col1.metric("Total Events", f"{total_events}")
722
+ col2.metric("Activations", f"{total_activations} ({activation_rate:.1%})")
723
+ col3.metric("Energy Savings", f"{energy_savings:.1%}")
724
+ col4.metric("Alerts", f"{len(monitor.alerts)}")
725
+
726
+ col1, col2, col3 = st.columns(3)
727
+ if show_bootstrap and len(activated_results) > 10:
728
+ col1.metric("F1 Score", f"{f1_mean:.3f}", help=f"95% CI: [{f1_low:.3f}, {f1_high:.3f}]")
729
+ col2.metric("Precision", f"{prec_mean:.3f}", help=f"95% CI: [{prec_low:.3f}, {prec_high:.3f}]")
730
+ col3.metric("Recall", f"{rec_mean:.3f}", help=f"95% CI: [{rec_low:.3f}, {rec_high:.3f}]")
731
+ else:
732
+ col1.metric("F1 Score", f"{f1:.3f}")
733
+ col2.metric("Precision", f"{precision:.3f}")
734
+ col3.metric("Recall", f"{recall:.3f}")
735
+
736
+ # Visualizations
737
+ st.header("๐Ÿ“ˆ Real-Time Visualizations")
738
+
739
+ # Glucose + Threshold
740
+ fig_col1, fig_col2 = st.columns(2)
741
+
742
+ with fig_col1:
743
+ st.subheader("Glucose Levels")
744
+ chart_data = results_df.set_index("timestamp")[["glucose"]]
745
+ st.line_chart(chart_data, height=250)
746
+
747
+ with fig_col2:
748
+ st.subheader("Significance vs Threshold (Adaptive PI Control)")
749
+ chart_data = results_df.set_index("timestamp")[["significance", "threshold"]]
750
+ st.line_chart(chart_data, height=250)
751
+
752
+ # Energy tracking
753
+ if show_energy_viz:
754
+ st.subheader("Energy Level (Bio-Inspired Regeneration)")
755
+ chart_data = results_df.set_index("timestamp")[["energy_level"]]
756
+ st.line_chart(chart_data, height=200)
757
+
758
+ # Significance components
759
+ if show_components and len(telemetry_df) > 0:
760
+ comp_cols = [c for c in telemetry_df.columns if c.startswith("comp_")]
761
+ if comp_cols:
762
+ st.subheader("Significance Components (Diabetes-Specific Risk Factors)")
763
+ chart_data = telemetry_df.set_index("timestamp")[comp_cols]
764
+ st.line_chart(chart_data, height=200)
765
+
766
+ # Alerts
767
+ st.header("โš ๏ธ Risk Alerts")
768
+ if monitor.alerts:
769
+ alerts_df = pd.DataFrame(monitor.alerts)
770
+ st.dataframe(alerts_df, use_container_width=True)
771
+ else:
772
+ st.info("No high-risk alerts triggered in this window.")
773
+
774
+ # Detailed telemetry
775
+ with st.expander("๐Ÿ” Detailed Telemetry (Last 100 Events)"):
776
+ st.dataframe(results_df.tail(100), use_container_width=True)
777
+
778
+ # Export telemetry
779
+ if export_telemetry:
780
+ st.header("๐Ÿ“ฅ Export Telemetry")
781
+ json_data = monitor.export_json()
782
+ st.download_button(
783
+ label="Download Telemetry JSON",
784
+ data=json_data,
785
+ file_name="sundew_diabetes_telemetry.json",
786
+ mime="application/json",
787
+ )
788
+ st.success("Telemetry ready for hardware validation workflows")
789
+
790
+ # Footer
791
+ st.divider()
792
+ st.caption(f"๐ŸŒฟ Powered by Sundew Algorithms v0.7+ | PipelineRuntime with custom DiabetesSignificanceModel | Research prototype")