Source code for qcelemental.molparse.nucleus

import re
from functools import lru_cache
from typing import List, Tuple

from ..exceptions import NotAnElementError, ValidationError
from ..periodic_table import periodictable
from .regex import NUCLEUS

_nucleus = re.compile(r"\A" + NUCLEUS + r"\Z", re.IGNORECASE | re.VERBOSE)


[docs]@lru_cache(maxsize=512) def reconcile_nucleus( A: int = None, Z: int = None, E: str = None, mass: float = None, real: bool = None, label: str = None, speclabel: bool = True, nonphysical: bool = False, mtol: float = 1.0e-3, verbose: int = 1, ) -> Tuple[int, int, str, float, bool, str]: r"""Forms consistent set of nucleus descriptors from all information from arguments, supplemented by the periodic table. At the least, must provide element identity somehow. Defaults to most-abundant isotope. Parameters ---------- A Mass number, number of protons and neutrons. Z Atomic number, number of protons. E Element symbol from periodic table. mass Atomic mass [u]. real Whether real or ghost/absent. label Atom label according to :py:data:`qcelemental.molparse.regex.NUCLEUS`. speclabel If `True`, interpret `label` as potentially full nucleus spec including ghosting, isotope, mass, tagging information, e.g., ``@13C_mine`` or ``He4@4.01``. If `False`, interpret `label` as only the user/tagging extension to nucleus label, e.g. ``_mine`` or ``4`` in the previous examples. nonphysical When `True`, turns off sanity checking that prevents periodic table violations (e.g, light uranium: ``1U@1.007``). mtol How different `mass` can be from a known nuclide mass and still merit the mass number assignment. Note that for elements dominated by a single isotope, the default may not be tight enough to prevent standard atomic weight (abundance-average of isotopes) from being labeled as the dominant isotope for A. verbose Quantity of printing. Returns ------- A, Z, E, mass, real, userlabel : int, int, str, float, bool, str mass number, unless clues don't point to a known nuclide, in which case `-1`. atomic number. element symbol, capitalized. mass value [u]. real/ghost. user portion of `label` if present, else ''. Raises ------ qcelemental.NotAnElementError qcelemental.ValidationError Examples -------- >>> reconcile_nucleus(E='co') >>> reconcile_nucleus(Z=27) >>> reconcile_nucleus(A=59, Z=27) >>> reconcile_nucleus(E='cO', mass=58.933195048) >>> reconcile_nucleus(A=59, Z=27, E='CO') >>> reconcile_nucleus(A=59, E='cO', mass=58.933195048) >>> reconcile_nucleus(label='co') >>> reconcile_nucleus(label='59co') >>> reconcile_nucleus(label='co@58.933195048') >>> reconcile_nucleus(A=59, Z=27, E='cO', mass=58.933195048, label='co@58.933195048') >>> reconcile_nucleus(A=59, Z=27, E='cO', mass=58.933195048, label='27@58.933195048') >>> reconcile_nucleus(label='27') 59, 27, 'Co', 58.933195048, True, '' >>> reconcile_nucleus(label='co_miNe') >>> reconcile_nucleus(label='co_mIne@58.933195048') 59, 27, 'Co', 58.933195048, True, '_mine' >>> reconcile_nucleus(E='cO', mass=58.933) >>> reconcile_nucleus(label='cO@58.933') 59, 27, 'Co', 58.933, True, '' >>> assert 59, 27, 'Co', 58.933, True, '' == reconcile_nucleus(E='cO', mass=58.933, mtol=1.e-4)) AssertionError >>> reconcile_nucleus(E='Co', A=60) >>> reconcile_nucleus(Z=27, A=60, real=True) >>> reconcile_nucleus(E='Co', A=60) >>> reconcile_nucleus(Z=27, mass=59.933817059) >>> reconcile_nucleus(A=60, Z=27, mass=59.933817059) >>> reconcile_nucleus(label='60Co') >>> reconcile_nucleus(label='27', mass=59.933817059) >>> reconcile_nucleus(label='Co', mass=59.933817059) >>> reconcile_nucleus(A=60, label='Co') 60, 27, 'Co', 59.933817059, True, '' >>> reconcile_nucleus(E='Co', A=60, real=False)) >>> reconcile_nucleus(A=60, Z=27, mass=59.933817059, real=0)) >>> reconcile_nucleus(label='@60Co')) >>> reconcile_nucleus(label='Gh(27)', mass=59.933817059)) >>> reconcile_nucleus(label='@Co', mass=59.933817059)) >>> reconcile_nucleus(A=60, label='Gh(Co)')) 60, 27, 'Co', 59.933817059, False, '' >>> reconcile_nucleus(Z=27, mass=200, nonphysical=True) 60, 27, 'Co', 200.00000000, True, '' >>> reconcile_nucleus(mass=60.6, Z=27) >>> reconcile_nucleus(mass=60.6, E='Co') >>> reconcile_nucleus(mass=60.6, label='27') >>> reconcile_nucleus(label='Co@60.6') -1, 27, 'Co', 60.6, True, '' >>> reconcile_nucleus(mass=60.6, Z=27, A=61)) >>> reconcile_nucleus(A=80, Z=27) >>> reconcile_nucleus(Z=27, mass=200) >>> reconcile_nucleus(Z=27, mass=-200, nonphysical=True) >>> reconcile_nucleus(Z=-27, mass=200, nonphysical=True) >>> reconcile_nucleus(Z=1, label='he') >>> reconcile_nucleus(A=4, label='3he') >>> reconcile_nucleus(label='@U', real=True) >>> reconcile_nucleus(label='U', real=False) ValidationError """ # <<< define functions log_text = verbose >= 2 def reconcile(exact, tests, feature): """Returns a member from `exact` that passes all `tests`, else raises error for `feature`.""" for candidate in exact: assessment = [fn(candidate) for fn in tests] if log_text: text.append( """Assess {} candidate {}: {} --> {}""".format(feature, candidate, assessment, all(assessment)) ) if all(assessment): return candidate err = """Inconsistent or unspecified {}: A: {}, Z: {}, E: {}, mass: {}, real: {}, label: {}""".format( feature, A, Z, E, mass, real, label ) if verbose > -1: print("\n\n" + "\n".join(text)) raise ValidationError(err) def offer_element_symbol(e): """Given an element, what can be suggested and asserted about Z, A, mass?""" _Z = periodictable.to_Z(e, strict=True) offer_atomic_number(_Z) def offer_atomic_number(z): """Given an atomic number, what can be suggested and asserted about Z, A, mass?""" z = int(z) z_symbol = periodictable.to_E(z) z_mass = periodictable.to_mass(z) z_a = periodictable.to_A(z) z_a2mass = periodictable._el2a2mass[z_symbol] z_a2mass_min = min(z_a2mass.keys()) z_a2mass_max = max(z_a2mass.keys()) z_mass2a_min = min(z_a2mass.values()) z_mass2a_max = max(z_a2mass.values()) Z_exact.append(z) Z_range.append(lambda x, z=z: x == z) A_exact.append(z_a) if nonphysical: A_range.append(lambda x: x == -1 or x >= 1) if log_text: text.append("""For A, input Z: {}, requires 1 < A or -1, nonphysical""".format(z)) else: A_range.append(lambda x, amin=z_a2mass_min, amax=z_a2mass_max: x == -1 or (x >= amin and x <= amax)) if log_text: text.append( """For A, input Z: {} requires {} < A < {} or -1, the known mass number range for element""".format( z, z_a2mass_min, z_a2mass_max ) ) m_exact.append(z_mass) if nonphysical: m_range.append(lambda x: x > 0.5) if log_text: text.append("""For mass, input Z: {}, requires 0.5 < mass, nonphysical""".format(z)) else: m_range.append(lambda x, mmin=z_mass2a_min, mmax=z_mass2a_max: x >= mmin - mmtol and x <= mmax + mmtol) if log_text: text.append( """For mass, input Z: {} requires {} < mass < {} +/-{}, the known mass range for element""".format( z, z_mass2a_min, z_mass2a_max, mmtol ) ) def offer_mass_number(z, a): """Given a mass number and element, what can be suggested and asserted about A, mass?""" a = int(a) a_eliso = periodictable.to_E(z) + str(a) a_mass = periodictable.to_mass(a_eliso) A_exact.append(a) A_range.append(lambda x, a=a: x == a) if log_text: text.append("""For A, inputs Z: {}, A: {} require A == {}.""".format(z, a, a)) m_exact.append(a_mass) m_range.append(lambda x, a_mass=a_mass: abs(x - a_mass) < mtol) if log_text: text.append("""For mass, inputs Z: {}, A: {} require abs(mass - {}) < {}""".format(z, a, a_mass, mtol)) def offer_mass_value(z, m): """Given a mass and element, what can be suggested and asserted about A, mass?""" m = float(m) m_a = int(round(m, 0)) m_eliso = periodictable.to_E(z) + str(m_a) try: if abs(periodictable.to_mass(m_eliso) - m) > mtol: # only offer A if known nuclide. C@12.4 != 12C m_a = -1 except NotAnElementError: m_a = -1 A_exact.append(m_a) A_range.append(lambda x, m_a=m_a: x == m_a) if log_text: text.append("""For A, inputs Z: {}, m: {} require A == {}""".format(z, m, m_a)) m_exact.append(m) m_range.append(lambda x, m=m: x == m) if log_text: text.append("""For mass, inputs Z: {}, m: {} require m == {}""".format(z, m, m)) def offer_reality(rgh): r_exact.append(rgh) r_range.append(lambda x, rgh=rgh: x == rgh) if log_text: text.append("""For real/ghost, input rgh: {} requires rgh == {}""".format(rgh, rgh)) def offer_user_label(lbl): lbl = str(lbl).lower() l_exact.append(lbl) l_range.append(lambda x, lbl=lbl: x == lbl) if log_text: text.append("""For user label, input lbl: {} requires lbl == {}""".format(lbl, lbl)) # <<< initialize text = ["", """--> Inp: A={}, Z={}, E={}, mass={}, real={}, label={}""".format(A, Z, E, mass, real, label)] Z_exact: List = [] # *_exact are candidates for the final value Z_range: List = [] # *_range are tests that the final value must pass to be valid A_exact: List = [] A_range: List = [] m_exact: List = [] m_range: List = [] r_exact = [True] # default real/ghost is real r_range: List = [] l_exact = [""] # default user label is empty string l_range: List = [] mmtol = 0.5 # tolerance for mass outside known masses for element # <<< collect evidence for Z/A/m, then reconcile Z if Z is not None: offer_atomic_number(Z) if E is not None: offer_element_symbol(E) if label is not None and speclabel is True: lbl_A, lbl_Z, lbl_E, lbl_mass, lbl_real, lbl_user = parse_nucleus_label(label) if lbl_Z is not None: offer_atomic_number(lbl_Z) if lbl_E is not None: offer_element_symbol(lbl_E) Z_final = reconcile(Z_exact, Z_range, "atomic number") E_final = periodictable.to_E(Z_final) # <<< collect more evidence for A/m, then reconcile them if A is not None: offer_mass_number(Z_final, A) if mass is not None: offer_mass_value(Z_final, mass) if real is not None: offer_reality(real) if label is not None: if speclabel: offer_reality(lbl_real) if lbl_A is not None: offer_mass_number(Z_final, lbl_A) if lbl_mass is not None: offer_mass_value(Z_final, lbl_mass) if lbl_user is not None: offer_user_label(lbl_user) else: offer_user_label(label) mass_final = reconcile(m_exact, m_range, "mass") A_final = reconcile(A_exact, A_range, "mass number") real_final = reconcile(r_exact, r_range, "real/ghost") user_final = reconcile(l_exact, l_range, "user label") if log_text: text.append( """<-- Out: A={}, Z={}, E={}, mass={}, real={}, user={}""".format( A_final, Z_final, E_final, mass_final, real_final, user_final ) ) if log_text: print("\n".join(text)) return (A_final, Z_final, E_final, mass_final, real_final, user_final)
[docs]def parse_nucleus_label(label: str): r"""Separate molecule nucleus string into fields. Parameters ---------- label Conveys at least element and ghostedness and possibly isotope, mass, and user info in accordance with :py:data:`qcelemental.molparse.regex.NUCLEUS`. Returns ------- A, Z, E, mass, real, user : int or None, int or None, str or None, float or None, bool, str or None Field breakdown of `label`. Raises ------ qcelemental.ValidationError If `label` does not match NUCLEUS. Examples -------- >>> parse_nucleus_label('@ca_miNe') None, None, 'ca', None False, '_miNe' >>> parse_nucleus_label('Gh(Ca_mine)') None, None, 'Ca', None '_mine', False >>> parse_nucleus_label('@Ca_mine@1.07') None, None, 'Ca', 1.07 False, '_mine' >>> parse_nucleus_label('Gh(cA_MINE@1.07)') None, None, 'cA', 1.07 False, '_MINE' >>> parse_nucleus_label('@40Ca_mine@1.07') 40, None, 'Ca', 1.07 False, '_mine' >>> parse_nucleus_label('Gh(40Ca_mine@1.07)') 40, None, 'Ca', 1.07 False, '_mine' >>> parse_nucleus_label('444lu333@4.0') 444, None, 'lu', 4.0 True, '333' >>> parse_nucleus_label('@444lu333@4.4') 444, None, 'lu', 4.4 False, '333' >>> parse_nucleus_label('8i') 8, None, 'i', None True, None >>> parse_nucleus_label('53_mI4') None, 53, None, None True, '_mI4' >>> parse_nucleus_label('@5_MINEs3@4.4') None, 5, None, 4.4 False, '_MINEs3' >>> parse_nucleus_label('Gh(555_mines3@0.1)') None, 555, None, 0.1 False, '_mines3' """ matchobj = _nucleus.match(label) if matchobj: real = not (matchobj.group("gh1") or matchobj.group("gh2")) if matchobj.group("A"): A = int(matchobj.group("A")) else: A = None if matchobj.group("Z"): Z = int(matchobj.group("Z")) else: Z = None E = matchobj.group("E") if matchobj.group("user1"): user = matchobj.group("user1") elif matchobj.group("user2"): user = matchobj.group("user2") else: user = None if matchobj.group("mass"): mass = float(matchobj.group("mass")) else: mass = None else: raise ValidationError("""Nucleus label is not parseable: {}""".format(label)) return A, Z, E, mass, real, user