# Copyright (c) 2016 Martin Sicho
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import molpher
import warnings
from molpher.core.ExplorationData import ExplorationData
from molpher.core.MolpherMol import MolpherMol
from molpher.core._utils import shorten_repr
from molpher.core.operations import TraverseOper
from molpher.core.operations.callbacks import TraverseCallback
[docs]class Callback(TraverseCallback):
"""
:param callback: the callable to call every time a molecule is encountered during traversal
:type callback: any callable object with one required parameter
Basic callback class used to traverse the tree with the `ExplorationTree.traverse()` method.
It registers a callable and calls it every time a :term:`morph` is processed.
"""
def __init__(self, callback):
super(Callback, self).__init__()
self._callback = callback
def __call__(self, morph):
"""
This method is called by the C++ code. It
just calls the registered callback for a :term:`morph`
:param morph: morph in the currently processed tree
:type morph: `molpher.swig_wrappers.core.MolpherMol`
"""
morph.__class__ = MolpherMol
self._callback(morph.tree.fetchMol(morph.smiles)) # needs to be done this way (SEGFAULT otherwise, the input parameter seems to be destroyed by SWIG after the call)
[docs]class ExplorationTree(molpher.swig_wrappers.core.ExplorationTree):
"""
This a specialized version of the `molpher.swig_wrappers.core.ExplorationTree` proxy class.
It implements some additional functionality for ease of use from Python.
.. attention:: This class has no constructor defined. Use the :meth:`create`
factory method to obtain instances of this class.
.. seealso:: `molpher.swig_wrappers.core.ExplorationTree`
"""
def __repr__(self):
return shorten_repr(ExplorationTree, self)
def __init__(self):
super(ExplorationTree, self).__init__()
@staticmethod
def _cast_mols(mols):
ret = [ x for x in mols ]
for morph in ret:
morph.__class__ = MolpherMol
return ret
@staticmethod
[docs] def create(tree_data=None, source=None, target=None, callback_class=Callback):
"""
Create an exploration tree.
:param tree_data: the morphing parameters (optional if ``source`` and ``target`` are specified)
:type tree_data: `molpher.swig_wrappers.core.ExplorationData` (or its derived class),
a `dict` of parameters (in the same format as in :class:`~molpher.core.ExplorationData.ExplorationData`'s constructor)
or a path to a :term:`XML template` or a :term:`tree snapshot`
:param source: SMILES of the source molecule or a molecule directly (uses a copy not the instance itself)
:type source: `str` or :class:`~molpher.core.MolpherMol.MolpherMol`
:param target: SMILES of the target molecule
:type target: `str`
:param callback_class: the class to use when making a callback using the :meth:`traverse` method
:type callback_class: any class derived from `Callback`
.. note:: When ``tree_data`` is specified, ``source`` and ``target`` are always ignored.
"""
ret = None
if tree_data and source and target:
warnings.warn(
"Both tree_data and source and target specified. Using the values in parameters..."
, RuntimeWarning
)
if tree_data and (isinstance(tree_data, molpher.wrappers.ExplorationData) or type(tree_data) == str):
ret = super(ExplorationTree, ExplorationTree).create(tree_data)
elif tree_data:
_params = ExplorationData(**tree_data)
ret = super(ExplorationTree, ExplorationTree).create(_params)
elif source and target:
if type(source) == str or type(target) == str:
if type(source) == MolpherMol:
source = source.getSMILES()
if type(target) == MolpherMol:
target = target.getSMILES()
ret = super(ExplorationTree, ExplorationTree).create(source, target)
else:
raise AttributeError('Invalid set of parameters specified.')
if not ret:
raise RuntimeError('No tree initilized.')
ret.callback_class = callback_class
ret.__class__ = ExplorationTree
return ret
@property
def params(self):
"""
A dictionary representing the current :term:`exploration parameters`.
It is possible to assign a new dictionary (or an instance of
the `molpher.swig_wrappers.core.ExplorationData` class)
to update the current parameters.
.. note:: Only parameters defined in the supplied dictionary are changed
and if an instance of `molpher.swig_wrappers.core.ExplorationData`
is supplied only the parameters are read from it (the tree structure
remains the same).
:return: current parameters
:rtype: `dict`
"""
data = ExplorationData(other=self.asData())
return data.param_dict
@params.setter
def params(self, params):
if isinstance(params, molpher.wrappers.ExplorationData):
self.update(params)
else:
new_params = self.params
new_params.update(params)
data = ExplorationData(**new_params)
self.update(data)
@property
def generation_count(self):
"""
:return: Number of :term:`morph generations <morph generation>` connected to the tree so far.
:rtype: `int`
"""
return self.getGenerationCount()
@property
def path_found(self):
"""
:return: `True` if the :term:`target molecule` is present in the tree, `False` otherwise.
:rtype: `bool`
"""
return self.isPathFound()
@property
def leaves(self):
"""
:return: the current leaves of the tree
:rtype: `tuple` of :class:`~molpher.core.MolpherMol.MolpherMol` instances
"""
return tuple(self._cast_mols(self.fetchLeaves()))
@property
def candidates(self):
"""
:return: the :term:`candidate morphs` (:term:`morphs <morph>` generated by a single call to `generateMorphs()`.)
:rtype: `tuple` of :class:`~molpher.core.MolpherMol.MolpherMol` instances
"""
return tuple(self._cast_mols(self.getCandidateMorphs()))
@property
def candidates_mask(self):
"""
A `tuple` of `bool` objects that serve as means of filtering the :term:`candidate morphs`.
Each :term:`morph` in `candidates` has a `bool` variable assigned to it in this `tuple`
-- only morphs with `True` at the appropriate position are added to the tree when :meth:`extend`
is called.
It can be changed by assigning a new `tuple` or a call to `setCandidateMorphsMask()`.
:return: currently selected :term:`candidate morphs` represented as a `tuple` of `bool` objects
:rtype: `tuple`
"""
return self.getCandidateMorphsMask()
@candidates_mask.setter
def candidates_mask(self, mask):
self.setCandidateMorphsMask(mask)
@property
def thread_count(self):
"""
:return: maximum number of threads this instance will use
:rtype: `int`
"""
return self.getThreadCount()
@thread_count.setter
def thread_count(self, val):
self.setThreadCount(val)
[docs] def fetchMol(self, canonSMILES):
"""
Returns a molecule from the tree using a canonical
SMILES string.
Raises a `RuntimeError` if the molecule is not found.
:param canonSMILES: SMILES string of the molecule to fetch
:type canonSMILES: `str`
:return: molecule from a tree
:rtype: :class:`~molpher.core.MolpherMol.MolpherMol`
"""
ret = super(ExplorationTree, self).fetchMol(canonSMILES)
ret.__class__ = MolpherMol
return ret
[docs] def traverse(self, callback, start_mol = None):
"""
This method can be used to traverse the whole tree structure (or just a subtree)
starting from the root to leaves. It takes a callback function that accepts a single required argument
and traverses the whole tree starting from its root (or root of a specified subtree -- see ``start_mol``) and
calls the supplied callback with with encountered morph as its parameter.
:param callback: the callback to call
:type callback: a callable object that takes a single argument
:param start_mol: the root of a subtree to explore as canonical SMILES
or :py:class:`~molpher.core.MolpherMol.MolpherMol` instance
:type start_mol: `str` or :py:class:`~molpher.core.MolpherMol.MolpherMol`
"""
if start_mol and type(start_mol) == MolpherMol:
TraverseOper(self, self.callback_class(callback), start_mol)()
elif start_mol:
mol = self.fetchMol(start_mol)
TraverseOper(self, self.callback_class(callback), mol)()
else:
cb = self.callback_class(callback)
TraverseOper(self, cb)()
[docs] def asData(self):
"""
:return: the tree as an :class:`~molpher.core.ExplorationData.ExplorationData` instance
:rtype: :class:`~molpher.core.ExplorationData.ExplorationData`
"""
return ExplorationData(other=super(ExplorationTree, self).asData())