Skip to content

Commit 39bada4

Browse files
authored
Claude/physiological model validation 01 (#19)
2 parents fd777ac + 1b6064c commit 39bada4

28 files changed

+7859
-120
lines changed

.github/workflows/d.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ name: D
66

77
on:
88
push:
9-
branches: [ "main" ]
9+
branches: [ "**" ] # Run on all branches
1010
pull_request:
1111
branches: [ "main" ]
1212

README.md

Lines changed: 442 additions & 116 deletions
Large diffs are not rendered by default.

dub.json

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,19 @@
33
"dontelightfoot"
44
],
55
"copyright": "Copyright © 2025, dontelightfoot",
6-
"description": "A minimal D application.",
7-
"license": "proprietary",
8-
"name": "apl"
6+
"description": "Multi-Heart-Model: Heart-Brain Coupling and Organ-On-Chip Platform",
7+
"license": "MIT",
8+
"name": "primal_overlay",
9+
"targetType": "executable",
10+
"targetName": "primal_overlay",
11+
"mainSourceFile": "source/app.d",
12+
"sourcePaths": ["source"],
13+
"importPaths": ["source"],
14+
"dependencies": {},
15+
"configurations": [
16+
{
17+
"name": "application",
18+
"targetType": "executable"
19+
}
20+
]
921
}
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Baroreflex Sensitivity Testing\n",
8+
"\n",
9+
"**Clinical Application:** Assessment of autonomic function and cardiovascular regulation\n",
10+
"\n",
11+
"**Learning Objectives:**\n",
12+
"1. Understand baroreflex physiology and clinical significance\n",
13+
"2. Simulate baroreceptor firing dynamics\n",
14+
"3. Compute baroreflex sensitivity (BRS)\n",
15+
"4. Interpret results in clinical contexts\n",
16+
"\n",
17+
"**Clinical Relevance:**\n",
18+
"- Post-MI risk stratification (La Rovere et al. 1998)\n",
19+
"- Heart failure prognosis\n",
20+
"- Autonomic neuropathy assessment\n",
21+
"- Syncope evaluation"
22+
]
23+
},
24+
{
25+
"cell_type": "code",
26+
"execution_count": null,
27+
"metadata": {},
28+
"outputs": [],
29+
"source": [
30+
"import sys\n",
31+
"sys.path.append('..')\n",
32+
"import numpy as np\n",
33+
"import matplotlib.pyplot as plt\n",
34+
"from src.autonomic.baroreflex import Baroreceptor, BaroreflexController, compute_baroreflex_sensitivity\n",
35+
"from src.validation.benchmarks import PhysiologicalBenchmarks\n",
36+
"\n",
37+
"%matplotlib inline\n",
38+
"plt.rcParams['figure.figsize'] = (14, 10)\n",
39+
"print(\"✓ Imports successful\")"
40+
]
41+
},
42+
{
43+
"cell_type": "markdown",
44+
"metadata": {},
45+
"source": [
46+
"## Part 1: Baroreceptor Firing Dynamics\n",
47+
"\n",
48+
"### Physiological Background\n",
49+
"\n",
50+
"Baroreceptors in carotid sinus and aortic arch sense arterial pressure changes:\n",
51+
"- **High pressure** → Increased firing → ↑ Vagal, ↓ Sympathetic → ↓ HR, ↓ BP\n",
52+
"- **Low pressure** → Decreased firing → ↓ Vagal, ↑ Sympathetic → ↑ HR, ↑ BP"
53+
]
54+
},
55+
{
56+
"cell_type": "code",
57+
"execution_count": null,
58+
"metadata": {},
59+
"outputs": [],
60+
"source": [
61+
"# Create baroreceptor model\n",
62+
"baroreceptor = Baroreceptor()\n",
63+
"\n",
64+
"# Test across pressure range\n",
65+
"pressures = np.linspace(60, 180, 100)\n",
66+
"firing_rates = [baroreceptor.compute_firing_rate(p, 0.001) for p in pressures]\n",
67+
"\n",
68+
"# Plot pressure-firing relationship\n",
69+
"fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))\n",
70+
"\n",
71+
"ax1.plot(pressures, firing_rates, 'b-', linewidth=2.5)\n",
72+
"ax1.axvline(93, color='g', linestyle='--', label='Normal MAP', alpha=0.7)\n",
73+
"ax1.axvline(100, color='r', linestyle='--', label='Sigmoid midpoint', alpha=0.7)\n",
74+
"ax1.set_xlabel('Mean Arterial Pressure (mmHg)', fontsize=12, fontweight='bold')\n",
75+
"ax1.set_ylabel('Firing Rate (spikes/s)', fontsize=12, fontweight='bold')\n",
76+
"ax1.set_title('Baroreceptor Pressure-Firing Relationship\\n(Chapleau & Abboud 2001)', fontsize=13, fontweight='bold')\n",
77+
"ax1.legend(fontsize=11)\n",
78+
"ax1.grid(True, alpha=0.3)\n",
79+
"\n",
80+
"# Derivative (sensitivity)\n",
81+
"dp = np.diff(pressures)\n",
82+
"dfr = np.diff(firing_rates)\n",
83+
"sensitivity = dfr / dp\n",
84+
"ax2.plot(pressures[:-1], sensitivity, 'r-', linewidth=2)\n",
85+
"ax2.set_xlabel('Pressure (mmHg)', fontsize=12, fontweight='bold')\n",
86+
"ax2.set_ylabel('Sensitivity (spikes/s/mmHg)', fontsize=12, fontweight='bold')\n",
87+
"ax2.set_title('Baroreceptor Sensitivity', fontsize=13, fontweight='bold')\n",
88+
"ax2.grid(True, alpha=0.3)\n",
89+
"\n",
90+
"plt.tight_layout()\n",
91+
"plt.show()\n",
92+
"\n",
93+
"print(f\"Firing at 80 mmHg: {baroreceptor.compute_firing_rate(80, 0.001):.1f} spikes/s\")\n",
94+
"print(f\"Firing at 100 mmHg: {baroreceptor.compute_firing_rate(100, 0.001):.1f} spikes/s\")\n",
95+
"print(f\"Firing at 120 mmHg: {baroreceptor.compute_firing_rate(120, 0.001):.1f} spikes/s\")"
96+
]
97+
},
98+
{
99+
"cell_type": "markdown",
100+
"metadata": {},
101+
"source": [
102+
"## Part 2: Baroreflex Control Loop\n",
103+
"\n",
104+
"Simulate complete baroreflex arc: Pressure → Baroreceptor → NTS → Autonomic output"
105+
]
106+
},
107+
{
108+
"cell_type": "code",
109+
"execution_count": null,
110+
"metadata": {},
111+
"outputs": [],
112+
"source": [
113+
"controller = BaroreflexController()\n",
114+
"\n",
115+
"# Simulate pressure ramp\n",
116+
"times = np.arange(0, 20, 0.01)\n",
117+
"pressures = 93 + 30 * np.sin(2 * np.pi * times / 10)\n",
118+
"\n",
119+
"vagal_outputs = []\n",
120+
"sympathetic_outputs = []\n",
121+
"heart_rates = []\n",
122+
"\n",
123+
"for t, p in zip(times, pressures):\n",
124+
" v, s = controller.compute_autonomic_output(p, 0.01, t)\n",
125+
" hr = controller.compute_heart_rate_response(p, baseline_hr=105, dt=0.01, t=t)\n",
126+
" vagal_outputs.append(v)\n",
127+
" sympathetic_outputs.append(s)\n",
128+
" heart_rates.append(hr)\n",
129+
"\n",
130+
"fig, axes = plt.subplots(4, 1, figsize=(14, 12), sharex=True)\n",
131+
"\n",
132+
"axes[0].plot(times, pressures, 'b-', linewidth=2)\n",
133+
"axes[0].set_ylabel('Pressure\\n(mmHg)', fontsize=11, fontweight='bold')\n",
134+
"axes[0].set_title('Baroreflex Response to Pressure Changes', fontsize=13, fontweight='bold')\n",
135+
"axes[0].grid(True, alpha=0.3)\n",
136+
"\n",
137+
"axes[1].plot(times, vagal_outputs, 'g-', linewidth=2, label='Vagal')\n",
138+
"axes[1].plot(times, sympathetic_outputs, 'r-', linewidth=2, label='Sympathetic')\n",
139+
"axes[1].set_ylabel('Autonomic\\nOutput', fontsize=11, fontweight='bold')\n",
140+
"axes[1].legend(fontsize=10)\n",
141+
"axes[1].grid(True, alpha=0.3)\n",
142+
"\n",
143+
"axes[2].plot(times, heart_rates, 'purple', linewidth=2)\n",
144+
"axes[2].set_ylabel('Heart Rate\\n(bpm)', fontsize=11, fontweight='bold')\n",
145+
"axes[2].grid(True, alpha=0.3)\n",
146+
"\n",
147+
"# Phase relationship\n",
148+
"axes[3].scatter(pressures, heart_rates, c=times, cmap='viridis', s=10, alpha=0.6)\n",
149+
"axes[3].set_xlabel('Pressure (mmHg)', fontsize=11, fontweight='bold')\n",
150+
"axes[3].set_ylabel('Heart Rate (bpm)', fontsize=11, fontweight='bold')\n",
151+
"axes[3].set_title('Pressure-HR Relationship', fontsize=12, fontweight='bold')\n",
152+
"axes[3].grid(True, alpha=0.3)\n",
153+
"\n",
154+
"plt.tight_layout()\n",
155+
"plt.show()\n",
156+
"\n",
157+
"print(\"✓ Baroreflex demonstrates reciprocal autonomic control\")"
158+
]
159+
},
160+
{
161+
"cell_type": "markdown",
162+
"metadata": {},
163+
"source": [
164+
"## Part 3: Clinical Baroreflex Sensitivity Testing\n",
165+
"\n",
166+
"### Sequence Method (La Rovere et al. 1998)\n",
167+
"\n",
168+
"Identify sequences where systolic BP and RR interval change in same direction"
169+
]
170+
},
171+
{
172+
"cell_type": "code",
173+
"execution_count": null,
174+
"metadata": {},
175+
"outputs": [],
176+
"source": [
177+
"# Simulate spontaneous BP variations\n",
178+
"np.random.seed(42)\n",
179+
"n_beats = 100\n",
180+
"baseline_sbp = 120\n",
181+
"baseline_rr = 850 # ms\n",
182+
"\n",
183+
"sbp_variations = baseline_sbp + np.random.randn(n_beats) * 5\n",
184+
"rr_variations = baseline_rr + (sbp_variations - baseline_sbp) * 10 + np.random.randn(n_beats) * 5\n",
185+
"\n",
186+
"# Compute BRS\n",
187+
"brs_values = []\n",
188+
"for i in range(len(sbp_variations) - 3):\n",
189+
" dp = sbp_variations[i+3] - sbp_variations[i]\n",
190+
" drr = rr_variations[i+3] - rr_variations[i]\n",
191+
" if abs(dp) > 1:\n",
192+
" brs = drr / dp\n",
193+
" if 3 < brs < 30:\n",
194+
" brs_values.append(brs)\n",
195+
"\n",
196+
"mean_brs = np.mean(brs_values) if brs_values else 0\n",
197+
"\n",
198+
"fig, axes = plt.subplots(2, 2, figsize=(14, 10))\n",
199+
"\n",
200+
"# Time series\n",
201+
"axes[0,0].plot(sbp_variations, 'b-', linewidth=1.5)\n",
202+
"axes[0,0].set_ylabel('SBP (mmHg)', fontsize=11, fontweight='bold')\n",
203+
"axes[0,0].set_title('Systolic Blood Pressure', fontsize=12, fontweight='bold')\n",
204+
"axes[0,0].grid(True, alpha=0.3)\n",
205+
"\n",
206+
"axes[0,1].plot(rr_variations, 'r-', linewidth=1.5)\n",
207+
"axes[0,1].set_ylabel('RR Interval (ms)', fontsize=11, fontweight='bold')\n",
208+
"axes[0,1].set_title('RR Intervals', fontsize=12, fontweight='bold')\n",
209+
"axes[0,1].grid(True, alpha=0.3)\n",
210+
"\n",
211+
"# Scatter plot\n",
212+
"axes[1,0].scatter(sbp_variations, rr_variations, alpha=0.6, s=50)\n",
213+
"z = np.polyfit(sbp_variations, rr_variations, 1)\n",
214+
"p = np.poly1d(z)\n",
215+
"axes[1,0].plot(sbp_variations, p(sbp_variations), 'r--', linewidth=2, label=f'BRS = {z[0]:.1f} ms/mmHg')\n",
216+
"axes[1,0].set_xlabel('SBP (mmHg)', fontsize=11, fontweight='bold')\n",
217+
"axes[1,0].set_ylabel('RR Interval (ms)', fontsize=11, fontweight='bold')\n",
218+
"axes[1,0].set_title('Baroreflex Sensitivity', fontsize=12, fontweight='bold')\n",
219+
"axes[1,0].legend(fontsize=11)\n",
220+
"axes[1,0].grid(True, alpha=0.3)\n",
221+
"\n",
222+
"# BRS distribution\n",
223+
"axes[1,1].hist(brs_values, bins=20, edgecolor='black', alpha=0.7)\n",
224+
"axes[1,1].axvline(mean_brs, color='r', linestyle='--', linewidth=2, label=f'Mean = {mean_brs:.1f}')\n",
225+
"axes[1,1].axvspan(3, 30, alpha=0.2, color='green', label='Normal range')\n",
226+
"axes[1,1].set_xlabel('BRS (ms/mmHg)', fontsize=11, fontweight='bold')\n",
227+
"axes[1,1].set_ylabel('Frequency', fontsize=11, fontweight='bold')\n",
228+
"axes[1,1].set_title('BRS Distribution', fontsize=12, fontweight='bold')\n",
229+
"axes[1,1].legend(fontsize=10)\n",
230+
"axes[1,1].grid(True, alpha=0.3, axis='y')\n",
231+
"\n",
232+
"plt.tight_layout()\n",
233+
"plt.show()\n",
234+
"\n",
235+
"benchmarks = PhysiologicalBenchmarks()\n",
236+
"print(f\"\\nBaroreflex Sensitivity: {mean_brs:.1f} ms/mmHg\")\n",
237+
"print(f\"Normal range: {benchmarks.baroreflex.brs_normal.min_value:.1f}-{benchmarks.baroreflex.brs_normal.max_value:.1f} ms/mmHg\")\n",
238+
"print(f\"Interpretation: {'Normal' if 3 < mean_brs < 30 else 'Impaired'}\")"
239+
]
240+
},
241+
{
242+
"cell_type": "markdown",
243+
"metadata": {},
244+
"source": [
245+
"## Summary\n",
246+
"\n",
247+
"### Clinical Interpretation\n",
248+
"\n",
249+
"**BRS Values:**\n",
250+
"- **>12 ms/mmHg**: Normal baroreflex function\n",
251+
"- **6-12 ms/mmHg**: Moderately impaired\n",
252+
"- **<6 ms/mmHg**: Severely impaired (high risk)\n",
253+
"\n",
254+
"**Clinical Applications:**\n",
255+
"1. Post-MI risk stratification\n",
256+
"2. Heart failure prognosis \n",
257+
"3. Autonomic neuropathy detection\n",
258+
"4. Drug effect assessment\n",
259+
"\n",
260+
"---\n",
261+
"© 2025 Multi-Heart-Model Project | MIT License"
262+
]
263+
}
264+
],
265+
"metadata": {
266+
"kernelspec": {
267+
"display_name": "Python 3",
268+
"language": "python",
269+
"name": "python3"
270+
}
271+
},
272+
"nbformat": 4,
273+
"nbformat_minor": 4
274+
}

0 commit comments

Comments
 (0)