Source code for lightweaver.zeeman

from dataclasses import dataclass
from fractions import Fraction
from typing import TYPE_CHECKING, Iterator, Optional, cast

import numpy as np

if TYPE_CHECKING:
    from .atomic_model import AtomicLine

[docs] def fraction_range(start: Fraction, stop: Fraction, step: Fraction=Fraction(1,1)) -> Iterator[Fraction]: ''' Works like range, but with Fractions. Does no checking, so best to make sure the range you're asking for is sane and divides down properly. ''' while start < stop: yield start start += step
[docs] @dataclass class ZeemanComponents: ''' Storage for communicating the Zeeman components between functions, also shared with the backend, giving a slightly tighter contract than usual: all arrays must be contiguous and alpha must be of dtype np.int32. ''' alpha: np.ndarray strength: np.ndarray shift: np.ndarray
[docs] def zeeman_strength(Ju: Fraction, Mu: Fraction, Jl: Fraction, Ml: Fraction) -> float: ''' Computes the strength of a Zeeman component, following del Toro Iniesta (p. 137) albeit larger by a factor of 2 which is corrected by normalisation. Takes J upper and lower (u and l respectively), and M upper and lower. ''' alpha = int(Ml - Mu) dJ = int(Ju - Jl) # These parameters are x2 those in del Toro Iniesta (p. 137), but we # normalise after the fact, so it's fine if dJ == 0: # jMin = ju = jl if alpha == 0: # pi trainsitions s = 2.0 * Mu**2 elif alpha == -1: # sigma_b transitions s = (Ju + Mu) * (Ju - Mu + 1.0) elif alpha == 1: # sigma_r transitions s = (Ju - Mu) * (Ju + Mu + 1.0) elif dJ == 1: # jMin = jl, Mi = Ml if alpha == 0: # pi trainsitions s = 2.0 * ((Jl + 1)**2 - Ml**2) elif alpha == -1: # sigma_b transitions s = (Jl + Ml + 1) * (Jl + Ml + 2.0) elif alpha == 1: # sigma_r transitions s = (Jl - Ml + 1.0) * (Jl - Ml + 2.0) elif dJ == -1: # jMin = ju, Mi = Mu if alpha == 0: # pi trainsitions s = 2.0 * ((Ju + 1)**2 - Mu**2) elif alpha == -1: # sigma_b transitions s = (Ju - Mu + 1) * (Ju - Mu + 2.0) elif alpha == 1: # sigma_r transitions s = (Ju + Mu + 1.0) * (Ju + Mu + 2.0) else: raise ValueError('Invalid dJ: %d' % dJ) return float(s)
[docs] def lande_factor(J: Fraction, L: int, S: Fraction) -> float: ''' Computes the Lande g-factor for an atomic level from the J, L, and S quantum numbers. ''' if J == 0.0: return 0.0 return float(1.5 + (S * (S + 1.0) - L * (L + 1)) / (2.0 * J * (J + 1.0)))
[docs] def effective_lande(line: 'AtomicLine'): ''' Computes the effective Lande g-factor for an atomic line. ''' if line.gLandeEff is not None: return line.gLandeEff i = line.iLevel j = line.jLevel if any(x is None for x in [i.J, i.L, i.S, j.J, j.L, j.S]): raise ValueError(('Cannot compute gLandeEff as gLandeEff not set and some of ' 'J, L and S None for line %s') % repr(line)) gL = lande_factor(i.J, i.L, i.S) # type: ignore gU = lande_factor(j.J, j.L, j.S) # type: ignore return 0.5 * (gU + gL) + \ 0.25 * (gU - gL) * (j.J * (j.J + 1.0) - i.J * (i.J + 1.0)) # type: ignore
[docs] def compute_zeeman_components(line: 'AtomicLine') -> Optional[ZeemanComponents]: ''' Computes, if possible, the set of Zeeman components for an atomic line. If gLandeEff is specified on the line, then basic three-component Zeeman splitting will be computed directly. Otherwise, if both the lower and upper levels of the line support LS-coupling (i.e. J, L, and S all specified, and J <= L + S), then the LS-coupling formalism is applied to compute the components of "anomalous" Zeeman splitting. If neither of these cases are fulfilled, then None is returned. Parameters ---------- line : AtomicLine The line to attempt to compute the Zeeman components from. Returns ------- components : ZeemanComponents or None The Zeeman splitting components, if possible. ''' # NOTE(cmo): Just do basic three-component Zeeman splitting if an effective # Lande g-factor is specified on the line. if line.gLandeEff is not None: alpha = np.array([-1, 0, 1], dtype=np.int32) strength = np.ones(3) shift = alpha * line.gLandeEff return ZeemanComponents(alpha, strength, shift) # NOTE(cmo): Do LS coupling ("anomalous" Zeeman splitting) if line.iLevel.lsCoupling and line.jLevel.lsCoupling: # Mypy... you're a pain sometimes... (even if you are technically correct) Jl = cast(Fraction, line.iLevel.J) Ll = cast(int, line.iLevel.L) Sl = cast(Fraction, line.iLevel.S) Ju = cast(Fraction, line.jLevel.J) Lu = cast(int, line.jLevel.L) Su = cast(Fraction, line.jLevel.S) gLl = lande_factor(Jl, Ll, Sl) gLu = lande_factor(Ju, Lu, Su) alpha = [] strength = [] shift = [] norm = np.zeros(3) for ml in fraction_range(-Jl, Jl+1): for mu in fraction_range(-Ju, Ju+1): if abs(ml - mu) <= 1.0: alpha.append(int(ml - mu)) shift.append(gLl*ml - gLu*mu) strength.append(zeeman_strength(Ju, mu, Jl, ml)) norm[alpha[-1]+1] += strength[-1] alpha = np.array(alpha, dtype=np.int32) strength = np.array(strength) shift = np.array(shift) strength /= norm[alpha + 1] return ZeemanComponents(alpha, strength, shift) return None