class AxesOfInertia(ObjectiveProvider): """ Calculates the axes of inertia of given molecules and returns their alignment deviation. Parameters ---------- reference : str Molecule name `targets` should align to. targets : list of str Names of molecules to be aligned to `reference` threshold : float Target average of cosine of angle of alignment between targets and reference. only_primaries : bool Consider only the largest inertia vectors. Returns ------- float Mean absolute difference of threshold alignment and mean of all the cosines involved for each axis. """ _validate = { parse.Required('reference'): parse.Molecule_name, parse.Required('targets'): [parse.Molecule_name], 'threshold': parse.All(parse.Coerce(float), parse.Range(min=0, max=1)), 'only_primaries': parse.Coerce(bool), } def __init__(self, reference=None, targets=None, only_primaries=False, threshold=0.84, *args, **kwargs): ObjectiveProvider.__init__(self, **kwargs) self.threshold = threshold self._reference = reference self._targets = targets def reference(self, individual): """ The reference molecule. Usually, the biggest in size """ return individual.find_molecule(self._reference).compound.mol def targets(self, individual): return [individual.find_molecule(name).compound.mol for name in self._targets] def evaluate(self, individual): reference = self.reference(individual) targets = self.targets(individual) all_axes = [] for target in [reference] + targets: axes = calculate_axes_of_inertia(target) all_axes.append(axes) best_cosines = list(calculate_alignment(all_axes[0], *all_axes[1:])) return abs(self.threshold - np.mean(best_cosines))
class Search(GeneProvider): """ Parameters ---------- target : namedtuple or Name of gaudi.genes.molecule Can be either: - The *anchor* atom of the molecule we want to move, with syntax ``<molecule_name>/<index>``. For example, if we want to move Ligand using atom with serial number = 1 as pivot, we would specify ``Ligand/1``. It's parsed to the actual chimera.Atom later on. - A name of gaudi.genes.molecule instance. In this case, the *anchor* atom for the movement of the molecule will be set to its nearest atom to the geometric center of the molecule. center : 3-item list or tuple of float, optional Coordinates to the center of the desired search sphere radius : float Maximum distance from center that the molecule can move rotate : bool, bool If False, don't rotatate the molecule - only translation precision : int, bool Rounds the decimal part of the 3D search matrix to get a coarser model of space. Ie, less points can be accessed, the search is less exhaustive, more variability in less runs. Attributes ---------- allele : 3-tuple of 4-tuple of floats A 4x3 matrix of float, as explained in Notes. origin : 3-tuple of float The initial position of the requested target molecule. If we don't take this into account, we can't move the molecule around was not originally in the center of the sphere. Notes ----- **How matricial translation and rotation takes place** A single movement is summed up in a 4x3 matrix: ( (R1, R2, R3, T1), (R4, R5, R6, T2), (R7, R8, R9, T3) ) R-elements contain the rotation information, while T elements account for the translation movement. That matrix can be obtained from multipying three different matrices with this expression: multiply_matrices(translation, rotation, to_zero) To understand the operation, it must be read from the right: 1. First, translate the molecule the origin of coordinates 0,0,0 2. In that position, the rotation can take place. 3. Then, translate to the final coordinates from zero. There's no need to get back to the original position. How do we get the needed matrices? - ``to_zero``. Record the original position (`origin`) of the molecule and multiply it by -1. Done with method `to_zero()`. - ``rotation``. Obtained directly from ``FitMap.search.random_rotation`` - ``translation``. Check docstring of ``random_translation()`` in this module. """ _validate = { parse.Required("target"): parse.Any(parse.Named_spec("molecule", "atom"), parse.Molecule_name), "center": parse.Any(parse.Coordinates, parse.Named_spec("molecule", "atom")), "radius": parse.Coerce(float), "rotate": parse.Boolean, "precision": parse.All(parse.Coerce(int), parse.Range(min=-3, max=6)), "interpolation": parse.All(parse.Coerce(float), parse.Range(min=0, max=1.0)), } def __init__(self, target=None, center=None, radius=None, rotate=True, precision=0, interpolation=0.5, **kwargs): GeneProvider.__init__(self, **kwargs) self.radius = radius self.rotate = rotate self.precision = precision self._center = center self.target = target self.interpolation = interpolation def __ready__(self): if isinstance(self.target, str): mol = self.parent.find_molecule(self.target).compound.mol anchor_atom = nearest_atom(mol, center(mol)) self.target = parse.MoleculeAtom(self.target, anchor_atom) self.allele = self.random_transform() @property def center(self): if self._center: return parse_origin(self._center, self.parent) else: return self.origin @property def molecule(self): return self.parent.find_molecule(self.target.molecule).compound.mol @property def origin(self): return parse_origin(self.target, self.parent) @property def to_zero(self): """ Return a translation matrix that takes the molecule from its original position to the origin of coordinates (0,0,0). Needed for rotations. """ x, y, z = self.origin return ((1.0, 0.0, 0.0, -x), (0.0, 1.0, 0.0, -y), (0.0, 0.0, 1.0, -z)) def express(self): """ Multiply all the matrices, convert the result to a chimera.CoordFrame and set that as the xform for the target molecule. If precision is set, round them. """ matrices = self.allele + (self.to_zero, ) if self.precision > 0: self.molecule.openState.xform = M.chimera_xform( M.multiply_matrices( *numpy_around(matrices, self.precision).tolist())) else: self.molecule.openState.xform = M.chimera_xform( M.multiply_matrices(*matrices)) def unexpress(self): """ Reset xform to unity matrix. """ self.molecule.openState.xform = X() def mate(self, mate): """ Interpolate the matrices and assign them to each individual. Ind1 gets the rotated interpolation, while Ind2 gets the translation. """ xf1 = M.chimera_xform(M.multiply_matrices(*self.allele)) xf2 = M.chimera_xform(M.multiply_matrices(*mate.allele)) interp = M.xform_matrix(M.interpolate_xforms(xf1, ZERO, xf2, 0.5)) interp_rot = [x[:3] + (0, ) for x in interp] interp_tl = [ y[:3] + x[-1:] for x, y in zip(interp, M.identity_matrix()) ] self.allele, mate.allele = ( (self.allele[0], interp_rot), (interp_tl, mate.allele[1]), ) def mutate(self, indpb): if random.random() < self.indpb: xf1 = M.chimera_xform(M.multiply_matrices(*self.allele)) xf2 = M.chimera_xform( M.multiply_matrices(*self.random_transform())) interp = M.xform_matrix( M.interpolate_xforms(xf1, ZERO, xf2, self.interpolation)) interp_rot = [x[:3] + (0, ) for x in interp] interp_tl = [ y[:3] + x[-1:] for x, y in zip(interp, M.identity_matrix()) ] self.allele = interp_tl, interp_rot ##### def random_transform(self): """ Wrapper function to provide translation and rotation in a single call """ rotation = random_rotation() if self.rotate else IDENTITY translation = random_translation(self.center, self.radius) return translation, rotation
class NormalModes(GeneProvider): """ NormalModes class Parameters ---------- method : str Either: - prody : calculate normal modes using prody algorithms - gaussian : read normal modes from a gaussian output file target : str Name of the Gene containing the actual molecule modes : list, optional, default=range(12) Modes to be used to move the molecule group_by : str or callable, optional, default=None group_by_*: algorithm name or callable coarseGrain(prm) which makes ``mol.select().setBetas(i)``, where ``i`` is the index Coarse Grain group, and ``prm`` is ``prody.AtomGroup`` group_lambda : int, optional Either: number of residues per group (default=7), or total mass per group (default=100) path : str Gaussian or prody modes output path. Required if ``method`` is ``gaussian``. write_modes: bool, optional write a ``molecule_modes.nmd`` file with the ProDy modes n_samples : int, optional, default=10000 number of conformations to generate rmsd : float, optional, default=1.0 average RMSD that the conformations will have with respect to the initial conformation Attributes ---------- allele : slice of prody.ensemble Randomly picked coordinates from NORMAL_MODE_SAMPLES NORMAL_MODES : prody.modes normal modes calculated for the molecule or readed from the gaussian frequencies output file stored in a prody modes class (ANM or RTB) NORMAL_MODE_SAMPLES : prody.ensemble configurations applying modes to molecule _original_coords : numpy.array Parent coordinates _chimera2prody : dict _chimera2prody[chimera_index] = prody_index """ _validate = { parse.Required('method'): parse.In(['prody', 'gaussian']), 'path': parse.RelPathToInputFile(), 'write_modes': parse.Boolean, parse.Required('target'): parse.Molecule_name, 'group_by': parse.In(['residues', 'mass', 'calpha', '']), 'group_lambda': parse.All(parse.Coerce(int), parse.Range(min=1)), 'modes': [parse.All(parse.Coerce(int), parse.Range(min=0))], 'n_samples': parse.All(parse.Coerce(int), parse.Range(min=1)), 'rmsd': parse.All(parse.Coerce(float), parse.Range(min=0)) } def __init__(self, method='prody', target=None, modes=None, n_samples=10000, rmsd=1.0, group_by=None, group_lambda=None, path=None, write_modes=False, **kwargs): # Fire up! GeneProvider.__init__(self, **kwargs) self.method = method self.target = target self.modes = modes if modes is not None else range(12) self.max_modes = max(self.modes) + 1 self.n_samples = n_samples self.rmsd = rmsd self.group_by = None self.group_by_options = None self.path = None self.write_modes = write_modes if method == 'prody': if path is None: self.normal_modes_function = self.calculate_prody_normal_modes self.group_by = group_by self.group_by_options = {} if group_lambda is None else { 'n': group_lambda } else: self.path = path self.normal_modes_function = self.read_prody_normal_modes else: # gaussian self.normal_modes_function = self.read_gaussian_normal_modes if path is None: raise ValueError('Path is required if method == gaussian') self.path = path if self.name not in self._cache: self._cache[self.name] = LRU(300) def __ready__(self): """ Second stage of initialization It saves the parent coordinates, calculates the normal modes and initializes the allele """ cached = self._CACHE.get('normal_modes') if not cached: normal_modes, normal_modes_samples, chimera2prody, prody_molecule = self.normal_modes_function( ) self._CACHE['normal_modes'] = normal_modes self._CACHE['normal_modes_samples'] = normal_modes_samples self._CACHE['chimera2prody'] = chimera2prody self._CACHE['original_coords'] = chimeracoords2numpy(self.molecule) if self.write_modes: title = os.path.join(self.parent.cfg.output.path, '{}_modes.nmd'.format(self.molecule.name)) prody.writeNMD(title, normal_modes, prody_molecule) self.allele = random.choice(self.NORMAL_MODES_SAMPLES) def express(self): """ Apply new coords as provided by current normal mode """ c2p = self._chimera2prody for atom in self.molecule.atoms: index = c2p[atom.serialNumber] new_coords = self.allele[index] atom.setCoord(chimera.Point(*new_coords)) def unexpress(self): """ Undo coordinates change """ for i, atom in enumerate(self.molecule.atoms): atom.setCoord(chimera.Point(*self._original_coords[i])) def mate(self, mate): """ .. todo:: Combine coords between two samples in NORMAL_MODES_SAMPLES? Or two samples between diferent NORMAL_MODES_SAMPLES? Or combine samples between two NORMAL_MODES_SAMPLES? For now : pass """ pass def mutate(self, indpb): """ (mutate to/get) another SAMPLE with probability = indpb """ if random.random() < self.indpb: return random.choice(self.NORMAL_MODES_SAMPLES) ##### @property def molecule(self): return self.parent.genes[self.target].compound.mol @property def _CACHE(self): return self._cache[self.name] @property def NORMAL_MODES(self): return self._CACHE.get('normal_modes') @property def NORMAL_MODES_SAMPLES(self): return self._CACHE.get('normal_modes_samples') @property def _chimera2prody(self): return self._CACHE.get('chimera2prody') @property def _original_coords(self): return self._CACHE.get('original_coords') def calculate_prody_normal_modes(self): """ calculate normal modes, creates a diccionary between chimera and prody indices and calculate n_confs number of configurations using this modes """ prody_molecule, chimera2prody = convert_chimera_molecule_to_prody( self.molecule) modes = prody_modes(prody_molecule, self.max_modes, GROUPERS[self.group_by], **self.group_by_options) samples = prody.sampleModes(modes=modes[self.modes], atoms=prody_molecule, n_confs=self.n_samples, rmsd=self.rmsd) samples.addCoordset(prody_molecule) samples_coords = [sample.getCoords() for sample in samples] return modes, samples_coords, chimera2prody, prody_molecule def read_prody_normal_modes(self): prody_molecule, chimera2prody = convert_chimera_molecule_to_prody( self.molecule) modes = prody.parseNMD(self.path)[0] samples = prody.sampleModes(modes=modes[self.modes], atoms=prody_molecule, n_confs=self.n_samples, rmsd=self.rmsd) samples.addCoordset(prody_molecule) samples_coords = [sample.getCoords() for sample in samples] return modes, samples_coords, chimera2prody, prody_molecule def read_gaussian_normal_modes(self): """ read normal modes, creates a diccionary between chimera and prody indices and calculate n_confs number of configurations using this modes """ prody_molecule, chimera2prody = convert_chimera_molecule_to_prody( self.molecule) modes = gaussian_modes(self.path) samples = prody.sampleModes(modes=modes[self.modes], atoms=prody_molecule, n_confs=self.n_samples, rmsd=self.rmsd) samples.addCoordset(prody_molecule) samples_coords = [sample.getCoords() for sample in samples] return modes, samples_coords, chimera2prody, prody_molecule
class Contacts(ObjectiveProvider): """ Contacts class Parameters ---------- probes : str Name of molecule gene that is object of contacts analysis radius : float Maximum distance from any point of probes that is searched for possible interactions which : {'hydrophobic', 'clashes'} Type of interactions to measure clash_threshold : float, optional Maximum overlap of van-der-Waals spheres that is considered as a contact (attractive). If the overlap is greater, it's considered a clash (repulsive) hydrophobic_threshold : float, optional Maximum overlap for hydrophobic patches. hydrophobic_elements : list of str, optional, defaults to [C, S] Which elements are allowed to interact in hydrophobic patches cutoff : float, optional If the overlap volume is greater than this, a penalty is applied. Useful to filter bad solutions. bond_separation : int, optional Ignore clashes or contacts between atoms within n bonds. only_internal : bool, optional If set to True, take into account only intramolecular interactions, defaults to False Returns ------- float Lennard-Jones-like energy when `which`=`hydrophobic`, and volumetric overlap of VdW spheres in A³ if `which`=`clashes`. """ _validate = { parse.Required('probes'): [parse.Molecule_name], 'radius': parse.All(parse.Coerce(float), parse.Range(min=0)), 'which': parse.In(['hydrophobic', 'clashes']), 'clash_threshold': parse.Coerce(float), 'hydrophobic_threshold': parse.Coerce(float), 'cutoff': parse.Coerce(float), 'hydrophobic_elements': [basestring], 'bond_separation': parse.All(parse.Coerce(int), parse.Range(min=2)), 'same_residue': parse.Coerce(bool), 'only_internal': parse.Coerce(bool) } def __init__(self, probes=None, radius=5.0, which='hydrophobic', clash_threshold=0.6, hydrophobic_threshold=-0.4, cutoff=0.0, hydrophobic_elements=('C', 'S'), bond_separation=4, same_residue=True, only_internal=False, *args, **kwargs): ObjectiveProvider.__init__(self, **kwargs) self.which = which self.radius = radius self.clash_threshold = clash_threshold self.hydrophobic_threshold = hydrophobic_threshold self.cutoff = cutoff self.hydrophobic_elements = set(hydrophobic_elements) self.bond_separation = bond_separation self.same_residue = same_residue self._probes = probes self.only_internal = only_internal if which == 'hydrophobic': self.evaluate = self.evaluate_hydrophobic self.threshold = hydrophobic_threshold else: self.evaluate = self.evaluate_clashes self.threshold = clash_threshold def molecules(self, ind): return [m.compound.mol for m in ind._molecules.values()] def probes(self, ind): return [ind.find_molecule(p).compound.mol for p in self._probes] def evaluate_clashes(self, ind): positive, negative = self.find_interactions(ind) clashscore = sum( abs(vol_overlap) for (a1, a2, overlap, vol_overlap) in negative) if self.cutoff and clashscore > self.cutoff: clashscore = -1000 * self.weight return clashscore def evaluate_hydrophobic(self, ind): positive, negative = self.find_interactions(ind) return sum(lj_energy for (a1, a2, overlap, lj_energy) in positive) def find_interactions(self, ind): atoms = self._surrounding_atoms(ind) options = dict(test=atoms, intraRes=self.same_residue, interSubmodel=True, clashThreshold=self.threshold, assumedMaxVdw=2.1, hbondAllowance=0.2, bondSeparation=self.bond_separation) clashes = DetectClash.detectClash(atoms, **options) return self._analyze_interactions(clashes) def _analyze_interactions(self, clashes): """ Interpret contacts provided by DetectClash. Parameters ---------- clashes : dict of dict Output of DetectClash. It's a dict of atoms, whose values are dicts. These subdictionaries contain all the contacting atoms as keys, and the respective overlaping length as values. Returns ------- positive : list of list Each sublist depict an interaction, with four items: the two involved atoms, their distance, and their Lennard-Jones score. negative : list of list Each sublist depict an interaction, with four items: the two involved atoms, their distance, and their volumetric overlap. .. note :: First, collect atoms that can be involved in hydrophobic interactions. Namely, C and S. Then, iterate the contacting atoms, getting the distances. For each interaction, analyze the distance and, based on the threshold, determine if it's attractive or repulsive. Attractive interactions are weighted with a Lennard-Jones like function (``_lennard_jones``), while repulsive attractions are measured with the volumetric overlap of the involved atoms' Van der Waals spheres. """ positive, negative = [], [] for a1, clash in clashes.items(): for a2, overlap in clash.items(): # overlap < clash threshold : can be a hydrophobic interaction if overlap <= self.clash_threshold: if (a1.element.name in self.hydrophobic_elements and a2.element.name in self.hydrophobic_elements): lj_energy = self._lennard_jones(a1, a2, overlap) positive.append([a1, a2, overlap, lj_energy]) # overlap > clash threshold : clash! else: volumetric_overlap = self._vdw_vol_overlap(a1, a2, overlap) negative.append([a1, a2, overlap, volumetric_overlap]) return positive, negative def _surrounding_atoms(self, ind): """ Get atoms in the search zone, based on the molecule, (possible) rotamer genes and the radius """ self.zone.clear() #Add all atoms of probes molecules self.zone.add([a for m in self.probes(ind) for a in m.atoms]) if not self.only_internal: #Add beta carbons of rotamers to find clashes/contacts in its surroundings rotamer_genes = [ name for name, g in ind.genes.items() if g.__class__.__name__ == 'Rotamers' ] beta_carbons = [] for n in rotamer_genes: for ((molname, pos), residue) in ind.genes[n].residues.items(): beta_carbons.extend( [a for a in residue.atoms if a.name == 'CB']) self.zone.add(beta_carbons) #Surrounding zone from probes+rotamers atoms self.zone.merge( chimera.selection.REPLACE, chimera.specifier.zone(self.zone, 'atom', None, self.radius, self.molecules(ind))) return self.zone.atoms() @staticmethod def _lennard_jones(a1, a2, overlap=None): """ VERY rough approximation of a Lennard-Jones score (12-6). Parameters ---------- a1, a2 : chimera.Atom overlap : float Overlapping radii of involved atoms, as provided by DetectClash. Notes ----- The usual implementation of a LJ potential is: LJ = 4*epsilon*(0.25*((r0/r)**12) - 0.5*((r0/r)**6)) Two approximations are done: - The atoms involves are considered equal, hence the distance at which the energy is minimum (r0) is just the sum of their radii. - Epsilon is always 1. """ r0 = a1.radius + a2.radius if overlap is None: distance = a1.xformCoord().distance(a2.xformCoord()) else: distance = r0 - overlap x = (r0 / distance)**6 return (x * x - 2 * x) @staticmethod def _vdw_vol_overlap(a1, a2, overlap=None): """ Volumetric overlap of Van der Waals spheres of atoms. Parameters ---------- a1, a2 : chimera.Atom overlap : float Overlapping sphere segment of involved atoms .. note :: Adapted from Eran Eyal, Comput Chem 25: 712-724, 2004 """ PI = 3.14159265359 if overlap is None: d = a1.xformCoord().distance(a2.xformCoord()) else: d = a1.radius + a2.radius - overlap if d == 0: return 1000 h_a, h_b = 0, 0 if d < (a1.radius + a2.radius): h_a = (a2.radius**2 - (d - a1.radius)**2) / (2 * d) h_b = (a1.radius**2 - (d - a2.radius)**2) / (2 * d) return (PI / 3) * ((h_a**2) * (3 * a1.radius - h_a) + (h_b**2) * (3 * a2.radius - h_b))
class Hbonds(ObjectiveProvider): """ Hbonds class Parameters ---------- probes : list of str Names of molecules being object of analysis radius : float Maximum distance from any point of probe that is searched for a possible interaction distance_tolerance : float, optional Allowed deviation from ideal distance to consider a valid H bond. angle_tolerance : float, optional Allowed deviation from ideal angle to consider a valid H bond. only_intermolecular : boolean, optional Only intermolecular interactions are considered (defaults to True) Returns ------- int Number of detected Hydrogen bonds. """ _validate = { parse.Required('probes'): [parse.Molecule_name], 'radius': parse.All(parse.Coerce(float), parse.Range(min=0)), 'distance_tolerance': float, 'angle_tolerance': float, 'only_intermolecular': bool } def __init__(self, probes=None, radius=5.0, distance_tolerance=0.4, angle_tolerance=20.0, only_intermolecular=True, *args, **kwargs): ObjectiveProvider.__init__(self, **kwargs) self._probes = probes self.distance_tolerance = distance_tolerance self.angle_tolerance = angle_tolerance self.radius = radius self.intramodel = False if only_intermolecular else True def molecules(self, ind): return [m.compound.mol for m in ind._molecules.values()] def probes(self, ind): return [ind.find_molecule(p).compound.mol for p in self._probes] def evaluate(self, ind): """ Find H bonds within self.radius angstroms from self.probes, and return only those that interact with probe. Ie, discard those hbonds in that search space whose none of their atoms involved are not part of self.probe. """ molecules = self.molecules(ind) probe_atoms = [a for m in self.probes(ind) for a in m.atoms] test_atoms = self._surrounding_atoms(probe_atoms, molecules) hbonds = findHBonds(molecules, cacheDA=self._cache, donors=test_atoms, acceptors=test_atoms, distSlop=self.distance_tolerance, angleSlop=self.angle_tolerance, intermodel=True, intramodel=self.intramodel) hbonds = filterHBondsBySel(hbonds, probe_atoms, 'any') return len(hbonds) def display(self, bonds): """ Mock method to show a graphical depiction of the found H Bonds. """ return draw_interactions(bonds, name=self.name, startCol='00FFFF', endCol='00FFFF') ### def _surrounding_atoms(self, atoms, molecules): self.zone.clear() self.zone.add(atoms) self.zone.merge(chimera.selection.REPLACE, chimera.specifier.zone(self.zone, 'atom', None, self.radius, molecules)) return self.zone.atoms()
class Torsion(GeneProvider): """ Parameters ---------- target: str Name of gaudi.genes.molecule instance to perform rotation on flexibility : int or float Maximum number of degrees a bond can rotate max_bonds : Expected number of free rotations in molecule. Needed to store arbitrary rotations. anchor : str Molecule/atom_serial_number of reference atom for torsions rotatable_atom_types : list of str Which type of atom types (as in chimera.Atom.idatmType) should rotate. Defaults to ('C3', 'N3', 'C2', 'N2', 'P'). rotatable_atom_names : list of str Which type of atom names (as in chimera.Atom.name) should rotate. Defaults to (). rotatable_bonds : list of [SerialNumberAtom1, SerialNumberAtom2, SerialNumberAnchor] Concrete bonds that are allowed to rotate. Atoms have to be designated using their chimera serial number. IMPORTANT: if set, these will be the ONLY bonds allowed to rotate, ignoring other possible conditions (e.g. rotatable_atom_types, rotatable_atom_names...). Attributes ---------- allele : tuple of float For i rotatable bonds in molecule, it contains i floats which correspond to each torsion angle. As such, each falls within [-180.0, 180.0). Notes ----- .. todo :: `max_bonds` is now automatically computed, but probably won't deal nicely with block-built ligands. """ _validate = { parse.Required("target"): parse.Molecule_name, "flexibility": parse.Degrees, "max_bonds": parse.All(parse.Coerce(int), parse.Range(min=0)), "anchor": parse.Named_spec("molecule", "atom"), "rotatable_atom_types": [basestring], "rotatable_atom_names": [basestring], "rotatable_elements": [basestring], "non_rotatable_bonds": [ parse.All( [parse.Named_spec("molecule", "atom")], parse.Length(min=2, max=2) ) ], "rotatable_bonds": [parse.All([parse.Coerce(int)], parse.Length(min=3, max=3))], "precision": parse.All(parse.Coerce(int), parse.Range(min=-3, max=3)), } BONDS_ROTS = {} def __init__( self, target=None, flexibility=360.0, max_bonds=None, anchor=None, rotatable_atom_types=("C3", "N3", "C2", "N2", "P"), rotatable_atom_names=(), rotatable_elements=(), non_rotatable_bonds=(), rotatable_bonds=(), precision=1, **kwargs ): GeneProvider.__init__(self, **kwargs) self._kwargs = kwargs self.target = target self.flexibility = 360.0 if flexibility > 360 else flexibility self.max_bonds = max_bonds self.rotatable_atom_types = rotatable_atom_types self.rotatable_atom_names = rotatable_atom_names self.rotatable_elements = rotatable_elements self.non_rotatable_bonds = non_rotatable_bonds self.concrete_rotatable_bonds = rotatable_bonds self.precision = precision self._anchor = anchor self.allele = [self.random_angle() for i in xrange(50)] def __expression_hooks__(self): if self.max_bonds is None: self.max_bonds = len(self.rotatable_bonds) self.allele = [self.random_angle() for i in xrange(self.max_bonds)] def express(self): """ Apply rotations to rotatable bonds """ for alpha, br in zip(self.allele, self.rotatable_bonds): try: if all(a.idatmType in ("C2", "N2") for a in br.bond.atoms): alpha = 0 if alpha <= 0 else 180 br.adjustAngle(alpha - br.angle, br.rotanchor) # A null bondrot was returned -> non-rotatable bond except AttributeError: continue def unexpress(self): """ Undo the rotations """ for br in self.rotatable_bonds: br.adjustAngle(-br.angle, br.rotanchor) def mate(self, mate): self_allele, mate_allele = cxSimulatedBinaryBounded( self.allele, mate.allele, eta=self.cxeta, low=-0.5 * self.flexibility, up=0.5 * self.flexibility, ) self.allele[:] = [round(n, self.precision) for n in self_allele] mate.allele[:] = [round(n, self.precision) for n in mate_allele] def mutate(self, indpb): if random.random() < 0.5: allele, = mutPolynomialBounded( self.allele, indpb=self.indpb, eta=self.mteta, low=-0.5 * self.flexibility, up=0.5 * self.flexibility, ) self.allele[:] = [round(n, self.precision) for n in allele] else: self.allele = [self.random_angle() for i in xrange(self.max_bonds)] def clear_cache(self): GeneProvider.clear_cache() self.BONDS_ROTS.clear() ##### @property def molecule(self): return self.parent.find_molecule(self.target).compound.mol def random_angle(self): """ Returns a random angle within flexibility limits """ return round( random.uniform(-0.5 * self.flexibility, 0.5 * self.flexibility), self.precision, ) @property def rotatable_bonds(self): """ Gets potentially rotatable bonds in molecule First, it retrieves all the atoms. Then, the bonds are filtered, discarding coordination (pseudo)bonds and sort them by atom serial. For each bond, try to retrieve it from the cache. If unavailable, request a bond rotation object to chimera.BondRot. In this step, we have to discard non rotatable atoms (as requested by the user), and make sure the involved atoms are of compatible type. Namely, one of them must be either C3, N3, C2 or N2, and both of them, non-terminal (more than one neighbor). If the bond is valid, get the BondRot object. Chimera will complain if we already have requested that bond previously, or if the bond is in a cycle. Handle those exceptions silently, and get the next bond in that case. If no exceptions are raised, then store the rotation anchor in the BondRot object (that's the nearest atom in the bond to the molecular anchor), and store the BondRot object in the rotations cache. """ try: return self.molecule._rotatable_bonds except AttributeError: self.molecule._rotatable_bonds = list(self._compute_rotatable_bonds()) return self.molecule._rotatable_bonds def _compute_rotatable_bonds(self): bonds = sorted( self.molecule.bonds, key=lambda b: min(y.serialNumber for y in b.atoms) ) non_rotatable_bonds = [] for atom_a, atom_b in self.non_rotatable_bonds: a = self.parent.find_molecule(atom_a.molecule).find_atom(atom_a.atom) b = self.parent.find_molecule(atom_b.molecule).find_atom(atom_b.atom) bond = a.findBond(b) if bond: non_rotatable_bonds.append(bond) else: logger.warning("Atoms {} and {} are not bonded!".format(a, b)) if self.concrete_rotatable_bonds: rotatable_bonds = [] for atom_a, atom_b, anchor in self.concrete_rotatable_bonds: a = self.parent.find_molecule(self.target).find_atom(atom_a) b = self.parent.find_molecule(self.target).find_atom(atom_b) an = self.parent.find_molecule(self.target).find_atom(anchor) bond = a.findBond(b) if bond: try: br = chimera.BondRot(bond) except (chimera.error, ValueError) as v: if "cycle" in str(v) or "already used" in str(v): continue # discard bonds in cycles and used! raise else: br.rotanchor = an yield br else: logger.warning("Atoms {} and {} are not bonded!".format(a, b)) else: def conditions(*atoms): for a in atoms: # Must be satisfied by at least one atom if a.numBonds == 1 or a.element.isMetal: return False for a in atoms: if ( a.name == "DUM" or a.idatmType in self.rotatable_atom_types or a.name in self.rotatable_atom_names or a.element.name in self.rotatable_elements ): return True for b in bonds: if b in non_rotatable_bonds: continue if conditions(*b.atoms): try: br = chimera.BondRot(b) except (chimera.error, ValueError) as v: if "cycle" in str(v) or "already used" in str(v): continue # discard bonds in cycles and used! raise else: br.rotanchor = box.find_nearest(self.anchor, b.atoms) yield br @property def anchor(self): """ Get the molecular anchor. Ie, the *root* of the rotations, the fixed atom of the molecule. Usually, this is the target atom in the Search gene, but if we can't find it, get the nearest atom to the geometric center of the molecule, and if it's not possible, the ``donor`` atom of the molecule. """ try: return self.molecule._rotation_anchor except AttributeError: pass if self._anchor is not None: mol, atom = self._anchor try: molecule_gene = self.parent.find_molecule(mol) anchor = molecule_gene.find_atom(atom) except StopIteration: pass else: self.molecule._rotation_anchor = anchor return anchor target_gene = self.parent.find_molecule(self.target) try: if isinstance(self.target, str): mol = target_gene.compound.mol anchor = target_gene.find_atom(nearest_atom(mol, center(mol))) except (StopIteration, AttributeError): anchor = target_gene.compound.donor self.molecule._rotation_anchor = anchor return anchor
class DSX(ObjectiveProvider): """ DSX class Parameters ---------- protein : str The molecule name that is acting as a protein ligand : str The molecule name that is acting as a ligand binary : str, optional Path to the DSX binary. Only needed if ``drugscorex`` is not in PATH. potentials : str, optional Path to DSX potentials. Only needed if ``DSX_POTENTIALS`` env var has not been set by the installation process (``conda install -c insilichem drugscorex`` normally takes care of that). terms : list of bool, optional Enable (True) or disable (False) certain terms in the score function in this order: distance-dependent pair potentials, torsion potentials, intramolecular clashes, sas potentials, hbond potentials sorting : int, defaults to 1 Sorting mode. An int between 0-6, read binary help for -S:: -S int : Here you can specify the mode that affects how the results will be sorted. The default mode is '-S 1', which sorts the ligands in the same order as they are found in the lig_file. The following modes are possible:: 0: Same order as in the ligand file 1: Ordered by increasing total score 2: Ordered by increasing per-atom-score 3: Ordered by increasing per-contact-score 4: Ordered by increasing rmsd 5: Ordered by increasing torsion score 6: Ordered by increasing per-torsion-score cofactor_mode : int, defaults to 0 Cofactor handling mode. An int between 0-7, read binary help for -I:: -I int : Here you can specify the mode that affects how cofactors, waters and metals will be handeled. The default mode is '-I 1', which means, that all molecules are treated as part of the protein. If a structure should not be treated as part of the protein you have supply a seperate file with seperate MOLECULE entries corresponding to each MOLECULE entry in the ligand_file (It is assumed that the structure, e.g. a cofactor, was kept flexible in docking, so that there should be a different geometry corresponding to each solution. Otherwise it won't make sense not to treat it as part of the protein.). The following modes are possible: 0: cofactors, waters and metals interact with protein, ligand and each other 1: cofactors, waters and metals are treated as part of the protein 2: cofactors and metals are treated as part of the protein (waters as in mode 0) 3: cofactors and waters are treated as part of the protein 4: cofactors are treated as part of the protein 5: metals and waters are treated as part of the protein 6: metals are treated as part of the protein 7: waters are treated as part of the protein Please note: Only those structures can be treated individually, which are supplied in seperate files. with_covalent : bool, defaults to False Whether to deal with covalently bonded atoms as normal atoms (False) or not (True) with_metals : bool, defaults to True Whether to deal with metal atoms as normal atoms (False) or not (True) Returns ------- float Interaction energy as reported by DSX output logs. """ _validate = { parse.Required('proteins'): [parse.Molecule_name], parse.Required('ligands'): [parse.Molecule_name], 'binary': parse.ExpandUserPathExists, 'potentials': parse.ExpandUserPathExists, 'terms': parse.All([parse.Coerce(bool)], parse.Length(min=5, max=5)), 'sorting': parse.All(parse.Coerce(int), parse.Range(min=0, max=6)), 'cofactor_mode': parse.All(parse.Coerce(int), parse.Range(min=0, max=7)), 'with_covalent': parse.Coerce(bool), 'with_metals': parse.Coerce(bool) } def __init__(self, binary=None, potentials=None, proteins=('Protein', ), ligands=('Ligand', ), terms=None, sorting=1, cofactor_mode=0, with_covalent=False, with_metals=True, *args, **kwargs): ObjectiveProvider.__init__(self, **kwargs) self.binary = find_executable( 'drugscorex') if binary is None else binary if not self.binary: raise ValueError( 'Could not find `drugscorex` executable. Please install it ' 'with `conda install -c insilichem drugscorex` or manually ' 'specify the location with `binary` and `potentials` keys.') self.potentials = potentials self.protein_names = proteins self.ligand_names = ligands self.terms = terms self.sorting = sorting self.cofactor_mode = cofactor_mode self.with_covalent = with_covalent self.with_metals = with_metals self._oldworkingdir = os.getcwd() self._paths = {} if os.name == 'posix' and os.path.exists('/dev/shm'): self.tmpdir = '/dev/shm' else: self.tmpdir = default_tempdir() def get_molecule_by_name(self, ind, *names): """ Get a molecule gene instance of individual by its name """ for name in names: yield ind.find_molecule(name) def evaluate(self, ind): """ Run a subprocess calling DSX binary with provided options, and parse the results. Clean tmp files at exit. """ self.tmpfile = os.path.join(self.tmpdir, next(tempnames())) proteins = list(self.get_molecule_by_name(ind, *self.protein_names)) ligands = list(self.get_molecule_by_name(ind, *self.ligand_names)) self.prepare_proteins(proteins) self.prepare_ligands(ligands) command = self.prepare_command() try: os.chdir(self.tmpdir) stream = subprocess.check_output(command, universal_newlines=True) except subprocess.CalledProcessError: logger.warning("Could not run DSX with command %s", command) return -100000 * self.weight else: return self.parse_output(stream) finally: self.clean() os.chdir(self._oldworkingdir) def prepare_proteins(self, proteins): proteinpath = '{}_proteins.pdb'.format(self.tmpfile) last_protein = proteins.pop() last_protein.write(absolute=proteinpath, combined_with=proteins, filetype='pdb') self._paths['proteins'] = proteinpath def prepare_ligands(self, ligands): ligandpath = '{}_ligands.mol2'.format(self.tmpfile) metalpath = '{}_metals.mol2'.format(self.tmpfile) ligand_mols = [lig.compound.mol for lig in ligands] if self.with_metals: # Split metals from ligand nonmetal_mols, metal_mols = [], [] for ligand in ligand_mols: nonmetals, metals = [], [] for atom in ligand.atoms: if atom.element.isMetal: metals.append(atom) else: nonmetals.append(atom) nonmetal_mols.append(molecule_from_atoms(ligand, nonmetals)) if metals: metal_mols.append(molecule_from_atoms(ligand, metals)) if metal_mols: writeMol2(metal_mols, metalpath, temporary=True) self._paths['metals'] = metalpath ligand_mols = nonmetal_mols writeMol2(ligand_mols, ligandpath, temporary=True, multimodelHandling='combined') self._paths['ligands'] = ligandpath def prepare_command(self): cmd = [self.binary] if self.with_covalent: cmd.append('-c') cmd.extend( ['-P', self._paths['proteins'], '-L', self._paths['ligands']]) if self.with_metals: metalpath = self._paths.get('metals') if metalpath: cmd.extend(['-M', metalpath]) if self.cofactor_mode is not None: cmd.extend(['-I', self.cofactor_mode]) if self.sorting is not None: cmd.extend(['-S', self.sorting]) if self.terms is not None: T0, T1, T2, T3, T4 = [1.0 * t for t in self.terms] cmd.extend(['-T0', T0, '-T1', T1, '-T2', T2, '-T3', T3, '-T4', T4]) if self.potentials is not None: cmd.extend(['-D', self.potentials]) return map(str, cmd) def parse_output(self, stream): # 1. Get output filename from stdout (located at working directory) # 2. Find line '@RESULTS' and go to sixth line below # 3. The score is in the first row of the table, at the third field dsx_results = os.path.join(self.tmpdir, stream.splitlines()[-2].split()[-1]) self._paths['output'] = dsx_results with open(dsx_results) as f: lines = f.read().splitlines() i = lines.index('@RESULTS') score = lines[i + 4].split('|')[3].strip() return float(score) def clean(self): for p in self._paths.values(): os.remove(p) self._paths.clear()
class Solvation(ObjectiveProvider): """ Solvation class Parameters ---------- targets : [str] Names of the molecule genes being analyzed threshold : float, optional, default=0 Optimize the difference to this value radius : float, optional, default=5.0 Max distance to search for neighbor atoms from targets. method : str, optional, default=area Which method should be used. Both methods compute the surface of the solvated molecule. `area` returns the surface area of such surface, while `volume` returns the volume occuppied by the model. Returns ------- float Surface area of solvated shell, in A² (if method=area), or volume of solvated shell, in A³ (if method=volume). """ _validate = { parse.Required('targets'): [parse.Molecule_name], 'threshold': parse.All(parse.Coerce(float), parse.Range(min=0)), 'radius': parse.All(parse.Coerce(float), parse.Range(min=0)), 'method': parse.In(['volume', 'area']) } def __init__(self, targets=None, threshold=0.0, radius=5.0, method='area', *args, **kwargs): ObjectiveProvider.__init__(self, **kwargs) self._targets = targets self.threshold = threshold self.radius = radius self.method = method if method == 'area': self.evaluate = self.evaluate_area else: self.evaluate = self.evaluate_volume def targets(self, ind): return [ ind.find_molecule(target).compound.mol for target in self._targets ] def molecules(self, ind): return tuple(m.compound.mol for m in ind._molecules.values()) def surface(self, ind): atoms = self.zone_atoms(self.targets(ind), self.molecules(ind)) return grid_sas_surface(atoms) def evaluate_area(self, ind): return abs(surface_area(*self.surface(ind)) - self.threshold) def evaluate_volume(self, ind): return abs(enclosed_volume(*self.surface(ind))[0] - self.threshold) def zone_atoms(self, probes, molecules): self.zone.clear() self.zone.add([a for probe in probes for a in probe.atoms]) if self.radius: self.zone.merge( chimera.selection.REPLACE, chimera.specifier.zone(self.zone, 'atom', None, self.radius, molecules)) return self.zone.atoms()
class Energy(ObjectiveProvider): """ Calculate the energy of a system Parameters ---------- targets : list of str, default=None If set, which molecules should be evaluated. Else, all will be evaluated. forcefields : list of str, default=('amber99sbildn.xml',) Which forcefields to use auto_parametrize: list of str, default=None List of Molecule instances GAUDI should try to auto parametrize with antechamber. parameters : list of 2-item list of str List of (gaff.mol2, .frcmod) files to use as parametrization source. platform : str Which platform to use for calculations. Choose between CPU, CUDA, OpenCL. Returns ------- float The estimated potential energy, in kJ/mol """ _validate = { 'targets': [parse.Molecule_name], 'forcefields': [ parse.Any(parse.ExpandUserPathExists, parse.In(_openmm_builtin_forcefields)) ], 'auto_parametrize': [parse.Molecule_name], 'parameters': [parse.All([parse.ExpandUserPathExists], parse.Length(min=2, max=2))], 'platform': parse.In(['CUDA', 'OpenCL', 'CPU']) } def __init__(self, targets=None, forcefields=('amber99sbildn.xml', ), auto_parametrize=None, parameters=None, platform=None, *args, **kwargs): if kwargs.get('precision', 6) < 6: kwargs['precision'] = 6 ObjectiveProvider.__init__(self, **kwargs) self.auto_parametrize = auto_parametrize self._targets = targets self._parameters = parameters self.platform = platform self.topology = None self._simulation = None additional_ffxml = [] if parameters: additional_ffxml.append(create_ffxml_file(*zip(*parameters))) if auto_parametrize: filenames = [ g.path for m in auto_parametrize for g in self.environment.cfg.genes if g.name == m ] additional_ffxml.append(self._gaff2xml(*filenames)) self._forcefields = tuple(forcefields) + tuple(additional_ffxml) self.forcefield = openmm_app.ForceField(*self._forcefields) def evaluate(self, individual): """ Calculates the energy of current individual Notes ----- For static calculations, where molecules are essentially always the same, but with different coordinates, we only need to generate topologies once. However, for dynamic jobs, with potentially different molecules involved each time, we cannot guarantee having the same topology. As a result, we generate it again for each evaluation. """ molecules = self.molecules(individual) coordinates = self.chimera_molecule_to_openmm_positions(*molecules) # Build topology if it's first time or a dynamic job if self.topology is None or not self._gaudi_is_static(individual): self.topology = self.chimera_molecule_to_openmm_topology( *molecules) self._simulation = None # This forces a Simulation rebuild return self.calculate_energy(coordinates) def molecules(self, individual): if self._targets is None: return [m.compound.mol for m in individual._molecules.values()] else: return [ individual.find_molecule(t).compound.mol for t in self._targets ] @property def simulation(self): """ Build a new OpenMM simulation if not yet defined and return it Notes ----- self.topology must be defined previously! Use self.chimera_molecule_to_openmm_topology to set it. """ if self._simulation is None: system = self.forcefield.createSystem( self.topology, nonbondedMethod=openmm_app.CutoffNonPeriodic, nonbondedCutoff=1.0 * unit.nanometers, rigidWater=True, constraints=None) integrator = openmm.VerletIntegrator(0.001) if self.platform is not None: platform = openmm.Platform.getPlatformByName(self.platform), else: platform = () self._simulation = openmm_app.Simulation(self.topology, system, integrator, *platform) return self._simulation def calculate_energy(self, coordinates): """ Set up an OpenMM simulation with default parameters and return the potential energy of the initial state Parameters ---------- coordinates : simtk.unit.Quantity Positions of the atoms in the system Returns ------- potential_energy : float Potential energy of the system, in kJ/mol """ self.simulation.context.setPositions(coordinates) # Retrieve initial energy state = self.simulation.context.getState(getEnergy=True) return state.getPotentialEnergy()._value @staticmethod def chimera_molecule_to_openmm_topology(*molecules): """ Convert a Chimera Molecule object to OpenMM structure, providing topology and coordinates. Parameters ---------- molecule : chimera.Molecule Returns ------- topology : simtk.openmm.app.topology.Topology coordinates : simtk.unit.Quantity """ # Create topology atoms, residues, chains = {}, {}, {} topology = openmm_app.Topology() for i, mol in enumerate(molecules): for a in mol.atoms: chain_id = (i, a.residue.id.chainId) try: chain = chains[chain_id] except KeyError: chain = chains[chain_id] = topology.addChain() r = a.residue try: residue = residues[r] except KeyError: residue = residues[r] = topology.addResidue(r.type, chain) name = a.name element = openmm_app.Element.getByAtomicNumber( a.element.number) serial = a.serialNumber atoms[a] = topology.addAtom(name, element, residue, serial) for b in mol.bonds: topology.addBond(atoms[b.atoms[0]], atoms[b.atoms[1]]) return topology @staticmethod def chimera_molecule_to_openmm_positions(*molecules): # Get positions positions = [ atom_positions(m.atoms, m.openState.xform) for m in molecules ] all_positions = numpy.concatenate(positions) return unit.Quantity(all_positions, unit=unit.angstrom) @staticmethod def _gaff2xml(*filenames, **kwargs): """ Use OpenMolTools wrapper to run antechamber programatically and auto parametrize requested molecules. Parameters ---------- filenames: list of str List of the filenames of the molecules to parametrize Returns ------- ffxmls : StringIO Compiled ffxml file produced by antechamber and openmoltools converter """ frcmods, gaffmol2s = [], [] for filename in filenames: name = '.'.join(filename.split('.')[:-1]) gaffmol2, frcmod = run_antechamber(name, filename, **kwargs) frcmods.append(frcmod) gaffmol2s.append(gaffmol2) return create_ffxml_file(gaffmol2s, frcmods) def _gaudi_is_static(self, individual): """ Check if this essay is performing topology changes. Genes that can change topologies: - gaudi.genes.rotamers with mutations ON - gaudi.genes.molecule with block building enabled Parameters ---------- individual : gaudi.base.Individual The individual to be analyzed for dynamic behaviour Returns ------- bool """ for gene in individual.genes.values(): if gene.__class__.__name__ == 'Mutamers': if gene.mutations: return False if gene.__class__.__name__ == 'Molecule': if len(gene.catalog) > 1: return False return True
class Trajectory(GeneProvider): """ Parameters ---------- target : str The Molecule that contains the topology of the trajectory. path : str Path to a MD trajectory file, as supported by mdtraj. max_frame : int Last frame of the trajectory that can be loaded. stride : int, optional Only load one in every `stride` frames preload : bool, optional Load the full trajectory in memory to accelerate expression. Not recommended for large files! Attributes ---------- allele : int The index of a frame in the MD trajectory. _traj : dict Alias to the frames cache """ _validate = { parse.Required('target'): parse.Molecule_name, parse.Required('path'): parse.ExpandUserPathExists, parse.Required('max_frame'): parse.All(parse.Coerce(int), parse.Range(min=1)), 'stride': parse.All(parse.Coerce(int), parse.Range(min=1)), 'preload': bool, } def __init__(self, target=None, path=None, max_frame=None, stride=1, preload=False, **kwargs): GeneProvider.__init__(self, **kwargs) self.target = target self.path = path self.max_frame = max_frame self.stride = stride self.preload = preload try: self._traj = self._cache[self.name] except KeyError: self._traj = self._cache[self.name] = {} def __ready__(self): self.allele = self.random_frame_number() self._original_xyz = self.molecule.xyz(transformed=False) def __expression_hooks__(self): if self.preload and self.path not in self._traj: self._traj[self.path] = mdtraj.load(self.path, top=self.topology) @property def molecule(self): """ The target Molecule gene """ return self.parent.find_molecule(self.target) @property def topology(self): """ Returns the equivalent mdtraj Topology object of the currently expressed Chimera molecule """ mol = self.molecule.compound.mol try: return mol._mdtraj_topology except AttributeError: openmm_top = Energy.chimera_molecule_to_openmm_topology(mol) mdtraj_top = mdtraj.Topology.from_openmm(openmm_top) mol._mdtraj_topology = mdtraj_top return mdtraj_top def express(self): """ Load the frame requested by the current allele into a new CoordSet object (always at index 1) and set that as the active one. """ traj = self.load_frame(self.allele) coords = traj.xyz[0] * 10 for a, xyz in zip(self.molecule.compound.mol.atoms, coords): a.setCoord(chimera.Point(*xyz)) def unexpress(self): """ Set the original coordinates (stored at mol.coordSets[0]) as the active ones. """ for a, xyz in zip(self.molecule.compound.mol.atoms, self._original_xyz): a.setCoord(chimera.Point(*xyz)) def mate(self, mate): """ Simply exchange alleles. Can't try to interpolate an intermediate structure because the result wouldn't probably belong to the original trajectory! """ self.allele, mate.allele = mate.allele, self.allele def mutate(self, indpb): if random.random() < indpb: self.allele = self.random_frame_number() def random_frame_number(self): return random.choice(range(0, self.max_frame, self.stride)) def load_frame(self, n): if self.preload: return self._traj[self.path][n] return mdtraj.load_frame(self.path, self.allele, top=self.topology)
class ObjectiveProvider(object): """ Base class that every `objectives` plugin MUST inherit. Mount point for plugins implementing new objectives to be evaluated by DEAP. The objective resides within the Fitness attribute of the individual. Do whatever you want, but use an evaluate() function to return the results. Apart from that, there's no requirements. The base class includes some useful attributes, so don't forget to call `ObjectiveProvider.__init__` in your overriden `__init__`. For example, `self.zone` is a `Chimera.selection.ItemizedSelection` object which is shared among all objectives. Use that to get atoms in the surrounding of the target gene, and remember to `self.zone.clear()` it before use. --- From (M.A. itself)[http://martyalchin.com/2008/jan/10/simple-plugin-framework/]: Now that we have a mount point, we can start stacking plugins onto it. As mentioned above, individual plugins will subclass the mount point. Because that also means inheriting the metaclass, the act of subclassing alone will suffice as plugin registration. Of course, the goal is to have plugins actually do something, so there would be more to it than just defining a base class, but the point is that the entire contents of the class declaration can be specific to the plugin being written. The plugin framework itself has absolutely no expectation for how you build the class, allowing maximum flexibility. Duck typing at its finest. """ __metaclass__ = plugin.PluginMount _cache = {} _validate = {} _schema = { parse.Required('environment'): Environment, 'module': parse.Importable, 'name': str, 'weight': parse.Coerce(float), 'zone': chimera.selection.ItemizedSelection, 'precision': parse.All(parse.Coerce(int), parse.Range(min=0, max=9)) } def __init__(self, environment=None, name=None, weight=None, zone=None, precision=3, **kwargs): self.environment = environment self.name = name if name is not None else str(uuid4()) self.weight = weight self.zone = zone if zone is not None else chimera.selection.ItemizedSelection( ) self.precision = precision def __ready__(self): pass @abc.abstractmethod def evaluate(self, individual): """ Return the score of the individual under the current conditions. """ @classmethod def clear_cache(cls): cls._cache.clear() @classmethod def validate(cls, data, schema=None): schema = cls._schema.copy() if schema is None else schema schema.update(cls._validate) return parse.validate(schema, data) @classmethod def with_validation(cls, **kwargs): cls.__init__(**cls.validate(kwargs))