matteoferla/Fragmenstein

View on GitHub
fragmenstein/mol3d_display.py

Summary

Maintainability
A
2 hrs
Test Coverage
"""
Due to the way ``py3Dmol`` works, I have to monkey patch it to add some functionality.
Namely, the ``py3Dmol.view.addModel`` is actually a __getattr__ call wrapping the JS,
which is not obeyed by super() and thus cannot be overridden by subclassing.

Therefore, I have to monkey patch it.

... code-block:: python

    viewer = py3Dmol.view()
    viewer.monkey_patch()
    viewer.add_template(vicky.apo_pdbblock)
    viewer.add_mol(vicky.hits[0], name='hit', carbon_color='cyan')
    viewer.add_mol(vicky.minimized_mol, name=vicky.long_name, colorscheme='coralCarbon')
    viewer.show()

carbon_color can be None (default), a CSS color name, or a hex color.
"""

# py3Dmol
import py3Dmol
from types import MethodType
from rdkit import Chem
from rdkit.Chem import AllChem
from typing import List, Optional

# https://3dmol.csb.pitt.edu/doc/global.html#:~:text=as%20the%20colorscheme-,Properties,-Name
py3Dmol.view.color_names = ['default',
                            'greenCarbon',
                            'cyanCarbon',
                            'magentaCarbon',
                            'purpleCarbon',
                            'whiteCarbon',
                            'orangeCarbon',
                            'yellowCarbon',
                            'blueCarbon']


def _add_mol(self, mol, name=None, carbon_color: Optional[str] = None, opacity=1., **kwargs):
    """
    Whereas there is ``IPythonConsole.addMolToView(caffeine, view)`` in rdkit using py3Dmol,
    I want more.

    :param mol:
    :param name:
    :param colorscheme:
    :param opacity:
    :param kwargs:
    :return:
    """
    if name is not None:
        pass
    elif mol.HasProp('_Name'):
        name: str = mol.GetProp('_Name')
    else:
        raise ValueError('No name provided and mol has no _Name property (title)')
    if name in self.model_addresses:
        raise ValueError(f'{name} is already present in the viewer')
    molblock: str = Chem.MolToMolBlock(mol)
    self.add_model(molblock, 'mol', name=name, **kwargs)
    # `.setStyle({'stick':{}})` will work, but I wanted the args to be explicit as I forget them.
    if carbon_color == 'default':
        self.setStyle({'model': -1, }, {'stick': {'colorscheme': 'default', 'opacity': opacity}})
    elif not isinstance(carbon_color, str):
        raise ValueError(f'No idea what is carbon_color={carbon_color}')
    elif 'Carbon' in carbon_color:
        self.setStyle({'model': -1, }, {'stick': {'colorscheme': carbon_color, 'opacity': opacity}})
    elif '#' not in carbon_color:
        self.setStyle({'model': -1, }, {'stick': {'colorscheme': f'{carbon_color}Carbon', 'opacity': opacity}})
    elif carbon_color == 'prop' or carbon_color is None:
        color = mol.GetProp('color')
        self.setStyle({'model': -1, }, {'stick': {'colorscheme': f'{color}Carbon', 'opacity': opacity}})
    else:
        # this will not work on an update which runs off updatejs
        self.startjs += f'''\n
    let customColorizeFor{name} = function(atom){{
          // attribute elem is from https://3dmol.csb.pitt.edu/doc/AtomSpec.html#elem
          if (atom.elem === 'C'){{
              return "{carbon_color}"
          }}else{{
              return $3Dmol.getColorFromStyle(atom, {{colorscheme: "whiteCarbon"}});
          }}
      }}\n'''
        self.setStyle({'model': -1}, {'stick': {'colorfunc': 'customColorize', 'opacity': opacity}})
        # make it a function not a string "customColorize"
        self.startjs = self.startjs.replace('"customColorize"', f'customColorizeFor{name}')


def _add_template(self, pdbblock: str, name='template', colorscheme='whiteCarbon'):
    self.add_model(pdbblock, 'PDB', name=name)
    self.setStyle({'model': -1}, {'cartoon': {'colorscheme': colorscheme, 'opacity': 1}})
    self.zoomTo()


def _get_idx(self, name):
    return self.model_addresses.index(name)


def _add_model(self, *args, name='unknown', **kwargs):
    self.addModel(*args, **kwargs)
    self.model_addresses.append(name)


def monkey_patch(viewer: py3Dmol.view):
    viewer.model_addresses: List[str] = []
    viewer.add_model = _add_model.__get__(viewer, py3Dmol.view)
    viewer.add_mol = _add_mol.__get__(viewer, py3Dmol.view)
    viewer.get_idx = _get_idx.__get__(viewer, py3Dmol.view)
    viewer.add_template = _add_template.__get__(viewer, py3Dmol.view)

py3Dmol.view.monkey_patch = monkey_patch

patched_3Dmol_view = py3Dmol.view