mgbam commited on
Commit
b63d191
·
verified ·
1 Parent(s): 2fd0892

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +24 -12
  2. app_space.py +98 -0
  3. requirements.txt +9 -0
README.md CHANGED
@@ -1,12 +1,24 @@
1
- ---
2
- title: SundewAIHealth
3
- emoji: 🐢
4
- colorFrom: purple
5
- colorTo: pink
6
- sdk: gradio
7
- sdk_version: 6.0.2
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Sundew Health Backend
2
+
3
+ Neurosymbolic, energy-aware ECG monitoring backend using FastAPI, PyTorch, and PostgreSQL.
4
+
5
+ ## Quickstart
6
+
7
+ 1. Create a `.env` from `.env.example` and set `DATABASE_URL`.
8
+ 2. Install dependencies (CPU PyTorch wheels): `pip install -e . --extra-index-url https://download.pytorch.org/whl/cpu`.
9
+ 3. Run the API: `uvicorn app.main:app --reload`.
10
+
11
+ ## Database (PostgreSQL + Alembic)
12
+
13
+ - Set `DATABASE_URL` to your Postgres DSN (e.g., `postgresql+asyncpg://user:pass@localhost:5432/sundew_health`).
14
+ - Run migrations: `alembic upgrade head`.
15
+ - Create new migrations: `alembic revision -m "message" --autogenerate`.
16
+
17
+ ## Tests
18
+
19
+ Run `pytest` to execute the test suite.
20
+
21
+ ## Notes
22
+
23
+ - Gating uses `sundew-algorithms` (significance + hysteresis) ahead of model inference.
24
+ - 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.
app_space.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import json
3
+ from typing import Any, Dict, List
4
+
5
+ import gradio as gr
6
+ import matplotlib.pyplot as plt
7
+
8
+ from app.ml.gating import gate_signal
9
+ from app.ml.inference import infer_ecg, load_model
10
+ from app.rules.engine import evaluate_ecg_rules
11
+
12
+
13
+ # Preload model (uses ./checkpoints/ecg_classifier.pt if present)
14
+ load_model()
15
+
16
+
17
+ def parse_signal(text: str | List[float]) -> List[float]:
18
+ if isinstance(text, list):
19
+ return [float(x) for x in text]
20
+ try:
21
+ return [float(x) for x in json.loads(text)]
22
+ except Exception:
23
+ raise gr.Error("Provide ECG samples as a JSON list, e.g., [0.1, 0.2, 0.3]")
24
+
25
+
26
+ def run_infer(signal_text: str) -> Dict[str, Any]:
27
+ sig = parse_signal(signal_text)
28
+ gated, gating_meta = gate_signal(sig, return_windows=True)
29
+ model_output: Dict[str, Any] = infer_ecg(
30
+ gated,
31
+ original_len=len(sig),
32
+ gating_meta=gating_meta,
33
+ )
34
+ patient_context = {"patient_id": "demo"}
35
+ rules_result = evaluate_ecg_rules(patient_context, model_output)
36
+ explanations = [*(model_output.get("gating", {}).get("explanations", []) if isinstance(model_output.get("gating"), dict) else []),
37
+ *rules_result.get("explanations", [])]
38
+ return {
39
+ "label": model_output.get("label"),
40
+ "score": round(float(model_output.get("score", 0.0)), 3),
41
+ "hr": model_output.get("hr"),
42
+ "alert_level": rules_result.get("alert_level", "none"),
43
+ "gated_ratio": round(model_output.get("gated_ratio", 1.0), 3),
44
+ "gating": gating_meta,
45
+ "explanations": explanations,
46
+ }
47
+
48
+
49
+ def plot_gating(signal_text: str):
50
+ sig = parse_signal(signal_text)
51
+ gated, meta = gate_signal(sig, return_windows=True)
52
+ fig, axes = plt.subplots(2, 1, figsize=(6, 4))
53
+ axes[0].plot(sig, color="#0066ff", linewidth=1)
54
+ axes[0].set_title("Raw signal")
55
+ axes[1].plot(gated, color="#ff6600", linewidth=1)
56
+ axes[1].set_title(f"Gated signal (ratio={meta['ratio']:.2f})")
57
+ fig.tight_layout()
58
+ buf = io.BytesIO()
59
+ fig.savefig(buf, format="png", dpi=120)
60
+ plt.close(fig)
61
+ buf.seek(0)
62
+ return buf
63
+
64
+
65
+ demo_normal = [0.05 for _ in range(256)]
66
+ demo_afib = [0.3 for _ in range(256)]
67
+
68
+ with gr.Blocks(title="Sundew ECG Demo") as demo:
69
+ gr.Markdown("### Neurosymbolic ECG • Sundew Gating + Rules")
70
+ with gr.Tabs():
71
+ with gr.Tab("Upload/Infer"):
72
+ inp = gr.Textbox(
73
+ label="ECG samples (JSON list)",
74
+ value=json.dumps(demo_afib[:128]),
75
+ )
76
+ out = gr.JSON(label="Inference")
77
+ btn = gr.Button("Run")
78
+ btn.click(run_infer, inputs=inp, outputs=out)
79
+ with gr.Tab("Gating Preview"):
80
+ inp2 = gr.Textbox(
81
+ label="ECG samples (JSON list)",
82
+ value=json.dumps(demo_afib[:128]),
83
+ )
84
+ img = gr.Image(type="filepath", label="Raw vs Gated")
85
+ btn2 = gr.Button("Show gating")
86
+ btn2.click(plot_gating, inputs=inp2, outputs=img)
87
+ with gr.Tab("Demos"):
88
+ out_demo = gr.JSON()
89
+ btn_n = gr.Button("Normal")
90
+ btn_a = gr.Button("Arrhythmia-ish")
91
+ hidden_n = gr.Textbox(value=json.dumps(demo_normal), visible=False)
92
+ hidden_a = gr.Textbox(value=json.dumps(demo_afib), visible=False)
93
+ btn_n.click(run_infer, inputs=hidden_n, outputs=out_demo)
94
+ btn_a.click(run_infer, inputs=hidden_a, outputs=out_demo)
95
+
96
+
97
+ if __name__ == "__main__":
98
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=4.44
2
+ torch==2.2.2+cpu
3
+ torchvision==0.17.2+cpu
4
+ --extra-index-url https://download.pytorch.org/whl/cpu
5
+ pandas
6
+ tqdm
7
+ sundew-algorithms
8
+ adaptive-sparse-training
9
+ matplotlib