mgbam commited on
Commit
8cc4f3e
·
verified ·
1 Parent(s): b29641a

Upload 3 files

Browse files
Files changed (2) hide show
  1. README.md +94 -17
  2. app_space_wow.py +273 -0
README.md CHANGED
@@ -1,32 +1,109 @@
1
  ---
2
  title: Sundew Health Demo
3
  sdk: gradio
4
- sdk_version: 6.0.2
5
- app_file: app_space.py
6
  pinned: false
7
  ---
8
 
9
- # Sundew Health Backend
10
 
11
- Neurosymbolic, energy-aware ECG monitoring backend using FastAPI, PyTorch, and PostgreSQL.
12
 
13
- ## Quickstart
14
 
15
- 1. Create a `.env` from `.env.example` and set `DATABASE_URL`.
16
- 2. Install dependencies (CPU PyTorch wheels): `pip install -e . --extra-index-url https://download.pytorch.org/whl/cpu`.
17
- 3. Run the API: `uvicorn app.main:app --reload`.
18
 
19
- ## Database (PostgreSQL + Alembic)
 
 
 
 
20
 
21
- - Set `DATABASE_URL` to your Postgres DSN (e.g., `postgresql+asyncpg://user:pass@localhost:5432/sundew_health`).
22
- - Run migrations: `alembic upgrade head`.
23
- - Create new migrations: `alembic revision -m "message" --autogenerate`.
24
 
25
- ## Tests
 
 
 
 
26
 
27
- Run `pytest` to execute the test suite.
 
 
 
 
 
28
 
29
- ## Notes
 
 
 
30
 
31
- - Gating uses `sundew-algorithms` (significance + hysteresis) ahead of model inference.
32
- - Adaptive Sparse Training (`adaptive-sparse-training`) is installed; torch shims are applied to load it, but training still uses the simpler loop until AST wiring is added.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Sundew Health Demo
3
  sdk: gradio
4
+ sdk_version: 4.44.0
5
+ app_file: app_space_wow.py
6
  pinned: false
7
  ---
8
 
9
+ # Sundew Health Monitor
10
 
11
+ **Neurosymbolic, energy-aware AI for continuous ECG monitoring**
12
 
13
+ Combines adaptive gating, ML inference, and symbolic reasoning to deliver trustworthy, energy-efficient medical biosignal monitoring.
14
 
15
+ ## Key Features
 
 
16
 
17
+ - **85% energy reduction** via Sundew gating algorithm
18
+ - **Explainable alerts** through symbolic rule chains
19
+ - **Real-time streaming** with WebSocket support
20
+ - **Multi-patient dashboard** for clinical monitoring
21
+ - **Production-ready API** with comprehensive SDK
22
 
23
+ ## Quick Start
 
 
24
 
25
+ ### Docker (Recommended)
26
+ ```bash
27
+ docker-compose up --build
28
+ ```
29
+ API: http://localhost:8000 | Docs: http://localhost:8000/docs
30
 
31
+ ### Local Development
32
+ ```bash
33
+ pip install -e . --extra-index-url https://download.pytorch.org/whl/cpu
34
+ alembic upgrade head
35
+ uvicorn app.main:app --reload
36
+ ```
37
 
38
+ ### Live Demo
39
+ ```bash
40
+ python app_space_live.py
41
+ ```
42
 
