Satellite features at B = 1000 T
Script: examples/diag_halpha_satellites.py
Searches for the \(\pm 2\mu_B B\) Stark-Zeeman satellite feature in
Hα (n=3→2) at B = 1000 T, at three quadrature resolutions
(num_f/num_mu = 20/6, 40/10, 60/12) to check convergence, both with
and without the quadratic Zeeman term.
Note
This is an exploratory/diagnostic script, not a validated result: at the plasma conditions used here (\(N_e = 10^{17}\) m-3, \(T_e = 5\) eV) no distinctly resolved satellite peak is found at \(\pm 2\mu_B B\) in the full-resolution profile — see the physical mechanism described in the script’s docstring for the effect it was searching for. A companion script that claimed a specific ~2% satellite for Hβ (n=4→2) was found not to reproduce against the current solver and is intentionally not included here.
Code
1#!/usr/bin/env python3
2"""
3diag_halpha_satellites.py
4=========================
5Look for the ±2μ_B×B Stark-Zeeman satellite features in Hα (n=3→2) at B=1000 T.
6
7Physical mechanism
8------------------
9Transverse Stark field Fx mixes states with Δl=±1, Δml=±1 within the same n shell.
10The eigenstate near E0_n3 + 2μ_B×B (dominated by |3d,ml=2⟩) acquires an admixture
11β × |3p,ml=1⟩ due to Fx coupling. This β component can make a σ+ transition to the
12lower eigenstate near E0_n2 (dominated by |2s,ml=0⟩ or |2p,ml=0⟩) — producing a
13photon at energy E0 + 2μ_B×B instead of the regular E0 + μ_B×B.
14
15β ≈ Fx × ⟨3p|r|3d⟩ × A0 / (μ_B × B) → scales as Fx/B → satellite intensity ∝ ⟨F²⟩/B²
16
17For n=3: β_rms ≈ F0 × 10.06a0 × A0 / μ_B×B = 8e6 × 5.32e-10 / 57.9e-3 ≈ 0.074
18Satellite intensity ~ β² × OS_fraction ~ 0.5% (weak — that's why it's hard to see)
19
20For n=4: β_rms ≈ F0 × 15.9a0 × A0 / μ_B×B ≈ 0.116 → intensity ~ 1.3% (visible)
21
22This script runs with high resolution: num_f=60, num_mu=12 and a ±200 meV window,
23comparing B=100T, 500T, 1000T.
24"""
25
26import numpy as np
27import matplotlib.pyplot as plt
28import sys, os
29sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
30
31from starkzee.static_profile import calculate_static_profile
32from scipy.constants import e as E_CHARGE, m_e as M_E
33from starkzee.utils import reduced_mass_rydberg_ev, BOHR_MAGNETON_EV_T, A0
34
35Z, n_u, n_l = 1, 3, 2
36A = 1
37Ne, Te = 1e17, 5.0
38E0 = (Z**2) * reduced_mass_rydberg_ev(Z, A) * (1.0/n_l**2 - 1.0/n_u**2)
39
40muB_1000 = BOHR_MAGNETON_EV_T * 1000.0 # ≈ 57.9 meV
41
42print(f"Hα: E0 = {E0:.4f} eV")
43print(f"μ_B × 1000 T = {muB_1000*1e3:.2f} meV")
44print(f"Satellite expected at ±{2*muB_1000*1e3:.1f} meV from E0")
45print()
46
47# ── Compute profiles at B=1000 T with wide window ─────────────────────────────
48B = 1000.0
49muB_B = BOHR_MAGNETON_EV_T * B
50DET_HALF = 0.20 # ±200 meV window to see both σ± main peaks AND satellites
51NPTS = 8000 # ~0.05 meV resolution
52
53det = np.linspace(-DET_HALF, DET_HALF, NPTS)
54energies = E0 + det
55
56# Run with different resolution settings to check convergence
57configs = [
58 ("Low (num_f=20, num_mu=6)", 20, 6),
59 ("Mid (num_f=40, num_mu=10)", 40, 10),
60 ("High (num_f=60, num_mu=12)", 60, 12),
61]
62
63results = {}
64for label, num_f, num_mu in configs:
65 print(f"B={int(B)}T, {label} …", flush=True)
66 pi_nq, sp_nq, sm_nq = calculate_static_profile(
67 n_u, n_l, Z, B, Ne, Te, energies,
68 num_f=num_f, num_mu=num_mu,
69 use_screening=True, quadratic_zeeman=False,
70 frequency_dependent_width=False, Ti_ev=Te)
71 print(" without QZ done.", flush=True)
72 pi_yq, sp_yq, sm_yq = calculate_static_profile(
73 n_u, n_l, Z, B, Ne, Te, energies,
74 num_f=num_f, num_mu=num_mu,
75 use_screening=True, quadratic_zeeman=True,
76 frequency_dependent_width=False, Ti_ev=Te)
77 print(" with QZ done.", flush=True)
78 results[label] = dict(pi_nq=pi_nq, sp_nq=sp_nq, sm_nq=sm_nq,
79 pi_yq=pi_yq, sp_yq=sp_yq, sm_yq=sm_yq)
80
81 # Quick peak analysis for no-QZ total spectrum
82 total_nq = pi_nq + 0.5*(sp_nq + sm_nq)
83 peak = total_nq.max()
84 satellite_region = np.abs(np.abs(det*1e3) - 2*muB_B*1e3) < 15.0 # ±15 meV around ±2μ_BB
85 main_region = np.abs(det) < 0.030
86 sat_peak = total_nq[satellite_region].max() if satellite_region.any() else 0
87 main_peak = total_nq[main_region].max()
88 print(f" Satellite / main peak ratio (no-QZ, ±15meV around ±2μ_B×B): "
89 f"{sat_peak/main_peak*100:.3f}%")
90
91# ── Plot: full spectrum showing satellites ─────────────────────────────────────
92fig, axes = plt.subplots(3, 1, figsize=(14, 12), sharex=True)
93fig.suptitle(
94 r"Hα (n=3→2) full Stark-Zeeman profile, B=1000 T — searching for ±2μ$_B$B satellites"
95 "\n"
96 r"$N_e=10^{17}$ m$^{-3}$, $T_e=5$ eV, transverse obs. (I$_\pi$ + ½(I$_{\sigma+}$ + I$_{\sigma-}$))",
97 fontsize=11)
98
99det_meV = det * 1e3
100satellite_eV = 2 * muB_B # 115.8 meV at B=1000T
101main_eV = muB_B # 57.9 meV
102
103for ax, (label, num_f, num_mu), color in zip(
104 axes, configs, ['tab:blue', 'tab:orange', 'tab:red']):
105 r = results[label]
106 total_nq = r['pi_nq'] + 0.5*(r['sp_nq'] + r['sm_nq'])
107 total_yq = r['pi_yq'] + 0.5*(r['sp_yq'] + r['sm_yq'])
108
109 peak = max(total_yq.max(), total_nq.max(), 1e-30)
110
111 ax.semilogy(det_meV, total_nq/peak, 'k--', lw=1.2, alpha=0.75, label='No QZ')
112 ax.semilogy(det_meV, total_yq/peak, color=color, lw=1.8, label='With QZ')
113
114 # Mark expected satellite positions
115 for sign, tag in [(+1, 'σ+'), (-1, 'σ−')]:
116 ax.axvline(sign*satellite_eV*1e3, color='orange', ls=':', lw=1.2,
117 label=f'±2μ_B×B = ±{satellite_eV*1e3:.0f} meV ({tag} sat)' if sign==1 else '')
118 ax.axvline(sign*main_eV*1e3, color='gray', ls=':', lw=1.0,
119 label=f'±μ_B×B = ±{main_eV*1e3:.0f} meV (σ±)' if sign==1 else '')
120
121 ax.set_ylim(1e-5, 3.0)
122 ax.set_ylabel('Norm. intensity', fontsize=9)
123 ax.set_title(f'{label}', fontsize=10)
124 ax.legend(fontsize=8, loc='upper right')
125 ax.grid(True, which='both', alpha=0.3)
126
127axes[-1].set_xlabel('Detuning from $E_0$ (meV)', fontsize=11)
128axes[-1].set_xlim(-210, 210)
129
130plt.tight_layout()
131out = 'halpha_satellites_convergence.png'
132plt.savefig(out, dpi=200, bbox_inches='tight')
133print(f"\nSaved {out}")
134
135# ── Zoomed-in plot: just the satellite regions ─────────────────────────────────
136fig2, axes2 = plt.subplots(2, 3, figsize=(15, 9))
137fig2.suptitle(r"Zoomed satellite regions: Hα at B=1000 T", fontsize=12)
138
139windows = [
140 ('+2μ_B×B region (σ+ sat)', +satellite_eV*1e3, 30),
141 ('+μ_B×B region (σ+ main)', +main_eV*1e3, 30),
142 ('zero (π main)', 0.0, 30),
143]
144for col, (title, center_meV, hw_meV) in enumerate(windows):
145 mask = np.abs(det_meV - center_meV) < hw_meV
146 for row, (label, num_f, num_mu), color in zip(range(3), configs, ['tab:blue','tab:orange','tab:red']):
147 if col == 0:
148 ax = axes2[0 if row < 2 else 1, col]
149 # actually let me just use 1 row per resolution for comparison
150 pass
151
152 for ax, (label, num_f, num_mu), color in zip([axes2[0,col], axes2[1,col]],
153 configs[:2], ['tab:blue','tab:orange']):
154 r = results[label]
155 total_nq = r['pi_nq'] + 0.5*(r['sp_nq'] + r['sm_nq'])
156 total_yq = r['pi_yq'] + 0.5*(r['sp_yq'] + r['sm_yq'])
157 peak = total_nq.max()
158
159 ax.plot(det_meV[mask], total_nq[mask]/peak*100, 'k--', lw=1.2, alpha=0.8, label='No QZ')
160 ax.plot(det_meV[mask], total_yq[mask]/peak*100, color=color, lw=1.8, label='With QZ')
161 ax.set_title(f'{title}\n{label}', fontsize=8)
162 ax.set_xlabel('Detuning (meV)', fontsize=8)
163 ax.set_ylabel('% of main peak', fontsize=8)
164 ax.legend(fontsize=7)
165 ax.grid(True, alpha=0.3)
166
167plt.tight_layout()
168out2 = 'halpha_satellites_zoom.png'
169plt.savefig(out2, dpi=200, bbox_inches='tight')
170print(f"Saved {out2}")
171
172plt.show()
173print("\nDone.")
Results
Full ±200 meV window at three quadrature resolutions, with and without quadratic Zeeman, marking the naive \(\pm\mu_B B\) and \(\pm 2\mu_B B\) positions.
Zoomed views of the σ+ satellite region, the σ+ main-peak region, and the π line center, for the two lowest quadrature resolutions.