Source code for gmso.core.element

"""Representation of the chemical elements."""

import json
import logging
from importlib.resources import as_file, files
from re import sub
from typing import Union

import numpy as np
import unyt as u
from pydantic import ConfigDict, Field, field_serializer

from gmso.abc.gmso_base import GMSOBase
from gmso.abc.serialization_utils import unyt_to_dict
from gmso.exceptions import GMSOError

logger = logging.getLogger(__name__)

exported = [
    "element_by_mass",
    "element_by_symbol",
    "element_by_name",
    "element_by_smarts_string",
    "element_by_atomic_number",
    "element_by_atom_type",
    "Element",
]


[docs] class Element(GMSOBase): """Chemical element object Template to create a chemical element. Properties of the element instance are immutable. All known elements are pre-built and stored internally. """ name: str = Field(..., description="Name of the element.") symbol: str = Field(..., description="Chemical symbol of the element.") atomic_number: int = Field(..., description="Atomic number of the element.") mass: u.unyt_quantity = Field(..., description="Mass of the element.")
[docs] @field_serializer("mass") def serialize_mass(self, mass: Union[u.unyt_quantity, None]): if mass is None: return None else: return unyt_to_dict(mass)
def __repr__(self): """Representation of the element.""" return ( f"<Element: {self.name}, symbol: {self.symbol}, " f"atomic number: {self.atomic_number}, mass: {self.mass.to('amu')}>" ) def __eq__(self, other): if other is self: return True if not isinstance(other, Element): return False return ( self.name == other.name and self.mass == other.mass and self.symbol == other.symbol and self.atomic_number == other.atomic_number ) model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True)
def element_by_symbol(symbol, verbose=False): """Search for an element by its symbol. Look up an element from a list of known elements by symbol. Return None if no match found. Parameters ---------- symbol : str Element symbol to look for, digits and spaces are removed before search. verbose : bool, optional, default=False If True, raise warnings if symbol has been trimmed before search. Returns ------- matched_element : element.Element or None Return an element from the periodic table if the symbol is found, otherwise return None """ symbol_trimmed = sub(r"[0-9 -]", "", symbol).capitalize() if symbol_trimmed != symbol and verbose: msg = ( f"Numbers and spaces are not considered when searching by element symbol.\n" f"{symbol} became {symbol_trimmed}" ) logger.info(msg) matched_element = symbol_dict.get(symbol_trimmed) return matched_element def element_by_name(name, verbose=False): """Search for an element by its name. Look up an element from a list of known elements by name. Return None if no match found. Parameters ---------- name : str Element name to look for, digits and spaces are removed before search. verbose : bool, optional, default=False If True, raise warnings if name has been trimmed before search. Returns ------- matched_element : element.Element or None Return an element from the periodic table if the name is found, otherwise return None """ name_trimmed = sub(r"[0-9 -]", "", name).lower() if name_trimmed != name and verbose: msg = ( "Numbers and spaces are not considered when searching by element name. \n" f"{name} became {name_trimmed}" ) logger.info(msg) matched_element = name_dict.get(name_trimmed) return matched_element def element_by_atomic_number(atomic_number, verbose=False): """Search for an element by its atomic number. Look up an element from a list of known elements by atomic number. Return None if no match found. Parameters ---------- atomic_number : int Element atomic number that need to look for if a string is provided, only numbers are considered during the search. verbose : bool, optional, default=False If True, raise warnings if atomic_number has been trimmed before search. Returns ------- matched_element : element.Element Return an element from the periodic table if we find a match, otherwise raise GMSOError """ if isinstance(atomic_number, str): atomic_number_trimmed = int( sub("[a-z -]", "", atomic_number.lower()).lstrip("0") ) if str(atomic_number_trimmed) != atomic_number and verbose: msg = ( f"Letters and spaces are not considered when searching by element atomic number. \n " f"{atomic_number} became {atomic_number_trimmed}" ) logger.info(msg) else: atomic_number_trimmed = atomic_number matched_element = atomic_dict.get(atomic_number_trimmed) if matched_element is None: raise GMSOError( f"Failed to find an element with atomic number {atomic_number_trimmed}" ) return matched_element def element_by_mass(mass, exact=True, verbose=False): """Search for an element by its mass. Look up an element from a list of known elements by mass. If given mass is an int or a float, it will be convert to a unyt quantity (u.amu). Return None if no match found. Parameters ---------- mass : int, float Element mass that need to look for, if a string is provided, only numbers are considered during the search. Mass unyt is assumed to be u.amu, unless specfied (which will be converted to u.amu). exact : bool, optional, default=True This method can be used to search for an exact mass (up to the first decimal place) or search for an element mass closest to the mass entered. verbose : bool, optional, default=False If True, raise warnings if mass has been trimmed before search. Returns ------- matched_element : element.Element or None Return an element from the periodict table if we find a match, otherwise return None """ if isinstance(mass, str): # Convert to float if a string is provided mass_trimmed = np.round(float(sub(r"[a-z -]", "", mass.lower()))) if str(mass_trimmed) != mass and verbose: msg1 = ( f"Letters and spaces are not considered when searching by element mass.\n" f"{mass} became {mass_trimmed}" ) logger.info(msg1) elif isinstance(mass, u.unyt_quantity): # Convert to u.amu if a unyt_quantity is provided mass_trimmed = np.round(float(mass.to("amu")), 1) else: mass_trimmed = np.round(mass, 1) if exact: # Exact search mode matched_element = mass_dict.get(mass_trimmed) else: # Closest match mode mass_closest = min(mass_dict.keys(), key=lambda k: abs(k - mass_trimmed)) if verbose: msg2 = f"Closest mass to {mass_trimmed}: {mass_closest}" logger.info(msg2) matched_element = mass_dict.get(mass_closest) return matched_element def element_by_smarts_string(smarts_string, verbose=False): """Search for an element by a given SMARTS string. Look up an element from a list of known elements by SMARTS string. Return None if no match found. Parameters ---------- smarts_string : str SMARTS string representation of an atom type or its local chemical context. The Foyer SMARTS parser will be used to find the central atom and look up an Element. Note that this means some SMARTS grammar may not be parsed properly. For details, see https://github.com/mosdef-hub/foyer/issues/63 verbose : bool, optional, default=False If True, raise warnings if smarts_string has been trimmed before search. Returns ------- matched_element : element.Element Return an element from the periodic table if we find a match Raises ------ GMSOError If no matching element is found for the provided smarts string """ from lark import UnexpectedCharacters from gmso.utils.io import import_ foyer = import_("foyer") SMARTS = foyer.smarts.SMARTS PARSER = SMARTS() try: symbols = PARSER.parse(smarts_string).iter_subtrees_topdown() except UnexpectedCharacters: raise GMSOError( f"Failed to find an element from SMARTS string {smarts_string}. " f"The SMARTS string contained unexpected characters." ) first_symbol = None for symbol in symbols: if symbol.data == "atom_symbol": first_symbol = symbol.children[0] break matched_element = None if first_symbol is not None: matched_element = element_by_symbol(first_symbol) if matched_element is None: raise GMSOError( f"Failed to find an element from SMARTS string {smarts_string}. The " f"parser detected a central node with name {first_symbol}" ) return matched_element def element_by_atom_type(atom_type, verbose=False): """Search for an element by a given gmso AtomType object. Look up an element from a list of known elements by atom type. Return None if no match is found. Parameters ---------- atom_type : gmso.core.atom_type.AtomType AtomType object to be parsed for element information. Attributes are looked up in the order of mass, name, and finally definition (the SMARTS string). Because of the loose structure of this class, a successful lookup is not guaranteed. verbose : bool, optional, default=False If True, raise warnings if atom_type has been trimmed before search. Returns ------- matched_element : element.Element or None Return an element from the periodict table if we find a match, otherwise return None """ matched_element = None if matched_element is None and atom_type.mass: matched_element = element_by_mass(atom_type.mass, exact=False, verbose=verbose) if matched_element is None and atom_type.name: matched_element = element_by_symbol(atom_type.name, verbose=verbose) if matched_element is None and atom_type.definition: matched_element = element_by_smarts_string( atom_type.definition, verbose=verbose ) if matched_element is None: raise GMSOError( "Failed to find an element from atom type" f"{atom_type} with " f"properties mass: {atom_type.mass}, name:" f"{atom_type.name}, and " f"definition: {atom_type.definition}" ) return matched_element # Get the JSON file from ele package for a standard representation fn = files("ele") / "lib/elements.json" with as_file(fn) as path: elements_dict = None elements = [] with open(path, "r") as el_json_file: elements_dict = json.load(el_json_file) elements_dict = {int(key): value for key, value in elements_dict.items()} for atom_number, element_properties in elements_dict.items(): assert atom_number == element_properties["atomic number"] elements.append( Element( atomic_number=element_properties["atomic number"], name=element_properties["name"], symbol=element_properties["symbol"], mass=element_properties["mass"] * u.amu, ) ) elements.sort(key=lambda el: el.atomic_number) symbol_dict = {element.symbol: element for element in elements} name_dict = {element.name: element for element in elements} atomic_dict = {element.atomic_number: element for element in elements} mass_dict = { np.round(float(element.mass.to("amu")), 1): element for element in elements } element_by_capitalized_names = { element.name.capitalize(): element for element in elements } def __getattr__(name): """Access an element by the object attribute.""" if name in exported: return globals()[name] elif name in element_by_capitalized_names: return element_by_capitalized_names[name] else: raise AttributeError(f"Module {__name__} has no attribute {name}")