From f5e7db8139e020c4ea24ddc8379f4875fcfb1e7c Mon Sep 17 00:00:00 2001 From: Christian Hill Date: Mon, 20 Jan 2025 13:05:59 +0100 Subject: [PATCH] Add J1J2_Coupling class --- setup.py | 3 +- src/pyvalem/stateful_species.py | 2 +- src/pyvalem/states/J1J2_coupling.py | 102 ++++++++++++++++++++++++++++ src/pyvalem/states/__init__.py | 1 + src/pyvalem/states/_state_parser.py | 2 + tests/test_J1J2_coupling.py | 30 ++++++++ tests/test_compound_LS_coupling.py | 3 + tests/test_stateful_species.py | 10 ++- 8 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 src/pyvalem/states/J1J2_coupling.py create mode 100644 tests/test_J1J2_coupling.py diff --git a/setup.py b/setup.py index 7962be9..338c7dc 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name="pyvalem", - version="2.7.1", + version="2.8.0", description="A package for managing simple chemical species and states", long_description=long_description, long_description_content_type="text/x-rst", @@ -29,6 +29,7 @@ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3 :: Only", "Operating System :: OS Independent", ], diff --git a/src/pyvalem/stateful_species.py b/src/pyvalem/stateful_species.py index ffb1325..4e9254a 100644 --- a/src/pyvalem/stateful_species.py +++ b/src/pyvalem/stateful_species.py @@ -29,7 +29,7 @@ def __init__(self, s): i = s.index(" ") self.formula = Formula(s[:i]) - s = s.replace(";", " ").replace(",", " ") + s = s.replace(";", " ").replace(", ", " ") self.states = state_parser(s[i + 1 :].split()) self._verify_states() diff --git a/src/pyvalem/states/J1J2_coupling.py b/src/pyvalem/states/J1J2_coupling.py new file mode 100644 index 0000000..1c89615 --- /dev/null +++ b/src/pyvalem/states/J1J2_coupling.py @@ -0,0 +1,102 @@ +""" +The J1J2_Coupling class, representing a complex atomic state with two +groups of separately spin-orbit coupled electrons giving rise to a +(J1, J2) term symbol, perhaps further coupled to a total (J1, J2)_J +level, with or without parity designator. + +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 + +Includes methods for parsing a string into quantum numbers and labels, +creating an HTML representation of the term symbol, etc. +""" + +import pyparsing as pp + +from pyvalem._utils import parse_fraction, float_to_fraction +from pyvalem.states._base_state import State, StateParseError + +integer = pp.Word(pp.nums) +J1str = (integer + pp.Optional(pp.Suppress("/") + "2")).setResultsName("J1str") +J2str = (integer + pp.Optional(pp.Suppress("/") + "2")).setResultsName("J2str") +Jstr = (integer + pp.Optional(pp.Suppress("/") + "2")).setResultsName("Jstr") +parity = pp.Literal("o").setResultsName("parity") +J1J2_term = ( + "(" + + J1str + + "," + + J2str + + ")" + + pp.Optional(parity) + + pp.Optional(pp.Suppress("_") + Jstr) + + pp.StringEnd() +) + + +class J1J2_CouplingError(StateParseError): + pass + + +class J1J2_CouplingValidationError(ValueError): + pass + + +class J1J2_Coupling(State): + def __init__(self, state_str): + self.state_str = state_str + self.J1 = None + self.J2 = None + self.parity = None + self.J = None + self._parse_state(state_str) + + def _parse_state(self, state_str): + try: + components = J1J2_term.parseString(state_str) + except pp.ParseException: + raise J1J2_CouplingError(f"Invalid J1J2 coupling term syntax: {state_str}") + self.J1 = parse_fraction(components.J1str) + self.J2 = parse_fraction(components.J2str) + self.J = parse_fraction(components.get("Jstr")) + self.parity = components.get("parity") + + if self.J is not None: + self._validate_J() + + def _validate_J(self): + J1_is_half_integer = int(2 * self.J1) % 2 + J2_is_half_integer = int(2 * self.J2) % 2 + J_is_half_integer = int(2 * self.J) % 2 + if (J1_is_half_integer == J2_is_half_integer) == J_is_half_integer: + raise J1J2_CouplingValidationError( + f"J={self.J} is invalid for J1={self.J1} and J2={self.J2}." + ) + if not abs(self.J1 - self.J2) <= self.J <= self.J1 + self.J2: + raise J1J2_CouplingValidationError( + f"Invalid atomic term symbol: {self.state_str}" + f" |J1-J2| <= J <= J1+J2 must be satisfied." + ) + + @property + def html(self): + html_chunks = [f"({float_to_fraction(self.J1)}, {float_to_fraction(self.J2)})"] + if self.parity: + html_chunks.append("o") + if self.J is not None: + J_str = float_to_fraction(self.J) + html_chunks.append(f"{J_str}") + return "".join(html_chunks) + + @property + def latex(self): + latex_chunks = [f"({float_to_fraction(self.J1)}, {float_to_fraction(self.J2)})"] + if self.parity: + latex_chunks.append("^o") + if self.J is not None: + J_str = float_to_fraction(self.J) + latex_chunks.append("_{{{}}}".format(J_str)) + return "".join(latex_chunks) + + def __repr__(self): + return self.state_str diff --git a/src/pyvalem/states/__init__.py b/src/pyvalem/states/__init__.py index c34eb0e..b84ec63 100644 --- a/src/pyvalem/states/__init__.py +++ b/src/pyvalem/states/__init__.py @@ -18,3 +18,4 @@ J1K_LK_CouplingError, J1K_LK_CouplingValidationError, ) +from .J1J2_coupling import J1J2_Coupling, J1J2_CouplingError diff --git a/src/pyvalem/states/_state_parser.py b/src/pyvalem/states/_state_parser.py index 92f5244..1b1c446 100644 --- a/src/pyvalem/states/_state_parser.py +++ b/src/pyvalem/states/_state_parser.py @@ -16,6 +16,7 @@ from .vibrational_state import VibrationalState from .compound_LS_coupling import CompoundLSCoupling from .J1K_LK_coupling import J1K_LK_Coupling +from .J1J2_coupling import J1J2_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 @@ -29,6 +30,7 @@ (DiatomicMolecularConfiguration, 1), (MolecularTermSymbol, 2), (J1K_LK_Coupling, 3), + (J1J2_Coupling, 3), (VibrationalState, 4), (RotationalState, 5), (RacahSymbol, 6), diff --git a/tests/test_J1J2_coupling.py b/tests/test_J1J2_coupling.py new file mode 100644 index 0000000..7efbba4 --- /dev/null +++ b/tests/test_J1J2_coupling.py @@ -0,0 +1,30 @@ +""" +Unit tests for the J1J2_coupling module of PyValem. +""" + +import unittest + +from pyvalem.states.J1J2_coupling import ( + J1J2_Coupling, + J1J2_CouplingError, + J1J2_CouplingValidationError, +) + + +class J1J2_CouplingTest(unittest.TestCase): + def test_J1J2_coupling(self): + t0 = J1J2_Coupling("(1,3)") + self.assertEqual(t0.J1, 1) + self.assertEqual(t0.J2, 3) + self.assertIsNone(t0.J) + + t1 = J1J2_Coupling("(2,3/2)o_1/2") + self.assertEqual(t1.J1, 2) + self.assertEqual(t1.J2, 1.5) + self.assertEqual(t1.J, 0.5) + + def test_J_validation(self): + with self.assertRaises(J1J2_CouplingValidationError): + J1J2_Coupling("(1,1/2)_2") + with self.assertRaises(J1J2_CouplingValidationError): + J1J2_Coupling("(5/2,1/2)_1") diff --git a/tests/test_compound_LS_coupling.py b/tests/test_compound_LS_coupling.py index 0f4629e..e7e820e 100644 --- a/tests/test_compound_LS_coupling.py +++ b/tests/test_compound_LS_coupling.py @@ -54,3 +54,6 @@ def test_compound_LS_coupling(self): self.assertEqual( s3.html, """5p5(2Po3/2)6d""" ) + + s4 = CompoundLSCoupling("4d9(2D_5/2)5s") + self.assertEqual(s4.html, """4d9(2D5/2)5s""") diff --git a/tests/test_stateful_species.py b/tests/test_stateful_species.py index de9b3a7..ab518ed 100644 --- a/tests/test_stateful_species.py +++ b/tests/test_stateful_species.py @@ -16,7 +16,7 @@ def test_stateful_species_parsing(self): _ = StatefulSpecies("Fe e5G") _ = StatefulSpecies("CrH 1sigma2.2sigma1.1pi4.3sigma1; 6SIGMA+") _ = StatefulSpecies("H(35Cl) J=2") - _ = StatefulSpecies("OH X(2Π_1/2, J=2") + _ = StatefulSpecies("OH X(2Π_1/2) J=2") _ = StatefulSpecies("HCl v=2 J=0") _ = StatefulSpecies("C2H3Cl") @@ -57,7 +57,7 @@ def test_state_appears_at_most_once(self): self.assertRaises(StatefulSpeciesError, StatefulSpecies, "CO J=0; J=1") self.assertRaises(StatefulSpeciesError, StatefulSpecies, "CO X(1PIu);2Σ-") self.assertRaises(StatefulSpeciesError, StatefulSpecies, "Ar *;**") - self.assertRaises(StatefulSpeciesError, StatefulSpecies, "Ar 2S, 2P_3/2") + self.assertRaises(StatefulSpeciesError, StatefulSpecies, "Ar 2S 2P_3/2") # StatefulSpecies('CH3Cl J=2;Ka=1;Kc=2') def test_atomic_configuration_verification(self): @@ -122,6 +122,8 @@ def test_compoundLScoupling_species(self): ss2 = StatefulSpecies("Fe+3 3d5(b2D)4s 3D_3") + ss3 = StatefulSpecies("Pd 4d9(2D_5/2)5s 2[5/2]") + def test_incomplete_atomic_orbital_specification(self): ss1 = StatefulSpecies("Al 3s2.nd; y2D") self.assertEqual(ss1.html, "Al 3s2nd y2D") @@ -131,6 +133,10 @@ def test_J1K_LK_coupling(self): _ = StatefulSpecies("Ne+3 2s2.2p2(3P_2)5g 2[6]_11/2") _ = StatefulSpecies("Ne+3 2s2.2p2(3P_2)5g 2[6]") + def test_J1J2_coupling(self): + _ = StatefulSpecies("Pd 6s2.6p.7s (3/2,1/2)") + _ = StatefulSpecies("Bi 5d4.6s(6D)31d (1/2,3/2)o_2") + if __name__ == "__main__": unittest.main()