"""
Implementation of the abstraction for the elctronic configuration object.
"""
from typing import Dict, List, Pattern, Tuple, Union
from collections import OrderedDict
import math
import re
ORBITALS = ("s", "p", "d", "f", "g", "h", "i", "j", "k")
SHELLS = ("K", "L", "M", "N", "O", "P", "Q")
def get_l(subshell: str) -> int:
"Return the orbital angular momentum quantum number for a given subshell"
if subshell.lower() in ORBITALS:
return ORBITALS.index(subshell.lower())
else:
raise ValueError(
(
f'wrong subshell label: "{subshell}",'
+ f' should be one of: {", ".join(ORBITALS)}'
)
)
def subshell_degeneracy(subshell: str) -> int:
"Return the degeneracy of a given subshell"
return 2 * get_l(subshell) + 1
def subshell_capacity(subshell: str) -> int:
"""
Return the subshell capacity (max number of electrons)
"""
return 2 * subshell_degeneracy(subshell)
def shell_capactity(shell: str) -> int:
"""
Return the shell capacity (max number of electrons)
The capacity is :math:`N=2n^{2}`, where :math:`n` is the principal
quantum number.
Args:
shell: str
Shell label, "K", "L", "M", ...
Returns:
capacity: int
Number of electrons that can occupy a given shell
"""
if shell.upper() in SHELLS:
return 2 * (SHELLS.index(shell.upper()) + 1) ** 2
else:
raise ValueError(
(
f'wrong shell label: "{shell}",'
+ f' should be one of: {", ".join(SHELLS)}'
)
)
[docs]
class ElectronicConfiguration(object):
"""Electronic configuration handler"""
noble = OrderedDict(
[
("He", "1s2"),
("Ne", "1s2 2s2 2p6"),
("Ar", "1s2 2s2 2p6 3s2 3p6"),
("Kr", "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6"),
("Xe", "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6"),
("Rn", "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6"),
]
)
def __init__(
self, conf: Union[str, Dict] = None, atomre: str = None, shellre: str = None
):
self.atomre = atomre
self.shellre = shellre
self.conf = conf
@property
def conf(self) -> OrderedDict:
"Return the configuration"
return self._conf
@conf.setter
def conf(self, value: Union[str, Dict]) -> OrderedDict:
"Setter method for initializing the configuration"
if isinstance(value, str):
self.confstr = value
self.parse(value)
elif isinstance(value, dict):
self._conf = OrderedDict(
sorted(value.items(), key=lambda x: (x[0][0] + get_l(x[0][1]), x[0][0]))
)
else:
raise ValueError(f"<conf> should be str or dict, got {type(value)}")
@property
def atomre(self) -> Pattern:
"Regular expression for atomic symbols"
return self._atomre
@atomre.setter
def atomre(self, value: str) -> None:
if value is None:
self._atomre = re.compile(r"\[([A-Z][a-z]*)\]")
else:
self._atomre = re.compile(value)
@property
def shellre(self) -> Pattern:
"Regular expression for the shell"
return self._shellre
@shellre.setter
def shellre(self, value: str) -> None:
if value is None:
self._shellre = re.compile(r"(?P<n>\d)(?P<o>[spdfghijk])(?P<e>\d+)?")
else:
self._shellre = re.compile(value)
[docs]
def parse(self, string: str) -> None:
"""
Parse a ``string`` with electronic configuration into an
``OrderedDict`` representation
"""
core = {}
citems = string.split()
if self.atomre.match(citems[0]):
symbol = str(self.atomre.match(citems[0]).group(1))
citems = citems[1:]
core = [
self.shellre.match(o).group("n", "o", "e")
for o in ElectronicConfiguration.noble[symbol].split()
if self.shellre.match(o)
]
core = OrderedDict(
[((int(n), o), (int(e) if e is not None else 1)) for (n, o, e) in core]
)
valence = [
self.shellre.match(o).group("n", "o", "e")
for o in citems
if self.shellre.match(o)
]
valence = OrderedDict(
[((int(n), o), (int(e) if e is not None else 1)) for (n, o, e) in valence]
)
self._conf = OrderedDict(list(core.items()) + list(valence.items()))
[docs]
def get_largest_core(self) -> Tuple:
"""
Find the largest noble gas core possible for the current
configuration and return the symbol of the corresponding noble
gas element.
"""
confset = set(self.conf.items())
for s, conf in reversed(ElectronicConfiguration.noble.items()):
ec = ElectronicConfiguration(conf)
nobleset = set(ec.conf.items())
ans = confset.issuperset(nobleset)
if ans:
return (s, ec)
[docs]
def get_valence(self):
"""
Find the valence configuration i.e. remove the largest noble gas
core from the current configuration and return the result.
"""
_, core_conf = self.get_largest_core()
valence = OrderedDict(set(self.conf.items()) - set(core_conf.conf.items()))
return ElectronicConfiguration(valence)
[docs]
def sort(self, inplace: bool = True):
"Sort the occupations OD"
if inplace:
self.conf = OrderedDict(
sorted(
self.conf.items(), key=lambda x: (x[0][0] + get_l(x[0][1]), x[0][0])
)
)
else:
return OrderedDict(
sorted(
self.conf.items(), key=lambda x: (x[0][0] + get_l(x[0][1]), x[0][0])
)
)
[docs]
def electrons_per_shell(self) -> Dict[str, int]:
"Return number of electrons per shell as dict"
return {
s: sum(v for k, v in self.conf.items() if k[0] == n)
for n, s in zip(range(1, self.max_n() + 1), SHELLS)
}
[docs]
def shell2int(self) -> List[Tuple[int]]:
"configuration as list of tuples (n, l, e)"
return [(x[0], get_l(x[1]), x[2]) for x in self.conf]
[docs]
def max_n(self) -> int:
"Return the largest value of principal quantum number for the atom"
return max(shell[0] for shell in self.conf.keys())
[docs]
def max_l(self, n: int) -> int:
"""
Return the largest value of azimutal quantum number for a given
value of principal quantum number
Args:
n : int
Principal quantum number
Returns:
l: int
Azimutal quantum number
"""
return ORBITALS[max(get_l(x[1]) for x in self.conf.keys() if x[0] == n)]
[docs]
def last_subshell(self, wrt: str = "order"):
"Return the valence shell"
if wrt.lower() == "order":
return list(self.conf.items())[-1]
elif wrt.lower() == "aufbau":
return sorted(
self.conf.items(), key=lambda x: (x[0][0] + get_l(x[0][1]), x[0][0])
)[-1]
else:
raise ValueError(f"wrong <wrt>: {wrt}")
[docs]
def nvalence(self, block: str, period: int, method: str = None) -> int:
"Return the number of valence electrons"
if block in {"s", "p"}:
return sum(v for k, v in self.conf.items() if k[0] == self.max_n())
elif block == "d":
if method == "simple":
return 2
return self.conf.get((period, "s"), 0) + self.conf.get((period - 1, "d"), 0)
elif block == "f":
if method == "simple":
return 2
return (
self.conf.get((period, "s"), 0)
+ self.conf.get((period, "p"), 0)
+ self.conf.get((period - 1, "d"), 0)
+ self.conf.get((period - 2, "f"), 0)
)
else:
raise ValueError(f"Cannot process block: {block}")
[docs]
def ne(self) -> int:
"Number of electrons"
return sum(list(self.conf.values()))
[docs]
def unpaired_electrons(self) -> int:
"Number of unpaired electrons"
so = self.spin_occupations()
return sum(v["unpaired"] for v in so.values())
[docs]
def ionize(self, n: int = 1):
"""
Remove `n` electrons from and return a new `ElectronicConfiguration`
object"""
newec = ElectronicConfiguration(str(self.__str__()))
for _ in range(n):
if not newec.conf:
raise ValueError("Cannot ionize further, no more electrons!")
(n, o), ne = newec.last_subshell()
if ne > 1:
newec.conf[(n, o)] = ne - 1
elif ne == 1:
newec.conf.pop((n, o))
return newec
[docs]
def spin_occupations(self):
"""
For each subshell calculate the number of `alpha`, `beta` electrons,
electron pairs and unpaired electrons
"""
so = OrderedDict()
for (n, orb), nele in self.conf.items():
ssc = subshell_capacity(orb)
ssd = subshell_degeneracy(orb)
if nele == ssc:
so[(n, orb)] = {"pairs": ssd, "alpha": ssd, "beta": ssd, "unpaired": 0}
else:
pairs = (nele % ssd) * (nele // ssd)
alpha = nele - pairs
beta = nele - alpha
unpaired = nele - pairs * 2
so[(n, orb)] = {
"pairs": pairs,
"alpha": alpha,
"beta": beta,
"unpaired": unpaired,
}
return so
[docs]
def spin_only_magnetic_moment(self) -> float:
"""
Return the magnetic moment insluding only spin of the electrons
and not the angular momentum
"""
ue = self.unpaired_electrons()
return math.sqrt(ue * (ue + 2))
[docs]
def slater_screening(self, n: int, o: str, alle: bool = False):
"""
Calculate the screening constant using the approach introduced by
Slater in Slater, J. C. (1930). Atomic Shielding Constants. Physical
Review, 36(1), 57-64.
`doi:10.1103/PhysRev.36.57 <http://www.dx.doi.org/10.1103/PhysRev.36.57>`_
Args:
n : int
Principal quantum number
o : str
orbtial label, (s, p, d, ...)
alle : bool
Use all the valence electrons, i.e. calculate screening for
an extra electron
"""
ne = 0 if alle else 1
coeff = 0.3 if n == 1 else 0.35
if o in {"s", "p"}:
# get the number of valence electrons - 1
vale = float(
sum(v for k, v in self.conf.items() if k[0] == n and k[1] in ["s", "p"])
- ne
)
n1 = sum(v * 0.85 for k, v in self.conf.items() if k[0] == n - 1)
n2 = sum(float(v) for k, v in self.conf.items() if k[0] in range(1, n - 1))
elif o in {"d", "f"}:
# get the number of valence electrons - 1
vale = float(
sum(v for k, v in self.conf.items() if k[0] == n and k[1] == o) - ne
)
n1 = sum(float(v) for k, v in self.conf.items() if k[0] == n and k[1] != o)
n2 = sum(float(v) for k, v in self.conf.items() if k[0] in range(1, n))
else:
raise ValueError("wrong valence subshell: ", o)
return n1 + n2 + vale * coeff
[docs]
def to_str(self) -> str:
"Return a string with the configuration"
return " ".join(
"{n:d}{s:s}{e:d}".format(n=k[0], s=k[1], e=v) for k, v in self.conf.items()
)
def __repr__(self) -> str:
return f'<ElectronicConfiguration(conf="{self.to_str()}")>'
def __str__(self) -> str:
return self.to_str()
def get_spin_strings(sodict, average: bool = True):
"""
spin strings as numpy arrays
This should be called for valence only
"""
alphas = []
betas = []
for (n, orb), occ in sodict.items():
nss = subshell_degeneracy(orb)
if average:
alphas.extend([occ["alpha"] / nss] * nss)
betas.extend([occ["beta"] / nss] * nss)
else:
alphas.extend([1.0] * occ["alpha"] + [0.0] * (nss - occ["alpha"]))
betas.extend([1.0] * occ["beta"] + [0.0] * (nss - occ["beta"]))
return alphas, betas
def print_spin_occupations(sodict, average=True):
"Pretty format for the spin occupations"
alphas = []
betas = []
for (n, orb), occ in sodict.items():
nss = subshell_degeneracy(orb)
if average:
fmt = "10.8f"
a = ", ".join(
"{0:{fmt}}".format(x, fmt=fmt) for x in [occ["alpha"] / nss] * nss
)
b = ", ".join(
"{0:{fmt}}".format(x, fmt=fmt) for x in [occ["beta"] / nss] * nss
)
else:
a = ", ".join(
"{0:3.1f}".format(x)
for x in [1] * occ["alpha"] + [0] * (nss - occ["alpha"])
)
b = ", ".join(
"{0:3.1f}".format(x)
for x in [1] * occ["beta"] + [0] * (nss - occ["beta"])
)
alphas.append(a)
betas.append(b)
print(f"{orb} alpha: ", a)
print(f"{orb} beta : ", b)
return alphas, betas