Skip to content

Commit

Permalink
Add J1J2_Coupling class
Browse files Browse the repository at this point in the history
  • Loading branch information
xnx committed Jan 20, 2025
1 parent 4add740 commit f5e7db8
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 4 deletions.
3 changes: 2 additions & 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.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",
Expand All @@ -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",
],
Expand Down
2 changes: 1 addition & 1 deletion src/pyvalem/stateful_species.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
102 changes: 102 additions & 0 deletions src/pyvalem/states/J1J2_coupling.py
Original file line number Diff line number Diff line change
@@ -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("<sup>o</sup>")
if self.J is not None:
J_str = float_to_fraction(self.J)
html_chunks.append(f"<sub>{J_str}</sub>")
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
1 change: 1 addition & 0 deletions src/pyvalem/states/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
J1K_LK_CouplingError,
J1K_LK_CouplingValidationError,
)
from .J1J2_coupling import J1J2_Coupling, J1J2_CouplingError
2 changes: 2 additions & 0 deletions src/pyvalem/states/_state_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,6 +30,7 @@
(DiatomicMolecularConfiguration, 1),
(MolecularTermSymbol, 2),
(J1K_LK_Coupling, 3),
(J1J2_Coupling, 3),
(VibrationalState, 4),
(RotationalState, 5),
(RacahSymbol, 6),
Expand Down
30 changes: 30 additions & 0 deletions tests/test_J1J2_coupling.py
Original file line number Diff line number Diff line change
@@ -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")
3 changes: 3 additions & 0 deletions tests/test_compound_LS_coupling.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,6 @@ def test_compound_LS_coupling(self):
self.assertEqual(
s3.html, """5p<sup>5</sup>(<sup>2</sup>P<sup>o</sup><sub>3/2</sub>)6d"""
)

s4 = CompoundLSCoupling("4d9(2D_5/2)5s")
self.assertEqual(s4.html, """4d<sup>9</sup>(<sup>2</sup>D<sub>5/2</sub>)5s""")
10 changes: 8 additions & 2 deletions tests/test_stateful_species.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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 3s<sup>2</sup><em>n</em>d y<sup>2</sup>D")
Expand All @@ -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()

0 comments on commit f5e7db8

Please sign in to comment.