Skip to content

Commit 283cd2a

Browse files
split species object in base and sqdt
1 parent 942f2e3 commit 283cd2a

4 files changed

Lines changed: 297 additions & 236 deletions

File tree

src/rydstate/species/__init__.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
from rydstate.species.cesium import Cesium
2-
from rydstate.species.hydrogen import Hydrogen, HydrogenTextBook
3-
from rydstate.species.lithium import Lithium
4-
from rydstate.species.potassium import Potassium
5-
from rydstate.species.rubidium import Rubidium
6-
from rydstate.species.sodium import Sodium
71
from rydstate.species.species_object import SpeciesObject
8-
from rydstate.species.strontium import Strontium87, Strontium88
9-
from rydstate.species.ytterbium import Ytterbium171, Ytterbium173, Ytterbium174
2+
from rydstate.species.sqdt import (
3+
Cesium,
4+
Hydrogen,
5+
HydrogenTextBook,
6+
Lithium,
7+
Potassium,
8+
Rubidium,
9+
Sodium,
10+
SpeciesObjectSQDT,
11+
Strontium87,
12+
Strontium88,
13+
Ytterbium171,
14+
Ytterbium173,
15+
Ytterbium174,
16+
)
1017

1118
__all__ = [
1219
"Cesium",
@@ -17,6 +24,7 @@
1724
"Rubidium",
1825
"Sodium",
1926
"SpeciesObject",
27+
"SpeciesObjectSQDT",
2028
"Strontium87",
2129
"Strontium88",
2230
"Ytterbium171",

src/rydstate/species/species_object.py

Lines changed: 3 additions & 228 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,15 @@
22

33
import inspect
44
import logging
5-
import re
65
from abc import ABC
7-
from fractions import Fraction
86
from functools import cache, cached_property
97
from 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
159
from rydstate.units import rydberg_constant, ureg
1610

1711
if 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
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from rydstate.species.sqdt.cesium import Cesium
2+
from rydstate.species.sqdt.hydrogen import Hydrogen, HydrogenTextBook
3+
from rydstate.species.sqdt.lithium import Lithium
4+
from rydstate.species.sqdt.potassium import Potassium
5+
from rydstate.species.sqdt.rubidium import Rubidium
6+
from rydstate.species.sqdt.sodium import Sodium
7+
from rydstate.species.sqdt.species_object_sqdt import SpeciesObjectSQDT
8+
from rydstate.species.sqdt.strontium import Strontium87, Strontium88
9+
from rydstate.species.sqdt.ytterbium import Ytterbium171, Ytterbium173, Ytterbium174
10+
11+
__all__ = [
12+
"Cesium",
13+
"Hydrogen",
14+
"HydrogenTextBook",
15+
"Lithium",
16+
"Potassium",
17+
"Rubidium",
18+
"Sodium",
19+
"SpeciesObjectSQDT",
20+
"Strontium87",
21+
"Strontium88",
22+
"Ytterbium171",
23+
"Ytterbium173",
24+
"Ytterbium174",
25+
]

0 commit comments

Comments
 (0)