diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index cc3c96f8..1021edca 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -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/* diff --git a/docs/source/changes.rst b/docs/source/changes.rst index 8001c984..11298bcc 100644 --- a/docs/source/changes.rst +++ b/docs/source/changes.rst @@ -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 diff --git a/docs/source/index.rst b/docs/source/index.rst index b5334c94..6eae91b7 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -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 `, then navigate to :ref:`Getting started with Ringtail ` 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 `. + +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: diff --git a/ringtail/cloptionparser.py b/ringtail/cloptionparser.py index e15ae82b..080530dc 100644 --- a/ringtail/cloptionparser.py +++ b/ringtail/cloptionparser.py @@ -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", ) diff --git a/ringtail/outputmanager.py b/ringtail/outputmanager.py index 3c270a68..0497f28a 100644 --- a/ringtail/outputmanager.py +++ b/ringtail/outputmanager.py @@ -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 diff --git a/ringtail/ringtailcore.py b/ringtail/ringtailcore.py index c020ac45..b2b43c05 100644 --- a/ringtail/ringtailcore.py +++ b/ringtail/ringtailcore.py @@ -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 @@ -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. @@ -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, @@ -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. @@ -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, @@ -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) @@ -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 @@ -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 @@ -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() @@ -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 @@ -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: @@ -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()]) @@ -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) diff --git a/ringtail/storagemanager.py b/ringtail/storagemanager.py index b7d7491d..71ba9bc7 100644 --- a/ringtail/storagemanager.py +++ b/ringtail/storagemanager.py @@ -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. @@ -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: @@ -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: @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/setup.py b/setup.py index 596ba86e..0aa1b391 100644 --- a/setup.py +++ b/setup.py @@ -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",