codex/magnesium-charge-states-and-resolution.py

magnesium-charge-states-and-resolution.py
"""Synthetic magnesium spectra; neutral masses approximate ion masses.

Modern input data, not historical Aston observations:
https://physics.nist.gov/cgi-bin/Compositions/stand_alone.pl?ascii=ascii&ele=Mg
Tested with Python 3.14, matplotlib 3.10.7, numpy 2.3.5.
Run from the desired output directory; emits this script's basename.png.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np

masses = np.array([23.985041697, 24.985836976, 25.982592968])
fractions = np.array([.7899, .1000, .1101])
colors = ['#16749b', '#b94b20', '#7754a3']
plt.rcParams.update({'font.size': 10, 'figure.facecolor':'white', 'axes.facecolor':'white'})
fig = plt.figure(figsize=(10.8, 8))
gs = fig.add_gridspec(3, 2, height_ratios=[1.05, 1, .83], hspace=.72, wspace=.25)
a=fig.add_subplot(gs[0,:])
for z,scale in ((1,1), (2,.28)):
    for i,(m,f,c) in enumerate(zip(masses,fractions,colors)):
        x=m/z
        a.vlines(x,0,scale*f,color=c,lw=3)
        a.annotate(f'{24+i}, z={z}', xy=(x,scale*f), xytext=(x + ((-.24, 0, .24)[i] if z==2 else 0), scale*f + ((.10,.23,.10)[i] if z==2 else .025)), ha='center', fontsize=9, arrowprops={'arrowstyle':'-', 'color':c, 'lw':.7} if z==2 else None)
a.set(xlim=(10.5,27.6),ylim=(0,.96),xlabel='x = (m / u) / z     [dimensionless]',ylabel='Chosen line area')
a.set_title('A. The same three isotopes occupy two charge-state groups', loc='left',weight='bold')
a.text(15.9,.58,'Doubly charged yield chosen as 0.28 × singly charged yield.\nPositions approximate ion masses by neutral masses.',fontsize=9)
xs=np.linspace(22.3,27.7,5000)
for j,width in enumerate((.12,1.20)):
    ax=fig.add_subplot(gs[1,j])
    sigma=width/(2*np.sqrt(2*np.log(2)))
    components=[f*np.exp(-.5*((xs-m)/sigma)**2)/(sigma*np.sqrt(2*np.pi)) for m,f in zip(masses,fractions)]
    for y,c in zip(components,colors):ax.plot(xs,y,color=c,lw=1.4)
    ax.plot(xs,np.sum(components,axis=0),color='#292929',lw=1.3,ls='--',label='Sum')
    ax.set(xlim=(22.6,27.4),xlabel='x',ylabel='Signal per unit x')
    ax.set_title(f'{"B" if j==0 else "C"}. Chosen FWHM = {width:.2f}; R₂₅ ≈ {masses[1]/width:.1f}',loc='left',weight='bold',fontsize=11)
    ax.legend(frameon=False,fontsize=9)
ax=fig.add_subplot(gs[2,:])
p=np.arange(3)
biased=fractions*np.array([1,2,1]);biased/=biased.sum()
ax.bar(p-.18,fractions,width=.34,label='Neutral isotope fractions',color='#16749b')
ax.bar(p+.18,biased,width=.34,label='Normalized areas: mass-25 response × 2',color='#c87933')
for i,(f,b) in enumerate(zip(fractions,biased)):
    ax.text(i-.18,f+.015,f'{f:.4f}',ha='center',fontsize=9)
    ax.text(i+.18,b+.015,f'{b:.4f}',ha='center',fontsize=9)
ax.set(xticks=p,xticklabels=['Mass number 24','Mass number 25','Mass number 26'],ylim=(0,1.04),ylabel='Fraction')
ax.set_title('D. A response change alters inferred fractions without altering the sample',loc='left',weight='bold')
ax.legend(frameon=False,loc='upper right',fontsize=9)
for ax in fig.axes:
    ax.spines[['top','right']].set_visible(False)
fig.subplots_adjust(left=.075,right=.98,top=.94,bottom=.09)
fig.text(.075,.025,'Original synthetic illustration: no measured counts, historical masses, or historical resolution are inferred.',fontsize=9)
fig.savefig(Path.cwd() / (Path(__file__).stem + '.png'),dpi=120,facecolor='white')
plt.close(fig)