codex/supernova-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()