StarkZee vs. built-in reference models

Script: examples/model_comparison.py

Compares StarkZee’s static and FFM (dynamic-ion) solvers against all five built-in reference models (Voigt, Stehlé, Stehlé-parameterized, Rosato; here Lomanowski is disabled since it has no magnetic-field treatment) for D-α (n=3→2) at \(N_e = 10^{20}\) m-3, \(T_i = T_e = 1\) eV, \(B = 3\) T, \(\theta = 90^\circ\). Uses use_empirical_data=True so both StarkZee solvers land on the measured NIST line center (see Empirical Field-Free Energies) for a fair comparison against the reference models, which are anchored to their own tabulated NIST air wavelength. Can save the figure to a file when given a path on the command line, which is how the figure at the end of the Built-in Reference Models page was produced (the same figure is shown below).

Code

  1"""
  2model_comparison.py — Compare StarkZee's static and FFM profiles against
  3analytical and tabulated models.
  4
  5Edit the parameters block inside run() to change plasma conditions.
  6All models receive the same (Ti, Te, Ne, B, angle), so differences are
  7purely due to the underlying physics model.
  8
  9Run directly::
 10
 11    python examples/model_comparison.py                 # show interactively
 12    python examples/model_comparison.py out/figure.png  # save to file
 13"""
 14
 15import time
 16import traceback
 17
 18import numpy as np
 19import matplotlib.pyplot as plt
 20
 21from starkzee.line_profile import LineProfile
 22from starkzee.convolutions import calculate_doppler_width_ev
 23import starkzee.models as models
 24
 25
 26def run(save_path=None):
 27    # ── parameters ────────────────────────────────────────────────────────────
 28    n_u, n_l       = 3, 2      # transition  (Hα)
 29    species        = 'D'       # emitting species: 'H', 'D', or 'T'
 30    Ne_m3          = 1e20      # electron density          [m⁻³]
 31    Te_ev          = 1       # electron temperature      [eV]  → Stark width
 32    Ti_ev          = 1       # ion temperature           [eV]  → Doppler width
 33    B              = 3.0       # magnetic field            [T]
 34    view_angle_deg = 90.0      # observation angle to B    [deg]
 35    # ──────────────────────────────────────────────────────────────────────────
 36
 37    # Line center and adaptive grid width
 38    lp = LineProfile(n_u=n_u, n_l=n_l, B=B, Ne_m3=Ne_m3, Te_ev=Te_ev, Ti_ev=Ti_ev,
 39                     species=species, view_angle_deg=view_angle_deg)
 40
 41    delta_E_D        = calculate_doppler_width_ev(lp.E0, Ti_ev, A_emitter=1)
 42    delta_lambda_D_nm = lp.E0_wavelength_nm * delta_E_D / lp.E0
 43    half_width_nm    = max(2.1, 4.0 * delta_lambda_D_nm)
 44
 45    # StarkZee computes in vacuum nm, on a grid centered on the gross-structure
 46    # Rydberg line center.  Compute it up front so the comparison models can be
 47    # referenced to its actual line center (next).
 48    wl_sz_nm = np.linspace(lp.E0_wavelength_nm - half_width_nm,
 49                            lp.E0_wavelength_nm + half_width_nm, 3000)
 50    print('--------\ntimings:\n--------')
 51    t0 = time.time()
 52    lp.compute_static_profile(wl_sz_nm, grid_type='wavelength_nm',  num_f=60, num_mu=11, use_empirical_data=True, atom=species)
 53    print(f'starkzee (static): {time.time() - t0:.3g} sec')
 54
 55    # StarkZee FFM (dynamic ion) profile, on the same vacuum-nm grid and
 56    # plasma parameters; Doppler broadening is applied internally by the FFM
 57    # solver (apply_doppler=True default), so both curves are directly
 58    # comparable.
 59    lp_ffm = LineProfile(n_u=n_u, n_l=n_l, B=B, Ne_m3=Ne_m3, Te_ev=Te_ev, Ti_ev=Ti_ev,
 60                         species=species, view_angle_deg=view_angle_deg)
 61    t0 = time.time()
 62    lp_ffm.compute_ffm_profile(wl_sz_nm, grid_type='wavelength_nm', sdt_bin_tol=1e-5, use_empirical_data=True, atom=species)
 63    print(f'starkzee (ffm): {time.time() - t0:.3g} sec')
 64    ffm_profile = lp_ffm.profile
 65
 66    # Physical line center = intensity-weighted centroid of the StarkZee profile.
 67    # With use_empirical_data the levels carry the Lamb shift, so the centroid
 68    # falls on the physical (NIST) wavelength.  The comparison models have
 69    # no fine structure and are symmetric about their grid mean, so centering their
 70    # grid and the vertical guide line on this centroid makes every peak overlay;
 71    # otherwise StarkZee appears offset from the others and from the vertical line.
 72    #center_air_nm = lp.E0_wavelength_air_nm
 73    center_air_nm = float(np.sum(lp.wavelengths_air_nm * lp.profile) / np.sum(lp.profile))
 74
 75    # Comparison models compute in air nm, centered on the same physical line center.
 76    wl_cmp_nm = np.linspace(center_air_nm - half_width_nm,
 77                             center_air_nm + half_width_nm, 5000)
 78
 79    # ── figure ────────────────────────────────────────────────────────────────
 80    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=[12, 5])
 81    fig.suptitle(
 82        f'$n_e = {Ne_m3:.2g}$ m$^{{-3}}$,  '
 83        f'$T_i = {Ti_ev:.3g}$ eV,  $T_e = {Te_ev:.3g}$ eV\n'
 84        f'$B = {B:.3g}$ T,  $\\theta = {view_angle_deg:.3g}$°'
 85    )
 86
 87    # ── comparison models ─────────────────────────────────────────────
 88    cmp_funcs = {
 89        'voigt':        models.voigt,
 90        'stehle':       models.stehle,
 91        'stehle_param': models.stehle_param,
 92        #'lomanowski':   models.lomanowski,
 93        'rosato':   models.rosato,
 94    }
 95
 96    for name, func in cmp_funcs.items():
 97        try:
 98            t0 = time.time()
 99            profile = func(
100                wl_cmp_nm, n_u, n_l, B, Ne_m3, Te_ev, Ti_ev,
101                view_angle_deg=view_angle_deg, species=species,
102            )
103            print(f'{name}: {time.time() - t0:.3g} sec')
104            ax1.plot(wl_cmp_nm, profile / profile.max(), label=name)
105            ax2.plot(wl_cmp_nm, profile / profile.max(), label=name)
106        except Exception as exc:
107            print(f'{name} failed: {exc}')
108            traceback.print_exc()
109
110    # ── StarkZee static and FFM profiles (computed above) ─────────────────────
111    y = lp.profile / lp.profile.max()
112    y_ffm = ffm_profile / ffm_profile.max()
113    for ax in (ax1, ax2):
114        ax.plot(lp.wavelengths_air_nm, y, 'k--', linewidth=2, label='starkzee (static)')
115        ax.plot(lp_ffm.wavelengths_air_nm, y_ffm, 'k:', linewidth=2, label='starkzee (ffm)')
116
117    # ── formatting ────────────────────────────────────────────────────────────
118    for ax in (ax1, ax2):
119        ax.set_xlim(wl_cmp_nm.min(), wl_cmp_nm.max())
120        ax.axvline(center_air_nm, ls='--', color='dimgrey', zorder=0)
121        ax.legend(fontsize=10)
122        ax.set_xlabel('wavelength (nm)', fontsize=10)
123
124    ax1.set_xlim(center_air_nm - 0.2, center_air_nm + 0.2)
125    ax2.set_xlim(center_air_nm - 2, center_air_nm + 2)
126    ax2.semilogy()
127    plt.tight_layout()
128
129    if save_path:
130        fig.savefig(save_path, dpi=200)
131        print(f'saved figure to {save_path}')
132    else:
133        plt.show()
134
135
136if __name__ == '__main__':
137    import sys
138    run(save_path=sys.argv[1] if len(sys.argv) > 1 else None)

Result

StarkZee static and FFM compared against Voigt, Stehle, Stehle-parameterized, and Rosato reference models for D-alpha

StarkZee (static and FFM) against the field-treating (Rosato) and field-free (Voigt, Stehlé, Stehlé-parameterized) reference models. Linear scale (left) and log scale (right).