"""
2D structures drawing
"""
# Copyright Schrodinger, LLC. All rights reserved.
from past.utils import old_div
import numpy
from rdkit import Chem
from rdkit import Geometry
import schrodinger.application.canvas.utils as canvasutils
from schrodinger import get_maestro
from schrodinger import structure
from schrodinger.infra import canvas
from schrodinger.infra import canvas2d
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.structutils import analyze
from schrodinger.thirdparty import rdkit_adapter
from schrodinger.ui import sketcher
from . import swidgets
if not canvas.ChmLicenseShared_isValid():
license = canvasutils.get_license(canvasutils.LICENSE_SHARED)
CONNECTION_COLORS = \
[[238, 162, 173], [191, 62, 255], [78, 238, 148], [192, 192, 192], [72, 209, 204],\
[255, 193, 37], [197, 193, 170], [202, 225, 255], [113, 113, 198], [238, 238, 0]]
maestro = get_maestro()
MAX_LIGAND_ATOMS = 200
[docs]def get_qpicture_protected(renderer, chmmol, gen_coord=True):
"""
Generate a QPicture for the given molecule. If the picture couldn't be
generated (e.g. the molecule is too large), then a QPicture will contain
the failure message text.
:type renderer: Chm2DRenderer intance
:param renderer: The renderer to use for generating the QPicture
:type chmmol: ChmMol instance.
:param chmmol: Structure to generate the picture for.
:type gen_coord: bool
:param gen_coord: if True (default) generate new coordinates;
if False, use existing 2D coordinates.
:rtype: QPicture
:return: The generated picture.
"""
try:
pic = renderer.getQPicture(chmmol, gen_coord)
except Exception as e:
if str(e) == "unknown": # Canvas raises 'unknown' as Exception
pic = QtGui.QPicture()
painter = QtGui.QPainter(pic)
txt = "Failed to render"
brect = painter.drawText(0, 0, 200, 200, QtCore.Qt.AlignCenter, txt)
painter.end()
pic.setBoundingRect(brect)
else:
raise e
return pic
[docs]def generate_qimage_from_chmmol(chmmol,
width,
height,
renderer=None,
bg_color=None,
max_scale=None,
gen_coord=True):
"""
"Generate a 2D image in QImage format of a ChmMol molecule."
:type chmmol: ChmMol instance.
:param chmmol: Structure to generate the picture for.
:type width: int
:param width: width in pixels of the generated QImage.
:type height: int
:param height: height in pixels of the generated QImage.
:type renderer: Chm2DRenderer intance or None
:param renderer: The renderer to use for generating the QImage.
If None, a new renderer will be created.
:type bg_color: PyQt5.QtGui.QColor or None
:param bg_color: background color filling the image around
the scaled molecule.
:type max_scale: float or None
:param max_scale: maximum scaling of the structure image.
:type gen_coord: bool
:param gen_coord: if True (default) generate new coordinates;
if False, use existing 2D coordinates.
:rtype: QImage
:return: The generated QImage.
"""
if renderer is None:
renderer = canvas2d.Chm2DRenderer(canvas2d.ChmRender2DModel())
pic = get_qpicture_protected(renderer, chmmol, gen_coord)
rect = QtCore.QRect(0, 0, width, height)
if bg_color is None:
model = renderer.getModel()
bg_color = QtGui.QColor(model.getBackgroundColor())
qimg = QtGui.QImage(QtCore.QSize(width, height), QtGui.QImage.Format_ARGB32)
with QtGui.QPainter(qimg) as painter:
painter.fillRect(0, 0, width, height, bg_color)
swidgets.draw_picture_into_rect(painter, pic, rect, max_scale)
return qimg
[docs]def generate_qimage_from_structure(st,
width,
height,
renderer=None,
bg_color=None,
max_scale=None,
stereo_mode=canvas2d.ChmMmctAdaptor.
StereoFromAnnotationAndGeometry_Safe,
gen_coord=True):
"""
Generate a 2D image in QImage format of a schrodinger.structure.Structure.
An intermediate ChmMol will be generated with stereochemstry deduced
according to the stereo_mode parameter.
:type st: Structure instance.
:param st: Structure to generate the picture for.
:type width: int
:param width: width in pixels of the generated QImage.
:type height: int
:param height: height in pixels of the generated QImage.
:type renderer: Chm2DRenderer intance or None
:param renderer: The renderer to use for generating the QImage.
:type bg_color: PyQt5.QtGui.QColor or None
:param bg_color: background color filling the image around the scaled molecule.
:type max_scale: float or None
:param max_scale: maximum scaling of the structure image.
:type stereo_mode: schrodinger.infra._canvas2d.ChmMmctAdaptor.StereoType
:param stereo_mode: stereo source for internal transformation to ChmMol.
:type gen_coord: bool
:param gen_coord: if True generate coordinates for chmmol.
:rtype: QImage
:return: The generated QImage.
"""
if not isinstance(st, structure.Structure):
raise TypeError(
"'st' parameter must be a structure.Structure instance.")
adaptor = canvas2d.ChmMmctAdaptor()
chmmol = adaptor.create(st.handle, stereo_mode,
canvas2d.ChmAtomOption.H_ExplicitOnly, False)
return generate_qimage_from_chmmol(chmmol, width, height, renderer,
bg_color, max_scale, gen_coord)
[docs]def get_qpicture_highlight(renderer,
chmmol,
atoms,
bonds,
color,
gen_coord=False):
"""
Generate a QPicture for the given molecule and highlight given atoms and
bonds. If the picture couldn't be generated (e.g. the molecule is too
large), then a QPicture will contain the failure message text.
:type renderer: Chm2DRenderer intance
:param renderer: The renderer to use for generating the QPicture
:type chmmol: ChmMol instance.
:param chmmol: Structure to generate the picture for.
:type atoms: list
:param atoms: list of atoms that should be highlighted
:type bonds: list
:param bonds: list of bonds that should be highlighted
:type color: `QtGui.QColor`
:param color: color that is used to highlight atoms and bonds
:type gen_coord: bool
:param gen_coord: if True generate coordinates.
:rtype: QPicture
:return: The generated picture.
"""
try:
pic = renderer.getQPicture(chmmol, atoms, bonds, color, gen_coord)
except Exception as e:
if str(e) == "unknown": # Canvas raises 'unknown' as Exception
pic = QtGui.QPicture()
painter = QtGui.QPainter(pic)
txt = "Failed to render"
brect = painter.drawText(0, 0, 200, 200, QtCore.Qt.AlignCenter, txt)
painter.end()
pic.setBoundingRect(brect)
else:
raise e
return pic
[docs]def get_chmmol_bonds_from_atoms(chmmol, atoms):
"""
This function returns a list of bonds that connect atoms in a given
list.
:param chmmol: molecule structure
:type chmmol: `canvas2d.ChmMol`
:param atoms: list of atom indices
:type atoms: list
"""
# FIXME: This is taken from
# schrodinger.application.desmond.fep_scholar_util
# per PANEL-7475
atoms = set(atoms)
core_bonds = []
swigchmmol = canvas2d.convertChmMoltoSWIG(chmmol)
bonds = swigchmmol.getBonds(True)
for i, bond in enumerate(bonds):
a1, a2 = bond.atom1(), bond.atom2()
if a1.getMolIndex() in atoms and a2.getMolIndex() in atoms:
core_bonds.append(i)
return core_bonds
[docs]def get_rdmol_for_2d_rendering(st):
"""
Generate a RDKit molecule from schrodinger structure object. Also modify
the newly generated RDKit molecule to make it suitable for 2D rendering.
:param st: structure object
:type st: structure.Structure
:return: RDKit molecule
:rtype: Chem.rdchem.Mol
"""
# `implicitH` as True speeds up the structure to RDKit molecule
# conversion significantly (especially for complicated molecules on
# which MCS calculation can hang).
rdmol = rdkit_adapter.to_rdkit(st, sanitize=False, implicitH=True)
# We do custom sanitation here in order to handle molecules with bad
# valences, yet still be able to properly handle aromatic bonds.
Chem.rdmolops.SanitizeMol(rdmol, Chem.rdmolops.SANITIZE_ALL)
scale = sketcher.THREE_D_COORDINATES_CONVERSION_FACTOR
conformer = rdmol.GetConformer()
coordinates_map = {}
for atom_idx in range(rdmol.GetNumAtoms()):
coords = Geometry.Point2D(conformer.GetAtomPosition(atom_idx))
# invert y-axis since in 2D system axis should be oriented downwards
coords.y = -coords.y
# scaling is done to convert 3D coordinates (angstrom) into 2D
# coordinates (pixels)
coordinates_map[atom_idx] = coords * scale
coords_gen_params = Chem.rdCoordGen.CoordGenParams()
coords_gen_params.SetCoordMap(coordinates_map)
Chem.rdCoordGen.AddCoords(rdmol, coords_gen_params)
return rdmol
[docs]def get_st_image_using_sketcher(st, width=200, height=200):
"""
:param st: structure object
:type st: structure.Structure
:param width: image width
:type width: int
:param height: image height
:type height: int
:return: 2d structure image rendered using sketcher
:rtype: QtGui.QImage
"""
renderer = sketcher.Renderer()
settings = sketcher.RendererSettings()
settings.width = width
settings.height = height
renderer.loadSettings(settings)
renderer.loadStructure(st)
return renderer.getImage()
[docs]def get_aligned_pictures(sts, renderer=None, atomTyping=11, core_color=None):
"""
Calculate the maximum common substructure (MCS) between the given ligands,
and generate 2D images, aligned by the core. If no MCS was detected, the
images will be unaligned.
NOTE: This function becomes exponentioally slow with larger number of
structures. Recommened maximum around 30 structures.
:type sts: Iterable of `structure.Structure` objects
:param sts: Structures to average
:type renderer: Chm2DRenderer intance
:param renderer: The renderer to use for generating the QPicture (optional)
:type atomTyping: int
:param atomTyping: Atom typing scheme to use. Default is 11. For list of
available schemes, see $SCHRODINGER/utilities/canvasMCS -h
:type core_color: `QColor`
:param highlight_color: Optional Color to highlight the common substructure.
:rtype: List of `QPicture` objects.
:return: QPictures for the aligned 2D images.
"""
# NOTE: We are not using analyze.find_common_substructure() here, because
# we also need bonds for the core and the ChmMol objects for the structures.
# Constants for now, but we may choose to expose them as options later:
IGNORE_HYDROGEN = 0
RESCALE = 2
FIXUP = True
if len(sts) > 50:
raise ValueError("Too many input CTs specified (max is 50)")
if core_color is None:
core_color = QtGui.QColor(0, 0, 0) # black
settings = canvas.MCSsettings()
settings.atomTyping = atomTyping
if renderer is None:
renderer = canvas2d.Chm2DRenderer(canvas2d.ChmRender2DModel())
# Convert CTs to ChmMol objects (both SWIG and SIP):
adaptor = canvas2d.ChmMmctAdaptor()
sip_mols = [adaptor.create(st.handle) for st in sts]
swig_mols = list(map(canvas2d.convertChmMoltoSWIG, sip_mols))
results = canvas.runMCS(swig_mols, settings)
template_chmmol = None
template_core_atoms = None
pictures = []
for chmmol, item in zip(sip_mols, results):
# Chm2DCoordGen expects 0-indexed atoms and bonds:
core_atoms = [index - 1 for index in item.mapAtoms]
core_bonds = [index - 1 for index in item.mapBonds]
if item.molID == 0:
# Generate the 2D image for the first molecule (template):
template_chmmol = chmmol
template_core_atoms = core_atoms
if item.molID == 0 or not core_atoms:
canvas2d.Chm2DCoordGen.generateAndApply(chmmol)
qpic = renderer.getQPicture(chmmol, [], core_bonds, core_color,
False)
else:
# Align molecule2 to molecule1:
canvas2d.Chm2DCoordGen.generateFromTemplateAndApply(
chmmol, template_chmmol, core_atoms, template_core_atoms,
IGNORE_HYDROGEN, RESCALE, FIXUP)
qpic = renderer.getQPicture(chmmol, [], core_bonds, core_color,
False)
pictures.append(qpic)
return pictures
[docs]def get_ligand(st):
"""
Return a substructure that can be rendered in a 2D image (the first ligand
in `st`, unless it's also has too many atoms).
:param st: the structure
:type st: structure.Structure
:return: the ligand structure or None if the structure has too many atoms
:rtype: structure.Structure or None
"""
max_atoms = MAX_LIGAND_ATOMS
if maestro:
max_atoms = int(maestro.get_command_option("prefer", "2dmaxatoms"))
if st.atom_total < max_atoms:
return st
lig_atoms = analyze.evaluate_asl(st, 'ligand')
if len(lig_atoms) == 0:
return None
lig_st = st.extract(lig_atoms, copy_props=True)
if lig_st.mol_total > 1:
lig_st = lig_st.molecule[1].extractStructure(copy_props=True)
if lig_st.atom_total < max_atoms:
return lig_st
[docs]class StructurePicture(QtWidgets.QLabel):
"""
This is the label that normally stores the picture of the molecule. It can
also store a text message.
We make sure that this stays the same size, no matter what data (if any) is
stored in it.
"""
[docs] def __init__(self,
parent=None,
layout=None,
height=200,
width=200,
background='white',
annotators=None):
"""
:type parent: QWidget
:param parent: the widget that owns this widget
:type layout: QLayout
:param layout: The layout that this widget should be placed in
:type height: int
:param height: the height of this label in pixels
:type width: int
:param width: the width of this label in pixels
:type annotators: list
:param annotators: Each item of the list should be a
`canvas2d.ChemViewAnnotator` object that will be applied to the
`canvas2d.ChmRender2DModel` when generating the image
"""
QtWidgets.QLabel.__init__(self, parent)
# Lock the size of this widget so it never changes
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed,
QtWidgets.QSizePolicy.Fixed)
self.user_width = width
self.user_height = height
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth())
self.setSizePolicy(sizePolicy)
self.setMinimumSize(QtCore.QSize(width, height))
# Put a box around this label
self.setFrameShape(QtWidgets.QFrame.Box)
self.setLineWidth(1)
# Everthing is centered horizontally and vertically
self.setAlignment(QtCore.Qt.AlignCenter)
# Set the background
if background:
self.setStyleSheet("background-color: " + background)
# Prepare the structure drawing utilities
self.adaptor = canvas2d.ChmMmctAdaptor()
self.model = canvas2d.ChmRender2DModel()
rect = canvas2d.ChmRect2D(0, 0, width - 10, height - 10)
self.renderer = canvas2d.Chm2DRenderer(self.model)
self.renderer.setBoundingBox(rect)
if annotators:
self.annotators = annotators
else:
self.annotators = []
# Put the widget in the GUI
if layout is not None:
layout.addWidget(self)
[docs] def sizeHint(self):
# Ensures that the widget always remains the same size
return QtCore.QSize(self.user_width, self.user_height)
[docs] def drawStructure(self,
structure,
StereoType=canvas2d.ChmMmctAdaptor.
StereoFromAnnotationAndGeometry_Safe,
hydrogenTreatment=canvas2d.ChmAtomOption.H_ExplicitOnly,
wantProperties=True,
wantMMStereoProps=True,
readAtomBondProperties=True,
allowRadicals=False):
"""
Makes a 2-D rendering of the structure
:type structure: schrodinger.structure.Structure class object
:param structure: structure to be drawn on the canvas
:type StereoType: canvas2d.ChmMmctAdaptor.StereoType
:param StereoType: Stereochemistry option to use. Available options:
# Does not include stereochemistry info:
canvas2d.ChmMmctAdaptor.NoStereo
# Ignores mmstereo annotations:
canvas2d.ChmMmctAdaptor.StereoFromGeometry
# Silently ignores stereo information that Canvas doesn't agree with:
canvas2d.ChmMmctAdaptor.StereoFromGeometry_Safe
# Ignores 3d geometry:
canvas2d.ChmMmctAdaptor.StereoFromAnnotation
# Silently ignores stereo information that Canvas doesn't agree with:
canvas2d.ChmMmctAdaptor.StereoFromAnnotation_Safe
# Uses mmstereo annotaions with 3D geometry as a backup:
canvas2d.ChmMmctAdaptor.StereoFromAnnotationAndGeometry
# Silently ignores stereo information that Canvas doesn't agree with:
canvas2d.ChmMmctAdaptor.StereoFromAnnotationAndGeometry_Safe
:type hydrogenTreatment: canvas2d.ChmAtomOption.H
:param hydrogenTreatment: Hydrogen treatment method. Available options:
canvas2d.ChmAtomOption.H_Never
canvas2d.ChmAtomOption.H_ExplicitOnly
canvas2d.ChmAtomOption.H_Polar
canvas2d.ChmAtomOption.H_ExplicitPolar
canvas2d.ChmAtomOption.H_Chiral
canvas2d.ChmAtomOption.H_ExplicitChiral
canvas2d.ChmAtomOption.H_ExplicitPolarAndChiral
canvas2d.ChmAtomOption.H_All
:type wantProperties: bool
:param wantProperties: Whether properties should be copied.
:type wantMMStereoProps: bool
:param wantMMStereoProps: Whether to copy mmstereo properties.
:type readAtomBondProperties: bool
:param readAtomBondProperties: Whether to copy atom and bond-level properties.
:type allowRadicals: bool
:param allowRadicals: Whether to assume that valence deficiencies
represent unpaired electrons.
"""
# First add all the annotators in the self.annotators list
self.model.clearAnnotators()
for annotator in self.annotators:
self.model.addAnnotator(annotator)
self.renderer.setModel(self.model)
chmmol = self.adaptor.create(int(structure), StereoType,
hydrogenTreatment, wantProperties,
wantMMStereoProps, readAtomBondProperties,
allowRadicals)
pic = get_qpicture_protected(self.renderer, chmmol)
self.setPicture(pic)
self.model.clearAnnotators()
[docs] def drawChmmol(
self,
chmmol,
atoms=[], # noqa: M511
bonds=[], # noqa: M511
color=QtGui.QColor(255, 255, 255), # noqa: M511
gen_coord=False):
"""
Makes a 2-D rendering of the chmmol object with optional
atoms and bonds highlighting.
:type chmmol: ChmMol instance.
:param chmmol: Structure to generate the picture for.
:type atoms: list
:param atoms: list of atoms that should be highlighted
:type bonds: list
:param bonds: list of bonds that should be highlighted
:type color: `QtGui.QColor`
:param color: color that is used to highlight atoms and bonds
:type gen_coord: bool
:param gen_coord: if True generate coordinates.
"""
self.model.clearAnnotators()
for annotator in self.annotators:
self.model.addAnnotator(annotator)
self.renderer.setModel(self.model)
pic = get_qpicture_highlight(self.renderer, chmmol, atoms, bonds, color,
gen_coord)
self.setPicture(pic)
self.model.clearAnnotators()
[docs] def setAnnotators(self, annotators):
"""
This function allows to reset annotators between renderning 2-D
structures.
:type annotators: list
:param annotators: Each item of the list should be a
`canvas2d.ChemViewAnnotator` object that will be applied to the
`canvas2d.ChmRender2DModel` when generating the image
"""
self.annotators = annotators
[docs]class structure_scene(QtWidgets.QGraphicsScene):
"""
Scene which holds the structure_view object
"""
[docs]class structure_view(QtWidgets.QGraphicsView):
"""
View which holds a structure_item object
"""
# Signals to emit when user clicks on an atom or bond
atom_clicked = QtCore.pyqtSignal(int)
bond_clicked = QtCore.pyqtSignal((int, int))
[docs] def __init__(self, scene):
super(structure_view, self).__init__(scene)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
[docs] def wheelEvent(self, event):
pass
[docs] def resizeEvent(self, event):
self.fitInView(self.scene().sceneRect(), QtCore.Qt.KeepAspectRatio)
[docs]class structure_item(QtWidgets.QGraphicsItem):
[docs] def __init__(self, rect=None):
"""
:type rect: QRectF
:param rect: Size of the rect of the bounding box.
"""
if rect:
self.rect = rect
else:
self.set_rect(QtCore.QRectF(0, 0, 500, 500))
super(structure_item, self).__init__()
self.colormap = None
self.chmmol = None
self.pic = None
self.adaptor = canvas2d.ChmMmctAdaptor()
self.model2d = canvas2d.ChmRender2DModel()
self.renderer = canvas2d.Chm2DRenderer(self.model2d)
self.annotators = []
self.annotator_function_returns = []
[docs] def boundingRect(self):
return self.rect
[docs] def clear(self):
"""
Clear picture from item.
"""
self.pic = None
self.chmmol = None
self.update()
[docs] def generate_picture(self, gen_coord=True):
"""
Generates a QPicture of the structure. This should
be called after setting the structure and the accompaning
annotators.
:type gen_coord: bool
:param gen_coord: if True generate coordinates.
"""
if self.colormap:
self.model2d.setColorMap(self.colormap)
for ann in self.annotators:
self.model2d.addAnnotator(ann)
self.renderer.setModel(self.model2d)
renderer_rect = canvas2d.ChmRect2D(self.rect.x(), self.rect.y(),
self.rect.width(),
self.rect.height())
self.renderer.setBoundingBox(renderer_rect)
self.pic = get_qpicture_protected(self.renderer, self.chmmol, gen_coord)
ret = []
for func in self.annotator_function_returns:
ret.append(func)
# cleanup
if self.colormap:
self.model2d.clearColorMap()
self.model2d.clearAnnotators()
self.update()
return ret
[docs] def paint(self, painter, option, widget=0):
"""
Overrides the paint function to draw the picture that has
already been generated. This should never be called manually.
"""
if not self.pic:
return
painter.drawPicture(0, 0, self.pic)
[docs] def set_colormap(self, colormap):
"""
Sets the colormap for the atom and bond coloring. Otherwise, the
coloring used will be that coming from the ct and/or chmmol.
"""
self.colormap = colormap
[docs] def set_structure(self,
struct,
StereoType=canvas2d.ChmMmctAdaptor.
StereoFromAnnotationAndGeometry_Safe,
hydrogenTreatment=canvas2d.ChmAtomOption.H_ExplicitOnly,
wantProperties=True,
wantMMStereoProps=True,
readAtomBondProperties=True,
allowRadicals=False):
"""
Set the structure to the given Structure object.
:type struct: schrodinger.structure.Structure class object
:param struct: structure to be drawn on the canvas
:type StereoType: ChmMmctAdaptor.StereoType
:param StereoType: Stereochemistry option to use. Avialable options:
# Does not include stereochemistry info:
canvas2d.ChmMmctAdaptor.NoStereo
# Ignores mmstereo annotations:
canvas2d.ChmMmctAdaptor.StereoFromGeometry
# Silently ignores stereo information that Canvas doesn't agree with:
canvas2d.ChmMmctAdaptor.StereoFromGeometry_Safe
# Ignores 3d geometry:
canvas2d.ChmMmctAdaptor.StereoFromAnnotation
# Silently ignores stereo information that Canvas doesn't agree with:
canvas2d.ChmMmctAdaptor.StereoFromAnnotation_Safe
# Uses mmstereo annotaions with 3D geometry as a backup:
canvas2d.ChmMmctAdaptor.StereoFromAnnotationAndGeometry
# Silently ignores stereo information that Canvas doesn't agree with:
canvas2d.ChmMmctAdaptor.StereoFromAnnotationAndGeometry_Safe
:type hydrogenTreatment: canvas2d.ChmAtomOption.H
:param hydrogenTreatment: Hydrogen treatment method.
canvas2d.ChmAtomOption.H_Never
canvas2d.ChmAtomOption.H_ExplicitOnly
canvas2d.ChmAtomOption.H_Polar
canvas2d.ChmAtomOption.H_ExplicitPolar
canvas2d.ChmAtomOption.H_Chiral
canvas2d.ChmAtomOption.H_ExplicitChiral
canvas2d.ChmAtomOption.H_ExplicitPolarAndChiral
canvas2d.ChmAtomOption.H_All
:type wantProperties: bool
:param wantProperties: Whether properties should be copied.
:type wantMMStereoProps: bool
:param wantMMStereoProps: Whether to copy mmstereo properties.
:type readAtomBondProperties: bool
:param readAtomBondProperties: Whether to copy atom and bond-level properties.
:type allowRadicals: bool
:param allowRadicals: Whether to assume that valence deficiencies
represent unpaired electrons.
"""
self.chmmol = self.adaptor.create(int(struct), StereoType,
hydrogenTreatment, wantProperties,
wantMMStereoProps,
readAtomBondProperties, allowRadicals)
[docs] def set_rect(self, rect):
"""
:type rect: QRect
:param rect: size of bounding box
"""
self.rect = rect
[docs] def set_text(self, text, alignment=None):
"""
Sets text in the view. This is useful if you display text in place of
a structure, in places where no structure is available.
:type text: string
:param text: text to be displayed
:type alignment: Qt.AlignmentFlags
:param alignment: alignment flags of text, defaults to
QtCore.Qt.AlignVCenter|QtCore.Qt.AlignCenter
"""
if not alignment:
alignment = QtCore.Qt.AlignVCenter | QtCore.Qt.AlignCenter
picture = QtGui.QPicture()
painter = QtGui.QPainter(picture)
textrect = painter.boundingRect(self.boundingRect().toAlignedRect(),
alignment, text)
finalrect = painter.drawText(textrect, alignment, text)
painter.end()
picture.setBoundingRect(finalrect)
self.pic = picture
[docs] def add_annotator(self, annotator):
"""
Adds annotator to stack. The order that these functions get added is the order
that they will be applied to the picture.
:type annotator: schrodinger.infra.canvas2d.ChemViewAnnotator
:param annotator: Annotator which draws on top an image
"""
self.annotators.append(annotator)
[docs] def add_annotator_function_return(self, function):
"""
Add functions that need to get returned after the structure is rendered on the
screen. An example of this would be a function that returns drawmol coordinates.
"""
self.annotator_function_returns.append(function)
[docs] def clear_annotators(self):
"""
Clears all annotators
"""
self.annotators = []
self.annotator_function_returns = []
[docs] def mousePressEvent(self, event):
"""
Emit a signal if the user left-clicked on an atom or a bond
"""
if not self.pic:
return QtWidgets.QGraphicsItem.mousePressEvent(self, event)
if event.button() != QtCore.Qt.LeftButton:
return QtWidgets.QGraphicsItem.mousePressEvent(self, event)
xval = event.pos().x() - self.pic.boundingRect().left()
yval = event.pos().y() - self.pic.boundingRect().top()
width = self.pic.boundingRect().width()
height = self.pic.boundingRect().height()
index = self.renderer.getAtomAtLocation(self.chmmol, width, height,
xval, yval) + 1
try:
view = self.scene().views()[0]
except (IndexError, AttributeError):
# If not assigned to a view or scene (though a click seems unlikely
# in that situation)
return QtWidgets.QGraphicsItem.mousePressEvent(self, event)
if index:
try:
view.atom_clicked.emit(index)
except AttributeError:
pass
else:
bond = self.renderer.getBondAtLocation(self.chmmol, width, height,
xval, yval)
if bond:
bonded_atoms = [x + 1 for x in bond]
try:
view.bond_clicked.emit(*bonded_atoms)
except AttributeError:
pass
return QtWidgets.QGraphicsItem.mousePressEvent(self, event)
[docs] def getBondAtLocation(self, pos):
"""
Return the bond at the specified coordinates
:param pos: The specified coordinates
:type pos: `PyQt5.QtCore.QPoint`
:return: If there is a bond at the specified coordinates, return a list
of the chmmol atom indices for the two bound atoms. (Note that the
chmmol atom indices are zero-indexed, so you should add one to each
index if you want the `schrodinger.structure.Structure` atom indices).
If there is no bond at the specified coordinates, return an empty list.
:rtype: list
"""
pos = pos - self.pic.boundingRect().topLeft()
rect = self.pic.boundingRect()
return self.renderer.getBondAtLocation(self.chmmol, rect.width(),
rect.height(), pos.x(), pos.y())
[docs]class ColoredArrowAnnotator(canvas2d.ChemViewAnnotator):
"""
This annotator allows you to add colored arrows to bonds in 2d renderings.
This is a slightly less terrible port from C++. Most of the logic
still keeps C++-style syntax, simply because rewriting it is not
worth the effort. The interface is now more pythonic, at least.
"""
[docs] def __init__(self, mol, bond_info=None, draw_arrowhead=True):
"""
:param mol: is a chmmol
:param bond_info: is a list containing individual lists of: (atom1,
atom2, outlined, QColor)
These lists are composed of:
- atom1 is a ct-atom index
- atom2 is a ct-atom index
- outlined (0/1) is whether you want the arrow to have a black outline
- qc is a qcolor for the bond
"""
canvas2d.ChemViewAnnotator.__init__(self)
self.bond_info = bond_info or []
mol = canvas2d.convertChmMoltoSWIG(mol)
self.draw_arrowhead = draw_arrowhead
[docs] def add_bond_arrow(self, atom1, atom2, outlined, qc):
"""
atom1 is a ct-atom index
atom2 is a ct-atom index
outlined (0/1) is whether you want the arrow to have a black outline
qc is the qcolor of the bond
"""
self.bond_info.append([atom1, atom2, outlined, qc])
[docs] def clear_bond_arrows(self):
"""
Clear all bond markers in this annotator
"""
self.bond_info = []
return
[docs] def annotate(self, view, dm):
"""
This function annotates the specified arrows.
"""
QPointF = QtCore.QPointF
QRectF = QtCore.QRectF
QPolygonF = QtGui.QPolygonF
# Determine bond direction indicators. This code was moved here
# from the initializer to fix RGA-866.
bsz = len(dm.bonds)
colors = [0] * bsz
bondDirectionIndicators = [0] * bsz * 2
for (atom1, atom2, outlined, qc) in self.bond_info:
atom1 -= 1 #change to 0-based chmmol indices
atom2 -= 1 #change to 0-based chmmol indices
for i in range(bsz):
b = dm.bonds[i]
a1, a2 = b.a1, b.a2
bondDir = False
if a1.getMolIndex() == atom1 and a2.getMolIndex() == atom2:
bondDir = 1
colors[i] = qc
elif a2.getMolIndex() == atom1 and a1.getMolIndex() == atom2:
bondDir = 2
if bondDir:
bondDirectionIndicators[i * 2] = bondDir
bondDirectionIndicators[i * 2 + 1] = outlined
colors[i] = qc
if not bondDirectionIndicators:
return
qp = QtGui.QPen()
qp.setJoinStyle(QtCore.Qt.MiterJoin)
qp.setWidth(0)
for i in range(bsz):
if bondDirectionIndicators[i * 2]:
if bondDirectionIndicators[i * 2 + 1] > 0:
qp.setColor(QtGui.QColor(0, 0, 0))
else:
qp.setColor(colors[i])
a1, a2 = dm.bonds[i].a1, dm.bonds[i].a2
if bondDirectionIndicators[i * 2] == 2:
a1, a2 = dm.bonds[i].a2, dm.bonds[i].a1
p = QPolygonF()
for j in [
QPointF(a1.x(), a1.y()),
QPointF(a1.x() + 0.1,
a1.y() + 0.1),
QPointF(a2.x() + 0.1,
a2.y() + 0.1),
QPointF(a2.x(), a2.y()),
QPointF(a1.x(), a1.y())
]:
p.append(j)
br = dm.bonds[i].boundingRect()
p2 = QPolygonF()
for j in [
br.topLeft(),
br.topRight(),
br.bottomRight(),
br.bottomLeft(),
br.topLeft()
]:
p2.append(j)
p = p.intersected(p2)
points = []
for point in p:
points.append((point.x(), point.y()))
def calc_endpoint(p_in, points):
def dist_sqrd(p1, p2):
return (p1[0] - p2[0]) * (p1[0] - p2[0]) + \
(p1[1] - p2[1]) * (p1[1] - p2[1])
ax, ay = points[0]
dsqd = dist_sqrd(p_in, points[0])
for p in points[1:]:
tmp_dsqd = dist_sqrd(p_in, p)
if tmp_dsqd < dsqd:
ax, ay = p
dsqd = tmp_dsqd
return (ax, ay)
a1x, a1y = calc_endpoint((a1.x(), a1.y()), points)
a2x, a2y = calc_endpoint((a2.x(), a2.y()), points)
x = a1x
y = a1y
xChange = x - (a1x * (old_div(4., 6.)) + a2x *
(old_div(2., 6.)))
yChange = y - (a1y * (old_div(4., 6.)) + a2y *
(old_div(2., 6.)))
x2 = (a2x * (old_div(4., 6.)) + a1x * (old_div(2., 6.)))
y2 = (a2y * (old_div(4., 6.)) + a1y * (old_div(2., 6.)))
change = [xChange, yChange]
multiplier = 9
if not self.draw_arrowhead:
multiplier = 15
xChange = xChange / numpy.linalg.norm(change) * \
view.getModel().getBondLineWidth() * multiplier
yChange = yChange / numpy.linalg.norm(change) * \
view.getModel().getBondLineWidth() * multiplier
# Here is the arrow as we draw it
# A
# / \
# / \
# B-C-F-G
# ||
# ||
# DE
na = numpy.array
p = []
if self.draw_arrowhead:
p.append(QPointF(x, y)) #A
p.append(
QPointF(x - (xChange + yChange),
y - (yChange - xChange))) #B
(px, py) = self._seg_intersect(
na([x - (xChange + yChange), y - (yChange - xChange)]),
na([x - (xChange - yChange), y - (yChange + xChange)]),
na([
x - old_div((xChange + yChange), 3), y - old_div(
(yChange - xChange), 3)
]),
na([
x2 - old_div((xChange + yChange), 3), y2 - old_div(
(yChange - xChange), 3)
]))
C = QPointF(px, py)
p.append(C) #C
p.append(
QPointF(x2 - old_div((xChange + yChange), 3), y2 - old_div(
(yChange - xChange), 3))) #D
p.append(
QPointF(x2 - old_div((xChange - yChange), 3), y2 - old_div(
(yChange + xChange), 3))) #E
(px, py) = self._seg_intersect(
na([x - (xChange + yChange), y - (yChange - xChange)]),
na([x - (xChange - yChange), y - (yChange + xChange)]),
na([
x - old_div((xChange - yChange), 3), y - old_div(
(yChange + xChange), 3)
]),
na([
x2 - old_div((xChange - yChange), 3), y2 - old_div(
(yChange + xChange), 3)
]))
p.append(QPointF(px, py)) #F
if self.draw_arrowhead:
p.append(
QPointF(x - (xChange - yChange),
y - (yChange + xChange))) #G
p.append(QPointF(x, y)) #A, to close shape
else:
p.append(C)
qpoly = QtGui.QPolygonF(p)
qgpi = QtWidgets.QGraphicsPolygonItem(qpoly)
qgpi.setPen(qp)
qgpi.setBrush(QtGui.QBrush(colors[i]))
qgpi.setZValue(10)
qgpi.setParentItem(dm.bonds[i])
def _perp(self, a):
b = numpy.empty_like(a)
b[0] = -a[1]
b[1] = a[0]
return b
def _seg_intersect(self, a1, a2, b1, b2):
"""
line 1 given by endpoints a1, a2
line 2 given by endpoints b1, b2
"""
da = a2 - a1
db = b2 - b1
dp = a1 - b1
dap = self._perp(da)
denom = numpy.dot(dap, db)
num = numpy.dot(dap, dp)
return (old_div(num, denom)) * db + b1
[docs]class CgCoreAnnotator(ColoredArrowAnnotator):
"""
This is the original ColoredArrowAnnotator class name.
When that class had the format of its input changed,
it became incompatible with the original class.
This class will remain to preserve backwards compatability.
"""
[docs] def __init__(self, bond_info, colors, atoms, atomColors, mol):
"""
bond_info is a list of size 3N, (atom1 idx, a2 idx, outline)
colors is a list of size 3N (R, G, B)
atoms/atomColors are deprecated
mol is a chmmol
"""
rearranged_input = []
for i in range(old_div(len(bond_info), 3)):
rearranged_input.append([
bond_info[i * 3], bond_info[i * 3 + 1], bond_info[i * 3 + 2],
QtGui.QColor(colors[i * 3], colors[i * 3 + 1],
colors[i * 3 + 2])
])
ColoredArrowAnnotator.__init__(self, mol, rearranged_input)
[docs]class BaseSquareAnnotator(canvas2d.ChemViewAnnotator):
"""
Base class for annotators that draw a square around atoms
"""
[docs] def annotate(self, view, dm):
"""
Add this sphere to the list of things in the picture
:type view: Chemview
:param view: the View this item goes in
:type dm: ChmDrawMol
:param dm: object that contains the list of atom and bond graphics
"""
for atom_num, color in self._label_dict.items():
brush = QtGui.QBrush(QtCore.Qt.transparent)
pen = QtGui.QPen(color)
pen.setWidth(4)
# Create the square
point = old_div(-self.size, 2)
square = QtWidgets.QGraphicsRectItem(point, point, self.size,
self.size)
square.setBrush(brush)
square.setPen(pen)
# Puts this square at the same location as the atom
square.setParentItem(dm.atoms[atom_num])
[docs]class RedSquareAnnotator(BaseSquareAnnotator):
"""
Create a square around the specified atom.
"""
[docs] def __init__(self, atom=None, size=100, color=QtCore.Qt.red):
"""
Instantiate a RedSquareAnnotator instance.
:type atom: int
:param atom: the atom number to which square applies. Note that this
expects the first atom to be atom 1, not 0.
:type size: float
:param size: the size of one side of the square in pixels.
:type color: QColor
:param color: the color used to draw annotator. Default is red.
"""
canvas2d.ChemViewAnnotator.__init__(self)
self._label_dict = {}
if atom:
self._label_dict[atom - 1] = color
self.size = size
self.color = color
[docs] def setAtom(self, atom, color=None):
"""
Set the atom number to which the square applies
:type atom: int
:param atom: the atom number to which square applies. Note that this
expects the first atom to be atom 1, not 0. If 0 is passed in, no atom
will be annotated.
:type color: QColor
:param color: the color used to draw annotator. If not given, the
previously set color for this annotator will be used.
"""
if not atom:
self.clearAtom()
return
if color is None:
color = self.color
self._label_dict = {atom - 1: color}
[docs] def clearAtom(self):
"""
Remove the current atom so no atoms are annotated
"""
self._label_dict = {}
[docs] def getAtom(self):
"""
Return the atom index current annotated
:rtype: int or None
:return: The index (1-based) of the atom annotated, or None if no atoms
are annotated
"""
try:
return list(self._label_dict)[0] + 1
except IndexError:
return None
[docs] def setColor(self, color):
"""
Set the color of the square
:type color: QColor
:param color: the color used to draw annotator.
"""
self.color = color
[docs]class MultiSquareAnnotator(BaseSquareAnnotator):
"""
Create a square around each specified atom.
"""
[docs] def __init__(self, atom_dict, size=100):
"""
Instantiate a MultiSquareAnnotator instance.
:type atom_dict: dict
:param atom_dict: A dictionary of {atom number: QColor} specifying the
appropriate color for each atom
:type size: float
:param size: The size of one side of the square in pixels. Defaults to
100 pixels.
"""
canvas2d.ChemViewAnnotator.__init__(self)
self.size = size
self._label_dict = {key - 1: val for key, val in atom_dict.items()}
[docs]class AtomNumberAnnotator(canvas2d.ChemViewAnnotator):
"""
Show the atom number of each atom rather than a vertex or atomic symbol
"""
[docs] def annotate(self, view, dm):
"""
Add atom number to each atom
:type view: Chemview
:param view: the View this item goes in
:type dm: ChmDrawMol
:param dm: object that contains the list of atom and bond graphics
"""
for i, a in enumerate(dm.atoms):
a.replaceLabel(str(i + 1), "")
dm.recreateBonds()
[docs]class AtomLabelAnnotator(canvas2d.ChemViewAnnotator):
"""
Changes the label displayed for an atom. The label can have subscripts.
"""
[docs] def __init__(self, atom_labels):
"""
Instantiate a AtomLabelAnnotator instance.
:type atom_labels: dict
:param atom_labels: A dictionary of {atom number: label} specifying the
label for each atom to be custom labeled. Each label may either be a
string or a (str, str) tuple. In the latter case, the first string is
the main label and the second string is the subscript. Note the atom
numbers used here should be 1-based (first atom number = 1)
"""
canvas2d.ChemViewAnnotator.__init__(self)
self.atom_labels = atom_labels
[docs] def annotate(self, view, drawmol):
"""
Add a label to each atom specified in the atom_labels property
:type view: Chemview
:param view: the View this item goes in
:type drawmol: ChmDrawMol
:param drawmol: object that contains the list of atom and bond graphics
"""
for index, atom in enumerate(drawmol.atoms, 1):
label = self.atom_labels.get(index)
if isinstance(label, str):
atom.replaceLabel(label, "")
elif isinstance(label, tuple):
atom.replaceLabel(label[0], label[1])
[docs]class CircleAnnotator(canvas2d.ChemViewAnnotator):
"""
Creates a circle behind one or more atoms.
"""
[docs] def __init__(self,
atom=None,
radius=75.,
color=QtCore.Qt.green,
gradient=False,
fill=False,
width=4):
"""
Instantiate a ColorCircleAnnotator instance.
:type atom: int
:param atom: the atom number to which radius applies. Note that this
expects the first atom to be atom 1, not 0.
:type radius: float
:param radius: the radius of the sphere in pixels. Default is 75.
:type color: QColor
:param color: The color of the circle fill
:type gradient: bool
:param gradient: If True, the circle is filled with a gradient that
goes from white at the center to the defined color. gradient is
exclusive with fill.
:type fill: bool
:param fill: If True, the circle is given a constant fill. fill is
exclusive with gradient.
:type width: int
:param width: The width of the pen if fill and gradient are both False.
Default is 4.
"""
canvas2d.ChemViewAnnotator.__init__(self)
self._label_dict = {}
self.color = color
self.radius = radius
if atom is not None:
self.addAtom(atom)
self.fill = fill
self.gradient = gradient
if self.fill and self.gradient:
raise RuntimeError('gradient and fill cannot both be True')
self.width = width
[docs] def addAtom(self, atom_num, radius=None, color=None):
"""
Add another sphere to this annotator
:type atom_num: int
:param atom_num: the atom number to which radius applies. Note that
this expects the first atom to be atom 1, not 0.
:type radius: float
:param radius: the radius of the sphere in pixels. If not given, the
radius given at the time of instance creation is used.
:type color: QColor
:param color: The color of the circle fill. If not given, the color
given at the time of instance creation is used.
"""
if not radius:
radius = self.radius
if not color:
color = self.color
self._label_dict[atom_num - 1] = (radius, color)
[docs] def removeAtom(self, atom_num):
"""
Remove an atom from the annotator
:type atom_num: int
:param atom_num: the atom number to which radius applies. Note that
this expects the first atom to be atom 1, not 0.
"""
del self._label_dict[atom_num - 1]
[docs] def clearAtoms(self):
"""
Remove all atoms from the annotator
"""
self._label_dict = {}
[docs] def setColor(self, color, reset_colors=False):
"""
Set the default color for this annotator
:type color: QColor
:param color: The color of the circle fill.
:type reset_colors: bool
:param reset_colors: If True, all existing circles will have their color
reset to this value
"""
self.color = color
if reset_colors:
for key in list(self._label_dict):
radius, old_color = self._label_dict[key]
self._label_dict[key] = (radius, color)
[docs] def setRadius(self, radius, reset_radii=False):
"""
Set the default radius for this annotator
:type radius: float
:param radius: The radius of the circle
:type reset_radii: bool
:param reset_radii: If True, all existing circles will have their radius
reset to this value
"""
self.radius = radius
if reset_radii:
for key in list(self._label_dict):
old_radius, color = self._label_dict[key]
self._label_dict[key] = (radius, color)
[docs] def annotate(self, view, dm):
"""
Add this sphere to the list of things in the picture
:type view: Chemview
:param view: the View this item goes in
:type dm: ChmDrawMol
:param dm: object that contains the list of atom and bond graphics
"""
for atom_num, data in self._label_dict.items():
radius, color = data
pen = QtGui.QPen(color)
# Create a gradient that goes from white at the center to full green
# halfway along the radius of the circle.
if self.gradient:
gradient = QtGui.QRadialGradient(0, 0, radius)
gradient.setColorAt(0, QtCore.Qt.white)
gradient.setColorAt(0.5, color)
brush = QtGui.QBrush(gradient)
elif self.fill:
brush = QtGui.QBrush(color)
else:
brush = None
pen.setWidth(self.width)
# Create the circle
point = old_div(-radius, 2)
circle = QtWidgets.QGraphicsEllipseItem(point, point, radius,
radius)
if brush is not None:
circle.setBrush(brush)
# Set the pen to the same color as the sphere so there is no border
circle.setPen(pen)
# Puts this circle at the same location as the atom
circle.setParentItem(dm.atoms[atom_num])
# Puts the circle behind the atom
circle.setFlag(QtWidgets.QGraphicsItem.ItemStacksBehindParent)
# Makes sure the atom stacks behind any bonds
dm.atoms[atom_num].setZValue(-50)
[docs]class ResizableStructurePicture(StructurePicture):
"""
StructurePicture widget that resizes the structure image with the widget.
"""
[docs] def __init__(self,
parent=None,
layout=None,
height=200,
width=200,
margin=10,
background='white',
annotators=None):
"""
See `StructurePicture` for all arguments, except:
:param margin: the margin to use around the image
:type margin: int
"""
super(ResizableStructurePicture,
self).__init__(parent, layout, height, width, background,
annotators)
self._margin = margin
self.setFrameShape(QtWidgets.QFrame.NoFrame)
self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
QtWidgets.QSizePolicy.MinimumExpanding)
self._args = [] # arguments needed for resizing/redrawing
[docs] def drawChmmol(
self,
chmmol,
atoms=[], # noqa: M511
bonds=[], # noqa: M511
color=QtGui.QColor(255, 255, 255), # noqa: M511
gen_coord=False):
super(ResizableStructurePicture,
self).drawChmmol(chmmol, atoms, bonds, color, gen_coord)
# when resizing the image the gen_coord should be False
self._args = [chmmol, atoms, bonds, color, False]
[docs] def resizeEvent(self, event):
super(ResizableStructurePicture, self).resizeEvent(event)
if self._args:
self.resizeImage()
[docs] def resizeImage(self):
rect = canvas2d.ChmRect2D(0, 0,
self.width() - self._margin,
self.height() - self._margin)
self.renderer.setBoundingBox(rect)
self.drawChmmol(*self._args)