mgbam commited on
Commit
4f9cd0f
·
verified ·
1 Parent(s): ce0b1fc

Upload app_space_wow.py

Browse files
Files changed (1) hide show
  1. app_space_wow.py +276 -0
app_space_wow.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Sundew Live Monitor - Enhanced "Wow" Demo
3
+ Production-quality interface showcasing neurosymbolic ECG monitoring
4
+ """
5
+ import io
6
+ import json
7
+ import math
8
+ import os
9
+ import sys
10
+ from typing import Any, Dict, List
11
+
12
+ import gradio as gr
13
+ import matplotlib.pyplot as plt
14
+ import matplotlib.image as mpimg
15
+ import numpy as np
16
+ import pandas as pd
17
+
18
+ ROOT = os.path.dirname(os.path.abspath(__file__))
19
+ if ROOT not in sys.path:
20
+ sys.path.insert(0, ROOT)
21
+
22
+ from app.ml.gating import gate_signal
23
+ from app.ml.inference import infer_ecg, load_model
24
+ from app.rules.engine import evaluate_ecg_rules
25
+
26
+ load_model()
27
+
28
+ SCENARIOS = {
29
+ "healthy": {
30
+ "name": "Healthy Adult (60yo)",
31
+ "age": 60,
32
+ "has_prior_stroke": False,
33
+ "signal_type": "normal",
34
+ "description": "Normal sinus rhythm, no risk factors, routine monitoring",
35
+ "icon": "✓"
36
+ },
37
+ "afib_high_risk": {
38
+ "name": "AFib Suspect (85yo, Prior Stroke)",
39
+ "age": 85,
40
+ "has_prior_stroke": True,
41
+ "signal_type": "afib",
42
+ "description": "Irregular rhythm detected, high-risk patient requiring immediate review",
43
+ "icon": "⚠"
44
+ },
45
+ "tachycardia": {
46
+ "name": "Tachycardia Episode (45yo)",
47
+ "age": 45,
48
+ "has_prior_stroke": False,
49
+ "signal_type": "tachy",
50
+ "description": "Elevated heart rate (120+ bpm), otherwise healthy patient",
51
+ "icon": "↑"
52
+ },
53
+ "elderly_normal": {
54
+ "name": "Elderly Patient (78yo, Normal ECG)",
55
+ "age": 78,
56
+ "has_prior_stroke": True,
57
+ "signal_type": "normal",
58
+ "description": "High-risk profile but currently stable rhythm",
59
+ "icon": "👤"
60
+ },
61
+ "noisy": {
62
+ "name": "Poor Signal Quality",
63
+ "age": 60,
64
+ "has_prior_stroke": False,
65
+ "signal_type": "noise",
66
+ "description": "Motion artifacts, low-quality signal requiring gating",
67
+ "icon": "~"
68
+ }
69
+ }
70
+
71
+
72
+ def generate_signal(signal_type: str, length: int = 512) -> List[float]:
73
+ if signal_type == "normal":
74
+ return [0.05 * math.sin(2 * math.pi * 2 * (i / length)) +
75
+ 0.02 * math.sin(2 * math.pi * 0.5 * (i / length)) for i in range(length)]
76
+ elif signal_type == "afib":
77
+ return [
78
+ 0.25 * math.sin(2 * math.pi * 6 * (i / length)) +
79
+ 0.05 * math.sin(2 * math.pi * 15 * (i / length)) +
80
+ (0.15 if i % 40 == 0 else 0.0) +
81
+ 0.03 * (hash(i) % 100 - 50) / 500
82
+ for i in range(length)
83
+ ]
84
+ elif signal_type == "tachy":
85
+ return [0.08 * math.sin(2 * math.pi * 4.5 * (i / length)) +
86
+ 0.03 * math.sin(2 * math.pi * 1 * (i / length)) for i in range(length)]
87
+ elif signal_type == "noise":
88
+ return [0.02 * math.sin(2 * math.pi * 1 * (i / length)) +
89
+ (0.01 if i % 13 == 0 else 0.0) +
90
+ 0.005 * (hash(i) % 100 - 50) / 50 for i in range(length)]
91
+ return [0.0] * length
92
+
93
+
94
+ def run_pipeline(scenario_key: str):
95
+ scenario = SCENARIOS[scenario_key]
96
+ signal = generate_signal(scenario["signal_type"], length=512)
97
+
98
+ gated, gating_meta = gate_signal(signal, return_windows=True)
99
+ model_output = infer_ecg(gated, original_len=len(signal), gating_meta=gating_meta)
100
+
101
+ patient_context = {
102
+ "patient_id": scenario_key,
103
+ "age": scenario["age"],
104
+ "has_prior_stroke": scenario["has_prior_stroke"],
105
+ }
106
+ rules_result = evaluate_ecg_rules(patient_context, model_output)
107
+
108
+ # Build comprehensive results
109
+ energy_saved = (1 - gating_meta.get("ratio", 1.0)) * 100
110
+
111
+ # Summary card
112
+ summary_html = f"""
113
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 25px; border-radius: 15px; margin: 10px 0;">
114
+ <h2 style="margin: 0 0 15px 0;">Patient: {scenario['name']}</h2>
115
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
116
+ <div style="background: rgba(255,255,255,0.1); padding: 15px; border-radius: 10px;">
117
+ <h3 style="margin: 0; font-size: 14px; opacity: 0.9;">Diagnosis</h3>
118
+ <p style="margin: 5px 0 0 0; font-size: 24px; font-weight: bold;">{model_output.get('label', 'Unknown').upper()}</p>
119
+ <p style="margin: 5px 0 0 0; opacity: 0.8;">Confidence: {model_output.get('score', 0.0):.1%}</p>
120
+ </div>
121
+ <div style="background: rgba(255,255,255,0.1); padding: 15px; border-radius: 10px;">
122
+ <h3 style="margin: 0; font-size: 14px; opacity: 0.9;">Alert Level</h3>
123
+ <p style="margin: 5px 0 0 0; font-size: 24px; font-weight: bold;">{rules_result.get('alert_level', 'NONE').upper()}</p>
124
+ <p style="margin: 5px 0 0 0; opacity: 0.8;">HR: {model_output.get('hr')} bpm</p>
125
+ </div>
126
+ </div>
127
+ <div style="margin-top: 15px; background: rgba(46,213,115,0.2); padding: 12px; border-radius: 8px; border-left: 4px solid #2ed573;">
128
+ <strong>Energy Savings: {energy_saved:.1f}%</strong> | Windows: {gating_meta.get('selected_windows', 0)}/{gating_meta.get('total_windows', 0)}
129
+ </div>
130
+ </div>
131
+ """
132
+
133
+ # Signal visualization
134
+ fig1, axes = plt.subplots(2, 1, figsize=(12, 6))
135
+ axes[0].plot(signal, color='#3498db', linewidth=1.5, alpha=0.8)
136
+ axes[0].set_title('Original ECG Signal', fontsize=13, fontweight='bold')
137
+ axes[0].set_ylabel('Amplitude')
138
+ axes[0].grid(alpha=0.3)
139
+ axes[0].set_xlim(0, len(signal))
140
+
141
+ axes[1].plot(gated, color='#e74c3c', linewidth=1.5, alpha=0.8)
142
+ axes[1].set_title(f'Gated Signal (Compression: {gating_meta.get("ratio", 1.0):.1%})', fontsize=13, fontweight='bold')
143
+ axes[1].set_xlabel('Sample Index')
144
+ axes[1].set_ylabel('Amplitude')
145
+ axes[1].grid(alpha=0.3)
146
+
147
+ fig1.tight_layout()
148
+ buf1 = io.BytesIO()
149
+ fig1.savefig(buf1, format='png', dpi=150, bbox_inches='tight')
150
+ plt.close(fig1)
151
+ buf1.seek(0)
152
+ signal_img = mpimg.imread(buf1)
153
+
154
+ # Energy bar chart
155
+ fig2, ax = plt.subplots(figsize=(10, 4))
156
+ categories = ['Baseline\n(No Gating)', 'Sundew\n(With Gating)']
157
+ compute = [100, gating_meta.get("ratio", 1.0) * 100]
158
+ colors = ['#e74c3c', '#2ecc71']
159
+
160
+ bars = ax.barh(categories, compute, color=colors, edgecolor='black', linewidth=1.5)
161
+ ax.set_xlabel('Compute Used (%)', fontsize=12, fontweight='bold')
162
+ ax.set_xlim(0, 110)
163
+
164
+ for bar, val in zip(bars, compute):
165
+ ax.text(val + 2, bar.get_y() + bar.get_height()/2,
166
+ f'{val:.1f}%', va='center', fontsize=12, fontweight='bold')
167
+
168
+ ax.text(55, 1.6, f'Energy Savings: {energy_saved:.1f}%',
169
+ ha='center', fontsize=14, fontweight='bold',
170
+ bbox=dict(boxstyle='round,pad=0.8', facecolor='#f39c12', alpha=0.8))
171
+
172
+ ax.set_title('Computational Efficiency', fontsize=14, fontweight='bold')
173
+ ax.spines['top'].set_visible(False)
174
+ ax.spines['right'].set_visible(False)
175
+ fig2.tight_layout()
176
+
177
+ buf2 = io.BytesIO()
178
+ fig2.savefig(buf2, format='png', dpi=150, bbox_inches='tight')
179
+ plt.close(fig2)
180
+ buf2.seek(0)
181
+ energy_img = mpimg.imread(buf2)
182
+
183
+ # Rule chain
184
+ rule_md = f"""### Rule Chain Trace
185
+
186
+ **Neural Network Output:**
187
+ - Label: `{model_output.get('label')}` (Confidence: {model_output.get('score', 0.0):.3f})
188
+ - Estimated HR: `{model_output.get('hr')} bpm`
189
+
190
+ **Patient Context:**
191
+ - Age: {scenario['age']} years
192
+ - Prior Stroke: {'Yes' if scenario['has_prior_stroke'] else 'No'}
193
+
194
+ **Rules Evaluated:**
195
+ """
196
+ for exp in rules_result.get('explanations', []):
197
+ rule_md += f"\n- {exp}"
198
+
199
+ rule_md += f"\n\n**Final Alert:** `{rules_result.get('alert_level', 'NONE').upper()}`"
200
+
201
+ return summary_html, signal_img, energy_img, rule_md
202
+
203
+
204
+ # Build Gradio Interface
205
+ with gr.Blocks(title="Sundew ECG Monitor", css="""
206
+ .gradio-container {font-family: 'Inter', sans-serif;}
207
+ .gr-button-primary {background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none;}
208
+ """) as demo:
209
+
210
+ # Header
211
+ gr.HTML("""
212
+ <div style="text-align: center; padding: 30px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 15px; margin-bottom: 20px;">
213
+ <h1 style="margin: 0; font-size: 42px; font-weight: 800;">Sundew ECG Monitor</h1>
214
+ <p style="margin: 10px 0 0 0; font-size: 18px; opacity: 0.95;">Neurosymbolic AI for Energy-Efficient Medical Monitoring</p>
215
+ <div style="margin-top: 15px; display: inline-flex; gap: 20px; flex-wrap: wrap; justify-content: center;">
216
+ <span style="background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 20px;">⚡ 85% Energy Savings</span>
217
+ <span style="background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 20px;">🧠 Explainable AI</span>
218
+ <span style="background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 20px;">🏥 Clinical-Grade Rules</span>
219
+ </div>
220
+ </div>
221
+ """)
222
+
223
+ with gr.Row():
224
+ with gr.Column(scale=1):
225
+ gr.Markdown("### Select Patient Scenario")
226
+ scenario_dropdown = gr.Radio(
227
+ choices=list(SCENARIOS.keys()),
228
+ value="afib_high_risk",
229
+ label="",
230
+ info="Choose a patient to analyze"
231
+ )
232
+
233
+ for key, val in SCENARIOS.items():
234
+ gr.Markdown(f"**{val['icon']} {val['name']}**\n{val['description']}", visible=(key=="afib_high_risk"))
235
+
236
+ run_btn = gr.Button("Run Analysis", variant="primary", size="lg")
237
+
238
+ gr.Markdown("---")
239
+ gr.Markdown("""
240
+ **Architecture:**
241
+ ```
242
+ ECG Signal → Sundew Gating → ML Inference → Rule Engine
243
+ (50-90% reduction) (PyTorch) (Symbolic)
244
+ ```
245
+ """)
246
+
247
+ with gr.Column(scale=2):
248
+ summary_card = gr.HTML()
249
+
250
+ with gr.Tabs():
251
+ with gr.Tab("📊 Signal Analysis"):
252
+ signal_plot = gr.Image(label="ECG: Original vs Gated")
253
+
254
+ with gr.Tab("⚡ Energy Efficiency"):
255
+ energy_plot = gr.Image(label="Compute Savings")
256
+
257
+ with gr.Tab("🔗 Rule Chain"):
258
+ rule_trace = gr.Markdown()
259
+
260
+ run_btn.click(
261
+ run_pipeline,
262
+ inputs=scenario_dropdown,
263
+ outputs=[summary_card, signal_plot, energy_plot, rule_trace]
264
+ )
265
+
266
+ # Footer
267
+ gr.HTML("""
268
+ <div style="text-align: center; padding: 20px; margin-top: 30px; border-top: 1px solid #eee;">
269
+ <p style="color: #666; font-size: 14px;">
270
+ Built with Sundew Algorithm · FastAPI · PyTorch · Gradio
271
+ </p>
272
+ </div>
273
+ """)
274
+
275
+ if __name__ == "__main__":
276
+ demo.launch()