codex/electron-wavelength-resolution-budget.py

electron-wavelength-resolution-budget.py
"""Original wavelength / heuristic blur model, not instrument measurements.

Python 3.14; matplotlib 3.10.7; numpy 2.3.5. Run from _media.
All blur terms are defined in optics.bigb; no measured parameters are fitted.
"""
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np

H = 6.62607015e-34
E = 1.602176634e-19
M = 9.1093837e-31
C = 299792458.


def wavelength(volts):
    energy = E*np.asarray(volts)
    return H/np.sqrt(2*M*energy*(1+energy/(2*M*C*C)))


def blur_terms(alpha):
    lam = wavelength(100000.)
    diffraction = .61*lam/alpha
    spherical = 1e-3*alpha**3
    chromatic = 1e-3*alpha*(.7/100000.)
    residual = np.full_like(alpha, 1e-10)
    total = np.sqrt(diffraction**2+spherical**2+chromatic**2+residual**2)
    return diffraction, spherical, chromatic, residual, total


def main():
    plt.rcParams.update({'font.size': 11, 'font.family': 'DejaVu Sans'})
    fig, axes = plt.subplots(1, 2, figsize=(11.2, 6.4), dpi=110, facecolor='white')
    fig.subplots_adjust(left=.075, right=.97, bottom=.30, top=.79, wspace=.29)
    ax = axes[0]
    voltage = np.geomspace(20, 300, 400)
    ax.plot(voltage, wavelength(voltage*1000)*1e12, color='#176c9e', linewidth=2.2)
    for kv, offset in [(30, (9, 4)), (100, (-8, 13)), (300, (-65, 13))]:
        pm = float(wavelength(kv*1000)*1e12)
        ax.plot(kv, pm, 'o', color='#176c9e', markersize=5)
        ax.annotate(f'{kv} kV: {pm:.3f} pm', (kv, pm), xytext=offset,
                    textcoords='offset points', fontsize=10)
    ax.set(xlabel='Accelerating voltage U (kV)', ylabel='Electron wavelength λ (pm)',
           xlim=(20, 325), ylim=(1.3, 9.5), title='Relativistic de Broglie relation')
    ax.grid(alpha=.2)
    ax = axes[1]
    alpha = np.geomspace(.0005, .03, 700)
    terms = blur_terms(alpha)
    for values, label, color, style in zip(
        terms,
        ['Ideal diffraction', 'Assumed spherical blur', 'Assumed chromatic blur',
         'Assumed residual blur', 'Quadrature model total'],
        ['#176c9e', '#c0582c', '#307d46', '#888888', '#202a32'],
        ['-', '-', '-', ':', '-']):
        ax.loglog(alpha*1000, values*1e9, label=label, color=color,
                  linestyle=style, linewidth=2.4 if label.endswith('total') else 1.7)
    a = .61*float(wavelength(100000.))
    s, t = 1e-3, 1e-3*.7/100000.
    # Exact minimizer of a²/α² + s²α⁶ + t²α² + b0², in the declared model.
    alpha_opt = (2*a*a/(np.sqrt(t**4+12*s*s*a*a)+t*t))**.25
    b_opt = float(blur_terms(np.array([alpha_opt]))[-1][0])
    ax.plot(alpha_opt*1000, b_opt*1e9, 'o', color='#202a32', markersize=5)
    ax.annotate(f'Model minimum\n{alpha_opt*1000:.2f} mrad, {b_opt*1e9:.3f} nm',
                (alpha_opt*1000, b_opt*1e9), xytext=(1.1, 4), textcoords='data',
                fontsize=10, bbox={'facecolor':'white', 'edgecolor':'none', 'pad':2},
                arrowprops={'arrowstyle':'->', 'color':'#202a32'})
    ax.set(xlabel='Aperture semi-angle α (mrad)', ylabel='Characteristic image scale (nm)',
           xlim=(.5, 30), ylim=(.01, 40), title='100 kV: aperture and assumed blur')
    ax.grid(which='both', alpha=.15)
    ax.legend(loc='upper left', bbox_to_anchor=(0, -.25), fontsize=9, ncol=2,
              frameon=False, columnspacing=1, handlelength=2)
    fig.suptitle('A short wavelength permits detail; lens blur can still hide it', y=.96, fontsize=16)
    fig.text(.5, .865, r'Chosen model: $C_s=C_c=1$ mm, $\Delta K=0.7$ eV, $b_0=0.10$ nm',
             ha='center', fontsize=11, color='#4c5964')
    fig.text(.25, .10, 'Calculated points; negligible starting energy.\nNo measurements or performance specifications.',
             ha='center', fontsize=10, color='#4c5964')
    fig.savefig(Path(__file__).stem+'.png', facecolor='white', transparent=False)
    plt.close(fig)


if __name__ == '__main__':
    main()