Source code for ccpn.ui.gui.widgets.CompoundView

"""
This widget is based on the CcpNmr ChemBuild.
Credits to Tim Stevens, University of Cambridge December 2010-2012
"""
#=========================================================================================
# Licence, Reference and Credits
#=========================================================================================
__copyright__ = "Copyright (C) CCPN project (http://www.ccpn.ac.uk) 2014 - 2021"
__credits__ = ("Ed Brooksbank, Luca Mureddu, Timothy J Ragan & Geerten W Vuister")
__licence__ = ("CCPN licence. See http://www.ccpn.ac.uk/v3-software/downloads/license")
__reference__ = ("Skinner, S.P., Fogh, R.H., Boucher, W., Ragan, T.J., Mureddu, L.G., & Vuister, G.W.",
                 "CcpNmr AnalysisAssign: a flexible platform for integrated NMR analysis",
                 "J.Biomol.Nmr (2016), 66, 111-124, http://doi.org/10.1007/s10858-016-0060-y")
#=========================================================================================
# Last code modification
#=========================================================================================
__modifiedBy__ = "$modifiedBy: Ed Brooksbank $"
__dateModified__ = "$dateModified: 2021-03-08 16:27:02 +0000 (Mon, March 08, 2021) $"
__version__ = "$Revision: 3.0.3 $"
#=========================================================================================
# Created
#=========================================================================================
__author__ = "$Author: CCPN $"
__date__ = "$Date: 2017-04-07 10:28:41 +0000 (Fri, April 07, 2017) $"
#=========================================================================================
# Start of code
#=========================================================================================

from PyQt5 import QtCore, QtGui, QtWidgets, QtSvg, QtPrintSupport
from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene
from math import atan2, sin, cos, sqrt, degrees, radians, hypot, pi
from ccpn.ui.gui.widgets.Base import Base
from ccpn.ui.gui.widgets.FileDialog import PDFFileDialog
from ccpn.ui.gui.guiSettings import getColours, BORDERFOCUS, BORDERNOFOCUS


Qt = QtCore.Qt
QPointF = QtCore.QPointF
QRectF = QtCore.QRectF
PI = 3.1415926535898


