codexhydrogen-equilibrium-and-frozen-rotational-heat-capacity.py
hydrogen-equilibrium-and-frozen-rotational-heat-capacity.py
"""Original exact-sum rotor comparison; Python 3.14, numpy 2.3.5, matplotlib 3.10.7.
Run from _media; writes this script's basename.png to the current directory.
Temperature is dimensionless: t = T/theta_r. No measured constants or data.
"""
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
def rotor_models(t, jmax=100):
"""Return equilibrium ortho fraction, C_eq/kB, C_frozen/kB, and pure-para C/kB.
Subtract each sector's lowest energy before exponentiating, retaining its
physical offset in mean energies. Frozen composition is x_ortho=3/4.
"""
j = np.arange(jmax+1)
e = j*(j+1)
means, variances, logq = [], [], []
for parity in [0, 1]:
en = e[j%2 == parity]
g = 2*j[j%2 == parity]+1
weights = g*np.exp(-(en-en[0])/t)
z = weights.sum()
p = weights/z
mean = np.dot(p, en)
means.append(mean)
variances.append(np.dot(p, (en-mean)**2))
logq.append(np.log(z)-en[0]/t)
f = 1/(1+np.exp(logq[0]-logq[1])/3)
within = (1-f)*variances[0]+f*variances[1]
between = f*(1-f)*(means[1]-means[0])**2
return f, (within+between)/t**2, (.25*variances[0]+.75*variances[1])/t**2, variances[0]/t**2
def main():
plt.rcParams.update({'font.size':11, 'figure.facecolor':'white', 'axes.facecolor':'white'})
t = np.geomspace(.12, 20, 750)
values = np.array([rotor_models(x) for x in t])
fig, axes = plt.subplots(2, 1, figsize=(9, 7), sharex=True, layout='constrained', gridspec_kw={'height_ratios':[1,1.6]})
a, b = axes
a.plot(t, values[:,0], color='#176b98', lw=2.5, label='Conversion maintains equilibrium')
a.axhline(.75, color='#b45220', ls='--', lw=2, label='Frozen normal hydrogen: 3/4 ortho')
a.set_ylabel('Ortho fraction'); a.set_ylim(-.035,.88); a.set_yticks([0,.25,.5,.75]); a.legend(loc='lower right', fontsize=10)
b.plot(t, values[:,1], color='#176b98', lw=2.5, label='Equilibrium composition')
b.plot(t, values[:,2], color='#b45220', ls='--', lw=2.5, label='Frozen 3:1 ortho/para composition')
b.plot(t, values[:,3], color='#567a3e', ls=':', lw=2, label='Pure para (even J only)')
b.axhline(1, color='#888888', lw=.8)
b.set_ylabel(r'Rotational heat capacity per molecule $c_{\rm rot}/k_{\rm B}$')
b.set_xlabel(r'Dimensionless temperature $t=T/\theta_r$')
b.set_ylim(-.05,2.3); b.legend(loc='upper right', fontsize=10)
b.annotate('Conversion contributes\nto equilibrium heat capacity', xy=(.52,1.9), xytext=(.13,1.1), arrowprops={'arrowstyle':'->','color':'#333333'}, fontsize=10)
for ax in axes:
ax.set_xscale('log'); ax.set_xlim(.12,20); ax.grid(alpha=.18); ax.spines[['top','right']].set_visible(False)
fig.suptitle('The same rotor levels, two different thermal constraints', fontsize=16, weight='bold')
fig.supxlabel('Ideal independent rotors; electronic and vibrational excitation omitted. Exact sums through J = 100.', fontsize=10)
fig.savefig(Path(__file__).stem+'.png', dpi=150, facecolor='white', transparent=False)
if __name__ == '__main__':
main()