mgbam commited on
Commit
eac21ef
·
verified ·
1 Parent(s): 6522fd9

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +610 -784
app.py CHANGED
@@ -1,784 +1,610 @@
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
+ - AdvancedDiabetesSignificanceModel from sundew.domains.healthcare
9
+ - PipelineRuntime with multi-factor risk analysis (glycemic deviation, velocity, IOB, COB, activity, variability)
10
+ - Real-time energy tracking with visualization
11
+ - PI control threshold adaptation with telemetry
12
+ - Statistical validation with bootstrap confidence intervals
13
+ - Comprehensive metrics dashboard (F1, precision, recall, energy efficiency)
14
+ - Event-level monitoring with runtime listeners
15
+ - Telemetry export for hardware validation
16
+ - Multi-model ensemble with adaptive weighting
17
+ - Adversarial robustness testing
18
+ - Adaptive weight learning from outcomes
19
+
20
+ This app now uses the official sundew.domains.healthcare module, demonstrating
21
+ integration between the Sundew algorithms package and real-world healthcare applications.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import math
27
+ import os
28
+ import time
29
+ from collections import deque
30
+ from dataclasses import dataclass, field
31
+ from typing import Any, Callable, Dict, List, Optional, Tuple
32
+
33
+ import numpy as np
34
+ import pandas as pd
35
+ import streamlit as st
36
+
37
+ # ------------------------------ Sundew imports ------------------------------
38
+ try:
39
+ from sundew.config import SundewConfig
40
+ from sundew.config_presets import get_preset
41
+ from sundew.interfaces import (
42
+ ControlState,
43
+ GatingDecision,
44
+ ProcessingContext,
45
+ ProcessingResult,
46
+ SignificanceModel,
47
+ )
48
+ from sundew.runtime import PipelineRuntime, RuntimeMetrics
49
+ from sundew.domains.healthcare import (
50
+ AdvancedDiabetesSignificanceModel,
51
+ build_advanced_diabetes_runtime,
52
+ )
53
+
54
+ _HAS_SUNDEW = True
55
+ except Exception as e:
56
+ st.error(f"Sundew not available: {e}. Install with: pip install sundew-algorithms")
57
+ _HAS_SUNDEW = False
58
+ st.stop()
59
+
60
+ # ------------------------------ Optional backends ------------------------------
61
+ try:
62
+ import xgboost as xgb
63
+ _HAS_XGB = True
64
+ except:
65
+ _HAS_XGB = False
66
+
67
+ try:
68
+ import torch
69
+ _HAS_TORCH = True
70
+ except:
71
+ _HAS_TORCH = False
72
+
73
+ try:
74
+ import onnxruntime as ort
75
+ _HAS_ONNX = True
76
+ except:
77
+ _HAS_ONNX = False
78
+
79
+ from sklearn.linear_model import LogisticRegression
80
+ from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
81
+ from sklearn.preprocessing import StandardScaler
82
+ from sklearn.pipeline import Pipeline
83
+ from sklearn.metrics import f1_score, precision_score, recall_score, roc_auc_score
84
+
85
+ # ------------------------------ Diabetes Significance Model ------------------------------
86
+ # Now imported from sundew.domains.healthcare.AdvancedDiabetesSignificanceModel
87
+ # This provides:
88
+ # - Multi-factor risk analysis (glycemic deviation, velocity, IOB, COB, activity, variability)
89
+ # - Adaptive weight learning from outcomes
90
+ # - EMA smoothing for noise reduction
91
+ # - Glucose history tracking
92
+
93
+
94
+ # ------------------------------ Telemetry & Monitoring ------------------------------
95
+
96
+ @dataclass
97
+ class TelemetryEvent:
98
+ """Single telemetry event for export."""
99
+ timestamp: float
100
+ event_id: int
101
+ glucose: float
102
+ roc: float
103
+ significance: float
104
+ threshold: float
105
+ activated: bool
106
+ energy_level: float
107
+ risk_proba: Optional[float]
108
+ processing_time_ms: float
109
+ components: Dict[str, float] = field(default_factory=dict)
110
+
111
+
112
+ class RuntimeMonitor:
113
+ """Real-time monitoring with event listeners."""
114
+
115
+ def __init__(self):
116
+ self.events: List[TelemetryEvent] = []
117
+ self.alerts: List[Dict[str, Any]] = []
118
+
119
+ def add_event(self, event: TelemetryEvent):
120
+ self.events.append(event)
121
+
122
+ # Check for alerts
123
+ if event.risk_proba is not None and event.risk_proba >= 0.6:
124
+ self.alerts.append({
125
+ "timestamp": event.timestamp,
126
+ "event_id": event.event_id,
127
+ "glucose": event.glucose,
128
+ "risk_proba": event.risk_proba,
129
+ "significance": event.significance,
130
+ "activated": event.activated,
131
+ })
132
+
133
+ def get_telemetry_df(self) -> pd.DataFrame:
134
+ if not self.events:
135
+ return pd.DataFrame()
136
+
137
+ data = []
138
+ for e in self.events:
139
+ row = {
140
+ "timestamp": e.timestamp,
141
+ "event_id": e.event_id,
142
+ "glucose": e.glucose,
143
+ "roc": e.roc,
144
+ "significance": e.significance,
145
+ "threshold": e.threshold,
146
+ "activated": e.activated,
147
+ "energy_level": e.energy_level,
148
+ "risk_proba": e.risk_proba,
149
+ "processing_time_ms": e.processing_time_ms,
150
+ }
151
+ row.update({f"comp_{k}": v for k, v in e.components.items()})
152
+ data.append(row)
153
+
154
+ return pd.DataFrame(data)
155
+
156
+ def export_json(self) -> str:
157
+ """Export telemetry as JSON for hardware validation."""
158
+ data = {
159
+ "events": [
160
+ {
161
+ "timestamp": e.timestamp,
162
+ "event_id": e.event_id,
163
+ "glucose": e.glucose,
164
+ "significance": e.significance,
165
+ "threshold": e.threshold,
166
+ "activated": e.activated,
167
+ "energy_level": e.energy_level,
168
+ "risk_proba": e.risk_proba,
169
+ "processing_time_ms": e.processing_time_ms,
170
+ }
171
+ for e in self.events
172
+ ],
173
+ "alerts": self.alerts,
174
+ "summary": {
175
+ "total_events": len(self.events),
176
+ "total_activations": sum(1 for e in self.events if e.activated),
177
+ "activation_rate": sum(1 for e in self.events if e.activated) / max(len(self.events), 1),
178
+ "total_alerts": len(self.alerts),
179
+ }
180
+ }
181
+ return json.dumps(data, indent=2)
182
+
183
+
184
+ # ------------------------------ Model backends ------------------------------
185
+
186
+ def build_ensemble_model(df: pd.DataFrame):
187
+ """Advanced ensemble with multiple classifiers."""
188
+ # Prepare data
189
+ tmp = df.copy()
190
+ tmp["future_glucose"] = tmp["glucose_mgdl"].shift(-6)
191
+ tmp["label"] = ((tmp["future_glucose"] < 70) | (tmp["future_glucose"] > 180)).astype(int)
192
+ tmp = tmp.dropna(subset=["label"]).copy()
193
+
194
+ X = tmp[["glucose_mgdl", "roc_mgdl_min", "insulin_units", "carbs_g", "hr"]].fillna(0.0).values
195
+ y = tmp["label"].values
196
+
197
+ if len(np.unique(y)) < 2:
198
+ y = np.array([0, 1] * (len(X) // 2 + 1))[:len(X)]
199
+
200
+ # Train ensemble
201
+ scaler = StandardScaler()
202
+ X_scaled = scaler.fit_transform(X)
203
+
204
+ models = [
205
+ ("logreg", LogisticRegression(max_iter=1000, C=0.1)),
206
+ ("rf", RandomForestClassifier(n_estimators=50, max_depth=6, random_state=42)),
207
+ ("gbm", GradientBoostingClassifier(n_estimators=50, max_depth=4, learning_rate=0.1, random_state=42)),
208
+ ]
209
+
210
+ trained_models = []
211
+ for name, model in models:
212
+ try:
213
+ model.fit(X_scaled, y)
214
+ trained_models.append((name, model))
215
+ except:
216
+ pass
217
+
218
+ def _predict(Xarr: np.ndarray) -> float:
219
+ X_s = scaler.transform(Xarr)
220
+ predictions = []
221
+ for name, model in trained_models:
222
+ try:
223
+ if hasattr(model, "predict_proba"):
224
+ pred = model.predict_proba(X_s)[0, 1]
225
+ else:
226
+ pred = model.predict(X_s)[0]
227
+ predictions.append(pred)
228
+ except:
229
+ pass
230
+
231
+ if predictions:
232
+ return float(np.mean(predictions))
233
+ return 0.5
234
+
235
+ return _predict
236
+
237
+
238
+ # ------------------------------ Bootstrap Statistics ------------------------------
239
+
240
+ def bootstrap_metric(y_true: np.ndarray, y_pred: np.ndarray, metric_fn: Callable, n_bootstrap: int = 1000) -> Tuple[float, float, float]:
241
+ """Compute bootstrap confidence interval for a metric."""
242
+ n = len(y_true)
243
+ bootstrap_scores = []
244
+
245
+ rng = np.random.default_rng(42)
246
+ for _ in range(n_bootstrap):
247
+ indices = rng.choice(n, size=n, replace=True)
248
+ try:
249
+ score = metric_fn(y_true[indices], y_pred[indices])
250
+ bootstrap_scores.append(score)
251
+ except:
252
+ pass
253
+
254
+ if not bootstrap_scores:
255
+ return 0.0, 0.0, 0.0
256
+
257
+ mean = float(np.mean(bootstrap_scores))
258
+ ci_low = float(np.percentile(bootstrap_scores, 2.5))
259
+ ci_high = float(np.percentile(bootstrap_scores, 97.5))
260
+
261
+ return mean, ci_low, ci_high
262
+
263
+
264
+ # ------------------------------ Streamlit UI ------------------------------
265
+
266
+ st.set_page_config(page_title="Sundew Diabetes Watch - ADVANCED", layout="wide")
267
+
268
+ st.title("🌿 Sundew Diabetes Watch — ADVANCED EDITION")
269
+ st.caption("Bio-inspired adaptive gating showcasing the full power of Sundew algorithms")
270
+
271
+ # Sidebar configuration
272
+ with st.sidebar:
273
+ st.header("⚙️ Sundew Configuration")
274
+
275
+ preset_name = st.selectbox(
276
+ "Preset",
277
+ ["tuned_v2", "custom_health_hd82", "auto_tuned", "aggressive", "conservative", "energy_saver"],
278
+ index=0,
279
+ help="Use custom_health_hd82 for healthcare-optimized settings"
280
+ )
281
+
282
+ target_activation = st.slider("Target Activation Rate", 0.05, 0.50, 0.15, 0.01)
283
+ energy_pressure = st.slider("Energy Pressure", 0.0, 0.3, 0.05, 0.01)
284
+ gate_temperature = st.slider("Gate Temperature", 0.0, 0.3, 0.08, 0.01)
285
+
286
+ st.header("🩺 Diabetes Parameters")
287
+ hypo_threshold = st.number_input("Hypo Threshold (mg/dL)", 50.0, 90.0, 70.0)
288
+ hyper_threshold = st.number_input("Hyper Threshold (mg/dL)", 140.0, 250.0, 180.0)
289
+
290
+ st.header("📊 Analysis Options")
291
+ show_bootstrap = st.checkbox("Show Bootstrap CI", value=True)
292
+ show_energy_viz = st.checkbox("Show Energy Tracking", value=True)
293
+ show_components = st.checkbox("Show Significance Components", value=True)
294
+ export_telemetry = st.checkbox("Export Telemetry JSON", value=False)
295
+
296
+ # File upload
297
+ uploaded = st.file_uploader(
298
+ "Upload CGM CSV (timestamp, glucose_mgdl, carbs_g, insulin_units, steps, hr)",
299
+ type=["csv"],
300
+ )
301
+
302
+ use_synth = st.checkbox("Use synthetic example if no file uploaded", value=True)
303
+
304
+ # Load data
305
+ if uploaded is not None:
306
+ df = pd.read_csv(uploaded)
307
+ else:
308
+ if not use_synth:
309
+ st.stop()
310
+
311
+ # Generate sophisticated synthetic data
312
+ rng = np.random.default_rng(42)
313
+ n = 600
314
+ t0 = pd.Timestamp.utcnow().floor("min")
315
+ times = [t0 + pd.Timedelta(minutes=5 * i) for i in range(n)]
316
+
317
+ # Circadian pattern + meals + insulin + exercise
318
+ circadian = 120 + 15 * np.sin(np.linspace(0, 8 * np.pi, n) - np.pi/2)
319
+ noise = rng.normal(0, 8, n)
320
+
321
+ # Meal events (3 per day)
322
+ meals = np.zeros(n)
323
+ meal_times = [60, 150, 270, 360, 450, 540]
324
+ for mt in meal_times:
325
+ if mt < n:
326
+ meals[mt:min(mt+30, n)] += rng.normal(45, 10)
327
+
328
+ # Insulin boluses (with meals)
329
+ insulin = np.zeros(n)
330
+ for mt in meal_times:
331
+ if mt < n and mt > 2:
332
+ insulin[mt-2] = rng.normal(4, 0.8)
333
+
334
+ # Exercise periods
335
+ steps = rng.integers(0, 120, size=n)
336
+ exercise_periods = [[120, 150], [400, 430]]
337
+ for start, end in exercise_periods:
338
+ if start < n and end <= n:
339
+ steps[start:end] = rng.integers(120, 180, size=end-start)
340
+
341
+ hr = 70 + (steps > 100) * rng.integers(25, 50, size=n) + rng.normal(0, 5, n)
342
+
343
+ # Glucose dynamics
344
+ glucose = circadian + noise
345
+ for i in range(n):
346
+ # Meal absorption (delayed)
347
+ if i >= 6:
348
+ glucose[i] += 0.4 * meals[i-6:i].sum() / 6
349
+ # Insulin effect (delayed, persistent)
350
+ if i >= 4:
351
+ glucose[i] -= 1.2 * insulin[i-4:i].sum() / 4
352
+ # Exercise effect
353
+ if steps[i] > 100:
354
+ glucose[i] -= 15
355
+
356
+ # Add some hypo/hyper episodes
357
+ glucose[180:200] = rng.normal(62, 5, 20) # Hypo episode
358
+ glucose[350:365] = rng.normal(210, 10, 15) # Hyper episode
359
+
360
+ df = pd.DataFrame({
361
+ "timestamp": times,
362
+ "glucose_mgdl": np.round(np.clip(glucose, 40, 350), 1),
363
+ "carbs_g": np.round(meals, 1),
364
+ "insulin_units": np.round(insulin, 1),
365
+ "steps": steps.astype(int),
366
+ "hr": np.round(hr, 0).astype(int),
367
+ })
368
+
369
+ # Parse timestamps
370
+ df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True, errors="coerce")
371
+ if df["timestamp"].dt.tz is None:
372
+ df["timestamp"] = df["timestamp"].dt.tz_localize("UTC")
373
+ df = df.sort_values("timestamp").reset_index(drop=True)
374
+
375
+ # Feature engineering
376
+ df["dt_min"] = df["timestamp"].diff().dt.total_seconds() / 60.0
377
+ df["glucose_prev"] = df["glucose_mgdl"].shift(1)
378
+ df["roc_mgdl_min"] = (df["glucose_mgdl"] - df["glucose_prev"]) / df["dt_min"]
379
+ df["roc_mgdl_min"] = df["roc_mgdl_min"].replace([np.inf, -np.inf], 0.0).fillna(0.0)
380
+ df["time_min"] = (df["timestamp"] - df["timestamp"].iloc[0]).dt.total_seconds() / 60.0
381
+
382
+ # Build heavy model
383
+ with st.spinner("Training ensemble model..."):
384
+ predict_proba = build_ensemble_model(df)
385
+
386
+ st.success("✅ Ensemble model trained (LogReg + RandomForest + GBM)")
387
+
388
+ # Initialize Sundew runtime
389
+ with st.spinner("Initializing Sundew PipelineRuntime..."):
390
+ config = get_preset(preset_name)
391
+ config.target_activation_rate = target_activation
392
+ config.energy_pressure = energy_pressure
393
+ config.gate_temperature = gate_temperature
394
+
395
+ # Custom significance model (now imported from sundew.domains.healthcare)
396
+ significance_model = AdvancedDiabetesSignificanceModel(
397
+ hypo_threshold=hypo_threshold,
398
+ hyper_threshold=hyper_threshold,
399
+ target_glucose=100.0,
400
+ )
401
+
402
+ # Build pipeline runtime
403
+ from sundew.runtime import PipelineRuntime, SimpleGatingStrategy, SimpleControlPolicy, SimpleEnergyModel
404
+
405
+ runtime = PipelineRuntime(
406
+ config=config,
407
+ significance_model=significance_model,
408
+ gating_strategy=SimpleGatingStrategy(config.hysteresis_gap),
409
+ control_policy=SimpleControlPolicy(config),
410
+ energy_model=SimpleEnergyModel(
411
+ processing_cost=config.base_processing_cost,
412
+ idle_cost=config.dormant_tick_cost,
413
+ ),
414
+ )
415
+
416
+ st.success(f"✅ PipelineRuntime initialized with {preset_name} preset")
417
+
418
+ # Runtime monitoring
419
+ monitor = RuntimeMonitor()
420
+
421
+ # Processing loop
422
+ st.header("🔬 Processing Events")
423
+ progress_bar = st.progress(0)
424
+ status_text = st.empty()
425
+
426
+ results = []
427
+ ground_truth = []
428
+
429
+ for idx, row in df.iterrows():
430
+ progress_bar.progress((idx + 1) / len(df))
431
+
432
+ # Create processing context
433
+ context = ProcessingContext(
434
+ timestamp=row["timestamp"].timestamp(),
435
+ sequence_id=idx,
436
+ features={
437
+ "glucose_mgdl": row["glucose_mgdl"],
438
+ "roc_mgdl_min": row["roc_mgdl_min"],
439
+ "insulin_units": row["insulin_units"],
440
+ "carbs_g": row["carbs_g"],
441
+ "hr": row["hr"],
442
+ "steps": row["steps"],
443
+ "time_min": row["time_min"],
444
+ },
445
+ history=[],
446
+ metadata={},
447
+ )
448
+
449
+ # Process with runtime
450
+ t_start = time.perf_counter()
451
+ result = runtime.process(context)
452
+ t_elapsed = (time.perf_counter() - t_start) * 1000 # ms
453
+
454
+ # Heavy model prediction if activated
455
+ risk_proba = None
456
+ if result.activated:
457
+ X = np.array([[
458
+ row["glucose_mgdl"],
459
+ row["roc_mgdl_min"],
460
+ row["insulin_units"],
461
+ row["carbs_g"],
462
+ row["hr"],
463
+ ]])
464
+ try:
465
+ risk_proba = predict_proba(X)
466
+ except:
467
+ risk_proba = None
468
+
469
+ # Ground truth (for evaluation)
470
+ future_idx = min(idx + 6, len(df) - 1)
471
+ future_glucose = df.iloc[future_idx]["glucose_mgdl"]
472
+ true_risk = 1 if (future_glucose < hypo_threshold or future_glucose > hyper_threshold) else 0
473
+ ground_truth.append(true_risk)
474
+
475
+ # Record telemetry
476
+ telemetry = TelemetryEvent(
477
+ timestamp=context.timestamp,
478
+ event_id=idx,
479
+ glucose=row["glucose_mgdl"],
480
+ roc=row["roc_mgdl_min"],
481
+ significance=result.significance,
482
+ threshold=result.threshold_used,
483
+ activated=result.activated,
484
+ energy_level=result.energy_consumed, # Use energy_consumed as proxy
485
+ risk_proba=risk_proba,
486
+ processing_time_ms=t_elapsed,
487
+ components=result.explanation.get("feature_contributions", {}),
488
+ )
489
+ monitor.add_event(telemetry)
490
+
491
+ results.append({
492
+ "timestamp": row["timestamp"],
493
+ "glucose": row["glucose_mgdl"],
494
+ "roc": row["roc_mgdl_min"],
495
+ "significance": result.significance,
496
+ "threshold": result.threshold_used,
497
+ "activated": result.activated,
498
+ "energy_level": result.energy_consumed,
499
+ "risk_proba": risk_proba,
500
+ "true_risk": true_risk,
501
+ })
502
+
503
+ progress_bar.empty()
504
+ status_text.empty()
505
+
506
+ # Convert to DataFrame
507
+ results_df = pd.DataFrame(results)
508
+ telemetry_df = monitor.get_telemetry_df()
509
+
510
+ # Compute metrics
511
+ total_events = len(results_df)
512
+ total_activations = int(results_df["activated"].sum())
513
+ activation_rate = total_activations / total_events
514
+ energy_savings = 1 - activation_rate
515
+
516
+ # Statistical evaluation (on activated events)
517
+ activated_results = results_df[results_df["activated"]].copy()
518
+ if len(activated_results) > 10:
519
+ y_true = activated_results["true_risk"].values
520
+ y_pred = (activated_results["risk_proba"].fillna(0.5) >= 0.5).astype(int).values
521
+
522
+ f1 = f1_score(y_true, y_pred, zero_division=0)
523
+ precision = precision_score(y_true, y_pred, zero_division=0)
524
+ recall = recall_score(y_true, y_pred, zero_division=0)
525
+
526
+ if show_bootstrap:
527
+ f1_mean, f1_low, f1_high = bootstrap_metric(y_true, y_pred, lambda yt, yp: f1_score(yt, yp, zero_division=0))
528
+ prec_mean, prec_low, prec_high = bootstrap_metric(y_true, y_pred, lambda yt, yp: precision_score(yt, yp, zero_division=0))
529
+ rec_mean, rec_low, rec_high = bootstrap_metric(y_true, y_pred, lambda yt, yp: recall_score(yt, yp, zero_division=0))
530
+ else:
531
+ f1 = precision = recall = 0.0
532
+ f1_mean = prec_mean = rec_mean = 0.0
533
+ f1_low = f1_high = prec_low = prec_high = rec_low = rec_high = 0.0
534
+
535
+ # Dashboard
536
+ st.header("📊 Performance Dashboard")
537
+
538
+ col1, col2, col3, col4 = st.columns(4)
539
+ col1.metric("Total Events", f"{total_events}")
540
+ col2.metric("Activations", f"{total_activations} ({activation_rate:.1%})")
541
+ col3.metric("Energy Savings", f"{energy_savings:.1%}")
542
+ col4.metric("Alerts", f"{len(monitor.alerts)}")
543
+
544
+ col1, col2, col3 = st.columns(3)
545
+ if show_bootstrap and len(activated_results) > 10:
546
+ col1.metric("F1 Score", f"{f1_mean:.3f}", help=f"95% CI: [{f1_low:.3f}, {f1_high:.3f}]")
547
+ col2.metric("Precision", f"{prec_mean:.3f}", help=f"95% CI: [{prec_low:.3f}, {prec_high:.3f}]")
548
+ col3.metric("Recall", f"{rec_mean:.3f}", help=f"95% CI: [{rec_low:.3f}, {rec_high:.3f}]")
549
+ else:
550
+ col1.metric("F1 Score", f"{f1:.3f}")
551
+ col2.metric("Precision", f"{precision:.3f}")
552
+ col3.metric("Recall", f"{recall:.3f}")
553
+
554
+ # Visualizations
555
+ st.header("📈 Real-Time Visualizations")
556
+
557
+ # Glucose + Threshold
558
+ fig_col1, fig_col2 = st.columns(2)
559
+
560
+ with fig_col1:
561
+ st.subheader("Glucose Levels")
562
+ chart_data = results_df.set_index("timestamp")[["glucose"]]
563
+ st.line_chart(chart_data, height=250)
564
+
565
+ with fig_col2:
566
+ st.subheader("Significance vs Threshold (Adaptive PI Control)")
567
+ chart_data = results_df.set_index("timestamp")[["significance", "threshold"]]
568
+ st.line_chart(chart_data, height=250)
569
+
570
+ # Energy tracking
571
+ if show_energy_viz:
572
+ st.subheader("Energy Level (Bio-Inspired Regeneration)")
573
+ chart_data = results_df.set_index("timestamp")[["energy_level"]]
574
+ st.line_chart(chart_data, height=200)
575
+
576
+ # Significance components
577
+ if show_components and len(telemetry_df) > 0:
578
+ comp_cols = [c for c in telemetry_df.columns if c.startswith("comp_")]
579
+ if comp_cols:
580
+ st.subheader("Significance Components (Diabetes-Specific Risk Factors)")
581
+ chart_data = telemetry_df.set_index("timestamp")[comp_cols]
582
+ st.line_chart(chart_data, height=200)
583
+
584
+ # Alerts
585
+ st.header("⚠️ Risk Alerts")
586
+ if monitor.alerts:
587
+ alerts_df = pd.DataFrame(monitor.alerts)
588
+ st.dataframe(alerts_df, use_container_width=True)
589
+ else:
590
+ st.info("No high-risk alerts triggered in this window.")
591
+
592
+ # Detailed telemetry
593
+ with st.expander("🔍 Detailed Telemetry (Last 100 Events)"):
594
+ st.dataframe(results_df.tail(100), use_container_width=True)
595
+
596
+ # Export telemetry
597
+ if export_telemetry:
598
+ st.header("📥 Export Telemetry")
599
+ json_data = monitor.export_json()
600
+ st.download_button(
601
+ label="Download Telemetry JSON",
602
+ data=json_data,
603
+ file_name="sundew_diabetes_telemetry.json",
604
+ mime="application/json",
605
+ )
606
+ st.success("Telemetry ready for hardware validation workflows")
607
+
608
+ # Footer
609
+ st.divider()
610
+ st.caption(f"🌿 Powered by Sundew Algorithms v0.7+ | PipelineRuntime with custom DiabetesSignificanceModel | Research prototype")