codexlamb-shift-chosen-resonance-line-fit.py
lamb-shift-chosen-resonance-line-fit.py
"""Original synthetic resonance fit, NOT historical data.
Python 3.14; numpy 2.3.5; matplotlib 3.10.7. Run from _media.
Analytic-Jacobian least squares uses only numpy; known independent noise SD.
"""
from pathlib import Path
import json
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
A_SLOPE = 20.0 # MHz/mT, stipulated exactly for this local linear model
DRIVE = 1300.0 # MHz
SIGMA = .004 # chosen detector signal units, independent noise SD
B = np.linspace(2, 18, 65) # mT
def signal_and_jacobian(p, field):
c, amp, centre, width = p
z = 2*(field-centre)/width
den = 1+z*z
y = c-amp/den
jac = np.column_stack((np.ones_like(field), -1/den,
-4*amp*z/(width*den**2),
-2*amp*z*z/(width*den**2)))
return y, jac
def calculate():
true = np.array([1., .25, 10., 5.])
clean,_ = signal_and_jacobian(true,B)
observed = clean + np.random.default_rng(1947).normal(0,SIGMA,B.size)
p=np.array([.99,.23,9.8,4.8])
for _ in range(30):
pred,jac=signal_and_jacobian(p,B)
step=np.linalg.lstsq(jac,observed-pred,rcond=None)[0]
p+=step
if np.max(abs(step))<1e-12:break
pred,jac=signal_and_jacobian(p,B)
cov=SIGMA**2*np.linalg.inv(jac.T@jac)
s_centre=np.sqrt(cov[2,2])
intercept=DRIVE-A_SLOPE*p[2]
# Calibration error is common to the whole scan; not independent noise per point.
fit_sd=A_SLOPE*s_centre
offset_sd=A_SLOPE*.020
gain_sd=A_SLOPE*p[2]*.001
combined=np.sqrt(fit_sd**2+offset_sd**2+gain_sd**2+.100**2)
out={'model':'synthetic, not historical measurements','seed':1947,'signal_units':'chosen arbitrary detector units','sample_count':int(B.size),'drive_MHz':DRIVE,'slope_MHz_per_mT':A_SLOPE,'noise_SD':SIGMA,'true_parameters':true.tolist(),'fit_parameters':p.tolist(),'centre_fit_SD_mT':float(s_centre),'zero_field_intercept_MHz':float(intercept),'fit_SD_MHz':float(fit_sd),'offset_SD_mT':.020,'gain_fraction_SD':.001,'drive_SD_MHz':.100,'combined_SD_MHz':float(combined),'chi_square':float(np.sum(((observed-pred)/SIGMA)**2)),'degrees_of_freedom':int(B.size-4)}
return p,observed,out
def main():
p,observed,out=calculate()
fig,(ax,ay)=plt.subplots(1,2,figsize=(10.5,4.7),facecolor='white',layout='constrained')
fig.suptitle('Chosen resonance model: line centre and magnetic calibration',fontsize=15)
fine=np.linspace(2,18,700)
ax.scatter(B,observed,s=12,color='#40515d',label='Synthetic samples (seed 1947)')
ax.plot(fine,signal_and_jacobian(p,fine)[0],color='#245a81',lw=2,label='Four-parameter fit')
ax.axvline(p[2],ls=':',color='#ac4c12')
ax.set(xlabel='Magnetic field B (mT)',ylabel='Detector current (chosen signal units)',ylim=(.72,1.025))
ax.text(.04,.06,f'Chosen FWHM: 100 MHz = 5 mT\nFit centre: {p[2]:.3f} ± {out["centre_fit_SD_mT"]:.3f} mT',transform=ax.transAxes,fontsize=10)
ax.legend(loc='upper left',fontsize=9)
f=np.linspace(0,13,200)
ay.plot(f,out['zero_field_intercept_MHz']+A_SLOPE*f,color='#245a81',lw=2)
ay.axhline(DRIVE,color='#ac4c12',ls='--',label='Chosen drive: 1300 MHz')
ay.scatter([p[2]],[DRIVE],color='#ac4c12',s=35,zorder=4)
ay.scatter([0],[out['zero_field_intercept_MHz']],color='#245a81',s=35,zorder=4)
ay.set(xlabel='Magnetic field B (mT)',ylabel='Transition frequency (MHz)',xlim=(-.6,13),ylim=(1075,1375))
ay.text(.04,.65,r'$\nu_{\mathrm{res}}(B)=\nu_0+aB$'+'\n'+r'$a=20$ MHz/mT (chosen)',transform=ay.transAxes,fontsize=11)
ay.text(.04,.09,f'Intercept: {out["zero_field_intercept_MHz"]:.2f} MHz\nFit SD: {out["fit_SD_MHz"]:.2f} MHz\nWith calibration: {out["combined_SD_MHz"]:.2f} MHz',transform=ay.transAxes,fontsize=10,bbox=dict(facecolor='white',edgecolor='none',alpha=.93,pad=3))
ay.legend(loc='upper left',fontsize=9)
for axis in (ax,ay):
axis.spines[['top','right']].set_visible(False);axis.grid(alpha=.18)
fig.supxlabel('Original toy calculation; linear branch neglects hyperfine mixing. No historical trace or measured Lamb value.',fontsize=10)
fig.savefig(Path.cwd()/(Path(__file__).stem+'.png'),dpi=140,facecolor='white')
plt.close(fig)
print(json.dumps(out,indent=2))
if __name__=='__main__':main()