# -*- coding: utf-8 -*-
# ruff: noqa: E741
from __future__ import division, print_function, absolute_import
from arc.alkali_atom_functions import AlkaliAtom, printStateLetter
from arc.wigner import Wigner3j, Wigner6j
from scipy.constants import physical_constants
import csv
from typing import List, Tuple
import os
import numpy as np
from math import sqrt
from arc._database import sqlite3, UsedModulesARC
sqlite3.register_adapter(np.float64, float)
sqlite3.register_adapter(np.float32, float)
sqlite3.register_adapter(np.int64, int)
sqlite3.register_adapter(np.int32, int)
[docs]
class DivalentAtom(AlkaliAtom):
"""
Implements general calculations for Alkaline Earths, and other divalent
atoms.
This class inherits :obj:`arc.alkali_atom_functions.AlkaliAtom` .
Most of the methods can be directly used from there, and the source
for them is provided in the base class. Few methods that are
implemented differently for Alkaline Earths are defined here.
Args:
preferQuantumDefects (bool):
Use quantum defects for energy level calculations. If False,
uses NIST ASD values where available. If True, uses quantum
defects for energy calculations for principal quantum numbers
within the range specified in :obj:`defectFittingRange` which
is specified for each element and series separately.
For principal quantum numbers below this value, NIST ASD values
are used if existing, since quantum defects. Default is True.
cpp_numerov (bool):
This switch for Alkaline Earths at the moment doesn't have
any effect since wavefunction calculation function is not
implemented (d.m.e. and quadrupole matrix elements are
calculated directly semiclassically)
"""
modelPotential_coef: dict = dict()
"""
Model potential parameters fitted from experimental observations for
different l (electron angular momentum)
"""
quantumDefect = [
[
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
],
[
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
],
[
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
],
[
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
],
]
""" Contains list of modified Rydberg-Ritz coefficients for calculating
quantum defects for
[[ :math:`^1S_{0},^1P_{1},^1D_{2},^1F_{3}`],
[ :math:`^3S_{0},^3P_{0},^3D_{1},^3F_{2}`],
[ :math:`^3S_{0},^3P_{1},^3D_{2},^3F_{3}`],
[ :math:`^3S_{1},^3P_{2},^3D_{3},^3F_{4}`]]."""
#: file with .csv data, each row is
#: `[n, l, s, j, energy, source, absolute uncertanty]`
levelDataFromNIST = ""
#: levels that are for smaller principal quantum number (n) than ground
#: level, but are above in energy due to angular part
extraLevels: List[Tuple[int, int, int, int]] = [] # type: ignore
#: Not used with DivalentAtom, see :obj:`defectFittingRange` instead.
minQuantumDefectN: int = 0
#: Used for AlkalineEarths to define minimum and maximum principal quantum
#: number for which quantum defects are valid. Ranges are stored under
#: keys defined as state terms ({'stateLabel':[minN, maxN]}, e.g. '1S0').
#: Dictionary returns array
#: stating minimal and maximal principal quantum number for which quantum
#: defects were fitted. For example::
#: limits = self.defectFittingRange['1S0']
#: print("Minimal n = %d" % limits[0])
#: print("Maximal n = %d" % limits[1]) 1
defectFittingRange: dict = {}
#: flag that is turned to True if the energy levels of this atom were
#: calculated by extrapolating with quantum defects values outside the
#: quantum defect fitting range.
energyLevelsExtrapolated = False
def __init__(self, preferQuantumDefects=True, cpp_numerov=True):
UsedModulesARC.divalent_atoms = True
self.cpp_numerov = cpp_numerov
self.preferQuantumDefects = preferQuantumDefects
self._databaseInit()
c = self.conn.cursor()
if self.cpp_numerov:
from .arc_c_extensions import NumerovWavefunction
self.NumerovWavefunction = NumerovWavefunction
# load dipole matrix elements previously calculated
data = []
if self.dipoleMatrixElementFile != "":
if preferQuantumDefects is False:
self.dipoleMatrixElementFile = (
"NIST_" + self.dipoleMatrixElementFile
)
try:
data = np.load(
os.path.join(self.dataFolder, self.dipoleMatrixElementFile),
encoding="latin1",
allow_pickle=True,
)
except IOError as e:
print(
"Error reading dipoleMatrixElement File "
+ os.path.join(
self.dataFolder, self.dipoleMatrixElementFile
)
)
print(e)
# save to SQLite database
try:
c.execute(
"""SELECT COUNT(*) FROM sqlite_master
WHERE type='table' AND name='dipoleME';"""
)
if c.fetchone()[0] == 0:
# create table
c.execute(
"""CREATE TABLE IF NOT EXISTS dipoleME
(n1 TINYINT UNSIGNED, l1 TINYINT UNSIGNED,
j1 TINYINT UNSIGNED,
n2 TINYINT UNSIGNED, l2 TINYINT UNSIGNED,
j2 TINYINT UNSIGNED, s TINYINT UNSIGNED,
dme DOUBLE,
PRIMARY KEY (n1,l1,j1,n2,l2,j2,s)
) """
)
if len(data) > 0:
c.executemany(
"INSERT INTO dipoleME VALUES (?,?,?,?,?,?,?,?)", data
)
self.conn.commit()
except sqlite3.Error as e:
print("Error while loading precalculated values into the database")
print(e)
exit()
# load quadrupole matrix elements previously calculated
data = []
if self.quadrupoleMatrixElementFile != "":
if preferQuantumDefects is False:
self.quadrupoleMatrixElementFile = (
"NIST_" + self.quadrupoleMatrixElementFile
)
try:
data = np.load(
os.path.join(
self.dataFolder, self.quadrupoleMatrixElementFile
),
encoding="latin1",
allow_pickle=True,
)
except IOError as e:
print(
"Error reading quadrupoleMatrixElementFile File "
+ os.path.join(
self.dataFolder, self.quadrupoleMatrixElementFile
)
)
print(e)
# save to SQLite database
try:
c.execute(
"""SELECT COUNT(*) FROM sqlite_master
WHERE type='table' AND name='quadrupoleME';"""
)
if c.fetchone()[0] == 0:
# create table
c.execute(
"""CREATE TABLE IF NOT EXISTS quadrupoleME
(n1 TINYINT UNSIGNED, l1 TINYINT UNSIGNED,
j1 TINYINT UNSIGNED,
n2 TINYINT UNSIGNED, l2 TINYINT UNSIGNED,
j2 TINYINT UNSIGNED, s TINYINT UNSIGNED,
qme DOUBLE,
PRIMARY KEY (n1,l1,j1,n2,l2,j2,s)
) """
)
if len(data) > 0:
c.executemany(
"INSERT INTO quadrupoleME VALUES (?,?,?,?,?,?,?,?)",
data,
)
self.conn.commit()
except sqlite3.Error as e:
print("Error while loading precalculated values into the database")
print(e)
exit()
if self.levelDataFromNIST == "":
print(
"NIST level data file not specified."
" Only quantum defects will be used."
)
else:
levels = self._parseLevelsFromNIST(
os.path.join(self.dataFolder, self.levelDataFromNIST)
)
br = 0
while br < len(levels):
self._addEnergy(*levels[br])
br = br + 1
try:
self.conn.commit()
except sqlite3.Error as e:
print(
"Error while loading precalculated values"
"into the database"
)
print(e)
exit()
self._readLiteratureValues()
def _parseLevelsFromNIST(self, fileData):
data = np.loadtxt(fileData, delimiter=",", usecols=(0, 1, 3, 2, 4))
return data
def _addEnergy(self, n, l, j, s, energy):
"""
Adds energy level relative to
NOTE:
Requres changes to be commited to the sql database afterwards!
Args:
n: principal quantum number
l: orbital angular momentum quantum number
j: total angular momentum quantum number
s: spin quantum number
energy: energy in cm^-1 relative to the ground state
"""
c = self.conn.cursor()
c.execute(
"INSERT INTO energyLevel VALUES (?,?,?,?,?)",
(
round(n),
round(l),
round(j),
round(s),
energy
* 1.0e2
* physical_constants[
"inverse meter-electron volt relationship"
][0]
- self.ionisationEnergy,
),
)
self.NISTdataLevels = max(self.NISTdataLevels, round(n))
# saves energy in eV
def _databaseInit(self):
self.conn = sqlite3.connect(
os.path.join(self.dataFolder, self.precalculatedDB)
)
c = self.conn.cursor()
# create space for storing NIST/literature energy levels
c.execute(
"""SELECT COUNT(*) FROM sqlite_master
WHERE type='table' AND name='energyLevel';"""
)
if c.fetchone()[0] != 0:
c.execute("""DROP TABLE energyLevel""")
# create fresh table
c.execute(
"""CREATE TABLE IF NOT EXISTS energyLevel
(n TINYINT UNSIGNED, l TINYINT UNSIGNED, j TINYINT UNSIGNED,
s TINYINT UNSIGNED,
energy DOUBLE,
PRIMARY KEY (n, l, j, s)
) """
)
self.conn.commit()
[docs]
def getEnergy(self, n, l, j, s=None):
if s is None:
raise ValueError(
"Spin state for DivalentAtom has to be "
"explicitly defined as a keyword argument "
"s=0 or s=1"
)
if l >= n:
raise ValueError(
"Requested energy for state l=%d >= n=%d !" % (l, n)
)
stateLabel = "%d%s%d" % (round(2 * s + 1), printStateLetter(l), j)
minQuantumDefectN = 100000
maxQuantumDefectN = 0
if stateLabel in self.defectFittingRange.keys():
minQuantumDefectN = self.defectFittingRange[stateLabel][0]
maxQuantumDefectN = self.defectFittingRange[stateLabel][1]
# use NIST data ?
if not self.preferQuantumDefects or n < minQuantumDefectN:
savedEnergy = self._getSavedEnergy(n, l, j, s=s)
if abs(savedEnergy) > 1e-8:
return savedEnergy
else:
if n < minQuantumDefectN or n > maxQuantumDefectN:
self.energyLevelsExtrapolated = True
# else, use quantum defects
defect = self.getQuantumDefect(n, l, j, s=s)
return -self.scaledRydbergConstant / ((n - defect) ** 2)
def _getSavedEnergy(self, n, l, j, s=0):
c = self.conn.cursor()
c.execute(
"""SELECT energy FROM energyLevel WHERE
n= ? AND l = ? AND j = ? AND
s = ? """,
(n, l, j, s),
)
energy = c.fetchone()
if energy:
return energy[0]
else:
return 0 # there is no saved energy level measurement
[docs]
def getRadialMatrixElement(
self, n1, l1, j1, n2, l2, j2, s=None, useLiterature=True
):
"""
Radial part of the dipole matrix element
Calculates :math:`\\int \\mathbf{d}r~R_{n_1,l_1,j_1}(r)\\cdot \
R_{n_1,l_1,j_1}(r) \\cdot r^3`.
Args:
n1 (int): principal quantum number of state 1
l1 (int): orbital angular momentum of state 1
j1 (float): total angular momentum of state 1
n2 (int): principal quantum number of state 2
l2 (int): orbital angular momentum of state 2
j2 (float): total angular momentum of state 2
s (float): is required argument, total spin angular momentum of
state. Specify `s=0` for singlet state or `s=1` for
triplet state.
useLiterature (bool): optional, should literature values for
dipole matrix element be used if existing? If true,
compiled values stored in `literatureDMEfilename` variable
for a given atom (file is stored locally at ~/.arc-data/),
will be checked, and if the value is found, selects the
value with smallest error estimate (if there are multiple
entries). If no value is found, it will default to numerical
integration of wavefunctions. By default True.
Returns:
float: dipole matrix element (:math:`a_0 e`).
"""
if s is None:
raise ValueError(
"You must specify total angular momentum s " "explicitly."
)
dl = abs(l1 - l2)
dj = abs(j2 - j2)
if not (dl == 1 and (dj < 1.1)):
return 0
if self.getEnergy(n1, l1, j1, s=s) > self.getEnergy(n2, l2, j2, s=s):
temp = n1
n1 = n2
n2 = temp
temp = l1
l1 = l2
l2 = temp
temp = j1
j1 = j2
j2 = temp
n1 = round(n1)
n2 = round(n2)
l1 = round(l1)
l2 = round(l2)
c = self.conn.cursor()
if useLiterature:
# is there literature value for this DME? If there is,
# use the best one (smalles error)
c.execute(
"""SELECT dme FROM literatureDME WHERE
n1= ? AND l1 = ? AND j1 = ? AND
n2 = ? AND l2 = ? AND j2 = ? AND s = ?
ORDER BY errorEstimate ASC""",
(n1, l1, j1, n2, l2, j2, s),
)
answer = c.fetchone()
if answer:
# we did found literature value
return answer[0]
# was this calculated before? If it was, retrieve from memory
c.execute(
"""SELECT dme FROM dipoleME WHERE
n1= ? AND l1 = ? AND j1 = ? AND
n2 = ? AND l2 = ? AND j2 = ? AND s = ?""",
(n1, l1, j1, n2, l2, j2, s),
)
dme = c.fetchone()
if dme:
return dme[0]
dipoleElement = self._getRadialDipoleSemiClassical(
n1, l1, j1, n2, l2, j2, s=s
)
c.execute(
""" INSERT INTO dipoleME VALUES (?,?,?, ?,?,?, ?, ?)""",
[n1, l1, j1, n2, l2, j2, s, dipoleElement],
)
self.conn.commit()
return dipoleElement
def _readLiteratureValues(self):
# clear previously saved results, since literature file
# might have been updated in the meantime
c = self.conn.cursor()
c.execute("""DROP TABLE IF EXISTS literatureDME""")
c.execute(
"""SELECT COUNT(*) FROM sqlite_master
WHERE type='table' AND name='literatureDME';"""
)
if c.fetchone()[0] == 0:
# create table
c.execute(
"""CREATE TABLE IF NOT EXISTS literatureDME
(n1 TINYINT UNSIGNED, l1 TINYINT UNSIGNED, j1 TINYINT UNSIGNED,
n2 TINYINT UNSIGNED, l2 TINYINT UNSIGNED, j2 TINYINT UNSIGNED,
s TINYINT UNSIGNED,
dme DOUBLE,
typeOfSource TINYINT,
errorEstimate DOUBLE,
comment TINYTEXT,
ref TINYTEXT,
refdoi TINYTEXT
);"""
)
c.execute(
"""CREATE INDEX compositeIndex
ON literatureDME (n1,l1,j1,n2,l2,j2,s); """
)
self.conn.commit()
if self.literatureDMEfilename == "":
return 0 # no file specified for literature values
try:
fn = open(
os.path.join(self.dataFolder, self.literatureDMEfilename), "r"
)
dialect = csv.Sniffer().sniff(fn.read(2024), delimiters=";,\t")
fn.seek(0)
data = csv.reader(fn, dialect, quotechar='"')
literatureDME = []
# i=0 is header
i = 0
for row in data:
if i != 0:
n1 = round(row[0])
l1 = round(row[1])
j1 = round(row[2])
s1 = round(row[3])
n2 = round(row[4])
l2 = round(row[5])
j2 = round(row[6])
s2 = round(row[7])
if s1 != s2:
raise ValueError(
"Error reading litearture: database "
"cannot accept spin changing "
"transitions"
)
s = s1
if self.getEnergy(n1, l1, j1, s=s) > self.getEnergy(
n2, l2, j2, s=s
):
temp = n1
n1 = n2
n2 = temp
temp = l1
l1 = l2
l2 = temp
temp = j1
j1 = j2
j2 = temp
# convered from reduced DME in J basis (symmetric notation)
# to radial part of dme as it is saved for calculated
# values
# To-DO : see in what notation are Strontium literature elements saved
print(
"To-do (_readLiteratureValues): see in what notation are Sr literature saved (angular part)"
)
dme = float(row[8]) / (
(-1) ** (round(l1 + s + j2 + 1.0))
* sqrt((2.0 * j1 + 1.0) * (2.0 * j2 + 1.0))
* Wigner6j(j1, 1.0, j2, l2, s, l1)
* (-1) ** l1
* sqrt((2.0 * l1 + 1.0) * (2.0 * l2 + 1.0))
* Wigner3j(l1, 1, l2, 0, 0, 0)
)
comment = row[9]
typeOfSource = round(row[10]) # 0 = experiment; 1 = theory
errorEstimate = float(row[11])
ref = row[12]
refdoi = row[13]
literatureDME.append(
[
n1,
l1,
j1,
n2,
l2,
j2,
s,
dme,
typeOfSource,
errorEstimate,
comment,
ref,
refdoi,
]
)
i += 1
fn.close()
try:
if i > 1:
c.executemany(
"""INSERT INTO literatureDME
VALUES (?,?,?, ?,?,?,?
?,?,?,?,?,?)""",
literatureDME,
)
self.conn.commit()
except sqlite3.Error as e:
print(
"Error while loading precalculated values "
"into the database"
)
print(e)
print(literatureDME)
exit()
except IOError as e:
print(
"Error reading literature values File "
+ self.literatureDMEfilename
)
print(e)
[docs]
def getLiteratureDME(self, n1, l1, j1, n2, l2, j2, s=0):
"""
Returns literature information on requested transition.
Args:
n1,l1,j1: one of the states we are coupling
n2,l2,j2: the other state to which we are coupling
s: (optional) spin of the state. Default s=0.
Returns:
bool, float, [int,float,string,string,string]:
hasLiteratureValue?, dme, referenceInformation
**If Boolean value is True**, a literature value for
dipole matrix element was found and reduced DME in J basis
is returned as the number. The third returned argument
(array) contains additional information about the
literature value in the following order [ typeOfSource,
errorEstimate , comment , reference, reference DOI]
upon success to find a literature value for dipole matrix
element:
* typeOfSource=1 if the value is theoretical\
calculation; otherwise, if it is experimentally \
obtained value typeOfSource=0
* comment details where within the publication the \
value can be found
* errorEstimate is absolute error estimate
* reference is human-readable formatted reference
* reference DOI provides link to the publication.
**Boolean value is False**, followed by zero and an empty
array if no literature value for dipole matrix element is
found.
Note:
The literature values are stored in /data folder in
<element name>_literature_dme.csv files as a ; separated
values. Each row in the file consists of one literature entry,
that has information in the following order:
* n1
* l1
* j1
* n2
* l2
* j2
* s
* dipole matrix element reduced l basis (a.u.)
* comment (e.g. where in the paper value appears?)
* value origin: 1 for theoretical; 0 for experimental values
* accuracy
* source (human readable formatted citation)
* doi number (e.g. 10.1103/RevModPhys.82.2313 )
If there are several values for a given transition, program
outputs the value that has smallest error (under column
accuracy). The list of values can be expanded - every time
program runs this file is read and the list is parsed again
for use in calculations.
"""
if self.getEnergy(n1, l1, j1, s=s) > self.getEnergy(n2, l2, j2, s=s):
temp = n1
n1 = n2
n2 = temp
temp = l1
l1 = l2
l2 = temp
temp = j1
j1 = j2
j2 = temp
# is there literature value for this DME? If there is,
# use the best one (wit the smallest error)
c = self.conn.cursor()
c.execute(
"""SELECT dme, typeOfSource,
errorEstimate ,
comment ,
ref,
refdoi FROM literatureDME WHERE
n1= ? AND l1 = ? AND j1 = ? AND
n2 = ? AND l2 = ? AND j2 = ? AND s = ?
ORDER BY errorEstimate ASC""",
(n1, l1, j1, n2, l2, j2, s),
)
answer = c.fetchone()
if answer:
# we did found literature value
return (
True,
answer[0],
[answer[1], answer[2], answer[3], answer[4], answer[5]],
)
# if we are here, we were unsucessfull in literature search
# for this value
return False, 0, []
[docs]
def getQuadrupoleMatrixElement(self, n1, l1, j1, n2, l2, j2, s=0.5):
"""
Radial part of the quadrupole matrix element
Calculates :math:`\\int \\mathbf{d}r~R_{n_1,l_1,j_1}(r)\\cdot \
R_{n_1,l_1,j_1}(r) \\cdot r^4`.
See `Quadrupole calculation example snippet`_ .
.. _`Quadrupole calculation example snippet`:
./Rydberg_atoms_a_primer.html#Quadrupole-matrix-elements
Args:
n1 (int): principal quantum number of state 1
l1 (int): orbital angular momentum of state 1
j1 (float): total angular momentum of state 1
n2 (int): principal quantum number of state 2
l2 (int): orbital angular momentum of state 2
j2 (float): total angular momentum of state 2
s (float): optional. Spin of the state. Default 0.5 is for
Alkali
Returns:
float: quadrupole matrix element (:math:`a_0^2 e`).
"""
dl = abs(l1 - l2)
dj = abs(j1 - j2)
if not ((dl == 0 or dl == 2 or dl == 1) and (dj < 2.1)):
return 0
if self.getEnergy(n1, l1, j1, s=s) > self.getEnergy(n2, l2, j2, s=s):
temp = n1
n1 = n2
n2 = temp
temp = l1
l1 = l2
l2 = temp
temp = j1
j1 = j2
j2 = temp
n1 = round(n1)
n2 = round(n2)
l1 = round(l1)
l2 = round(l2)
# was this calculated before? If yes, retrieve from memory.
c = self.conn.cursor()
c.execute(
"""SELECT qme FROM quadrupoleME WHERE
n1= ? AND l1 = ? AND j1 = ? AND
n2 = ? AND l2 = ? AND j2 = ? AND s= ?""",
(n1, l1, j1, n2, l2, j2, s),
)
qme = c.fetchone()
if qme:
return qme[0]
# if it wasn't, calculate now
quadrupoleElement = self._getRadialQuadrupoleSemiClassical(
n1, l1, j1, n2, l2, j2, s=s
)
c.execute(
""" INSERT INTO quadrupoleME VALUES (?,?,?, ?,?,?,?, ?)""",
[n1, l1, j1, n2, l2, j2, s, quadrupoleElement],
)
self.conn.commit()
return quadrupoleElement
[docs]
def radialWavefunction(
self, l, s, j, stateEnergy, innerLimit, outerLimit, step
):
"""
Not implemented for Alkaline earths
"""
raise NotImplementedError(
"radialWavefunction calculation for alkaline"
" earths has not been implemented yet."
)
[docs]
def effectiveCharge(self, l, r):
"""
Not implemented for Alkaline earths
"""
raise NotImplementedError(
"effectiveCharge calculation for alkaline"
" earths has not been implemented yet."
)
[docs]
def corePotential(self, l, r):
"""
Not implemented for Alkaline earths
"""
raise NotImplementedError(
"corePotential calculation for alkaline"
" earths has not been implemented yet."
)
[docs]
def potential(self, l, s, j, r):
"""
Not implemented for Alkaline earths
"""
raise NotImplementedError(
"potential calculation for alkaline"
" earths has not been implemented yet."
)
[docs]
def getStateLifetime(
self, n, l, j, temperature=0, includeLevelsUpTo=0, s=0
):
print(
"WARNING: For AlkalineEarths, lifetimes are observed to be "
"significantly modified by inter-electron correlations that are "
"not included in this code (see Vaillant et al., J. Phys B 47 "
"155001 (2015) for examples). Use with caution."
)
# after waring user, call method from the parent class
# (parent of DivalentAtom is AlkaliAtom)
return super(DivalentAtom, self).getStateLifetime(
n,
l,
j,
temperature=temperature,
includeLevelsUpTo=includeLevelsUpTo,
s=s,
)