Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
run: |
python3 setup.py sdist bdist_wheel
- name: Upload artifacts for inspection
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/*
Expand Down
7 changes: 7 additions & 0 deletions docs/source/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
Changes in Ringtail
######################

Changes in 2.1.2: bug fixes
****************************
* Removing of union operand that made Ringtail incompatible with python=3.9
* Pymol now displays receptor if present in database
* Proper handling in preparing rdkit Mols in absence of flexible residues
* Enhanced error messages and docs related to plotting and pymol

Changes in 2.1.1: bug fixes and result plot enhancements
********************************************************
Enhancements
Expand Down
4 changes: 4 additions & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ Ringtail offers a wealth of database creation and filtering options. The differe
To get started, first follow the instructions to :ref:`install Ringtail <installation>`, then navigate to :ref:`Getting started with Ringtail <get_started>` for a quick overview of the basic usage of Ringtail from the command line.
For more advanced and customizable use, learn how to use the :ref:`Ringtail API <api>`.


Ringtail v2 comes with improved database write and filtering speeds. This includes preparing a database of 2 million ligands in less than an hour (tested on a Macbook Pro with Apple silicon chip)! Filtering the docked ligands based on the docking score, or more complex interaction filtering criteria, can be completed in a matter of seconds.


.. toctree::
:maxdepth: 2
:hidden:
Expand Down
2 changes: 1 addition & 1 deletion ringtail/cloptionparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ def cmdline_parser(defaults: dict = {}):
output_group.add_argument(
"-py",
"--pymol",
help="Lauch PyMOL session and plot of ligand efficiency vs docking score for molecules in bookmark specified with --bookmark_name. Will display molecule in PyMOL when clicked on plot. Will also open receptor if given.",
help="Lauch PyMOL session and plot of ligand efficiency vs docking score for molecules in bookmark specified with --bookmark_name. Will display molecule in PyMOL when clicked on plot. Will open receptor if one is saved in the database.",
action="store_true",
)

Expand Down
2 changes: 1 addition & 1 deletion ringtail/outputmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ def plot_single_points(

def save_scatterplot(self):
"""
Saves current figure as scatter.png
Saves and closes current figure as scatter.png

Raises:
OutputError
Expand Down
124 changes: 55 additions & 69 deletions ringtail/ringtailcore.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import matplotlib.pyplot as plt
import json
from meeko import RDKitMolCreate
from meeko.export_flexres import pdb_updated_flexres_from_rdkit
from .storagemanager import StorageManager
from .resultsmanager import ResultsManager
from .receptormanager import ReceptorManager
Expand Down Expand Up @@ -574,7 +573,7 @@ def _add_results(
interaction_tolerance: float = None,
interaction_cutoffs: list = None,
max_proc: int = None,
options_dict: dict | None = None,
options_dict: dict = None,
finalize: bool = True,
):
"""Method that is agnostic of results type, and will do the actual call to storage manager to process result files and add to database.
Expand Down Expand Up @@ -1223,7 +1222,7 @@ def filter(
ligand_substruct=None,
ligand_substruct_pos=None,
ligand_max_atoms=None,
filters_dict: dict | None = None,
filters_dict: dict = None,
# other processing options:
enumerate_interaction_combs: bool = False,
output_all_poses: bool = None,
Expand All @@ -1235,7 +1234,7 @@ def filter(
outfields: str = None,
bookmark_name: str = None,
filter_bookmark: str = None,
options_dict: dict | None = None,
options_dict: dict = None,
return_iter=False,
):
"""Prepare list of filters, then hand it off to storageman to perform filtering. Creates log of all ligand docking results that passes.
Expand Down Expand Up @@ -1476,67 +1475,9 @@ def filter(

return ligands_passed

def write_flexres_pdb(
self, receptor_polymer, ligname: str, filename: str, bookmark_name: str = None
):
"""
Writes a receptor pdb with flexible residues based on the ligand provided

Args:
receptor_polymer (Polymer): version of receptor produced by meeko
ligname (str): ligand name for which the receptor flexible residue info should be collected
filename (str): name of the output pdb, extension is optional, will default to '.pdb'
bookmark_name (str, optional): will use last used bookmark if not specified, will not work in a db without any filtering performed
"""
# make flexres rdkit mols for ligand-receptor docking
if bookmark_name is not None:
self.set_storageman_attributes(bookmark_name=bookmark_name)

with self.storageman:
self.storageman.create_temp_table_from_bookmark()
ligname, ligand_smile, atom_index_map, hydrogen_parents = (
self.storageman.fetch_single_ligand_output_info(ligname)
)
flexible_residues, flexres_atomnames = self.storageman.fetch_flexres_info()
if flexible_residues != []: # converts string to list
flexible_residues = json.loads(flexible_residues)
flexres_atomnames = json.loads(flexres_atomnames)

ligand_mol, flexres_mols, _ = self._create_rdkit_mol(
ligname,
ligand_smile,
atom_index_map,
hydrogen_parents,
flexible_residues,
flexres_atomnames,
)
if filename:
# if providing filename, make sure it has .pdb extension
root, ext = os.path.splitext(filename)
if not ext:
ext = ".pdb"
path = root + ext
else:
# name if after the receptor
receptor_name, _ = self.storageman.fetch_receptor_objects()[0]
path = receptor_name + ".pdb"

flexmoldict = {}
# string in list of strings
for index, flexres in enumerate(flexible_residues):
# res id is chain:resnum
flexmoldict[f"{flexres[4]}:{flexres[-3:]}"] = flexres_mols[index]

pdb_str = pdb_updated_flexres_from_rdkit(receptor_polymer, flexmoldict)
# write pdb string to file
with open(path, "w") as file:
file.write(pdb_str)

return ligand_mol, flexmoldict

def write_molecule_sdfs(
self,
sdf_path: str | None = None,
sdf_path: str = None,
all_in_one: bool = True,
bookmark_name: str = None,
write_nonpassing: bool = None,
Expand Down Expand Up @@ -1665,7 +1606,9 @@ def ligands_rdkit_mol(self, bookmark_name=None, write_nonpassing=False) -> dict:
passing_molecule_info = self.storageman.fetch_passing_ligand_output_info()
flexible_residues, flexres_atomnames = self.storageman.fetch_flexres_info()

if flexible_residues != []:
if flexible_residues is None:
flexible_residues, flexres_atomnames = [], []
elif flexible_residues != []:
flexible_residues = json.loads(flexible_residues)
flexres_atomnames = json.loads(flexres_atomnames)

Expand Down Expand Up @@ -1732,9 +1675,10 @@ def plot(
"""
Get data needed for creating Ligand Efficiency vs
Energy scatter plot from storageManager. Call OutputManager to create plot.
Option to save the plot and close it immediately, or keep it open and save it manually later.

Args:
save (bool): whether to save plot to cd
save (bool): whether to save plot to cd. Will save and close figure
bookmark_name (str): bookmark from which to fetch filtered data to plot
return_fig_handle (bool): use to return a handle to the matplotlib figure instead of saving or showing figure

Expand Down Expand Up @@ -1775,7 +1719,7 @@ def plot(
markersize = 20
# for smaller dataset, scale num of bins and markersize to size of dataset
else:
num_of_bins = round(datalength / 10)
num_of_bins = max(1, round(datalength / 10))
markersize = 60 - (datalength / 25)

# plot the data
Expand Down Expand Up @@ -1814,6 +1758,16 @@ def get_plot_data(self, bookmark_name: str = None):
"""
if bookmark_name is not None:
self.set_storageman_attributes(bookmark_name=bookmark_name)
else:
if self.storageman.bookmark_name in self.get_bookmark_names():
self.logger.debug(
f"No bookmark specified, using bookmark '{self.storageman.bookmark_name}'."
)
else:
self.logger.info(
"No bookmark specified or available, passing_data will return empty."
)

with self.storageman:
all_data, passing_data = self.storageman.get_plot_data()

Expand All @@ -1824,7 +1778,7 @@ def display_pymol(self, bookmark_name=None):
Launch pymol session and plot of LE vs docking score. Displays molecules when clicked.

Args:
bookmark_name (str): bookmark name to use in pymol. 'None' uses the whole db?
bookmark_name (str): bookmark name to use in pymol. Will look for the default bookmark 'passing_results' (or last used bookmark) if None is provided.
"""

import subprocess
Expand All @@ -1838,10 +1792,19 @@ def display_pymol(self, bookmark_name=None):
# ensure pymol was opened
import time

time.sleep(2)
time.sleep(10)

if bookmark_name is not None:
self.set_storageman_attributes(bookmark_name=bookmark_name)
else:
if self.storageman.bookmark_name in self.get_bookmark_names():
self.logger.debug(
f"No bookmark specified, using bookmark '{self.storageman.bookmark_name}'."
)
else:
self.logger.error(
"No bookmark specified or available. display_pymol() currently only works for filtered data. "
)

poseIDs = {}
with self.storageman:
Expand All @@ -1864,6 +1827,27 @@ def display_pymol(self, bookmark_name=None):
"Error establishing connection with PyMol. Try manually launching PyMol with `pymol -R` in another terminal window."
) from e

# check if receptor in db
receptor = self.storageman.fetch_receptor_objects()[0]
if receptor[1]:
rec_name = receptor[0]
rec_string = ReceptorManager.blob2str(receptor[1])
import tempfile

rec_string = ReceptorManager.blob2str(receptor[1])
# with the rdkit pymol api, easiest to read receptor from file
with tempfile.NamedTemporaryFile(suffix=".pdbqt") as temp_file:
temp_file.write(rec_string.encode("utf-8"))
temp_file.flush()
temp_file_path = temp_file.name
pymol.LoadFile(temp_file_path, rec_name)
# center view on receptor
pymol.server.do("zoom")
else:
self.logger.debug(
"No receptor information in the database, receptor will not be displayed."
)

def onpick(event):
line = event.artist
coords = tuple([c[0] for c in line.get_data()])
Expand All @@ -1879,7 +1863,9 @@ def onpick(event):
flexible_residues, flexres_atomnames = (
self.storageman.fetch_flexres_info()
)
if flexible_residues != []: # converts string to list
if flexible_residues is None:
flexible_residues, flexres_atomnames = [], []
elif flexible_residues != []: # converts string to list
flexible_residues = json.loads(flexible_residues)
flexres_atomnames = json.loads(flexres_atomnames)

Expand Down
14 changes: 7 additions & 7 deletions ringtail/storagemanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class StorageManager:
_db_schema_code_compatibility = {
"1.0.0": ["1.0.0"],
"1.1.0": ["1.1.0"],
"2.0.0": ["2.0.0", "2.1.0", "2.1.1"],
"2.0.0": ["2.0.0", "2.1.0", "2.1.1", "2.1.2"],
}

"""Base class for a generic virtual screening database object.
Expand Down Expand Up @@ -251,7 +251,7 @@ def filter_results(self, all_filters: dict, suppress_output=False) -> iter:
)
return filtered_results

def check_passing_bookmark_exists(self, bookmark_name: str | None = None):
def check_passing_bookmark_exists(self, bookmark_name: str = None):
"""Checks if bookmark name is in database

Args:
Expand Down Expand Up @@ -1606,7 +1606,7 @@ def set_bookmark_suffix(self, suffix):
else:
self.view_suffix = suffix

def fetch_filters_from_bookmark(self, bookmark_name: str | None = None):
def fetch_filters_from_bookmark(self, bookmark_name: str = None):
"""Method that will retrieve filter values used to construct bookmark

Args:
Expand Down Expand Up @@ -2052,7 +2052,7 @@ def _fetch_all_plot_data(self):
"SELECT docking_score, leff FROM Results GROUP BY LigName"
)

def _fetch_passing_plot_data(self, bookmark_name: str | None = None):
def _fetch_passing_plot_data(self, bookmark_name: str = None):
"""Fetches cursor for best energies and leffs for
ligands passing filtering

Expand Down Expand Up @@ -2200,7 +2200,7 @@ def fetch_summary_data(

# region Methods dealing with filtered results

def _get_number_passing_ligands(self, bookmark_name: str | None = None):
def _get_number_passing_ligands(self, bookmark_name: str = None):
"""Returns count of the number of ligands that
passed filtering criteria

Expand Down Expand Up @@ -2669,7 +2669,7 @@ def _generate_result_filtering_query(self, filters_dict):
view_query = f"SELECT * FROM {filtering_window} R " + query
return output_query, view_query

def _prepare_cluster_query(self, unclustered_query: str) -> str | None:
def _prepare_cluster_query(self, unclustered_query: str) -> str:
"""
These methods will take data returned from unclustered filter query, then run the cluster query and cluster the filtered data.
This will output pose_ids that are representative of the clusters, and these pose_ids will be returned so that
Expand Down Expand Up @@ -3058,7 +3058,7 @@ def _prepare_interaction_indices_for_filtering(self, interaction_list: list):
)
else:
# create string representation of ecah interaction not found
interaction_not_found.append(":".join(interaction[:4]))
interaction_not_found.append(":".join(interaction[:5]))
continue # ends this iteration of the for loop

# create a list of lists for interactions to either include or exclude
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def find_files(directory):

setup(
name="ringtail",
version="2.1.1",
version="2.1.2",
author="Forli Lab",
author_email="forli@scripps.edu",
url="https://github.com/forlilab/Ringtail",
Expand Down