Source code for chemistry_tools.elements.classes

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#  elements.py
#
#  Copyright (c) 2020 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#
#  Based on molmass (https://github.com/cgohlke/molmass)
#  |  Copyright (c) 1990-2020, Christoph Gohlke
#  |  All rights reserved.
#  |  Licensed under the BSD 3-Clause License
#  |  Redistribution and use in source and binary forms, with or without
#  |  modification, are permitted provided that the following conditions are met:
#  |
#  |  1. Redistributions of source code must retain the above copyright notice,
#  |     this list of conditions and the following disclaimer.
#  |
#  |  2. Redistributions in binary form must reproduce the above copyright notice,
#  |     this list of conditions and the following disclaimer in the documentation
#  |     and/or other materials provided with the distribution.
#  |
#  |  3. Neither the name of the copyright holder nor the names of its
#  |     contributors may be used to endorse or promote products derived from
#  |     this software without specific prior written permission.
#  |
#  |  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#  |  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#  |  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#  |  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
#  |  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
#  |  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
#  |  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
#  |  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
#  |  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
#  |  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
#  |  POSSIBILITY OF SUCH DAMAGE.
#  |
#


# stdlib
from functools import lru_cache

# 3rd party
from domdf_python_tools import doctools
from domdf_python_tools.bases import Dictable
from memoized_property import memoized_property

# this package
from . import _table


