diff --git a/README.md b/README.md index b12ad99..6819a5e 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # rdeditor Simple RDKit molecule editor GUI using PySide6 -![rdeditor, the RDKit molecule editor](./Screenshots/Main_window.png) + +![rdeditor, the RDKit molecule editor](https://github.com/EBjerrum/rdeditor/blob/master/Screenshots/Main_window.png?raw=true) ## Installation - requirements -RDKit, NumPy, PySide6 and pyqtdarktheme should be automatically pip installed by the setup.py script. +RDKit, NumPy, PySide6 and pyqtdarktheme should be automatically installed when installed via pip. - installation @@ -20,12 +21,39 @@ A launch script will also be added so that it can be started from the command li ## Usage -Can be started with `rdEditor` or `rdEditor your_molecule.mol` to start edit an existing molecule. -Interactions with the molecule are done via clicking on the canvas, atoms or bonds. A choice of tools is available. +Can be started from the command line with `rdEditor` or `rdEditor your_molecule.mol` to start edit an existing molecule. +Interactions with the molecule are done via clicking and dragging on the canvas, atoms or bonds. A choice of tools is available. + +To edit a molecule, select the pen tool, and an atom, bond or template type and click on the canvas to add it. + +Clicking: + +- When clicking existing atoms or bonds, the clicked atom or bond will be modified, depending on the atom or bond type selected. +- If a bondtype is selected, the bond will be added to the clicked atom with a carbon atom at the other end. +- If a bondtype is selected and a bond is clicked, the bond will be changed to that type if different. +- If you click multiple times on a bond with an atomtype selected, the bondtype will cycle between single, double and triple bond. + +Dragging: + +- If you click and drag from an atom, the selected atomtype will be added at the end of a single bond. +- Dragging between two atoms will add a bond between them. If a bondtype is selected, the bondorder will correspond to the bondtype selected. +- Dragging on the canvas with an atomtype will add two atoms of that type with a single bond between them. +- Dragging on the canvas with a bondtype will simply add that bond with carbon atoms at both ends. + +Templates: + +- Templates work kind of like atoms, so if you click on an atom, the template will be added directly to that atom. +- If a template is selected and dragged from an atom, the template will be added with a single bond to the clicked atom. +- Some templates can also be added to bonds by clicking on the middle of the bond. +- Dragging on a canvas with a template will add a carbon atom and a single bond to the template. + +Other actions: + +- most other actions (R/S, E/Z, Increase/Decrease charge, Adjust atom number) works by clikcing on existing atoms. #### Top Menu: -![top menu of rdeditor, the RDKit molecule editor](./Screenshots/Top_Menu.png) +![top menu of rdeditor, the RDKit molecule editor](https://github.com/EBjerrum/rdeditor/blob/master/Screenshots/Top_Menu.png?raw=true) From left to right @@ -35,20 +63,21 @@ From left to right - Arrow: Select tool. Click on an atom to select it, click on the canvas to deselect. Clicking on multiple atoms one after another will select them, but only the lastly clicked one will be highlighted in red and used for operations, such as bond creation to another existing atom. - Pen: Add tool. Clicking on an existing atom will add the current selected atom type to that atom with a single bond. Clicking on the canvas will add a disconnected atom. Clicking on a bond will cycle through single, double and triple bonds. -- Add bond / Join atoms: Will add a single bond between a clicked atom (or a previously selected atom) and the next atom clicked. -- Change Atom: Will substitute the atom clicked, with the currently selected atom type -- R/S: Change the stereo chemistry of the selected atom (see issues below) -- E/Z: Change E/Z stereo of double bonds (see issues below) +- R/S: Change the stereo chemistry of the selected atom +- E/Z: Change E/Z stereo of double bonds - Increase/Decrease charge: Will increase or decrease the charge of the atom clicked +- Set atommap or R-group number: Will set the atommap or R-group number of the atom clicked +- clean up coordinates: recalculate coordinates disregarding existing coordinates. +- clean up chemistry. Sanitize and/or Kekulize the molecule. - Delete atom/bond: - Clear Canvas - Undo. #### Side Bar: -![top menu of rdeditor, the RDKit molecule editor](./Screenshots/Side_bar.png) +![top menu of rdeditor, the RDKit molecule editor](https://github.com/EBjerrum/rdeditor/blob/master/Screenshots/Side_bar.png?raw=true) -Most commonly used bond types, and atom types can be selected. A Periodic table is accessible for exotic atom types. +Most commonly used bond types, and atom types can be selected. Templates and R-group (dummy atoms) are also accessible. A Periodic table is accessible for exotic atom types. #### Dropdown menus @@ -56,10 +85,12 @@ Access to all standard operations as well as less used atom types and bond-types #### Settings -Themes can be selected from the ones available on your platform (Mac/Linux/Windows) +Themes can be selected from the ones available on your platform (Mac/Linux/Windows). The debug level can be selected +Cleanup settings can be selected if the molecule should be sanitized or kekulized during cleanup. + ## Development Instructions to set it up in editable modes and instructions for eventual contributions can be found in the [DEVELOPER.md](./DEVELOPER.md) file. diff --git a/Screenshots/Main_window.png b/Screenshots/Main_window.png index 31e5b50..8914d53 100644 Binary files a/Screenshots/Main_window.png and b/Screenshots/Main_window.png differ diff --git a/Screenshots/Side_bar.png b/Screenshots/Side_bar.png index 6101b1b..6c163d5 100644 Binary files a/Screenshots/Side_bar.png and b/Screenshots/Side_bar.png differ diff --git a/Screenshots/Top_Menu.png b/Screenshots/Top_Menu.png index bee43d0..ea08642 100644 Binary files a/Screenshots/Top_Menu.png and b/Screenshots/Top_Menu.png differ diff --git a/rdeditor/molEditWidget.py b/rdeditor/molEditWidget.py index 5d8905e..f25abac 100644 --- a/rdeditor/molEditWidget.py +++ b/rdeditor/molEditWidget.py @@ -1,6 +1,14 @@ #!/usr/bin/python # Import required modules from PySide6 import QtCore, QtGui, QtSvg, QtWidgets + +from PySide6.QtWidgets import QApplication, QMainWindow +from PySide6.QtSvgWidgets import QSvgWidget +from PySide6.QtCore import Qt, QPointF +from PySide6.QtGui import QMouseEvent, QPainter, QPen +import math + + import sys import logging from warnings import warn @@ -24,7 +32,8 @@ from .ptable import symboltoint -debug = True + +debug = True # TODO is this still used? # The Molblock editor class @@ -34,6 +43,7 @@ def __init__(self, mol=None, parent=None): super(MolEditWidget, self).__init__(parent) # This sets the window to delete itself when its closed, so it doesn't keep querying the model self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.is_dragging = False # If a drag event is being performed # Templater handler self.templatehandler = TemplateHandler() @@ -72,7 +82,7 @@ def __init__(self, mol=None, parent=None): def action(self): return self._action - @action.setter + @action.setter # TODO make it more explicit what actions are available here. def action(self, actionname): if actionname != self.action: self._action = actionname @@ -294,94 +304,166 @@ def get_molobject(self, event): def mousePressEvent(self, event): if event.button() is QtCore.Qt.LeftButton: - clicked = self.get_molobject(event) - if isinstance(clicked, Chem.rdchem.Atom): - self.logger.debug( - "You clicked atom %i, with atomic number %i" % (clicked.GetIdx(), clicked.GetAtomicNum()) - ) - # Call the atom_click function - self.atom_click(clicked) - # self.add_atom(self.pen, clicked) - elif isinstance(clicked, Chem.rdchem.Bond): - self.logger.debug("You clicked bond %i with type %s" % (clicked.GetIdx(), clicked.GetBondType())) - self.bond_click(clicked) - elif isinstance(clicked, Point2D): - self.logger.debug("Canvas Click") - self.canvas_click(clicked) + # For visual feedback on the dragging event + self.press_pos = event.position() + self.current_pos = event.position() + self.is_dragging = True + + # For chemistry + self.start_molobject = self.get_molobject(event) + + def mouseMoveEvent(self, event: QMouseEvent): + if self.is_dragging: + self.current_pos = event.position() + self.update() + + def mouseReleaseEvent(self, event): + if event.button() is QtCore.Qt.LeftButton: + end_mol_object = self.get_molobject(event) + if self.is_same_object(self.start_molobject, end_mol_object): + self.event_handler(self.start_molobject, None) # Click events has None as second object else: - self.logger.error(f"Clicked entity, {clicked} of unknown type {type(clicked)}") + self.event_handler( + self.start_molobject, end_mol_object + ) # Drag events has different objects as start and end + self.start_molobject = None + + self.is_dragging = False + self.update() # Final repaint to clear the line + + def paintEvent(self, event): + super().paintEvent(event) # Render the SVG (Molecule) + + # Paint a line from where the canvas was clicked to the current position. + if self.is_dragging: + painter = QPainter(self) + pen = QPen(Qt.gray, 4, Qt.SolidLine) + painter.setPen(pen) + painter.drawLine(self.press_pos, self.current_pos) + + def is_same_object(self, object1, object2): + if isinstance(object1, Chem.rdchem.Atom) and isinstance(object2, Chem.rdchem.Atom): + return object1.GetIdx() == object2.GetIdx() + if isinstance(object1, Chem.rdchem.Bond) and isinstance(object2, Chem.rdchem.Bond): + return object1.GetIdx() == object2.GetIdx() + if isinstance(object1, Point2D) and isinstance(object2, Point2D): + distance = (object1 - object2).Length() + self.logger.debug(f"Dragged distance on Canvas {distance}") + if distance < 0.1: + return True + return False + + # def clicked_handler(self, clicked): + # try: + # self.event_handler(clicked, None) + # except Exception as e: + # self.logger.error(f"Error in clicked_handler: {e}") + + # def drag_handler(self, object1, object2): + # try: + # self.event_handler(object1, object2) + # except Exception as e: + # self.logger.error(f"Error in drag_handler: {e}") + + def event_handler(self, object1, object2): + # Matches which objects are clicked/dragged and what chemical type and action is selected + # With click events, the second object is None + # Canvas clicks and drags are Point2D objects + match (object1, object2, self.chemEntityType, self.action): + # Atom click events + # different enitity types + case (Chem.rdchem.Atom(), None, "atom", "Add"): + self.replace_on_atom(object1) + case (Chem.rdchem.Atom(), None, "ring", "Add"): + self.add_ring_to_atom(object1) + case (Chem.rdchem.Atom(), None, "bond", "Add"): + self.add_bond_to_atom(object1) + + # Atom click with differentactions + case (Chem.rdchem.Atom(), None, _, "Remove"): + self.remove_atom(object1) + case (Chem.rdchem.Atom(), None, _, "Select"): + self.select_atom_add(object1) + case (Chem.rdchem.Atom(), None, _, "Increase Charge"): + self.increase_charge(object1) + case (Chem.rdchem.Atom(), None, _, "Decrease Charge"): + self.decrease_charge(object1) + case (Chem.rdchem.Atom(), None, _, "Number Atom"): + self.number_atom(object1) + case (Chem.rdchem.Atom(), None, _, "RStoggle"): + self.toogleRS(object1) + + # Bond click events + case (Chem.rdchem.Bond(), None, _, "Add"): + self.add_to_bond(object1) + case (Chem.rdchem.Bond(), None, _, "Remove"): + self.remove_bond(object1) + case (Chem.rdchem.Bond(), None, _, "Select"): + self.select_bond(object1) + case (Chem.rdchem.Bond(), None, _, "Replace"): + self.replace_on_bond(object1) + case (Chem.rdchem.Bond(), None, _, "EZtoggle"): + self.toogleEZ(object1) + + # Canvas click events + case (Point2D(), None, _, "Add"): + self.add_canvas_entity(object1) + case (Point2D(), None, _, "Select"): + self.clearAtomSelection() + + # Drag events + # Atom to Atom + case (Chem.rdchem.Atom(), Chem.rdchem.Atom(), _, "Add"): + self.add_bond_between_atoms(object1, object2) + + # Atom to Canvas actions + case (Chem.rdchem.Atom(), Point2D(), "atom", "Add"): + self.add_atom_to_atom(object1) + case (Chem.rdchem.Atom(), Point2D(), "ring", "Add"): + self.add_bonded_ring_to_atom(object1) + case (Chem.rdchem.Atom(), Point2D(), "bond", "Add"): + self.add_bond_to_atom(object1) + # Drag on canvas + case (Point2D(), Point2D(), _, "Add"): + self.canvas_drag(object1, object2) + + # Default case for undefined actions + case _: + self.logger.warning( + f"Undefined action for combination: " + f"{(type(object1), type(object2), self.chemEntityType, self.action)}" + ) - # Lookup tables to relate actions to context type with action type #TODO more clean to use Dictionaries?? def atom_click(self, atom): - if self.action == "Add": - self.add_to_atom(atom) - elif self.action == "Remove": - self.remove_atom(atom) - elif self.action == "Select": - self.select_atom_add(atom) - elif self.action == "Replace": - self.replace_on_atom(atom) - elif self.action == "Add Bond": - self.add_bond(atom) - elif self.action == "Increase Charge": - self.increase_charge(atom) - elif self.action == "Decrease Charge": - self.decrease_charge(atom) - elif self.action == "Number Atom": - self.number_atom(atom) - elif self.action == "RStoggle": - self.toogleRS(atom) - else: - self.logger.warning("The combination of Atom click and Action %s undefined" % self.action) + self.logger.warn("atom_click is deprecated. Use event_handler instead.", DeprecationWarning, stacklevel=2) + + def atom_drag(self, atom): + self.logger.warn("atom_drag is deprecated. Use event_handler instead.", DeprecationWarning, stacklevel=2) def bond_click(self, bond): - if self.action == "Add": - self.add_to_bond(bond) - elif self.action == "Add Bond": - self.replace_bond(bond) - elif self.action == "Remove": - self.remove_bond(bond) - elif self.action == "Select": - self.select_bond(bond) - elif self.action == "Replace": - self.replace_on_bond(bond) - elif self.action == "EZtoggle": - self.toogleEZ(bond) - else: - self.logger.warning("The combination of Bond click and Action %s undefined" % self.action) + self.logger.warn("bond_click is deprecated. Use event_handler instead.", DeprecationWarning, stacklevel=2) def canvas_click(self, point): - if self.action == "Add": - self.add_canvas_entity(point) - - elif self.action == "Select": - # Click on canvas - # Unselect any selected - if len(self.selectedAtoms) > 0: - self.clearAtomSelection() - else: - self.logger.warning("The combination of Canvas click and Action %s undefined" % self.action) + self.logger.warn("canvas_click is deprecated. Use event_handler instead.", DeprecationWarning, stacklevel=2) def add_to_atom(self, atom): - if self.chemEntityType == "atom": - self.add_atom_to_atom(atom) - if self.chemEntityType == "ring": - self.add_ring_to_atom(atom) - if self.chemEntityType == "bond": - self.add_bond_to_atom(atom) + self.logger.warn("add_to_atom is deprecated. Use event_handler instead.", DeprecationWarning, stacklevel=2) - def getNewAtom(self): - newatom = Chem.rdchem.Atom(self.chemEntity) + def getNewAtom(self, chemEntity): + newatom = Chem.rdchem.Atom(chemEntity) if newatom.GetAtomicNum() == 0: newatom.SetProp("dummyLabel", "R") return newatom - def add_atom_to_atom(self, atom): + def add_atom_to_atom(self, atom, chemEntity=None): + if not chemEntity: + chemEntity = self.chemEntity rwmol = Chem.rdchem.RWMol(self.mol) - newatom = self.getNewAtom() + newatom = self.getNewAtom(chemEntity) newidx = rwmol.AddAtom(newatom) newbond = rwmol.AddBond(atom.GetIdx(), newidx, Chem.rdchem.BondType.SINGLE) self.mol = rwmol + return self.mol.GetAtomWithIdx(newidx) def add_bond_to_atom(self, atom): rwmol = Chem.rdchem.RWMol(self.mol) @@ -390,6 +472,10 @@ def add_bond_to_atom(self, atom): newbond = rwmol.AddBond(atom.GetIdx(), newidx, order=self.chemEntity) self.mol = rwmol + def add_bonded_ring_to_atom(self, atom): + new_atom = self.add_atom_to_atom(atom, chemEntity="C") + self.add_ring_to_atom(new_atom) + def add_ring_to_atom(self, atom): mol = self.templatehandler.apply_template_to_atom(atom, self.chemEntity) self.mol = mol @@ -414,12 +500,14 @@ def add_canvas_entity(self, point): if self.chemEntityType == "bond": self.add_canvas_bond(point) - def add_canvas_atom(self, point): + def add_canvas_atom(self, point, chemEntity=None): + if chemEntity is None: + chemEntity = self.chemEntity rwmol = Chem.rdchem.RWMol(self.mol) if rwmol.GetNumAtoms() == 0: point.x = 0.0 point.y = 0.0 - newatom = self.getNewAtom() + newatom = self.getNewAtom(chemEntity) newidx = rwmol.AddAtom(newatom) # This should only trigger if we have an empty canvas if not rwmol.GetNumConformers(): @@ -428,8 +516,9 @@ def add_canvas_atom(self, point): p3 = Point3D(point.x, point.y, 0) conf.SetAtomPosition(newidx, p3) self.mol = rwmol + return self.mol.GetAtomWithIdx(newidx) - def add_canvas_bond(self, point): + def add_canvas_bond(self, point, point2=None): rwmol = Chem.rdchem.RWMol(self.mol) if rwmol.GetNumAtoms() == 0: point.x = 0.0 @@ -437,7 +526,6 @@ def add_canvas_bond(self, point): atom_0 = rwmol.AddAtom(Chem.rdchem.Atom(6)) atom_1 = rwmol.AddAtom(Chem.rdchem.Atom(6)) - print(self.chemEntity) newidx = rwmol.AddBond(atom_0, atom_1, order=self.chemEntity) # This should only trigger if we have an empty canvas @@ -445,7 +533,10 @@ def add_canvas_bond(self, point): rdDepictor.Compute2DCoords(rwmol) conf = rwmol.GetConformer(0) p3 = Point3D(point.x, point.y, 0) - conf.SetAtomPosition(self.mol.GetNumAtoms(), p3) + conf.SetAtomPosition(atom_0, p3) + if point2: + p3 = Point3D(point2.x, point2.y, 0) + conf.SetAtomPosition(atom_1, p3) self.mol = rwmol def add_canvas_ring(self, point): @@ -477,23 +568,50 @@ def replace_on_atom(self, atom): def replace_atom(self, atom): rwmol = Chem.rdchem.RWMol(self.mol) - newatom = self.getNewAtom() + newatom = self.getNewAtom(self.chemEntity) rwmol.ReplaceAtom(atom.GetIdx(), newatom) self.mol = rwmol # Double step action - def add_bond(self, atom): - if len(self.selectedAtoms) > 0: - selected = self.selectedAtoms[-1] - rwmol = Chem.rdchem.RWMol(self.mol) - neighborIdx = [atm.GetIdx() for atm in self.mol.GetAtomWithIdx(selected).GetNeighbors()] - if atom.GetIdx() not in neighborIdx: # check if bond already exists - bondType = self.chemEntity if self.chemEntityType == "bond" else Chem.rdchem.BondType.SINGLE - rwmol.AddBond(selected, atom.GetIdx(), order=bondType) - self.mol = rwmol - self.selectedAtoms = [] - else: - self.select_atom(atom) + def add_bond_to_last_selected(self, atom): + self.logger.warn( + "add_bond_to_last_selected is deprecated. Use event_handler instead.", DeprecationWarning, stacklevel=2 + ) + + def add_bond_between_atoms(self, atom1, atom2): + rwmol = Chem.rdchem.RWMol(self.mol) + neighborIdx = [atm.GetIdx() for atm in atom1.GetNeighbors()] + if atom2.GetIdx() not in neighborIdx: # check if bond already exists + bondType = self.chemEntity if self.chemEntityType == "bond" else Chem.rdchem.BondType.SINGLE + rwmol.AddBond(atom1.GetIdx(), atom2.GetIdx(), order=bondType) + self.mol = rwmol + + def canvas_drag(self, point1, point2): + if self.chemEntityType == "atom": + self.canvas_drag_atom(point1, point2) + if self.chemEntityType == "ring": + self.canvas_drag_ring(point1, point2) + if self.chemEntityType == "bond": + self.canvas_drag_bond(point1, point2) + + def canvas_drag_atom(self, point1, point2): + # In essence adding a bond, but can be between non-carbon atoms, and make behaviour more consistent + # i.e. if drag-drawing) + rwmol = Chem.rdchem.RWMol(self.mol) + newatom = self.getNewAtom(self.chemEntity) + newatom2 = self.getNewAtom(self.chemEntity) + newidx = rwmol.AddAtom(newatom) + newidx2 = rwmol.AddAtom(newatom2) + newbond = rwmol.AddBond(newidx, newidx2, Chem.rdchem.BondType.SINGLE) + self.mol = rwmol + + def canvas_drag_ring(self, point1, point2): + # TODO, in principle to be consistent we should be adding a two templates with a bond in between?? + newatom = self.add_canvas_atom(point1, chemEntity="C") + self.add_bonded_ring_to_atom(newatom) + + def canvas_drag_bond(self, point1, point2): + self.add_canvas_bond(point1, point2) def toogleRS(self, atom): self.backupMol() diff --git a/rdeditor/molViewWidget.py b/rdeditor/molViewWidget.py index db07ea9..8cd606f 100644 --- a/rdeditor/molViewWidget.py +++ b/rdeditor/molViewWidget.py @@ -258,6 +258,18 @@ def updateStereo(self): sanitizeSignal = QtCore.Signal(str, name="sanitizeSignal") + def updateStereo(self): + self.logger.debug("Updating stereo info") + for atom in self.mol.GetAtoms(): + if atom.HasProp("_CIPCode"): + atom.ClearProp("_CIPCode") + for bond in self.mol.GetBonds(): + if bond.HasProp("_CIPCode"): + bond.ClearProp("_CIPCode") + Chem.rdmolops.SetDoubleBondNeighborDirections(self.mol) + self.mol.UpdatePropertyCache(strict=False) + Chem.rdCIPLabeler.AssignCIPLabels(self.mol) + @QtCore.Slot() def sanitizeDrawMol(self, kekulize=False, drawkekulize=False): self.updateStereo() diff --git a/rdeditor/rdEditor.py b/rdeditor/rdEditor.py index c1ea99d..dd6f895 100644 --- a/rdeditor/rdEditor.py +++ b/rdeditor/rdEditor.py @@ -168,9 +168,11 @@ def CreateMenus(self): self.toolMenu.addAction(self.selectAction) self.toolMenu.addAction(self.addAction) - self.toolMenu.addAction(self.addBondAction) - self.toolMenu.addAction(self.replaceAction) - self.toolMenu.addAction(self.rsAction) + # self.toolMenu.addAction(self.addBondAction) + # self.toolMenu.addAction(self.replaceAction) + self.toolMenu.addAction( + self.rsAction + ) # TODO, R/S and E/Z could be changed for a single action? it really depends if an atom or a bond is clicked! self.toolMenu.addAction(self.ezAction) self.toolMenu.addAction(self.increaseChargeAction) self.toolMenu.addAction(self.decreaseChargeAction) @@ -257,8 +259,8 @@ def CreateToolBars(self): self.mainToolBar.addSeparator() self.mainToolBar.addAction(self.selectAction) self.mainToolBar.addAction(self.addAction) - self.mainToolBar.addAction(self.addBondAction) - self.mainToolBar.addAction(self.replaceAction) + # self.mainToolBar.addAction(self.addBondAction) + # self.mainToolBar.addAction(self.replaceAction) self.mainToolBar.addAction(self.rsAction) self.mainToolBar.addAction(self.ezAction) self.mainToolBar.addAction(self.increaseChargeAction) diff --git a/rdeditor/templatehandler.py b/rdeditor/templatehandler.py index b4f5e55..56a489b 100644 --- a/rdeditor/templatehandler.py +++ b/rdeditor/templatehandler.py @@ -9,42 +9,42 @@ class TemplateHandler: templates = { "benzene": { "canvas": "C1=CC=CC=C1", - "atom": "[998*:1]>>[beginisotope*:1]-C1=C-C=C-C=C-1", + "atom": "[998*:1]>>[beginisotope*:1]1=C-C=C-C=C-1", "sp3": "[998*:1]-[999*:2]>>[beginisotope*:1]1-[endisotope*:2]=C-C=C-C=1", "sp2": "[998*:1]~[999*:2]>>[beginisotope*:1]1~[endisotope*:2]-C=C-C=C-1", "aromatic": "[998*:1]~[999*:2]>>[beginisotope*:1]1:[beginisotope*:2]:C:C:C:C:1", }, "cyclohexane": { "canvas": "C1CCCCC1", - "atom": "[998*:1]>>[beginisotope*:1]-C1CCCCC1", + "atom": "[998*:1]>>[beginisotope*:1]1CCCCC1", "sp3": "[998*:1]-[999*:2]>>[beginisotope*:1]1-[endisotope*:2]-C-C-C-C-1", "sp2": "[998*:1]~[999*:2]>>[beginisotope*:1]1~[endisotope*:2]-C-C-C-C-1", "aromatic": "[998*:1]~[999*:2]>>[beginisotope*:1]1:[beginisotope*:2]-C-C-C-C-1", }, "cyclopentane": { "canvas": "C1CCCC1", - "atom": "[998*:1]>>[beginisotope*:1]-C1CCCC1", + "atom": "[998*:1]>>[beginisotope*:1]1CCCC1", "sp3": "[998*:1]-[999*:2]>>[beginisotope*:1]1-[endisotope*:2]-C-C-C-1", "sp2": "[998*:1]~[999*:2]>>[beginisotope*:1]1~[endisotope*:2]-C-C-C-1", "aromatic": "[998*:1]~[999*:2]>>[beginisotope*:1]1:[beginisotope*:2]-C-C-C-1", }, "cyclobutane": { "canvas": "C1CCC1", - "atom": "[998*:1]>>[beginisotope*:1]-C1CCC1", + "atom": "[998*:1]>>[beginisotope*:1]1CCC1", "sp3": "[998*:1]-[999*:2]>>[beginisotope*:1]1-[endisotope*:2]-C-C-1", "sp2": "[998*:1]~[999*:2]>>[beginisotope*:1]1~[endisotope*:2]-C-C-1", "aromatic": "[998*:1]~[999*:2]>>[beginisotope*:1]1:[beginisotope*:2]-C-C-1", }, "cyclopropane": { "canvas": "C1CC1", - "atom": "[998*:1]>>[beginisotope*:1]-C1CC1", + "atom": "[998*:1]>>[beginisotope*:1]1CC1", "sp3": "[998*:1]-[999*:2]>>[beginisotope*:1]1-[endisotope*:2]-C-1", "sp2": "[998*:1]~[999*:2]>>[beginisotope*:1]1~[endisotope*:2]-C-1", "aromatic": "[998*:1]~[999*:2]>>[beginisotope*:1]1:[beginisotope*:2]-C-1", }, "carboxylic acid": { "canvas": "C(=O)[O]", - "atom": "[998*:1]>>[beginisotope*:1]-C(=O)[O]", + "atom": "[998*:1]>>[beginisotope*:1](=O)[O]", }, # These types of templates need more work, i.e. if an NC bond is clicked, the addition can be non-sanitizable # due to the explicit H (or vice versa!)