diff --git a/setup.py b/setup.py
index 0590d59..43bf5f9 100644
--- a/setup.py
+++ b/setup.py
@@ -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",
diff --git a/src/pyvalem/states/J1K_LK_coupling.py b/src/pyvalem/states/J1K_LK_coupling.py
new file mode 100644
index 0000000..a4e8be9
--- /dev/null
+++ b/src/pyvalem/states/J1K_LK_coupling.py
@@ -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 = "o"
+ J_html = ""
+ if self.J is not None:
+ J_html = f"{float_to_fraction(self.J)}"
+ html_chunks = [
+ f"{str(self.Smult)}",
+ 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
diff --git a/src/pyvalem/states/_state_parser.py b/src/pyvalem/states/_state_parser.py
index e381402..92f5244 100644
--- a/src/pyvalem/states/_state_parser.py
+++ b/src/pyvalem/states/_state_parser.py
@@ -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
@@ -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),
]
)
diff --git a/src/pyvalem/states/atomic_configuration.py b/src/pyvalem/states/atomic_configuration.py
index 335de06..bbfb0a3 100644
--- a/src/pyvalem/states/atomic_configuration.py
+++ b/src/pyvalem/states/atomic_configuration.py
@@ -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")
)
@@ -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:
@@ -134,9 +135,14 @@ def html(self):
-------
str
"""
+
+ if self.n == "n":
+ s_n = "n"
+ else:
+ s_n = str(self.n)
if self.nocc != 1:
- return "{}{}{}".format(self.n, self.lletter, self.nocc)
- return "{}{}".format(self.n, self.lletter)
+ return f"{s_n}{self.lletter}{self.nocc}"
+ return f"{s_n}{self.lletter}"
@property
def latex(self):
@@ -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)
diff --git a/tests/test_J1K_LK_coupling.py b/tests/test_J1K_LK_coupling.py
new file mode 100644
index 0000000..c61c87a
--- /dev/null
+++ b/tests/test_J1K_LK_coupling.py
@@ -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, "2[9/2]")
+ self.assertEqual(t0.latex, "{}^{2}[9/2]")
+
+ t1 = J1K_LK_Coupling("2[9/2]_5")
+ self.assertEqual(t1.html, "2[9/2]5")
+ self.assertEqual(t1.latex, "{}^{2}[9/2]_{5}")
+
+ t2 = J1K_LK_Coupling("3[9/2]o_11/2")
+ self.assertEqual(t2.html, "3[9/2]o11/2")
+ 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")
diff --git a/tests/test_atomic_configurations.py b/tests/test_atomic_configurations.py
index fa91b67..1502737 100644
--- a/tests/test_atomic_configurations.py
+++ b/tests/test_atomic_configurations.py
@@ -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, "1s2")
self.assertEqual(c1.html, "1s22s2")
@@ -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, "1s22p")
+ 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")
@@ -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, "1s2np")
+ self.assertEqual(c1.latex, "1s^{2}np")
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_stateful_species.py b/tests/test_stateful_species.py
index 2396828..de9b3a7 100644
--- a/tests/test_stateful_species.py
+++ b/tests/test_stateful_species.py
@@ -120,6 +120,17 @@ def test_compoundLScoupling_species(self):
self.assertEqual(scl.html, "2s22p2(3P)6d")
self.assertEqual(sat.html, "4F")
+ 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 3s2nd y2D")
+
+ 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()