Skip to content

Commit

Permalink
Add J1K_LK_coupling class
Browse files Browse the repository at this point in the history
  • Loading branch information
xnx committed Jun 9, 2024
1 parent 22afca7 commit 2207efc
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 15 deletions.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

setup(
name="pyvalem",
version="2.6.1",
version="2.7",
description="A package for managing simple chemical species and states",
long_description=long_description,
long_description_content_type="text/x-rst",
Expand Down
121 changes: 121 additions & 0 deletions src/pyvalem/states/J1K_LK_coupling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""
The J1K_LK_Coupling class, representing an atomic term symbol under
the coupling conditions J1l -> K, J1L2 -> K or L, S1 -> K. These
are described in Martin et al. (sec. 11.8.4 and 11.8.5). NB there is
currently no check that the coupling quantum numbers given actually
make sense:
W. C. Martin, W. Wiese, A. Kramida, "Atomic Spectroscopy" in "Springer
Handbook of Atomic, Molecular and Optical Physics", G. W. F. Drake (ed.),
https://doi.org/10.1007/978-3-030-73893-8_11
"""

import pyparsing as pp

from pyvalem.states._base_state import State, StateParseError
from pyvalem._utils import parse_fraction, float_to_fraction

integer = pp.Word(pp.nums)
Smult = integer.setResultsName("Smult")

fraction = integer + pp.Optional(pp.Suppress("/") + "2")
Kstr = fraction.setResultsName("Kstr")
Jstr = fraction.setResultsName("Jstr")
parity = pp.Literal("o").setResultsName("parity")

J1K_LK_term = (
Smult
+ pp.Suppress("[")
+ Kstr
+ pp.Suppress("]")
+ pp.Optional(parity)
+ pp.Optional(pp.Suppress("_") + Jstr)
+ pp.StringEnd()
)


class J1K_LK_CouplingError(StateParseError):
pass


class J1K_LK_CouplingValidationError(ValueError):
pass


class J1K_LK_Coupling(State):
def __init__(self, state_str):
self.state_str = state_str
self.Smult = None
self.K = None
self.J = None
self._parse_state(state_str)

def _parse_state(self, state_str):
try:
components = J1K_LK_term.parseString(state_str)
except pp.ParseException:
raise J1K_LK_CouplingError(
f'Invalid J1K / LK term symbol syntax:"{state_str}"'
)
self.Smult = int(components.Smult)
self.S = (self.Smult - 1) / 2.0
self.parity = components.get("parity")
try:
self.K = parse_fraction(components.Kstr)
except ValueError as err:
raise J1K_LK_CouplingError(err)
try:
self.J = parse_fraction(components.Jstr)
except ValueError as err:
raise J1K_LK_CouplingError(err)
if self.J is not None:
self._validate_J()

def _validate_J(self):
S_is_half_integer = int(2 * self.S) % 2
K_is_half_integer = int(2 * self.K) % 2
J_is_half_integer = int(2 * self.J) % 2
if J_is_half_integer != S_is_half_integer ^ K_is_half_integer:
raise J1K_LK_CouplingValidationError(
f"J={self.J} is invalid for S={self.S}, K={self.K}."
)
if not abs(self.K - self.S) <= self.J <= self.K + self.S:
raise J1K_LK_CouplingValidationError(
f"Invalid J1K_LK coupling symbol: {self.state_str}"
" |K-S| <= J <= K+S must be satisfied."
)

@property
def html(self):
parity_html = ""
if self.parity:
parity_html = "<sup>o</sup>"
J_html = ""
if self.J is not None:
J_html = f"<sub>{float_to_fraction(self.J)}</sub>"
html_chunks = [
f"<sup>{str(self.Smult)}</sup>",
f"[{float_to_fraction(self.K)}]",
parity_html,
J_html,
]
return "".join(html_chunks)

@property
def latex(self):
parity_latex = ""
if self.parity:
parity_latex = "^o"
J_latex = ""
if self.J is not None:
J_latex = f"_{{{float_to_fraction(self.J)}}}"
latex_chunks = [
"{}" + f"^{{{self.Smult}}}",
f"[{float_to_fraction(self.K)}]",
parity_latex,
J_latex,
]
return "".join(latex_chunks)

def __repr__(self):
return self.state_str
10 changes: 6 additions & 4 deletions src/pyvalem/states/_state_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .rotational_state import RotationalState
from .vibrational_state import VibrationalState
from .compound_LS_coupling import CompoundLSCoupling
from .J1K_LK_coupling import J1K_LK_Coupling

# the following has two purposes: keys determine the order in which the
# states are parsed, and the values determine the sorting order of states
Expand All @@ -27,10 +28,11 @@
(AtomicTermSymbol, 2),
(DiatomicMolecularConfiguration, 1),
(MolecularTermSymbol, 2),
(VibrationalState, 3),
(RotationalState, 4),
(RacahSymbol, 5),
(KeyValuePair, 6),
(J1K_LK_Coupling, 3),
(VibrationalState, 4),
(RotationalState, 5),
(RacahSymbol, 6),
(KeyValuePair, 7),
]
)

Expand Down
33 changes: 23 additions & 10 deletions src/pyvalem/states/atomic_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
noble_gas = pp.oneOf(["[{}]".format(symbol) for symbol in noble_gases])

atom_orbital = pp.Group(
integer.setResultsName("n")
(integer | "n").setResultsName("n")
+ pp.oneOf(atomic_orbital_symbols).setResultsName("lletter")
+ nocc_integer.setResultsName("nocc")
)
Expand Down Expand Up @@ -92,6 +92,7 @@ class AtomicOrbital:

def __init__(self, n, l=None, nocc=0, lletter=None):
self.n = n
self.incompletely_specified = False
if l is None:
self.lletter = lletter
try:
Expand Down Expand Up @@ -134,9 +135,14 @@ def html(self):
-------
str
"""

if self.n == "n":
s_n = "<em>n</em>"
else:
s_n = str(self.n)
if self.nocc != 1:
return "{}{}<sup>{}</sup>".format(self.n, self.lletter, self.nocc)
return "{}{}".format(self.n, self.lletter)
return f"{s_n}{self.lletter}<sup>{self.nocc}</sup>"
return f"{s_n}{self.lletter}"

@property
def latex(self):
Expand All @@ -160,13 +166,20 @@ def _validate_atomic_orbital(self):
------
AtomicOrbitalError
"""
if self.l > self.n - 1:
raise AtomicOrbitalError("l >= n in atomic orbital {}".format(self))
if self.nocc < 0:
raise AtomicOrbitalError(
"Negative nocc = {} not allowed in"
" orbital: {}".format(self.nocc, self.nocc)
)

if self.n == "n":
self.incompletely_specified = True
else:
# Do checks on n
if self.l > self.n - 1:
raise AtomicOrbitalError("l >= n in atomic orbital {}".format(self))
if self.nocc < 0:
raise AtomicOrbitalError(
"Negative nocc = {} not allowed in"
" orbital: {}".format(self.nocc, self.nocc)
)
# Even if the orbital is incompletely specified, we can still check
# the number of electrons against the quantum number l.
if self.nocc > 2 * (2 * self.l + 1):
raise AtomicOrbitalError(
"Too many electrons in atomic" " orbital: {}".format(self)
Expand Down
34 changes: 34 additions & 0 deletions tests/test_J1K_LK_coupling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
Unit tests for the J1K_LK_coupling module of PyValem.
"""

import unittest

from pyvalem.states.J1K_LK_coupling import (
J1K_LK_Coupling,
J1K_LK_CouplingError,
J1K_LK_CouplingValidationError,
)


class J1K_LK_CouplingTest(unittest.TestCase):
def test_J1K_LK_coupling(self):
t0 = J1K_LK_Coupling("2[9/2]")
self.assertEqual(t0.html, "<sup>2</sup>[9/2]")
self.assertEqual(t0.latex, "{}^{2}[9/2]")

t1 = J1K_LK_Coupling("2[9/2]_5")
self.assertEqual(t1.html, "<sup>2</sup>[9/2]<sub>5</sub>")
self.assertEqual(t1.latex, "{}^{2}[9/2]_{5}")

t2 = J1K_LK_Coupling("3[9/2]o_11/2")
self.assertEqual(t2.html, "<sup>3</sup>[9/2]<sup>o</sup><sub>11/2</sub>")
self.assertEqual(t2.latex, "{}^{3}[9/2]^o_{11/2}")

def test_J_validation(self):
with self.assertRaises(J1K_LK_CouplingValidationError):
J1K_LK_Coupling("2[9/2]_11/2")
with self.assertRaises(J1K_LK_CouplingValidationError):
J1K_LK_Coupling("3[9/2]o_5")
with self.assertRaises(J1K_LK_CouplingValidationError):
J1K_LK_Coupling("2[9/2]o_3")
12 changes: 12 additions & 0 deletions tests/test_atomic_configurations.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def test_atomic_configuration_html_and_latex(self):
c0 = AtomicConfiguration("1s2")
c1 = AtomicConfiguration("1s2.2s2")
c2 = AtomicConfiguration("[Ar].4s2.3d10.4p5")
c3 = AtomicConfiguration("1s2.2p")

self.assertEqual(c0.html, "1s<sup>2</sup>")
self.assertEqual(c1.html, "1s<sup>2</sup>2s<sup>2</sup>")
Expand All @@ -52,6 +53,10 @@ def test_atomic_configuration_html_and_latex(self):
self.assertEqual(c1.latex, "1s^{2}2s^{2}")
self.assertEqual(c2.latex, r"\mathrm{[Ar]}4s^{2}3d^{10}4p^{5}")

self.assertEqual(repr(c3), "1s2.2p")
self.assertEqual(c3.html, "1s<sup>2</sup>2p")
self.assertEqual(c3.latex, "1s^{2}2p")

def test_atomic_configuration_equality(self):
c1 = AtomicConfiguration("[Ar].4s2.3d10.4p5")
c2 = AtomicConfiguration("[Ar].4s2.3d10.4p5")
Expand Down Expand Up @@ -102,6 +107,13 @@ def test_atomic_configuration_default_nocc(self):
self.assertEqual(c3.orbitals[0].nocc, 1)
self.assertEqual(c3.orbitals[1].nocc, 2)

def test_atomic_configuration_unspecified_n(self):
c1 = AtomicConfiguration("1s2.np")
self.assertEqual(c1.nelectrons, 3)
self.assertEqual(str(c1), "1s2.np")
self.assertEqual(c1.html, "1s<sup>2</sup><em>n</em>p")
self.assertEqual(c1.latex, "1s^{2}np")


if __name__ == "__main__":
unittest.main()
11 changes: 11 additions & 0 deletions tests/test_stateful_species.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ def test_compoundLScoupling_species(self):
self.assertEqual(scl.html, "2s<sup>2</sup>2p<sup>2</sup>(<sup>3</sup>P)6d")
self.assertEqual(sat.html, "<sup>4</sup>F")

ss2 = StatefulSpecies("Fe+3 3d5(b2D)4s 3D_3")

def test_incomplete_atomic_orbital_specification(self):
ss1 = StatefulSpecies("Al 3s2.nd; y2D")
self.assertEqual(ss1.html, "Al 3s<sup>2</sup><em>n</em>d y<sup>2</sup>D")

def test_J1K_LK_coupling(self):
_ = StatefulSpecies("C 2s2.2p(2Po_1/2)5g 2[7/2]o_3")
_ = StatefulSpecies("Ne+3 2s2.2p2(3P_2)5g 2[6]_11/2")
_ = StatefulSpecies("Ne+3 2s2.2p2(3P_2)5g 2[6]")


if __name__ == "__main__":
unittest.main()

0 comments on commit 2207efc

Please sign in to comment.