matteoferla/Fragmenstein

View on GitHub
colab-notebooks/colab_playground.ipynb

Summary

Maintainability
Test Coverage
{
 "nbformat": 4,
 "nbformat_minor": 0,
 "metadata": {
  "colab": {
   "name": "colab_fragmenstein_playground.ipynb",
   "provenance": [],
   "collapsed_sections": []
  },
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "cells": [
  {
   "cell_type": "markdown",
   "source": [
    "### Fragmenstein demo — Playground (Light)\n",
    "\n",
    "Fragmenstein is a position-based fragment-merging/placing python3 tool.\n",
    "\n",
    "<img src=\"https://github.com/matteoferla/Fragmenstein/raw/master/images/overview.png\" width=\"800\" alt=\"logo\">\n",
    "\n",
    "In its merging/linking operation, under the coordination of the class Victor,\n",
    "the class Monster finds spatially overlapping atoms and stitches them together (with RDKit),\n",
    "then the class Igor reanimates (minimises in PyRosetta) them within the protein site restraining the atoms to original positions.\n",
    "As this compound may not be purchasable, one can use the placement operation to\n",
    "make a stitched-together molecule based a template.\n",
    "\n",
    "Fragmenstein can partially work without PyRosetta, as PyRosetta take a few minutes to be installed it's not everyone's cup of tea. Hence this lighter version.\n",
    "\n",
    "| Name | Colab Link | PyRosetta | Description |\n",
    "| :--- | :--- | :---: | :--- |\n",
    "| Pipeline | [![colab demo](https://img.shields.io/badge/Run_full_demo-fragmenstein.ipynb-f9ab00?logo=googlecolab)](https://colab.research.google.com/github/matteoferla/Fragmenstein/blob/master/colab-notebooks/colab_fragmenstein.ipynb) | &#10004;| Given a template and a some hits, <br>merge them <br>and place the most similar purchasable analogues from Enamine REAL |\n",
    "| Light | [![colab demo](https://img.shields.io/badge/Run_light_demo-fragmenstein.ipynb-f9ab00?logo=googlecolab)](https://colab.research.google.com/github/matteoferla/Fragmenstein/blob/master/colab-notebooks/colab_playground.ipynb) | &#10060;| Generate molecules and see how they merge<br>and how a placed compound fairs|\n",
    "\n",
    "### Operations\n",
    "\n",
    "1. Generate molecules (as a test of operations)\n",
    "2. Move molecules for testing and merge molecules via the classes Walton+Monster\n",
    "3. Search for similars\n",
    "4. Place a SMILES\n",
    "\n",
    "### See also\n",
    "* Fragmenstein:\n",
    "   [![Documentation Status](https://readthedocs.org/projects/fragmenstein/badge/?version=latest)](https://fragmenstein.readthedocs.io/en/latest/?badge=latest)\n",
    "[![ github forks matteoferla Fragmenstein?label=Fork&style=social](https://img.shields.io/github/forks/matteoferla/Fragmenstein?label=Fork&style=social&logo=github)](https://github.com/matteoferla/Fragmenstein)\n",
    "[![ github stars matteoferla Fragmenstein?style=social](https://img.shields.io/github/stars/matteoferla/Fragmenstein?style=social&logo=github)](https://github.com/matteoferla/Fragmenstein)\n",
    "[![ github watchers matteoferla Fragmenstein?label=Watch&style=social](https://img.shields.io/github/watchers/matteoferla/Fragmenstein?label=Watch&style=social&logo=github)](https://github.com/matteoferla/Fragmenstein)\n",
    "* [SmallWorld server](https://sw.docking.org/search.html) by Prof John Irwin\n",
    "* SmallWorld API Python-client: \n",
    "   [![Documentation Status](https://readthedocs.org/projects/python-smallworld-api/badge/?version=latest)](https://python-smallworld-api.readthedocs.io/en/latest/?badge=latest)\n",
    "[![ github forks matteoferla Fragmenstein?label=Fork&style=social](https://img.shields.io/github/forks/matteoferla/Python_SmallWorld_API?label=Fork&style=social&logo=github)](https://github.com/matteoferla/Fragmenstein)\n",
    "[![ github stars matteoferla Fragmenstein?style=social](https://img.shields.io/github/stars/matteoferla/Python_SmallWorld_API?style=social&logo=github)](https://github.com/matteoferla/Python_SmallWorld_API)\n",
    "[![ github watchers matteoferla Python_SmallWorld_API?label=Watch&style=social](https://img.shields.io/github/watchers/matteoferla/Python_SmallWorld_API?label=Watch&style=social&logo=github)](https://github.com/matteoferla/Python_SmallWorld_API)\n",
    "* Hackish widget for JSME in Colab:\n",
    "[![ github forks matteoferla Fragmenstein?label=Fork&style=social](https://img.shields.io/github/forks/matteoferla/JSME_notebook_hack?label=Fork&style=social&logo=github)](https://github.com/matteoferla/JSME_notebook_hack)\n",
    "[![ github stars matteoferla Fragmenstein?style=social](https://img.shields.io/github/stars/matteoferla/JSME_notebook_hack?style=social&logo=github)](https://github.com/matteoferla/JSME_notebook_hack)\n",
    "[![ github watchers matteoferla JSME_notebook_hack?label=Watch&style=social](https://img.shields.io/github/watchers/matteoferla/JSME_notebook_hack?label=Watch&style=social&logo=github)](https://github.com/matteoferla/JSME_notebook_hack)\n",
    "* [Frankenstein by Mary Shelley](https://en.wikipedia.org/wiki/Frankenstein)"
   ],
   "metadata": {
    "id": "CILZq0Td_y_W"
   }
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "daU_OLXJW4Qf",
    "cellView": "form"
   },
   "outputs": [],
   "source": [
    "#@title Installation\n",
    "#@markdown Press the play button on the top right hand side of this cell\n",
    "#@markdown once you have checked the settings.\n",
    "#@markdown You will be notified that this notebook is not from Google:\n",
    "#@markdown that is normal.\n",
    "\n",
    "#@markdown Also the code snippet sidebar will appear every time a  `nglview`\n",
    "#@markdown is added, so don't close it but resize it to a mill wide\n",
    "\n",
    "#@markdown ### Error reporting\n",
    "#@markdown To help Matteo improve the code, do you wish to\n",
    "#@markdown automatically send anonymous error messages?\n",
    "#@markdown See [here for more](https://github.com/matteoferla/notebook-error-reporter)\n",
    "report_errors = False #@param {type:\"boolean\"}\n",
    "\n",
    "# One dependency has a version conflict with the default colab install\n",
    "# loading module first then\n",
    "import ipywidgets as widgets\n",
    "from IPython.display import clear_output\n",
    "widgets.Text(description='Loading widgets', value='Foo')\n",
    "clear_output\n",
    "\n",
    "!pip install -q py3Dmol fragmenstein smallworld-api notebook-error-reporter jsme_notebook\n",
    "import py3Dmol\n",
    "#from google.colab import output  # noqa it's a google thing\n",
    "#output.enable_custom_widget_manager()\n",
    "\n",
    "if report_errors:\n",
    "    from notebook_error_reporter import ErrorServer\n",
    "\n",
    "    es = ErrorServer(url='https://errors.matteoferla.com', notebook='frag_playground')\n",
    "    es.enable()\n",
    "\n",
    "#from google.colab import output\n",
    "#output.enable_custom_widget_manager()\n",
    "\n",
    "# ------- here for permeance on cell reruns!\n",
    "from fragmenstein import Walton, display_mols, Victor\n",
    "import logging\n",
    "Victor.enable_stdout(logging.ERROR)\n",
    "walton = Walton([])"
   ]
  },
  {
   "cell_type": "code",
   "source": [
    "#@title Create molecules\n",
    "#@markdown Running this cells reveals a bunch of widgets allowing \n",
    "#@markdown the creation of molecules that will be places in the next cell\n",
    "\n",
    "#@markdown NB. The iconic JSME smiley-face SMILES input does not work here, \n",
    "#@markdown but right-click on the grey menu works though\n",
    "\n",
    "#@markdown NB2. Adding ünicødè letters (inc. emoji 👾) in the names \n",
    "#@markdown seems to work at first, but will cause issues.\n",
    "\n",
    "#@markdown NB3. If no molecules are added (as happens with `Run all`), two hits are added \n",
    "#@markdown from a past panDDA experiment\n",
    "\n",
    "from jsme_notebook import JSMENotebook\n",
    "from IPython.display import display\n",
    "import ipywidgets as widgets\n",
    "from rdkit import Chem\n",
    "from rdkit.Chem import AllChem\n",
    "from fragmenstein import display_mols\n",
    "import re, os\n",
    "\n",
    "# -------------------------------------------------\n",
    "\n",
    "name_input = widgets.Text(description='Name molecule', value='')\n",
    "add_button = widgets.Button(description=\"Add molecule\", icon='plus')\n",
    "clear_button = widgets.Button(description=\"Discard molecules\", icon='trash')\n",
    "# style does not work in colabs\n",
    "output = widgets.Output(layout={'border': '1px solid black'})\n",
    "uploader = widgets.FileUpload(icon='upload')\n",
    "\n",
    "jsme = JSMENotebook()  # outputs to display as is not a widget\n",
    "grid = widgets.GridspecLayout(1, 4)\n",
    "grid[0,0] = name_input\n",
    "grid[0,1] = add_button\n",
    "grid[0,2] = uploader\n",
    "grid[0,3] = clear_button\n",
    "display(grid, output)\n",
    "\n",
    "def update_output():\n",
    "    with output:\n",
    "        output.clear_output()\n",
    "        if walton.mols == []:\n",
    "            print('No molecules to show')\n",
    "        else:\n",
    "            for mol in walton.mols:\n",
    "                print(f'{mol.GetProp(\"_Name\")} {Chem.MolToSmiles(mol)}')\n",
    "        display_mols(walton.mols, useSVG=False)\n",
    "\n",
    "update_output()\n",
    "\n",
    "random_names = ['scribbed', 'custom',\n",
    "                'foo', 'bar', 'baz', 'quack', 'querky',\n",
    "                'something-ol',\n",
    "                'something acid',\n",
    "                'nonexistantene',\n",
    "                'nonexistant-diene',\n",
    "                'made-up', \n",
    "                'confabulated', \n",
    "                'rubbish',\n",
    "                'trash']\n",
    "\n",
    "def on_click_add(remove:bool):\n",
    "    \"\"\"\n",
    "    Add molecule\n",
    "    \"\"\"\n",
    "    mol = Chem.MolFromSmiles(jsme.smiles)\n",
    "    assert mol is not None, f'molecule {jsme.smiles} failed to be parsed'\n",
    "    AllChem.EmbedMolecule(mol)\n",
    "    if name_input.value:\n",
    "        mol.SetProp('_Name', name_input.value)\n",
    "    else:\n",
    "        mol.SetProp('_Name', random_names.pop(0))\n",
    "    walton.mols.append(mol)\n",
    "    update_output()\n",
    "\n",
    "dejaloaded = set()\n",
    "def observe_upload(*args, **kwargs):\n",
    "    for new_name in set(uploader.value.keys()) - dejaloaded:  #: str\n",
    "        forename, ext = os.path.splitext(new_name)\n",
    "        if ext not in '.mol':\n",
    "            raise ValueError('Not a mol file')\n",
    "        dejaloaded.add(new_name)\n",
    "        mol = Chem.MolFromMolBlock(uploader.value[new_name]['content'])\n",
    "        if not mol.HasProp('_Name') or not mol.GetProp('_Name'):\n",
    "            mol.SetProp('_Name', forename)\n",
    "        walton.mols.append(mol)\n",
    "    update_output()\n",
    "\n",
    "def on_click_clear(remove:bool):\n",
    "    \"\"\"Remove all mols\"\"\"\n",
    "    walton.mols[:] = []\n",
    "    update_output()\n",
    "\n",
    "uploader.observe(observe_upload)\n",
    "add_button.on_click(on_click_add)\n",
    "clear_button.on_click(on_click_clear)"
   ],
   "metadata": {
    "cellView": "form",
    "id": "8zbOMslxH8fK"
   },
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "code",
   "outputs": [],
   "source": [
    "#@title Viewing the molecules\n",
    "\n",
    "#@markdown Update. Colab has changed and NGLView at first did not maintain colours,\n",
    "#@markdown then it broke the widgets. Therefore, the viewer is now py3Dmol.\n",
    "#@markdown This means the click to get index is gone.\n",
    "#@markdown Additionally, a further issue is present in that py3Dmol does not work if the view is manipulated outside of its namespace. Therefore a bare minimum viewer is presented here.\n",
    "\n",
    "#@markdown Please run this cell, then the next, and re-run this cell. It will no longer update automatically.\n",
    "\n",
    "import py3Dmol\n",
    "from fragmenstein import Monster, divergent_colors\n",
    "from fragmenstein.demo import TestSet\n",
    "from fragmenstein.display import py3Dmol_monkey_patch\n",
    "\n",
    "if len(walton.mols) == 0:\n",
    "    print('The previous cell was not used to add `mol`, using mac-x0138 and mac-x0398')\n",
    "    x0138 = TestSet.get_mol('mac-x0138')\n",
    "    x0398 = TestSet.get_mol('mac-x0398')\n",
    "    walton.mols = [x0398, x0138]\n",
    "\n",
    "# there is the possibility that there are duplicate names... so no dict\n",
    "names = [mol.GetProp('_Name') for mol in walton.mols]\n",
    "\n",
    "# walton.color_in() will have set the mol prop _color \n",
    "# which controls the carbon_color of the component\n",
    "# but the update_ball_and_stick oddly does not seem to stick most times\n",
    "# select\n",
    "\n",
    "\n",
    "walton()\n",
    "walton.color_in(False)\n",
    "view = py3Dmol.view()\n",
    "py3Dmol_monkey_patch(view)\n",
    "for mol in walton.mols:\n",
    "  view.add_mol(carbon_color=mol.GetProp('color'), mol=mol, opacity=0.5)\n",
    "view.add_mol(carbon_color='white', mol=walton.merged, opacity=1)\n",
    "view.zoomTo()\n",
    "view.show()"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "source": [
    "#@title Rototranslate molecules around\n",
    "#@markdown The following widgets allow basic movements of the molecules.\n",
    "#@markdown The class that does these movements is Walton, who\n",
    "#@markdown part of the fragmenstein package but is not involved in any\n",
    "#@markdown of the merging/placing operatations \n",
    "#@markdown —it simply exists for illustrative purposes.\n",
    "#@markdown (Captain Walton in the novel _Frankenstein_ is the narrator)\n",
    "\n",
    "#@markdown Notes and caveats:\n",
    "\n",
    "#@markdown * The GUI is a simple demo and \n",
    "#@markdown many Walton methods are unavailable \n",
    "#@markdown (align_by_map, create_polygon and\n",
    "#@markdown all the point based methods such as get_centroid_of_atoms)\n",
    "#@markdown or are doing a bare minimium. For see Fragmenstein documenation\n",
    "#@markdown or even tests for examples.\n",
    "\n",
    "#@markdown * Tooltips _will_ show if you hover over the text of the buttons\n",
    "#@markdown and keep still for an eternity.\n",
    "\n",
    "\n",
    "\n",
    "#@markdown * The molecules are non properly minimised,\n",
    "#@markdown therefore beware of proximity bonding by the viewer. \n",
    "\n",
    "#@markdown ### Button meanings\n",
    "\n",
    "from warnings import warn\n",
    "from fragmenstein import Walton, Monster\n",
    "from ipywidgets import TwoByTwoLayout\n",
    "from IPython.display import clear_output, display, HTML\n",
    "from functools import partial\n",
    "from rdkit import Chem, Geometry\n",
    "from rdkit.Chem import PandasTools\n",
    "import pandas as pd\n",
    "from smallworld_api import SmallWorld\n",
    "from warnings import warn\n",
    "from threading import Lock\n",
    "from jsme_notebook.rdkit import JSMERDKit\n",
    "\n",
    "# ========== Namespace pollution ==============\n",
    "jsme2 = None\n",
    "monster = None\n",
    "# =============================================\n",
    "\n",
    "# this no longer applies\n",
    "#molview.add_selection_signal('clicked_mol', 'clicked_idx')\n",
    "\n",
    "\n",
    "\n",
    "mol_dropdown = widgets.Dropdown(\n",
    "    options=names,\n",
    "    tooltip='The molecule to move',\n",
    "    description='Molecule to rototranslate:',\n",
    "    disabled=False,\n",
    ")\n",
    "\n",
    "ref_dropdown = widgets.Dropdown(\n",
    "    options=names,\n",
    "    description='Ref molecule:',\n",
    "    disabled=False,\n",
    ")\n",
    "\n",
    "atom_idx0_text = widgets.IntText(\n",
    "                                  value=0,\n",
    "                                  description='1st atom:',\n",
    "                                  disabled=False\n",
    "                              )\n",
    "atom_idx1_text = widgets.IntText(\n",
    "                                  value=1,\n",
    "                                  description='2nd atom:',\n",
    "                                  disabled=False\n",
    "                              )\n",
    "atom_idx2_text = widgets.IntText(\n",
    "                                  value=1,\n",
    "                                  description='3rd atom:',\n",
    "                                  disabled=False\n",
    "                              )\n",
    "x_text = widgets.IntText(\n",
    "                                  value=0,\n",
    "                                  description='x [Å]',\n",
    "                                  disabled=False\n",
    "                              )\n",
    "\n",
    "y_text = widgets.IntText(\n",
    "                                  value=0,\n",
    "                                  description='y [Å]',\n",
    "                                  disabled=False\n",
    "                              )\n",
    "\n",
    "z_text = widgets.IntText(\n",
    "                                  value=0,\n",
    "                                  description='z [Å]',\n",
    "                                  disabled=False\n",
    "                              )\n",
    "\n",
    "theta_text = widgets.IntText(\n",
    "                                  value=0,\n",
    "                                  description='Angle [°]',\n",
    "                                  disabled=False\n",
    "                              )\n",
    "\n",
    "distance_text = widgets.IntText(\n",
    "                                  value=0,\n",
    "                                  description='Distance [Å]',\n",
    "                                  disabled=False\n",
    "                              )\n",
    "axis0_dropdown = widgets.Dropdown(\n",
    "    options=['x', 'y', 'z'],\n",
    "    value='x',\n",
    "    description='1st axis:',\n",
    "    disabled=False,\n",
    ")\n",
    "\n",
    "axis1_dropdown = widgets.Dropdown(\n",
    "    options=['x', 'y', 'z'],\n",
    "    value='y',\n",
    "    description='2nd axis:',\n",
    "    disabled=False,\n",
    ")\n",
    "\n",
    "grid = widgets.GridspecLayout(8, 4)  # row, col\n",
    "row = 0\n",
    "grid[row, 0] = mol_dropdown\n",
    "grid[row, 1] = ref_dropdown\n",
    "\n",
    "row += 1\n",
    "grid[row, 0] = atom_idx0_text\n",
    "grid[row, 1] = atom_idx1_text\n",
    "grid[row, 2] = atom_idx2_text\n",
    "\n",
    "row += 1\n",
    "grid[row, 0] = axis0_dropdown\n",
    "grid[row, 1] = axis1_dropdown\n",
    "grid[row, 2] = distance_text\n",
    "\n",
    "row += 1\n",
    "grid[row,0] = x_text\n",
    "grid[row,1] = y_text\n",
    "grid[row,2] = z_text\n",
    "grid[row,3] = theta_text\n",
    "\n",
    "# --------------------------------------------------\n",
    "\n",
    "def call_walton_method(method, remove:bool):\n",
    "    possible = dict(mol_idx=names.index(mol_dropdown.value),\n",
    "                  ref_mol_idx=names.index(ref_dropdown.value),\n",
    "                  x=x_text.value,\n",
    "                  y=y_text.value,\n",
    "                  z=z_text.value,\n",
    "                  atom_idx=atom_idx0_text.value,\n",
    "                  axis=axis0_dropdown.value,\n",
    "                  base_atom_idx=atom_idx0_text.value,\n",
    "                  pointer_atom_idx=atom_idx1_text.value,\n",
    "                  atom_idcs=(atom_idx0_text.value, atom_idx1_text.value, atom_idx2_text.value),\n",
    "                  plane=axis0_dropdown.value+axis1_dropdown.value,\n",
    "                  theta=theta_text.value,\n",
    "                  distance=distance_text.value,\n",
    "                  point=Geometry.Point3D(x_text.value,\n",
    "                                         y_text.value,\n",
    "                                         z_text.value),\n",
    "                  minimize=True,\n",
    "                  degrees=True,\n",
    "                  scale=1)\n",
    "    for k in method.__annotations__:\n",
    "        if k not in possible:\n",
    "            assert f'Whats {k} of {method}?'\n",
    "    method(**{k: possible[k] for k in method.__annotations__ if k != 'return'})\n",
    "    #walton.refresh_nglview(molview)\n",
    "    #molview.center()\n",
    "\n",
    "def make_button(method, description:str, icon:str='check') -> widgets.Button:\n",
    "    button = widgets.Button(description=description, icon=icon)\n",
    "    button.on_click(partial(call_walton_method, method))\n",
    "    button.tooltip=method.__doc__ if method.__doc__ else 'no docstring'\n",
    "    return button\n",
    "\n",
    "row += 1\n",
    "#@markdown The `duplicate` method copies the main molecule\n",
    "grid[row, 0] = make_button(walton.duplicate, 'Duplicate', 'copy')\n",
    "#@markdown The `translate` method translates (moves w/o rotation) the main molecule by (x, y, z)\n",
    "grid[row, 1] = make_button(walton.translate, 'Translate molecule', 'arrow-up')\n",
    "#@markdown The `rotate` method rotates the main molecule by theta degrees on the 1st axis\n",
    "grid[row, 2] = make_button(walton.rotate, 'Rotate molecule', 'rotate-right')\n",
    "#@markdown The `translate_parallel` method moves the main molecule \n",
    "#@markdown on the axis specified by distance &Aring;\n",
    "#@markdown in the vector parallel to the first atom of the ref mol\n",
    "#@markdown (base) towards the second atom of the ref mol\n",
    "#@markdown (ref and main mols can be the same)\n",
    "grid[row, 3] = make_button(walton.translate_parallel, 'Translate molecule parallel', 'arrow-right')\n",
    "\n",
    "row += 1\n",
    "#@markdown Given 3 atoms it will lay the first and third\n",
    "#@markdown on the primary axis and the second on the plane subtended\n",
    "#@markdown by the two axes\n",
    "grid[row, 0] = make_button(walton.flatten_trio, 'Lay 3 atoms on plane', 'align-center')\n",
    "grid[row, 1] = make_button(walton.atom_on_axis, 'Atom on axis', 'align-center')\n",
    "grid[row, 2] = make_button(walton.atom_on_plane, 'Atom on plane', 'align-center')\n",
    "grid[row, 3] = make_button(walton.atom_to_origin, 'Atom to origin', 'align-center')\n",
    "\n",
    "row += 1\n",
    "#@markdown Superpose ('align') the other mols onto the selected one\n",
    "grid[row, 0] = make_button(walton.superpose_by_mcs, 'Superpose onto', 'objects-align-center-horizontal')\n",
    "#@markdown The `translate_by_point` method translates to the point (x, y, z)\n",
    "grid[row, 1] = make_button(walton.translate_by_point, 'Translate to point', 'arrow-down')\n",
    "#@markdown The 'Make merger' button calls the `Monster.combine` method\n",
    "#@markdown when complete a JSME editor will appear\n",
    "#@markdown Pressing the 'Place edited' will place it.\n",
    "#@markdown Search for similar will query the Smallworld server for similars\n",
    "#@markdown in Enamine REAL and place the one stated by the slider.\n",
    "\n",
    "mol_listing = widgets.Output(description='SMILES of merger', layout={'border': '1px solid black'})\n",
    "with mol_listing:\n",
    "  clear_output()\n",
    "  display_mols(walton.mols, useSVG=False)\n",
    "\n",
    "output2 = widgets.Output(description='SMILES of merger', layout={'border': '1px solid black'})\n",
    "jsme_output = widgets.Output(description='JSME')\n",
    "\n",
    "        \n",
    "row += 1\n",
    "target_text = name_input = widgets.Text(description='Place SMILES', value='')\n",
    "merge_button = widgets.Button(description='Make merger', \n",
    "                              button_style='success',\n",
    "                              tooltip=walton.__call__.__doc__,\n",
    "                              icon='calculator')\n",
    "\n",
    "place_edited_button = widgets.Button(description='Place edited', \n",
    "                                     button_style='warning',\n",
    "                                     tooltip='Place edited',\n",
    "                                     icon='pencil')\n",
    "search_button = widgets.Button(description='Search for similars', \n",
    "                                     button_style='warning',\n",
    "                                     tooltip='Search via Smallworld server',\n",
    "                                       icon='magnifying-glass')\n",
    "\n",
    "search_show_slider = widgets.IntSlider(min=0,\n",
    "                                       max=9,\n",
    "                                      step=1,\n",
    "                                        description='Show similar')\n",
    "\n",
    "grid[row, 0] = merge_button\n",
    "grid[row, 1] = place_edited_button\n",
    "grid[row, 2] = search_button\n",
    "grid[row, 3] = search_show_slider\n",
    "\n",
    "\n",
    "def update_jsme(mol: Chem.Mol):\n",
    "    with jsme_output:\n",
    "        clear_output()\n",
    "        global jsme2\n",
    "        jsme2 = JSMERDKit(mol)  # type dispatched\n",
    "\n",
    "def make_merger(remove=False):\n",
    "    merger:Chem.Mol = walton()\n",
    "    with output2:\n",
    "        clear_output()\n",
    "        print(Chem.MolToSmiles(merger))\n",
    "    merge_button.button_style = 'success'\n",
    "    place_edited_button.button_style = 'primary'\n",
    "    search_button.button_style = ''\n",
    "    update_jsme(merger)\n",
    "    with mol_listing:\n",
    "      clear_output()\n",
    "      display_mols([*walton.mols, merger], useSVG=False)\n",
    "\n",
    "merge_button.on_click(make_merger)\n",
    "\n",
    "def place_edited(remove: bool=False):\n",
    "    if not jsme2:\n",
    "        return\n",
    "    global monster\n",
    "    monster = Monster([walton.merged])\n",
    "    monster.place(jsme2.mol, \n",
    "             merging_mode='expansion')\n",
    "    #walton.refresh_nglview(molview)\n",
    "    #molview.add_mol(monster.positioned_mol, carbon_color='#DC143C')  # crimson\n",
    "    #molview.center()\n",
    "    with mol_listing:\n",
    "      clear_output()\n",
    "      display_mols([*walton.mols, walton.merged, monster.positioned_mol], useSVG=False)\n",
    "    merge_button.button_style = 'info'\n",
    "    place_edited_button.button_style = 'info'\n",
    "    search_button.button_style = 'success'\n",
    "        \n",
    "place_edited_button.on_click(place_edited)\n",
    "\n",
    "sws = SmallWorld()\n",
    "# this call requires an internet connection\n",
    "chemical_databases:pd.DataFrame = sws.retrieve_databases()\n",
    "similars = pd.DataFrame()\n",
    "previous_search = ''\n",
    "def search_for_similar(remove=True):\n",
    "    \"\"\"\n",
    "    I tried adding a Lock, but it got messy.\"\"\"\n",
    "    global similars\n",
    "    global previous_search\n",
    "    with output2:\n",
    "        if not walton.merged:\n",
    "            print('Generate merger first')\n",
    "            return\n",
    "        if jsme2.smiles:\n",
    "            wanted_smiles = jsme2.smiles\n",
    "        else:\n",
    "            wanted_smiles = Chem.MolToSmiles(walton.merged)\n",
    "        if wanted_smiles == previous_search:\n",
    "            print('Already done')\n",
    "            sliced = similars[['name', 'smiles', 'molecule', 'dist', 'mw']]\n",
    "            PandasTools.RenderImagesInAllDataFrames(images=True)\n",
    "            with jsme_output:\n",
    "                clear_output()\n",
    "                display(HTML(sliced.to_html()))\n",
    "            return\n",
    "        print('Running: be patient. Do not elevator call mash!')\n",
    "        previous_search = wanted_smiles\n",
    "        \n",
    "    similars = sws.search(wanted_smiles,\n",
    "                               dist=25,\n",
    "                               length=10,\n",
    "                               db=sws.REAL_dataset,\n",
    "                               tolerated_exceptions=())\n",
    "    if similars is None:\n",
    "        raise ValueError('An error arose in the search — try later?')\n",
    "    PandasTools.AddMoleculeColumnToFrame(similars,'smiles','molecule')\n",
    "    sliced = similars[['name', 'smiles', 'molecule', 'dist', 'mw']]\n",
    "    PandasTools.RenderImagesInAllDataFrames(images=True)\n",
    "    with jsme_output:\n",
    "        clear_output()\n",
    "        display(HTML(sliced.to_html()))\n",
    "    target_text.value = sliced.smiles[0]\n",
    "    monster.place(sliced.molecule[0])\n",
    "    # walton.refresh_nglview(molview)\n",
    "    # molview.add_mol(monster.positioned_mol, carbon_color='#DC143C')  # crimson\n",
    "    # molview.center()\n",
    "    search_button.button_style = 'info'\n",
    "    \n",
    "def search_change(*agrs, **kwargs):\n",
    "    if similars is None or not len(similars):\n",
    "        return\n",
    "    monster.place(similars.molecule[search_show_slider.value])  # providing a custom_map here?\n",
    "    # walton.refresh(molview)\n",
    "    # molview.add_mol(monster.positioned_mol, carbon_color='#DC143C')  # crimson\n",
    "    # molview.center()\n",
    "    with mol_listing:\n",
    "      clear_output()\n",
    "      display_mols([*walton.mols, walton.merged, monster.positioned_mol], useSVG=False)\n",
    "\n",
    "search_button.on_click(search_for_similar)\n",
    "search_show_slider.observe(search_change)\n",
    "\n",
    "\n",
    "display(mol_listing,\n",
    "        #molview, \n",
    "        # HTML('<div>Last clicked: atom index <span id=\"clicked_idx\">None</span>'+\n",
    "        #      ' of <span id=\"clicked_mol\">None</span><div>'),\n",
    "        grid,\n",
    "        jsme_output,\n",
    "        output2)"
   ],
   "metadata": {
    "cellView": "form",
    "id": "2sfp5sBkShQz"
   },
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "code",
   "source": [
    "#@title Advanced mapping\n",
    "\n",
    "#@markdown The code above may make a bad choice and one may want to give a custom\n",
    "#@markdown map override.\n",
    "\n",
    "#@markdown The mapping in the placement can be overridden, by providing a dictionary of \n",
    "#@markdown strings (template molecule names) to a dictionary of template atom indices\n",
    "#@markdown to follow-up indices as described in\n",
    "#@markdown https://fragmenstein.readthedocs.io/en/latest/doc_custom_mapping.html\n",
    "#@markdown which details the special cases (e.g. forbidding a pair etc.)\n",
    "\n",
    "from fragmenstein.branding import divergent_colors\n",
    "import json\n",
    "\n",
    "\n",
    "\n",
    "if monster is not None and monster.positioned_mol:\n",
    "    mol = monster.positioned_mol\n",
    "else:\n",
    "    mol = walton.merged\n",
    "\n",
    "\n",
    "jsme3 = JSMERDKit(mol)\n",
    "mol_listing2 = widgets.Output(description='SMILES of merger', layout={'border': '1px solid black'})\n",
    "\n",
    "def update_view(*mols):\n",
    "  color_series = iter(divergent_colors[len(mols)])\n",
    "  with mol_listing2:\n",
    "      clear_output()\n",
    "      display_mols(mols, useSVG=False, molsPerRow=3, subImgSize=(200,200))\n",
    "      # #v = MolNGLWidget()\n",
    "      # for mol in mols:\n",
    "      #     carbon_color = next(color_series)\n",
    "      #     v.add_mol(carbon_color=carbon_color, mol=mol)\n",
    "      # display(v)\n",
    "      # v.handle_resize()\n",
    "display(mol_listing2)\n",
    "edited = jsme3.mol\n",
    "edited.SetProp('_Name', 'edited')\n",
    "u#pdate_view(*walton.mols, edited)\n",
    "\n",
    "\n",
    "custom_map = {mol.GetProp('_Name'): {} for mol in walton.mols}\n",
    "\n",
    "custom_text = widgets.Text(description='custom_map', \n",
    "                           value=json.dumps(custom_map))\n",
    "display(custom_text)\n",
    "\n",
    "custom_button = widgets.Button(description='Place', icon='calculator', button_style='primary')\n",
    "display(custom_button)\n",
    "\n",
    "\n",
    "def on_click_custom_button(remove):\n",
    "  edited = jsme3.mol\n",
    "  edited.SetProp('_Name', 'edited')\n",
    "  # circuitous way to read the string:\n",
    "  # JSON does not accept nums as keys.\n",
    "  # but user may have written an invalid string\n",
    "  escaped = re.sub(r'(?<![\\w_.\\-\"\\'])(\\d+)(?![\\w_.\\-\"\\'])', r'\"\\1\"', custom_text.value)\n",
    "  custom_map = json.loads(escaped)\n",
    "  custom_map = {name: {int(k): int(v) \n",
    "                            for k, v in custom_map[name].items()} \n",
    "                              for name in custom_map}\n",
    "  print(custom_map)\n",
    "  monster = Monster(walton.mols)\n",
    "  monster.place(edited, custom_map=custom_map)\n",
    "  #update_view(*walton.mols, monster.positioned_mol)\n",
    "\n",
    "custom_button.on_click(on_click_custom_button)\n"
   ],
   "metadata": {
    "cellView": "form",
    "id": "GNRKY6Q3y5je"
   },
   "execution_count": null,
   "outputs": []
  }
 ]
}