mgbam commited on
Commit
6522fd9
Β·
verified Β·
1 Parent(s): 2a5ba1f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +784 -784
app.py CHANGED
@@ -1,784 +1,784 @@
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 (pass features dict, not ProcessingContext)
624
- t_start = time.perf_counter()
625
- result = runtime.process(context.features)
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
+ 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")