codexsupernova-standardizable-candle-readout.py
supernova-standardizable-candle-readout.py
"""Generate a functional, schematic supernova distance readout (Python 3.14, NumPy 2.3, Matplotlib 3.10)."""
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
def main():
fig = plt.figure(figsize=(12, 7.8), facecolor='white', dpi=100)
ax = fig.add_axes([0, 0, 1, 1]); ax.set(xlim=(0, 12), ylim=(0, 7.8)); ax.axis('off')
ink, blue, orange = '#233548', '#17658a', '#ad5318'
def box(x, y, w, h, title, body, color=blue):
ax.add_patch(FancyBboxPatch((x,y),w,h,boxstyle='round,pad=0.08',fc='#f4f8fa',ec=color,lw=1.5))
ax.text(x+w/2,y+h-.25,title,ha='center',va='top',fontsize=12.5,weight='bold',color=color)
ax.text(x+w/2,y+h/2-.17,body,ha='center',va='center',fontsize=11,color=ink,linespacing=1.5)
def arrow(a,b):
ax.add_patch(FancyArrowPatch(a,b,arrowstyle='-|>',mutation_scale=15,lw=1.6,color=ink))
ax.text(.45,7.42,'How a standardizable candle measures relative distance',fontsize=20,weight='bold',color=ink)
ax.text(.45,7.05,'Functional schematic • arbitrary light curves below • no survey data',fontsize=12,color='#596875')
box(.45,5.3,3.2,1.35,'1 Find and separate','Repeat images − reference image\nRemove host light; recover source flux')
box(4.4,5.3,3.2,1.35,'2 Measure a spectrum','Classify the transient\n'+r'$z=\lambda_{\rm obs}/\lambda_{\rm rest}-1$')
box(8.35,5.3,3.2,1.35,'3 Follow brightness','Calibrate several filters over time\nFit peak, width and colour')
arrow((3.75,5.97),(4.27,5.97)); arrow((7.7,5.97),(8.22,5.97))
plot = fig.add_axes([.065,.365,.30,.245],facecolor='white')
time=np.linspace(-20,60,350)
for stretch,amp,col in [(.8,.78,orange),(1.15,1.15,blue)]:
u=time/stretch
template=np.where(u<0,np.exp(-.5*(u/8)**2),np.exp(-u/24))
plot.plot(time,amp*template,color=col,lw=2,label=f'width {stretch:.2f} (toy)')
plot.set(xlabel='Rest-frame time (days)',ylabel='Flux (arbitrary units)',xlim=(-20,60),ylim=(0,1.32))
plot.spines[['top','right']].set_visible(False);plot.tick_params(labelsize=9);plot.legend(fontsize=9,frameon=False,loc='upper right')
box(5.05,2.88,6.5,1.83,'4 Standardize in a common emitted band',r'$u=(t_{\rm obs}-t_{\max})/[s(1+z)]$'+'\n'+r'$m_{\rm corr}=m_{\rm obs}-K-A+\alpha(s-1)$'+'\nCorrect band shift, attenuation and intrinsic width',orange)
arrow((4.1,3.82),(4.9,3.82)); arrow((9.95,5.2),(9.95,4.83))
box(.45,.68,5.15,1.42,'5 Read a relative distance',r'$m_{\rm corr}=M_0+\mu$'+'\n'+r'$d_{L,2}/d_{L,1}=10^{(m_{{\rm corr},2}-m_{{\rm corr},1})/5}$')
box(6.4,.68,5.15,1.42,'6 Test an expansion history','Compare the shape with FLRW predictions\nAssume stable calibration, populations and transparency',orange)
arrow((5.7,1.38),(6.25,1.38)); arrow((6.1,2.78),(3.05,2.22))
ax.text(6,.22,r'Unknown $M_0$ and $H_0$ share an intercept; brightness alone does not determine acceleration.',ha='center',fontsize=11,color=ink)
fig.savefig(Path(__file__).stem+'.png',facecolor='white',dpi=100)
plt.close(fig)
if __name__ == '__main__':
main()