diff --git a/docs/notebooks/docking.ipynb b/docs/notebooks/docking.ipynb new file mode 100644 index 0000000..13c6441 --- /dev/null +++ b/docs/notebooks/docking.ipynb @@ -0,0 +1,818 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Docking\n", + "\n", + "This tutorials showcases how to use ProLIF to generate an interaction fingerprint for interactions between a protein and different docking poses.\n", + "\n", + "ProLIF currently provides file readers for docking poses for MOL2, SDF and PDBQT files which rely on RDKit or MDAnalysis (or both).\n", + "\n", + ":::{important}\n", + "For convenience, the different files for this tutorial are included with the ProLIF installation, and we'll access these files through `plf.datafiles.datapath` which is a {class}`pathlib.Path` object pointing to the tutorials data directory. This makes it easier to manipulate paths to files, match filenames using wildcards...etc. in a Pythonic way, but you can also use plain strings, e.g. `\"/home/user/prolif/data/vina/rec.pdb\"` instead of `plf.datafiles.datapath / \"vina\" / \"rec.pdb\"` if you prefer.\n", + "\n", + "**Remember to replace any reference to `plf.datafiles.datapath` with the actual paths to your inputs outside of this tutorial.**\n", + ":::\n", + "\n", + ":::{tip}\n", + "At the top of the page you can find links to either download this notebook or run it in\n", + "Google Colab. You can install the dependencies for the tutorials with the command:\n", + "\n", + "```shell\n", + "pip install prolif[tutorials]\n", + "```\n", + ":::" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Protein preparation\n", + "\n", + "Let's start by importing MDAnalysis and ProLIF to read our protein for this tutorial.\n", + "\n", + "In this tutorial we'll be using a PDB file but you can use any format **as long as it contains explicit hydrogens**.\n", + "\n", + "### PDB file\n", + "\n", + "We have 2 options to read the protein file: MDAnalysis (preferred) or RDKit (faster but risky, see below).\n", + "\n", + ":::{important}\n", + "While RDKit is going to be much faster at parsing the file, it can only recognize standard residue names, so some protonated residues may be incorrectly parsed.\n", + "\n", + "For example, the protonated histidine residue HSE347 in our protein is not correcltly parsed which removes aromaticity on the ring, meaning that ProLIF will not be able to match this residue for pi-stacking interactions.\n", + ":::" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import prolif as plf\n", + "from rdkit import Chem\n", + "\n", + "protein_file = str(plf.datafiles.datapath / \"vina\" / \"rec.pdb\")\n", + "\n", + "rdkit_prot = Chem.MolFromPDBFile(protein_file, removeHs=False)\n", + "protein_mol = plf.Molecule(rdkit_prot)\n", + "# histidine HSE347 not recognized by RDKit\n", + "plf.display_residues(protein_mol, slice(260, 263))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Unless you know for sure that all the residue names in your PDB file are standard, it is thus recommanded that you use MDAnalysis for parsing the protein." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import MDAnalysis as mda\n", + "import prolif as plf\n", + "\n", + "u = mda.Universe(protein_file)\n", + "protein_mol = plf.Molecule.from_mda(u)\n", + "plf.display_residues(protein_mol, slice(260, 263))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + ":::{admonition} Troubleshooting\n", + "\n", + "In some cases, some atomic clashes may be incorrectly classified as bonds and will prevent the conversion of the MDAnalysis molecule to RDKit through ProLIF. Since MDAnalysis uses van der Waals radii for bond detection, one can modify the default radii that are used:\n", + "\n", + "```python\n", + "u.atoms.guess_bonds(vdwradii={\"H\": 1.05, \"O\": 1.48})\n", + "```\n", + ":::" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### MOL2 file\n", + "\n", + "You could also use a MOL2 file for the protein, here's a code snippet to guide you:\n", + "\n", + "```python\n", + "u = mda.Universe(\"protein.mol2\")\n", + "# add \"elements\" category\n", + "elements = mda.topology.guessers.guess_types(u.atoms.names)\n", + "u.add_TopologyAttr(\"elements\", elements)\n", + "# create protein mol\n", + "protein_mol = plf.Molecule.from_mda(u)\n", + "```\n", + "\n", + ":::{admonition} Troubleshooting\n", + "\n", + "While doing so, you may run into one of these errors:\n", + "- **`RDKit ERROR: Can't kekulize mol. Unkekulized atoms`**\n", + "- **`RDKit ERROR: non-ring atom marked aromatic`**\n", + "\n", + "This usually happens when some of the bonds in the MOL2 file are unconventional. For example in MOE, charged histidines are represented part with aromatic bonds and part with single and double bonds, presumably to capture the different charged resonance structures in a single one. A practical workaround for this is to redefine problematic bonds as single bonds in the `Universe` object:\n", + "\n", + "```python\n", + "u = mda.Universe(\"protein.mol2\")\n", + "# replace aromatic bonds with single bonds\n", + "for i, bond_order in enumerate(u._topology.bonds.order):\n", + " # you may need to replace double bonds (\"2\") as well\n", + " if bond_order == \"ar\":\n", + " u._topology.bonds.order[i] = 1\n", + "# clear the bond cache, just in case\n", + "u._topology.bonds._cache.pop(\"bd\", None)\n", + "# infer bond orders again\n", + "protein_mol = plf.Molecule.from_mda(u)\n", + "```\n", + ":::\n", + "\n", + "The parsing of residue info in MOL2 files can sometimes fail and lead to the residue index being appended to the residue name and number. You can fix this with the following snippet:\n", + "\n", + "```python\n", + "import numpy as np\n", + "\n", + "u = mda.Universe(\"protein.mol2\")\n", + "resids = [\n", + " plf.ResidueId.from_string(x) for x in u.residues.resnames\n", + "]\n", + "u.residues.resnames = np.array([x.name for x in resids], dtype=object)\n", + "u.residues.resids = np.array([x.number for x in resids], dtype=np.uint32)\n", + "u.residues.resnums = u.residues.resids\n", + "protein_mol = plf.Molecule.from_mda(u)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Docking poses preparation\n", + "\n", + "Loading docking poses is done through one of the `supplier` functions available in ProLIF. These will read your input file with either RDKit or MDAnalysis and handle any additional preparation.\n", + "\n", + "### SDF format\n", + "\n", + "The SDF format is the easiest way for ProLIF to parse docking poses:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# load ligands\n", + "poses_path = str(plf.datafiles.datapath / \"vina\" / \"vina_output.sdf\")\n", + "pose_iterable = plf.sdf_supplier(poses_path)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### MOL2 format\n", + "\n", + "MOL2 is another format that should be easy for ProLIF to parse:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# load ligands\n", + "poses_path = str(plf.datafiles.datapath / \"vina\" / \"vina_output.mol2\")\n", + "pose_iterable = plf.mol2_supplier(poses_path)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### PDBQT format\n", + "\n", + "The typical use case here is getting the IFP from AutoDock Vina's output. It requires a few additional steps and informations compared to other formats like MOL2, since the PDBQT format gets rid of most hydrogen atoms and doesn't contain bond order information, which are both needed for ProLIF to work.\n", + "\n", + ":::{important}\n", + "Consider using [Meeko](https://github.com/forlilab/Meeko) to prepare your inputs for Vina as it contains some utilities to [convert the output poses](https://github.com/forlilab/Meeko#examples-using-the-command-line-scripts) to the SDF format which is a lot safer than the solution proposed here.\n", + ":::\n", + "\n", + ":::{tip}\n", + "Do not use OpenBabel to convert your PDBQT poses to SDF, it will not be able to guess the bond orders and charges correctly.\n", + ":::\n", + "\n", + "The prerequisites for a successfull usage of ProLIF in this case is having external files that contain bond orders and formal charges for your ligand (like SMILES, SDF or MOL2).\n", + "\n", + ":::{note}\n", + "Please note that your PDBQT input must have a single model per file (this is required by `MDAnalysis`). Splitting a multi-model file can be done using the `vina_split` command-line tool that comes with AutoDock Vina: `vina_split --input vina_output.pdbqt`\n", + ":::\n", + "\n", + "Let's start by loading our \"template\" file with bond orders. It can be a SMILES string, MOL2, SDF file or anything supported by RDKit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from rdkit import Chem\n", + "\n", + "template = Chem.MolFromSmiles(\n", + " \"C[NH+]1CC(C(=O)NC2(C)OC3(O)C4CCCN4C(=O)C(Cc4ccccc4)N3C2=O)C=C2c3cccc4[nH]cc(c34)CC21\"\n", + ")\n", + "template" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we'll use the PDBQT supplier which loads each file from a list of paths, and assigns bond orders and charges using the template." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# load list of ligands\n", + "pdbqt_files = sorted(plf.datafiles.datapath.glob(\"vina/*.pdbqt\"))\n", + "pdbqt_files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pose_iterable = plf.pdbqt_supplier(pdbqt_files, template)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fingerprint generation\n", + "\n", + "We can now generate a fingerprint. By default, ProLIF will calculate the following interactions: Hydrophobic, HBDonor, HBAcceptor, PiStacking, Anionic, Cationic, CationPi, PiCation, VdWContact.\n", + "You can list all interactions that are available with the following command:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plf.Fingerprint.list_available()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + ":::{tip}\n", + "The default fingerprint will only keep track of the first group of atoms that satisfied the constraints per interaction type and residue pair.\n", + "\n", + "If you want to keep track of all possible interactions to generate a count-fingerprint (e.g. when there are two atoms in the ligand that make an HBond-donor interaction with residue X), use `plf.Fingerprint(count=True)`.\n", + "This is also quite useful for visualization purposes as you can then display the atom pair that has the shortest distance which will look more accurate.\n", + "This fingerprint type is however a bit slower to compute.\n", + ":::" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# use default interactions\n", + "fp = plf.Fingerprint()\n", + "# run on your poses\n", + "fp.run_from_iterable(pose_iterable, protein_mol)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + ":::{tip}\n", + "The `run` method will automatically select residues that are close to the ligand (6.0 Å) when computing the fingerprint. You can modify the 6.0 Å cutoff by specifying `plf.Fingerprint(vicinity_cutoff=7.0)`, but this is only useful if you decide to change the distance parameters for an interaction class (see in the advanced section of the tutorials).\n", + "\n", + "Alternatively, you can pass a list of residues like so:\n", + "\n", + "```python\n", + "fp.run(, residues=[\"TYR38.A\", \"ASP129.A\"])\n", + "```\n", + ":::" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can save the fingerprint object with `fp.to_pickle` and reload it later with `Fingerprint.from_pickle`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fp.to_pickle(\"fingerprint.pkl\")\n", + "fp = plf.Fingerprint.from_pickle(\"fingerprint.pkl\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Analysis\n", + "\n", + "Once the execution is done, you can access the results through `fp.ifp` which is a nested dictionary:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pose_index = 0\n", + "ligand_residue = \"UNL1\"\n", + "protein_residue = \"ASP129.A\"\n", + "\n", + "fp.ifp[pose_index][(ligand_residue, protein_residue)]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "While this contains all the details about the different interactions that were detected, it's not the easiest thing to digest.\n", + "\n", + "The best way to analyse our results is to export the interaction fingerprint to a Pandas DataFrame. You can read more about pandas in their\n", + "[user_guide](https://pandas.pydata.org/docs/user_guide/index.html)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = fp.to_dataframe(index_col=\"Pose\")\n", + "# show only the 5 first poses\n", + "df.head(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can change the type used for the bits if need be:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "df_uint8 = fp.to_dataframe(dtype=np.uint8)\n", + "df_uint8.head(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here are some common pandas snippets to extract useful information from the fingerprint table.\n", + "\n", + ":::{important}\n", + "Make sure to remove the `.head(5)` at the end of the commands to display the results for all the frames.\n", + ":::" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# hide an interaction type (Hydrophobic)\n", + "df.drop(\"Hydrophobic\", level=\"interaction\", axis=1).head(5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# show only one protein residue (ASP129.A)\n", + "df.xs(\"ASP129.A\", level=\"protein\", axis=1).head(5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# show only an interaction type (PiStacking)\n", + "df.xs(\"PiStacking\", level=\"interaction\", axis=1).head(5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# percentage of poses where each interaction is present\n", + "(df.mean().sort_values(ascending=False).to_frame(name=\"%\").T * 100)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# same but we regroup all interaction types\n", + "(\n", + " df.T.groupby(level=[\"ligand\", \"protein\"])\n", + " .sum()\n", + " .T.astype(bool)\n", + " .mean()\n", + " .sort_values(ascending=False)\n", + " .to_frame(name=\"%\")\n", + " .T\n", + " * 100\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# percentage of poses where PiStacking interactions are present, by residue\n", + "(\n", + " df.xs(\"PiStacking\", level=\"interaction\", axis=1)\n", + " .mean()\n", + " .sort_values(ascending=False)\n", + " .to_frame(name=\"%\")\n", + " .T\n", + " * 100\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# percentage of poses where interactions with PHE330 occur, by interaction type\n", + "(\n", + " df.xs(\"PHE330.B\", level=\"protein\", axis=1)\n", + " .mean()\n", + " .sort_values(ascending=False)\n", + " .to_frame(name=\"%\")\n", + " .T\n", + " * 100\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# percentage of poses where each interaction type is present\n", + "(\n", + " df.T.groupby(level=\"interaction\")\n", + " .sum()\n", + " .T.astype(bool)\n", + " .mean()\n", + " .sort_values(ascending=False)\n", + " .to_frame(name=\"%\")\n", + " .T\n", + " * 100\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 10 residues most frequently interacting with the ligand\n", + "(\n", + " df.T.groupby(level=[\"ligand\", \"protein\"])\n", + " .sum()\n", + " .T.astype(bool)\n", + " .mean()\n", + " .sort_values(ascending=False)\n", + " .head(10)\n", + " .to_frame(\"%\")\n", + " .T\n", + " * 100\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can compute a Tanimoto similarity between poses:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Tanimoto similarity between the first frame and the rest\n", + "from rdkit import DataStructs\n", + "\n", + "bitvectors = fp.to_bitvectors()\n", + "tanimoto_sims = DataStructs.BulkTanimotoSimilarity(bitvectors[0], bitvectors)\n", + "tanimoto_sims" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Comparing docking poses with a reference ligand\n", + "\n", + "If you have a reference ligand that you wish to use for comparison with your docking poses, you can do so the following way:\n", + "\n", + ":::{important}\n", + "Just like for the protein, you reference ligand must contain explicit hydrogens. If the reference comes from a PDB file, you could use PyMOL to add hydrogens to it and export the prepared reference.\n", + ":::" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# load the reference\n", + "ref = mda.Universe(plf.datafiles.datapath / \"vina\" / \"lig.pdb\")\n", + "ref_mol = plf.Molecule.from_mda(ref)\n", + "\n", + "# generate IFP for the reference\n", + "fp_ref = plf.Fingerprint(fp.interactions)\n", + "fp_ref.run_from_iterable([ref_mol], protein_mol)\n", + "df_ref = fp_ref.to_dataframe(index_col=\"Pose\")\n", + "\n", + "# set the \"pose index\" to -1\n", + "df_ref.rename(index={0: -1}, inplace=True)\n", + "# set the ligand name to be the same as poses\n", + "df_ref.rename(columns={str(ref_mol[0].resid): df.columns.levels[0][0]}, inplace=True)\n", + "df_ref" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# concatenate both dataframes\n", + "import pandas as pd\n", + "\n", + "df_ref_poses = (\n", + " pd.concat([df_ref, df])\n", + " .fillna(False)\n", + " .sort_index(\n", + " axis=1,\n", + " level=0,\n", + " key=lambda index: [plf.ResidueId.from_string(x) for x in index],\n", + " )\n", + ")\n", + "df_ref_poses" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now calculate the Tanimoto similarity between our reference ligand and the docking poses:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bitvectors = plf.to_bitvectors(df_ref_poses)\n", + "tanimoto_sims = DataStructs.BulkTanimotoSimilarity(bitvectors[0], bitvectors[1:])\n", + "tanimoto_sims" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualisation\n", + "\n", + "There are a few different options builtin when it comes to visualisation.\n", + "\n", + "You can start by plotting the interactions over time:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# %matplotlib ipympl\n", + "\n", + "fp.plot_barcode(xlabel=\"Pose\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + ":::{tip}\n", + "If you uncomment `%matplotlib ipympl` at the top of the above cell, you should be able to see an interactive version of the plot.\n", + ":::\n", + "\n", + "You can also display the interactions in a 2D interactive diagram:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fp.plot_lignetwork(pose_iterable[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This diagram is interactive, you can:\n", + "- zoom and pan,\n", + "- move residues around,\n", + "- click on the legend to display or hide types of residues or interactions,\n", + "- hover an interaction line to display the distance.\n", + "\n", + ":::{note}\n", + "It is not possible to export it as an image, but you can always take a screenshot.\n", + ":::\n", + "\n", + "You can generate 2 types of diagram with this function, controlled by the `kind` argument:\n", + "- `frame`: shows a single specific docking pose (specified with `frame`, corresponds to the `Pose` index in the dataframe).\n", + "- `aggregate` (default): the interactions from all poses are grouped and displayed. An optional `threshold` parameter controls the minimum frequency required for an interaction to be displayed (default `0.3`, meaning that interactions occuring in less than 30% of poses will be hidden). The width of interactions is linked to the frequency." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Showing a specific pose:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fp.plot_lignetwork(pose_iterable[0], kind=\"frame\", frame=0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Up to now we've only been using the default fingerprint generator, but you can optionally enable the `count` parameter to enumerate all occurences of an interaction (the default fingerprint generator will stop at the first occurence), and then display all of them by specifying `display_all=True`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fp_count = plf.Fingerprint(count=True)\n", + "fp_count.run_from_iterable(pose_iterable, protein_mol)\n", + "fp_count.plot_lignetwork(pose_iterable[0], kind=\"frame\", frame=0, display_all=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also visualize this information in 3D:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "view = fp_count.plot_3d(pose_iterable[0], protein_mol, frame=0, display_all=False)\n", + "view" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As in the lignetwork plot, you can hover atoms and interactions to display more information.\n", + "\n", + "The advantage of using a count fingerprint in that case is that it will automatically select the interaction occurence with the shortest distance for a more intuitive visualization.\n", + "\n", + "Once you're satisfied with the orientation, you can export the view as a PNG image with the following snippet:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import Javascript\n", + "\n", + "Javascript(\n", + " \"\"\"\n", + " var png = viewer_%s.pngURI()\n", + " var a = document.createElement('a')\n", + " a.href = png\n", + " a.download = \"prolif-3d.png\"\n", + " a.click()\n", + " a.remove()\n", + "\"\"\"\n", + " % view.uniqueid\n", + ")" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "8787e9fc73b27535744a25d17e74686c0add9df598b8e27ca04412fce7f0c7ae" + }, + "kernelspec": { + "display_name": "Python 3.8.5 ('prolif')", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/source/tutorials.rst b/docs/source/tutorials.rst index 38a8e2e..f53ef51 100644 --- a/docs/source/tutorials.rst +++ b/docs/source/tutorials.rst @@ -45,7 +45,7 @@ that are being analyzed: Docking ------- -TODO +:ref:`notebooks/docking:Docking` PDB file --------