From bd319edbc1cb5e703440301b42a702f300810ee5 Mon Sep 17 00:00:00 2001 From: Christian Hill Date: Tue, 30 Apr 2024 15:25:09 +0200 Subject: [PATCH] Add CompoundLSCoupling state class --- setup.py | 3 +- src/pyvalem/states/_state_parser.py | 2 + src/pyvalem/states/atomic_term_symbol.py | 9 +++ src/pyvalem/states/compound_LS_coupling.py | 82 ++++++++++++++++++++++ tests/test_atomic_term_symbols.py | 15 ++++ tests/test_compound_LS_coupling.py | 56 +++++++++++++++ tests/test_stateful_species.py | 8 +++ 7 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 src/pyvalem/states/compound_LS_coupling.py create mode 100644 tests/test_compound_LS_coupling.py diff --git a/setup.py b/setup.py index 70f7be7..23f8b11 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name="pyvalem", - version="2.5.16", + version="2.6", description="A package for managing simple chemical species and states", long_description=long_description, long_description_content_type="text/x-rst", @@ -28,6 +28,7 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3 :: Only", "Operating System :: OS Independent", ], diff --git a/src/pyvalem/states/_state_parser.py b/src/pyvalem/states/_state_parser.py index be231a9..e381402 100644 --- a/src/pyvalem/states/_state_parser.py +++ b/src/pyvalem/states/_state_parser.py @@ -14,6 +14,7 @@ from .racah_symbol import RacahSymbol from .rotational_state import RotationalState from .vibrational_state import VibrationalState +from .compound_LS_coupling import CompoundLSCoupling # the following has two purposes: keys determine the order in which the # states are parsed, and the values determine the sorting order of states @@ -22,6 +23,7 @@ [ (GenericExcitedState, 0), (AtomicConfiguration, 1), + (CompoundLSCoupling, 1), (AtomicTermSymbol, 2), (DiatomicMolecularConfiguration, 1), (MolecularTermSymbol, 2), diff --git a/src/pyvalem/states/atomic_term_symbol.py b/src/pyvalem/states/atomic_term_symbol.py index 761dfe9..f934993 100644 --- a/src/pyvalem/states/atomic_term_symbol.py +++ b/src/pyvalem/states/atomic_term_symbol.py @@ -24,11 +24,13 @@ integer + pp.Optional(pp.Suppress("/") + "2") + pp.StringEnd() ).setResultsName("Jstr") atom_parity = pp.Literal("o").setResultsName("parity") +seniority = pp.Suppress("{") + integer.setResultsName("seniority") + pp.Suppress("}") atom_term = ( pp.Optional(moore_label) + atom_Smult + atom_Lletter + pp.Optional(atom_parity) + + pp.Optional(seniority) + pp.Optional(pp.Suppress("_") + atom_Jstr) + pp.StringEnd() ) @@ -48,6 +50,7 @@ def __init__(self, state_str): self.parity = None self.J = None self.moore_label = "" + self.seniority = None self._parse_state(state_str) def _parse_state(self, state_str): @@ -62,6 +65,8 @@ def _parse_state(self, state_str): self.Lletter = components.Lletter self.L = atom_L_symbols.index(components.Lletter) self.parity = components.get("parity") + if components.get("seniority"): + self.seniority = int(components.get("seniority")) self.moore_label = components.get("moore_label", "") try: self.J = parse_fraction(components.Jstr) @@ -92,6 +97,8 @@ def html(self): ] if self.parity: html_chunks.append("o") + if self.seniority: + html_chunks.append(str(self.seniority)) if self.J is not None: j_str = float_to_fraction(self.J) html_chunks.append("{0:s}".format(j_str)) @@ -106,6 +113,8 @@ def latex(self): ] if self.parity: latex_chunks.append("^o") + if self.seniority: + latex_chunks.append(str(self.seniority)) if self.J is not None: j_str = float_to_fraction(self.J) latex_chunks.append("_{{{}}}".format(j_str)) diff --git a/src/pyvalem/states/compound_LS_coupling.py b/src/pyvalem/states/compound_LS_coupling.py new file mode 100644 index 0000000..fec885e --- /dev/null +++ b/src/pyvalem/states/compound_LS_coupling.py @@ -0,0 +1,82 @@ +""" +The CompoundLSCoupling class, representing a complex atomic state with +multiple configurations coupling within the LS formulism to several +intermediate terms, e.g. "4f(2Fo)5d2(1G)6s(2G)". The final term should +be given as its own AtomicTermSymbol. The full CompoundLSCoupling state +should contain no spaces; NB the different interaction strength cases +detailed in Martin et al. (sec. 11.8.1) are not yet implemented. + +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 re + +from pyvalem.states._base_state import State, StateParseError +from .atomic_term_symbol import AtomicTermSymbol, AtomicTermSymbolError +from .atomic_configuration import AtomicConfiguration, AtomicConfigurationError + +term_patt = r"\((.*?)\)" + + +class CompoundLSCouplingError(StateParseError): + pass + + +class CompoundLSCoupling(State): + def __init__(self, state_str): + self.state_str = state_str + self.atomic_configurations = [] + self.term_symbols = [] + self._parse_state(self.state_str) + + def _parse_state(self, state_str): + terms = re.findall(term_patt, state_str) + configs = state_str.split("(") + + if len(configs) == 0 or len(terms) == 0: + raise CompoundLSCouplingError + + try: + for i, config in enumerate(configs[1:], start=1): + configs[i] = configs[i][len(terms[i - 1]) + 1 :] + except IndexError: + raise CompoundLSCouplingError + + if not configs[-1]: + configs = configs[:-1] + + try: + self.terms = [AtomicTermSymbol(term) for term in terms] + self.atomic_configurations = [ + AtomicConfiguration(config) for config in configs + ] + except (AtomicTermSymbolError, AtomicConfigurationError): + raise CompoundLSCouplingError + + def __repr__(self): + return self.state_str + + @property + def html(self): + html_chunks = [] + for config, term in zip(self.atomic_configurations, self.terms): + html_chunks.append(config.html) + html_chunks.append(f"({term.html})") + if len(self.atomic_configurations) > len(self.terms): + html_chunks.append(self.atomic_configurations[-1].html) + return "".join(html_chunks) + + @property + def latex(self): + latex_chunks = [] + for config, term in zip(self.atomic_configurations, self.terms): + latex_chunks.append(config.latex) + latex_chunks.append(f"({term.latex})") + if len(self.atomic_configurations) > len(self.terms): + latex_chunks.append(self.atomic_configurations[-1].latex) + return "".join(latex_chunks) diff --git a/tests/test_atomic_term_symbols.py b/tests/test_atomic_term_symbols.py index 7c7e0b7..4adc7ad 100644 --- a/tests/test_atomic_term_symbols.py +++ b/tests/test_atomic_term_symbols.py @@ -65,6 +65,21 @@ def test_moore_label(self): self.assertEqual(a1.latex, r"z{}^{3}\mathrm{P}^o") self.assertRaises(AtomicTermSymbolError, AtomicTermSymbol, "A5D") + def test_seniority_label(self): + a0 = AtomicTermSymbol("2D{1}_3/2") + a1 = AtomicTermSymbol("2D{2}_3/2") + a2 = AtomicTermSymbol("4I{3}") + self.assertEqual(a0.seniority, 1) + self.assertEqual(a1.seniority, 2) + self.assertEqual(a2.seniority, 3) + + self.assertEqual(a0.html, "2D13/2") + self.assertEqual(a0.latex, r"{}^{2}\mathrm{D}1_{3/2}") + self.assertEqual(a1.html, "2D23/2") + self.assertEqual(a1.latex, r"{}^{2}\mathrm{D}2_{3/2}") + self.assertEqual(a2.html, "4I3") + self.assertEqual(a2.latex, r"{}^{4}\mathrm{I}3") + if __name__ == "__main__": unittest.main() diff --git a/tests/test_compound_LS_coupling.py b/tests/test_compound_LS_coupling.py new file mode 100644 index 0000000..0f4629e --- /dev/null +++ b/tests/test_compound_LS_coupling.py @@ -0,0 +1,56 @@ +""" +Unit tests for the Compound LS Coupling module of PyValem +""" + +import unittest + +from pyvalem.states.atomic_configuration import AtomicConfiguration +from pyvalem.states.atomic_term_symbol import AtomicTermSymbol +from pyvalem.states.compound_LS_coupling import CompoundLSCoupling + + +class CompoundLSCouplingTest(unittest.TestCase): + def test_compound_LS_coupling(self): + s0 = CompoundLSCoupling("3d10.4f7(8So)4p6.5d(9Do)6s(8Do)7s") + a0 = AtomicConfiguration("3d10.4f7") + a1 = AtomicConfiguration("4p6.5d") + a2 = AtomicConfiguration("6s") + a3 = AtomicConfiguration("7s") + for i, ac in enumerate((a0, a1, a2, a3)): + self.assertEqual(ac, s0.atomic_configurations[i]) + self.assertEqual( + s0.html, + """3d104f7(8So)4p65d(9Do)6s(8Do)7s""", + ) + self.assertEqual( + s0.latex, + r"3d^{10}4f^{7}({}^{8}\mathrm{S}^o)4p^{6}5d({}^{9}\mathrm{D}^o)6s({}^{8}\mathrm{D}^o)7s", + ) + + s1 = CompoundLSCoupling("4f10(3K{2})6s.6p(1Po)") + t0 = AtomicTermSymbol("3K{2}") + t1 = AtomicTermSymbol("1Po") + for i, ts in enumerate((t0, t1)): + self.assertEqual(ts, s1.terms[i]) + self.assertEqual(s1.terms[0].seniority, 2) + self.assertEqual( + s1.html, + """4f10(3K2)6s6p(1Po)""", + ) + self.assertEqual( + s1.latex, r"4f^{10}({}^{3}\mathrm{K}2)6s6p({}^{1}\mathrm{P}^o)" + ) + + s2 = CompoundLSCoupling("4d9(2P)5d2(3Fo)") + self.assertEqual( + s2.html, + """4d9(2P)5d2(3Fo)""", + ) + self.assertEqual( + s2.latex, r"4d^{9}({}^{2}\mathrm{P})5d^{2}({}^{3}\mathrm{F}^o)" + ) + + s3 = CompoundLSCoupling("5p5(2Po_3/2)6d") + self.assertEqual( + s3.html, """5p5(2Po3/2)6d""" + ) diff --git a/tests/test_stateful_species.py b/tests/test_stateful_species.py index c5f43a2..2396828 100644 --- a/tests/test_stateful_species.py +++ b/tests/test_stateful_species.py @@ -112,6 +112,14 @@ def test_atomic_term_symbols(self): self.assertRaises(StateParseError, StatefulSpecies, "Ti +2 A5D") + def test_compoundLScoupling_species(self): + ss1 = StatefulSpecies("N 2s2.2p2(3P)6d 4F") + self.assertEqual(ss1.formula.formula, "N") + scl = ss1.states[0] + sat = ss1.states[1] + self.assertEqual(scl.html, "2s22p2(3P)6d") + self.assertEqual(sat.html, "4F") + if __name__ == "__main__": unittest.main()