codexlung-volume-subdivisions.py
lung-volume-subdivisions.py
"""Constructed lung-volume spirogram and endpoint ledger.
Python 3.14, NumPy 2.3.5, Matplotlib 3.10.7. Run in _media; output PNG in cwd.
Absolute volume is stipulated: spirometry alone does not determine the RV offset.
"""
from pathlib import Path
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
rv, frc, vt, tlc = 1.4, 2.5, .5, 6.0
assert abs((tlc-rv)-(3.0+.5+1.1))<1e-12
fig, (ax, ledger) = plt.subplots(1, 2, figsize=(9, 5), dpi=100,
gridspec_kw={'width_ratios':[1.45, 1]}, facecolor='white')
t0=np.linspace(0, 6, 301);v0=frc+vt*(1-np.cos(np.pi*t0))/2
# Quiet breaths end at FRC; smooth maximal inspiration, full expiration, recovery.
t1=np.linspace(6, 8, 101);v1=frc+(tlc-frc)*(1-np.cos(np.pi*(t1-6)/2))/2
t2=np.linspace(8, 11, 151);v2=tlc-(tlc-rv)*(1-np.cos(np.pi*(t2-8)/3))/2
t3=np.linspace(11, 13, 101);v3=rv+(frc-rv)*(1-np.cos(np.pi*(t3-11)/2))/2
ax.plot(np.r_[t0,t1,t2,t3],np.r_[v0,v1,v2,v3],color='#246b91',lw=2.2)
levels=[(rv,'RV = 1.4 L'),(frc,'FRC = 2.5 L'),(frc+vt,'End-tidal inspiration = 3.0 L'),(tlc,'TLC = 6.0 L')]
for y,label in levels:
ax.axhline(y,ls=':',color='#9a9a9a',lw=.8)
ax.set(xlim=(0,13.2),ylim=(0,6.5),xlabel='Illustrative time (s)',ylabel='Stipulated lung-gas volume (L)')
ax.text(2.4,3.35,'Quiet tidal breaths',fontsize=9,ha='center')
ax.text(8,6.17,'Maximal inspiration',fontsize=9,ha='center')
ax.text(11,1.10,'Maximal expiration',fontsize=9,ha='center')
ax.spines[['top','right']].set_visible(False)
ledger.set(xlim=(-.2,5.5),ylim=(0,6.5));ledger.axis('off')
for y,label in levels:
ledger.hlines(y,-.1,5.1,color='#b0b0b0',ls=':',lw=.8)
ledger.text(-.1,y+.08,label,fontsize=8,color='#555555')
segments=[(.25,0,rv,'RV\n1.4 L','#66757f'),(1.25,rv,frc,'ERV\n1.1 L','#287c5d'),
(1.25,frc,frc+vt,'VT 0.5 L','#a26726'),(1.25,frc+vt,tlc,'IRV\n3.0 L','#246b91'),
(3.0,frc,tlc,'IC\n3.5 L','#704795'),(4.35,rv,tlc,'VC\n4.6 L','#9b4545')]
for x,a,b,label,color in segments:
ledger.annotate('',xy=(x,b),xytext=(x,a),arrowprops=dict(arrowstyle='<->',lw=1.6,color=color))
ledger.text(x+.13,(a+b)/2,label,va='center',fontsize=9,color=color)
fig.suptitle('Lung-volume subdivisions: the endpoints define the quantity',fontsize=13)
fig.text(.5,.035,'Constructed values, not reference limits. The absolute residual-volume offset requires an independent measurement.',ha='center',fontsize=9)
fig.subplots_adjust(left=.075,right=.98,bottom=.17,top=.85,wspace=.24)
fig.savefig(Path('lung-volume-subdivisions.png'),facecolor='white',dpi=100)
plt.close(fig)