Skip to content

Commit e8260e9

Browse files
mrp089claude
andcommitted
Compare estimated volume0 to the 3D cavity volume
The 0D Stage-1 estimate of volume0 (Guccione-sphere fit to the load phase) matches the 3D unloaded cavity volume (V at P=0) essentially perfectly: r=1.000, RMS diff 0.11 mL, max 0.12 mL over 124 cycles. The unloaded volume sits at ~75% of EDV, mid-range between ESV and EDV -- the ventricle compresses below it in systole and stretches above it in diastole, which is why it is the correct stretch reference. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 14c8360 commit e8260e9

1 file changed

Lines changed: 64 additions & 0 deletions

File tree

scripts/compare_v0_3d.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Compare the 0D estimated volume0 (Stage-1 load-phase fit) to the 3D cavity
2+
volumes: the unloaded volume (V at P=0) and the operating range (ESV-EDV)."""
3+
import glob
4+
import os
5+
import numpy as np
6+
import matplotlib
7+
matplotlib.use("Agg")
8+
import matplotlib.pyplot as plt
9+
10+
import calibrate_yale as cy
11+
12+
ML = 1e6 # m^3 -> mL
13+
14+
15+
def main():
16+
paths = sorted(glob.glob(os.path.join(cy.DATA_DIR, "*", "cav.LV.csv")),
17+
key=lambda p: int(os.path.basename(os.path.dirname(p)).split("_")[1]))
18+
v0_est, v_unl, esv, edv = [], [], [], []
19+
for p in paths:
20+
b_f, b_t = cy.read_b(p)
21+
v0_est.append(cy.estimate_passive(p, b_f, b_t)[0]) # 0D Guccione-sphere fit
22+
v_unl.append(cy.read_unloaded(p)) # 3D unloaded (V at P=0)
23+
_, _, V = cy.load_cycle(p)
24+
esv.append(V.min()); edv.append(V.max())
25+
v0_est = np.array(v0_est) * ML; v_unl = np.array(v_unl) * ML
26+
esv = np.array(esv) * ML; edv = np.array(edv) * ML
27+
28+
r = np.corrcoef(v0_est, v_unl)[0, 1]
29+
diff = v0_est - v_unl
30+
print(f"estimated volume0 vs 3D unloaded volume (V at P=0), {len(paths)} cycles:")
31+
print(f" r = {r:.4f}, mean diff {diff.mean():+.2f} mL, "
32+
f"RMS diff {np.sqrt((diff**2).mean()):.2f} mL, max |diff| {np.abs(diff).max():.2f} mL")
33+
frac = v0_est / edv
34+
print(f" unloaded volume0 is {np.median(frac):.0%} of EDV (median); "
35+
f"sits between ESV (median {np.median(esv):.0f} mL) and "
36+
f"EDV (median {np.median(edv):.0f} mL)")
37+
38+
fig, ax = plt.subplots(1, 2, figsize=(12, 5))
39+
# panel 1: agreement scatter
40+
lim = [min(v0_est.min(), v_unl.min()) - 3, max(v0_est.max(), v_unl.max()) + 3]
41+
ax[0].plot(lim, lim, "k--", lw=1, label="identity")
42+
ax[0].scatter(v_unl, v0_est, s=22, color="#367", edgecolor="k", lw=0.3)
43+
ax[0].set(xlabel="3D unloaded cavity volume (V at P=0) [mL]",
44+
ylabel="0D estimated volume0 [mL]", xlim=lim, ylim=lim,
45+
title=f"Estimated vs 3D unloaded volume (r = {r:.3f})")
46+
ax[0].legend()
47+
48+
# panel 2: volume0 in the operating-range context, cycles sorted by EDV
49+
o = np.argsort(edv)
50+
x = np.arange(len(o))
51+
ax[1].fill_between(x, esv[o], edv[o], color="#cdd", label="operating range (ESV-EDV)")
52+
ax[1].plot(x, v0_est[o], color="#c44", lw=1.6, label="unloaded volume0 (estimated)")
53+
ax[1].plot(x, esv[o], color="#666", lw=0.8)
54+
ax[1].plot(x, edv[o], color="#666", lw=0.8)
55+
ax[1].set(xlabel="cycle (sorted by EDV)", ylabel="volume [mL]",
56+
title="Unloaded volume0 vs operating range")
57+
ax[1].legend(fontsize=9)
58+
fig.tight_layout()
59+
out = os.path.join(cy.OUT_DIR, "viz_volume0_vs_3d.png")
60+
fig.savefig(out, dpi=120); print("wrote", out)
61+
62+
63+
if __name__ == "__main__":
64+
main()

0 commit comments

Comments
 (0)