43
+ ## Architecture
44
+
45
+ ```
46
+ ECG Stream → Sundew Gating → ML Inference → Rule Engine → Alert
47
+ (50-90% reduction) (PyTorch CNN) (Symbolic)
48
+ ```
49
+
50
+ **Three-stage neurosymbolic pipeline:**
51
+ 1. **Gating** - Adaptive windowing with significance scoring
52
+ 2. **Inference** - Energy-efficient neural network
53
+ 3. **Rules** - Medical knowledge base for explainability
54
+
55
+ ## API Endpoints
56
+
57
+ - `POST /ecg/infer` - Run full pipeline
58
+ - `WS /ws/ecg/{patient_id}` - Real-time streaming
59
+ - `GET /dashboard/stats` - System metrics
60
+ - `GET /dashboard/patients` - Patient summaries
61
+ - `GET /dashboard/alerts` - Alert queue
62
+
63
+ ## SDK Usage
64
+
65
+ ```python
66
+ from sundew_sdk import SundewClient
67
+
68
+ client = SundewClient(base_url="http://localhost:8000")
69
+ result = client.infer_ecg(
70
+ patient_id="p001",
71
+ signal=[...],
72
+ age=72,
73
+ has_prior_stroke=False
74
+ )
75
+
76
+ print(f"{result['label']} | Alert: {result['alert_level']}")
77
+ ```
78
+
79
+ See `sdk/example_client.py` for complete examples.
80
+
81
+ ## Benchmarking
82
+
83
+ Evaluate against MIT-BIH Arrhythmia Database:
84
+
85
+ ```bash
86
+ bash scripts/run_full_benchmark.sh
87
+ ```
88
+
89
+ **Metrics tracked:**
90
+ - Accuracy, Precision, Recall, F1
91
+ - FLOPs reduction
92
+ - Energy savings
93
+ - Latency
94
+
95
+ Results saved to `benchmarks/results/`
96
+
97
+ ## Documentation
98
+
99
+ - [DEPLOYMENT.md](DEPLOYMENT.md) - Production deployment guide
100
+ - [LAUNCH.md](LAUNCH.md) - Complete feature overview
101
+ - [CLAUDE.md](CLAUDE.md) - Architecture documentation
102
+
103
+ ## Technology Stack
104
+
105
+ - **Backend:** FastAPI, SQLAlchemy, Alembic
106
+ - **ML:** PyTorch, Adaptive Sparse Training
107
+ - **Algorithms:** Sundew (gating), symbolic reasoning
108
+ - **Database:** PostgreSQL (production), SQLite (dev)
109
+ - **Demo:** Gradio
app_space_wow.py ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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") as demo:
206
+
207
+ # Header
208
+ gr.HTML("""
209
+ <div style="text-align: center; padding: 30px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 15px; margin-bottom: 20px;">
210
+ <h1 style="margin: 0; font-size: 42px; font-weight: 800;">Sundew ECG Monitor</h1>
211
+ <p style="margin: 10px 0 0 0; font-size: 18px; opacity: 0.95;">Neurosymbolic AI for Energy-Efficient Medical Monitoring</p>
212
+ <div style="margin-top: 15px; display: inline-flex; gap: 20px; flex-wrap: wrap; justify-content: center;">
213
+ <span style="background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 20px;">⚡ 85% Energy Savings</span>
214
+ <span style="background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 20px;">🧠 Explainable AI</span>
215
+ <span style="background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 20px;">🏥 Clinical-Grade Rules</span>
216
+ </div>
217
+ </div>
218
+ """)
219
+
220
+ with gr.Row():
221
+ with gr.Column(scale=1):
222
+ gr.Markdown("### Select Patient Scenario")
223
+ scenario_dropdown = gr.Radio(
224
+ choices=list(SCENARIOS.keys()),
225
+ value="afib_high_risk",
226
+ label="",
227
+ info="Choose a patient to analyze"
228
+ )
229
+
230
+ for key, val in SCENARIOS.items():
231
+ gr.Markdown(f"**{val['icon']} {val['name']}**\n{val['description']}", visible=(key=="afib_high_risk"))
232
+
233
+ run_btn = gr.Button("Run Analysis", variant="primary", size="lg")
234
+
235
+ gr.Markdown("---")
236
+ gr.Markdown("""
237
+ **Architecture:**
238
+ ```
239
+ ECG Signal → Sundew Gating → ML Inference → Rule Engine
240
+ (50-90% reduction) (PyTorch) (Symbolic)
241
+ ```
242
+ """)
243
+
244
+ with gr.Column(scale=2):
245
+ summary_card = gr.HTML()
246
+
247
+ with gr.Tabs():
248
+ with gr.Tab("📊 Signal Analysis"):
249
+ signal_plot = gr.Image(label="ECG: Original vs Gated")
250
+
251
+ with gr.Tab("⚡ Energy Efficiency"):
252
+ energy_plot = gr.Image(label="Compute Savings")
253
+
254
+ with gr.Tab("🔗 Rule Chain"):
255
+ rule_trace = gr.Markdown()
256
+
257
+ run_btn.click(
258
+ run_pipeline,
259
+ inputs=scenario_dropdown,
260
+ outputs=[summary_card, signal_plot, energy_plot, rule_trace]
261
+ )
262
+
263
+ # Footer
264
+ gr.HTML("""
265
+ <div style="text-align: center; padding: 20px; margin-top: 30px; border-top: 1px solid #eee;">
266
+ <p style="color: #666; font-size: 14px;">
267
+ Built with Sundew Algorithm · FastAPI · PyTorch · Gradio
268
+ </p>
269
+ </div>
270
+ """)
271
+
272
+ if __name__ == "__main__":
273
+ demo.launch()