[docs]class Element(Dictable): """Chemical element. Attributes ---------- number : int Atomic number. symbol : str of length 1 or 2 Chemical symbol. name : str Name in English. group : int Group in periodic table. period : int Period in periodic table. block : int Block in periodic table. series : int Index to chemical series. protons : int Number of protons. neutrons : int Number of neutrons in the most abundant naturally occurring stable. isotope nominalmass : int Mass number of the most abundant naturally occurring stable isotope. electrons : int Number of electrons. mass : float Relative atomic mass. Ratio of the average mass of atoms of the element to 1/12 of the mass of an atom of 12C. exactmass : float Relative atomic mass calculated from the isotopic composition. eleneg : float Electronegativity (Pauling scale). covrad : float Covalent radius in Angstrom. atmrad : Atomic radius in Angstrom. vdwrad : float Van der Waals radius in Angstrom. tboil : float Boiling temperature in K. tmelt : float Melting temperature in K. density : float Density at 295K in g/cm3 respectively g/L. oxistates : str Oxidation states. eleaffin : float Electron affinity in eV. eleconfig : str Ground state electron configuration. eleconfig_dict : dict Ground state electron configuration (shell, subshell): electrons. eleshells : int Number of electrons per shell. ionenergy : tuple Ionization energies in eV isotopes : dict Isotopic composition. keys: isotope mass number values: Isotope(relative atomic mass, abundance) """ def __init__( self, number, symbol, name, group=0, period=0, block='', series=0, mass=0.0, eleneg=0.0, eleaffin=0.0, covrad=0.0, atmrad=0.0, vdwrad=0.0, tboil=0.0, tmelt=0.0, density=0.0, eleconfig='', oxistates='', ionenergy=None, isotopes=None, description='', ): super().__init__() self._number = number self._symbol = symbol self._name = name self._electrons = number self._protons = number self._group = group self._period = period self._block = block self._series = series self._mass = mass self._eleneg = eleneg self._eleaffin = eleaffin self._covrad = covrad self._atmrad = atmrad self._vdwrad = vdwrad self._tboil = tboil self._tmelt = tmelt self._density = density self._eleconfig = eleconfig self._oxistates = oxistates self._description = description if ionenergy is None: self._ionenergy = tuple() else: self._ionenergy = ionenergy self._isotopes = {} if isotopes is not None: for massnumber, isotope in isotopes.items(): if isinstance(isotope, Isotope): self._isotopes[int(massnumber)] = isotope elif isinstance(isotope, (list, tuple)): self._isotopes[int(massnumber)] = Isotope(*isotope, massnumber) def __dict__(self): return dict( number=self._number, symbol=self._symbol, name=self._name, group=self._group, period=self._period, block=self._block, series=self._series, mass=self._mass, eleneg=self._eleneg, eleaffin=self._eleaffin, covrad=self._covrad, atmrad=self._atmrad, vdwrad=self._vdwrad, tboil=self._tboil, tmelt=self._tmelt, density=self._density, eleconfig=self._eleconfig, oxistates=self._oxistates, ionenergy=self._ionenergy, isotopes=self._isotopes, description=self._description ) @memoized_property def number(self): return self._number @memoized_property def symbol(self): return self._symbol @memoized_property def name(self): return self._name @memoized_property def electrons(self): return self._electrons @memoized_property def protons(self): return self._protons @memoized_property def group(self): return self._group @memoized_property def period(self): return self._period @memoized_property def block(self): return self._block @memoized_property def series(self): return self._series @memoized_property def mass(self): return self._mass @memoized_property def molecular_weight(self): return self._mass @memoized_property def eleneg(self): return self._eleneg @memoized_property def eleaffin(self): return self._eleaffin @memoized_property def covrad(self): return self._covrad @memoized_property def atmrad(self): return self._atmrad @memoized_property def vdwrad(self): return self._vdwrad @memoized_property def tboil(self): return self._tboil @memoized_property def tmelt(self): return self._tmelt @memoized_property def density(self): return self._density @memoized_property def eleconfig(self): return self._eleconfig @memoized_property def oxistates(self): return self._oxistates @memoized_property def ionenergy(self): return self._ionenergy @memoized_property def isotopes(self): return self._isotopes @memoized_property def description(self): return self._description def __str__(self): return self.name def __repr__(self): ionenergy = [] for i, j in enumerate(self.ionenergy): if i and (i % 5 == 0): ionenergy.append(f'\n {j}') else: ionenergy.append(f'{j}') ionenergy = ', '.join(ionenergy) if len(self.ionenergy) > 5: ionenergy = f'(\n {ionenergy},\n )' elif len(self.ionenergy) == 1: ionenergy = f'({ionenergy},)' else: ionenergy = f'({ionenergy})' isotopes = [] for massnum in sorted(self.isotopes): iso = self.isotopes[massnum] isotopes.append( '{0}: Isotope({1}, {2}, {0})'.format( massnum, iso.mass, iso.abundance ) ) isotopes = ',\n '.join(isotopes) if len(self.isotopes) > 1: isotopes = f'{{\n {isotopes},\n }},' else: isotopes = f'{{{isotopes}}},' return ',\n '.join(( f"Element(\n {self.number}, '{self.symbol}', '{self.name}'", f"group={self.group}, period={self.period}," f" block='{self.block}', series={self.series}", f"mass={self.mass}, eleneg={self.eleneg}," f" eleaffin={self.eleaffin}", f"covrad={self.covrad}, atmrad={self.atmrad}," f" vdwrad={self.vdwrad}", f"tboil={self.tboil}, tmelt={self.tmelt}, density={self.density}", f"eleconfig='{self.eleconfig}'", f"oxistates='{self.oxistates}'", f"ionenergy={ionenergy}", f"isotopes={isotopes}\n)" )) @memoized_property def nominalmass(self): """ Return mass number of most abundant natural stable isotope. """ nominalmass = 0 maxabundance = 0 for massnum, iso in self.isotopes.items(): if iso.abundance > maxabundance: maxabundance = iso.abundance nominalmass = massnum return nominalmass @memoized_property def neutrons(self): """ Return number neutrons in most abundant natural stable isotope. """ return self.nominalmass - self.protons @memoized_property def exactmass(self): """ Return relative atomic mass calculated from isotopic composition. """ return sum(iso.mass * iso.abundance for iso in self.isotopes.values()) @memoized_property def eleconfig_dict(self): """ Return electron configuration as dict. """ adict = {} if self.eleconfig.startswith('['): base = self.eleconfig.split(' ', 1)[0][1:-1] adict.update(_table.ELEMENTS[base].eleconfig_dict) for e in self.eleconfig.split()[bool(adict):]: adict[(int(e[0]), e[1])] = int(e[2:]) if len(e) > 2 else 1 return adict @memoized_property def eleshells(self): """ Return number of electrons in shell as tuple. """ eleshells = [0, 0, 0, 0, 0, 0, 0] for key, val in self.eleconfig_dict.items(): eleshells[key[0] - 1] += val return tuple(ele for ele in eleshells if ele)
[docs] def validate(self): """Check consistency of data. Raise Error on failure.""" assert self.period in _table.PERIODS assert self.group in _table.GROUPS assert self.block in _table.BLOCKS assert self.series in _table.SERIES if self.number != self.protons: raise ValueError( f'{self.symbol} - atomic number must equal proton number' ) if self.protons != sum(self.eleshells): raise ValueError( f'{self.symbol} - number of protons must equal electrons' ) if len(self.ionenergy) > 1: ionev_ = self.ionenergy[0] for ionev in self.ionenergy[1:]: if ionev <= ionev_: raise ValueError( f'{self.symbol} - ionenergy not increasing' ) ionev_ = ionev mass = 0.0 frac = 0.0 for iso in self.isotopes.values(): mass += iso.abundance * iso.mass frac += iso.abundance if abs(mass - self.mass) > 0.03: raise ValueError( f'{self.symbol} - average of isotope masses ' f'({mass:.4f}) != mass ({self.mass:.4f})' ) if abs(frac - 1.0) > 1e-9: raise ValueError( f'{self.symbol} - sum of isotope abundances != 1.0' )
[docs]class Isotope(Dictable): """Isotope massnumber, relative atomic mass, and abundance.""" def __init__(self, mass=0.0, abundance=1.0, massnumber=0): super().__init__() self._mass = mass self._abundance = abundance self._massnumber = massnumber @memoized_property def mass(self): return self._mass @memoized_property def abundance(self): return self._abundance @memoized_property def massnumber(self): return self._massnumber def __str__(self): return '{}, {:.4f}, {:.6f}%'.format( self.massnumber, self.mass, self.abundance * 100 ) def __repr__(self): return 'Isotope({}, {}, {})'.format( repr(self.mass), repr(self.abundance), repr(self.massnumber) ) def __eq__(self, other): if isinstance(other, self.__class__): return self.__dict__() == other.__dict__() def __dict__(self): return dict( mass=self.mass, abundance=self.abundance, massnumber=self.massnumber, )
# Isotope 0 Key: # mass of the most abundant isotope and 1.0 abundance. # TODO: make frozen
[docs]class Elements: """ Ordered dict of Elements with lookup by number, symbol, and name. """ def __init__(self, *elements): self._list = [] self._dict = {} for element in elements: if element.number > len(self._list) + 1: raise ValueError('Elements must be added in order') if element.number <= len(self._list): self._list[element.number - 1] = element else: self._list.append(element) self._dict[element.number] = element self._dict[element.symbol] = element self.add_alternate_spelling(element, element.name) def __str__(self): return f'[{", ".join(ele.symbol for ele in self._list)}]' def __repr__(self): elements = ',\n '.join( '\n '.join(line for line in repr(element).splitlines()) for element in self._list ) elements = f'Elements(\n {elements},\n)' return elements def __contains__(self, item): return item in self._dict def __iter__(self): return iter(self._list) def __len__(self): return len(self._list) def __getitem__(self, key): if isinstance(key, str): try: return self._dict[key.casefold()] except KeyError: return self._dict[key] elif isinstance(key, int): return self._dict[key] elif isinstance(key, float): return self._dict[int(key)] elif isinstance(key, slice): start, stop, step = key.indices(len(self._list)) return self._list[slice(start - 1, stop - 1, step)] else: try: symbol, isotope = self.split_isotope(key) return self._dict[symbol.capitalize()] except: raise KeyError(f"Unknown key: '{key}'")
[docs] @lru_cache() def split_isotope(self, string): from chemistry_tools.formulae.formula import _split_isotope return _split_isotope(string)
[docs] def add_alternate_spelling(self, element, spelling): self._dict[spelling] = element self._dict[spelling.lower()] = element self._dict[spelling.casefold()] = element
@memoized_property def symbols(self): return [element.symbol for element in sorted(self._list, key=lambda e: e.number)] @memoized_property def names(self): return [str(element) for element in sorted(self._list, key=lambda e: e.number)] @memoized_property def lower_names(self): return [str(element).lower() for element in sorted(self._list, key=lambda e: e.number)]
[docs]@doctools.append_docstring_from(Element) class HeavyHydrogen(Element): """ Subclass of Element to handle the Heavy Hydrogen isotopes Deuterium and Tritium """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.symbol not in {"D", "T"}: raise ValueError("'HeavyHydrogen' can only be used for Deuterium and Tritium") @memoized_property def nominalmass(self): """ Return mass number of most abundant natural stable isotope. """ if self.symbol == "D": return 2 elif self.symbol == "T": return 3 @memoized_property def as_isotope(self): """ Return the isotope in H[X] format :rtype: str """ return f'H[{self.nominalmass}]'