22
33import inspect
44import logging
5- import re
65from abc import ABC
7- from fractions import Fraction
86from functools import cache , cached_property
97from typing import TYPE_CHECKING , ClassVar , overload
108
11- import numpy as np
12-
13- from rydstate .angular .angular_ket import AngularKetLS
14- from rydstate .species .utils import calc_nu_from_energy , convert_electron_configuration
159from rydstate .units import rydberg_constant , ureg
1610
1711if TYPE_CHECKING :
18- from pathlib import Path
12+ from typing_extensions import Self
1913
20- from rydstate .angular .angular_ket import AngularKetBase
2114 from rydstate .radial .model import PotentialType
2215 from rydstate .units import PintFloat
2316
@@ -40,21 +33,6 @@ class SpeciesObject(ABC):
4033 """Nuclear spin, (default None to ignore hyperfine structure, will be treated like i_c = 0)."""
4134 number_valence_electrons : ClassVar [int ]
4235 """Number of valence electrons (i.e. 1 for alkali atoms and 2 for alkaline earth atoms)."""
43- ground_state_shell : ClassVar [tuple [int , int ]]
44- """Shell (n, l) describing the electronic ground state configuration."""
45- _additional_allowed_shells : ClassVar [list [tuple [int , int ]]] = []
46- """Additional allowed shells (n, l), which (n, l) is smaller than the ground state shell."""
47-
48- _core_electron_configuration : ClassVar [str ]
49- """Electron configuration of the core electrons, e.g. 4p6 for Rb or 5s for Sr."""
50- _ionization_energy : tuple [float , float | None , str ]
51- """Ionization energy with uncertainty and unit: (value, uncertainty, unit)."""
52-
53- # Parameters for the extended Rydberg Ritz formula, see calc_nu
54- _quantum_defects : ClassVar [dict [tuple [int , float , float ], tuple [float , float , float , float , float ]] | None ] = None
55- """Dictionary containing the quantum defects for each (l, j_tot, s_tot) combination, i.e.
56- _quantum_defects[(l,j_tot,s_tot)] = (d0, d2, d4, d6, d8)
57- """
5836
5937 _corrected_rydberg_constant : tuple [float , float | None , str ]
6038 r"""Corrected Rydberg constant stored as (value, uncertainty, unit)"""
@@ -84,96 +62,12 @@ class SpeciesObject(ABC):
8462 defined in: Y. Fei et al., Chin. Phys. B 18, 4349 (2009), https://iopscience.iop.org/article/10.1088/1674-1056/18/10/025
8563 """
8664
87- _nist_energy_levels_file : Path | None = None
88- """Path to the NIST energy levels file for this species.
89- The file should be directly downloaded from https://physics.nist.gov/PhysRefData/ASD/levels_form.html
90- in the 'Tab-delimited' format and in units of Hartree.
91- """
92-
93- def __init__ (self ) -> None :
94- """Initialize an species instance.
95-
96- Use this init method to set up additional properties and data for the species,
97- like loading NIST energy levels from a file.
98-
99- """
100- self ._nist_energy_levels : dict [tuple [int , int , float , float ], float ] = {}
101- if self ._nist_energy_levels_file is not None :
102- self ._setup_nist_energy_levels (self ._nist_energy_levels_file )
103-
104- def _setup_nist_energy_levels (self , file : Path ) -> None : # noqa: C901, PLR0912
105- """Set up NIST energy levels from a file.
106-
107- This method should be called in the constructor to load the NIST energy levels
108- from the specified file. It reads the file and prepares the data for further use.
109-
110- Args:
111- file: Path to the NIST energy levels file.
112- n_max: Maximum principal quantum number for which to load the NIST energy levels.
113- For large quantum numbers, the NIST data is not accurate enough
114- (it does not even show fine structure splitting),
115- so we limit the maximum principal quantum number to 15 by default.
116-
117- """
118- if not file .exists ():
119- raise ValueError (f"NIST energy data file { file } does not exist." )
120-
121- header = file .read_text ().splitlines ()[0 ]
122- if "Level (Hartree)" not in header :
123- raise ValueError (
124- f"NIST energy data file { file } not given in Hartree, please download the data in units of Hartree."
125- )
126-
127- data = np .loadtxt (file , skiprows = 1 , dtype = str , quotechar = '"' , delimiter = "\t " )
128- # data[i] := (Configuration, Term, J, Prefix, Energy, Suffix, Uncertainty, Reference)
129- core_config_parts = convert_electron_configuration (self ._core_electron_configuration )
130-
131- for row in data :
132- if re .match (r"^([A-Z])" , row [0 ]):
133- # Skip rows, where the first column starts with an element symbol
134- continue
135-
136- try :
137- config_parts = convert_electron_configuration (row [0 ])
138- except ValueError :
139- # Skip rows with invalid electron configuration format
140- # (they usually correspond to core configurations, that are not the ground state configuration)
141- # e.g. strontium "4d.(2D<3/2>).4f"
142- continue
143- if sum (part [2 ] for part in config_parts ) != sum (part [2 ] for part in core_config_parts ) + 1 :
144- # Skip configurations, where the number of electrons does not match the core configuration + 1
145- continue
146-
147- for part in core_config_parts :
148- if part in config_parts :
149- config_parts .remove (part )
150- elif (part [0 ], part [1 ], part [2 ] + 1 ) in config_parts :
151- config_parts .remove ((part [0 ], part [1 ], part [2 ] + 1 ))
152- config_parts .append ((part [0 ], part [1 ], 1 ))
153- else :
154- break
155- if sum (part [2 ] for part in config_parts ) != 1 :
156- # Skip configurations, where the inner electrons are not in the ground state configuration
157- continue
158- n , l = config_parts [0 ][:2 ]
159-
160- multiplicity = int (row [1 ][0 ])
161- s_tot = (multiplicity - 1 ) / 2
162-
163- j_tot_list = [float (Fraction (j_str )) for j_str in row [2 ].split ("," )]
164- for j_tot in j_tot_list :
165- energy = float (row [4 ])
166- self ._nist_energy_levels [(n , l , j_tot , s_tot )] = energy
167-
168- if len (self ._nist_energy_levels ) == 0 :
169- raise ValueError (f"No NIST energy levels found for species { self .name } in file { file } ." )
170-
17165 def __repr__ (self ) -> str :
17266 return f"{ self .__class__ .__name__ } ()"
17367
17468 @classmethod
17569 @cache
176- def from_name (cls , name : str ) -> SpeciesObject :
70+ def from_name (cls : type [ Self ] , name : str ) -> Self :
17771 """Create an instance of the species class from the species name.
17872
17973 This method searches through all subclasses of SpeciesObject until it finds one with a matching species name.
@@ -198,7 +92,7 @@ def from_name(cls, name: str) -> SpeciesObject:
19892 )
19993
20094 @classmethod
201- def _get_concrete_subclasses (cls ) -> list [type [SpeciesObject ]]:
95+ def _get_concrete_subclasses (cls : type [ Self ] ) -> list [type [Self ]]:
20296 subclasses = []
20397 for subclass in cls .__subclasses__ ():
20498 if not inspect .isabstract (subclass ) and hasattr (subclass , "name" ):
@@ -218,64 +112,6 @@ def get_available_species(cls) -> list[str]:
218112 """
219113 return sorted ([subclass .name for subclass in cls ._get_concrete_subclasses ()])
220114
221- def is_allowed_shell (self , n : int , l : int , s_tot : float | None = None ) -> bool :
222- """Check if the quantum numbers describe an allowed shell.
223-
224- I.e. whether the shell is above the ground state shell.
225-
226- Args:
227- n: Principal quantum number
228- l: Orbital angular momentum quantum number
229- s_tot: Total spin quantum number
230-
231- Returns:
232- True if the quantum numbers specify a shell equal to or above the ground state shell, False otherwise.
233-
234- """
235- if s_tot is None :
236- if self .number_valence_electrons > 1 :
237- raise ValueError ("s_tot must be specified for species with more than one valence electron." )
238- s_tot = self .number_valence_electrons / 2
239- if (self .number_valence_electrons / 2 ) % 1 != s_tot % 1 or s_tot > self .number_valence_electrons / 2 :
240- raise ValueError (f"Invalid spin { s_tot = } for { self .name } ." )
241-
242- if (n , l ) == self .ground_state_shell :
243- return s_tot != 1 # For alkaline earth atoms, the triplet state of the ground state shell is not allowed
244- if n < 1 or l < 0 or l >= n :
245- raise ValueError (f"Invalid shell: (n={ n } , l={ l } ). Must be n >= 1 and 0 <= l <= n-1." )
246- if (n , l ) >= self .ground_state_shell :
247- return True
248- return (n , l ) in self ._additional_allowed_shells
249-
250- @overload
251- def get_ionization_energy (self , unit : None = None ) -> PintFloat : ...
252-
253- @overload
254- def get_ionization_energy (self , unit : str ) -> float : ...
255-
256- def get_ionization_energy (self , unit : str | None = "hartree" ) -> PintFloat | float :
257- """Return the ionization energy in the desired unit.
258-
259- Args:
260- unit: Desired unit for the ionization energy. Default is atomic units "hartree".
261-
262- Returns:
263- Ionization energy in the desired unit.
264-
265- """
266- ionization_energy : PintFloat = ureg .Quantity (self ._ionization_energy [0 ], self ._ionization_energy [2 ])
267- ionization_energy = ionization_energy .to ("hartree" , "spectroscopy" )
268- if unit is None :
269- return ionization_energy
270- if unit == "a.u." :
271- return ionization_energy .magnitude
272- return ionization_energy .to (unit , "spectroscopy" ).magnitude
273-
274- @cached_property
275- def ionization_energy_au (self ) -> float :
276- """Ionization energy in atomic units (Hartree)."""
277- return self .get_ionization_energy ("hartree" )
278-
279115 @overload
280116 def get_corrected_rydberg_constant (self , unit : None = None ) -> PintFloat : ...
281117
@@ -327,64 +163,3 @@ def reduced_mass_au(self) -> float:
327163
328164 """
329165 return self .get_corrected_rydberg_constant ("hartree" ) / rydberg_constant .to ("hartree" ).m
330-
331- def calc_nu (
332- self ,
333- n : int ,
334- angular_ket : AngularKetBase ,
335- * ,
336- use_nist_data : bool = True ,
337- nist_n_max : int = 15 ,
338- ) -> float :
339- r"""Calculate the effective principal quantum number nu of a Rydberg state with the given n, l, j_tot and s_tot.
340-
341- I.e. either look up the energy for low lying states in the nist data (if use_nist_data is True),
342- and calculate nu from the energy via (see also `calc_nu_from_energy`):
343-
344- .. math::
345- \nu = \sqrt{\frac{1}{2} \frac{\mu/m_e}{-E/E_H}}
346-
347- Or calculate nu via the quantum defect theory,
348- where nu is defined as series expansion :math:`\nu = n^* = n - \delta_{lj}(n)`
349- with the quantum defect
350-
351- .. math::
352- \delta_{lj}(n) = d0_{lj} + d2_{lj} / [n - d0_{lj}(n)]^2 + d4_{lj} / [n - \delta_{lj}(n)]^4 + ...
353-
354- References:
355- - On a New Law of Series Spectra, Ritz; DOI: 10.1086/141591, https://ui.adsabs.harvard.edu/abs/1908ApJ....28..237R/abstract
356- - Rydberg atoms, Gallagher; DOI: 10.1088/0034-4885/51/2/001, (Eq. 16.19)
357-
358- Args:
359- n: The principal quantum number of the Rydberg state.
360- angular_ket: The angular ket specifying l, j_tot, and s_tot of the Rydberg state.
361- use_nist_data: Whether to use NIST energy data.
362- Default is True.
363- nist_n_max: Maximum principal quantum number for which to use the NIST energy data.
364- Default is 15.
365-
366- """
367- if not isinstance (angular_ket , AngularKetLS ):
368- raise NotImplementedError ("calc_nu is only implemented for AngularKetLS." )
369-
370- l , j_tot , s_tot = angular_ket .l_r , angular_ket .j_tot , angular_ket .s_r
371-
372- if n <= nist_n_max and use_nist_data : # try to use NIST data
373- if (n , l , j_tot , s_tot ) in self ._nist_energy_levels :
374- energy_au = self ._nist_energy_levels [(n , l , j_tot , s_tot )]
375- energy_au -= self .ionization_energy_au # use the cached ionization energy for better performance
376- return calc_nu_from_energy (self .reduced_mass_au , energy_au )
377- logger .debug (
378- "NIST energy levels for (n=%d, l=%d, j_tot=%s, s_tot=%s) not found, using quantum defect theory." ,
379- * (n , l , j_tot , s_tot ),
380- )
381-
382- if self ._quantum_defects is None :
383- raise ValueError (f"No quantum defect data available for species { self .name } ." )
384- quantum_defects = list (self ._quantum_defects .get ((l , j_tot , s_tot ), []))
385- if len (quantum_defects ) > 5 :
386- raise ValueError (f"Quantum defects for { self .name } must be a list with up to 5 elements." )
387-
388- d0 , d2 , d4 , d6 , d8 = quantum_defects + [0 ] * (5 - len (quantum_defects ))
389- delta_nlj = d0 + d2 / (n - d0 ) ** 2 + d4 / (n - d0 ) ** 4 + d6 / (n - d0 ) ** 6 + d8 / (n - d0 ) ** 8
390- return n - delta_nlj
0 commit comments