Due to NGLView (the Python module) having a frozen older IPywidget version it breaks Colab and the major change for the latter library was a year ago (early 2023), so I am forced to revisit old code and switch to py3Dmol in my Colab demos. Today I figured out how to use custom carbon colours.
Given a hex code for a colour (#ππππππ), in PyMOL one can set it to the carbons via color 0xππππππ, element C and πΎπΎ, which is a bit cryptic (0x is for hex numbers): so even with a common program that's a desktop binary and not a JS widget in Python the synthax can be tricky. In py3Dmol it is very tricky. If the color is a CSS built-in, in py3Dmol / 3Dmol.js name+'Carbon' will work. Example:
import py3Dmol
from rdkit import Chem
from rdkit.Chem import AllChem
# A fun 3D molecule (if you don't like the look of a tropane/adamantane ring please go to A&E / ER)
atropine = Chem.MolFromSmiles('CN(C)C(=O)OC1C2CC3CC1CC(C2)(C3)OC(=O)C')
AllChem.EmbedMolecule(atropine)
viewer = py3Dmol.view(width=350, height=350)
viewer.addModel(Chem.MolToMolBlock(atropine), 'sdf')
viewer.setStyle({'model':-1}, {'stick':{'colorscheme':'coralCarbon', 'opacity': 1}})
viewer.zoomTo()
viewer.show()
If a hex code is passed it will not. The 3Dmol.js documentation talks of passing a JS function to Python to define custom colour schemes here. So how does py3Dmol inject JS?
This depends on whether a viewer is displayed or not. There are three string attributes startjs, endjs, and updatejs. The method show calls _make_html, which concatenates startjs and endjs, and adds it via IPython.display.publish_display_data —which is really cool and I only learnt about it this way. The update method does the same but on the updatejs string.
So to inject JS in a yet to be shown viewer one can do:
viewer = py3Dmol.view(width=350, height=350)
viewer.addModel(Chem.MolToMolBlock(atropine), 'sdf')
viewer.startjs += '''\n
let customColorize = function(atom){
// attribute elem is from https://3dmol.csb.pitt.edu/doc/AtomSpec.html#elem
if (atom.elem === 'C'){
return "#00FF00"
}else{
return $3Dmol.getColorFromStyle(atom, {colorscheme: "whiteCarbon"});
}
}
\n'''
viewer.setStyle({'model':-1}, {'stick':{'colorfunc': 'customColorize', 'opacity': 0.7}})
# make it a function not a string "customColorize"
viewer.startjs = viewer.startjs.replace('"customColorize"', 'customColorize')
viewer.zoomTo()
viewer.show()In the above, a trick happens: customColorize is added as a string, but the code is changed to make it a variable. Actually this is not the only case where this happens: the code will have a viewer_UNIQUEID main namespace variable going on, which gets replaced by the value of viewer.uniqueid. Main namespace pollution is frowned upon by purists, but this is very handy for debugging as one can make the dev console pop-up and type window.viewer_πΎπΎπΎπΎπΎ and do whatever test!
Footnote
def display_source(function):
"""
Display the source code formatted
"""
import inspect
from IPython.display import HTML, display
from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import HtmlFormatter
code:str = inspect.getsource(function)
html:str = highlight(code, PythonLexer(), HtmlFormatter(style='colorful'))
stylesheet:str = f""
display(HTML(f"{stylesheet}{html}"))
display_source(type(viewer))

py3Dmol is part of 3Dmol.js repo - you can see the code here: https://github.com/3dmol/3Dmol.js/tree/master/py3Dmol
ReplyDeleteAh. Thank you for pointing it out! (fixed in text)
Delete