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