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()