[docs]class CompoundView(QGraphicsView, Base): def __init__(self, parent=None, smiles=None, variant=None, preferences=None, **kwds): super(CompoundView, self).__init__(parent) Base._init(self, **kwds) self.scene = QGraphicsScene(self) self.scene.setSceneRect(0, 0, 300, 300) self.setScene(self.scene) self.setCacheMode(QGraphicsView.CacheBackground) # QtWidgets.QGraphicsView.__init__(self, parent) self._parent = parent self.preferences = preferences self.backgroundColor = QtGui.QColor(10, 1, 0, 0) self.bondColor = Qt.black # self.setCompound = self.setCompound self.rotatePos = None if variant: self.compound = variant.compound else: self.compound = None self.dustbin = set() self.variant = variant self.atomViews = {} self.selectedViews = set() self.bondItems = {} self.groupItems = {} self.update() self.nameAtoms = True self.showChargeSymbols = True self.showChiralities = True self.showStats = False self.showGroups = False self.menuAtomView = None self.menuAtom = None self.movePos = None self.zoomLevel = 1.0 # Context menu self.needMenuAtom = [] self.needSelectedAtom = [] self.needFurtherCheck = [] self.contextMenu = self.setupContextMenu() # self.setGeometry(20, 40, 350, 350) #[(1) < left, (2) < up, (3)Width, (4)Height self.setRenderHint(QtGui.QPainter.Antialiasing) self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground) self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter) self.setDragMode(QtWidgets.QGraphicsView.RubberBandDrag) self.setViewportUpdateMode(QtWidgets.QGraphicsView.FullViewportUpdate) self.setInteractive(True) self.resetView() self.selectionBox = SelectionBox(self.scene, self) self.scene.addItem(self.selectionBox) self.editAtom = None self.editWidget = QtWidgets.QLineEdit() self.editWidget.setMaxLength(8) self.editWidget.resize(50, 30) effect = QtWidgets.QGraphicsDropShadowEffect(self) effect.setBlurRadius(3) effect.setOffset(2, 2) #self.editWidget.setGraphicsEffect(effect) self.editWidget.returnPressed.connect(self.setAtomName) self.editWidget.hide() self.editProxy = self.scene.addWidget(self.editWidget) self.editProxy.setZValue(2) # self.setBackgroundBrush(self.backgroundColor) # TODO: Add settings for this self.showSkeletalFormula = False self.showSkeletalFormulaColor = True self.snapToGrid = False self.autoChirality = True self.addGraphicsItems() if self.variant and self.showSkeletalFormula: self.variant.snapAtomsToGrid(50.0) self.updateAll() self.smiles = smiles if smiles: self.setSmiles(self.smiles) self._setFocusColour()
[docs] def setSmiles(self, smiles): 'set the smiles' compound = importSmiles(smiles) variant = list(compound.variants)[0] self.setVariant(variant) variant.snapAtomsToGrid(ignoreHydrogens=False) self.smiles = smiles self.centerView() self.resetView() self.updateAll() self.show()
def _setFocusColour(self, focusColour=None, noFocusColour=None): """Set the focus/noFocus colours for the widget """ focusColour = getColours()[BORDERFOCUS] noFocusColour = getColours()[BORDERNOFOCUS] styleSheet = "QGraphicsView { " \ "border: 1px solid;" \ "border-radius: 1px;" \ "border-color: %s;" \ "} " \ "QGraphicsView:focus { " \ "border: 1px solid %s; " \ "border-radius: 1px; " \ "}" % (noFocusColour, focusColour) self.setStyleSheet(styleSheet)
[docs] def resizeEvent(self, event): return QtWidgets.QGraphicsView.resizeEvent(self, event)
# def paintEvent(self, event: QtGui.QPaintEvent): # return QtWidgets.QGraphicsView.paintEvent(self, event)
[docs] def setAtomName(self): atom = self.editAtom if not atom: return text = self.editWidget.text().strip() if text and (text != atom.name): used = set([a.name for a in atom.compound.atoms]) if text in used: prevAtom = self.compound.atomDict[text] name2 = text + '!' while name2 in used: name2 = name2 + '!' prevAtom.setName(name2) atom.setName(text) else: atom.setName(text) self.editAtom = None self.editWidget.hide() self.updateAll()
[docs] def setCompound(self, compound, variantInd=None, replace=True): """ Set the compound on the graphic scene. """ if compound is not self.compound: if replace or not self.compound: self.compound = compound if variantInd: variant = list(compound.variants)[variantInd] else: variants = list(compound.variants) if variants: for variant2 in variants: if (variant2.polyLink == 'none') and (variant2.descriptor == 'neutral'): variant = variant2 break else: for variant2 in variants: if variant2.polyLink == 'none': variant = variant2 break else: variant = variants[0] else: variant = Variant(compound) self.setVariant(variant) variant.snapAtomsToGrid(ignoreHydrogens=False) self.variant = variant self.centerView() self.resetView() self.updateAll() # self.show() else: variant = list(compound.variants)[0] x, y = self.getAddPoint() self.compound.copyVarAtoms(variant.varAtoms, (x, y)) self.centerView() self.resetView() self.updateAll() self.centerView() self.resetView() self.updateAll()
[docs] def queryAtomName(self, atomLabel): self.editAtom = atom = atomLabel.atom self.editWidget.setText(atom.name or atom.element) self.editWidget.setVisible(True) center = QtCore.QPointF(self.editWidget.rect().center()) pos = atomLabel.pos() - center self.editProxy.setPos(pos)
[docs] def drawForeground(self, painter, viewRect): QtWidgets.QGraphicsView.drawForeground(self, painter, viewRect)
[docs] def drawBackground(self, painter, viewRect): transform = painter.transform() scale = float(transform.m11()) unScale = 1.0 / scale QtWidgets.QGraphicsView.drawBackground(self, painter, viewRect) # Text pad = 2.0 qPoint = QtCore.QPointF qRectF = QtCore.QRectF painter.setPen(ATOM_NAME_FG) painter.setFont(QtGui.QFont("DejaVu Sans Mono", 12)) painter.scale(unScale, unScale) fontMetric = QtGui.QFontMetricsF(painter.font()) tl = viewRect.topLeft() x0 = tl.x() * scale y0 = tl.y() * scale y1 = y0 + viewRect.height() * scale
[docs] def resetView(self): self.resetCachedContent() self.fitInView(self.sceneRect(), Qt.KeepAspectRatio)
[docs] def setupContextMenu(self): # QAction = QtWidgets.QAction menu = QtWidgets.QMenu(self) action = QAction('Reset View', self, triggered=self.resetView) menu.addAction(action) subMenu = menu.addMenu('Export') action = QAction('Export PDF(3D)', self, triggered=self.exportPdf) subMenu.addAction(action) action = QAction('Export SVG(2D)', self, triggered=self.exportSvg) subMenu.addAction(action) subMenu = menu.addMenu('Edit') action = QAction('Show Skeletal structure ', self, triggered=self.showSkeletal) subMenu.addAction(action) action = QAction('Show 3D structure ', self, triggered=self.show3D) subMenu.addAction(action) action = QAction('Rotate Left', self, triggered=self.rotateLeft) subMenu.addAction(action) self.needMenuAtom.append(action) action = QAction('Rotate Right', self, triggered=self.rotateRight) subMenu.addAction(action) return menu
[docs] def popupContextMenu(self, pos): menuAtomView = self.menuAtomView menuAtom = self.menuAtom if menuAtom: for action in self.needMenuAtom: action.setEnabled(True) else: for action in self.needMenuAtom: action.setEnabled(False) if self.selectedViews: for action in self.needSelectedAtom: action.setEnabled(True) else: for action in self.needSelectedAtom: action.setEnabled(False) for action, func in self.needFurtherCheck: action.setEnabled(bool(func())) self.contextMenu.popup(pos)
[docs] def getMenuAtom(self, posFilter=None, negFilter=None): if self.variant and self.menuAtom: elem = self.menuAtom.element if posFilter is None: if negFilter is None: return self.menuAtom elif elem not in negFilter: return self.menuAtom elif elem in posFilter: if negFilter is None: return self.menuAtom elif elem not in negFilter: return self.menuAtom
[docs] def getMenuLinkAtoms(self): if self.variant and self.menuAtom: if self.menuAtom.element == 'H': atoms = [self.menuAtom, ] elif self.menuAtom.element in ('O', 'S'): atoms = [self.menuAtom, ] for atomB in self.menuAtom.neighbours: if atomB.element == 'H': atoms.append(atomB) break else: return else: return return atoms
[docs] def setVariant(self, variant): if variant is not self.variant: scene = self.scene self.atomViews = {} self.selectedViews = set() self.bondItems = {} self.groupItems = {} items = set(scene.items()) items.remove(self.editProxy) items.remove(self.selectionBox) # print(items) for item in items: item.hide() for item in items: del item self.resetCachedContent() self.variant = variant self.compound = variant.compound self._parent.variant = variant self._parent.compound = self.compound self.addGraphicsItems() self.centerView()
[docs] def updateAll(self): var = self.variant scene = self.scene if var: getView = self.atomViews.get bondItems = self.bondItems getBondItem = bondItems.get groupDict = self.groupItems usedGroups = set(var.atomGroups) for group in usedGroups: if group in groupDict: groupDict[group].syncGroup() elif group.groupType == EQUIVALENT: EquivItem(scene, self, group) elif group.groupType == PROCHIRAL: ProchiralItem(scene, self, group) elif group.groupType == AROMATIC: AromaticItem(scene, self, group) zombieGroups = set(groupDict.keys()) - usedGroups for group in zombieGroups: groupItem = groupDict[group] del groupDict[group] del groupItem for atom in var.varAtoms: atomView = getView(atom) if atomView: atomView.syncToAtom() else: atomView = AtomItem(scene, self, atom) zombieBonds = set(self.bondItems.keys()) - set(var.bonds) for bond in zombieBonds: bondItem = bondItems[bond] del bondItems[bond] del bondItem for bond in var.bonds: bondItem = getBondItem(bond) if not bondItem: bondItem = BondItem(scene, self, bond)
[docs] def addGraphicsItems(self): if not self.variant: return scene = self.scene # Draw groups for group in self.variant.atomGroups: if group.groupType == EQUIVALENT: EquivItem(scene, self, group) elif group.groupType == PROCHIRAL: ProchiralItem(scene, self, group) elif group.groupType == AROMATIC: AromaticItem(scene, self, group) # Draw atoms self.atomViews = {} for atom in self.variant.varAtoms: a = AtomItem(scene, self, atom) scene.addItem(a) # Draw bonds done = set() self.bondItems = bondDict = {} for bond in self.variant.bonds: atoms = frozenset(bond.varAtoms) if atoms in done: pass bondItem = BondItem(scene, self, bond) scene.addItem(bondItem) done.add(atoms)
[docs] def centroid(self, views): x0 = 0.0 y0 = 0.0 n = 0.0 for view in views: x1, y1, z1 = view.atom.coords x0 += x1 y0 += y1 n += 1.0 x0 /= n y0 /= n return x0, y0
[docs] def centroidAtoms(self, atoms): x0 = 0.0 y0 = 0.0 n = 0.0 for atom in atoms: x1, y1, z1 = atom.coords x0 += x1 y0 += y1 n += 1.0 x0 /= n y0 /= n return x0, y0
[docs] def changeBackground(self): pass
[docs] def exportPdf(self): if self.compound: printer = QtPrintSupport.QPrinter() oldRes = printer.resolution() newRes = 600.0 fRes = oldRes / newRes printer.setResolution(newRes) fType = 'PDF (*.pdf)' dialog = PDFFileDialog(parent=self, acceptMode='export', fileFilter=fType) dialog._show() filePaths = dialog.selectedFiles() if filePaths and len(filePaths) > 0: filePath = filePaths[0] if filePath: printer.setOutputFileName(filePath) pdfPainter = QtGui.QPainter(printer) self.bondColor = Qt.black scene = self.scene items = scene.items() cache = [None] * len(items) for i, item in enumerate(items): cache[i] = item.cacheMode() item.setCacheMode(item.NoCache) scene.render(pdfPainter) pdfPainter.end() self.bondColor = Qt.white for i in range(len(items)): items[i].setCacheMode(cache[i])
[docs] def exportSvg(self): if self.compound: printer = QtSvg.QSvgGenerator() scene = self.scene w = scene.width() h = scene.height() paperWidth = 200 paperHeight = paperWidth * h / w resolution = printer.resolution() / 25.4 printer.setSize(QtCore.QSize(paperWidth * resolution, paperHeight * resolution)) fType = 'SVG (*.svg)' dialog = QtWidgets.QFileDialog filePath = dialog.getSaveFileName(self, filter=fType) if filePath: self.bondColor = QtCore.Qt.black printer.setFileName(filePath) svgPainter = QtGui.QPainter(printer) oldBackground = self.backgroundColor items = scene.items() cache = [None] * len(items) for i, item in enumerate(items): cache[i] = item.cacheMode() item.setCacheMode(item.NoCache) scene.render(svgPainter) svgPainter.end() self.backgroundColor = oldBackground self.bondColor = QtCore.Qt.white for i in range(len(items)): items[i].setCacheMode(cache[i])
[docs] def showSkeletal(self): self.showSkeletalFormula = True self.updateAll()
[docs] def show3D(self): self.showSkeletalFormula = False self.updateAll()
[docs] def rotateLeft(self, angle=PI * 5.0 / 180): selected = self.selectedViews or self.atomViews.values() if not selected: return x0, y0 = self.centroid(selected) self.rotateAtoms(x0, y0, angle)
[docs] def rotateRight(self, angle=PI * 5.0 / 180): selected = self.selectedViews or self.atomViews.values() if not selected: return x0, y0 = self.centroid(selected) self.rotateAtoms(x0, y0, -angle)
[docs] def rotateAtoms(self, x0, y0, deltaAngle): atoms = [v.atom for v in self.selectedViews] if atoms: for atom in atoms: x, y, z = atom.coords dx = x - x0 dy = y - y0 r = sqrt(dx * dx + dy * dy) angle2 = atan2(dx, dy) + deltaAngle x = x0 + r * sin(angle2) y = y0 + r * cos(angle2) atom.setCoords(x, y, z) for atom in atoms: atom.updateValences() elif self.compound: for atom in self.compound.atoms: for varAtom in atom.varAtoms: x, y, z = varAtom.coords dx = x - x0 dy = y - y0 r = sqrt(dx * dx + dy * dy) angle2 = atan2(dx, dy) + deltaAngle x = x0 + r * sin(angle2) y = y0 + r * cos(angle2) varAtom.coords = (x, y, z) for atom in self.compound.atoms: for varAtom in atom.varAtoms: varAtom.updateValences() self.updateAll()
[docs] def centerView(self): if self.variant: x, y, z = self.variant.getCentroid() else: x, y = 0, 0 self.ensureVisible(x - 50, y - 50, 100, 100)
[docs] def resetZoom(self): fac = 1.0 / self.zoomLevel self.scale(fac, fac) self.zoomLevel = 1.0
[docs] def wheelEvent(self, event): if event.angleDelta().y() < 0: fac = 0.8333 else: fac = 1.2 newLevel = self.zoomLevel * fac if 0.5 < newLevel < 5.0: self.zoomLevel = newLevel self.scale(fac, fac) event.accept()
[docs] def mousePressEvent(self, event): QtWidgets.QGraphicsView.mousePressEvent(self, event) button = event.button() pos = event.pos() mods = event.modifiers() haveCtrl = mods & Qt.CTRL haveShift = mods & Qt.SHIFT bondItem = None item = self.itemAt(pos) if item and isinstance(item, AtomItem): self.menuAtomView = item self.menuAtom = item.atom elif item and isinstance(item, AtomLabel): self.menuAtomView = item.atomView self.menuAtom = item.atom elif item and isinstance(item, BondItem): self.menuAtomView = None self.menuAtom = None if item.getDistToBond(self.mapToScene(pos)) <= 8: bondItem = item elif item and isinstance(item, AromaticItem): self.menuAtomView = None self.menuAtom = None bondItem = item else: self.menuAtomView = None self.menuAtom = None # deal with inconsistency in Qt versions for button naming try: MiddleButton = Qt.MiddleButton except AttributeError: MiddleButton = Qt.MidButton if button == Qt.LeftButton: if not self.menuAtom: self.selectionBox.updateRegion(begin=self.mapToScene(pos)) if not (bondItem or haveCtrl or haveShift): for view in list(self.selectedViews): view.deselect() elif button == MiddleButton: if (haveCtrl or haveShift): selected = self.selectedViews or self.atomViews.values() if len(selected) > 1: spos = self.mapToScene(pos) x0, y0 = self.centroid(selected) startAngle = atan2(spos.x() - x0, spos.y() - y0) self.rotatePos = (startAngle, (x0, y0)) else: pos = event.pos() h = self.horizontalScrollBar().sliderPosition() v = self.verticalScrollBar().sliderPosition() self.movePos = pos.x() + h, pos.y() + v elif button == Qt.RightButton: self.popupContextMenu(event.globalPos()) # for atomView in self.selectedViews: atomView.setSelected(True) if item is not self.editProxy: self.setAtomName()
[docs] def mouseMoveEvent(self, event): pos = event.pos() self.menuAtomView = None self.menuAtom = None if self.movePos: x0, y0 = self.movePos pos = event.pos() self.horizontalScrollBar().setSliderPosition(x0 - pos.x()) self.verticalScrollBar().setSliderPosition(y0 - pos.y()) elif self.rotatePos: startAngle, center = self.rotatePos x0, y0 = center spos = self.mapToScene(pos) dx = spos.x() - x0 dy = spos.y() - y0 angle = atan2(dx, dy) deltaAngle = angle - startAngle self.rotateAtoms(x0, y0, deltaAngle) self.rotatePos = (angle, center) self.update() else: self.selectionBox.updateRegion(end=self.mapToScene(pos)) QtWidgets.QGraphicsView.mouseMoveEvent(self, event)
[docs] def mouseReleaseEvent(self, event): if self.selectionBox.region: posA = self.selectionBox.region.topLeft() posB = self.selectionBox.region.bottomRight() if posA != posB: xs = [posA.x(), posB.x()] ys = [posA.y(), posB.y()] xs.sort() ys.sort() x1, x2 = xs y1, y2 = ys for atomView in self.atomViews.values(): pos = atomView.pos() x = pos.x() y = pos.y() if (x1 < x < x2) and (y1 < y < y2): atomView.select() self.selectionBox.updateRegion() self.rotatePos = None self.movePos = None self.update() QtWidgets.QGraphicsView.mouseReleaseEvent(self, event)
[docs] def getStats(self): pass
# def getAddPoint(self): # """ Set the compound on the specific position on the graphic scene. """ # compoundView = CompoundView # globalPos = QtGui.QCursor.pos() # pos = compoundView.mapFromGlobal(globalPos) # widget = compoundView.childAt(pos) # if widget: # x = pos.x() # y = pos.y() # else: # x = compoundView.width()/2.0 # y = compoundView.height()/2.0 # point = compoundView.mapToScene(x, y) # return point.x(), point.y() # Constants PI = 3.1415926535898 MIMETYPE_ELEMENT = 'application/x-ccpn-element' MIMETYPE_COMPOUND = 'application/x-ccpn-compound' MIMETYPE = 'application/x-ccpn' EQUIVALENT = 'equivalent' PROCHIRAL = 'prochiral' NONSTEREO = 'prochiral' AROMATIC = 'aromatic' CCPN_MOLTYPES = ('other', 'protein', 'DNA', 'RNA', 'carbohydrate') ELEMENTS = {'Common' : ['H', 'C', 'N', 'O', 'P', 'S', 'Se'], 'Halogens': ['F', 'Cl', 'Br', 'I'], 'Others' : ['B', 'Si', 'As'], } COVALENT_ELEMENTS = set(['H', 'C', 'N', 'O', 'P', 'S', 'Se', 'F', 'Cl', 'Br', 'I', 'B', 'Si', 'As']) PERIODIC_TABLE = [ ['Fr', 'Cs', 'Rb', 'K', 'Na', 'Li', 'H', ], ['Ra', 'Ba', 'Sr', 'Ca', 'Mg', 'Be', None], ['Ac', 'La', None, None, None, None, None], ['Th', 'Ce', None, None, None, None, None], ['Pa', 'Pr', None, None, None, None, None], ['U', 'Nd', None, None, None, None, None], ['Np', 'Pm', None, None, None, None, None], ['Pu', 'Sm', None, None, None, None, None], ['Am', 'Eu', None, None, None, None, None], ['Cm', 'Gd', None, None, None, None, None], ['Bk', 'Tb', None, None, None, None, None], ['Cf', 'Dy', None, None, None, None, None], ['Es', 'Ho', None, None, None, None, None], ['Fm', 'Er', None, None, None, None, None], ['Md', 'Tm', None, None, None, None, None], ['No', 'Yb', None, None, None, None, None], ['Lr', 'Lu', 'Y', 'Sc', None, None, None], ['Rf', 'Hf', 'Zr', 'Ti', None, None, None], ['Db', 'Ta', 'Nb', 'V', None, None, None], ['Sg', 'W', 'Mo', 'Cr', None, None, None], ['Bh', 'Re', 'Tc', 'Mn', None, None, None], ['Hs', 'Os', 'Ru', 'Fe', None, None, None], ['Mt', 'Ir', 'Rh', 'Co', None, None, None], ['Ds', 'Pt', 'Pd', 'Ni', None, None, None], ['Rg', 'Au', 'Ag', 'Cu', None, None, None], ['Cn', 'Hg', 'Cd', 'Zn', None, None, None], [None, 'Tl', 'In', 'Ga', 'Al', 'B', None], [None, 'Pb', 'Sn', 'Ge', 'Si', 'C', None], [None, 'Bi', 'Sb', 'As', 'P', 'N', None], [None, 'Po', 'Te', 'Se', 'S', 'O', None], [None, 'At', 'I', 'Br', 'Cl', 'F', None], [None, 'Rn', 'Xe', 'Kr', 'Ar', 'Ne', 'He'], ] VAR_TAG_ORDER = {'neutral' : 0, 'prot' : 0, 'deprot' : 1, 'link' : 2, 'stereo_1': 3, 'stereo_2': 3} LINKS = [('Previous residue', 'prev', 'link-prev.png'), ('Next residue', 'next', 'link-next.png'), ('Generic link', 'link', 'link.png')] LINK = 'link' DISALLOWED = [set([LINK, LINK]), set(['H', 'H']), set(['H', LINK])] HIGHLIGHT = QtGui.QColor(10, 250, 0, 255) HIGHLIGHT_BG = QtGui.QColor(10, 250, 0, 64) ATOM_NAME_FG = QtGui.QColor(255, 255, 255, 128) LINK_COLOR = QtGui.QColor(50, 200, 50, 128) EQUIV_COLOR = QtGui.QColor(255, 128, 128, 128) PROCHIRAL_COLOR = QtGui.QColor(128, 192, 255, 128) ELEMENT_FONT = QtGui.QFont("DejaVu Sans Mono", 9) ELEMENT_DATA = { LINK: (1, LINK_COLOR), # element: (default valances, colour, (common valances)) 'C' : (4, QtGui.QColor(180, 180, 180, 255), (4,)), 'N' : (3, QtGui.QColor(110, 110, 255, 255), (3,)), 'H' : (1, QtGui.QColor(255, 255, 255, 255), (1,)), 'O' : (2, QtGui.QColor(255, 80, 80, 255), (2,)), 'P' : (5, QtGui.QColor(255, 80, 255, 255), (5,)), 'S' : (2, QtGui.QColor(255, 180, 50, 255), (2, 4, 6)), 'Se': (2, QtGui.QColor(255, 100, 50, 255), (2,)), 'F' : (1, QtGui.QColor(255, 255, 128, 255), (1,)), 'Cl': (1, QtGui.QColor(128, 255, 80, 255), (1,)), 'Br': (1, QtGui.QColor(255, 128, 80, 255), (1,)), 'I' : (1, QtGui.QColor(150, 80, 255, 255), (1,)), 'B' : (3, QtGui.QColor(120, 120, 80, 255), (3,)), 'Si': (4, QtGui.QColor(200, 200, 255, 255), (4,)), 'As': (3, QtGui.QColor(230, 80, 255, 255), (3,)), 'Be': (2, QtGui.QColor(255, 255, 160, 255), (2,)), 'Mg': (2, QtGui.QColor(255, 255, 160, 255), (0, 2,)), 'Al': (3, QtGui.QColor(200, 200, 255, 255), (0, 3,)), 'Fe': (0, QtGui.QColor(160, 160, 255, 255), (0,)), '?' : (3, QtGui.QColor(160, 160, 255, 255), (1, 2, 3, 4)), } PERIODIC_TABLE_COLORS = (QtGui.QColor(255, 160, 160, 255), QtGui.QColor(255, 255, 160, 255), QtGui.QColor(160, 255, 160, 255), QtGui.QColor(160, 160, 255, 255), QtGui.QColor(200, 200, 255, 255), QtGui.QColor(255, 160, 255, 255)) for row, group in enumerate(PERIODIC_TABLE): for elem in group: if elem not in ELEMENT_DATA: if row == 0: color = PERIODIC_TABLE_COLORS[0] elif row == 1: color = PERIODIC_TABLE_COLORS[1] elif row < 16: color = PERIODIC_TABLE_COLORS[2] elif row < 26: color = PERIODIC_TABLE_COLORS[3] elif row < 30: color = PERIODIC_TABLE_COLORS[4] else: color = PERIODIC_TABLE_COLORS[5] ELEMENT_DATA[elem] = (0, color, (0,)) # Constants ELEMENT_DEFAULT = (0, QtGui.QColor(160, 160, 255, 255), (1,)) # Other oxidation states? Coordination ATOM = 'atom' PROPERTY = 'property' PROPERTIES_ATOM = [ ('Bonding', (('Toggle aromatic ring', 'toggle-aromatic.png', 'bond-aromatic'), ('Toggle dative bond', 'bond-dative.png', 'bond-dative'))), ('Atomic Charge', (('Add positive charge', 'charge-pos.png', '+'), ('Set neutral charge', 'charge-none.png', '0'), ('Add negative charge', 'charge-neg.png', '-'))), ('Valence Slots', (('Add valance', 'valence-add.png', 'v+'), ('Remove valance', 'valence-remove.png', 'v-'))), ('Stereochemistry', (('Toggle stereo centre', 'stereo.png', 'st'), ('Move atom forward', 'stereo-up.png', 'st+'), ('Move atom backward', 'stereo-down.png', 'st-'))), ('Chirality Label', (('R chirality', 'stereo-R.png', 'chiral-r'), ('S chirality', 'stereo-S.png', 'chiral-s'), ('No chirality label', 'stereo-none.png', 'chiral-n'), ('Alpha chirality', 'stereo-a.png', 'chiral-a'), ('Beta chirality', 'stereo-b.png', 'chiral-b'), )), ] # ('R/S', None, 'chiral-rs'), PROPERTIES_MULTI = [ ('Atomic Exchange', (('Toggle variable atom', 'variable-atom.png', 'xv'), ('Toggle fast exchange (H+)', 'exchange-hydrogen.png', 'xf'))), ('NMR Groups', (('NMR equivalent', 'nmr-equivalent.png', 'e'), ('NMR non-stereo (e.g. prochiral)', 'nmr-prochiral.png', 'p'), ('No atom group', 'nmr-no-group.png', 'u'))), ] CHARGE_FONT = QtGui.QFont("DejaVu Sans Mono", 11, QtGui.QFont.Bold) NEG_COLOR = QtGui.QColor(255, 40, 40, 255) POS_COLOR = QtGui.QColor(40, 40, 255, 255) CHARGE_BG_COLOR = QtGui.QColor(255, 255, 255, 128) CHIRAL_FONT = QtGui.QFont("DejaVu Sans Mono", 7, QtGui.QFont.Bold) CHIRAL_COLOR = QtGui.QColor(255, 255, 0, 255) ELEMENT_ISO_ABUN = { 'Ac': ((0.0, 227, 227.0277470),), 'Ag': ((0.518390, 107, 106.9050930), (0.481610, 109, 108.9047560),), 'Al': ((1.0, 27, 26.9815384),), 'Am': ((0.0, 243, 243.0613727), (0.0, 241, 241.0568229),), 'Ar': ((0.996003, 40, 39.9623831), (0.003365, 36, 35.9675463), (0.000632, 38, 37.9627322),), 'As': ((1.0, 75, 74.9215964),), 'At': ((0.0, 211, 210.9874810), (0.0, 210, 209.9871310),), 'Au': ((1.0, 197, 196.9665520),), 'B' : ((0.801000, 11, 11.0093055), (0.199000, 10, 10.0129370),), 'Ba': ((0.716980, 138, 137.9052410), (0.112320, 137, 136.9058210), (0.078540, 136, 135.9045700), (0.065920, 135, 134.9056830), (0.024170, 134, 133.9045030), (0.001060, 130, 129.9063100), (0.001010, 132, 131.9050560),), 'Be': ((1.0, 9, 9.0121821),), 'Bh': ((0.0, 264, 258.0984250),), 'Bi': ((1.0, 209, 208.9803830),), 'Bk': ((0.0, 249, 249.0749800), (0.0, 247, 247.0702990),), 'Br': ((0.506900, 79, 78.9183376), (0.493100, 81, 80.9162910),), 'C' : ((0.989300, 12, 12.0000000), (0.010700, 13, 13.0033548), (0.0, 14, 14.0032420),), 'Ca': ((0.969410, 40, 39.9625912), (0.020860, 44, 43.9554811), (0.006470, 42, 41.9586183), (0.001870, 48, 47.9525340), (0.001350, 43, 42.9587668), (0.000040, 46, 45.9536928),), 'Cd': ((0.287300, 114, 113.9033581), (0.241300, 112, 111.9027572), (0.128000, 111, 110.9041820), (0.124900, 110, 109.9030060), (0.122200, 113, 112.9044009), (0.074900, 116, 115.9047550), (0.012500, 106, 105.9064580), (0.008900, 108, 107.9041830),), 'Ce': ((0.884500, 140, 139.9054340), (0.111140, 142, 141.9092400), (0.002510, 138, 137.9059860), (0.001850, 136, 135.9071400),), 'Cf': ((0.0, 252, 252.0816200), (0.0, 251, 251.0795800), (0.0, 250, 250.0764000), (0.0, 249, 249.0748470),), 'Cl': ((0.757800, 35, 34.9688527), (0.242200, 37, 36.9659026),), 'Cm': ((0.0, 248, 248.0723420), (0.0, 247, 247.0703470), (0.0, 246, 246.0672176), (0.0, 245, 245.0654856), (0.0, 244, 244.0627463), (0.0, 243, 243.0613822),), 'Co': ((1.0, 59, 58.9332002),), 'Cr': ((0.837890, 52, 51.9405119), (0.095010, 53, 52.9406538), (0.043450, 50, 49.9460496), (0.023650, 54, 53.9388849),), 'Cs': ((1.0, 133, 132.9054470),), 'Cu': ((0.691700, 63, 62.9296011), (0.308300, 65, 64.9277937),), 'Db': ((0.0, 262, 258.0984250),), 'Dy': ((0.281800, 164, 163.9291710), (0.255100, 162, 161.9267950), (0.249000, 163, 162.9287280), (0.189100, 161, 160.9269300), (0.023400, 160, 159.9251940), (0.001000, 158, 157.9244050), (0.000600, 156, 155.9242780),), 'Er': ((0.336100, 166, 165.9302900), (0.267800, 168, 167.9323680), (0.229300, 167, 166.9320450), (0.149300, 170, 169.9354600), (0.016100, 164, 163.9291970), (0.001400, 162, 161.9287750),), 'Es': ((0.0, 252, 252.0829700),), 'Eu': ((0.521900, 153, 152.9212260), (0.478100, 151, 150.9198460),), 'F' : ((1.0, 19, 18.9984032),), 'Fe': ((0.917540, 56, 55.9349421), (0.058450, 54, 53.9396148), (0.021190, 57, 56.9353987), (0.002820, 58, 57.9332805),), 'Fm': ((0.0, 257, 257.0950990),), 'Fr': ((0.0, 223, 223.0197307),), 'Ga': ((0.601080, 69, 68.9255810), (0.398920, 71, 70.9247050),), 'Gd': ((0.248400, 158, 157.9241010), (0.218600, 160, 159.9270510), (0.204700, 156, 155.9221200), (0.156500, 157, 156.9239570), (0.148000, 155, 154.9226190), (0.021800, 154, 153.9208620), (0.002000, 152, 151.9197880),), 'Ge': ((0.362800, 74, 73.9211782), (0.275400, 72, 71.9220762), (0.208400, 70, 69.9242504), (0.077300, 73, 72.9234594), (0.076100, 76, 75.9214027),), 'H' : ((0.999850, 1, 1.0078250), (0.000150, 2, 2.0141018), (0.0, 3, 3.0160492),), 'He': ((0.999999, 4, 4.0026032), (0.000001, 3, 3.0160293),), 'Hf': ((0.350800, 180, 179.9465488), (0.272800, 178, 177.9436977), (0.186000, 177, 176.9432200), (0.136200, 179, 178.9458151), (0.052600, 176, 175.9414018), (0.001600, 174, 173.9400400),), 'Hg': ((0.298600, 202, 201.9706260), (0.231000, 200, 199.9683090), (0.168700, 199, 198.9682620), (0.131800, 201, 200.9702850), (0.099700, 198, 197.9667520), (0.068700, 204, 203.9734760), (0.001500, 196, 195.9658150),), 'Ho': ((1.0, 165, 164.9303190),), 'Hs': ((0.0, 277, 258.0984250),), 'I' : ((1.0, 127, 126.9044680),), 'In': ((0.957100, 115, 114.9038780), (0.042900, 113, 112.9040610),), 'Ir': ((0.627000, 193, 192.9629240), (0.373000, 191, 190.9605910),), 'K' : ((0.932581, 39, 38.9637069), (0.067302, 41, 40.9618260), (0.000117, 40, 39.9639987),), 'Kr': ((0.570000, 84, 83.9115070), (0.173000, 86, 85.9106103), (0.115800, 82, 81.9134846), (0.114900, 83, 82.9141360), (0.022800, 80, 79.9163780), (0.003500, 78, 77.9203860),), 'La': ((0.999100, 139, 138.9063480), (0.000900, 138, 137.9071070),), 'Li': ((0.924100, 7, 7.0160040), (0.075900, 6, 6.0151223),), 'Lr': ((0.0, 262, 258.0984250),), 'Lu': ((0.974100, 175, 174.9407679), (0.025900, 176, 175.9426824),), 'Md': ((0.0, 258, 258.0984250), (0.0, 256, 256.0940500),), 'Mg': ((0.789900, 24, 23.9850419), (0.110100, 26, 25.9825930), (0.100000, 25, 24.9858370),), 'Mn': ((1.0, 55, 54.9380496),), 'Mo': ((0.241300, 98, 97.9054078), (0.166800, 96, 95.9046789), (0.159200, 95, 94.9058415), (0.148400, 92, 91.9068100), (0.096300, 100, 99.9074770), (0.095500, 97, 96.9060210), (0.092500, 94, 93.9050876),), 'Mt': ((0.0, 268, 258.0984250),), 'N' : ((0.996320, 14, 14.0030740), (0.003680, 15, 15.0001089),), 'Na': ((1.0, 23, 22.9897697),), 'Nb': ((1.0, 93, 92.9063775),), 'Nd': ((0.272000, 142, 141.9077190), (0.238000, 144, 143.9100830), (0.172000, 146, 145.9131120), (0.122000, 143, 142.9098100), (0.083000, 145, 144.9125690), (0.057000, 148, 147.9168890), (0.056000, 150, 149.9208870),), 'Ne': ((0.904800, 20, 19.9924402), (0.092500, 22, 21.9913855), (0.002700, 21, 20.9938467),), 'Ni': ((0.680769, 58, 57.9353479), (0.262231, 60, 59.9307906), (0.036345, 62, 61.9283488), (0.011399, 61, 60.9310604), (0.009256, 64, 63.9279696),), 'No': ((0.0, 259, 258.0984250),), 'Np': ((0.0, 239, 239.0529314), (0.0, 237, 237.0481673),), 'O' : ((0.997570, 16, 15.9949146), (0.002050, 18, 17.9991604), (0.000380, 17, 16.9991315),), 'Os': ((0.407800, 192, 191.9614790), (0.262600, 190, 189.9584450), (0.161500, 189, 188.9581449), (0.132400, 188, 187.9558360), (0.019600, 187, 186.9557479), (0.015900, 186, 185.9538380), (0.000200, 184, 183.9524910),), 'P' : ((1.0, 31, 30.9737615),), 'Pa': ((1.0, 231, 231.0358789),), 'Pb': ((0.524000, 208, 207.9766360), (0.241000, 206, 205.9744490), (0.221000, 207, 206.9758810), (0.014000, 204, 203.9730290),), 'Pd': ((0.273300, 106, 105.9034830), (0.264600, 108, 107.9038940), (0.223300, 105, 104.9050840), (0.117200, 110, 109.9051520), (0.111400, 104, 103.9040350), (0.010200, 102, 101.9056080),), 'Pm': ((0.0, 147, 146.9151340), (0.0, 145, 144.9127440),), 'Po': ((0.0, 210, 209.9828570), (0.0, 209, 208.9824160),), 'Pr': ((1.0, 141, 140.9076480),), 'Pt': ((0.338320, 195, 194.9647740), (0.329670, 194, 193.9626640), (0.252420, 196, 195.9649350), (0.071630, 198, 197.9678760), (0.007820, 192, 191.9610350), (0.000140, 190, 189.9599300),), 'Pu': ((0.0, 244, 244.0641980), (0.0, 242, 242.0587368), (0.0, 241, 241.0568453), (0.0, 240, 240.0538075), (0.0, 239, 239.0521565), (0.0, 238, 238.0495534),), 'Ra': ((0.0, 228, 228.0310641), (0.0, 226, 226.0254026), (0.0, 224, 224.0202020), (0.0, 223, 223.0184970),), 'Rb': ((0.721700, 85, 84.9117893), (0.278300, 87, 86.9091835),), 'Re': ((0.626000, 187, 186.9557508), (0.374000, 185, 184.9529557),), 'Rf': ((0.0, 261, 258.0984250),), 'Rh': ((1.0, 103, 102.9055040),), 'Rn': ((0.0, 222, 222.0175705), (0.0, 220, 220.0113841), (0.0, 211, 210.9905850),), 'Ru': ((0.315500, 102, 101.9043495), (0.186200, 104, 103.9054300), (0.170600, 101, 100.9055822), (0.127600, 99, 98.9059393), (0.126000, 100, 99.9042197), (0.055400, 96, 95.9075980), (0.018700, 98, 97.9052870),), 'S' : ((0.949300, 32, 31.9720707), (0.042900, 34, 33.9678668), (0.007600, 33, 32.9714585), (0.000200, 36, 35.9670809),), 'Sb': ((0.572100, 121, 120.9038180), (0.427900, 123, 122.9042157),), 'Sc': ((1.0, 45, 44.9559102),), 'Se': ((0.496100, 80, 79.9165218), (0.237700, 78, 77.9173095), (0.093700, 76, 75.9192141), (0.087300, 82, 81.9167000), (0.076300, 77, 76.9199146), (0.008900, 74, 73.9224766),), 'Sg': ((0.0, 266, 258.0984250),), 'Si': ((0.922297, 28, 27.9769265), (0.046832, 29, 28.9764947), (0.030872, 30, 29.9737702),), 'Sm': ((0.267500, 152, 151.9197280), (0.227500, 154, 153.9222050), (0.149900, 147, 146.9148930), (0.138200, 149, 148.9171800), (0.112400, 148, 147.9148180), (0.073800, 150, 149.9172710), (0.030700, 144, 143.9119950),), 'Sn': ((0.325800, 120, 119.9021966), (0.242200, 118, 117.9016060), (0.145400, 116, 115.9017440), (0.085900, 119, 118.9033090), (0.076800, 117, 116.9029540), (0.057900, 124, 123.9052746), (0.046300, 122, 121.9034401), (0.009700, 112, 111.9048210), (0.006600, 114, 113.9027820), (0.003400, 115, 114.9033460),), 'Sr': ((0.825800, 88, 87.9056143), (0.098600, 86, 85.9092624), (0.070000, 87, 86.9088793), (0.005600, 84, 83.9134250),), 'Ta': ((0.999880, 181, 180.9479960), (0.000120, 180, 179.9474660),), 'Tb': ((1.0, 159, 158.9253430),), 'Tc': ((0.0, 99, 98.9062546), (0.0, 98, 97.9072160), (0.0, 97, 96.9063650),), 'Te': ((0.340800, 130, 129.9062228), (0.317400, 128, 127.9044614), (0.188400, 126, 125.9033055), (0.070700, 125, 124.9044247), (0.047400, 124, 123.9028195), (0.025500, 122, 121.9030471), (0.008900, 123, 122.9042730), (0.000900, 120, 119.9040200),), 'Th': ((1.0, 232, 232.0380504), (0.0, 230, 230.0331266),), 'Ti': ((0.737200, 48, 47.9479471), (0.082500, 46, 45.9526295), (0.074400, 47, 46.9517638), (0.054100, 49, 48.9478708), (0.051800, 50, 49.9447921),), 'Tl': ((0.704760, 205, 204.9744120), (0.295240, 203, 202.9723290),), 'Tm': ((1.0, 169, 168.9342110),), 'U' : ((0.992745, 238, 238.0507826), (0.007200, 235, 235.0439231), (0.000055, 234, 234.0409456), (0.0, 236, 236.0455619), (0.0, 233, 233.0396280),), 'V' : ((0.997500, 51, 50.9439637), (0.002500, 50, 49.9471628),), 'W' : ((0.306400, 184, 183.9509326), (0.284300, 186, 185.9543620), (0.265000, 182, 181.9482060), (0.143100, 183, 182.9502245), (0.001200, 180, 179.9467060),), 'Xe': ((0.268900, 132, 131.9041545), (0.264400, 129, 128.9047795), (0.211800, 131, 130.9050819), (0.104400, 134, 133.9053945), (0.088700, 136, 135.9072200), (0.040800, 130, 129.9035079), (0.019200, 128, 127.9035304), (0.000900, 126, 125.9042690), (0.000900, 124, 123.9058958),), 'Y' : ((1.0, 89, 88.9058479),), 'Yb': ((0.318300, 174, 173.9388581), (0.218300, 172, 171.9363777), (0.161300, 173, 172.9382068), (0.142800, 171, 170.9363220), (0.127600, 176, 175.9425680), (0.030400, 170, 169.9347590), (0.001300, 168, 167.9338940),), 'Zn': ((0.486300, 64, 63.9291466), (0.279000, 66, 65.9260368), (0.187500, 68, 67.9248476), (0.041000, 67, 66.9271309), (0.006200, 70, 69.9253250),), 'Zr': ((0.514500, 90, 89.9047037), (0.173800, 94, 93.9063158), (0.171500, 92, 91.9050401), (0.112200, 91, 90.9056450), (0.028000, 96, 95.9082760),), } Qt = QtCore.Qt QPointF = QtCore.QPointF QRectF = QtCore.QRectF RADIUS = 50.0 BOND_SEP = 3.0 NULL_RECT = QRectF() NULL_POINT = QPointF() FONT_METRIC = QtGui.QFontMetricsF(ELEMENT_FONT) SHADOW_COLOR = QtGui.QColor(64, 64, 64) SHADOW_RADIUS = 4 SHADOW_OFFSET = (2, 2) AURA_COLOR = QtGui.QColor(255, 255, 255) AURA_OFFSET = (0, 0) AURA_RADIUS = 4 ItemIsMovable = QtWidgets.QGraphicsItem.ItemIsMovable ItemIsSelectable = QtWidgets.QGraphicsItem.ItemIsSelectable ItemPositionChange = QtWidgets.QGraphicsItem.ItemPositionChange ItemSendsGeometryChanges = QtWidgets.QGraphicsItem.ItemSendsGeometryChanges REGION_PEN = QtGui.QPen(HIGHLIGHT, 0.8, Qt.SolidLine) BOND_CHANGE_DICT = {'single' : 'double', 'aromatic' : 'double', 'singleplanar': 'double', 'double' : 'triple', 'triple' : 'single', 'dative' : 'single'}
[docs]class AtomLabel(QtWidgets.QGraphicsItem): def __init__(self, scene, atomView, compoundView, atom): super(AtomLabel, self).__init__() # QtWidgets.QGraphicsItem.__init__(self) self.scene = scene #effect = QtWidgets.QGraphicsDropShadowEffect(compoundView) #effect.setBlurRadius(SHADOW_RADIUS) #effect.setColor(SHADOW_COLOR) #effect.setOffset(*SHADOW_OFFSET) self.setAcceptHoverEvents(True) self.setAcceptedMouseButtons(Qt.LeftButton) #self.setGraphicsEffect(effect) self.setZValue(3) self.compoundView = compoundView self.atomView = atomView self.hover = False self.atom = atom self.bbox = NULL_RECT self.drawData = () self.syncLabel() self.setCacheMode(self.DeviceCoordinateCache)
[docs] def hoverEnterEvent(self, event): self.hover = True self.update()
[docs] def hoverLeaveEvent(self, event): self.hover = False self.update()
[docs] def mouseDoubleClickEvent(self, event): if self.atom.element == LINK: return self.compoundView.queryAtomName(self) return QtWidgets.QGraphicsItem.mouseDoubleClickEvent(self, event)
[docs] def syncLabel(self): rad = 15.0 atom = self.atom xa, ya, za = atom.coords if atom.bonds or atom.freeValences: angles = atom.getBondAngles() angles += atom.freeValences angles = [a % (2.0 * PI) for a in angles] angles.sort() angles.append(angles[0] + 2.0 * PI) diffs = [(round(angles[i + 1] - a, 3), a) for i, a in enumerate(angles[:-1])] diffs.sort() delta, angle = diffs[-1] angle += delta / 2.0 else: angle = 1.0 name = atom.name if name: text = name else: text = '?' textRect = FONT_METRIC.tightBoundingRect(text) w = textRect.width() / 1.5 h = textRect.height() / 2.0 x = xa + (rad + w) * sin(angle) y = ya + (rad + h) * cos(angle) # Global absolute centre self.setPos(QPointF(x, y)) center = QPointF(-w, h) self.drawData = (center, text) rect = QRectF(QPointF(-w, -h), QPointF(w, h)) self.bbox = rect.adjusted(-h, -h, h, h) self.update()
[docs] def boundingRect(self): if not self.compoundView.nameAtoms: return NULL_RECT return self.bbox
[docs] def paint(self, painter, option, widget): painter.setRenderHint(QtGui.QPainter.Antialiasing, True) #if self.compoundView.showSkeletalFormula and self.atom.element == 'H': #for n in self.atom.neighbours: #if n.element != 'C': #return useName = self.compoundView.nameAtoms if useName and self.drawData: point, text = self.drawData painter.setFont(ELEMENT_FONT) if self.hover: painter.setPen(Qt.white) elif not isinstance(self.compoundView, QtWidgets.QGraphicsItem): if hasattr(self.compoundView, 'setAtomColorWhite'): painter.setPen(Qt.white) else: painter.setPen(Qt.black) painter.drawText(point, text) painter.setRenderHint(QtGui.QPainter.Antialiasing, False)
[docs]class SelectionBox(QtWidgets.QGraphicsItem): def __init__(self, scene, compoundView): super(SelectionBox, self).__init__() # QtWidgets.QGraphicsItem.__init__(self) # self._scene = scene # print(self.scene(), scene, 'TEST') self.setZValue(1) self.compoundView = compoundView self.begin = None self.region = None
[docs] def updateRegion(self, begin=None, end=None): if begin and end: self.region = QRectF(begin, end).normalized() self.begin = begin elif begin: self.region = QRectF(begin, begin).normalized() self.begin = begin elif end and self.begin: self.region = QRectF(self.begin, end).normalized() else: self.region = None self.begin = None self.update()
[docs] def boundingRect(self): if self.region: pad = 2 return self.region.adjusted(-pad, -pad, pad, pad) else: return NULL_RECT
[docs] def paint(self, painter, option, widget): if self.region: painter.setPen(REGION_PEN) painter.setBrush(HIGHLIGHT_BG) painter.drawRect(self.region)
[docs]class AtomGroupItem(QtWidgets.QGraphicsItem): def __init__(self, scene, compoundView, atomGroup): super(AtomGroupItem, self).__init__() # QtWidgets.QGraphicsItem.__init__(self) self.scene = scene compoundView.groupItems[atomGroup] = self self.compoundView = compoundView self.atomGroup = atomGroup self.atoms = atomGroup.varAtoms self.setZValue(-1) self.bbox = NULL_RECT self.drawData = () self.center = NULL_POINT self.syncGroup() self.setCacheMode(self.DeviceCoordinateCache)
[docs] def boundingRect(self): return self.bbox
[docs] def paint(self, painter, option, widget): pass
[docs]class EquivItem(AtomGroupItem):
[docs] def syncGroup(self): coords = [a.coords for a in self.atoms] n = float(len(coords)) xl = [xyz[0] for xyz in coords] yl = [xyz[1] for xyz in coords] xc = sum(xl) / n yc = sum(yl) / n xa = min(xl) ya = min(yl) xb = max(xl) yb = max(yl) dx = xb - xa dy = yb - ya self.center = QPointF(xc - xa, yc - ya) self.drawData = [QPointF(xyz[0] - xa, xyz[1] - ya) for xyz in coords] self.setPos(QPointF(xa, ya)) rect = QRectF(QPointF(0.0, 0.0), QPointF(dx, dy)) pad = 2 self.bbox = rect.normalized().adjusted(-pad, -pad, pad, pad) self.update()
[docs] def paint(self, painter, option, widget): if not self.compoundView.showGroups: return pen = QtGui.QPen(EQUIV_COLOR, 2, Qt.DotLine) painter.setPen(pen) painter.setBrush(EQUIV_COLOR) center = self.center for point in self.drawData: painter.drawLine(point, center) pen = QtGui.QPen(EQUIV_COLOR, 2, Qt.SolidLine) painter.setPen(pen) painter.drawEllipse(center, 2.0, 2.0)
[docs]class ProchiralItem(AtomGroupItem):
[docs] def syncGroup(self): atoms = self.atoms if len(atoms) == 2: atomA, atomB = atoms x1, y1, z1 = atomA.coords x2, y2, z2 = atomB.coords dx = x2 - x1 dy = y2 - y1 anchorPoint = QPointF(x1, y1) startPoint = QPointF(0.0, 0.0) endPoint = QPointF(dx, dy) else: groupDict = self.compoundView.groupItems groups = list(self.atomGroup.subGroups) if len(groups) == 2: groupA = groups[0] groupB = groups[1] gItemA = groupDict.get(groupA) gItemB = groupDict.get(groupB) if gItemA and gItemB: centerA = gItemA.center centerB = gItemB.center anchorPoint = centerA startPoint = self.mapFromItem(gItemA, centerA) endPoint = self.mapFromItem(gItemB, centerB) else: self.drawData = None return else: self.drawData = None return pad = 2.0 rect = QRectF(startPoint, endPoint) self.center = rect.center() self.bbox = rect.normalized().adjusted(-pad, -pad, pad, pad) self.drawData = (startPoint, endPoint) self.setPos(anchorPoint) self.update()
[docs] def paint(self, painter, option, widget): if not self.compoundView.showGroups: return if self.drawData: startPoint, endPoint = self.drawData pen = QtGui.QPen(PROCHIRAL_COLOR, 2, Qt.DotLine) painter.setPen(pen) painter.drawLine(startPoint, endPoint)
[docs]class AromaticItem(AtomGroupItem):
[docs] def syncGroup(self): coords = [a.coords for a in self.atoms] n = len(coords) nl = range(n) n = float(n) xl = [xyz[0] for xyz in coords] yl = [xyz[1] for xyz in coords] xc = sum(xl) / n yc = sum(yl) / n xa = min(xl) ya = min(yl) xb = max(xl) yb = max(yl) dx = xb - xa dy = yb - ya dx2 = [(x - xc) * (x - xc) for x in xl] dy2 = [(y - yc) * (y - yc) for y in yl] d2 = [dx2[i] + dy2[i] for i in nl] r = sqrt(min(d2)) * cos(PI / n) r = max(2 * BOND_SEP, r - 2 * BOND_SEP) self.center = QPointF(xc - xa, yc - ya) self.drawData = [r, r] self.setPos(QPointF(xa, ya)) rect = QRectF(QPointF(0.0, 0.0), QPointF(dx, dy)) pad = 2 self.bbox = rect.normalized().adjusted(-pad, -pad, pad, pad) self.update()
[docs] def paint(self, painter, option, widget): r1, r2 = self.drawData center = self.center pen = QtGui.QPen(Qt.black, 1, Qt.SolidLine) painter.setPen(pen) painter.drawEllipse(center, r1, r2)
[docs]class AtomItem(QtWidgets.QGraphicsItem): def __init__(self, scene, compoundView, atom): super(AtomItem, self).__init__() # QtWidgets.QGraphicsItem.__init__(self) self.scene = scene compoundView.atomViews[atom] = self self.compoundView = compoundView self.variant = atom.variant self.atom = atom self.bondItems = [] self.bbox = NULL_RECT self.setFlag(ItemIsSelectable) self.selected = False self.setFlag(ItemIsMovable) self.setFlag(ItemSendsGeometryChanges) self.setAcceptHoverEvents(True) self.setAcceptedMouseButtons(Qt.LeftButton) color = ELEMENT_DATA.get(atom.element, ELEMENT_DEFAULT)[1] self.gradient = QtGui.QRadialGradient(0, 0, 9, 4, -4) self.gradient.setColorAt(1, color.darker()) self.gradient.setColorAt(0.5, color) self.gradient.setColorAt(0, color.lighter()) self.gradient2 = QtGui.QRadialGradient(0, 0, 9, 4, -4) self.gradient2.setColorAt(1, color.darker().darker()) self.gradient2.setColorAt(0.5, color.darker()) self.gradient2.setColorAt(0, color) #effect = QtWidgets.QGraphicsDropShadowEffect(compoundView) #effect.setBlurRadius(SHADOW_RADIUS) #effect.setColor(SHADOW_COLOR) #effect.setOffset(*SHADOW_OFFSET) #self.setGraphicsEffect(effect) self.setCacheMode(self.DeviceCoordinateCache) self.highlights = set() self.makeBonds = set() self.hover = False self.rightBond = False self.freeDrag = False self.atomLabel = AtomLabel(scene, self, compoundView, atom) compoundView.scene.addItem(self.atomLabel) self.syncToAtom()
[docs] def itemChange(self, change, value): if change == ItemPositionChange: compoundView = self.compoundView x0, y0, z = self.atom.coords x = value.x() y = value.y() dx = x0 - x dy = y0 - y freeDrag = self.freeDrag nSelected = len(compoundView.selectedViews) # This code block is to handle position changes of branches when snapping to grid. # In practice it will allow two branches to swap places. if compoundView.snapToGrid and (dx != 0 or dy != 0) and nSelected <= 1: # Find which of the neighbouring atoms form the longest branch. A more proper check # to ensure the backbone is found could be done. # The shortest branch and the branch of the selected atom can be moved. neighbours = sorted(self.atom.neighbours, key=lambda atom: atom.name) prevAtom = None longestBranchLen = 0 for neighbour in neighbours: branch = self.atom.findAtomsInBranch(neighbour) branchLen = len(branch) if branchLen > longestBranchLen: prevAtom = neighbour longestBranch = branch longestBranchLen = branchLen if prevAtom: xP = prevAtom.coords[0] yP = prevAtom.coords[1] # bondLength = hypot(x0 - xP, y0 - yP) bondLength = 50 # TODO: A smarter way of setting the right bondlength neighbours = sorted(prevAtom.neighbours, key=lambda atom: atom.name) longestBranchLen = 0 # Find a reference atom next to prevAtom to calculate bond angles. frozenNeighbour = None for neighbour in neighbours: if neighbour != self.atom and neighbour.element != 'H': branch = prevAtom.findAtomsInBranch(neighbour) branchLen = len(branch) if branchLen > longestBranchLen: longestBranch = branch longestBranchLen = branchLen frozenNeighbour = neighbour prevAngle = round(degrees(prevAtom.getBondAngle(neighbour)), 0) if frozenNeighbour: neighbour = frozenNeighbour atoms = neighbour.findAtomsInBranch(prevAtom) - set([prevAtom, self.atom]) else: for neighbour in self.atom.neighbours: if neighbour.element != 'H': frozenNeighbour = prevAtom prevAngle = 330 atoms = set([]) break if not frozenNeighbour: print("No frozenNeighbour", longestBranchLen) prevAtom = None freeDrag = True else: freeDrag = True # If the item was selected and Ctrl is pressed when the item is moved the item does not count a selected. Let it be freely movable. # If there are more atoms selected also let them be freely movable. if self.selected and not freeDrag: prefAngles = prevAtom.getPreferredBondAngles(prevAngle, None, atoms, False) # If the dragged atom only has neighbours with hydrogens (i.e. the reference atom has no other visible atoms connected) add # an extra possible angle. if frozenNeighbour == prevAtom and prevAngle == 330: prefAngles.append(0) if len(prefAngles) < 2: # or self.atom.atomInSameRing(prevAtom): value.setX(x0) value.setY(y0) return QtWidgets.QGraphicsItem.itemChange(self, change, value) prevAngle = radians(prevAngle) currAngle = (prevAtom.getBondAngle(self.atom) - prevAngle) % (2.0 * PI) newAngle = (atan2(y - yP, x - xP) - prevAngle) % (2.0 * PI) # Find which preferred angle is closest to the new angle. bestAngle = None bestDiff = None for angle in prefAngles: if angle < 0: angle += 360 angle = radians(angle) diff = abs(newAngle - angle) if not bestAngle or diff < bestDiff: bestAngle = angle bestDiff = diff # If the current angle is agreeing best just set the selection circle to where the atom is # and return. if abs(bestAngle - currAngle) < 0.001: value.setX(x0) value.setY(y0) return QtWidgets.QGraphicsItem.itemChange(self, change, value) bestAngle += prevAngle if self.atom.element == 'H': x = xP + bondLength * 0.75 * cos(bestAngle) y = yP + bondLength * 0.75 * sin(bestAngle) else: x = xP + bondLength * cos(bestAngle) y = yP + bondLength * sin(bestAngle) self.atom.setCoords(x, y, z) if prevAtom and not self.atom.atomInSameRing(prevAtom): # First snap the atoms in the same branch as the selected atom (these will not take the other branch into account when # deciding their positions). sameBranch = sorted(prevAtom.findAtomsInBranch(self.atom) - set([self.atom])) atoms -= longestBranch atoms = sorted(atoms, key=lambda atom: atom.name) if len(sameBranch) > 0: for atom in sameBranch: if atom in atoms: atoms.remove(atom) self.variant.snapAtomsToGrid(sameBranch, self.atom, compoundView.showSkeletalFormula, bondLength=bondLength) # Snap the second branch to the grid taking the first branch into account. if len(atoms) > 0: self.variant.snapAtomsToGrid(atoms, prevAtom, compoundView.showSkeletalFormula, bondLength=bondLength) self.compoundView.updateAll() # Synch positions # implicitly set bonds too else: self.atom.setCoords(x, y, z) # Make bond detection d = RADIUS # When using snapToGrid the distance between the atoms is so small that double and tripe bonds are created too easily if d2 is not made smaller. if compoundView.snapToGrid: d2 = d * 0.25 else: d2 = d * 0.5 r2 = 15 atomViews = compoundView.atomViews self.makeBonds = set() addBond = self.makeBonds.add valencesS = self.atom.freeValences atoms = self.variant.varAtoms - set([self.atom]) positionsS = [(x + r2 * sin(angle), y + r2 * cos(angle), angle) for angle in valencesS] if valencesS: for atom in atoms: valences = atom.freeValences if not valences: continue atomView = atomViews.get(atom) if not atomView: continue x1, y1, z1 = atom.coords dx = x1 - x dy = y1 - y dist2 = (dx * dx) + (dy * dy) # consider using items directly if dist2 < d * d: # Other atom positions = [(x1 + r2 * sin(a), y1 + r2 * cos(a), a) for a in valences] for x2, y2, a2 in positions: for x3, y3, a3 in positionsS: dx2 = x3 - x2 dy2 = y3 - y2 if (dx2 * dx2) + (dy2 * dy2) < (d2 * d2): atomView.highlights.add(a2) self.highlights.add(a3) addBond((atom, self.atom)) atomView.update() self.update() for bondItem in self.bondItems: bondItem.syncToBond() groupItems = compoundView.groupItems for group in self.atom.atomGroups: groupItems[group].syncGroup() self.atomLabel.syncLabel() return QtWidgets.QGraphicsItem.itemChange(self, change, value)
[docs] def moveAtom(self, coords): if isinstance(coords, tuple): (x, y) = coords else: x = coords.x() y = coords.y() z = self.atom.coords[2] self.atom.setCoords(float(x), float(y), z) self.syncToAtom()
[docs] def syncToAtom(self): atom = self.atom compoundView = self.compoundView bondDict = compoundView.bondItems self.bondItems = [bondDict[bond] for bond in atom.bonds if bond in bondDict] if atom.chirality: r = 21.0 elif atom.freeValences or abs(atom.charge) > 1: r = 18.0 else: r = 15.0 x, y, z = atom.coords # Global location coords = QPointF(x, y) # Define where local origin is in global self.setPos(coords) rightBond = 0 leftBond = 0 if abs(atom.charge) > 1 or atom.chirality: adj = 9 else: adj = 0 #if self.compoundView.showSkeletalFormula: #for neighbour in atom.neighbours: #nH = 0 #if neighbour.element == 'H': #nH += 1 #if atom.element != 'C' or atom.freeValences or atom.chirality:# or nH == 4: #for neighbour in atom.neighbours: #if neighbour.element == 'H': #continue #nX, nY, nZ = neighbour.coords #if nX > x: #dY = max(0.0001, abs(nY - y)) #currD = 1/dY #if currD > rightBond: #rightBond = currD #elif nX < x: #dY = max(0.0001, abs(nY - y)) #currD = 1/dY #if currD > rightBond: #leftBond = currD #adj += 9 #if nH > 1: #adj += 9 # In local coords if rightBond > leftBond: self.rightBond = True self.bbox = QRectF(QPointF(-r - adj, -r), QPointF(r, r + adj)) else: self.rightBond = False self.bbox = QRectF(QPointF(-r, -r), QPointF(r + adj, r + adj)) for bondItem in self.bondItems: bondItem.syncToBond() groupItems = compoundView.groupItems for group in atom.atomGroups: groupItems[group].syncGroup() if self.compoundView.autoChirality: atom.autoSetChirality() for neighbour in atom.neighbours: neighbour.autoSetChirality() self.atomLabel.syncLabel() self.update()
[docs] def boundingRect(self): #if self.compoundView.showSkeletalFormula and self.atom.element == 'H': #return NULL_RECT return self.bbox
[docs] def delete(self): compoundView = self.compoundView atom = self.atom self.deselect() scene = compoundView.scene for bondItem in list(self.bondItems): bondItem.delete() del compoundView.atomViews[atom] # Delete the master atom, not the var one masterAtom = atom.atom if not masterAtom.isDeleted: masterAtom.delete() del self.atomLabel del self
[docs] def deselect(self): self.selected = False self.setSelected(False) selected = self.compoundView.selectedViews if self in selected: selected.remove(self) for bondItem in self.bondItems: bondItem.deselect() self.update()
[docs] def select(self): self.selected = True self.setSelected(True) selected = self.compoundView.selectedViews selected.add(self) for bondItem in self.bondItems: atomItemA, atomItemB = bondItem.atomItems if atomItemA.selected and atomItemB.selected: bondItem.select() self.update()
[docs] def hoverEnterEvent(self, event): self.hover = True self.update()
[docs] def hoverLeaveEvent(self, event): self.hover = False self.update()
[docs] def mousePressEvent(self, event): QtWidgets.QGraphicsItem.mousePressEvent(self, event) selected = list(self.compoundView.selectedViews) mods = event.modifiers() button = event.button() haveCtrl = mods & Qt.CTRL haveShift = mods & Qt.SHIFT if haveCtrl or haveShift: if self.selected: self.deselect() else: self.select() elif not self.selected: for view in selected: view.deselect() self.select() self.update()
[docs] def mouseMoveEvent(self, event): mods = event.modifiers() haveCtrl = mods & Qt.CTRL if haveCtrl: self.freeDrag = True if not self.selected: self.select() QtWidgets.QGraphicsItem.mouseMoveEvent(self, event) self.freeDrag = False
[docs] def mouseDoubleClickEvent(self, event): if self.atomLabel.hover: self.compoundView.queryAtomName(self.atomLabel) return mods = event.modifiers() button = event.button() haveCtrl = mods & Qt.CTRL haveShift = mods & Qt.SHIFT if haveCtrl or haveShift: if self.selected: self.deselect() else: self.select() else: nAtoms = 1 nPrev = 0 atoms = set([self.atom]) while nAtoms != nPrev: nPrev = nAtoms for atom in list(atoms): atoms.update(atom.neighbours) nAtoms = len(atoms) viewDict = self.compoundView.atomViews for atom in self.variant.varAtoms: atomView = viewDict[atom] if atom in atoms: atomView.select() else: atomView.deselect() self.update() QtWidgets.QGraphicsItem.mouseDoubleClickEvent(self, event)
[docs] def mouseReleaseEvent(self, event): parent = self.compoundView atom = self.atom x, y, z = atom.coords if isinstance(parent, QtWidgets.QGraphicsItem): w = parent.boundingRect().width() h = parent.boundingRect().height() else: w = parent.width() h = parent.height() tl = parent.mapToScene(0, 0) br = parent.mapToScene(w, h) x1 = tl.x() x2 = br.x() y1 = tl.y() y2 = br.y() if x < x1 or y < y1 or x > x2 or y > y2: if not atom.neighbours: self.delete() else: d2 = RADIUS / 2.0 z = atom.coords[2] atom.setCoords(float(x), float(y), z) atoms = set() for varAtomA, varAtomB in self.makeBonds: elemA = varAtomA.element elemB = varAtomB.element if not varAtomA.freeValences: continue if not varAtomB.freeValences: continue if set([elemA, elemB]) in DISALLOWED: continue if varAtomA in varAtomB.neighbours: bond = self.variant.getBond(varAtomA, varAtomB) if varAtomA.freeValences and varAtomB.freeValences: bond.setBondType(BOND_CHANGE_DICT[bond.bondType]) else: Bond((varAtomA, varAtomB), autoVar=True) atoms.add(varAtomA) atoms.add(varAtomB) #neighbourhood = set() #for atom in atoms: # neighbourhood.update(atom.neighbours) #if atoms: # #if atoms == neighbourhood: # # self.variant.minimise2d([atoms.pop(),]) # #else: # self.variant.minimise2d([atom,]) for bond in self.atom.bonds: atomA, atomB = bond.varAtoms if atomA is not atom: atomA.updateValences() if atomB is not atom: atomB.updateValences() self.atom.updateValences() if isinstance(parent, QtWidgets.QGraphicsItem): if parent.showSkeletalFormula: parent.alignMolecule() else: parent.alignMolecule(False) self.compoundView.updateAll() # Render new objs QtWidgets.QGraphicsItem.mouseReleaseEvent(self, event) self.update()
[docs] def paint(self, painter, option, widget): textAlign = QtCore.Qt.AlignCenter qRect = QRectF qBrush = QtGui.QBrush qPoint = QPointF qPoly = QtGui.QPolygonF showChargeSymbols = self.compoundView.showChargeSymbols showChiralities = self.compoundView.showChiralities highlights = self.highlights atom = self.atom r = 9.0 r2 = 15.0 r3 = 3.0 d = RADIUS d2 = d / 2.0 elem = atom.element drawText = painter.drawText drawEllipse = painter.drawEllipse drawLine = painter.drawLine drawPoly = painter.drawPolygon painter.setPen(Qt.black) painter.setFont(ELEMENT_FONT) color = ELEMENT_DATA.get(elem, ELEMENT_DEFAULT)[1] if isinstance(self.compoundView, QtWidgets.QGraphicsItem): # SpecView if self.compoundView.container: backgroundColor = QtGui.QColor(*self.glWidget._hexToRgba(self.compoundView.container.mainApp.colors[0])) foregroundColor = QtGui.QColor(*self.glWidget._hexToRgba(self.compoundView.container.mainApp.colors[1])) # Analysis elif self.compoundView.glWidget: raise NotImplementedError("Needs rewriting - analysisProfile does not exist") backgroundColor = QtGui.QColor() backgroundColor.setRgbF(*self.glWidget._hexToRgba(self.glWidget.spectrumWindow.analysisProfile.bgColor)) foregroundColor = Qt.black # Fallback alternative else: backgroundColor = Qt.black foregroundColor = Qt.black else: backgroundColor = self.compoundView.backgroundColor foregroundColor = Qt.blue if not self.compoundView.showSkeletalFormula: if self.hover: brush = qBrush(self.gradient2) else: brush = qBrush(self.gradient) brush.setStyle(Qt.RadialGradientPattern) else: brush = backgroundColor x, y, z = 0, 0, 0 center = qPoint(x, y) painter.setBrush(foregroundColor) for angle in atom.freeValences: x2 = x + r2 * sin(angle) y2 = y + r2 * cos(angle) if angle in highlights: painter.setBrush(HIGHLIGHT) drawEllipse(qPoint(x2, y2), r3, r3) painter.setBrush(foregroundColor) else: drawEllipse(qPoint(x2, y2), r3, r3) if showChiralities and atom.chirality: if atom.bonds or atom.freeValences: angles = atom.getBondAngles() angles += atom.freeValences angles = [-(a - PI / 2.0) % (2.0 * PI) for a in angles] angles.sort() angles.append(angles[0] + 2.0 * PI) diffs = [(round(angles[i + 1] - a, 3), a) for i, a in enumerate(angles[:-1])] diffs.sort() delta, angle = diffs[-1] angle += delta / 2.0 else: angle = 0.0 painter.setBrush(brush) if self.compoundView.showSkeletalFormula: skeletalColor = self.compoundView.showSkeletalFormulaColor #nH = 0 #for neighbour in atom.neighbours: #if neighbour.element == 'H': #nH += 1 nDouble = 0 for bond in atom.bonds: if bond.bondType == 'double': nDouble += 1 #if elem == 'C' and len(atom.freeValences) == 0 and nH < 4 and nDouble < len(atom.bonds): if elem == 'C' and len(atom.freeValences) == 0 and nDouble < len(atom.bonds): transparent = QtGui.QColor(0, 0, 0, 0) painter.setPen(transparent) painter.setBrush(transparent) painter.drawEllipse(center, r, r) if self.selected: painter.setPen(HIGHLIGHT) drawEllipse(center, r + 1, r + 1) painter.setBrush(brush) if showChiralities: if atom.chirality and atom.chirality not in ('e', 'z', 'E', 'Z'): painter.setFont(CHIRAL_FONT) if skeletalColor: painter.setPen(CHIRAL_COLOR) else: painter.setPen(foregroundColor) chirality = atom.chirality if chirality in ('r', 's'): chirality = '(%s)' % chirality.upper() fontMetric = QtGui.QFontMetricsF(painter.font()) width = fontMetric.width(chirality) height = fontMetric.height() chiralityX = x + (r + 1) * cos(angle) - width / 2 chiralityY = y + (r + 1) * sin(angle) + height / 2 drawText(qPoint(chiralityX, chiralityY), chirality) return #if elem == 'H': #for n in atom.neighbours: #if n.element != 'C': #return font = painter.font() smallFont = painter.font() font.setPointSizeF(font.pointSize() * 1.5) painter.setFont(font) fontMetric = QtGui.QFontMetricsF(painter.font()) bbox = fontMetric.tightBoundingRect(elem) width = fontMetric.width(elem) hydrogenWidth = fontMetric.width('H') h2 = bbox.height() / 2.0 w2 = bbox.width() / 2.0 if not skeletalColor: if isinstance(self.compoundView, QtWidgets.QGraphicsItem): # SpecView if self.compoundView.container: color = foregroundColor # Analyis elif self.compoundView.glWidget: color = foregroundColor # Fallback else: color = Qt.darkGray else: color = Qt.darkGray if elem == LINK: angles = atom.getBondAngles() + atom.freeValences if angles: startAngle = -1.0 * angles[0] angles = [startAngle + (i * 2 * PI / 3.0) for i in (0, 1, 2)] if self.selected: painter.setPen(HIGHLIGHT) poly = qPoly() for angle in angles: x2 = x + (r + 1) * sin(angle) y2 = y - (r + 1) * cos(angle) poly.append(qPoint(x2, y2)) drawPoly(poly) painter.setPen(foregroundColor) poly = qPoly() for angle in angles: x2 = x + r * sin(angle) y2 = y - r * cos(angle) poly.append(qPoint(x2, y2)) drawPoly(poly) else: if self.selected: painter.setPen(HIGHLIGHT) drawEllipse(center, r + 1, r + 1) painter.setPen(backgroundColor) drawEllipse(center, r, r) textPoint = qPoint(x - w2, y + h2) painter.setPen(color) drawText(textPoint, elem) #hydrogenText= hydrogenNr = None #if nH != 0: #hydrogenText = 'H' #if nH > 1: #hydrogenNr = "%d" % nH #if hydrogenText: #nrWidth = 0 #if hydrogenNr: #painter.setFont(smallFont) #fontMetric = QtGui.QFontMetricsF(smallFont) #nrWidth = fontMetric.width(hydrogenNr) #if self.rightBond: #textPoint = qPoint(x-width/2-nrWidth, y+h2+bbox.height()/3) #else: #textPoint = qPoint(x+width/2+hydrogenWidth, y+h2+bbox.height()/3) #drawText(textPoint, hydrogenNr) #painter.setFont(font) #if self.rightBond: #textPoint = qPoint(x-width/2-nrWidth-hydrogenWidth, y+h2) #else: #textPoint = qPoint(x+width/2, y+h2) #drawText(textPoint, hydrogenText) if skeletalColor: painter.setPen(foregroundColor) charge = atom.charge if showChiralities: if atom.chirality and atom.chirality not in ('e', 'z', 'E', 'Z'): painter.setFont(CHIRAL_FONT) if skeletalColor: painter.setPen(CHIRAL_COLOR) chirality = atom.chirality if chirality in ('r', 's'): chirality = '(%s)' % chirality.upper() fontMetric = QtGui.QFontMetricsF(painter.font()) width = fontMetric.width(chirality) height = fontMetric.height() chiralityX = x + (r + 1) * cos(angle) - width / 2 chiralityY = y + (r + 1) * sin(angle) + height / 2 drawText(qPoint(chiralityX, chiralityY), chirality) #elif atom.stereo: # painter.setFont(CHIRAL_FONT) # painter.setPen(CHIRAL_COLOR) # drawText(qPoint(x+r, y+r+4), '*') if showChargeSymbols: if charge: painter.setFont(smallFont) if charge == -1: text = '-' if skeletalColor: color = NEG_COLOR elif charge == 1: if skeletalColor: color = POS_COLOR text = '+' elif charge > 0: if skeletalColor: color = POS_COLOR text = '%d+' % charge else: if skeletalColor: color = NEG_COLOR text = '%d-' % abs(charge) if skeletalColor: painter.setPen(CHARGE_BG_COLOR) if self.rightBond: painter.setPen(color) drawText(qPoint(x - r - 3, y - r + 3), text) else: painter.setPen(color) drawText(qPoint(x + r - 3, y - r + 3), text) else: fontMetric = QtGui.QFontMetricsF(painter.font()) bbox = fontMetric.tightBoundingRect(elem) h2 = bbox.height() / 2.0 w2 = bbox.width() / 2.0 if elem == LINK: angles = atom.getBondAngles() + atom.freeValences if angles: startAngle = -1.0 * angles[0] angles = [startAngle + (i * 2 * PI / 3.0) for i in (0, 1, 2)] if self.selected: painter.setPen(HIGHLIGHT) poly = qPoly() for angle in angles: x2 = x + (r + 1) * sin(angle) y2 = y - (r + 1) * cos(angle) poly.append(qPoint(x2, y2)) drawPoly(poly) painter.setPen(Qt.black) poly = qPoly() for angle in angles: x2 = x + r * sin(angle) y2 = y - r * cos(angle) poly.append(qPoint(x2, y2)) drawPoly(poly) else: if self.selected: painter.setPen(HIGHLIGHT) drawEllipse(center, r + 1, r + 1) painter.setPen(Qt.black) drawEllipse(center, r, r) textPoint = qPoint(x - w2, y + h2) drawText(textPoint, elem) charge = atom.charge if showChiralities: if atom.chirality and atom.chirality not in ('e', 'z', 'E', 'Z'): painter.setFont(CHIRAL_FONT) painter.setPen(CHIRAL_COLOR) chirality = atom.chirality if chirality in ('r', 's'): chirality = '(%s)' % chirality.upper() fontMetric = QtGui.QFontMetricsF(painter.font()) width = fontMetric.width(chirality) height = fontMetric.height() chiralityX = x + (r2 + 1) * cos(angle) - width / 2 chiralityY = y + (r2 + 1) * sin(angle) + height / 2 drawText(qPoint(chiralityX, chiralityY), chirality) #elif atom.stereo: # painter.setFont(CHIRAL_FONT) # painter.setPen(CHIRAL_COLOR) # drawText(qPoint(x+r, y+r+4), '*') if showChargeSymbols: if charge: if charge == -1: text = '-' color = NEG_COLOR elif charge == 1: color = POS_COLOR text = '+' elif charge > 0: color = POS_COLOR text = '%d+' % charge else: color = NEG_COLOR text = '%d-' % abs(charge) painter.setFont(CHARGE_FONT) painter.setPen(CHARGE_BG_COLOR) drawText(qPoint(x + r - 2, y - r + 2), text) drawText(qPoint(x + r - 4, y - r + 4), text) drawText(qPoint(x + r - 2, y - r + 4), text) drawText(qPoint(x + r - 4, y - r + 2), text) painter.setPen(color) drawText(qPoint(x + r - 3, y - r + 3), text) self.highlights = set()
[docs]class BondItem(QtWidgets.QGraphicsItem): def __init__(self, scene, compoundView, bond): super(BondItem, self).__init__() # QtWidgets.QGraphicsItem.__init__(self) self.scene = scene compoundView.bondItems[bond] = self effect = QtWidgets.QGraphicsDropShadowEffect(compoundView) effect.setBlurRadius(SHADOW_RADIUS) effect.setColor(SHADOW_COLOR) effect.setOffset(*SHADOW_OFFSET) #self.setGraphicsEffect(effect) #self.setFlag(ItemIsSelectable) #self.setFlag(ItemIsMovable) #self.setCacheMode(self.NoCache) self.setAcceptedMouseButtons(Qt.LeftButton) self.selected = False self.setZValue(-2) self.compoundView = compoundView self.bond = bond self.atomItems = [] self.setCacheMode(self.DeviceCoordinateCache) self.drawData = () self.bbox = NULL_RECT self.syncToBond()
[docs] def delete(self): for atomItem in self.atomItems: atomItem.bondItems.remove(self) compoundView = self.compoundView del compoundView.bondItems[self.bond] self.bond.deleteAll() del self
[docs] def select(self): self.selected = True self.update()
[docs] def deselect(self): self.selected = False self.update()
[docs] def syncToBond(self): bond = self.bond atomA, atomB = bond.varAtoms atomDict = self.compoundView.atomViews self.atomItems = [atomDict[atomA], atomDict[atomB]] for atomItem in self.atomItems: if self not in atomItem.bondItems: atomItem.bondItems.append(self) xa, ya, za = atomA.coords xb, yb, zb = atomB.coords # Model curation dx = xb - xa dy = yb - ya dz = zb - za angleA = atan2(dx, -dy) xg = BOND_SEP * cos(angleA) yg = BOND_SEP * sin(angleA) if atomA.isLabile or atomB.isLabile: isLabile = True if self.compoundView.showChargeSymbols: style = Qt.DashLine else: style = Qt.SolidLine else: isLabile = False style = Qt.SolidLine stereoA = atomA.stereo stereoB = atomB.stereo if stereoA or stereoB: if stereoA: # if they both are... zbase = 0 index = stereoA.index(atomB) nStereo = len(stereoA) else: zbase = 1 index = stereoB.index(atomA) nStereo = len(stereoB) if 3 < nStereo < 8: zstep = BOND_STEREO_DICT[nStereo][index] else: zstep = 0 else: zstep = 0 zbase = 0 direction = self.bond.direction if direction: if direction is atomA: direct = 0 else: direct = 1 else: direct = None # Geometry # Set global position using first point self.setPos(QPointF(xa, ya)) # Draw local relative to origin self.drawData = (style, dx, dy, xg, yg, zstep, zbase, direct) # Setup render coords nLines = int(BOND_TYPE_VALENCES[bond.bondType]) pad = 2.0 * BOND_SEP * (nLines - 1.0) + 0.75 if zstep: pad += BOND_SEP / 2 if direct is not None: pad += BOND_SEP * 2 rect = QRectF(QPointF(0.0, 0.0), QPointF(dx, dy)) self.bbox = rect.normalized().adjusted(-pad, -pad, pad, pad) self.update()
[docs] def boundingRect(self): #if self.compoundView.showSkeletalFormula: #for atom in self.bond.varAtoms: #if atom.element == 'H': #for n in atom.neighbours: #if n.element != 'C': #return NULL_RECT return self.bbox
[docs] def getDistToBond(self, pos): xm = pos.x() ym = pos.y() atomItemA, atomItemB = self.atomItems pos = atomItemA.pos() xa = pos.x() ya = pos.y() pos = atomItemB.pos() xb = pos.x() yb = pos.y() xb -= xa yb -= ya lb = hypot(xb, yb) xb /= lb yb /= lb xm -= xa ym -= ya dp = xb * xm + yb * ym xb *= dp yb *= dp xb -= xm yb -= ym r = hypot(xb, yb) return r
[docs] def mousePressEvent(self, event): selected = list(self.compoundView.selectedViews) mods = event.modifiers() button = event.button() haveCtrl = mods & Qt.CTRL haveShift = mods & Qt.SHIFT if self.getDistToBond(self.mapToScene(event.pos())) > 8: return atomItemA, atomItemB = self.atomItems if haveCtrl or haveShift: if atomItemA.selected and atomItemB.selected: atomItemA.deselect() atomItemB.deselect() else: atomItemA.select() atomItemB.select() elif not (atomItemA.selected and atomItemB.selected): for view in selected: view.deselect() atomItemA.select() atomItemB.select() atomItemA.update() atomItemB.update() QtWidgets.QGraphicsItem.mousePressEvent(self, event)
[docs] def mouseDoubleClickEvent(self, event): bond = self.bond if bond.bondType == 'aromatic': return if self.getDistToBond(self.mapToScene(event.pos())) > 8: return varAtom1, varAtom2 = bond.varAtoms atomA = varAtom1.atom atomB = varAtom2.atom for var in bond.compound.variants: varAtomA = var.atomDict.get(atomA) varAtomB = var.atomDict.get(atomB) if varAtomA and varAtomB: common = varAtomA.bonds & varAtomB.bonds if common: bond = common.pop() if varAtomA.freeValences and varAtomB.freeValences: bond.setBondType(BOND_CHANGE_DICT[bond.bondType]) varAtomA.updateValences() varAtomB.updateValences() elif bond.bondType in ('double', 'triple', 'quadruple'): bond.setBondType('single') varAtomA.updateValences() varAtomB.updateValences() if self.compoundView.autoChirality: varAtom1.autoSetChirality() varAtom2.autoSetChirality() self.compoundView.updateAll()
[docs] def paint(self, painter, option, widget): if not self.drawData: return if self.selected: color = HIGHLIGHT elif isinstance(self.compoundView, QtWidgets.QGraphicsItem): # SpecView if self.compoundView.container: color = QtGui.QColor(*self.glWidget._hexToRgba(self.compoundView.container.mainApp.colors[1])) # Analysis elif self.compoundView.glWidget: raise NotImplementedError("Needs rewriting - analysisProfile does not exist") backgroundColor = QtGui.QColor() backgroundColor.setRgbF(*self.glWidget._hexToRgba(self.glWidget.spectrumWindow.analysisProfile.bgColor)) color = Qt.black # Fallback else: color = self.compoundView.bondColor else: color = self.compoundView.bondColor style, dx, dy, xg, yg, zstep, zbase, direct = self.drawData bondType = self.bond.bondType drawLine = painter.drawLine pen = QtGui.QPen(color, 1.5, style) painter.setPen(pen) #if self.compoundView.showSkeletalFormula: #for atom in self.bond.varAtoms: #if atom.element == 'H': #for n in atom.neighbours: #if n.element != 'C': #return if bondType in ('single', 'dative'): if bondType == 'dative': xindent = 3.5 * yg yindent = -3.5 * xg if direct: x1 = dx - xindent * 1.5 y1 = dy - yindent * 1.5 x0 = y0 = 0.0 else: x0 = xindent * 1.5 y0 = yindent * 1.5 x1 = dx y1 = dy else: x0 = y0 = 0.0 x1 = dx y1 = dy if zstep: drawPoly = painter.drawPolygon dashPattern = [0.33, 0.5] if zbase == 0: # A basis if zstep < 0: # dashed line pen = QtGui.QPen(color, 4.5, Qt.DotLine) pen.setDashPattern(dashPattern) pen.setCapStyle(Qt.FlatCap) painter.setPen(pen) drawLine(QPointF(x0, y0), QPointF(x1, y1)) else: # # solid triangle, points at B pen = QtGui.QPen(color, 0.5, Qt.SolidLine) painter.setPen(pen) painter.setBrush(color) p1 = QPointF(dx + xg, dy + yg) p2 = QPointF(dx - xg, dy - yg) p3 = QPointF(x0, x0) drawPoly(p1, p2, p3) else: # B basis if zstep < 0: # dashed line pen = QtGui.QPen(color, 4.5, Qt.DotLine) pen.setDashPattern(dashPattern) pen.setCapStyle(Qt.FlatCap) painter.setPen(pen) drawLine(QPointF(x0, y0), QPointF(x1, y1)) else: # solid triangle, points at A pen = QtGui.QPen(color, 0.5, Qt.SolidLine) painter.setPen(pen) painter.setBrush(color) p1 = QPointF(xg, yg) p2 = QPointF(-xg, -yg) p3 = QPointF(x1, y1) drawPoly(p1, p2, p3) else: drawLine(QPointF(x0, y0), QPointF(x1, y1)) if bondType == 'dative': drawPoly = painter.drawPolygon pen = QtGui.QPen(color, 0.5, Qt.SolidLine) pen.setJoinStyle(Qt.RoundJoin) painter.setPen(pen) painter.setBrush(color) if direct: xc = dx - xindent yc = dy - yindent p1 = QPointF(xc - xindent + 2 * xg, yc - yindent + 2 * yg) p2 = QPointF(xc - xindent - 2 * xg, yc - yindent - 2 * yg) p3 = QPointF(xc, yc) else: xc = xindent yc = yindent p1 = QPointF(xc + xindent + 2 * xg, yc + yindent + 2 * yg) p2 = QPointF(xc + xindent - 2 * xg, yc + yindent - 2 * yg) p3 = QPointF(xc, yc) drawPoly(p1, p2, p3) elif bondType == 'double': drawLine(QPointF(xg, yg), QPointF(dx + xg, dy + yg)) drawLine(QPointF(-xg, -yg), QPointF(dx - xg, dy - yg)) elif bondType == 'aromatic': drawLine(QPointF(0, 0), QPointF(dx, dy)) elif bondType == 'singleplanar': drawLine(QPointF(0, 0), QPointF(dx, dy)) elif bondType == 'triple': drawLine(0.0, 0.0, dx, dy) xg *= 2.0 yg *= 2.0 drawLine(QPointF(xg, yg), QPointF(dx + xg, dy + yg)) drawLine(QPointF(-xg, -yg), QPointF(dx - xg, dy - yg)) elif bondType == 'quadruple': drawLine(QPointF(xg, yg), QPointF(dx + xg, dy + yg)) drawLine(QPointF(-xg, -yg), QPointF(dx - xg, dy - yg)) xg *= 3.0 yg *= 3.0 drawLine(QPointF(xg, yg), QPointF(dx + xg, dy + yg)) drawLine(QPointF(-xg, -yg), QPointF(dx - xg, dy - yg))
# Smiles organicAtoms = set(["B", "C", "N", "O", "P", "S", "F", "Cl", "Br", "I"]) cnos = set(["C", "N", "O", "S"])
[docs]def importSmiles(smilesString, compoundName='Unnamed', project=None): compound = Compound(compoundName) var = Variant(compound) compound.defaultVars.add(var) aromatics = set() exludeHydrogens = set() rings = {} branch = [] chirals = {} def _addStereo(varAtom1, varAtom2): varAtom1.stereo.append(varAtom2) if len(varAtom1.stereo) == 4: if chirals[varAtom1] > 0: a, b, c, d = varAtom1.stereo varAtom1.stereo = [a, d, c, b] # del chirals[varAtom1] n = len(smilesString) i = 0 hIndex = 1 prev = None bondType = 'single' configList = '' lastDouble = None addOrder = [] while i < n: element = None charge = 0 chiral = 0 numH = None isAromatic = False char = smilesString[i:i + 1] if prev: x, y, z = prev.coords else: x = 0.0 y = 0.0 z = 0.0 if char.isspace(): i += 1 continue if smilesString[i:i + 2] in organicAtoms: element = smilesString[i:i + 2] elif char.upper() in organicAtoms: element = char.upper() if (element in cnos) and char.islower(): isAromatic = True elif char == '[': i += 1 char = smilesString[i:i + 1] while char.isdigit(): # isotope label i += 1 char = smilesString[i:i + 1] element = char.upper() if (element in cnos) and char.islower(): isAromatic = True i += 1 char = smilesString[i:i + 1] if char == '@': i += 1 chiral = -1 char = smilesString[i:i + 1] if char == '@': i += 1 chiral = 1 char = smilesString[i:i + 1] if char not in 'H+-]': element += char i += 1 char = smilesString[i:i + 1] if char == 'H': numH = 1 i += 1 char = smilesString[i:i + 1] if char.isdigit(): numH = int(char) i += 1 char = smilesString[i:i + 1] if char == '+': while char == '+': charge += 1 i += 1 char = smilesString[i:i + 1] if char.isdigit(): charge = int(char) i += 1 char = smilesString[i:i + 1] if char == '-': while char == '-': charge -= 1 i += 1 char = smilesString[i:i + 1] if char.isdigit(): charge = -int(char) i += 1 char = smilesString[i:i + 1] if char == '=': bondType = 'double' elif char == '#': bondType = 'triple' elif char == ':': bondType = 'aromatic' elif char == '-': bondType = 'single' elif char.isdigit() or char == '%': if char.isdigit(): ringKey = char else: i += 1 char = smilesString[i:i + 1] ringKey = '' while char.isdigit(): ringKey += char i += 1 char = smilesString[i:i + 1] i -= 1 char = smilesString[i:i + 1] if ringKey in rings: varAtomB, bondTypeB = rings[ringKey] Bond((varAtomB, prev), bondTypeB, autoVar=False) if bondTypeB == 'double': lastDouble = (prev, varAtomB) if prev in chirals: _addStereo(prev, varAtomB) if varAtomB in chirals: _addStereo(varAtomB, prev) var.minimise2d(maxCycles=4) if ringKey in varAtomB.stereo: index = varAtomB.stereo.index(ringKey) varAtomB.stereo[index] = prev if ringKey in prev.stereo: index = prev.stereo.index(ringKey) prev.stereo[index] = varAtomB del rings[ringKey] else: rings[ringKey] = prev, bondType if prev in chirals: _addStereo(prev, ringKey) bondType = 'single' elif char == '(': branch.append(prev) elif char == ')': if branch: prev = branch.pop() elif char == '/': configList += char elif char == '\\': configList += char elif char == '@': # TBD proper coordinate-based, clock or anti prev.setChirality('RS') elif char == '.': bondType = None elif char == '>': bondType = None if element: atom = Atom(compound, element, None) angle = 1.57 if prev: prev.updateValences() freeValences = prev.freeValences if freeValences: numVal = len(freeValences) m = numVal // 2 if numVal % 2 == 0: angle1 = freeValences[m - 1] % (2 * pi) angle2 = freeValences[m] % (2 * pi) s = (sin(angle1) + sin(angle2)) / 2.0 c = (cos(angle1) + cos(angle2)) / 2.0 angle = atan2(s, c) else: angle = freeValences[m] x += 50.0 * sin(angle) y += 50.0 * cos(angle) varAtom = VarAtom(var, atom, coords=(x, y, z), charge=charge) varAtom.updateValences() addOrder.append(varAtom) if chiral: varAtom.stereo = [prev, ] chirals[varAtom] = chiral if isAromatic: aromatics.add(varAtom) if numH: angles = list(varAtom.freeValences) for h in range(numH): if angles: angle = angles.pop() else: angle = 0.0 x2 = x + 50.0 * sin(angle) y2 = y + 50.0 * cos(angle) name = 'H%d' % hIndex hIndex += 1 masterAtom = Atom(compound, 'H', name) varAtomH = VarAtom(var, masterAtom, coords=(x2, y2, z)) Bond((varAtom, varAtomH), 'single', autoVar=False) if varAtom in chirals: _addStereo(varAtom, varAtomH) elif numH == 0: exludeHydrogens.add(varAtom) if prev and bondType: Bond((varAtom, prev), bondType, autoVar=False) if bondType == 'double': lastDouble = (prev, varAtom) bondType = 'single' prev.updateValences() varAtom.updateValences() else: bondType = 'single' for varAtomB in chirals.keys(): if varAtomB in varAtom.neighbours: _addStereo(varAtomB, varAtom) if lastDouble and len(configList) > 1: config = configList[-2:] varAtomA, varAtomB = lastDouble x1, y1, z1 = varAtomA.coords x2, y2, z2 = varAtomB.coords dx = x2 - x1 dy = y2 - y1 angle = atan2(dx, dy) cx = (x1 + x2) / 2.0 cy = (y1 + y2) / 2.0 da = 1.57 nudge = 30.0 if config == '/\\': x3 = nudge * sin(angle + da) y3 = nudge * cos(angle + da) x1 += x3 x2 += x3 y1 += y3 y2 += y3 varAtomA.coords = x1, y1, z1 varAtomB.coords = x2, y2, z2 elif config == '//': x3 = nudge * sin(angle + da) y3 = nudge * cos(angle + da) x1 += x3 y1 += y3 varAtomA.coords = x1, y1, z1 x3 = nudge * sin(angle - da) y3 = nudge * cos(angle - da) x2 += x3 y2 += y3 varAtomB.coords = x2, y2, z2 elif config == '\\\\': x3 = nudge * sin(angle - da) y3 = nudge * cos(angle - da) x1 += x3 y1 += y3 varAtomA.coords = x1, y1, z1 x3 = nudge * sin(angle + da) y3 = nudge * cos(angle + da) x2 += x3 y2 += y3 varAtomB.coords = x2, y2, z2 elif config == '\\/': x3 = nudge * sin(angle - da) y3 = nudge * cos(angle - da) x1 += x3 x2 += x3 y1 += y3 y2 += y3 varAtomA.coords = x1, y1, z1 varAtomB.coords = x2, y2, z2 lastDouble = None prev = varAtom i += 1 # set aromatics if aromatics: rings = var.getRings(aromatics) for varAtoms2 in rings: if varAtoms2 & aromatics == varAtoms2: varAtoms3 = [va for va in addOrder if va in varAtoms2] x, y, z = var.getCentroid(varAtoms3) dAngle = 2.0 * pi / float(len(varAtoms3)) angle = 0.0 for j, va in enumerate(varAtoms3): x1 = x + 50.0 * sin(angle) y1 = y + 50.0 * cos(angle) va.coords = (x1, y1, z) angle += dAngle AtomGroup(compound, varAtoms2, AROMATIC) var.minimise2d(varAtoms2, maxCycles=50) # add H for varAtom in list(var.varAtoms): if varAtom.element not in cnos: continue if varAtom in exludeHydrogens: continue varAtom.updateValences() newAtoms = [] x, y, z = varAtom.coords for angle in list(varAtom.freeValences): x2 = x + 34.0 * sin(angle) y2 = y + 34.0 * cos(angle) name = 'H%d' % hIndex hIndex += 1 masterAtom = Atom(compound, 'H', name) hydrogen = VarAtom(var, masterAtom, coords=(x2, y2, z)) Bond((hydrogen, varAtom), 'single', autoVar=False) compound.center((0, 0, 0)) var.minimise2d(maxCycles=50) var.minimise3d(maxCycles=50) var.minimise2d(maxCycles=50) var.checkBaseValences() #var.shuffleStereo() if project: try: nmrChain = project.newNmrChain(compoundName) nmrResidue = nmrChain.newNmrResidue(compoundName) for atom in compound.atoms: nmrAtom = nmrResidue.fetchNmrAtom(atom.name) nmrAtom._compoundViewAtom = atom nmrResidue._compoundViewCompound = compound except Exception as e: print(e) return compound
[docs]class VarAtom: def __init__(self, variantA, atom, freeValences=None, chirality=None, coords=(0.0, 0.0, 0.0), isLabile=False, charge=0): if variantA is None: # Means all vars variants = list(atom.compound.variants) variant = variants[0] otherVars = variants[1:] else: variant = variantA otherVars = [] self.variant = variant self.atom = atom self.element = element = atom.element self.name = atom.name self.isVariable = atom.isVariable self.stereo = [] compound = variant.compound compound.isModified = True self.compound = compound self.isDeleted = False self.coords = tuple(coords) self.chirality = chirality self.isLabile = isLabile self.charge = charge self.bonds = set() self.neighbours = set() self.atomGroups = set() if element not in variant.elementDict: variant.elementDict[element] = set() variant.elementDict[element].add(self) variant.varAtoms.add(self) variant.atomDict[atom] = self atom.varAtoms.add(self) if freeValences is None: self.freeValences = None self.updateValences() else: self.freeValences = list(freeValences) self.updateNeighbours() for var in otherVars: VarAtom(var, atom, freeValences, chirality, coords, isLabile, charge) var.updatePolyLink() variant.updatePolyLink() if atom.isVariable: for var in self.compound.variants: var.updatePolyLink() var.updateDescriptor() def __repr__(self): aName = self.name return '<VarAtom %s>' % (aName)
[docs] def setStereo(self, stereoVarAtoms): # Clockwise rule relative to first nStereo = len(self.neighbours) if nStereo < 4: stereoVarAtoms = [] if stereoVarAtoms: if set(stereoVarAtoms) != set(self.neighbours): # print "Attempt to set stereo set to non-neighbours" return atom = self.atom for varAtom in atom.varAtoms: if len(varAtom.neighbours) != nStereo: varAtom.stereo = [] elif varAtom is self: self.stereo = stereoVarAtoms else: # all vars same, as far as possible, respecting a/b forms var = varAtom.variant stereoVarAtoms2 = [var.atomDict.get(va.atom) for va in stereoVarAtoms] if None in stereoVarAtoms2: continue if len(stereoVarAtoms2) == nStereo: stereo = stereoVarAtoms2 if varAtom.chirality == self.chirality: varAtom.stereo = stereo else: a = stereo[0] rest = stereo[1:] rest.reverse() varAtom.stereo = [a, ] + rest
[docs] def toggleAromatic(self): if self.isAromatic(): self.compound.unsetAromatic([self.atom, ]) else: self.compound.setAromatic([self.atom, ])
[docs] def isHydrogen(self): return self.element == 'H'
[docs] def isAromatic(self): for group in self.atomGroups: if group.groupType == AROMATIC: return True return False
[docs] def getRings(self): stack = [] rings = set() for varAtom in self.neighbours: if varAtom.element != 'H': stack.append([self, varAtom]) while stack: prev = stack.pop() if len(prev) > 8: continue prevSet = set(prev) nextAtoms = set([va for va in prev[-1].neighbours if va.element != 'H']) if prev[-2] in nextAtoms: nextAtoms.remove(prev[-2]) for varAtom in nextAtoms: if varAtom.element == 'C': if not varAtom.freeValences: if not varAtom.isAromatic: continue if varAtom is self: rings.add(frozenset(prev)) elif varAtom not in prevSet: nextSet = prev[:] + [varAtom, ] stack.append(nextSet) filteredRings = set() if rings: rings = sorted(rings, key=lambda ring: len(ring)) minSz = len(rings[0]) minRing = rings[0] filteredRings.add(minRing) uniqueAtoms = set() for ring in rings[1:]: unique = ring for ring2 in filteredRings: unique = unique - ring2 nUnique = len(unique) if nUnique > 0 and unique not in uniqueAtoms: for ua in unique: if ua in self.neighbours: break else: continue uniqueAtoms.add(unique) filteredRings.add(ring) # for ring in rings: # if len(ring) < minSz+2: # filteredRings.add(ring) return filteredRings
[docs] def setLabile(self, value=True): if (self.element == 'H') and (self.isLabile != value): neighbourhood = self.getContext() compound = self.compound compound.isModified = True # Set as labile only if the neighbours are the same as this var atom's # automatically includes its self for var in self.compound.variants: varAtom = var.atomDict.get(self.atom) if not varAtom: continue neighbourhoodB = varAtom.getContext() if neighbourhoodB != neighbourhood: continue varAtom.isLabile = value
[docs] def setCoords(self, x, y, z): # Compound wide for now for varAtom in self.atom.varAtoms: varAtom.coords = (x, y, z) compound = self.compound compound.isModified = True
[docs] def setName(self, name): self.atom.setName(name)
[docs] def setChirality(self, chirality, autoVar=True): if self.element == LINK: return if self.element == 'H': return compound = self.compound variant = self.variant atom = self.atom compound.isModified = True if chirality is not None: nVal = len(self.freeValences) + len(self.bonds) nNei = len(self.neighbours) if nVal < 3: chirality = None if nVal > nNei: chirality = None hydrogens = [a for a in self.neighbours if a.element == 'H'] if len(hydrogens) > 1: chirality = None if autoVar: variants = list(compound.variants) variants.remove(variant) variants = [variant, ] + variants # this one must be first - so it is not deleted else: variants = [variant, ] if chirality in ('R', 'S'): for var in variants: varAtom = var.atomDict[atom] varAtom.chirality = chirality varDict = {} for var in variants: key = frozenset(var.atomDict.keys()) if key not in varDict: varDict[key] = [] varDict[key].append(var) for key in varDict: vars2 = varDict[key] while len(vars2) > 1: var = vars2.pop() var.delete() elif chirality in ('a', 'b'): varDict = {} for var in variants: key = frozenset(var.atomDict.keys()) if key not in varDict: varDict[key] = [] varDict[key].append(var) for key in varDict: vars2 = varDict[key] while len(vars2) > 2: var = vars2.pop() var.delete() while len(vars2) < 2: var = Variant(compound, vars2[0].varAtoms) vars2.append(var) variants.append(var) varA, varB = vars2 if variant is varA: if chirality == 'b': varA, varB = varB, varA elif variant is varB: if chirality == 'a': varA, varB = varB, varA varAtomA = varA.atomDict[atom] varAtomB = varB.atomDict[atom] varAtomA.chirality = 'a' varAtomB.chirality = 'b' if self.stereo: a, b, c, d = self.stereo if chirality == 'a': stereoA = [varA.atomDict[va.atom] for va in [a, b, c, d]] stereoB = [varB.atomDict[va.atom] for va in [a, d, c, b]] else: stereoA = [varA.atomDict[va.atom] for va in [a, d, c, b]] stereoB = [varB.atomDict[va.atom] for va in [a, b, c, d]] varAtomA.stereo = stereoA varAtomB.stereo = stereoB elif not chirality: for var in variants: varAtom = var.atomDict[atom] varAtom.chirality = chirality varAtom.stereo = [] varDict = {} for var in variants: key = frozenset(var.atomDict.keys()) if key not in varDict: varDict[key] = [] varDict[key].append(var) for key in varDict: vars2 = varDict[key] while len(vars2) > 1: var = vars2.pop() var.delete() # Once all set for var in variants: var.updateDescriptor()
[docs] def getContext(self): # Get all of the (master) atoms surrounding this one return set([va.atom for va in self.neighbours])
[docs] def setCharge(self, charge, autoVar=True): elem = self.element if elem == LINK: return if elem == 'H': return compound = self.compound compound.isModified = True defaultVal = self.atom.baseValences neighbourhood = self.getContext() if autoVar: variants = compound.variants else: variants = [self.variant, ] for var in variants: varAtom = var.atomDict.get(self.atom) if not varAtom: continue if varAtom.getContext() != neighbourhood: continue nVal = len(varAtom.bonds) + len(varAtom.freeValences) if (elem in COVALENT_ELEMENTS) and (charge < 0): charge = max(-nVal, charge) targetVal = defaultVal + charge elif (elem in COVALENT_ELEMENTS) and (charge > 0): targetVal = defaultVal + charge else: targetVal = defaultVal # add any extra while nVal < targetVal: varAtom.freeValences.append(0.0) nVal += 1 # remove exess unbound while nVal > targetVal: if varAtom.freeValences: varAtom.freeValences.pop() nVal -= 1 else: break # otherwise remove extra H if nVal > targetVal: for bond in list(varAtom.bonds): atomA, atomB = bond.varAtoms if (atomA is not varAtom) and (atomA.element == LINK): bond.delete() nVal -= 1 if (atomB is not varAtom) and (atomB.element == LINK): bond.delete() nVal -= 1 if (atomA is not varAtom) and (atomA.element == 'H'): bond.delete() nVal -= 1 if (atomB is not varAtom) and (atomB.element == 'H'): bond.delete() nVal -= 1 if nVal == targetVal: break # otherwise remove anything if nVal > targetVal: for bond in list(varAtom.bonds): bond.delete() nVal -= 1 if nVal == targetVal: break varAtom.charge = charge varAtom.updateValences() var.updateDescriptor()
[docs] def getBondAngles(self): x1, y1, z1 = self.coords angles = [] for bond in self.bonds: varAtoms = set(bond.varAtoms) varAtoms.remove(self) varAtom = varAtoms.pop() x2, y2, z2 = varAtom.coords dx = x2 - x1 dy = y2 - y1 angle = atan2(dx, dy) % (2.0 * PI) angles.append(angle) return angles
[docs] def getBondAngle(self, varAtom): x1, y1, z1 = self.coords x2, y2, z2 = varAtom.coords dx = x2 - x1 dy = y2 - y1 angle = atan2(dy, dx) % (2.0 * PI) return angle
[docs] def getBondToAtom(self, varAtom): for bond in self.bonds: if self in bond.varAtoms and varAtom in bond.varAtoms: return bond return None
[docs] def getAtomDist(self, varAtom): x1, y1, z1 = self.coords x2, y2, z2 = varAtom.coords dx = x2 - x1 dy = y2 - y1 return hypot(dx, dy)
[docs] def setVariable(self, boolean): self.atom.setVariable(boolean)
[docs] def updateValences(self): defaultVal = self.atom.baseValences if self.freeValences is None: if defaultVal: gap = 2.0 * PI / defaultVal self.freeValences = [gap * i for i in range(defaultVal)] else: self.freeValences = [] else: bound = self.getBondAngles() numVal = defaultVal for bond in self.bonds: numVal -= BOND_TYPE_VALENCES[bond.bondType] if self.element in COVALENT_ELEMENTS: numVal += self.charge if numVal and self.isAromatic(): numVal -= 1 self.freeValences = [0.0] * numVal if bound: bound.sort() bound.append(bound[0] + (2.0 * PI)) gaps = [(bound[i + 1] - x, x, bound[i + 1], []) for i, x in enumerate(bound[:-1])] gaps.sort() for i in range(numVal): size, begin, end, indices = gaps[-1] indices.append(i) sizeB = (end - begin) / (len(indices) + 1.0) for k, j in enumerate(indices): delta = (1.0 + k) * sizeB self.freeValences[j] = (begin + delta) % (2 * PI) gaps[-1] = (sizeB, begin, end, indices) gaps.sort() elif self.freeValences: gap = 2.0 * PI / len(self.freeValences) self.freeValences = [gap * i for i in range(numVal)]
[docs] def updateNeighbours(self): atoms = set() for bond in self.bonds: atoms.update(bond.varAtoms) if atoms: atoms.remove(self) if self.neighbours != atoms: hydrogens = [a for a in atoms if a.element == 'H'] changed = atoms ^ self.neighbours for atom in changed: if (atom.element == 'H') and self.element == 'O': atom.setLabile(True) elif (atom.element == 'H') and self.element == 'N': if self.charge and len(hydrogens) > 2: for h in hydrogens: h.setLabile(True) else: for h in hydrogens: h.setLabile(False) if (self.element == 'C') and hydrogens: hydrogens = [h.atom for h in hydrogens] self.compound.unsetAtomsProchiral(hydrogens) self.compound.unsetAtomsEquivalent(hydrogens) if len(hydrogens) == 2: self.compound.setAtomsProchiral(hydrogens) elif len(hydrogens) == 3: self.compound.setAtomsEquivalent(hydrogens) self.neighbours = atoms
[docs] def delete(self): variant = self.variant if self not in variant.varAtoms: return compound = self.compound compound.isModified = True atom = self.atom for bond in list(self.bonds): bond.delete() if self.element in variant.elementDict: variant.elementDict[self.element].remove(self) variant.varAtoms.remove(self) del variant.atomDict[atom] atom.varAtoms.remove(self) # Remove any vars that have identical atoms atoms = set([(va.atom, va.chirality) for va in variant.varAtoms]) for var in list(self.compound.variants): if var is variant: continue atoms2 = set([(va.atom, va.chirality) for va in var.varAtoms]) if atoms2 == atoms: var.delete() for group in list(self.atomGroups): group.delete() if not atom.varAtoms: if not atom.isDeleted: atom.delete() if not variant.varAtoms: variant.delete()
# Snap the atom to one of the specified angles. The angle is chosen based on collisions with already placed atoms.
[docs] def snapToGrid(self, prevAtom, bondLength=50.0, prevAngle=None, angles=[], remainingAtoms=[], ignoreHydrogens=True): prevX = prevAtom.coords[0] prevY = prevAtom.coords[1] oldX, oldY, oldZ = self.coords if len(angles) == 0: angles = [120, -120] if prevAngle == None: prevAngle = 210 for i in range(len(angles)): angles[i] = radians((prevAngle + angles[i]) % 360) minD = None minI = None minPen = None allAtoms = self.variant.varAtoms - set(remainingAtoms) for i, angle in enumerate(angles): x = prevX + bondLength * cos(angle) y = prevY + bondLength * sin(angle) pen = 1 for atom in allAtoms: if atom == self or atom in self.neighbours or (ignoreHydrogens and atom.element == 'H'): continue atomDist = hypot(x - atom.coords[0], y - atom.coords[1]) if atomDist < bondLength: if atomDist < 1: pen *= 100 if atomDist < 0.000001: atomDist = 0.000001 pen *= 2 * bondLength / atomDist if not minPen or pen < minPen: minPen = pen minI = i bestX = x bestY = y if pen == 1: break self.coords = (bestX, bestY, oldZ) return degrees(angles[minI])
[docs] def getPreferredBondAngles(self, prevAngle, neighbours=None, ignoredAtoms=[], ignoreHydrogens=True): bonds = self.bonds nBonds = len(bonds) angles = [] ringAtoms = [] nHydrogens = 0 if neighbours == None: neighbours = sorted(self.neighbours, key=lambda atom: atom.name) for neighbour in neighbours: if neighbour.element == 'H': nHydrogens += 1 if ignoreHydrogens: nBonds -= nHydrogens rings = set(self.getRings()) if len(rings) > 0: for ring in rings: # Hydrogens receive a special treatment. #if not ignoreHydrogens: #if nBonds == 4: #if nHydrogens == 2: #angles = [70, 160, -70, -160] #if nHydrogens == 1: #angles = [180] #return angles ringSize = len(ring) ringAngle = ((ringSize - 2) * 180) / ringSize angle = (180 - 0.5 * ringAngle) if nBonds > 3: angle /= nBonds - 2 if not ignoreHydrogens and abs(360 - angle - 2 * angle) > 0.1: for a in [angle, -angle, 2 * angle, -2 * angle]: angles.append(a) else: for a in [angle, -angle]: angles.append(a) if angle < 120: angles.append(3 * angle) return angles else: # Triple bonds or two double bonds have a 180 degrees angle. nDouble = 0 for bond in bonds: if bond.bondType == 'triple': angles = [180] if bond.bondType == 'double': nDouble += 1 if nDouble == len(bonds): angles = [180] if len(angles) == 0: angles = [120, -120] if prevAngle != None and (90 <= prevAngle < 180 or 270 <= prevAngle < 360): for i in range(len(angles)): angles[i] = -angles[i] if nBonds > 2: neighbourAngles = [] for neighbour in neighbours: if not neighbour in ignoredAtoms: # and (not ignoreHydrogens or neighbour.element != 'H'): neighbourAngle = round(degrees(self.getBondAngle(neighbour)), 0) neighbourAngle -= prevAngle neighbourAngles.append(neighbourAngle) if nBonds == 4: if len(neighbourAngles) > 1: angles = [67.5, 172.5, 120, -120] else: angles = [120, -120, 67.5, 172.5] elif nBonds >= 4: angles = [] for i in range(1, nBonds): angles.append(i * 360 / nBonds) for neighbourAngle in neighbourAngles: if abs(neighbourAngle - 120) < 1 or abs(neighbourAngle + 240) < 1: for i in range(len(angles)): angles[i] = -angles[i] break return angles
[docs] def findAtomsInBranch(self, firstAtomInBranch, atoms=None): if not atoms: atoms = set([firstAtomInBranch]) if not self in atoms: temporarySelfAdd = True atoms.add(self) else: temporarySelfAdd = False neighbours = set(firstAtomInBranch.neighbours) while neighbours: neighbour = neighbours.pop() if neighbour == self or neighbour in atoms: continue atoms.add(neighbour) if neighbour.element == 'H': continue for neighbour2 in neighbour.neighbours: if neighbour2 in atoms: continue atoms.add(neighbour2) if neighbour2.element == 'H': continue atoms.update(neighbour.findAtomsInBranch(neighbour2, atoms)) if temporarySelfAdd: atoms.remove(self) return atoms
# Returns a list of atoms sorted by their respective branch lengths, shortest first.
[docs] def getBranchesSortedByLength(self): branchLens = [] for a in self.neighbours: branchLens.append([len(self.findAtomsInBranch(a)), a]) branchLens = sorted(branchLens, key=lambda x: x[0]) branches = [b[1] for b in branchLens] return branches
[docs] def atomInSameRing(self, atom): selfRings = self.getRings() atomRings = atom.getRings() if len(selfRings) > 0 and len(atomRings) > 0: for ring in atomRings: if ring in selfRings: return True return False
[docs] def snapRings(self, rings, neighbours, atoms, prevAngle, bondLength): skippedAtoms = set([]) for ring in rings: if len(atoms) == 0: return for ringAtom in ring: if ringAtom in atoms: if ringAtom in neighbours: neighbours.remove(ringAtom) atoms.remove(ringAtom) else: skippedAtoms.add(ringAtom) if ringAtom != self: prevAngle = None self.variant.snapRingToGrid(ring, self, prevAngle, bondLength, skippedAtoms) skippedAtoms.add(self) for ring in rings: for ringAtom in ring: if ringAtom in skippedAtoms: continue ringAtomRings = ringAtom.getRings() if ring in ringAtomRings: ringAtomRings.remove(ring) for ringAtomRing in set(ringAtomRings): for atom in atoms: if atom in ringAtomRing: break else: ringAtomRings.remove(ringAtomRing) if len(ringAtomRings) > 0: ringAtomRings = sorted(ringAtomRings, key=lambda ring: len(ring), reverse=True) ringAtomNeighbours = sorted(ringAtom.neighbours, key=lambda atom: atom.name) ringAtom.snapRings(ringAtomRings, ringAtomNeighbours, atoms, None, bondLength)
[docs] def autoSetChirality(self): variants = self.compound.variants atom = self.atom stereo = None if self.chirality in ['a', 'b']: return chirality = self.getStereochemistry() # Make the automatically set chirality lower case to be able to distinguish it from user specified chirality. if chirality: chirality = chirality.lower() if self.chirality and self.chirality.isupper(): branches = self.getBranchesSortedByLength() if len(branches) == 4: stereo = [branches[3], branches[0], branches[2], branches[1]] elif len(branches) >= 4: stereo = [branches[3], branches[0], branches[2], branches[1], branches[4:]] if stereo: self.setStereo(stereo) for var in variants: varAtom = var.atomDict.get(atom) if not varAtom: continue vAChirality = varAtom.chirality if stereo and not varAtom.stereo: for a in stereo: varAtom.stereo.append(var.atomDict.get(a.atom)) chirality = varAtom.getStereochemistry() if chirality: chirality = chirality.lower() if vAChirality and vAChirality.isupper(): if vAChirality in ('R', 'S'): if chirality and vAChirality.lower() != chirality: temp = varAtom.stereo[3] varAtom.stereo[3] = varAtom.stereo[1] varAtom.stereo[1] = temp chirality = varAtom.getStereochemistry() chirality = chirality.lower() if vAChirality.lower() != chirality: raise Exception("Stereochemistry of %s different even after attempted swap. (%s and %s)" % (varAtom, vAChirality, chirality)) else: varAtom.chirality = chirality
[docs] def getStereochemistry(self): neighbours = self.neighbours nNeighbours = len(neighbours) stereochemistry = None if nNeighbours == 4: nHydrogens = 0 for neighbour in neighbours: if neighbour.element == 'H': nHydrogens += 1 if nHydrogens <= 1 and self.stereo != []: stereochemistry = self.getStereochemistryRS() elif nNeighbours == 3: for neighbour in neighbours: bond = self.getBondToAtom(neighbour) if bond.bondType == 'double': if len(neighbour.neighbours) == 3: stereochemistry = self.getStereochemistryEZ(neighbour) return stereochemistry
[docs] def getStereochemistryRS(self): prio = self.getPriorities() if prio == None: return None lowPrio = prio[-1] refBondAngle = self.getBondAngle(prio[0]) secondBondAngle = (self.getBondAngle(prio[1]) - refBondAngle) % (2 * PI) thirdBondAngle = (self.getBondAngle(prio[2]) - refBondAngle) % (2 * PI) if secondBondAngle < thirdBondAngle: stereo = 1 elif secondBondAngle > thirdBondAngle: stereo = -1 else: return None lowPrioIndex = self.stereo.index(lowPrio) if lowPrioIndex == 3: stereo *= -1 elif lowPrioIndex != 1: lowHighAngle = abs(self.getBondAngle(lowPrio) - self.getBondAngle(self.stereo[3])) % (2 * PI) if lowHighAngle > PI / 2: stereo *= -1 if stereo == 1: return "R" if stereo == -1: return "S" return None
[docs] def getStereochemistryEZ(self, other): selfPrio = self.getPriorities() if selfPrio == None: return None otherPrio = other.getPriorities() if otherPrio == None: return None angleMain = self.getBondAngle(other) selfHighPrio = selfLowPrio = None otherHighPrio = otherLowPrio = None for atom in selfPrio: if atom != other: if selfHighPrio == None: selfHighPrio = atom else: selfLowPrio = atom for atom in otherPrio: if atom != self: if otherHighPrio == None: otherHighPrio = atom else: otherLowPrio = atom angleHighPrio = selfHighPrio.getBondAngle(otherHighPrio) angleLowPrio = selfHighPrio.getBondAngle(otherLowPrio) diff = abs(angleMain - angleHighPrio) altDiff = abs(angleMain - angleLowPrio) diff = min(diff, abs(2 * PI - diff)) altDiff = min(altDiff, abs(2 * PI - altDiff)) if diff < altDiff: return 'Z' if diff > altDiff: return 'E' return None
# This function returns a list of neighbours ranked according to Cahn-Ingold-Prelog rules (not taking more than the fourth level of neighbours into account. # The neighbour with highest priority is first in the returned list.
[docs] def getPriorities(self): prio = 0 firstList = self.getFirstNeighbourPriorities() foundSimilar = False for i, item in enumerate(firstList): for otherItem in firstList[(i + 1):]: if item[1] == otherItem[1]: foundSimilar = True break if foundSimilar: break if foundSimilar: secondList = self.getSecondNeighbourPriorities() thirdList = self.getThirdNeighbourPriorities() fourthList = self.getFourthNeighbourPriorities() n = len(firstList) prioList = [None] * n while prio < n: item = firstList[prio] if prio + 1 == n: prioList[prio] = item[0] prio += 1 else: nextItem = firstList[prio + 1] if item[1] != nextItem[1]: prioList[prio] = item[0] prio += 1 continue similar = [prio, prio + 1] i = 2 while prio + i < n and firstList[prio][1] == firstList[prio + i][1]: similar.append(prio + i) i += 1 nSim = len(similar) while nSim > 0: newN = 0 for i in range(1, nSim): this = similar[i] prev = similar[i - 1] if secondList[prev][-1] == secondList[this][-1] and thirdList[prev][-1] == thirdList[this][-1] and \ fourthList[prev][-1] == fourthList[this][-1]: return None if secondList[prev][-1] < secondList[this][-1] or \ (secondList[prev][-1] == secondList[this][-1] and (thirdList[prev][-1] < thirdList[this][-1] or \ (thirdList[prev][-1] == thirdList[this][-1] and fourthList[prev][-1] < fourthList[this][-1]))): temp = similar[i] similar[i] = similar[i - 1] similar[i - 1] = temp newN = i nSim = newN for item in similar: prioList[prio] = firstList[item][0] prio += 1 return prioList
# This should actually be the atomic number, but here the mass number of the main isotope is used instead. Should still # sort in the same order.
[docs] def priorityNumber(self): if self.atom.element == LINK: return ELEMENT_ISO_ABUN['C'][0][1] return ELEMENT_ISO_ABUN[self.atom.element][0][1]
[docs] def getFirstNeighbourPriorities(self): priorityList = [] for neighbour in self.neighbours: p = neighbour.priorityNumber() priorityList.append([neighbour, p]) priorityList = sorted(priorityList, key=lambda neighbour: neighbour[1], reverse=True) return priorityList
[docs] def getSecondNeighbourPriorities(self): priorityList = [] maxLen = None for a, p in self.getFirstNeighbourPriorities(): if a == None: continue localPriorityList = [] for atom, prio in a.getFirstNeighbourPriorities(): if atom != self: localPriorityList.append([atom, prio]) bond = a.getBondToAtom(atom) if bond.bondType == 'double' or bond.bondType == 'triple' or a.isAromatic(): localPriorityList.append([None, prio]) if bond.bondType == 'triple': localPriorityList.append([None, prio]) localPriorityList = sorted(localPriorityList, key=lambda neighbour: neighbour[1], reverse=True) l = len(localPriorityList) if maxLen == None or l > maxLen: maxLen = l priorityList.append(localPriorityList) maxLen -= 1 for localPriorityList in priorityList: score = 0 for i, (a, p) in enumerate(localPriorityList): score += p * (10 ** ((maxLen - i) * 2)) localPriorityList.append([None, score]) return priorityList
[docs] def getThirdNeighbourPriorities(self): priorityList = [] maxLen = None for list in self.getSecondNeighbourPriorities(): localPriorityList = [] for a, p in list: if a == None: continue for atom, prio in a.getFirstNeighbourPriorities(): if atom not in self.neighbours: localPriorityList.append([atom, prio]) bond = a.getBondToAtom(atom) if bond.bondType == 'double' or bond.bondType == 'triple' or a.isAromatic(): localPriorityList.append([None, prio]) if bond.bondType == 'triple': localPriorityList.append([None, prio]) l = len(localPriorityList) if maxLen == None or l > maxLen: maxLen = l priorityList.append(localPriorityList) maxLen -= 1 for localPriorityList in priorityList: score = 0 for i, (a, p) in enumerate(localPriorityList): score += p * (10 ** ((maxLen - i) * 2)) localPriorityList.append([None, score]) return priorityList
[docs] def getFourthNeighbourPriorities(self): priorityList = [] maxLen = None nextNeighbours = set() for list in self.getThirdNeighbourPriorities(): localPriorityList = [] for a, p in list: if a == None: continue for atom, prio in a.getFirstNeighbourPriorities(): localPriorityList.append([atom, prio]) l = len(localPriorityList) if maxLen == None or l > maxLen: maxLen = l priorityList.append(localPriorityList) maxLen -= 1 for localPriorityList in priorityList: score = 0 for i, (a, p) in enumerate(localPriorityList): score += p * (10 ** ((maxLen - i) * 2)) localPriorityList.append([None, score]) return priorityList
[docs]def vectorsSubtract(v1, v2): """ Subtract vectors v1 and v2. """ n = len(v1) if n != len(v2): raise Exception('length of v1 != length of v2') v = n * [0] for i in range(n): v[i] = v1[i] - v2[i] return v
[docs]def dotProduct(v1, v2): """ The inner product between v1 and v2. """ n = len(v1) if (n != len(v2)): raise Exception('v1 and v2 must be same length') d = 0 for i in range(n): d = d + v1[i] * v2[i] return d
[docs]def crossProduct(v1, v2): """ Returns the cross product of v1 and v2. Both must be 3-dimensional vectors. """ if (len(v1) != 3 or len(v2) != 3): raise Exception('v1 and v2 must be of length 3') return [v1[1] * v2[2] - v1[2] * v2[1], v1[2] * v2[0] - v1[0] * v2[2], v1[0] * v2[1] - v1[1] * v2[0]]
[docs]class Variant: def __init__(self, compound, templateVarAtoms=None): self.compound = compound self.polyLink = None # Auto derived self.descriptor = None # Auto derived self.varAtoms = set() self.bonds = set() self.atomDict = {} self.atomGroups = set() self.elementDict = {} compound.isModified = True compound.variants.add(self) if templateVarAtoms: self.copyAtoms(templateVarAtoms, (0, 0), False) self.updatePolyLink() self.updateDescriptor() def __repr__(self): h, l, s = self.descriptor return '<Variant %s %s %s %s>' % (self.polyLink, h, l, s)
[docs] def delete(self): compound = self.compound compound.isModified = True for varAtom in list(self.varAtoms): varAtom.delete() self.varAtoms = set() self.bonds = set() self.atomDict = {} self.elementDict = {} if len(compound.variants) > 1: if self in compound.variants: compound.variants.remove(self) del self nVars = len(compound.variants) for atom in compound.atoms: if atom.isVariable and (len(atom.varAtoms) == nVars): atom.isVariable = False for var in compound.variants: var.updatePolyLink() var.updateDescriptor()
[docs] def setDefault(self, value=True): compound = self.compound compound.isModified = True defaultVars = compound.defaultVars current = set([v for v in defaultVars if v.polyLink == self.polyLink]) if value: for var in current: defaultVars.remove(var) defaultVars.add(self) elif self in current: defaultVars.remove(self)
[docs] def getRings(self, varAtoms): for varAtom in varAtoms: if varAtom.variant is not self: msg = 'Variant.getRings: Input VarAtom does not belong to Variant' raise Exception(msg) varAtoms = set(varAtoms) rings = [] while varAtoms: varAtom = varAtoms.pop() rings += varAtom.getRings() for varAtoms2 in rings: varAtoms = varAtoms - varAtoms2 return rings
[docs] def checkBaseValences(self): for varAtom in self.varAtoms: baseValances = len(varAtom.freeValences) - varAtom.charge for bond in varAtom.bonds: baseValances += BOND_TYPE_VALENCES[bond.bondType] if varAtom.isAromatic(): baseValances += 1 if varAtom.atom.baseValences != baseValances: varAtom.atom.setBaseValences(baseValances)
[docs] def shuffleStereo(self): for varAtom in self.varAtoms: stereo = varAtom.stereo if stereo: if len(stereo) != 4: continue indices = {} for i, va in enumerate(stereo): indices[va] = i sortList = [(len(va.neighbours), va.name, va) for va in stereo] sortList.sort() a, b, c, d = stereo perms = [[a, b, c, d], [a, c, d, b], [a, d, b, c], [b, a, d, c], [b, d, c, a], [b, c, a, d], [c, a, b, d], [c, b, d, a], [c, d, a, b], [d, a, c, b], [d, c, b, a], [d, b, a, c]] w, x, y, z = [v[2] for v in sortList] for p, q, r, s in perms: if p in (y, z) and r in (y, z): varAtom.stereo = [p, q, r, s] break
[docs] def deduceStereo(self): for varAtom in self.varAtoms: neighbours = list(varAtom.neighbours) if len(neighbours) < 4: continue center = varAtom.coords vecs = [(vectorsSubtract(va.coords, center), va) for va in neighbours] vec1, va1 = vecs[0] dotProds = [(dotProduct(vec, vec1), vec, va) for vec, va in vecs[1:]] dotProds.sort() dp, vec2, va2 = dotProds[0] norm = crossProduct(vec1, vec2) len1 = sqrt(dotProduct(norm, norm)) angles = [] for dp, vec, va in dotProds[1:]: len2 = sqrt(dotProduct(vec, vec)) * len1 c = dotProduct(vec, norm) / len2 cp = crossProduct(vec, norm) s = sqrt(dotProduct(cp, cp)) / len2 angle = atan2(s, c) angles.append((angle, va)) angles.sort() stereo = [va1, ] stereo += [x[1] for x in angles] stereo += [va2, ] varAtom.stereo = stereo self.shuffleStereo()
[docs] def getBond(self, varAtomA, varAtomB, autoVar=True): if not varAtomA.freeValences: return if not varAtomB.freeValences: return if varAtomA in varAtomB.neighbours: common = set(varAtomA.bonds & varAtomB.bonds) return common.pop() return Bond((varAtomA, varAtomB), autoVar=autoVar)
[docs] def updateDescriptor(self): self.descriptor = self.getDescriptor()
[docs] def getCommonIsoMass(self): mass = 0.0 for element in self.elementDict: if element == LINK: continue n = len(self.elementDict[element]) mass += n * ELEMENT_ISO_ABUN[element][0][2] return mass
[docs] def getMolFormula(self): counts = {} for element in self.elementDict: if element == LINK: continue counts[element] = len(self.elementDict[element]) elements = counts.keys() elements.sort() if 'H' in elements: elements.remove('H') elements.insert(0, 'H') if 'C' in elements: elements.remove('C') elements.insert(0, 'C') formula = [] for elem in elements: n = counts[elem] if n > 1: text = '%s%d' % (elem, n) else: text = elem formula.append(text) formula = ' '.join(formula) return formula
[docs] def getDescriptor(self, ccpnStyle=False): if ccpnStyle: prot = 'prot' deprot = 'deprot' link = 'link' sep = ':' joinStr = ';' else: prot = '+' deprot = '-' link = '' sep = '' joinStr = ',' neutral = 'neutral' equivVars = [v for v in self.compound.variants if v.polyLink == self.polyLink] hAtomVars = {} stereoTypes = {} for var in equivVars: for atom in var.varAtoms: name = atom.name if name not in stereoTypes: stereoTypes[name] = set() stereoTypes[name].add(atom.chirality) if (atom.element == 'H') and atom.atom.isVariable: if name not in hAtomVars: hAtomVars[name] = set() hAtomVars[name].add(var) tags = [] tagsLink = [] tagsStereo = [] tagsProton = [] # Links linkAtoms = self.elementDict.get(LINK, []) genLinks = [a for a in linkAtoms if ('prev' not in a.name) \ and ('next' not in a.name)] for atom in genLinks: neighbours = atom.neighbours name = ','.join([a.name for a in neighbours]) tags.append((link, name)) tagsLink.append((link, name)) # Protonation isNeutral = sum([abs(va.charge) for va in self.varAtoms]) == 0 # Names of variable atoms for name in hAtomVars: # Vars that have this hydrogen varsA = hAtomVars[name] if self in varsA: tags.append((prot, name)) tagsProton.append((prot, name)) else: tags.append((deprot, name)) tagsProton.append((deprot, name)) # stereochemistry if ccpnStyle: stereoDict = {'R': 1, 'S': 2, 'a': 1, 'b': 2} for atom in self.varAtoms: name = atom.name if (atom.chirality) and len(stereoTypes.get(name, [])) > 1: tags.append(('stereo_%d' % stereoDict[atom.chirality], name)) tagsStereo.append(('stereo_%d' % stereoDict[atom.chirality], name)) else: for atom in self.varAtoms: name = atom.name if (atom.chirality) and len(stereoTypes.get(name, [])) > 1: tags.append((atom.chirality, name)) tagsStereo.append((atom.chirality, name)) #if isNeutral and not (tagsStereo or tagsLink): # tags = [] # tagsProton = [] if ccpnStyle: tagNames = [] sortDict = {} for cat, name in tags: if cat not in sortDict: sortDict[cat] = [] sortDict[cat].append(name) cats = [(VAR_TAG_ORDER[cat], cat) for cat in sortDict.keys()] cats.sort() for i, cat in cats: if cat == 'link': if self.compound.ccpMolType not in ('protein', 'RNA', 'DNA'): continue if cat == neutral: tagNames.append(neutral) else: names = sortDict[cat] names.sort() tagStr = ','.join(names) tagNames.append('%s%s%s' % (cat, sep, tagStr)) return str(joinStr.join(tagNames)) or neutral else: if isNeutral: defaultH = neutral else: defaultH = 'default' tagsProton.sort() tagsProton = joinStr.join(['%s%s' % x for x in tagsProton]) or defaultH tagsStereo.sort() tagsStereo = joinStr.join(['(%s)%s' % x for x in tagsStereo]) or 'default' tagsLink.sort() tagsLink = joinStr.join(['%s%s' % x for x in tagsLink]) or 'none' return tagsProton, tagsLink, tagsStereo
[docs] def copyAtoms(self, varAtoms, coords=None, tempNames=True): if not varAtoms: return compound = self.compound compound.isModified = True cx = 0.0 cy = 0.0 n = 0.0 for atom in varAtoms: x, y, z = atom.coords cx += x cy += y n += 1.0 cx /= n cy /= n if coords is None: x0, y0 = cx, cy else: x0, y0 = coords mapping = {} bonds = set() groups = set() newAtoms = set() addList = [(a.name, a) for a in varAtoms] addList.sort() for name, atom in addList: if atom.element == LINK: if 'prev' in name: if self.polyLink == 'middle': continue elif self.polyLink == 'end': continue if 'next' in name: if self.polyLink == 'middle': continue elif self.polyLink == 'start': continue x, y, z = atom.coords dx = x - cx dy = y - cy bonds.update(atom.bonds) groups.update(atom.atomGroups) if tempNames: name = '@%s' % name masterAtom = compound.getAtom(atom.element, name, atom.atom.isVariable) masterAtom.baseValences = atom.atom.baseValences newAtom = VarAtom(self, masterAtom, atom.freeValences, atom.chirality, (x0 + dx, y0 + dy, z), atom.isLabile, atom.charge) newAtoms.add(newAtom) mapping[atom] = newAtom for bond in bonds: atomA, atomB = bond.varAtoms newAtomA = mapping.get(atomA) newAtomB = mapping.get(atomB) if newAtomA and newAtomB: Bond((newAtomA, newAtomB), bondType=bond.bondType, autoVar=False) for group in groups: newAtomsG = set() for varAtom in group.varAtoms: if varAtom not in mapping: break newAtomsG.add(mapping[varAtom]) else: AtomGroup(compound, newAtomsG, groupType=group.groupType) for varAtom in mapping: if varAtom.stereo: stereo = [] for varAtom2 in varAtom.stereo: newAtom = mapping.get(varAtom2) if newAtom: stereo.append(newAtom) else: break else: mapping[varAtom].setStereo(stereo) for var in self.compound.variants: var.updatePolyLink() var.updateDescriptor() return newAtoms
[docs] def minimise2d(self, atoms=None, maxCycles=250, bondLength=50.0, drawFunc=None): from math import sqrt allAtoms = list(self.varAtoms) if not atoms: atoms = set(self.varAtoms) if len(atoms) < 2: return atoms.pop() if not atoms: return compound = self.compound compound.isModified = True from random import random, shuffle cx = 0.0 cy = 0.0 cz = 0.0 for atom in atoms: x, y, z = atom.coords x += random() y += random() atom.coords = x, y, z cx += x cy += y cz += x n = float(len(atoms)) cx /= n cy /= n cz /= n #self.minimise3d(atoms, maxCycles*5, bondLength, drawFunc) #self.center(atoms, (cx, cy, cz)) distances = {} distances2 = {} bondLengths = {} getBond = self.getBond aromatics = set() for varAtom in atoms: elem = varAtom.element neighbours = varAtom.neighbours n = float(len(neighbours)) for atomGroup in varAtom.atomGroups: if atomGroup.groupType == AROMATIC: aromatics.add(atomGroup) for varAtom2 in neighbours: elem2 = varAtom2.element if 'H' in (elem2, elem): bl = 0.75 * bondLength else: bond = set(varAtom2.bonds & varAtom.bonds).pop() if bond.bondType in ('double', 'triple'): bl = 0.87 * bondLength else: bl = bondLength key = frozenset([varAtom2, varAtom]) bondLengths[key] = bl for group in aromatics: varAtoms = group.varAtoms n = float(len(varAtoms)) t = (n - 2.0) * PI / (2.0 * n) dist = bondLength * 2.0 * sin(t) for varAtom in varAtoms: neighbours = [va for va in varAtom.neighbours if va in varAtoms] if len(neighbours) == 2: distances2[frozenset(neighbours)] = dist b2 = bondLength * bondLength r2limit = 4 * b2 for c in range(maxCycles): change = 0.0 g = 0.005 * float(maxCycles - c) / maxCycles for atom in atoms: neighbours = atom.neighbours if not neighbours: continue vx = 0.0 vy = 0.0 x, y, z = atom.coords for atom2 in allAtoms: # only neighbours? if atom2 is atom: continue x2, y2, z2 = atom2.coords dx = x - x2 dy = y - y2 r2 = (dx * dx) + (dy * dy) pair = frozenset([atom, atom2]) if pair in distances2: f = distances2[pair] - sqrt(r2) elif atom2 in neighbours: f = bondLengths[pair] - sqrt(r2) elif r2 < r2limit: f = 5e6 / (r2 * r2) else: continue f = min(2.0, max(-2.0, f)) vx += dx * f * g vy += dy * f * g x += max(min(10.0 * vx, 10.0), -10.0) y += max(min(10.0 * vy, 10.0), -10.0) atom.coords = (x, y, z) change += abs(vx) change += abs(vy) if (c % 5 == 0) and drawFunc: self.center(atoms, (cx, cy, cz)) drawFunc() shuffle(allAtoms) # quit early if nothing happens if change < 0.01: break for atom in atoms: x, y, z = atom.coords atom.setCoords(x, y, 0.0) # updates all vars atom.updateValences() if drawFunc: drawFunc()
[docs] def getCentroid(self, atoms=None): if not atoms: atoms = self.varAtoms xs = 0.0 ys = 0.0 zs = 0.0 n = 0.0 for varAtom in atoms: x1, y1, z1 = varAtom.coords xs += x1 ys += y1 zs += z1 n += 1.0 if n: xs /= n ys /= n zs /= n return (xs, ys, zs) else: return (0.0, 0.0, 0.0)
[docs] def center(self, atoms=None, origin=None): if not atoms: atoms = self.varAtoms if origin: x0, y0, z0 = origin else: x0 = 0.0 y0 = 0.0 z0 = 0.0 xs, ys, zs = self.getCentroid(atoms) xs -= x0 ys -= y0 zs -= z0 for varAtom in atoms: x1, y1, z1 = varAtom.coords x1 -= xs y1 -= ys z1 -= zs varAtom.coords = x1, y1, z1
[docs] def minimise3d(self, atoms=None, maxCycles=100, bondLength=50.0, drawFunc=None): from math import sqrt from random import random, shuffle allAtoms = list(self.varAtoms) if not atoms: atoms = set(self.varAtoms) if len(atoms) < 2: return atoms.pop() if not atoms: return compound = self.compound compound.isModified = True distances = {} aromatics = set() bl = bondLength * 2.0 for varAtom in atoms: neighbours = [n for n in varAtom.neighbours if n.element != 'H'] n = float(len(neighbours)) for varAtom2 in neighbours: for varAtom3 in neighbours: if varAtom2 is not varAtom3: if n == 3.0: distances[frozenset([varAtom2, varAtom3])] = bl * 0.8660254 else: distances[frozenset([varAtom2, varAtom3])] = bl * 0.5 for group in aromatics: varAtoms = group.varAtoms n = float(len(varAtoms)) for varAtom in varAtoms: neighbours = [va for va in varAtom.neighbours if va in varAtoms] if len(neighbours) == 2: varAtomA, varAtomB = neighbours distances[frozenset([varAtom, varAtomA])] = bondLength distances[frozenset([varAtom, varAtomB])] = bondLength t = (n - 2.0) * PI / n distances[frozenset([varAtomA, varAtomB])] = bondLength * 2.0 * sin(t) cx = 0.0 cy = 0.0 cz = 0.0 for atom in atoms: x, y, z = atom.coords x += random() y += random() z += random() atom.coords = x, y, z cx += x cy += y cz += z n = float(len(atoms)) cx /= n cy /= n cz /= n b2 = bondLength * bondLength r2limit = 4 * b2 for c in range(maxCycles): change = 0.0 g = 5.0 * float(c) / maxCycles for atom in atoms: neighbours = atom.neighbours if not neighbours: continue vx = 0.0 vy = 0.0 vz = 0.0 x, y, z = atom.coords for atom2 in allAtoms: # only neighbours? if atom2 is atom: continue x2, y2, z2 = atom2.coords dx = x - x2 dy = y - y2 dz = z - z2 r2 = max(0.001, (dx * dx) + (dy * dy) + (dz * dz)) pair = frozenset([atom, atom2]) if pair in distances: dl = 1 * (distances[pair] - sqrt(r2)) vx += dx * dl / r2 vy += dy * dl / r2 vz += dz * dl / r2 elif atom2 in neighbours: dl = 1 * (bondLength - sqrt(r2)) vx += dx * dl / r2 vy += dy * dl / r2 vz += dz * dl / r2 else: f = 5 * b2 / (r2 * r2) vx += dx * f vy += dy * f vz += dz * f f = dz / r2 vz += g * dz * f x += max(min(10.0 * vx, 10), -10) y += max(min(10.0 * vy, 10), -10) z += max(min(10.0 * vz, 10), -10) atom.coords = (x, y, z) change += vx change += vy change += vz if (c % 5 == 0) and drawFunc: self.center(atoms, (cx, cy, cz)) drawFunc() shuffle(allAtoms) # quit early if nothing happens if abs(change) < 1e-4: break for atom in atoms: x, y, z = atom.coords atom.setCoords(x, y, z) # updates all vars atom.updateValences() if drawFunc: drawFunc()
# This function snaps the atoms in atoms to suitable bond angles. When determining where to place atoms # only already placed atoms are taken into account (i.e. those that are not present in atoms)
[docs] def snapAtomsToGrid(self, atoms=None, prevAtom=None, ignoreHydrogens=True, bondLength=50.0): if not atoms: atoms = sorted(self.varAtoms, key=lambda atom: atom.name) prevX = prevY = prevZ = None else: if len(atoms) < 1: return chiralities = [] for atom in atoms: if atom.chirality: for c in chiralities: if c[0] == atom.name: break else: chiralities.append((atom.name, atom.chirality)) # Make a separate list of hydrogens, which will be placed last. hydrogens = [] atomsTemp = list(atoms) for atom in atomsTemp: if atom.element == 'H': #if ignoreHydrogens: atoms.remove(atom) hydrogens.append(atom) atom = None neighbour = None molCnt = 0 prevAngle = None # prevAtom is an already positioned atom and is used as a reference for placing its neighbouring atoms. if prevAtom: prevX, prevY, prevZ = prevAtom.coords neighbours = sorted(prevAtom.neighbours, key=lambda atom: atom.name) for neighbour in neighbours: if neighbour.element != 'H' and neighbour in atoms: atom = neighbour break # Find a reference angle to an already placed atom. for neighbour in neighbours: if neighbour != prevAtom and neighbour.element != 'H' and neighbour not in atoms: prevAngle = degrees(prevAtom.getBondAngle(neighbour)) break # If a reference atom was submitted and it is in a ring snap the ring before proceeding to other atoms. atom = prevAtom rings = sorted(atom.getRings(), key=lambda ring: len(ring), reverse=True) atom.snapRings(rings, neighbours, atoms, prevAngle, bondLength) atom = None while atoms: # Find an atom with an already placed neighbour for atom in atoms: neighbours = sorted(atom.neighbours, key=lambda atom: atom.name) for neighbour in neighbours: if neighbour.element != 'H' and neighbour not in atoms: prevX, prevY, prevZ = neighbour.coords prevAtom = neighbour neighbours2 = sorted(neighbour.neighbours, key=lambda atom: atom.name[-1]) # Try to find a reference angle (if the neighbour has a neighbour that has been placed). for neighbour2 in neighbours2: if neighbour2 != atom and neighbour2 not in atoms and (not ignoreHydrogens or neighbour2.element != 'H'): prevAngle = round(degrees(neighbour.getBondAngle(neighbour2)), 0) break else: prevAngle = None break else: continue break else: prevAtom = None atom = None if atom and atom in atoms: atoms.remove(atom) if not atom: # Start by placing the atom involved in most rings. This can help avoid clashes. atomWithMostRings = None mostRings = 0 for atom in atoms: nRings = len(atom.getRings()) if nRings > mostRings: atomWithMostRings = atom mostRings = nRings if mostRings > 0: atom = atomWithMostRings atoms.remove(atom) else: atom = atoms.pop(0) # Set default coordinates since this is the first atom. atom.coords = (20 + 250 * molCnt, 20, atom.coords[2]) molCnt += 1 angles = [] neighbours = sorted(atom.neighbours, key=lambda atom: atom.name) if prevAtom: prevChiral = prevAtom.chirality angles = prevAtom.getPreferredBondAngles(prevAngle, None, atoms + [atom]) if prevChiral and prevChiral.upper() in ('E', 'Z'): for prevNeighbour in prevAtom.neighbours: if prevAtom.getBondToAtom(prevNeighbour).bondType == 'double' and not prevNeighbour in atoms: prio = prevAtom.getPriorities() prioIndex = prio.index(atom) if prioIndex == 0 or (prioIndex == 1 and prio[0] == prevNeighbour): selfHeavy = True else: selfHeavy = False prevPrio = prevNeighbour.getPriorities() for p in prevPrio: if prevAtom != p: if p not in atoms: prevAngle = round(degrees(prevAtom.getBondAngle(prevNeighbour)), 0) angles = prevAtom.getPreferredBondAngles(prevAngle, None, atoms + [atom], ignoreHydrogens) prevHeavyNeighbourAngle = degrees(p.getBondAngle(prevNeighbour)) if (selfHeavy and prevChiral.upper() in ('Z')) or \ (not selfHeavy and prevChiral.upper() in ('E')): angles = [round(prevAngle - prevHeavyNeighbourAngle, 0)] else: angles = [-round(prevAngle - prevHeavyNeighbourAngle, 0)] break break if not prevAtom or prevX == None or prevY == None: prevX, prevY, prevZ = atom.coords prevAtom = atom prevAngle = None else: bonds = set(prevAtom.bonds & atom.bonds) if len(bonds) > 0: bond = bonds.pop() else: msg = 'Variant.snapAtomsToGrid: Cannot find bond between atoms to snap (%s and %s).' % (atom, prevAtom) raise Exception(msg) if bond.bondType == 'triple' or bond.bondType == 'double': bl = bondLength * 0.87 else: bl = bondLength prevAngle = atom.snapToGrid(prevAtom, bl, prevAngle, angles, atoms, ignoreHydrogens) prevAngle = (180 + prevAngle) % 360 rings = sorted(atom.getRings(), key=lambda ring: len(ring), reverse=True) if len(rings) > 0: atom.snapRings(rings, neighbours, atoms, prevAngle, bondLength) # Place the hydrogens if hydrogens: while hydrogens: atom = hydrogens.pop() # If placing hydrogens bound to a ring make sure that the prevAtom # is also in a ring. This makes it possible to avoid hydrogens # inside the ring. if atom.getRings(): for prevAtom in atom.neighbours: if prevAtom.getRings(): break else: prevAtom = set(atom.neighbours).pop() prevX, prevY, prevZ = prevAtom.coords neighbours = sorted(prevAtom.neighbours, key=lambda atom: atom.name) for neighbour in neighbours: if neighbour != atom and not neighbour in hydrogens: prevAngle = round(degrees(prevAtom.getBondAngle(neighbour)), 0) break angles = prevAtom.getPreferredBondAngles(prevAngle, neighbours, hydrogens + [atom], ignoreHydrogens=False) atom.snapToGrid(prevAtom, bondLength * 0.75, prevAngle, angles, hydrogens, ignoreHydrogens=False) for c in chiralities: for a in self.varAtoms: if a.name == c[0]: sc = a.getStereochemistry() if sc and sc.upper() != c[1].upper(): if sc.upper() in ('R', 'S'): temp = a.stereo[3] a.stereo[3] = a.stereo[1] a.stereo[1] = temp
# Snap the atoms in a ring to the preferred bond angles.
[docs] def snapRingToGrid(self, ring, anchor=None, prevAngle=None, bondLength=45.0, skippedAtoms=set([])): # ringAtoms = set(ring) ringAtoms = sorted(ring, key=lambda atom: atom.name) ringSize = len(ringAtoms) if ringSize < 3: msg = 'Variant.snapRingToGrid: Ring size must be larger than 2' raise Exception(msg) prevAtom = None ringAngle = ((ringSize - 2) * 180) / ringSize if anchor and anchor in ringAtoms: ringAtoms.remove(anchor) for skippedAtom in skippedAtoms: if skippedAtom in ringAtoms: ringAtoms.remove(skippedAtom) # Find a suitable angle of the "anchor atom" relative to a neighbour outside the ring. if anchor: atom = anchor neighbours = anchor.neighbours nNeighbours = len(neighbours) for neighbour in neighbours: if neighbour.element == 'H': nNeighbours -= 1 angle = (180 - 0.5 * ringAngle) if nNeighbours > 3: angle /= nNeighbours - 2 angles = [angle] if not prevAngle: for neighbour in neighbours: if neighbour in ring and neighbour not in ringAtoms: prevAngle = degrees(atom.getBondAngle(neighbour)) break if neighbour in skippedAtoms: d = atom.getAtomDist(neighbour) if abs(d - bondLength) < 0.001: prevAngle = degrees(atom.getBondAngle(neighbour)) else: if not prevAngle: prevAngle = 330 else: atom = ringAtoms.pop(0) prevAngle = 330 angles = [-ringAngle] if len(skippedAtoms) > 1: skippedNeighbours = anchor.neighbours & skippedAtoms if skippedNeighbours: sortedSkippedAtoms = sorted(skippedAtoms, key=lambda atom: atom.name) # for skippedNeighbour in skippedNeighbours: # if sortedSkippedAtoms.index(skippedNeighbour) != 0: # ringAngle = -ringAngle # break angles = [-ringAngle, ringAngle] anchor = None prevAtom = atom while ringAtoms: neighbours = sorted(prevAtom.neighbours & ring - skippedAtoms, key=lambda atom: atom.name) found = False atom = None if len(neighbours) != 0: for atom in neighbours: if atom in ringAtoms: break if not atom: for atom in ringAtoms: neighbours = atom.neighbours & ring if len(neighbours) > 0: neighbour = neighbours.pop() nextNeighbours = neighbour.neighbours & ring - set(ringAtoms) if atom in nextNeighbours: nextNeighbours.remove(atom) nextNeighbour = nextNeighbours.pop() prevAtom = neighbour prevAngle = degrees(neighbour.getBondAngle(nextNeighbour)) oldPrevAngle = prevAngle prevAngle = atom.snapToGrid(prevAtom, bondLength, prevAngle, angles) if not anchor and abs((oldPrevAngle + ringAngle) % 360 - prevAngle) < 1: angles = [ringAngle] anchor = None else: angles = [-ringAngle] prevAngle = (180 + prevAngle) % 360 ringAtoms.remove(atom) prevAtom = atom
[docs] def autoNameAtoms(self, varAtoms): used = self.compound.atomDict nonH = [a for a in varAtoms if a.element not in ('H', LINK)] hydrogens = [a for a in varAtoms if a.element == 'H'] if nonH: nAtoms = 1 nPrev = 0 atom = nonH[0] atoms = set([atom, ]) orderList = [atom] while nAtoms != nPrev: nPrev = nAtoms for atom in list(atoms): for atomB in atom.neighbours: if atomB.element in ('H', LINK): continue if atomB not in atoms: orderList.append(atomB) atoms.add(atomB) nAtoms = len(atoms) for i, atom in enumerate(orderList): atom.setName('@%d%s' % (i, atom.name)) i = 1 for atom in orderList: name = '%s%d' % (atom.element, i) while name in used: i += 1 name = '%s%d' % (atom.element, i) atom.setName(name) if hydrogens: for i, atom in enumerate(hydrogens): atom.setName('@%d%s' % (i, atom.name)) for atom in hydrogens: for atomB in atom.neighbours: if atomB.element in ('H', LINK): continue totalH = [a for a in atomB.neighbours if a.element == 'H'] name = nameBase = 'H%s' % (atomB.name[len(atomB.element):]) if len(totalH) > 1: index = totalH.index(atom) number = '%d' % (index + 1) if nameBase[-1].isdigit(): name = nameBase + '_' + number else: name = nameBase + number i = 1 while name in used: number = '%d' % (i) if nameBase[-1].isdigit(): name = nameBase + '_' + number else: name = nameBase + number i += 1 if atomB.element != 'C': i = 1 while name in used: name = 'H%s_%d' % (atomB.name, i) i += 1 i = 1 while name in used: name = 'H%d' % (i) i += 1 atom.setName(name) break
################
[docs]class Atom: def __init__(self, compound, element, name, isVariable=False): self.compound = compound self.element = element self.isVariable = isVariable self.varAtoms = set() self.isDeleted = False self.baseValences = ELEMENT_DATA.get(element, ELEMENT_DEFAULT)[0] if not name: self.defaultName() else: self.name = name compound.isModified = True compound.atoms.add(self) compound.atomDict[self.name] = self if isVariable: for varAtom in self.varAtoms: var = varAtom.variant var.updateDescriptor() def __repr__(self): return '<Atom %s %s>' % (self.element, self.name)
[docs] def setName(self, name): if name == self.name: return compound = self.compound compound.isModified = True prevName = self.name used = set(compound.atomDict.keys()) if name in used: name = self.defaultName() #raise Exception('Atom name "%s" already in use' % name) #return self.name = name neighbours = set() for varAtom in self.varAtoms: varAtom.name = name var = varAtom.variant if self.element == LINK: var.updatePolyLink() else: neighbours.update([va.atom for va in varAtom.neighbours]) if self.isVariable: for var in compound.variants: var.updateDescriptor() if prevName not in compound.atomDict: print("Atom Dict missing", prevName) nn = compound.atomDict.keys() nn.sort() print(', '.join(nn)) else: del compound.atomDict[prevName] compound.atomDict[name] = self for atom in neighbours: if atom.element == LINK: if ('prev' not in atom.name) and ('next' not in atom.name): atom.setName('link_%s' % name)
[docs] def defaultName(self): atoms = self.compound.atoms used = set([a.name for a in atoms]) elem = self.element i = 1 name = '%s%d' % (elem, i) while name in used: i += 1 name = '%s%d' % (elem, i) self.name = name return name
[docs] def setBaseValences(self, n): self.baseValences = n for varAtom in self.varAtoms: varAtom.updateValences()
[docs] def setVariable(self, value=True): compound = self.compound compound.isModified = True variants = list(compound.variants) for varAtom in self.varAtoms: varAtom.isVariable = value if value and not self.isVariable: self.isVariable = value # if it is variable need duplicate vars +/- this atom if self.element == 'H': for varAtom in self.varAtoms: varAtom.isLabile = True for varA in variants: # If a var has this atom if self not in varA.atomDict: continue varAtomA = varA.atomDict[self] bound = [va.atom for va in varAtomA.neighbours if va.element != 'H'] # make a copy without this atom atomsA = set(varA.varAtoms) atomsA.remove(varAtomA) var = Variant(self.compound, atomsA) for atomB in bound: varAtomB = var.atomDict.get(atomB) if varAtomB: varAtomB.setCharge(varAtomB.charge - 1, autoVar=False) elif not value: self.isVariable = value # If it is not variable only need the one eqiv var with this atom for varA in variants: # If a var has this atom if self not in varA.atomDict: continue # Remove other vars that are the same, save this atom atomsA = set([va.atom for va in varA.varAtoms]) atomsA.remove(self) for varB in variants: if varB is varA: continue atomsB = set([va.atom for va in varB.varAtoms]) if atomsA == atomsB: varB.delete() for var in self.compound.variants: var.updatePolyLink() var.updateDescriptor() names = [a.name for a in var.varAtoms] names.sort()
[docs] def delete(self): self.isDeleted = True compound = self.compound compound.isModified = True varAtoms = list(self.varAtoms) if self.element == LINK: # Delete all vars with same link for varAtom in varAtoms: if len(self.compound.variants) == 1: break else: varAtom.variant.delete() for varAtom in varAtoms: varAtom.delete() # deletes bonds and updates any neighbours compound.atoms.remove(self) del compound.atomDict[self.name] for var in self.compound.variants: var.updatePolyLink() var.updateDescriptor() del self
[docs]class AtomGroup: def __init__(self, compound, varAtoms, groupType): self.compound = compound self.varAtoms = set(varAtoms) self.groupType = groupType self.subGroups = set() self.variant = list(varAtoms)[0].variant compound = self.compound compound.isModified = True existing = set() orphaned = set() for varAtom in varAtoms: if varAtom.atomGroups: existing.update(varAtom.atomGroups) else: orphaned.add(varAtom) if groupType == AROMATIC: if existing: for group in existing: if group.groupType == AROMATIC: varAtoms2 = set(group.varAtoms) if len(varAtoms) > len(varAtoms2): if not varAtoms2 - varAtoms: group.delete() else: if not varAtoms - varAtoms2: group.delete() elif existing: # Check union with single other group; replace any if len(existing) == 1: group = existing.pop() if group.groupType != AROMATIC: group.delete() # Subgroups only allowed if fill current completely # Subgroups must be defined first elif orphaned: for group in existing: if group.groupType != AROMATIC: group.delete() else: self.subGroups = existing for varAtom in varAtoms: varAtom.atomGroups.add(self) self.variant.atomGroups.add(self) compound.atomGroups.add(self) if self.groupType == AROMATIC: for varAtom in self.varAtoms: for bond in varAtom.bonds: varAtomA, varAtomB = bond.varAtoms if (varAtomA in self.varAtoms) and (varAtomB in self.varAtoms): bond.bondType = 'aromatic' varAtom.updateValences()
[docs] def delete(self): compound = self.compound compound.isModified = True for varAtom in self.varAtoms: varAtom.atomGroups.remove(self) self.variant.atomGroups.remove(self) compound.atomGroups.remove(self) if self.groupType == AROMATIC: for bond in varAtom.bonds: varAtomA, varAtomB = bond.varAtoms if (varAtomA in self.varAtoms) and (varAtomB in self.varAtoms): bond.bondType = 'single' for varAtom in self.varAtoms: varAtom.updateValences() del self
BOND_TYPE_VALENCES = {'single': 1, 'double': 2, 'aromatic': 1, 'quadruple': 4, 'triple': 3, 'singleplanar': 1, 'dative': 1} BOND_STEREO_DICT = {4: (0, -1, 0, 1), # tetrahedral 5: (0, -1, 0, 1, 0), # trigonal bipyramidal 6: (0, -1, -1, 1, 1, 0), # octahedral 7: (0, -1, -1, 1, 1, 0, 0), # pentagonal bipyramidal }
[docs]class Bond: def __init__(self, varAtoms, bondType='single', autoVar=True): varAtomA, varAtomB = varAtoms if varAtomA.variant is not varAtomB.variant: raise Exception('VarAtom mismatch in bond formation %s-%s' % (varAtomA.name, varAtomB.name)) self.varAtoms = set(varAtoms) self.variant = variant = varAtomA.variant self.compound = self.variant.compound self.compound.isModified = True self.direction = varAtomA if bondType == 'dative' else None varAtomA.bonds.add(self) varAtomB.bonds.add(self) self.bondType = bondType self.removeDuplicates() varAtomA.updateValences() # Need this before auto varing varAtomB.updateValences() variant.bonds.add(self) varAtomA.updateNeighbours() varAtomB.updateNeighbours() nameA = varAtomA.name nameB = varAtomB.name atomA = varAtomA.atom atomB = varAtomB.atom elementA = varAtomA.element elementB = varAtomB.element nameLink = None nameH = None if (elementA == LINK) and (nameA == LINK): nameLink = (varAtomA, varAtomB) elif (elementB == LINK) and (nameB == LINK): nameLink = (varAtomB, varAtomA) elif (elementA == 'H') and nameB: nameH = (varAtomA, varAtomB) elif (elementB == 'H') and nameA: nameH = (varAtomB, varAtomA) if nameLink: varAtom1, varAtom2 = nameLink name = varAtom2.name or varAtom2.element varAtom1.atom.setName('%s_%s' % (LINK, name)) elif nameH and autoVar: varAtom1, varAtom2 = nameH name = varAtom2.name if name.startswith(varAtom2.element): name = name[len(varAtom2.element):] if name: firstName = 'H' + name hydrogens = [a for a in varAtom2.neighbours if a.element == 'H'] if len(hydrogens) > 1: variant.autoNameAtoms(hydrogens) if autoVar: failedVars = set() for var in list(self.compound.variants): if var is not variant: atomDict = var.atomDict varAtomC = atomDict.get(atomA) varAtomD = atomDict.get(atomB) if varAtomC and varAtomD: # Both exist in this var getBond = var.getBond(varAtomC, varAtomD, autoVar=False) if not getBond: if varAtomC not in varAtomD.neighbours: failedVars.add(var) elif varAtomC and not varAtomC.neighbours: varAtomC.delete() # E.g. proton not in this link var elif varAtomD and not varAtomD.neighbours: varAtomD.delete() # E.g. proton not in this link var for var in failedVars: var.delete() if elementA == 'O' and elementB == 'H': self.checkCarboxylVar(varAtomA, varAtomB) elif elementB == 'O' and elementA == 'H': self.checkCarboxylVar(varAtomB, varAtomA) elif elementA == 'N' and elementB == 'H': self.checkAmineVar(varAtomA, varAtomB) elif elementB == 'N' and elementA == 'H': self.checkAmineVar(varAtomB, varAtomA) varAtomA.updateValences() varAtomB.updateValences() def __repr__(self): aNames = [a.name for a in self.varAtoms] aNames.sort() aName = '-'.join(aNames) return '<Bond %s %s>' % (aName, self.bondType)
[docs] def checkCarboxylVar(self, oAtom, hAtom): neighbours = set(oAtom.neighbours) neighbours.remove(hAtom) if not neighbours: return other = neighbours.pop() if other.element == 'C': neighbours2 = set(other.neighbours) neighbours2.remove(oAtom) variant = oAtom.variant compound = variant.compound bondsC = set(other.bonds) for atom in neighbours2: if atom.element == 'O': bondsO = set(atom.bonds) common = bondsO & bondsC if not common: continue if common.pop().bondType != 'double': continue hAtom.atom.setVariable(True) for varAtom in oAtom.atom.varAtoms: if varAtom.freeValences: varAtom.setCharge(-1) for var in compound.variants: var.updatePolyLink() var.updateDescriptor()
[docs] def checkAmineVar(self, nAtom, hAtom): neighbours = nAtom.neighbours hydrogens = [a for a in neighbours if a.element == 'H'] if len(hydrogens) > 2 and len(neighbours) == 4: compound = nAtom.variant.compound hAtom.setVariable(True) for varAtom in nAtom.atom.varAtoms: if varAtom.freeValences: varAtom.setCharge(0) for var in compound.variants: var.updatePolyLink() var.updateDescriptor()
[docs] def setBondType(self, bondType): if bondType != self.bondType: compound = self.compound compound.isModified = True varAtomA, varAtomB = self.varAtoms nValPrev = BOND_TYPE_VALENCES[self.bondType] nValNext = BOND_TYPE_VALENCES[bondType] added = nValNext - nValPrev while added > 0: if varAtomA.freeValences: varAtomA.freeValences.pop() if varAtomB.freeValences: varAtomB.freeValences.pop() added -= 1 while added < 0: varAtomA.freeValences.append(0.0) varAtomB.freeValences.append(0.0) added += 1 if bondType == 'dative': if self.direction is varAtomA: self.direction = varAtomB else: self.direction = varAtomA if added: varAtomA.updateValences() varAtomB.updateValences() self.bondType = bondType
[docs] def removeDuplicates(self): varAtomA, varAtomB = self.varAtoms nVals = int(BOND_TYPE_VALENCES[self.bondType]) # Could trap errors here commonBonds = varAtomA.bonds & varAtomB.bonds n = len(commonBonds) if n > 1: for bond in commonBonds: if bond is not self: bond.delete()
[docs] def delete(self): compound = self.compound compound.isModified = True varAtomA, varAtomB = self.varAtoms varAtomA.variant.bonds.remove(self) varAtomA.bonds.remove(self) varAtomB.bonds.remove(self) varAtomA.stereo = [] varAtomB.stereo = [] varAtomA.freeValences.append(0.0) varAtomB.freeValences.append(0.0) groups = varAtomA.atomGroups | varAtomB.atomGroups for group in groups: if group.groupType == AROMATIC: if self.bondType == AROMATIC: group.delete() else: group.delete() varAtomA.updateValences() varAtomB.updateValences() varAtomA.updateNeighbours() varAtomB.updateNeighbours() del self
[docs] def deleteAll(self): atoms = set([va.atom for va in self.varAtoms]) delBonds = [] for var in self.compound.variants: for bond in var.bonds: atoms2 = set([va.atom for va in bond.varAtoms]) if atoms == atoms2: delBonds.append(bond) for bond in delBonds: bond.delete()
[docs]def loadCompoundPickle(fileName): pass
[docs]class Compound: def __init__(self, name): self.name = name self.keywords = set() self.details = None self.variants = set() self.atoms = set() self.atomDict = {} self.atomGroups = set() self.defaultVars = set() self.ccpCode = None self.ccpMolType = 'other' self.isModified = True
[docs] def hasSubGraph(self, fragement): pass
[docs] def getAtom(self, element,