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) against the field-treating (Rosato) and field-free (Voigt, Stehlé, Stehlé-parameterized) reference models. Linear scale (left) and log scale (right).