Source code for Chemistry.base.compounds

# Copyright (c) 2014 Dan Obermiller
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# You should have received a copy of the MIT License along with this program.
# If not, see <http://opensource.org/licenses/MIT>


"""This module provides the underlying framework for the package. It provides
the base Compound class that is used throughout to represent molecules, as well
as the abstract class _CompoundWrapper that is used throughout whenever the
Compound class is wrapped (such as by Reactant).  These classes are fundamental
to the rest of the program.
"""


import abc
import json

import networkx as nx

import Chemistry.base.periodic_table as pt


[docs]class Compound(nx.Graph): """A molecule stored in all its glory. Implemented as a graph (subclassing networkx.Graph). Parameters ---------- atoms : dict A dictionary storing all the atoms of a molecule. Should be presented in a form like `{'a1': 'H', 'a2': 'O', 'a3': 'H'}`. Accepted format is "a#" as a key and the atom's atomic symbol as the value. bonds : dict A dictionary storing all the bonds of a molecule. Should be presented in a form like `{'b1': ('a1', 'a2', {'order': 1, 'chirality': None}), 'b2': ('a2', 'a3', {'order': 1, 'chirality': None})}`. Accepted format is "b#" as a key and a three-item tuple as the value. The items at indices 0 and 1 should be the keys of the two atoms being bonded (order doesn't matter here) and the item at index 2 should be a dictionary of relevant information. Information that is not provided will be assigned reasonable default values. The chirality is from index 0 to index 1 of the tuple. other_info : dict, optional. A dictionary that stores all other relevant information about a molecule. Things like molecular charge, pka, the name/id of the molecule, etc. If no information is provided the constructor will attempt to ascertain any information it needs. """ @classmethod
[docs] def json_serialize(cls, obj, as_str=False): """Serializes an object for json, used for __str__ and __repr__ Parameters ---------- obj : Object The object to be serialized. Can be anything, although this function is largely intended to work with Compound objects as_str : bool, optional Specifies whether `repr` or `str` will be used to generate the strings """ d = {} try: if isinstance(obj, Compound): for key, value in vars(obj).iteritems(): if key in ['atoms', 'bonds', 'other_info']: d.update({key: value}) return d else: return vars(obj) except AttributeError: stringify_function = (str if as_str else repr) return map(stringify_function, obj)
@classmethod
[docs] def node_matcher(cls, node1, node2): """Helper function to check for isomorphic graphs Parameters ---------- node1 : Object The first node evaluated when checking graph isomorphism node2 : Object The second node evaluated when checking graph isomorphism Returns ------- bool Whether or not the nodes can be considered equivalent """ return node1['symbol'] == node2['symbol']
@classmethod
[docs] def edge_matcher(cls, edge1, edge2): """Helper function to check for isomorphic graphs Parameters ---------- edge1 : Object The first edge evaluated when checking graph isomorphism edge2 : Object The second edge evalutated when checking graph isomorphism """ return edge1 == edge2
def __init__(self, atoms, bonds, other_info={}): super(Compound, self).__init__() self.atoms = {} self.bonds = {} self._add_nodes_from(atoms) self._add_edges_from(bonds) self.other_info = other_info self.graph.update(self.other_info) self.molecule = {'other_info': self.other_info, 'atoms': self.atoms, 'bonds': self.bonds} def _add_nodes_from(self, atoms): """Adds a group of nodes Parameters ---------- atoms : dict The atoms that are being added to the molecule """ for key, atom in atoms.iteritems(): self._add_node(key, pt.get_element(atom)) def _add_edges_from(self, bonds): """Adds a group of edges Parameters ---------- bonds : dict The bonds that are being added to the molecule """ for id_, bond in bonds.iteritems(): self._add_edge(id_, *bond) def _add_node(self, key, atom): """Adds a single node. Parameters ---------- key : string The key associated with the given atom. atom : dict Dictionary representing the atom. """ if key in self.atoms: raise KeyError("There is already an atom {}".format(key)) self.add_node(key, **atom) self.atoms[key] = atom['symbol'] def _add_edge(self, key, first, second, rest=None): """Adds a single edge. Parameters ---------- key : string The key associated with the given atom. first : string The key of one of the atoms in the bond second : string The key of the other atom in the bond rest : dict, optional Other information relevant to the bond. """ if rest is None: rest = {} try: _ = self.bonds[key] except KeyError: d = {'order': 1, 'chirality': None} d.update(rest) self.add_edge(first, second, key=key, **d) self.bonds[key] = first, second, d else: raise KeyError("There is already a bond {}".format(key)) def __str__(self): return json.dumps(Compound.json_serialize(self, as_str=True), sort_keys=True, indent=4) def __repr__(self): return json.dumps(Compound.json_serialize(self), sort_keys=True, indent=4)
[docs] def is_isomorphic(self, other): """Determines whether or not a molecule is isomorphically equivalent to another. Parameters ---------- other : Compound, _CompoundWrapper The Compound that is being check for isomorphism. """ return nx.is_isomorphic(self, other, node_match=Compound.node_matcher, edge_match=Compound.edge_matcher)
def __eq__(self, other): return self.is_isomorphic(other) def __ne__(self, other): return not self.is_isomorphic(other)
class _CompoundWrapper(object): """Abstract base class for compound wrapping classes. Parameters ---------- compound : Compound The compound being wrapped. Attributes ---------- compound Notes ----- This class exists to be subclassed by other classes, such as Chemistry.base.reactants.Reactant, or Chemistry.base.products.Product. This is easier than creating a brand new Compound object whenever I want to analyze a molecule as an Acid, or a Base, or some other reactant or product. """ __metaclass__ = abc.ABCMeta _compound = None def __init__(self, compound): self.compound = compound @property def compound(self): """The underlying compound object that is being wrapped.""" return self._compound @compound.setter def compound(self, comp): self._compound = comp def __getattr__(self, attr): return getattr(self.compound, attr) def __eq__(self, other): if hasattr(other, 'compound'): return self.compound == other.compound else: return self.compound == other def __str__(self): return str(self.compound) def __repr__(self): return str(self) def __len__(self): return len(self.compound) def __getitem__(self, key): return self.compound[key]