# coding=utf8
#
# This code is Copyright (C) 2015 The Cambridge Crystallographic Data Centre
# (CCDC) of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC.
# This code may not be used, reproduced, translated, modified, disassembled or
# copied, except in accordance with a valid licence agreement with CCDC and may
# not be disclosed or redistributed in any form, either in whole or in part, to
# any third party. All copies of this code made in accordance with a valid
# licence agreement as referred to above must contain this copyright notice.
#
# No representations, warranties, or liabilities are expressed or implied in the
# supply of this code by CCDC, its servants or agents, except where such
# exclusion or limitation is prohibited, void or unenforceable under governing
# law.
#
#################################################################################################
'''
The :mod:`ccdc.docking` module provides an API to molecular docking functionality.
.. note:: The :mod:`ccdc.docking` module is available only to CSD-Discovery and CSD-Enterprise users.
The class :class:`ccdc.docking.Docker.LigandPreparation` provides functionality
for preparing ligands for docking. This classes encapsulate the
typical preparation activities, such as protonation and bond typing.
'''
# >>> import os
# >>> if 'GOLD_DIR' in os.environ and os.environ['GOLD_DIR']:
# ... from ccdc.docking import Docker
# ... from ccdc.io import MoleculeReader
# ... import os
# ... docker = Docker()
# ... settings = docker.settings
# ... protein_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'testsuite', 'testdata', '1fax_protein.mol2')
# ... aspirin = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'testsuite', 'testdata', 'aspirin.mol2')
# ... settings.add_protein_file(protein_file)
# ... settings.add_ligand_file(aspirin)
# ... settings.autoscale = 10.
# ... import tempfile
# ... tempd = tempfile.mkdtemp()
# ... settings.output_directory = tempd
# ... settings.output_file = 'aspirin_dock.mol2'
# ... settings.fitness_function = 'plp'
# ... ligand = MoleculeReader(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'testsuite', 'testdata', '1fax_ligand.mol2'))[0]
# ... settings.binding_site = settings.BindingSiteFromPoint(
# ... settings.proteins[0], ligand.centre_of_geometry(), 10.0
# ... )
# ... results = docker.dock()
# ... return_code = results.return_code
# ... ligand_reader = results.ligands
# ... output_file = os.path.join(settings.output_directory, settings.output_file)
# >>> #docked_molecules = [m for m in MoleculeReader(os.path.join(tempd, output_file))]
#
##########################################################################
import collections
import glob
import os
import re
import shutil
import socket
import subprocess
import sys
from ccdc import io
from ccdc.entry import Entry
from ccdc.molecule import Coordinates, Molecule
from ccdc.protein import Protein
from ccdc.utilities import Logger, nested_class
from ccdc.utilities import _private_importer
with _private_importer() as pi:
pi.import_ccdc_module('DockingLib')
pi.import_ccdc_module('ChemicalAnalysisLib')
pi.import_ccdc_module('ChemistryLib')
pi.import_ccdc_module('AnnotationsLib')
pi.import_ccdc_module('FileFormatsLib')
DockingLib.licence_check()
##########################################################################
[docs]class Docker(object):
'''Docker.'''
[docs] @nested_class('Docker')
class LigandPreparation(object):
'''Prepare ligands for docking.'''
[docs] @nested_class('Docker.LigandPreparation')
class Settings(object):
'''Configuration options for the preparation of ligands.'''
remove_unknown_atoms = True # Whether or not to remove unknown atoms
assign_bond_types = True # Whether or not to assign bond types
standardise_bond_types = False # Whether or not to standardise bonds to CSD conventions
add_hydrogens = True # Whether hydrogens need to be added
protonate = True # Whether protonation rules need to be applied
protonation_rules_file = None # Location of a file containing protonation rules
def __init__(self, settings=None):
if settings is None:
self.settings = Docker.LigandPreparation.Settings()
else:
self.settings = settings
if self.settings.protonation_rules_file is None:
rules_dir = io._CSDDatabaseLocator.get_optimisation_parameter_file_location()
rules_file = os.path.join(rules_dir, 'protonation_rules.txt')
else:
rules_file = self.settings.protonation_rules_file
if os.path.exists(rules_file):
self._protonation_rules = ChemicalAnalysisLib.ProtonationRules(rules_file)
else:
self._protonation_rules = None
[docs] def prepare(self, entry):
'''Prepare an entry for docking.
:param entry: :class:`ccdc.entry.Entry` instance
:returns: :class:`ccdc.entry.Entry` instance with specified rules applied.
'''
m = entry.molecule
if len(m.components) > 1:
raise RuntimeError('Docking of multi-component molecules is not supported')
if self.settings.remove_unknown_atoms:
m.remove_unknown_atoms()
if self.settings.assign_bond_types:
m.assign_bond_types()
if self.settings.standardise_bond_types:
m.standardise_aromatic_bonds()
m.standardise_delocalised_bonds()
if self.settings.protonate and self._protonation_rules is not None and self._protonation_rules.valid():
self._protonation_rules.apply_rules(m._molecule)
if self.settings.add_hydrogens:
m.remove_hydrogens()
m.add_hydrogens()
return Entry.from_molecule(m, **entry.attributes)
[docs] @nested_class('Docker')
class Settings(object):
'''Settings for docker.'''
_fitness_functions = ['goldscore', 'chemscore', 'asp', 'plp']
@classmethod
def _path_in_distribution(self, value):
if 'GOLD_DIR' in os.environ:
file_name = os.path.join(os.environ['GOLD_DIR'], 'gold', value)
if not os.path.exists(file_name):
file_name = os.path.join(os.environ['GOLD_DIR'], value)
elif 'MAINDIR' in os.environ:
file_name = os.path.join(os.environ['MAINDIR'], '..', 'goldsuite', 'gold_dist', 'gold', value)
else:
file_name = value
return file_name
[docs] @nested_class('Docker.Settings')
class LigandFileInfo(object):
'''Information about a ligand file.'''
def __init__(self, file_name, ndocks=1, start=0, finish=0):
self.file_name = file_name
self.ndocks = ndocks
self.start = start
self.finish = finish
def __str__(self):
return '{file_name} {ndocks} docks, starting at {start} finishing at {finish}'.format(
**self.__dict__
)
def __repr__(self):
return "LigandFileInfo('{file_name}', {ndocks}, {start}, {finish})".format(**self.__dict__)
def __eq__(self, other):
return (
self.file_name == other.file_name and
self.ndocks == other.ndocks and
self.start == other.start and
self.finish == other.finish
)
def __init__(self, _settings=None):
'''Initialise settings.'''
if _settings is None:
self._settings = DockingLib.GoldConfFile()
self.clear_protein_files()
self.autoscale = 100.
self._conf_file_name = './api_gold.conf'
self._fitness_function = ''
self._rescore_function = ''
self.fitness_function = 'goldscore'
self.rescore_function = ''
self._constraints = []
self._binding_site = None
else:
self._constraints = []
self._binding_site = None
self._settings = _settings
self._settings.set_preserve_mol2_comments(True)
self._gold_exe = None
self._socket = None
self._save_binding_site_atoms = False
self.logger = Logger()
def __del__(self):
if self._socket is not None:
self._socket.close()
del self._socket
[docs] @staticmethod
def from_file(file_name):
'''Read docking settings from a gold.conf file.
:param file_name: Location of the gold.conf file.
'''
settings = Docker.Settings(
_settings=DockingLib.GoldConfFileReader().read(file_name)
)
settings._conf_file_name = os.path.abspath(file_name)
settings.make_absolute_file_names(settings._conf_file_name)
_ = settings.constraints # Ensure they are read
settings.binding_site = Docker.Settings.BindingSite._from_settings(settings)
if settings._settings.run_type() == settings._settings.RESCORE_RUN:
settings._rescore_function = settings._settings.gold_fitness_function_path()
settings._fitness_function = ''
elif settings._settings.run_type() == settings._settings.CONSENSUS_SCORE:
settings._fitness_function = settings._settings.docking_fitness_function_path()
settings._rescore_function = settings._settings.rescore_fitness_function_path()
else:
settings._fitness_function = settings._settings.gold_fitness_function_path()
settings._rescore_function = settings._settings.rescore_fitness_function_path()
score_pars = settings.score_parameter_file
if score_pars and score_pars != 'DEFAULT' and not os.path.exists(score_pars):
settings.score_parameter_file = os.path.basename(score_pars)
tor_file = settings.torsion_distribution_file
if tor_file and tor_file != 'DEFAULT' and not os.path.exists(tor_file):
settings.torsion_distribution_file = tor_file
rotatable_bond_override_file = settings.rotatable_bond_override_file
if rotatable_bond_override_file and rotatable_bond_override_file != 'DEFAULT' and \
not os.path.exists(rotatable_bond_override_file):
settings.rotatable_bond_override_file = rotatable_bond_override_file
return settings
[docs] def make_absolute_file_names(self, file_name, relative=False):
'''Convert any relative file names to absolute file names.
:param file_name: str, the location of the settings file.
:param relative: bool, whether to make file names relative to the settings file.
'''
dirpath = os.path.dirname(os.path.abspath(file_name))
if not os.path.exists(dirpath):
os.makedirs(dirpath)
ligand_files = self.ligand_files
self.clear_ligand_files()
for lf in ligand_files:
file_name = lf.file_name
abs_path = os.path.abspath(os.path.join(dirpath, file_name))
rel_path = os.path.abspath(os.path.join(dirpath, os.path.basename(file_name)))
if relative:
# Don't copy ligands
lf.file_name = abs_path
else:
lf.file_name = abs_path
self.add_ligand_file(lf)
protein_files = self.protein_files
self.clear_protein_files()
for pf in protein_files:
file_name = pf.file_name
abs_path = os.path.abspath(os.path.join(dirpath, file_name))
rel_path = os.path.abspath(os.path.join(dirpath, os.path.basename(file_name)))
if relative:
if not os.path.exists(rel_path) and abs_path != rel_path:
shutil.copyfile(abs_path, rel_path)
# pf.file_name = os.path.basename(file_name)
pf.file_name = rel_path
else:
pf.file_name = abs_path
self.add_protein_file(pf)
if self._settings.cavity_file():
file_name = self._settings.cavity_file()
abs_path = os.path.abspath(os.path.join(dirpath, file_name))
rel_path = os.path.abspath(os.path.join(dirpath, os.path.basename(file_name)))
if relative:
if os.path.exists(abs_path) and abs_path != rel_path:
shutil.copyfile(abs_path, rel_path)
self._settings.set_cavity_file(rel_path)
else:
self._settings.set_cavity_file(abs_path)
if self._settings.ligand_reference_file():
file_name = self._settings.ligand_reference_file()
abs_path = os.path.abspath(os.path.join(dirpath, file_name))
rel_path = os.path.abspath(os.path.join(dirpath, os.path.basename(file_name)))
if relative:
if os.path.exists(abs_path) and abs_path != rel_path:
shutil.copyfile(abs_path, rel_path)
self._settings.set_ligand_reference_file(rel_path)
else:
self._settings.set_ligand_reference_file(abs_path)
waters = self._settings.waters()
if len(waters) > 0:
for water in waters:
file_name = water.path_
abs_path = os.path.abspath(os.path.join(dirpath, file_name))
rel_path = os.path.abspath(os.path.join(dirpath, os.path.basename(file_name)))
if relative:
if os.path.exists(abs_path) and abs_path != rel_path:
shutil.copyfile(abs_path, rel_path)
water.path_ = rel_path
else:
water.path_ = abs_path
self._settings.set_gold_waters(waters)
# Parameter files, ...
seed_file_setting = self._settings.seed_file()
if seed_file_setting:
abs_path = os.path.abspath(os.path.join(dirpath, seed_file_setting))
rel_path = os.path.abspath(os.path.join(dirpath, os.path.basename(seed_file_setting)))
if relative:
# Setting to rel_path doesn't work for a dock + rescore, for some reason
# if os.path.exists(abs_path) and abs_path != rel_path:
# shutil.copyfile(abs_path, rel_path)
self._settings.set_seed_file(abs_path)
else:
self._settings.set_seed_file(abs_path)
# output directory and file
if self.output_directory:
self._settings.set_directory(os.path.join(dirpath, self.output_directory))
if self.output_file:
self._settings.set_concatenated_output(
os.path.join(self.output_directory, os.path.basename(self.output_file)))
# fitting points
if self._settings.read_fitting_points():
abs_path = os.path.abspath(os.path.join(dirpath, self._settings.fitting_points_file()))
rel_path = os.path.join(dirpath, os.path.basename(self._settings.fitting_points_file()))
if relative:
self._settings.set_fitting_points_file(rel_path)
if self._settings.read_fitting_points():
if relative:
if os.path.exists(abs_path) and abs_path != rel_path:
shutil.copyfile(abs_path, rel_path)
else:
self._settings.set_fitting_points_file(abs_path)
@property
def conf_file(self):
'''The GOLD conf file represented by this settings instance.'''
return self._conf_file_name
# Ligands
@property
def ligand_files(self):
'''The ligand datafile settings.
:returns: tuple of class:`ccdc.docking.Docker.Settings.LigandFileInfo` instances.
'''
return tuple(
Docker.Settings.LigandFileInfo(
df.ligand_filename_,
df.n_ga_runs_,
df.start_ligand_,
df.finish_ligand_
) for df in self._settings.ligand_datafiles()
)
[docs] def clear_ligand_files(self):
'''Remove all ligand datafiles from settings.'''
self._settings.set_ligand_datafiles(tuple())
[docs] def add_ligand_file(self, file_name, ndocks=1, start=0, finish=0):
'''Add a file of ligands to the docking settings.
:param file_name: a mol2 or sdf file of ligand molecules, or a :class:`ccdc.docking.Docker.Settings.LigandFileInfo` instance.
:param ndocks: int, the number of docking attempts for each ligand
:param start: int, index of ligand at which to start
:param finish: int, index of ligand at which to finish
'''
df = DockingLib.LigandDataFile()
if isinstance(file_name, Docker.Settings.LigandFileInfo):
df.ligand_filename_ = file_name.file_name
df.n_ga_runs_ = file_name.ndocks
df.start_ligand_ = file_name.start
df.finish_ligand_ = file_name.finish
else:
df.ligand_filename_ = file_name
df.n_ga_runs_ = ndocks
df.start_ligand_ = start
df.finish_ligand_ = finish
self._settings.add_ligand_datafile(df)
@property
def ligands(self):
'''The ligands specified for docking.'''
ligands = io.MoleculeReader(
[l.file_name for l in self.ligand_files]
)
return ligands
# Proteins
[docs] @nested_class('Docker.Settings')
class ProteinFileInfo(object):
'''Data associated with a protein for docking.'''
def __init__(self, file_name=None, _protein_data=None, settings=None):
'''Initialise a ProteinFileInfo instance.
:param file_name: str
'''
if _protein_data is None:
_protein_data = DockingLib.GoldConfProteinData()
_protein_data.set_protein_datafile(file_name)
self._protein_data = _protein_data
self._constraints = tuple()
self._rotamer_libraries = []
@property
def file_name(self):
'''The file name of the protein.'''
return self._protein_data.protein_datafile()
@file_name.setter
def file_name(self, file_name):
self._protein_data.set_protein_datafile(file_name)
def __str__(self):
return "ProteinFileInfo('%s')" % self.file_name
__repr__ = __str__
def __eq__(self, other):
return self.file_name == other.file_name
[docs] def add_constraint(self, constraint):
'''Add a constraint to the protein.'''
self._protein_data.add_constraint(constraint._constraint)
self._constraints = self._constraints + (constraint,)
[docs] def clear_constraints(self):
'''Remove all constraints.'''
self._protein_data.clear_constraints()
self._constraints = tuple()
@property
def constraints(self):
'''The constraints associated with this protein.'''
return self._constraints
@property
def rotamer_libraries(self):
'''The set of defined rotamer libraries for this protein.'''
return self._rotamer_libraries
[docs] def add_rotamer_library(self, rotamer_library):
'''Add a rotamer library to this protein.'''
self._protein_data.add_rotamer_library(rotamer_library._rotamer_library)
self._rotamer_libraries.append(rotamer_library)
[docs] def update_rotamer_library(self, rotamer_library):
'''Update rotamer library to this protein.'''
existing_rls = self._rotamer_libraries
self._rotamer_libraries = []
self._protein_data.clear_rotamer_libraries()
for rl in existing_rls:
if rl._rotamer_library.name() == rotamer_library._rotamer_library.name():
self.add_rotamer_library(rotamer_library)
else:
self.add_rotamer_library(rl)
[docs] def clear_rotamer_libraries(self):
'''Remove all rotamer libraries'''
self._protein_data.clear_rotamer_libraries()
self._rotamer_libraries = []
@property
def protein_files(self):
'''The protein file targets.'''
if not hasattr(self, '_protein_info'):
self._protein_info = tuple(
Docker.Settings.ProteinFileInfo(_protein_data=p, settings=self)
for p in self._settings.protein_data()
)
for p in self._protein_info:
p._constraints = tuple(
Docker.Settings.Constraint._make_constraint(
self, p._protein_data.constraint(i), p.file_name
)
for i in range(p._protein_data.nconstraints())
)
return self._protein_info
@property
def proteins(self):
'''The proteins.'''
def _read_protein(file_name):
file_name = os.path.join(os.path.dirname(self.conf_file), file_name)
p = Protein.from_file(file_name)
return p
if not hasattr(self, '_proteins'):
self._proteins = tuple(_read_protein(f.file_name) for f in self.protein_files)
return self._proteins
[docs] def clear_protein_files(self):
'''Clear the set of targets.'''
self._settings.set_protein_data(tuple())
if hasattr(self, '_proteins'):
del self._proteins
if hasattr(self, '_protein_info'):
del self._protein_info
if hasattr(self, '_rotamer_info'):
del self._rotamer_info
[docs] def add_protein_file(self, file_name):
'''Add a target file to be docked against.'''
if isinstance(file_name, Docker.Settings.ProteinFileInfo):
prot_data = file_name._protein_data
else:
prot_data = DockingLib.GoldConfProteinData()
prot_data.set_protein_datafile(file_name)
self._settings.add_protein_data(prot_data)
if hasattr(self, '_proteins'):
del self._proteins
if hasattr(self, '_protein_info'):
del self._protein_info
if hasattr(self, '_rotamer_info'):
del self._rotamer_info
@property
def fix_all_protein_rotatable_bonds(self):
'''Get or set whether to fix all terminal protein rotatable bonds during docking
(can be set to True to fix them all or False to allow them to move)
'''
return self._settings.fix_protein_rotatable_bonds()
@fix_all_protein_rotatable_bonds.setter
def fix_all_protein_rotatable_bonds(self, value):
self._settings.set_fix_protein_rotatable_bonds(bool(value))
[docs] @nested_class('Docker.Settings')
class WaterFileInfo(object):
'''Information about an active water file.'''
_ToggleStates = {"on": DockingLib.GoldConfFile.WATER_ON,
"off": DockingLib.GoldConfFile.WATER_OFF,
"toggle": DockingLib.GoldConfFile.WATER_TOGGLE}
_SpinStates = {"fix": DockingLib.GoldConfFile.WATER_FIX,
"spin": DockingLib.GoldConfFile.WATER_SPIN,
"trans_spin": DockingLib.GoldConfFile.WATER_TRANS_SPIN}
@classmethod
def _get_toggle_state_text(cls, enum_value):
for state in cls._ToggleStates:
if cls._ToggleStates[state] == enum_value:
return state
raise RuntimeError("invalid enum value? %d" % enum_value)
@classmethod
def _get_spin_state_text(cls, enum_value):
for state in cls._SpinStates:
if cls._SpinStates[state] == enum_value:
return state
raise RuntimeError("invalid enum value? %d" % enum_value)
'''Information about a flexible water.'''
def __init__(self, file_name, toggle_state="toggle", spin_state="spin", movable_distance=0.0):
self._water = DockingLib.GoldWater()
self.file_name = file_name
self.toggle_state = toggle_state
self.spin_state = spin_state
self.movable_distance = movable_distance
@property
def atom_index(self):
''' :return the index of the atom in the water that is the Oxygen '''
return self._water.atom_index_
@property
def file_name(self):
'''
:return the filename of the file that contains the water to be active in docking
'''
return self._water.path_
@file_name.setter
def file_name(self, file_name):
'''
set the filename of the file that contains the water to be active in docking.
The file must contain a single water atom (just 3 atoms) or this will throw RuntimeError
:param the filename of the file containing a single water molecule
'''
def oxygen_index(file_name):
mol = io.MoleculeReader(file_name)[0]
if len(mol.atoms) != 3:
raise RuntimeError(
'File passed in as a water {} must contain a single water molecule (with 3 atoms)'.format(
file_name))
if len([atom for atom in mol.atoms if atom.atomic_number == 1]) != 2:
raise RuntimeError('File passed in as a water {} must contain 2 hydrogens'.format(file_name))
index = 1
for atom in mol.atoms:
if atom.atomic_number == 8: # The oxygen
return index
index += 1
raise RuntimeError('File passed in as a water {} must contain an oxygen'.format(file_name))
if not os.path.exists(file_name):
raise RuntimeError('Water file {} must exist'.format(file_name))
self._water.atom_index_ = oxygen_index(file_name)
self._water.path_ = file_name
@property
def toggle_state(self):
return self._get_toggle_state_text(self._water.toggle_state_)
@toggle_state.setter
def toggle_state(self, state):
if state not in self._ToggleStates:
raise RuntimeError('Toggle state setting %s is invalid: must be one of on, off or toggle' % state)
self._water.toggle_state_ = self._ToggleStates[state]
@property
def spin_state(self):
return self._get_spin_state_text(self._water.spin_state_)
@spin_state.setter
def spin_state(self, state):
if state not in self._SpinStates:
raise RuntimeError(
'Spin state setting %s is invalid: must be one of fix, spin or trans_spin' % state)
self._water.spin_state_ = self._SpinStates[state]
@property
def movable_distance(self):
return self._water.distance_
@movable_distance.setter
def movable_distance(self, value):
if value > 0.0 and self.spin_state != "trans_spin":
raise RuntimeError(
'Spin state setting %s is inconsistent with distance setting (distance is > 0.0 but trans_spin not requested)' % self.spin_state)
if value <= 0.0 and self.spin_state == "trans_spin":
raise RuntimeError(
'Spin state setting %s is inconsistent with distance setting (distance is 0.0 but trans_spin requested)' % self.spin_state)
self._water.distance_ = value
def __str__(self):
return 'Water from %s is set to %s and %s with a movable distance of %.2f' % (
self.file_name, self.toggle_state, self.spin_state, self.movable_distance)
def __repr__(self):
return "WaterFileInfo('%s', %s, %s, %.2f)" % (
self.file_name, self.toggle_state, self.spin_state, self.movable_distance)
def __eq__(self, other):
return (
# Only check user-provided data
self.file_name == other.file_name and
self.toggle_state == other.toggle_state and
self.spin_state == other.spin_state and
self.movable_distance == other.movable_distance
)
[docs] def add_water_file(self, file_name, toggle_state="toggle", spin_state="spin", movable_distance=0.0):
'''Add a water file to be docked.'''
def check_input(file_name):
for water in self._settings.waters():
if water.path_ == file_name:
raise RuntimeError(
"Cant add a water with file %s: water file already added in settings" % file_name)
if isinstance(file_name, Docker.Settings.WaterFileInfo):
check_input(file_name.file_name)
water = file_name
else:
check_input(file_name)
water = Docker.Settings.WaterFileInfo(file_name, toggle_state, spin_state, movable_distance)
self._settings.add_water(water._water)
[docs] def clear_water_files(self):
'''Remove all water objects from settings.'''
self._settings.set_gold_waters(tuple())
@property
def water_files(self):
'''The water datafile settings.
:returns: tuple of class:`ccdc.docking.Docker.Settings.WaterFileInfo` instances.
'''
return tuple(
Docker.Settings.WaterFileInfo(
water.path_,
Docker.Settings.WaterFileInfo._get_toggle_state_text(water.toggle_state_),
Docker.Settings.WaterFileInfo._get_spin_state_text(water.spin_state_),
water.distance_
) for water in self._settings.waters()
)
@property
def waters(self):
'''The waters specified for docking as read in from the input files'''
# Work around PYAPI-2600 by never creating an empty MoleculeReader.
# if PYAPI-2600 is fixed this can be changed to
#
# return io.MoleculeReader([l.file_name for l in self.water_files])
files = [l.file_name for l in self.water_files]
if len(files) > 0:
return io.MoleculeReader(files)
else:
return ()
@property
def reference_ligand_file(self):
'''Any reference ligand file name set.'''
return self._settings.ligand_reference_file()
@reference_ligand_file.setter
def reference_ligand_file(self, file_name):
self._settings.set_ligand_reference_file(file_name)
# Output
@property
def output_directory(self):
'''Directory to which output will be sent.'''
return self._settings.directory()
@output_directory.setter
def output_directory(self, dir_name):
'''Set the output directory.'''
self._settings.set_directory(dir_name)
@property
def output_file(self):
'''Output file.
If this is an empty string then each docking will be in a separate file.
'''
return self._settings.concatenated_output()
@output_file.setter
def output_file(self, file_name):
'''Set the output file.'''
self._settings.set_concatenated_output(file_name)
self.output_format = os.path.splitext(file_name)[1][1:]
@property
def output_format(self):
'''Desired format for output file.'''
x = self._settings.output_file_format()
if x == DockingLib.GoldConfFile.MOL2:
return 'mol2'
elif x == DockingLib.GoldConfFile.MACCS:
return 'sdf'
else:
return None
@output_format.setter
def output_format(self, value):
if value.lower() == 'mol2':
self._settings.set_output_file_format(DockingLib.GoldConfFile.MOL2)
elif value.lower() == 'sdf':
self._settings.set_output_file_format(DockingLib.GoldConfFile.MACCS)
else:
self._settings.set_output_file_format(DockingLib.GoldConfFile.FILEFORMAT_NOTSET)
# Lone Pairs in Output Files
@property
def save_lone_pairs(self):
''' Get or set whether to include lone pairs in output files or not
(can be True of False: False will mean lone pairs are omitted from output)
'''
return self._settings.save_lone_pairs()
@save_lone_pairs.setter
def save_lone_pairs(self, value):
self._settings.set_save_lone_pairs(bool(value))
@property
def flip_free_corners(self):
'''Get or set whether to flip ring free corners during docking (can be True or False)
'''
return self._settings.flip_free_corners()
@flip_free_corners.setter
def flip_free_corners(self, value):
self._settings.set_flip_free_corners(bool(value))
@property
def flip_amide_bonds(self):
'''Get or set whether to flip amide bonds during docking (can be True or False)
'''
return self._settings.flip_amide_bonds()
@flip_amide_bonds.setter
def flip_amide_bonds(self, value):
self._settings.set_flip_amide_bonds(bool(value))
@property
def flip_pyramidal_nitrogen(self):
'''get or set whether to flip pyramidal nitrogens during docking (can be True or False)
'''
return self._settings.flip_pyramidal_N()
@flip_pyramidal_nitrogen.setter
def flip_pyramidal_nitrogen(self, value):
self._settings.set_flip_pyramidal_N(bool(value))
@property
def flip_planar_nitrogen(self):
'''Return the current settings for flexibility of planar nitrogens
.. seealso:: :attr:`set_flip_planar_nitrogen`
'''
ring_NHR = self._settings.bond_flexibility_text(self._settings.ring_NHR_flexibility())
if ring_NHR == 'rot':
ring_NHR = 'rotate'
ring_NRR = self._settings.bond_flexibility_text(self._settings.ring_NRR_flexibility())
if ring_NRR == 'rot':
ring_NRR = 'rotate'
return {'setting': self._settings.flip_planar_N(),
'ring_NHR': ring_NHR, 'ring_NRR': ring_NRR}
[docs] def set_flip_planar_nitrogen(self, setting, ring_NRR=None, ring_NHR=None):
'''Set the flip_planar_nitrogen settings
:param setting: whether to switch this off or on
:param ring_NRR: whether to fix, flip or rotate ring NRR groups (Can be 'fix', 'flip', 'rotate' or None. \
None means keep the current setting)
:param ring_NHR: whether to fix, flip or rotate ring NHR groups (Can be 'fix', 'flip', 'rotate' or None. \
None means keep the current setting)
.. seealso:: :attr:`flip_planar_nitrogen`
'''
allowed = set(['flip', 'fix', 'rotate', 'rot', None])
if ring_NRR not in allowed:
raise ValueError(f'invalid ring_NRR arguemnt: {ring_NRR}')
if ring_NHR not in allowed:
raise ValueError(f'invalid ring_NHR arguemnt: {ring_NHR}')
if ring_NRR is not None:
self._settings.set_ring_NRR_flexibility(self._settings.bond_flexibility_enum(ring_NRR))
if ring_NHR is not None:
self._settings.set_ring_NHR_flexibility(self._settings.bond_flexibility_enum(ring_NHR))
self._settings.set_flip_planar_N(bool(setting))
@property
def fix_ligand_rotatable_bonds(self):
'''Set or get the state of fixing rotatable bonds in docked ligands
* 'all' means all ligand bonds will be held rigid in docking
* 'all_but_terminal' means all bonds except terminal groups such as hydroxyls, \
methyls etc will be held rigid
* 'specific' means that certain bonds have been set as rigid. It is assumed that the \
indexes of the atoms involved are valid for all ligands docked, but the API will refer to bonds in \
the first input ligand expressed in the :class:`Docker.Settings` object. See \
:attr:`specific_fixed_rotatable_bonds` to interrogate which bonds are held fixed
* None means no bonds are held rigid
.. seealso::
* :func:`add_specific_fixed_rotatable_bond`
* :func:`remove_specific_fixed_rotatable_bond`
* :attr:`specific_fixed_rotatable_bonds`
:return: 'all', 'all_but_terminal','specific' or None
'''
if self._settings.ligand_bond_flexibility() == DockingLib.GoldConfFile.ROTATE_BOND:
return None
if self._settings.ligand_bond_flexibility() == DockingLib.GoldConfFile.FIX_BOND:
return 'specific'
return self._settings.bond_flexibility_text(self._settings.ligand_bond_flexibility())
@fix_ligand_rotatable_bonds.setter
def fix_ligand_rotatable_bonds(self, value):
if value is None:
self._settings.set_ligand_bond_flexibility(DockingLib.GoldConfFile.ROTATE_BOND)
elif value == 'specific':
if len(self._settings.fixed_bonds()) == 0:
raise ValueError("Can't set to 'specific' as no fixed bonds have been added")
else:
self._settings.set_ligand_bond_flexibility(DockingLib.GoldConfFile.FIX_BOND)
elif value in ['all', 'all_but_terminal']:
self._settings.set_ligand_bond_flexibility(self._settings.bond_flexibility_enum(value))
else:
raise ValueError(f"Unknown setting '{value}'")
@property
def specific_fixed_rotatable_bonds(self):
''' Return the specific bonds that are fixed in docking, or an empty list if none are fixed
The bonds returned relate to the first loaded ligand in the settings note.
Will raise an IndexError if no ligands are available but fixed_rotatable_bonds have been read
in from a GOLD configuration file
.. seealso::
* :func:`add_specific_fixed_rotatable_bonds`,
* :func:`remove_specific_fixed_rotatable_bond`
* :attr:`fix_ligand_rotatable_bonds`
:return: a list of bonds that are held fixed (all are bonds in the first input ligand note)
'''
def find_bond(pair):
for bd in self.ligands[0].bonds:
if sorted([bd.atoms[0].index, bd.atoms[1].index]) == sorted([pair[0] - 1, pair[1] - 1]):
return bd
return None
return [x for x in [find_bond(pair) for pair in self._settings.fixed_bonds()] if x is not None]
[docs] def add_specific_fixed_rotatable_bond(self, bond):
'''Set a specific rotatable bond, or a list or rotatable bonds as fixed in docking.
The index of the two atoms forming each bond passed in will ultimately be used
in the docking, and so if docking multiple ligands, bonds relating to
these indexes will be fixed for all of them.
.. seealso::
* :attr:`specific_fixed_rotatable_bonds`,
* :func:`remove_specific_fixed_rotatable_bond`
* :attr:`fix_ligand_rotatable_bonds`
:param bond: the bond to add, or a list of bonds to add
'''
def find_pair(bond):
for pair in self._settings.fixed_bonds():
if sorted([bond.atoms[0].index, bond.atoms[1].index]) == sorted([pair[0] - 1, pair[1] - 1]):
return pair
return None
try:
if find_pair(bond) is not None:
return
ids = sorted([bond.atoms[0].index + 1, bond.atoms[1].index + 1])
self._settings.add_fixed_bond(ids[0], ids[1])
self.fix_ligand_rotatable_bonds = 'specific'
except AttributeError:
try:
[self.add_specific_fixed_rotatable_bond(b) for b in bond]
except ValueError:
raise ValueError(f"Can't set specific rotatable bonds - invalid argument {bond}")
[docs] def remove_specific_fixed_rotatable_bond(self, bond):
'''Remove a specific rotatable bond or a list or rotatable bonds that were previously added
.. seealso::
* :attr:`specific_fixed_rotatable_bonds`,
* :func:`add_specific_fixed_rotatable_bond`
* :attr:`fix_ligand_rotatable_bonds`
:param bond: the bond to remove, or a list of bonds to remove
'''
try:
ids = [bond.atoms[0].index + 1, bond.atoms[1].index + 1]
fixed_bonds = self._settings.fixed_bonds()
bonds_to_set = [f for f in fixed_bonds if f not in [(ids[0], ids[1]), (ids[1], ids[0])]]
if len(fixed_bonds) == len(bonds_to_set):
return
self._settings.set_fixed_bonds(bonds_to_set)
if self.fix_ligand_rotatable_bonds == 'specific' and \
len(self._settings.fixed_bonds()) == 0:
self.fix_ligand_rotatable_bonds = None
except AttributeError:
try:
[self.remove_specific_fixed_rotatable_bond(b) for b in bond]
except ValueError:
raise ValueError(f"Can't set specific rotatable bonds - invalid argument {bond}")
@property
def match_template_conformations(self):
'''Get or set whether to match template conformations of rings during docking
(can be True or False)
'''
return self._settings.match_ring_templates()
@match_template_conformations.setter
def match_template_conformations(self, value):
self._settings.set_match_ring_templates(bool(value))
@property
def detect_internal_hydrogen_bonds(self):
'''Get or set whether to detect and score internal hydrogen bonds in ligands during docking
(can be True or False)
'''
return self._settings.allow_internal_ligand_hbonds()
@detect_internal_hydrogen_bonds.setter
def detect_internal_hydrogen_bonds(self, value):
self._settings.set_allow_internal_ligand_hbonds(bool(value))
@property
def rotate_carboxylic_hydroxyl_groups(self):
'''get or set whether to rotate carboxylic hydroxyl group during docking
:param setting: a string that can be one of 'flip', 'fix' or 'rotate'
'''
value = self._settings.bond_flexibility_text(self._settings.carboxylic_OH_flexibility())
# Lower level code uses 'rot' as the keyword but writes 'rotate'!
if value == 'rot': value = 'rotate'
return value
@rotate_carboxylic_hydroxyl_groups.setter
def rotate_carboxylic_hydroxyl_groups(self, setting):
if setting not in ('flip', 'fix', 'rotate'):
raise ValueError("carboxylic rotation setting must be one of flip,fix or rotate")
self._settings.set_carboxylic_OH_flexibility(self._settings.bond_flexibility_enum(setting))
# fitting points
@property
def fitting_points_file(self):
'''A file to read or write the fitting points.'''
if not self._settings.fitting_points_file():
self._settings.set_fitting_points_file('fit_pts.mol2')
return self._settings.fitting_points_file()
@fitting_points_file.setter
def fitting_points_file(self, file_name):
if not file_name:
self._settings.set_fitting_points_file('fit_pts.mol2')
self._settings.set_read_fitting_points(False)
else:
self._settings.set_fitting_points_file(file_name)
self._settings.set_read_fitting_points(True)
# Binding site
@property
def binding_site(self):
''' Set or get the binding site configuration for this docking.
:parameter value: should be a :class:`Docker.Settings.BindingSite` object (or derived class.)
.. seealso::
* :class:`Docker.Settings.BindingSite` and related classes
* :attr:`Docker.Settings.detect_cavity`
'''
return self._binding_site
@binding_site.setter
def binding_site(self, value):
self._binding_site = value
@property
def save_binding_site_atoms(self):
'''Whether or not to write the binding site atom file.'''
return self._save_binding_site_atoms
@save_binding_site_atoms.setter
def save_binding_site_atoms(self, value):
self._save_binding_site_atoms = bool(value)
@property
def detect_cavity(self):
'''Set or get whether to detect the cavity from the binding site definition or not
(i.e. post-process the binding site)
:parameter value: True if cavity detection should be switched on, False otherwise
:raises: RuntimeError if no binding site is set
:return: The setting of cavity detection
.. seealso::
* :class:`Docker.Settings.BindingSite` and related classes
* :attr:`Docker.Settings.binding_site`
'''
if self.binding_site is not None:
return self.binding_site.detect_cavity
else:
raise RuntimeError('No binding site is set, the setting of detect_cavity cannot be established')
@detect_cavity.setter
def detect_cavity(self, value):
if self.binding_site is not None:
self.binding_site.detect_cavity = bool(value)
else:
raise RuntimeError('No binding site is set, so detect_cavity cannot be set')
@property
def solvate_all(self):
'''Get or set whether to treat all fitting points associated with solvent-accessible donors or acceptors as
themselves solvent-accessible. (See the GOLD conf file documentation for more information)
(can be True or False)
'''
return self._settings.solvate_all()
@solvate_all.setter
def solvate_all(self, value):
self._settings.set_solvate_all(bool(value))
[docs] def write(self, file_name):
'''Write docking settings to a GOLD configuration file
Note that calling this method writes a self-contained configuration file
but may also cause copying of other files to locations relative to
the configuration file, so file names in the calling settings object
will change to point to these copies.
No tests are performed to check for over-writing of files when calling write.
:param file_name: The path of the output GOLD configuration file
:raises: RuntimeError if no fitness and rescore function is set
'''
if not self.fitness_function and not self.rescore_function:
raise RuntimeError('No fitness or rescore function set')
self._conf_file_name = file_name
self.make_absolute_file_names(file_name, relative=True)
constraints = self.constraints
if self.binding_site is not None:
self.binding_site._to_settings(self)
for c in constraints:
c._write_mol_files(self)
c._constraint.from_string(c._to_string())
for p in self.protein_files:
for c in p.constraints:
c._write_mol_files(self)
c._constraint.from_string(c._to_string())
if self.save_binding_site_atoms:
with open(os.path.join(os.path.dirname(self.conf_file), 'cavity.atoms'), 'w') as writer:
for i, a in enumerate(self.binding_site.atoms):
if i and i % 10 == 0:
writer.write('\n')
writer.write('%s%d' % ('' if i % 10 == 0 else ' ', a.index + 1))
writer.write('\n\n')
writer = DockingLib.GoldConfFileWriter(self._settings)
writer.write(file_name)
[docs] @nested_class('Docker.Settings')
class BindingSite(Protein.BindingSite):
def __init__(self):
'''Initialise a binding site definition.'''
self.detect_cavity = True
def _to_settings(self, settings):
settings._settings.set_detect_cavity(self.detect_cavity)
settings._settings.set_cavity_origin((0, 0, 0))
settings._settings.set_cavity_radius(10)
settings._settings.set_floodfill_atom_no(0)
settings._settings.set_cavity_file('')
settings._settings.set_cavity_contact_distance(10)
@staticmethod
def _from_settings(settings):
_mode_dict = {
DockingLib.GoldConfFile.CAVITY_FROM_POINT: Docker.Settings.BindingSiteFromPoint,
DockingLib.GoldConfFile.CAVITY_FROM_ATOM: Docker.Settings.BindingSiteFromAtom,
DockingLib.GoldConfFile.CAVITY_FROM_RESIDUE: Docker.Settings.BindingSiteFromResidue,
DockingLib.GoldConfFile.CAVITY_FROM_LIST_OF_ATOMS: Docker.Settings.BindingSiteFromListOfAtoms,
DockingLib.GoldConfFile.CAVITY_FROM_LIST_OF_RESIDUES: Docker.Settings.BindingSiteFromListOfResidues,
DockingLib.GoldConfFile.CAVITY_FROM_LIGAND: Docker.Settings.BindingSiteFromLigand
}
klass = _mode_dict[settings._settings.cavity_definition_mode()]
return klass._from_settings(settings)
[docs] class BindingSiteFromPoint(Protein.BindingSiteFromPoint, BindingSite):
'''A cavity defined from a point.'''
def __init__(self, protein, origin=(0, 0, 0), distance=12.):
Protein.BindingSiteFromPoint.__init__(self, protein, origin, distance)
Docker.Settings.BindingSite.__init__(self)
def _to_settings(self, settings):
super(self.__class__, self)._to_settings(settings)
settings._settings.set_cavity_origin(self.origin)
settings._settings.set_cavity_radius(self.distance)
settings._settings.set_cavity_definition_mode(DockingLib.GoldConfFile.CAVITY_FROM_POINT)
@staticmethod
def _from_settings(settings):
pt = settings._settings.cavity_origin()
bs = Docker.Settings.BindingSiteFromPoint(
None,
Coordinates(pt.x(), pt.y(), pt.z()),
settings._settings.cavity_radius()
)
bs.detect_cavity = settings._settings.detect_cavity()
return bs
[docs] class BindingSiteFromAtom(Protein.BindingSiteFromAtom, BindingSite):
'''A cavity defined from a protein atom.'''
def __init__(self, protein, atom, distance):
Protein.BindingSiteFromAtom.__init__(self, protein, atom, distance)
Docker.Settings.BindingSite.__init__(self)
def _to_settings(self, settings):
super(self.__class__, self)._to_settings(settings)
settings._settings.set_floodfill_atom_no(self.atom.index + 1)
settings._settings.set_cavity_radius(self.distance)
settings._settings.set_cavity_origin(self.atom.coordinates)
settings._settings.set_cavity_definition_mode(DockingLib.GoldConfFile.CAVITY_FROM_ATOM)
@staticmethod
def _from_settings(settings):
p = settings.proteins[0]
at = p.atoms[settings._settings.floodfill_atom_no() - 1]
bs = Docker.Settings.BindingSiteFromAtom(settings.proteins[0], at, settings._settings.cavity_radius())
bs.detect_cavity = settings._settings.detect_cavity()
return bs
[docs] class BindingSiteFromResidue(Protein.BindingSiteFromResidue, BindingSite):
'''A cavity defined from a protein residue.'''
def __init__(self, protein, residue, distance):
Protein.BindingSiteFromResidue.__init__(self, protein, residue, distance)
Docker.Settings.BindingSite.__init__(self)
def _to_settings(self, settings):
super(self.__class__, self)._to_settings(settings)
settings._settings.set_floodfill_atom_no(self.residue.atoms[0].index)
settings._settings.set_cavity_radius(self.distance)
settings._settings.set_cavity_definition_mode(DockingLib.GoldConfFile.CAVITY_FROM_RESIDUE)
@staticmethod
def _from_settings(settings):
p = settings.proteins[0]
at = p.atoms[settings._settings.floodfill_atom_no()]
for res in p.residues:
if at in res.atoms:
break
else:
raise RuntimeError('The atom %s does not appear to be in a residue.' % str(at))
bs = Docker.Settings.BindingSiteFromResidue(
p, res, settings._settings.cavity_radius()
)
bs.detect_cavity = settings._settings.detect_cavity()
return bs
[docs] class BindingSiteFromListOfAtoms(Protein.BindingSiteFromListOfAtoms, BindingSite):
def __init__(self, protein, atoms):
Protein.BindingSiteFromListOfAtoms.__init__(self, protein, atoms)
Docker.Settings.BindingSite.__init__(self)
def _to_settings(self, settings):
super(self.__class__, self)._to_settings(settings)
if settings._settings.cavity_file():
file_name = os.path.join(os.path.dirname(settings.conf_file),
os.path.basename(settings._settings.cavity_file()))
settings._settings.set_cavity_file(file_name)
else:
file_name = os.path.join(os.path.dirname(settings.conf_file), 'cavity.atoms')
with open(file_name, 'w') as writer:
for i, a in enumerate(self.atoms):
if i and i % 10 == 0:
writer.write('\n')
writer.write('%d ' % (a.index + 1)) # Cavity atom file is indexed from 1
settings._settings.set_cavity_file(file_name)
settings._settings.set_cavity_definition_mode(DockingLib.GoldConfFile.CAVITY_FROM_LIST_OF_ATOMS)
@staticmethod
def _from_settings(settings):
file_name = os.path.join(os.path.dirname(settings.conf_file), settings._settings.cavity_file())
if not os.path.exists(file_name):
raise RuntimeError('The cavity file %s does not exist' % file_name)
with open(file_name) as f:
indices = [int(i) for i in f.read().split()]
prot = settings.proteins[0]
prot_atoms = prot.atoms
# cavity.atoms file is indexed from 1 rather than 0
inxs = [i - 1 for i in indices if i - 1 < len(prot_atoms)]
bs = Docker.Settings.BindingSiteFromListOfAtoms(
prot, tuple(prot_atoms[i] for i in inxs)
)
bs.detect_cavity = settings._settings.detect_cavity()
return bs
[docs] class BindingSiteFromListOfResidues(Protein.BindingSiteFromListOfResidues, BindingSite):
'''Cavity defined from a list of residues.'''
def __init__(self, protein, residues):
Protein.BindingSiteFromListOfResidues.__init__(self, protein, residues)
Docker.Settings.BindingSite.__init__(self)
def _to_settings(self, settings):
super(self.__class__, self)._to_settings(settings)
file_name = os.path.join(os.path.dirname(settings.conf_file), 'cavity.residues')
settings._settings.set_cavity_file(file_name)
with open(file_name, 'w') as writer:
writer.write('> <Gold.Protein.ActiveResidues>\n')
for i, r in enumerate(self.residues):
if i and i % 8 == 0:
writer.write('\n')
writer.write('%s ' % r.identifier)
writer.write('\n\n')
settings._settings.set_cavity_definition_mode(DockingLib.GoldConfFile.CAVITY_FROM_LIST_OF_RESIDUES)
@staticmethod
def _from_settings(settings):
file_name = os.path.join(os.path.dirname(settings.conf_file), settings._settings.cavity_file())
if not os.path.exists(file_name):
raise RuntimeError('The cavity file %s does not exist' % file_name)
p = settings.proteins[0]
with open(file_name) as f:
lines = f.readlines()[1:]
text = ' '.join(lines)
residue_names = set(text.split())
name_dic = {r.identifier: r for r in p.residues}
no_chain_dic = collections.defaultdict(list)
for r in p.residues:
no_chain_dic[r.identifier[r.identifier.index(':') + 1:]].append(r)
residues = []
for n in residue_names:
if n in name_dic:
residues.append(name_dic[n])
elif n in no_chain_dic:
if len(no_chain_dic[n]) == 1:
residues.append(no_chain_dic[n][0])
else:
raise RuntimeError('Ambiguous residue name %s' % n)
else:
raise RuntimeError('Residue name %s not found in protein' % n)
if len(residues) != len(residue_names):
raise RuntimeError('The number of residues and the number of residue names do not match')
bs = Docker.Settings.BindingSiteFromListOfResidues(p, residues)
bs.detect_cavity = settings._settings.detect_cavity()
return bs
[docs] class BindingSiteFromLigand(Protein.BindingSiteFromMolecule, BindingSite):
'''A cavity defined from a ligand and a contact distance.'''
def __init__(self, protein, ligand, distance=6.0, whole_residues=False):
"""
:param protein: the protein used in the docking
:param ligand: the ligand used to define the extent of the binding site
:param distance: the distance threshold for inclusion in the cavity definition
:param whole_residues: expand any included atom to include all atoms of that protein residue
"""
Protein.BindingSiteFromMolecule.__init__(self, protein, ligand, distance, whole_residues=whole_residues)
Docker.Settings.BindingSite.__init__(self)
def _to_settings(self, settings):
super(self.__class__, self)._to_settings(settings)
if settings._settings.cavity_file() and settings._settings.cavity_file().endswith('.mol2'):
file_name = os.path.join(os.path.dirname(settings.conf_file),
os.path.basename(settings._settings.cavity_file()))
settings._settings.set_cavity_file(file_name)
else:
file_name = os.path.join(os.path.dirname(settings.conf_file),
'cavity_%s.mol2' % self.molecule.identifier.replace(':', '_'))
with io.MoleculeWriter(file_name) as writer:
writer.write(self.molecule)
settings._settings.set_cavity_file(file_name)
settings._settings.set_cavity_contact_distance(self.distance)
settings._settings.set_cavity_radius(self.distance)
settings._settings.set_use_whole_residues(self.whole_residues)
settings._settings.set_cavity_definition_mode(DockingLib.GoldConfFile.CAVITY_FROM_LIGAND)
@staticmethod
def _from_settings(settings):
file_name = os.path.join(settings.conf_file, settings._settings.cavity_file())
with io.MoleculeReader(file_name) as reader:
ligand = reader[0]
bs = Docker.Settings.BindingSiteFromLigand(
settings.proteins[0],
ligand,
settings._settings.cavity_contact_distance(),
settings._settings.use_whole_residues()
)
bs.detect_cavity = settings._settings.detect_cavity()
return bs
# Constraints
[docs] @nested_class('Docker.Settings')
class Constraint(object):
'''A docking constraint.
This is the base class for all constraint and restraint like objects that GOLD can
use in docking.
'''
def __init__(self, _constraint=None):
self._constraint = _constraint
@staticmethod
def _make_constraint(settings, _constraint, protein_file=None):
_class_map = dict(
distance=Docker.Settings.DistanceConstraint,
h_bond=Docker.Settings.HBondConstraint,
protein_h_bond=Docker.Settings.ProteinHBondConstraint,
substructure=Docker.Settings.SubstructureConstraint,
similarity=Docker.Settings.TemplateSimilarityConstraint,
scaffold=Docker.Settings.ScaffoldMatchConstraint,
sphere=Docker.Settings.RegionConstraint,
pharmacophore=Docker.Settings.PharmacophoreConstraint
)
return _class_map[_constraint.type()]._from_string(settings, _constraint=_constraint,
protein_file=protein_file)
def _write_mol_files(self, settings):
pass
@staticmethod
def _is_protein_atom(atom):
for i in range(atom._molecule.natoms()):
a = atom._molecule.atom(i)
annos = a.annotations()
if annos and AnnotationsLib.has_ProteinSubstructureData(annos):
psd = annos.find_ProteinSubstructureData()
if not psd.heterogen():
return True
return False
[docs] class DistanceConstraint(Constraint):
r'''A distance constraint.
A distance constraint adds a quadratic penalty function into a docking that penalizes the score by
wd\ :sup:`2` where w is a weight, and d is the deviation of a given distance between a protein atom and a
ligand atom from the limits specified.
The constraint can be set so that all distances between topologically equivalant to those specified are
tested. In this case, only one distance needs to fall inside the specified bounds to avoid a penalty.
:param atom1, atom2: :class:`ccdc.molecule.Atom` instances. These may be either protein or ligand atoms.
:param limits: range of values with no penalty.
:param weight: spring constant
:param topological_equivalent: whether or not to accept a topologically equivalent atom
'''
def __init__(self, atom1, atom2, limits=(1.5, 3.5), weight=5.0, topological_equivalent=False,
_constraint=None):
self.atom1 = atom1
self.atom2 = atom2
self.limits = (min(limits), max(limits))
self.weight = weight
self.topological_equivalent = topological_equivalent
if _constraint is None:
_constraint = DockingLib.GoldDistanceConstraint()
_constraint.from_string(self._to_string())
super(self.__class__, self).__init__(_constraint)
if self._is_protein_atom(atom1):
self._add_to_protein = atom1._molecule
elif self._is_protein_atom(atom2):
self._add_to_protein = atom2._molecule
def _to_string(self):
s = '%s %d %s %d %.4f %.4f %.4f %s' % (
'protein' if self._is_protein_atom(self.atom1) else 'ligand',
self.atom1.index + 1,
'protein' if self._is_protein_atom(self.atom2) else 'ligand',
self.atom2.index + 1,
max(self.limits),
min(self.limits),
self.weight,
'on' if self.topological_equivalent else 'off'
)
return s
@staticmethod
def _from_string(settings, _constraint, protein_file=None):
parts = _constraint.to_string().split()
if parts[0] == 'protein':
atom1 = settings.proteins[0].atoms[int(parts[1]) - 1]
else:
atom1 = settings.ligands[0].atoms[int(parts[1]) - 1]
if parts[2] == 'protein':
atom2 = settings.proteins[0].atoms[int(parts[3]) - 1]
else:
atom2 = settings.ligands[0].atoms[int(parts[3]) - 1]
limits = (float(parts[4]), float(parts[5]))
weight = float(parts[6])
topological_equivalent = parts[7] == 'on'
return Docker.Settings.DistanceConstraint(
atom1, atom2, limits, weight, topological_equivalent, _constraint
)
[docs] class HBondConstraint(Constraint):
'''An HBond constraint.
An HBond constraint expresses that a given hydrogen bond should be formed between a protein and a ligand atom.
In this case, this means that the respective scoring function's score for an H-Bond must be attractive for
this particular H-Bond. The atoms involved are also always forced to map to one another in the docking mapping
algorithm.
:param atom1, atom2: :class:`ccdc.molecule.Atom` instances.
One of the atoms should be a donatable hydrogen, the other a HBond acceptor atom. One atom should be on
the protein, the other on a ligand.
'''
def __init__(self, atom1, atom2, _constraint=None):
if self._is_protein_atom(atom1):
self._add_to_protein = atom1._molecule
if self._is_protein_atom(atom2):
raise RuntimeError('HBond must be between a ligand atom and a protein atom')
self.atom1 = atom2
self.atom2 = atom1
elif not self._is_protein_atom(atom2):
raise RuntimeError('HBond must be between a ligand atom and a protein atom')
else:
self._add_to_protein = atom2._molecule
self.atom1 = atom1
self.atom2 = atom2
if self.atom1 is not None and self.atom2 is not None:
if not (
(self.atom1.atomic_number == 1 and self.atom1.neighbours[
0].is_donor and self.atom2.is_acceptor) or
(self.atom1.is_acceptor and self.atom2.atomic_number == 1 and self.atom2.neighbours[
0].is_donor)
):
raise RuntimeError('The specified atoms do not form an HBond')
if _constraint is None:
_constraint = DockingLib.GoldHBondConstraint()
_constraint.from_string(self._to_string())
super(self.__class__, self).__init__(_constraint)
def _to_string(self):
s = '%s %d %s %d' % (
'protein' if self._is_protein_atom(self.atom1) else 'ligand',
self.atom1.index + 1,
'protein' if self._is_protein_atom(self.atom2) else 'ligand',
self.atom2.index + 1
)
return s
@staticmethod
def _from_string(settings, _constraint, protein_file=None):
parts = _constraint.to_string().split()
if parts[0] == 'protein':
atom1 = settings.proteins[0].atoms[int(parts[1]) - 1]
else:
atom1 = settings.ligands[0].atoms[int(parts[1]) - 1]
if parts[2] == 'protein':
atom2 = settings.proteins[0].atoms[int(parts[3]) - 1]
else:
atom2 = settings.ligands[0].atoms[int(parts[3]) - 1]
return Docker.Settings.HBondConstraint(atom1, atom2, _constraint)
[docs] class ProteinHBondConstraint(Constraint):
'''A Protein HBond constraint.
This constraint forces specfic protein donors or acceptors to form a hydrogen bond if possible to at least one
ligand atom. Unlike the :class:`ccdc.docking.Docker.Settings.HBondConstraint` this constraint only requires
the user to specify atoms in the protein; any complementary atom in the ligand will be considered.
:param atoms: a list :class:`ccdc.molecule.Atom` instances from the protein. The atoms should be donatable hydrogens or acceptor atoms.
:param weight: the penalty to be applied for no atom of the list forming an HBond.
:param min_hbond_score: the minimal score of an HBond to be considered a valid HBond.
'''
def __init__(self, atoms, weight=10.0, min_hbond_score=0.005, _constraint=None):
self.atoms = atoms[:]
for a in self.atoms:
if not self._is_protein_atom(a):
raise RuntimeError('One of the atoms is not in the protein')
if (
not (a.atomic_number == 1 and a.neighbours[0].is_donor) and
not (a.is_acceptor)
):
raise RuntimeError('One of the atoms does not form an HBond')
self._add_to_protein = atoms[0]._molecule
self.weight = weight
self.min_hbond_score = min_hbond_score
if _constraint is None:
_constraint = DockingLib.GoldProteinHBondConstraint()
_constraint.from_string(self._to_string())
super(self.__class__, self).__init__(_constraint)
def _to_string(self):
s = '%.4f %.4f %s' % (
self.weight, self.min_hbond_score, ' '.join(str(a.index + 1) for a in self.atoms)
)
return s
@staticmethod
def _from_string(settings, _constraint, protein_file=None):
parts = _constraint.to_string().split()
weight = float(parts[0])
min_hbond_score = float(parts[1])
# Can't get from settings.proteins since they read the constraints
file_name = os.path.join(os.path.dirname(settings.conf_file), protein_file)
prot = Protein.from_file(os.path.join(os.path.dirname(settings.conf_file), protein_file))
pats = prot.atoms
ats = [pats[int(i) - 1] for i in parts[2:]]
return Docker.Settings.ProteinHBondConstraint(ats, weight, min_hbond_score)
[docs] class SubstructureConstraint(Constraint):
'''A Substructure constraint.
This constraint allows a user to express a substructure to match onto a ligand and then express a distance constraint with respect to an atom in that substructure.
Note that a ligand will still be docked if it does not contain the substructure. If you wish
to skip such ligands, the setting :attr:`ccdc.docking.Docker.Settings.force_constraints` must be set to True
:param protein_atom: an atom in the protein
:param substructure: a :class:`ccdc.search.QuerySubstructure` object
:param substructure_atom: an atom in the substructure
:param limits: a tuple of size 2 that expresses the maximum and minimum allowed distances
'''
def __init__(self, protein_atom, substructure, substructure_atom, limits, weight=5.0, use_ring_centre=True,
substructure_file_name=None, _constraint=None):
if not self._is_protein_atom(protein_atom):
raise RuntimeError('Atom %s is not a protein atom' % protein_atom)
if substructure_atom not in substructure.atoms:
raise RuntimeError('Atom %s is not in the substructure' % substructure_atom)
self._add_to_protein = protein_atom._molecule
self.protein_atom = protein_atom
self.substructure = substructure
self.substructure_atom = substructure_atom
self.limits = limits
self.weight = weight
self.use_ring_centre = use_ring_centre
if substructure_file_name is None:
substructure_file_name = 'substructure_' + substructure.identifier.replace(':', '_') + '.mol2'
self.substructure_file_name = substructure_file_name
if _constraint is None:
_constraint = DockingLib.GoldSubstructureConstraint()
_constraint.from_string(self._to_string())
_constraint.substructure = substructure
super(self.__class__, self).__init__(_constraint)
def _to_string(self):
return 'protein %d %s %d %.4f %.4f %.4f %s' % (
self.protein_atom.index + 1,
self.substructure_file_name,
self.substructure_atom.index + 1,
max(self.limits),
min(self.limits),
self.weight,
'ring_center' if self.use_ring_centre else ''
)
@staticmethod
def _from_string(settings, _constraint, protein_file=None):
parts = _constraint.to_string().split()
protein_atom = settings.proteins[0].atoms[int(parts[1]) - 1]
if parts[-1] == 'ring_center':
use_ring_centre = True
last = -2
else:
use_ring_centre = False
last = -1
weight = float(parts[last])
limits = (float(parts[last - 1]), float(parts[last - 2]))
sub_index = int(parts[last - 3])
substructure_file_name = ' '.join(parts[2:last - 3])
substructure_file_name = os.path.join(os.path.dirname(settings.conf_file), substructure_file_name)
if os.path.exists(substructure_file_name):
substructure = io.MoleculeReader(substructure_file_name)[0]
else:
substructure = _constraint.substructure
substructure_atom = substructure.atoms[sub_index - 1]
return Docker.Settings.SubstructureConstraint(
protein_atom, substructure, substructure_atom, limits, weight, use_ring_centre,
substructure_file_name, _constraint
)
def _write_mol_files(self, settings):
self.substructure_file_name = os.path.abspath(
os.path.join(os.path.dirname(settings.conf_file), os.path.basename(self.substructure_file_name)))
with io.MoleculeWriter(self.substructure_file_name) as writer:
writer.write(self.substructure)
[docs] class TemplateSimilarityConstraint(Constraint):
'''A template similarity constraint.
This is a fuzzy score reward; on docking the overlap of atoms in the docked pose is measured using the overlap
of gaussians positioned at atoms in a template with atoms in the pose. The types of atoms assessed can be donor
atoms, acceptor atoms or all atoms. The gaussian overlap is assessed and normalised into a range of 0->1; this
is then multiplied by a weight to provide a score to add to the docking score as an overlap regard.
:param type: must be 'donor', 'acceptor' or 'all'
:param template: a :class:`ccdc.molecule.Molecule` instance
:param template_file_name: where the template may be written. If not provided, the identifier of the template will be used.
:param weight: the maximum weight to be given in the event of an exact match with the template.
'''
def __init__(self, type, template, weight=5.0, template_file_name=None, _constraint=None):
self.type = type.lower()
if self.type not in ['donor', 'acceptor', 'all']:
raise RuntimeError('Invalid type %s for TemplateSimilarityConstraint' % self.type)
self.template = template
if template_file_name is None:
template_file_name = template.identifier + '.mol2'
self.template_file_name = template_file_name
self.weight = weight
if _constraint is None:
_constraint = DockingLib.GoldTemplateSimilarityConstraint()
_constraint.from_string(self._to_string())
_constraint.template = template
super(self.__class__, self).__init__(_constraint)
def _to_string(self):
s = '%s %s %.4f' % (
self.type, self.template_file_name, self.weight
)
return s
@staticmethod
def _from_string(settings, _constraint, protein_file=None):
parts = _constraint.to_string().split()
type = parts[0]
weight = float(parts[-1])
template_file_name = ' '.join(parts[1:-1])
template_file_name = os.path.join(os.path.dirname(settings.conf_file), template_file_name)
if os.path.exists(template_file_name):
template = io.MoleculeReader(template_file_name)[0]
else:
template = _constraint.template
return Docker.Settings.TemplateSimilarityConstraint(
type, template, weight, template_file_name, _constraint
)
def _write_mol_files(self, settings):
self.template_file_name = os.path.abspath(
os.path.join(os.path.dirname(settings.conf_file), os.path.basename(self.template_file_name)))
with io.MoleculeWriter(self.template_file_name) as writer:
writer.write(self.template)
[docs] class ScaffoldMatchConstraint(Constraint):
'''A scaffold match constraint.
A scaffold match constraint will attempt to force a substructure in an input ligand to match onto a template
provided by the user. The template is substructure matched onto the ligand and then a distance constraint for
each matching atom pair is added.
:param molecule: a :class:`ccdc.molecule.Molecule` instance
:param weight: a spring constant
:param atoms: a list of :class:`ccdc.molecule.Atom` instances
'''
def __init__(self, molecule, weight=5.0, atoms=None, _constraint=None):
self.molecule = molecule
self.weight = weight
self.atoms = atoms
if _constraint is not None:
self.file_name = _constraint.file_name
else:
self.file_name = 'scaffold_%s.mol2' % self.molecule.identifier.replace(':', '_')
if _constraint is None:
_constraint = DockingLib.GoldScaffoldMatchConstraint()
_constraint.from_string(self._to_string())
_constraint._molecule = molecule
super(self.__class__, self).__init__(_constraint)
@staticmethod
def _from_string(settings, _constraint, protein_file=None):
s = _constraint.to_string()
if 'list' in s:
indexes = s[s.index('list'):].split()[1:]
s = s[:s.index('list')]
else:
indexes = None
bits = s.split()
weight = float(bits[-1])
file_name = ' '.join(bits[:-1])
file_name = os.path.join(os.path.dirname(settings.conf_file), file_name)
_constraint.file_name = file_name
if os.path.exists(file_name):
with io.MoleculeReader(file_name) as reader:
molecule = reader[0]
if indexes:
atoms = [molecule.atoms[int(i) - 1] for i in indexes]
else:
atoms = None
else:
molecule = None
atoms = None
return Docker.Settings.ScaffoldMatchConstraint(molecule, weight, atoms, _constraint)
def _to_string(self):
if self.atoms is not None:
ats = ' list %s' % ' '.join('%d' % a.index for a in self.atoms)
else:
ats = ''
s = '%s %.4f%s' % (self.file_name, self.weight, ats)
return s
def _write_mol_files(self, settings):
self.file_name = os.path.abspath(
os.path.join(os.path.dirname(settings.conf_file), os.path.basename(self.file_name)))
with io.MoleculeWriter(self.file_name) as writer:
writer.write(self.molecule)
[docs] class RegionConstraint(Constraint):
'''A region constraint.
A region constraint is a coarse method for ensuring that either aromatic atoms, hydrophobic atoms or atoms
indexes explicitly listed (these will be matched by index in the input ligands) occupy a particular spherical
region in the binding site. Some of the functionality provided in this constraint is superceded by
functionality in :class:`ccdc.docking.Docker.Settings.PharmacophoreConstraint`
For the spherical binding site region, atoms should be specified using a list of :class:`ccdc.molecule.Atom`
instances. Alternatively, it is possible to use either all hydrophobic ligand atoms, or to use only those
hydrophobic atoms in aromatic rings by setting the optional ‘type’ parameter to 'hydrophobic' or
'arom_ring_atoms' respectively.
:param origin: a point in space
:param radius: a radius of the sphere around the origin - this defines the sphere where a docked pose should have the desired atoms
:param type: Must be one of 'hydrophobic atoms', 'arom_ring_atoms', ''None'' (default)
:param weight: the value to add to the score if the constraint is not fulfilled
:param atoms: a list of :class:`ccdc.molecule.Atom` instances whose indexes will be extracted
'''
def __init__(self, origin, radius, type, weight=5.0, atoms=None, _constraint=None):
self.origin = origin
if radius <= 0.0:
raise RuntimeError('Invalid radius for RegionConstraint')
self.radius = radius
type = type.lower()
if type.startswith('arom'):
self.type = 'aromatic'
self._type = 'arom_ring_atoms'
if atoms is not None:
raise RuntimeError(
'Atoms should not be provided if the type of a RegionConstraint is to be aromatic')
elif type.startswith('hydro'):
self.type = 'hydrophobic'
self._type = 'hydrophobic_atoms'
if atoms is not None:
raise RuntimeError(
'Atoms should not be provided if the type of a RegionConstraint is to be hydrophobic')
else:
self.type = 'explicit'
self._type = 'list'
if atoms is None:
raise RuntimeError('Atoms must be provided if the type of a RegionConstraint is to be explicit')
self.atoms = atoms
self.weight = weight
if _constraint is None:
_constraint = DockingLib.GoldRegionConstraint()
_constraint.from_string(self._to_string())
super(self.__class__, self).__init__(_constraint)
def _to_string(self):
s = '%.4f %.4f %.4f %.4f %.4f %s ' % (
self.origin[0], self.origin[1], self.origin[2], self.radius, self.weight, self._type
)
if self.atoms is not None:
s += ' '.join(str(a.index + 1) for a in self.atoms)
return s
@staticmethod
def _from_string(settings, _constraint, protein_file=None):
parts = _constraint.to_string().split()
x = float(parts[0])
y = float(parts[1])
z = float(parts[2])
r = float(parts[3])
weight = float(parts[4])
if parts[5] == 'list':
type = 'list'
atom_ids = [int(i) for i in parts[6:]]
l = settings.ligands[0]
atoms = [l.atoms[i - 1] for i in atom_ids]
else:
type = parts[5]
atoms = None
return Docker.Settings.RegionConstraint(
(x, y, z), r, type, weight, atoms, _constraint
)
[docs] class PharmacophoreConstraint(Constraint):
''' A pharmacophore constraint.
A PharmacophoreConstraint is in practice a docking reward: poses that match the constraint are rewarded with
a contribution to the score. They are formulated as a point, a tolerance
and then an overlay function with a given function shape.
You can also optionally use them to add fitting points to the docking, which will make poses that match
the pharmacophore more likely. If, in a pose, an atom or ring center of the proscribed type is within
the distance tolerance specified, the score of the pose will be increased by the value of the
score weight. If outside, the behaviour will vary depending on the function shape.
A gaussian function shape will make the contribution to the score decay based on a gaussian with the excess
distance. A block function shape will mean an atom outside the distance will contribute nothing to the score.
:param origin: a point in space where the pharamcophore point will be placed
:param type: the type of pharmacophore point. Must be one of 'acceptor', 'donor_acceptor', 'donor', 'aromatic_center' or 'ring_center'
:param radius: the distance a point in the pose can be away from the origin to be accepted as a match for the pharmacophore
:param function_shape: how to score the match (can be gaussian or block.)
:param use_as_fitting_point: whether to use the point additionally as a fitting point
:param weight: the reward a perfect match to the pharmacophore makes in the score
:param fitting_point_weight: the weight given to the associated fitting point
'''
def __init__(self, origin, type, radius, function_shape='block', use_as_fitting_point=True, weight=10.0,
fitting_point_weight=1.0, _constraint=None):
if len(origin) != 3:
raise RuntimeError('Invalid point coordinate %s', str(origin))
if radius <= 0.0:
raise RuntimeError(
'Invalid radius for PharmacophoreConstraint %.4f : must be greater than zero' % radius)
if weight <= 0.0:
raise RuntimeError(
'Invalid weight for PharmacophoreConstraint %.4f : must be greater than zero' % weight)
if fitting_point_weight <= 0.0:
raise RuntimeError(
'Invalid fitting point weight for PharmacophoreConstraint %.4f : must be greater than zero' % fitting_point_weight)
try:
self._conf_type(type.lower())
except KeyError:
raise RuntimeError('Invalid pharmacophore point type %s - must be one of %s' % (
type, ' '.join([key for key in self._TypeLookup]))
)
try:
self._conf_function_shape(function_shape.lower())
except KeyError:
raise RuntimeError('Invalid function shape %s - must be one of %s' % (
function_shape, ' '.join([key for key in self._FunctionShapeLookup]))
)
self.origin = origin
self.radius = radius
self.type = type.lower()
self.function_shape = function_shape.lower()
self.use_as_fitting_point = use_as_fitting_point
self.weight = weight
self.fitting_point_weight = fitting_point_weight
if _constraint is None:
_constraint = DockingLib.GoldPointConstraint()
_constraint.from_string(self._to_string())
super(self.__class__, self).__init__(_constraint)
_TypeLookup = {
"acceptor": "ACC",
"donor_acceptor": "D_A",
"donor": "DON",
"aromatic_center": "ARO_CNT",
"aromatic_centre": "ARO_CNT",
"ring_center": "RING_CNT",
"ring_centre": "RING_CNT"
}
_FunctionShapeLookup = {
"gaussian": "GAUSS",
"block": "BLOCK"
}
@classmethod
def _conf_type(cls, api_type):
return cls._TypeLookup[api_type]
@classmethod
def _api_type(cls, conf_type):
for x in cls._TypeLookup:
if conf_type == cls._TypeLookup[x]:
return x
raise RuntimeError("No such type supported in GOLD configuration file")
@classmethod
def _conf_function_shape(cls, api_function_shape):
return cls._FunctionShapeLookup[api_function_shape]
@classmethod
def _api_function_shape(cls, conf_function_shape):
for x in cls._FunctionShapeLookup:
if conf_function_shape == cls._FunctionShapeLookup[x]:
return x
raise RuntimeError("No such overlay function supported in GOLD configuration file")
def _to_string(self):
s = '%s %s %.4f %.4f %.4f %d %.4f %.4f %.4f ' % (
self._conf_type(self.type), self._conf_function_shape(self.function_shape), self.origin[0],
self.origin[1], self.origin[2], self.use_as_fitting_point, self.radius, self.weight,
self.fitting_point_weight
)
return s
@staticmethod
def _from_string(settings, _constraint, protein_file=None):
parts = _constraint.to_string().split()
type = Docker.Settings.PharmacophoreConstraint._api_type(parts[0])
function_shape = Docker.Settings.PharmacophoreConstraint._api_function_shape(parts[1])
x = float(parts[2])
y = float(parts[3])
z = float(parts[4])
use_as_fitting_point = bool(int(parts[5]))
radius = float(parts[6])
weight = float(parts[7])
fitting_point_weight = float(parts[8])
return Docker.Settings.PharmacophoreConstraint(
(x, y, z), type, radius, function_shape, use_as_fitting_point, weight, fitting_point_weight,
_constraint
)
@property
def constraints(self):
'''The tuple of constraints set.'''
if not self._constraints:
self._constraints = [
Docker.Settings.Constraint._make_constraint(self, self._settings.constraint(i))
for i in range(self._settings.nconstraints())
]
for pinfo in self.protein_files:
self._constraints.extend(pinfo.constraints)
return self._constraints
[docs] def clear_constraints(self):
'''Clears the set of constraints.'''
self._constraints = []
self._settings.clear_constraints()
for pfinfo in self.protein_files:
pfinfo._protein_data.clear_constraints()
[docs] def add_constraint(self, constraint):
'''Add a constraint to the docking.
:param constraint: :class:`ccdc.docking.Docker.Settings.Constraint` instance.
'''
if hasattr(constraint, '_add_to_protein'):
for p, inf in zip(self.proteins, self.protein_files):
if p._molecule == constraint._add_to_protein:
inf.add_constraint(constraint)
break
else:
raise RuntimeError('Cannot find appropriate protein.')
else:
self._settings.add_constraint(constraint._constraint)
self._constraints.append(constraint)
@property
def force_constraints(self):
'''Whether the constraints are to be forced.'''
return self._settings.force_constraints()
@force_constraints.setter
def force_constraints(self, tf):
self._settings.set_force_constraints(tf)
# Rotamer Angle Range
[docs] @nested_class('Docker.Settings')
class RotamerAngleRange():
'''A rotamer angle range.
:param angle_range: a tuple representing the angle range
The tuple must include one, two or three elements, which represent
the following cases respectively:
1. The angle is set, the deltas are set to 0
2. The angle is set with the first element, the deltas are set with the second element
3. The angle is set with the first element, the lower delta is set with the second element, the upper delta is set with the third element
'''
def __init__(self, angle_range=(0,)):
'''Create a rotamer angle range
:param angle_range: A tuple representing the angle range
'''
if len(angle_range) > 3:
raise ValueError('Expect a tuple of length 3 or less')
self._angle_range = DockingLib.RotamerAngleRange(*angle_range)
def __rep__(self):
return f'({self.angle},{self.min_delta},{self.max_delta})'
__str__ = __rep__
@property
def angle(self):
'''Get and set the angle (in degrees)'''
return self._angle_range.angle()
@angle.setter
def angle(self, angle):
self._angle_range.set_angle(angle)
@property
def min_delta(self):
'''Get and set the lower delta (in degrees)'''
return self._angle_range.min_delta()
@min_delta.setter
def min_delta(self, min_delta):
self._angle_range.set_deltas(min_delta, self._angle_range.max_delta())
@property
def max_delta(self):
'''Get and set the upper delta (in degrees)'''
return self._angle_range.max_delta()
@max_delta.setter
def max_delta(self, max_delta):
self._angle_range.set_deltas(self._angle_range.min_delta(), max_delta)
# Rotamer
[docs] @nested_class('Docker.Settings')
class Rotamer():
'''A rotamer in a rotamer library.
:param num_torsions: the number of torsions in the residue
The torsions can be accessed or set via ``chi<n>`` attributes.
For examples::
rotamer = Docker.Settings.Rotamer(3)
rotamer.energy = 10
rotamer.chi1 = (-60,)
rotamer.chi2 = (-75, 10)
rotamer.chi3 = (-90, 10, 20)
assert(rotamer.chi1.angle, -60)
assert(rotamer.chi2.min_delta, 10)
assert(rotamer.chi3.max_delta, 20)
'''
def __init__(self, num_torsions, rotamer=None):
'''Create a rotamer for a residue
'''
if num_torsions > 9:
raise ValueError('Invalid number of angles')
self._num_torsions = num_torsions
# This dictionary stores the mapping of chi_n's to the
# RotamerAngleRange objects.
self._chis = {}
if rotamer is not None:
# Initiate from an existing internal rotamer object
self._rotamer = rotamer
for index in range(self._num_torsions):
ar = rotamer.angle(index)
self._chis[f'chi{index + 1}'] = Docker.Settings.RotamerAngleRange(
(ar.angle(), ar.min_delta(), ar.max_delta()))
else:
# Initiate a default (0,0,0) rotamer
self._rotamer = DockingLib.Rotamer()
for index in range(self._num_torsions):
self._chis[f'chi{index + 1}'] = Docker.Settings.RotamerAngleRange()
self._rotamer.insert(index, self._chis[f'chi{index + 1}']._angle_range)
def __getattribute__(self, attr):
if attr.startswith('chi'):
if attr not in self._chis:
raise AttributeError
else:
return self._chis[attr]
return object.__getattribute__(self, attr)
def __setattr__(self, attr, value):
if attr.startswith('chi'):
if attr not in self._chis:
raise AttributeError
else:
self._chis[attr] = Docker.Settings.RotamerAngleRange(value)
self._rotamer.set_angle(int(attr[3]) - 1,
self._chis[attr]._angle_range)
return super().__setattr__(attr, value)
@property
def improper(self):
'''Get or set whether the rotamer is improper'''
return self._rotamer.improper()
@improper.setter
def improper(self, is_improper):
self._rotamer.set_improper(is_improper)
@property
def improper_angle(self):
'''Get or set the improper angle
If setting the improper angle, the tuple must include one, two
or three elements, which represent the following cases
respectively:
1. The angle is set, the deltas are set to 0
2. The angle is set with the first element, the deltas are set with the second element
3. The angle is set with the first element, the lower delta is set with the second element, the upper delta is set with the third element
:returns: a :class:`ccdc.docking.Docker.Settings.RotamerAngleRange` object
:raises: RuntimeError if rotamer is not an improper rotamer
'''
if self.improper:
ia = self._rotamer.improper_angle()
return Docker.Settings.RotamerAngleRange(
(ia.angle(), ia.min_delta(), ia.max_delta())
)
else:
raise RuntimeError('Not an improper rotamer')
@improper_angle.setter
def improper_angle(self, improper_angle=(0, 10)):
if self.improper:
ia_range = DockingLib.RotamerAngleRange(*improper_angle)
return self._rotamer.set_improper_angle(ia_range)
else:
raise RuntimeError('Not an improper rotamer')
@property
def energy(self):
'''Get or set the rotamer energy'''
return self._rotamer.energy()
@energy.setter
def energy(self, energy):
self._rotamer.set_energy(energy)
# Rotamer Library
[docs] @nested_class('Docker.Settings')
class RotamerLibrary():
'''A rotamber library associated with a protein binding site.
A binding site should have been configured because a residue from
the list of binding site residues is required to create this.
:param protein_file_info: the :class:`Docker.Settings.ProteinFileInfo` object the rotamer library is associated with
:param residue: the residue of the binding site the rotamer library is associated with
'''
def __init__(self, protein_file_info, residue, _rotamer_library=None):
'''Create a rotamer library for a residue of the binding site
'''
self.DEFAULT_LIBRARY = Docker.Settings._path_in_distribution("rotamer_library.txt")
self._protein_file_info = protein_file_info
self._residue = residue
self._rotamer_library = _rotamer_library
self._improper = False
if self._rotamer_library is not None:
self._improper = self._rotamer_library.improper()
# translate the AA three letter code to the FileFormatsLib.AminoAcid.Type enum e.g. AminoAcid::GLU
self._residue_type = FileFormatsLib.AminoAcid.amino_acid_type(self._residue.three_letter_code)
self._num_torsions = FileFormatsLib.num_residue_torsion_atoms(residue._residue) - 3
# Get the improper angle
improper_atoms = FileFormatsLib.improper_torsion_atoms(self._residue._residue)
indices = []
for atom in improper_atoms:
indices.append(atom.annotations().obtain_FileOrdering().file_order())
self.rotamer_angle = DockingLib.RotamerAngle("Improper", *indices)
self.improper_angle = FileFormatsLib.improper_torsion_angle_degree(self._residue._residue)
@staticmethod
def _make_rotamer_library(settings, protein, protein_file_info, _rotamer_library):
# This contains the residue name
rotamer_library_name = [_rotamer_library.name()]
# A cleaned up version of the residue name
rotamer_library_name.append(rotamer_library_name[0].replace('_', '').upper())
# Use the residue atoms to create binding site
residue_atom_indices = set()
for angle in _rotamer_library.angles():
residue_atom_indices.update([angle.index(ii) for ii in range(4)])
atoms = [protein.atoms[i] for i in residue_atom_indices]
binding_site = Docker.Settings.BindingSiteFromListOfAtoms(protein, atoms)
# Cycle through the binding site reisdues to find the one
# that matche the rotamer library name
residue = None
for rr in binding_site.residues:
if any([rr.identifier.endswith(rln) for rln in rotamer_library_name]):
residue = rr
break
if residue is not None:
return Docker.Settings.RotamerLibrary(protein_file_info, residue, _rotamer_library)
else:
return None
def __len__(self):
if self._rotamer_library is not None:
return self._rotamer_library.n_rotamers()
else:
return 0
@property
def number_of_torsions(self):
'''Number of torsion angles of the associated residue'''
return self._num_torsions
@property
def improper(self):
return self._improper
def _reset_improper(self):
if self._rotamer_library != None:
if self._rotamer_library.n_angles() <= 1:
self._rotamer_library = None
else:
self._rotamer_library.remove_improper()
[docs] def set_improper(self, is_improper, deltas=(10,)):
'''To set the improper status of the rotamer library
:param is_improper: a boolean for the improper state
:param deltas: a tuple of 1 or 2 to define the rotamer angle ranges for improper angle
'''
if self.improper == is_improper and is_improper == False:
return
if self.improper == is_improper and is_improper == True:
self._reset_improper()
self._improper = is_improper
if is_improper == True:
# If the rotamer_libary is empty, create a crystal rotamer library
if self._rotamer_library == None:
# Get torsions
torsions = FileFormatsLib.residue_torsions(self._residue._residue)
# Create a library with the crystal structure
self._rotamer_library = DockingLib.RotamerLibrary(self._residue._residue, torsions, 0)
rotamer_angle_range = DockingLib.RotamerAngleRange(self.improper_angle, *deltas)
# Update the rotamer library
self._rotamer_library.insert_improper(self.rotamer_angle, rotamer_angle_range)
else:
self._reset_improper()
# Update the protein data accordingly
protein_data = self._protein_file_info._protein_data
rls = protein_data.rotamer_libraries()
protein_data.clear_rotamer_libraries()
for rl in rls:
if rl.name() == self._rotamer_library.name():
protein_data.add_rotamer_library(self._rotamer_library)
else:
protein_data.add_rotamer_library(rl)
[docs] def add_default_rotamers(self, deltas=(10,)):
'''Add default rotamers for this residue
The rotamers are read from rotamer_library.txt in the distribution.
This is equivalent to the "Library" button in the Hermes GUI.
:param deltas: the angle range to be set for improper angle, if applicable
'''
default_library = DockingLib.GoldConfProteinData.read_rotamer_library(self.DEFAULT_LIBRARY,
self._residue_type)
if self._rotamer_library == None:
self._rotamer_library = DockingLib.RotamerLibrary(self._residue._residue, default_library)
else:
if self._improper:
rotamer_angle_range = DockingLib.RotamerAngleRange(self.improper_angle, *deltas)
default_library.insert_improper(self.rotamer_angle, rotamer_angle_range)
rotamers = default_library.rotamers()
for r in rotamers:
self._rotamer_library.add_rotamer(r)
[docs] def add_crystal_rotamer(self, delta=10):
'''Add a rotamer corresponding to the residue in the crystal
This is equivalent to the "Crystal" button in the Hermes GUI.
:param delta: the tolerance of the crystal rotamer angle, and also for improper angle
'''
# Get torsions
torsions = FileFormatsLib.residue_torsions(self._residue._residue)
# Create a library with the crystal structure
library_with_crystal = DockingLib.RotamerLibrary(self._residue._residue, torsions,
delta)
if self._improper:
rotamer_angle_range = DockingLib.RotamerAngleRange(self.improper_angle, delta)
library_with_crystal.insert_improper(self.rotamer_angle, rotamer_angle_range)
if self._rotamer_library != None:
rotamers = self._rotamer_library.rotamers()
for r in rotamers:
library_with_crystal.add_rotamer(r)
self._rotamer_library = library_with_crystal
[docs] def add_free_rotamer(self, deltas=(10,)):
'''Add a free (delta=180) rotamer
This is equivalent to the "Free" button in the Hermes GUI.
Doing this will remove all existing rotamers in this library.
:param deltas: the angle range to be set for improper angle, if applicable
'''
self._rotamer_library = DockingLib.RotamerLibrary(self._residue._residue)
if self._improper:
rotamer_angle_range = DockingLib.RotamerAngleRange(self.improper_angle, *deltas)
self._rotamer_library.insert_improper(self.rotamer_angle, rotamer_angle_range)
[docs] def add_rotamer(self, rotamer, angle=0, deltas=(10,)):
'''Add a user-defined rotamer
This is equivalent to the "From dials" button in the Hermes GUI.
:param rotamer: a :class:`ccdc.docking.Docker.Settings.Rotamer` object to add
:param angle: the improper angle to be set, if applicable
:param deltas: the angle range to be set for improper angle, if applicable
'''
if self._improper:
rotamer_angle_range = DockingLib.RotamerAngleRange(angle, *deltas)
rotamer._rotamer.insert_improper(rotamer_angle_range)
if self._rotamer_library == None:
self._rotamer_library = DockingLib.RotamerLibrary(self._residue._residue)
self._rotamer_library.set_rotamer(0, rotamer._rotamer)
else:
self._rotamer_library.add_rotamer(rotamer._rotamer)
[docs] def remove_rotamer(self, index):
'''Remove a rotamer from the library
:param index: the index of the rotamer to remove
:returns: the rotamer removed from the library
'''
rotamer = self.rotamers()[index]
DockingLib.remove_rotamer(self._rotamer_library, index)
return rotamer
[docs] def remove_rotamers(self):
'''Remove all rotamers
This is equivalent to the "Rigid" button in the Hermes GUI.
'''
self._rotamer_library = None
self._improper = False
[docs] def rotamers(self):
'''Return the list of rotamers from this rotamer library
Note that the returned objects are views of the rotamers in the
settings. Changing them does not change the settings.
:returns: list of :class:`ccdc.docking.Docker.Settings.Rotamer` objects
'''
return [Docker.Settings.Rotamer(self._num_torsions, r) for r in self._rotamer_library.rotamers()]
[docs] def rotamer_libraries(self, protein=None):
'''Get the set of defined rotamer libraries
If protein is None all rotamer libraries in the settings will be
returned. Otherise only rotamer libraries associated with the
protein will be returned.
:param protein: A :class:`ccdc.docking.Docker.Settings.Protein` instance
:returns: A list of :class:`ccdc.docking.Docker.Settings.RotamerLibrary objects
'''
if not hasattr(self, '_rotamer_info'):
_ = self.protein_files # in case the protein info hasnt been assigned yet
# Set up the rotamers
for index, p in enumerate(self._protein_info):
rotamer_libraries = p._protein_data.rotamer_libraries()
for rl in rotamer_libraries:
rotamerlibrary = Docker.Settings.RotamerLibrary._make_rotamer_library(
self, self.proteins[index], p, rl)
if rotamerlibrary:
p._rotamer_libraries.append(rotamerlibrary)
self._rotamer_info = True
rotamer_libraries = []
for prot, info in zip(self.proteins, self.protein_files):
if protein is None or prot == protein:
rotamer_libraries.extend(info.rotamer_libraries)
return rotamer_libraries
[docs] def rotamer_library(self, protein, residue_id):
'''Return the rotamer library for this residue in this protein
:param protein: A :class:`ccdc.docking.Docker.Settings.Protein` instance
:param residue_id: A residue ID string
:returns: A :class:`ccdc.docking.Docker.Settings.RotamerLibrary objects
'''
p_rotamer_libraries = self.rotamer_libraries(protein)
for rotamer_library in p_rotamer_libraries:
if rotamer_library._residue.identifier.endswith(residue_id):
return rotamer_library
return None
[docs] def add_rotamer_library(self, protein, rotamer_library):
'''Add a rotamer library to a protein
:param protein: A :class:`ccdc.docking.Docker.Settings.Protein` instance
:param rotamer_library: A :class:`ccdc.docking.Docker.Settings.RotamerLibrary` instance to add
:raises: ValueError if protein cannot be found
'''
for prot, info in zip(self.proteins, self.protein_files):
if prot == protein:
info.add_rotamer_library(rotamer_library)
break
else:
raise ValueError('Cannot find appropriate protein.')
[docs] def update_rotamer_library(self, protein, rotamer_library):
'''Update a rotamer library to a protein
The name of the rotamer library is used to determine which existing
library to update.
:param protein: A :class:`ccdc.docking.Docker.Settings.Protein` instance
:param rotamer_library: A :class:`ccdc.docking.Docker.Settings.RotamerLibrary` instance to update
:raises: ValueError if protein cannot be found
'''
for prot, info in zip(self.proteins, self.protein_files):
if prot == protein:
info.update_rotamer_library(rotamer_library)
break
else:
raise ValueError('Cannot find appropriate protein.')
[docs] def clear_rotamer_libraries(self):
'''Remove all rotamer libraries'''
for pfinfo in self.protein_files:
pfinfo.clear_rotamer_libraries()
# Fitness function
@property
def fitness_function(self):
'''Which fitness function to use.
Options are 'goldscore', 'chemscore', 'asp', 'plp'. GoldScore is selected by default.
Note that if you pass in 'None' the API will assume you are planning a rescoring run, set
the fitness function to the rescore function, and set the run type as a rescore-only run. One other
side effect of this will be to disable :attr:`use_internal_ligand_energy_offset`
'''
return self._fitness_function
@fitness_function.setter
def fitness_function(self, value):
if not value:
self._fitness_function = ''
self._settings.set_gold_fitness_function_path(self._rescore_function)
self._settings.set_run_type(self._settings.RESCORE_RUN)
self._settings.set_use_relative_ligand_energy(False)
else:
value = value.lower()
if value in self._fitness_functions:
self._fitness_function = value
self._settings.set_gold_fitness_function_path(value)
if self._rescore_function:
self._settings.set_gold_fitness_function_path('consensus_score')
self._settings.set_docking_fitness_function_path(value)
self._settings.set_rescore_fitness_function_path(self._rescore_function)
self._settings.set_run_type(self._settings.CONSENSUS_SCORE)
else:
self._settings.set_run_type(self._settings.STANDARD_RUN)
else:
raise TypeError('%s is not a recognised fitness function' % value)
@property
def use_internal_ligand_energy_offset(self):
'''Get or set whether to apply a score offset in scoring that accounts for the lowest ligand energy observed
in the search (can be True or False)
Note that this only applies when running a full docking run. If you try to set this for a rescoring run,
you will get a ValueError raised, as rescoring runs do not have access to the previous GA sampling.
'''
return self._settings.use_relative_ligand_energy()
@use_internal_ligand_energy_offset.setter
def use_internal_ligand_energy_offset(self, value):
setting = bool(value)
if self._settings.run_type() == self._settings.RESCORE_RUN and setting:
raise ValueError("Rescore runs can not use an internal ligand energy offset")
self._settings.set_use_relative_ligand_energy(setting)
@property
def rescore_function(self):
'''The fitness function used for rescoring.
Should not be the same as the fitness function.
'''
return self._rescore_function
@rescore_function.setter
def rescore_function(self, value):
'''Set the rescore function.
Note that if you pass in 'None' the API will assume you are planning a standard docking run.
Setting a rescoring function without a pre-set fitness function will mean the API will run a rescore-only run.
One side effect of this will be to turn off :attr:`use_internal_ligand_energy_offset`
'''
if not value:
self._rescore_function = ''
self._settings.set_rescore_fitness_function_path('')
self._settings.set_run_type(DockingLib.GoldConfFile.STANDARD_RUN)
return
value = value.lower()
if value not in self._fitness_functions:
raise TypeError('%s is not a recognised fitness function' % value)
elif value == self.fitness_function:
raise TypeError('%s is the current fitness function' % value)
elif value == self._settings.docking_fitness_function_path():
raise TypeError('%s is the current docking function' % value)
else:
self._rescore_function = value
if self.fitness_function == '':
self._settings.set_gold_fitness_function_path(value)
self._settings.set_run_type(self._settings.RESCORE_RUN)
self._settings.set_use_relative_ligand_energy(False)
else:
self._settings.set_rescore_fitness_function_path(value)
self._settings.set_gold_fitness_function_path('consensus_score')
self._settings.set_docking_fitness_function_path(self._fitness_function)
self._settings.set_run_type(self._settings.CONSENSUS_SCORE)
@property
def score_parameter_file(self):
'''The location of an alternative score parameter file.
If set to a relative path the file will be found in the standard GOLD distribution.
If set to None, the DEFAULT file will be used.
'''
return self._settings.score_parameter_file()
@score_parameter_file.setter
def score_parameter_file(self, value):
if not value or value == 'DEFAULT':
self._settings.set_score_parameter_file('DEFAULT')
else:
if os.path.isabs(value):
file_name = value
else:
file_name = Docker.Settings._path_in_distribution(value)
if os.path.exists(file_name):
self._settings.set_score_parameter_file(file_name)
else:
raise RuntimeError('score_parameter_files: Cannot find path for %s' % value)
@property
def use_torsion_angle_distributions(self):
'''Set whether to use torsion distributions from a torsion distributions file in the genetic algorithm.
Can be set to True or False
:see: :attr:`torsion_distribution_file` for information on how to configure the distributions to use
'''
return self._settings.use_torsion_distribution()
@use_torsion_angle_distributions.setter
def use_torsion_angle_distributions(self, value):
self._settings.set_use_torsion_distribution(bool(value))
@property
def torsion_distribution_file(self):
'''The location of a torsion distribution file.
If set to a relative path, the file will be found in the standard GOLD distribution.
If set to None, the DEFAULT file will be used.
Note: setting this value will not turn using torsion distributions on or off. You can use
:attr:`use_torsion_angle_distributions` to switch this on or off
'''
return self._settings.torsion_distribution_file()
@torsion_distribution_file.setter
def torsion_distribution_file(self, value):
if not value or value == 'DEFAULT':
self._settings.set_torsion_distribution_file('DEFAULT')
else:
if os.path.isabs(value):
file_name = value
else:
file_name = Docker.Settings._path_in_distribution(value)
if os.path.exists(file_name):
self._settings.set_torsion_distribution_file(file_name)
else:
raise RuntimeError('torsion_distribution_file: Cannot find path for %s' % value)
@property
def rotatable_bond_override_file(self):
'''The location of a rotatable bond override file.
If set to a relative path, the file will be found in the standard GOLD distribution.
If set to None, the DEFAULT file will be used.
Note: setting this value will not turn using the rotatable bond override file on or off. You can use
:attr:`use_rotatable_bond_override_file` to switch this on or off
'''
return self._settings.rotatable_bond_override_file()
@rotatable_bond_override_file.setter
def rotatable_bond_override_file(self, value):
if not value or value == 'DEFAULT':
self._settings.set_rotatable_bond_override_file('DEFAULT')
else:
if os.path.isabs(value):
file_name = value
else:
file_name = Docker.Settings._path_in_distribution(value)
if os.path.exists(file_name):
self._settings.set_rotatable_bond_override_file(file_name)
else:
raise RuntimeError('rotatable_bond_override_file: Cannot find path for %s' % value)
@property
def use_rotatable_bond_override_file(self):
'''Set whether to use overrides from a rotatable bond override file in the genetic algorithm.
Can be set to True or False
:see: :attr:`rotatable_bond_override_file` for information on how to configure the file to use
'''
return self._settings.postprocess_bonds()
@use_rotatable_bond_override_file.setter
def use_rotatable_bond_override_file(self, value):
self._settings.set_postprocess_bonds(bool(value))
# GA parameters
@property
def autoscale(self):
'''The autoscale percentage, which controls how much searching is performed.
The docker will determine how much docking is reasonable to perform on a ligand based
on the number of rotatable bonds and the number of hydrogen donors and acceptors. This
percentage will scale the amount of docking done to perform faster or more thorough docking.
'''
return self._settings.autoscale()
@autoscale.setter
def autoscale(self, percent):
'''Set the autoscale factor.'''
self._settings.set_autoscale(int(percent))
# Termination options
@property
def early_termination(self):
'''Whether early termination is permitted.
If early termination is permitted this will be (True, number_of_solutions, rmsd_threshold),
if not this will be (False, None, None)'''
x = self._settings.use_early_termination()
if x:
return (
x, self._settings.early_termination_n_top_solutions(), self._settings.early_termination_rmsd()
)
else:
return (x, None, None)
@early_termination.setter
def early_termination(self, value):
try:
tf = bool(value[0])
except (ValueError, TypeError, IndexError):
if value:
self._settings.set_use_early_termination(True)
self._settings.set_early_termination_n_top_solutions(3)
self._settings.set_early_termination_rmsd(1.5)
else:
self._settings.set_use_early_termination(False)
else:
self._settings.set_use_early_termination(tf)
if tf:
try:
n = int(value[1])
except (ValueError, TypeError, IndexError):
self._settings.set_early_termination_n_top_solutions(3)
self._settings.set_early_termination_rmsd(1.5)
else:
self._settings.set_early_termination_n_top_solutions(n)
try:
f = float(value[2])
except (ValueError, TypeError, IndexError):
self._settings.set_early_termination_rmsd(1.5)
else:
self._settings.set_early_termination_rmsd(f)
@property
def write_options(self):
'''Determines which write options are set.
The options are:
* MIN_OUT: Use this to write only the gold.log and bestranking.lst files. This is the recommended option for high-throughput virtual screening
* NO_LOG_FILES: Use this to disable the writing of all ligand log files and the gold_protein.log file.
* NO_LINK_FILES: Use this to disable the writing of ranked pose shortcut files to solution files. By default, one file is written per solution file.
* NO_RNK_FILES: Use this to disable the writing of the ranked fitness lists (.rnk extension) for each molecule. By default, one file is written per ligand.
* NO_BESTRANKING_LST_FILE: Use this to disable the writing of the bestranking.lst file which includes a list of the highest scoring pose for each ligand.
* NO_GOLD_SOLN_LIGAND_MOL2_FILES: Use this to disable the writing of all solution files. As there would be nothing to point to, this option also disables the writing of the ranked pose shortcut files.
* NO_GOLD_LIGAND_MOL2_FILE: Use this to disable the writing of all ligand files. By default, one file is written per ligand.
* NO_GOLD_PROTEIN_MOL2_FILE: Use this to disable the writing of the protein file. By default, one file is written per target protein.
* NO_LGFNAME_FILE: Use this to disable the writing of the .lgfname file.
* NO_PLP_MOL2_FILES: If using the ChemPLP scoring function, use this to disable the writing of plp_ligand.mol2 and plp_protein.mol2.
* NO_PID_FILE: Use this to disable the writing of the gold.pid file.
* NO_SEED_LOG_FILE: Use this to disable the writing of the gold.seed_log file.
* NO_GOLD_ERR_FILE: Use this to disable the writing of the gold.err file.
* NO_FIT_PTS_FILES: Use this to disable the writing of all files related to fitting points including, but not limited to, fit_pts.mol2 and fit_pts_merged.mol2.
* NO_ASP_MOL2_FILES: If using the ASP scoring function, use this to disable the writing of asp_ligand.mol2 and asp_protein.mol2.
* NO_GOLD_LOG_FILE: Use this to disable the writing of gold.log.
Returns a list of enabled write options.
'''
current_write_options = []
if self._settings.no_gold_log_file():
current_write_options.append('NO_GOLD_LOG_FILE')
if self._settings.no_bestranking_lst_file():
current_write_options.append('NO_BESTRANKING_LST_FILE')
if self._settings.min_out():
current_write_options.append('MIN_OUT')
if self._settings.no_log_files():
current_write_options.append('NO_LOG_FILES')
if self._settings.no_link_files():
current_write_options.append('NO_LINK_FILES')
if self._settings.no_rnk_files():
current_write_options.append('NO_RNK_FILES')
if self._settings.no_gold_soln_ligand_mol2_files():
current_write_options.append('NO_GOLD_SOLN_LIGAND_MOL2_FILES')
if self._settings.no_gold_ligand_mol2_file():
current_write_options.append('NO_GOLD_LIGAND_MOL2_FILE')
if self._settings.no_gold_protein_mol2_file():
current_write_options.append('NO_GOLD_PROTEIN_MOL2_FILE')
if self._settings.no_lgfname_file():
current_write_options.append('NO_LGFNAME_FILE')
if self._settings.no_plp_mol2_files():
current_write_options.append('NO_PLP_MOL2_FILES')
if self._settings.no_pid_file():
current_write_options.append('NO_PID_FILE')
if self._settings.no_seed_log_file():
current_write_options.append('NO_SEED_LOG_FILE')
if self._settings.no_gold_err_file():
current_write_options.append('NO_GOLD_ERR_FILE')
if self._settings.no_fit_pts_files():
current_write_options.append('NO_FIT_PTS_FILES')
if self._settings.no_asp_mol2_files():
current_write_options.append('NO_ASP_MOL2_FILES')
return current_write_options
@write_options.setter
def write_options(self, value):
if isinstance(value, str):
value = value.upper()
else:
try:
value = ' '.join(v.upper() for v in value if isinstance(v, str))
except:
raise TypeError('write_options() requires an argument of type \'str\'.')
# refresh write options to default
self._settings.set_no_log_files(False)
self._settings.set_no_link_files(False)
self._settings.set_no_rnk_files(False)
self._settings.set_no_bestranking_lst_file(False)
self._settings.set_no_gold_soln_ligand_mol2_files(False)
self._settings.set_no_gold_ligand_mol2_file(False)
self._settings.set_no_gold_protein_mol2_file(False)
self._settings.set_no_lgfname_file(False)
self._settings.set_no_plp_mol2_files(False)
self._settings.set_no_pid_file(False)
self._settings.set_no_seed_log_file(False)
self._settings.set_no_gold_err_file(False)
self._settings.set_no_fit_pts_files(False)
self._settings.set_no_asp_mol2_files(False)
self._settings.set_no_gold_log_file(False)
self._settings.set_min_out(False)
if 'NO_GOLD_LOG_FILE' in value:
self._settings.set_no_gold_log_file(True)
if 'NO_BESTRANKING_LST_FILE' in value:
self._settings.set_no_bestranking_lst_file(True)
if 'MIN_OUT' in value or 'MINIMUM_OUTPUT' in value:
self._settings.set_min_out(True)
else:
if 'NO_LOG_FILES' in value:
self._settings.set_no_log_files(True)
if 'NO_LINK_FILES' in value:
self._settings.set_no_link_files(True)
if 'NO_RNK_FILES' in value:
self._settings.set_no_rnk_files(True)
if 'NO_GOLD_SOLN_LIGAND_MOL2_FILES' in value:
self._settings.set_no_gold_soln_ligand_mol2_files(True)
if 'NO_GOLD_LIGAND_MOL2_FILE' in value:
self._settings.set_no_gold_ligand_mol2_file(True)
if 'NO_GOLD_PROTEIN_MOL2_FILE' in value:
self._settings.set_no_gold_protein_mol2_file(True)
if 'NO_LGFNAME_FILE' in value:
self._settings.set_no_lgfname_file(True)
if 'NO_PLP_MOL2_FILES' in value:
self._settings.set_no_plp_mol2_files(True)
if 'NO_PID_FILE' in value:
self._settings.set_no_pid_file(True)
if 'NO_SEED_LOG_FILE' in value:
self._settings.set_no_seed_log_file(True)
if 'NO_GOLD_ERR_FILE' in value:
self._settings.set_no_gold_err_file(True)
if 'NO_FIT_PTS_FILES' in value:
self._settings.set_no_fit_pts_files(True)
if 'NO_ASP_MOL2_FILES' in value:
self._settings.set_no_asp_mol2_files(True)
@property
def diverse_solutions(self):
'''Diverse solutions settings.
If diverse solutions is enabled this will be (True, cluster size, rmsd), otherwise
(False, None, None)
'''
tf = self._settings.use_diverse_solutions()
if tf:
return (
True, self._settings.diverse_solutions_cluster_size(), self._settings.diverse_solutions_rmsd()
)
else:
return (False, None, None)
@diverse_solutions.setter
def diverse_solutions(self, value):
try:
tf = bool(value[0])
except (ValueError, TypeError, IndexError):
if value:
self._settings.set_use_diverse_solutions(True)
self._settings.set_diverse_solutions_cluster_size(1)
self._settings.set_diverse_solutions_rmsd(1.5)
else:
self._settings.set_use_diverse_solutions(False)
else:
self._settings.set_use_diverse_solutions(tf)
if tf:
try:
n = int(value[1])
except (ValueError, TypeError, IndexError):
self._settings.set_diverse_solutions_cluster_size(1)
self._settings.set_diverse_solutions_rmsd(1.5)
else:
self._settings.set_diverse_solutions_cluster_size(n)
try:
f = float(value[2])
except (ValueError, TypeError, IndexError):
self._settings.set_diverse_solutions_rmsd(1.5)
else:
self._settings.set_diverse_solutions_rmsd(f)
@property
def seed_file(self):
'''The seed file for the pseudo random number generator'''
return self._settings.seed_file()
@seed_file.setter
def seed_file(self, value):
self._settings.set_seed_file(value)
[docs] def set_hostname(self, hostname='localhost', ndocks=1):
'''Set the hostname on which docking jobs will be run.'''
self._socket = self._pick_unused_port(hostname)
self._port = self._socket.getsockname()[1]
self._settings.set_ligands_from_socket(
hostname, self._port, ndocks
)
self._settings.set_ligands_to_socket(
hostname, self._port
)
self.output_file = ''
@staticmethod
def _pick_unused_port(hostname='localhost'):
'''Private: get an unused port for a socket.
May fail if someone else grabs it between query and binding.
'''
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((hostname, 0))
# addr, port = s.getsockname()
# s.close()
return s
def _start_gold(self, file_name=None, mode='foreground'):
'''Private: start the gold server.'''
if self._gold_exe is None:
if 'GOLD_EXE' in os.environ:
self._gold_exe = os.path.abspath(os.environ['GOLD_EXE'])
else:
if 'GOLD_DIR' in os.environ:
self._gold_dir = os.environ['GOLD_DIR']
else:
raise RuntimeError('''GOLD not installed or configured.
Some features of the CSD Python API will not be available.''')
if not os.path.exists(self._gold_dir):
raise RuntimeError('Unable to find a GOLD executable in %s' % self._gold_dir)
if sys.platform == 'win32':
self._gold_exe = os.path.join(self._gold_dir, 'gold', 'd_win32', 'bin', 'gold_win32.exe')
else:
self._gold_exe = os.path.join(self._gold_dir, 'bin', 'gold_auto')
if not os.path.exists(self._gold_exe):
raise RuntimeError('GOLD executable not found at %s' % self._gold_exe)
if file_name is None:
file_name = os.path.abspath('./api_gold.conf')
# file_name = os.path.join(self.output_directory, 'api_gold.conf')
else:
file_name = os.path.abspath(file_name)
if not os.path.exists(os.path.dirname(file_name)):
os.makedirs(os.path.dirname(file_name))
if mode.lower().startswith('inter'):
# Preserve old tags
self._settings.set_replace_tags(False)
# Disable ranking stuff
self._settings.set_bestranking_list_filename('')
self._settings.set_delete_rank_files(True)
# Sockets - check that sockets are enabled, or create them
self.clear_ligand_files()
pars = self._settings.socket_parameters()
hostname, port = pars[0], pars[1]
if not hostname or port == 0:
self.set_hostname()
else:
if hostname != 'localhost':
# Assume it's running???
self.logger.info(f'RUNNING ON {hostname}')
fname = os.path.join(self.output_directory, 'gold.pid')
if not os.path.exists(fname):
raise RuntimeError('GOLD does not appear to be running on %s:%d' % (hostname, port))
with open(fname) as f:
pid = int(f.read())
return Docker.InteractiveResults(self, pid=pid)
# if not os.path.exists(self.output_directory):
# os.makedirs(self.output_directory)
if not os.path.exists(os.path.dirname(file_name)):
os.makedirs(os.path.dirname(file_name))
self.write(file_name)
self.logger.info(f'Starting GOLD with conf file {file_name}')
p = subprocess.Popen(
[self._gold_exe, file_name]
)
if mode.lower().startswith('back'):
return Docker.Results(self, pid=p.pid)
elif mode.lower().startswith('fore'):
return Docker.Results(self, return_code=p.wait())
else:
# Socket docking
return Docker.InteractiveResults(self, pid=p.pid)
[docs] @nested_class('Docker')
class Results(object):
'''Docking results.
'''
[docs] class DockedLigand(Entry):
'''Subclass of :class:`ccdc.entry.Entry` to provide nicer access to the scoring terms of the docking.
'''
def __init__(self, entry, settings):
self._entry = entry._entry
self.attributes = entry.attributes
self.settings = settings
@staticmethod
def _is_float(t):
try:
x = float(t)
return True
except ValueError:
return False
[docs] def fitness(self, fitness_function=None):
'''The recorded fitness of a docking.
:param fitness_function: one of the fitness functions of the :class:`ccdc.docking.Docker.Settings` or ``None``.
If the docking has exactly one fitness attribute, *i.e.*, no rescoring has been performed, then there is
no need to specify the fitness_function.
'''
possibles = [(k, float(v)) for k, v in self.attributes.items() if
'fitness' in k.lower() and self._is_float(v)]
if len(possibles) == 0:
raise RuntimeError('No fitness term in the entry')
terms = [
k.split('.')[1].lower() for k, v in possibles
]
if fitness_function is None:
if len(possibles) == 1:
return possibles[0][1]
else:
raise RuntimeError('Fitness terms for %s in entry' % ', '.join(terms))
else:
matched = [(k, v) for k, v in possibles if fitness_function.lower() in k.lower()]
if len(matched) == 0:
raise RuntimeError('No matching fitness term. Available are %s' % (', ').join(terms))
elif len(matched) == 1:
return matched[0][1]
else:
raise RuntimeError('Multiple matching fitness terms, %s' % ', '.join(k for k, v in matched))
[docs] def scoring_term(self, *filters):
'''Individual or dicts of scoring terms from the entry.
:param fitness_function: any of the fitness functions of :class:`ccdc.docking.Settings`
:param `*filters`: an iterable of additional constraints to put on the name of the term.
:returns: a float if the specification is exact or a dictionary of key:float if ambiguous.
'''
terms = [(k, float(v)) for k, v in self.attributes.items() if self._is_float(v)]
terms = [(k, v) for k, v in terms if all(x.lower() in k.lower() for x in filters)]
if len(terms) == 0:
raise RuntimeError('No scoring term matched')
elif len(terms) == 1:
return terms[0][1]
else:
return dict(terms)
[docs] class ProteinRotatedTorsion(object):
'''Details of a protein amino acid side chain rotated in the docking solution.'''
def __init__(self, protein_rotated_torsion_sd_tag):
'''Construct from the contents of a Gold.Protein.RotatedTorsions solution file SD tag.
:param protein_rotated_torsion_sd_tag: the docked ligand Gold.Protein.RotatedTorsions SD tag string.
'''
self._gold_rotated_torsion = DockingLib.GoldRotatedTorsion(protein_rotated_torsion_sd_tag)
self._type_map = {DockingLib.GoldRotatedTorsion.SIDE_CHAIN: "sidechain",
DockingLib.GoldRotatedTorsion.ROTATED_H: "rotated_h",
DockingLib.GoldRotatedTorsion.UNKNOWN: "unknown"}
@property
def name(self):
return self._gold_rotated_torsion.name()
@property
def chi_number(self):
'''The flexible sidechain chi number or -1 if this torsion is not part of a RotamerLibrary.'''
chi_index_str = self._gold_rotated_torsion.chi_number()
if chi_index_str == "":
return -1
return int(chi_index_str)
@property
def input_angle(self):
'''The torsion angle in the protein before docking.'''
return float(self._gold_rotated_torsion.input_angle())
@property
def final_angle(self):
'''The torsion angle in the final pose.'''
return float(self._gold_rotated_torsion.final_angle())
@property
def atom_indices(self):
'''The file order index of each atom in the torsion.'''
return list(self._gold_rotated_torsion.atoms())
@property
def type(self):
'''The torsion type as a string.
"sidechain" : a torsion in a residue sidechain as defined in a rotamer library.
"rotated_h" : a residue terminal rotatable hydrogen e.g. in a SER, THR, TYR hydroxyl or LYS NH3.
"unknown" : type not set, should not usually be encountered.
:return string representation of the protein rotated torsion type.
'''
return self._type_map[self._gold_rotated_torsion.type()]
[docs] def protein_rotated_torsions(self):
'''Return the set of protein rotated torsions for a docked ligand.'''
torsions = []
if "Gold.Protein.RotatedTorsions" in self.attributes:
rotated_torsions_str = self.attributes["Gold.Protein.RotatedTorsions"]
lines = rotated_torsions_str.splitlines()
for l in lines:
protein_rotated_torsion = Docker.Results.DockedLigand.ProteinRotatedTorsion(l)
torsions.append(protein_rotated_torsion)
return torsions
[docs] class HBond(Molecule.HBond):
'''A hydrogen bond in the docked ligand.'''
def __init__(self, text, ligand, results):
self.results = results
def _get_atom(mol_ref, at_ref):
if mol_ref.startswith('P'):
inx = int(mol_ref[1:]) - 1
return self.results.proteins[inx].atoms[int(at_ref) - 1]
else:
return ligand.molecule.atoms[int(at_ref) - 1]
parts = text.split()
at0 = _get_atom(parts[0], parts[3])
at1 = _get_atom(parts[4], parts[5])
strength = float(parts[6])
_contact = ChemistryLib.MolecularContact(at0._molecule, at0._atom, at1._molecule, at1._atom,
strength, ChemistryLib.MolecularContact.HBOND_CONTACT)
super(self.__class__, self).__init__(_contact)
def hbonds(self, which=None):
if which is None:
s = self.settings.settings.fitness_function
else:
s = which
if s.lower() == 'plp' or s.lower() == 'chemscore':
tag = 'Gold.Chemscore.Hbonds'
elif s.lower() == 'asp':
return None
elif s.lower() == 'goldscore':
tag = 'Gold.Goldscore.Hbonds'
text = self.attributes.get(tag)
if text is not None:
text = text.split('\n')
return tuple(
Docker.Results.DockedLigand.HBond(l, self, self.settings)
for l in text[1:] if l
)
@property
def docked_waters(self):
'''
Optionally, it is possible in GOLD to specify waters that are docked alongside a given ligand. This property
provides access to the positions of these waters.
:return molecules representing the docked waters in the binding site that are associated with this ligand pose as placed in the binding site
'''
water_data = DockingLib.GoldProteinManager.build_active_waters(self._entry)
return tuple(Molecule(_molecule=mol) for mol in water_data)
[docs] class DockedLigandReader(io.EntryReader):
'''Subclass of :class:`ccdc.io.EntryReader` to provide :class:`ccdc.docking.Docker.Results.DockedLigand` instances.'''
def __new__(kl, file_name, settings):
ret = io._ReaderFactory.__new__(kl, file_name)
# super(self.__class__, self).__new__(io.EntryReader, file_name)
# super(self.__class__, self).__init__(file_name)
ret.settings = settings
return ret
def _make_entry(self, _entry):
return Docker.Results.DockedLigand(
super(self.__class__, self)._make_entry(_entry), self.settings
)
def __iter__(self):
'''Iterator.'''
return self.entries() # pylint: disable=E1101
def __getitem__(self, i):
return self._make_entry(self._enumerator.entry(i)) # pylint: disable=E1101
def __init__(self, settings, return_code=None, pid=None):
self.logger = Logger()
self.settings = settings
self.return_code = return_code
self.pid = pid
def _read_file(self, file_name):
'''Read it if it exists.'''
fname = os.path.join(self.settings.output_directory, file_name)
if os.path.exists(fname):
with open(fname) as f:
return f.read()
@property
def protein_log(self):
'''The content of the protein log file.'''
return self._read_file('gold_protein.log')
@property
def error_log(self):
'''The content of the docking error log file.'''
return self._read_file('gold.err')
@property
def docking_log(self):
'''The content of the docking log file.'''
return self._read_file('gold.log')
[docs] def ligand_log(self, index):
'''The content of a ligand log.'''
l = glob.glob(os.path.join(self.settings.output_directory, 'gold_*_m*.log'))
def mtime(f):
return os.path.getmtime(f)
l.sort(key=mtime)
if index < len(l):
return self._read_file(l[index])
@property
def ligands(self):
'''The ligands of the docking.
The value of this property is a :class:`ccdc.io.EntryReader`.
Each entry has an attributes property, a dictionary of the docking information pertaining to
the docking.
'''
dock_files = DockingLib.QtGoldDockingSolutionFiles(self.settings._conf_file_name)
return Docker.Results.DockedLigandReader([x.filename_ for x in dock_files.solution_filenames()], self)
@property
def proteins(self):
'''The protein(s) of the docking.
:returns: a tuple of :class:`ccdc.protein.Protein`.
This tuple will have more than one entry if ensemble docking was used.
'''
if not hasattr(self, '_proteins'):
dock_files = DockingLib.QtGoldDockingSolutionFiles(self.settings._conf_file_name)
self._proteins = tuple(
Protein.from_file(df.filename_)
for df in dock_files.protein_filenames()
)
return self._proteins
[docs] def make_complex(self, ligand):
'''Make the complex with the ligand, adjusting rotatables as required and including any docked active waters
:return: a :class:`ccdc.protein.Protein` with the ligand added.
'''
prot_id = int(ligand.attributes.get('Gold.Ensemble.ID', 1)) - 1
prot = self.proteins[prot_id]
if not hasattr(prot, '_complex'):
prot._complex = None
if not hasattr(prot, '_added_waters'):
prot._added_waters = []
if prot._complex is not None:
lig = prot._protein_structure.ligand(prot._complex)
if lig is not None:
prot._protein_structure.remove_ligand(lig, False)
[prot._protein_structure.remove_water(water) for water in prot._added_waters]
prot._complex = prot._protein_structure.nligands()
prot.add_ligand(ligand.molecule)
if not hasattr(prot, '_manager'):
prot._manager = DockingLib.GoldProteinManager(prot.identifier, prot._molecule)
prot._manager.set_rotated_atoms(ligand._entry)
prot._added_waters = [prot._protein_structure.add_water(water._molecule, False) for water in
ligand.docked_waters]
return prot
[docs] @nested_class('Docker')
class InteractiveResults(Results):
'''A session connecting to a GOLD process.
If the :class:`ccdc.docking.Docker.InteractiveResults` instance has an attribute,
'ligand_preparation', this should be either None, in which case no ligand preparation is performed,
or an instance of :class:`ccdc.docking.Docker.LigandPreparation` whose prepare method will be
called for each interactive docking attempted. A default constructed :class:`ccdc.docking.Docker.LigandPreparation` will be used if the attribute is not present.
'''
_line_match = re.compile(
r".*'(?P<file_name>[^']*)'.*'(?P<identifier>[^']*)'.*$"
)
_file_name_match = re.compile(
r".*gold_soln_(?P<identifier>.*)_m[0-9]+_[0-9]+\.mol2"
)
def __init__(self, settings, pid=None):
super(self.__class__, self).__init__(settings, pid=pid)
# Set up the socket
pars = settings._settings.socket_parameters()
hostname, port = pars[0], pars[1]
if not hasattr(settings, '_socket'):
self._socket = socket.socket(
socket.AF_INET, socket.SOCK_STREAM
)
self._socket.bind(('', port))
else:
self._socket = settings._socket
self._socket.listen(5)
# for now, just a single GOLD job.
self._socket.settimeout(5 * 60.)
try:
self._client_socket, address = self._socket.accept()
except socket.timeout:
raise RuntimeError('Socket timed out on accept()')
self._client_socket.settimeout(None)
self.logger.info(f'CONNECTED TO {hostname} {port} {address}')
self._docked_ligand_count = 0
self._wait_for_gold()
def __del__(self):
if hasattr(self, '_socket'):
self._socket.close()
self._socket = None
if hasattr(self, '_client_socket'):
self._client_socket.close()
self._client_socket = None
fname = os.path.join(self.settings.output_directory, 'gold.pid')
if os.path.exists(fname):
try:
os.unlink(fname)
except OSError:
# File in use on windows
pass
socket_files = glob.glob(os.path.join(self.settings.output_directory, 'gold_SOCKET_m*.log'))
for fname in socket_files:
try:
os.unlink(fname)
except OSError:
# File in use on windows
pass
[docs] def dock(self, entry):
'''Send an entry to be docked.
:returns: a tuple of :class:`ccdc.entry.Entry` instances. These are the docked poses.
'''
if not hasattr(self, 'ligand_preparation'):
self.ligand_preparation = Docker.LigandPreparation()
if self.ligand_preparation is not None:
entry = self.ligand_preparation.prepare(entry)
structure = entry.to_string(format='mol2') + '\nGOLDMINE MOL2 TERMINATOR\n'
structure = structure.replace('\n', '\r\n')
header = '%d %d %s.mol2\n' % (len(structure), self._docked_ligand_count, entry.identifier)
self._docked_ligand_count += 1
self._send(header)
l = self._recv_line()
if l.startswith('SEND LIGAND'):
pass
self._send(structure, add_cr=False)
l = self._recv_line()
if l.startswith('GOT LIGAND'):
pass
return self._get_ligands()
def _wait_for_gold(self):
while True:
l = self._recv_line()
if l.startswith('SEND LIGAND HEADER'):
return
def _recv_line(self):
l = []
while 1:
c = self._client_socket.recv(1)
c = c.decode('ISO-8859-1')
if not c:
self.logger.warning('SOCKET CLOSED?')
raise IOError('Socket failed (probably client died).')
l.append(c)
if c == '\n':
return ''.join(l)
def _send(self, msg, add_cr=True):
if add_cr:
msg = msg.replace('\n', '\r\n')
msg = msg.encode()
to_send = len(msg)
total_sent = 0
while total_sent < to_send:
sent = self._client_socket.send(msg[total_sent:])
if sent == 0:
pars = self.settings._settings.socket_parameters()
hostname, port = pars[0], pars[1]
raise RuntimeError('Socket connection %s:%d broken' % (hostname, port))
total_sent += sent
def _get_ligands(self):
ligs = []
chunks = []
bytes_in = 0
in_ligand = False
while True:
line = self._recv_line()
if line.startswith('SEND LIGAND HEADER'):
break
if line.startswith('DOCKED LIGAND'):
in_ligand = True
if line.startswith('END DOCKED LIGAND'):
in_ligand = False
structure = ''.join(chunks).replace('\r', '')
lig = Entry.from_string(structure, format='mol2')
# Patch up identifier
parts = lig.identifier.split('|')
identifier = '%s|%s|%s' % (parts[0], parts[0], '|'.join(parts[2:]))
lig.identifier = identifier
ligs.append(Docker.Results.DockedLigand(lig, self.settings))
chunks = []
else:
if in_ligand:
chunks.append(line)
else:
self.logger.warning('UNRECOGNISED LINE:', line)
# Remove ranked_ files
ranked_files = glob.glob(os.path.join(self.settings.output_directory, 'ranked_*'))
for r in ranked_files:
os.unlink(r)
# Patch bestranking.lst
best_ranking_file = os.path.join(self.settings.output_directory, 'bestranking.lst')
if os.path.exists(best_ranking_file):
with open(best_ranking_file) as f:
lines = f.readlines()
last = lines[-1]
linem = Docker.InteractiveResults._line_match.match(last)
if linem is not None:
gd = linem.groupdict()
fname = gd['file_name']
identifier = gd['identifier']
match = Docker.InteractiveResults._file_name_match.match(fname)
if match is not None:
new_name = fname.replace(match.groupdict()['identifier'], identifier)
last = last.replace(fname, new_name)
lines = lines[:-1] + [last]
with open(best_ranking_file, 'w') as writer:
writer.write(''.join(lines))
# rename mol2 files
last_mol2_file = glob.glob(
os.path.join(self.settings.output_directory, 'gold_*_m%d.mol2' % (self._docked_ligand_count)))
if last_mol2_file:
try:
os.rename(last_mol2_file[0], os.path.join(self.settings.output_directory, 'gold_%s_m%d.mol2' % (
ligs[0].identifier.split('|')[0], self._docked_ligand_count)))
except IndexError:
pass
# rename log files
last_log_file = glob.glob(
os.path.join(self.settings.output_directory, 'gold_SOCKET_m%d.log' % (self._docked_ligand_count)))
if last_log_file:
try:
os.rename(last_log_file[0], os.path.join(self.settings.output_directory, 'gold_%s_m%d.log' % (
ligs[0].identifier.split('|')[0], self._docked_ligand_count)))
except IndexError:
pass
return tuple(ligs)
def __init__(self, settings=None):
'''Initialise the docker.'''
if settings is None:
settings = Docker.Settings()
self.settings = settings
[docs] def dock(self, file_name=None, mode='foreground'):
'''Dock from the current settings.
:param file_name: file name for the settings. If ``None``, current settings are written to a temporary directory.
:param mode: one of 'foreground', 'background' or 'interactive'.
:raises: RuntimeError if no GOLD executable is found.
'''
return self.settings._start_gold(file_name=file_name, mode=mode)
@property
def results(self):
'''The docking results.
If the docking is still in progress, the results may be partial.
'''
pidfile = os.path.join(self.settings.output_directory, 'gold.pid')
if os.path.exists(pidfile):
with open(pidfile) as f:
pid = f.read()
return_code = None
else:
pid = None
return_code = 0
return Docker.Results(self.settings, return_code=return_code, pid=pid)
[docs] def dock_status(self):
'''Check the status of a docking job via the gold.pid file.'''
pidfile = os.path.join(self.settings.output_directory, 'gold.pid')
if os.path.exists(pidfile):
return 1
else:
return 0
[docs] def copy_settings(self, newdocker):
'''Copy this docker's settings to another docker instance
'''
for fn in self.settings.protein_files:
newdocker.settings.add_protein_file(fn)
for ligand_file in self.settings.ligand_files:
newdocker.settings.add_ligand_file(
ligand_file.file_name,
ligand_file.ndocks,
ligand_file.start,
ligand_file.finish
)
for water_file in self.settings.water_files:
newdocker.settings.add_water_file(
water_file.file_name,
water_file.toggle_state,
water_file.spin_state,
water_file.movable_distance
)
newdocker.settings.add_specific_fixed_rotatable_bond(self.settings.specific_fixed_rotatable_bonds)
planar_n_settings = self.settings.flip_planar_nitrogen
newdocker.settings.set_flip_planar_nitrogen(setting=planar_n_settings['setting'],
ring_NHR=planar_n_settings['ring_NHR'],
ring_NRR=planar_n_settings['ring_NRR'])
for constraint in self.settings.constraints:
newdocker.settings.add_constraint(constraint)
exclusions = ['proteins', 'ligands', 'waters', 'ligand_files',
'protein_files', 'water_files', 'specific_fixed_rotatable_bond',
'flip_planar_nitrogen', 'constraints']
simple_attributes = [p for p in dir(Docker.Settings) if
p not in exclusions and
isinstance(getattr(Docker.Settings, p), property)]
for attribute in simple_attributes:
try:
setattr(newdocker.settings, attribute,
getattr(self.settings, attribute))
except AttributeError:
pass
def _count_mol_file(self, mol_filename):
'''Private: count number of molecules in a file
returns -1 if an error occurs
'''
try:
mol_reader = io.MoleculeReader(mol_filename)
except IOError:
return -1
return len(mol_reader)
def _split_ligand_files(self, maximum_size, ligand_file_lengths={}):
'''Private: split the ligand files based on a maximum size
Returns a list of lists of tuples in form [(fn1,start,end),(fn2,start,end)]
Includes explicit start and end molecules
If not provided and the original ligand files have 0 for end, it will
read the file to determine it's size
'''
# Let's start by counting ligands in files as needed
for ligand_file in self.settings.ligand_files:
try:
count = ligand_file_lengths[ligand_file.file_name]
except KeyError:
# we have to read this file to get the length unless the ligand count is set
if ligand_file.finish != 0:
ligand_file_lengths[ligand_file.file_name] = ligand_file.finish - ligand_file.start
else:
mr = io.MoleculeReader(ligand_file.file_name)
ligand_file_lengths[ligand_file.file_name] = len(mr)
# This is done as the next file will start at this position
maximum_size = maximum_size - 1
# Now generate the splits
ligand_file_splits = []
curcount = 0
t = []
for ligand_file in self.settings.ligand_files:
if curcount + ligand_file_lengths[ligand_file.file_name] <= maximum_size:
# We can add the whole ligand file entry to this split
startmol = ligand_file.start
if ligand_file.finish == 0:
endmol = ligand_file.start + ligand_file_lengths[ligand_file.file_name]
else:
endmol = ligand_file.finish
if startmol == 0:
startmol = 1
t.append(Docker.Settings.LigandFileInfo(
ligand_file.file_name, ligand_file.ndocks, startmol, endmol
))
curcount += ligand_file_lengths[ligand_file.file_name]
if curcount == maximum_size:
ligand_file_splits.append(t[:])
t = []
curcount = 0
else:
if ligand_file.finish == 0:
ligand_file = Docker.Settings.LigandFileInfo(
ligand_file.file_name,
ligand_file.ndocks,
ligand_file.start,
ligand_file_lengths[ligand_file.file_name]
)
try:
x = endmol
except NameError:
endmol = -1
while endmol != ligand_file.finish:
try:
x = startmol
except NameError:
startmol = ligand_file.start
if startmol == 0:
startmol = 1
endmol = startmol + (maximum_size - curcount)
if endmol > ligand_file.finish:
endmol = ligand_file.finish
cursize = endmol - startmol
curcount += cursize
t.append(
Docker.Settings.LigandFileInfo(
ligand_file.file_name, ligand_file.ndocks, startmol, endmol
)
)
if endmol == ligand_file.finish:
startmol = 1
else:
startmol = endmol + 1
if curcount == maximum_size:
ligand_file_splits.append(t[:])
t = []
curcount = 0
ligand_file_splits.append(t[:])
return ligand_file_splits
##########################################################################