def _create_phase(self, phase, reference_system, positions, atom_indices, thermodynamic_state, protocols=None, options=None, mpicomm=None): """ Create a repex object for a specified phase. Parameters ---------- phase : str The phase being initialized (one of ['complex', 'solvent', 'vacuum']) reference_system : simtk.openmm.System The reference system object from which alchemical intermediates are to be construcfted. positions : list of simtk.unit.Qunatity objects containing (natoms x 3) positions (as np or lists) The list of positions to be used to seed replicas in a round-robin way. atom_indices : dict atom_indices[phase][component] is the set of atom indices associated with component, where component is ['ligand', 'receptor'] thermodynamic_state : ThermodynamicState Thermodynamic state from which reference temperature and pressure are to be taken. protocols : dict of list of AlchemicalState, optional, default=None If specified, the alchemical protocol protocols[phase] will be used for phase 'phase' instead of the default. options : dict of str, optional, default=None If specified, these options will override default repex simulation options. """ # Combine simulation options with defaults to create repex options. repex_options = dict(self.default_options.items() + options.items()) # Make sure positions argument is a list of coordinate snapshots. if hasattr(positions, 'unit'): # Wrap in list. positions = [positions] # Check the dimensions of positions. for index in range(len(positions)): # Make sure it is recast as a np array. positions[index] = unit.Quantity(np.array(positions[index] / positions[index].unit), positions[index].unit) [natoms, ndim] = (positions[index] / positions[index].unit).shape if natoms != reference_system.getNumParticles(): raise Exception("positions argument must be a list of simtk.unit.Quantity of (natoms,3) lists or np array with units compatible with nanometers.") # Create metadata storage. metadata = dict() # Make a deep copy of the reference system so we don't accidentally modify it. reference_system = copy.deepcopy(reference_system) # TODO: Use more general approach to determine whether system is periodic. is_periodic = self._is_periodic(reference_system) # Make sure pressure is None if not periodic. if not is_periodic: thermodynamic_state.pressure = None # Compute standard state corrections for complex phase. metadata['standard_state_correction'] = 0.0 # TODO: Do we need to include a standard state correction for other phases in periodic boxes? if phase == 'complex-implicit': # Impose restraints for complex system in implicit solvent to keep ligand from drifting too far away from receptor. logger.debug("Creating receptor-ligand restraints...") reference_positions = positions[0] if self.restraint_type == 'harmonic': restraints = HarmonicReceptorLigandRestraint(thermodynamic_state, reference_system, reference_positions, atom_indices['receptor'], atom_indices['ligand']) elif self.restraint_type == 'flat-bottom': restraints = FlatBottomReceptorLigandRestraint(thermodynamic_state, reference_system, reference_positions, atom_indices['receptor'], atom_indices['ligand']) else: raise Exception("restraint_type of '%s' is not supported." % self.restraint_type) force = restraints.getRestraintForce() # Get Force object incorporating restraints reference_system.addForce(force) metadata['standard_state_correction'] = restraints.getStandardStateCorrection() # standard state correction in kT elif phase == 'complex-explicit': # For periodic systems, we do not use a restraint, but must add a standard state correction for the box volume. # TODO: What if the box volume fluctuates during the simulation? box_vectors = reference_system.getDefaultPeriodicBoxVectors() box_volume = thermodynamic_state._volume(box_vectors) STANDARD_STATE_VOLUME = 1660.53928 * unit.angstrom**3 metadata['standard_state_correction'] = np.log(STANDARD_STATE_VOLUME / box_volume) # TODO: Check sign. # Use default alchemical protocols if not specified. if not protocols: protocols = self.default_protocols # Create alchemically-modified states using alchemical factory. logger.debug("Creating alchemically-modified states...") #factory = AbsoluteAlchemicalFactory(reference_system, ligand_atoms=atom_indices['ligand'], test_positions=positions[0], platform=repex_options['platform']) factory = AbsoluteAlchemicalFactory(reference_system, ligand_atoms=atom_indices['ligand']) alchemical_states = protocols[phase] alchemical_system = factory.alchemically_modified_system thermodynamic_state.system = alchemical_system # Check systems for finite energies. finite_energy_check = False if finite_energy_check: logger.debug("Checking energies are finite for all alchemical systems.") integrator = openmm.VerletIntegrator(1.0 * unit.femtosecond) context = openmm.Context(alchemical_system, integrator) context.setPositions(positions[0]) for alchemical_state in alchemical_states: AbsoluteAlchemicalFactory.perturbContext(context, alchemical_state) potential = context.getState(getEnergy=True).getPotentialEnergy() if np.isnan(potential / unit.kilocalories_per_mole): raise Exception("Energy for system %d is NaN." % index) del context, integrator logger.debug("All energies are finite.") # Randomize ligand position if requested, but only for implicit solvent systems. if self.randomize_ligand and (phase == 'complex-implicit'): logger.debug("Randomizing ligand positions and excluding overlapping configurations...") randomized_positions = list() nstates = len(systems) for state_index in range(nstates): positions_index = np.random.randint(0, len(positions)) current_positions = positions[positions_index] new_positions = ModifiedHamiltonianExchange.randomize_ligand_position(current_positions, atom_indices['receptor'], atom_indices['ligand'], self.randomize_ligand_sigma_multiplier * restraints.getReceptorRadiusOfGyration(), self.randomize_ligand_close_cutoff) randomized_positions.append(new_positions) positions = randomized_positions if self.randomize_ligand and (phase == 'complex-explicit'): logger.warning("Ligand randomization requested, but will not be performed for explicit solvent simulations.") # Identify whether any atoms will be displaced via MC, unless option is turned off. mc_atoms = None if self.mc_displacement_sigma: mc_atoms = list() if 'ligand' in atom_indices: mc_atoms = atom_indices['ligand'] # Set up simulation. # TODO: Support MPI initialization? logger.debug("Creating replica exchange object...") store_filename = os.path.join(self._store_directory, phase + '.nc') self._store_filenames[phase] = store_filename simulation = ModifiedHamiltonianExchange(store_filename, mpicomm=mpicomm) simulation.create(thermodynamic_state, alchemical_states, positions, displacement_sigma=self.mc_displacement_sigma, mc_atoms=mc_atoms, options=repex_options, metadata=metadata) # Initialize simulation. # TODO: Use the right scheme for initializing the simulation without running. #logger.debug("Initializing simulation...") #simulation.run(0) # TODO: Process user-supplied options. # Clean up simulation. del simulation return
def _create_phase(self, thermodynamic_state, alchemical_phase): """ Create a repex object for a specified phase. Parameters ---------- thermodynamic_state : ThermodynamicState (System need not be defined) Thermodynamic state from which reference temperature and pressure are to be taken. alchemical_phase : AlchemicalPhase The alchemical phase to be created. """ # We add default repex options only on creation, on resume repex will pick them from the store file repex_parameters = { 'number_of_equilibration_iterations': 0, 'number_of_iterations': 100, 'timestep': 2.0 * unit.femtoseconds, 'collision_rate': 5.0 / unit.picoseconds, 'minimize': False, 'show_mixing_statistics': True, # this causes slowdown with iteration and should not be used for production 'displacement_sigma': 1.0 * unit. nanometers # attempt to displace ligand by this stddev will be made each iteration } repex_parameters.update(self._repex_parameters) # Convenience variables positions = alchemical_phase.positions reference_system = copy.deepcopy(alchemical_phase.reference_system) atom_indices = alchemical_phase.atom_indices alchemical_states = alchemical_phase.protocol # If temperature and pressure are specified, make sure MonteCarloBarostat is attached. if thermodynamic_state.temperature and thermodynamic_state.pressure: forces = { reference_system.getForce(index).__class__.__name__: reference_system.getForce(index) for index in range(reference_system.getNumForces()) } if 'MonteCarloAnisotropicBarostat' in forces: raise Exception( 'MonteCarloAnisotropicBarostat is unsupported.') if 'MonteCarloBarostat' in forces: logger.debug( 'MonteCarloBarostat found: Setting default temperature and pressure.' ) barostat = forces['MonteCarloBarostat'] # Set temperature and pressure. try: barostat.setDefaultTemperature( thermodynamic_state.temperature) except AttributeError: # versions previous to OpenMM7.1 barostat.setTemperature(thermodynamic_state.temperature) barostat.setDefaultPressure(state.pressure) else: # Create barostat and add it to the system if it doesn't have one already. logger.debug('MonteCarloBarostat not found: Creating one.') barostat = openmm.MonteCarloBarostat( thermodynamic_state.pressure, thermodynamic_state.temperature) reference_system.addForce(barostat) # Check the dimensions of positions. for index in range(len(positions)): n_atoms, _ = (positions[index] / positions[index].unit).shape if n_atoms != reference_system.getNumParticles(): err_msg = "Phase {}: number of atoms in positions {} and and " \ "reference system differ ({} and {} respectively)" err_msg.format(alchemical_phase.name, index, n_atoms, reference_system.getNumParticles()) logger.error(err_msg) raise RuntimeError(err_msg) # Inizialize metadata storage. metadata = dict() # Store a serialized copy of the reference system. metadata['reference_system'] = openmm.XmlSerializer.serialize( reference_system) metadata['topology'] = utils.serialize_topology( alchemical_phase.reference_topology) # TODO: Use more general approach to determine whether system is periodic. is_periodic = self._is_periodic(reference_system) is_complex_explicit = len(atom_indices['receptor']) > 0 and is_periodic is_complex_implicit = len( atom_indices['receptor']) > 0 and not is_periodic # Make sure pressure is None if not periodic. if not is_periodic: thermodynamic_state.pressure = None # Create a copy of the system for which the fully-interacting energy is to be computed. # For explicit solvent calculations, an enlarged cutoff is used to account for the anisotropic dispersion correction. fully_interacting_system = copy.deepcopy(reference_system) if is_periodic: # Expand cutoff to maximum allowed # TODO: Should we warn if cutoff can't be extended enough? # TODO: Should we extend to some minimum cutoff rather than the maximum allowed? box_vectors = fully_interacting_system.getDefaultPeriodicBoxVectors( ) max_allowed_cutoff = 0.499 * max([ max(vector) for vector in box_vectors ]) # TODO: Correct this for non-rectangular boxes logger.debug( 'Setting cutoff for fully interacting system to maximum allowed (%s)' % str(max_allowed_cutoff)) for force_index in range(fully_interacting_system.getNumForces()): force = fully_interacting_system.getForce(force_index) if hasattr(force, 'setCutoffDistance'): force.setCutoffDistance(max_allowed_cutoff) if hasattr(force, 'setCutoff'): force.setCutoff(max_allowed_cutoff) # Construct thermodynamic state fully_interacting_state = copy.deepcopy(thermodynamic_state) fully_interacting_state.system = fully_interacting_system # Compute standard state corrections for complex phase. metadata['standard_state_correction'] = 0.0 # TODO: Do we need to include a standard state correction for other phases in periodic boxes? if is_complex_implicit: # Impose restraints for complex system in implicit solvent to keep ligand from drifting too far away from receptor. logger.debug("Creating receptor-ligand restraints...") reference_positions = positions[0] if self._restraint_type == 'harmonic': restraints = HarmonicReceptorLigandRestraint( thermodynamic_state, reference_system, reference_positions, atom_indices['receptor'], atom_indices['ligand']) elif self._restraint_type == 'flat-bottom': restraints = FlatBottomReceptorLigandRestraint( thermodynamic_state, reference_system, reference_positions, atom_indices['receptor'], atom_indices['ligand']) else: raise Exception("restraint_type of '%s' is not supported." % self._restraint_type) force = restraints.getRestraintForce( ) # Get Force object incorporating restraints reference_system.addForce(force) metadata[ 'standard_state_correction'] = restraints.getStandardStateCorrection( ) # standard state correction in kT elif is_complex_explicit: # For periodic systems, we do not use a restraint, but must add a standard state correction for the box volume. # TODO: What if the box volume fluctuates during the simulation? box_vectors = reference_system.getDefaultPeriodicBoxVectors() box_volume = thermodynamic_state._volume(box_vectors) STANDARD_STATE_VOLUME = 1660.53928 * unit.angstrom**3 metadata['standard_state_correction'] = -np.log( STANDARD_STATE_VOLUME / box_volume) # Create alchemically-modified states using alchemical factory. logger.debug("Creating alchemically-modified states...") try: alchemical_indices = atom_indices[ 'ligand_counterions'] + atom_indices['ligand'] except KeyError: alchemical_indices = atom_indices['ligand'] factory = AbsoluteAlchemicalFactory(reference_system, ligand_atoms=alchemical_indices, **self._alchemy_parameters) alchemical_system = factory.alchemically_modified_system thermodynamic_state.system = alchemical_system # Check systems for finite energies. # TODO: Refactor this into another function. finite_energy_check = False if finite_energy_check: logger.debug( "Checking energies are finite for all alchemical systems.") integrator = openmm.VerletIntegrator(1.0 * unit.femtosecond) context = openmm.Context(alchemical_system, integrator) context.setPositions(positions[0]) for alchemical_state in alchemical_states: AbsoluteAlchemicalFactory.perturbContext( context, alchemical_state) potential = context.getState( getEnergy=True).getPotentialEnergy() if np.isnan(potential / unit.kilocalories_per_mole): raise Exception("Energy for system %d is NaN." % index) del context, integrator logger.debug("All energies are finite.") # Randomize ligand position if requested, but only for implicit solvent systems. if self._randomize_ligand and is_complex_implicit: logger.debug( "Randomizing ligand positions and excluding overlapping configurations..." ) randomized_positions = list() nstates = len(alchemical_states) for state_index in range(nstates): positions_index = np.random.randint(0, len(positions)) current_positions = positions[positions_index] new_positions = ModifiedHamiltonianExchange.randomize_ligand_position( current_positions, atom_indices['receptor'], atom_indices['ligand'], self._randomize_ligand_sigma_multiplier * restraints.getReceptorRadiusOfGyration(), self._randomize_ligand_close_cutoff) randomized_positions.append(new_positions) positions = randomized_positions if self._randomize_ligand and is_complex_explicit: logger.warning( "Ligand randomization requested, but will not be performed for explicit solvent simulations." ) # Identify whether any atoms will be displaced via MC, unless option is turned off. mc_atoms = None if self._mc_displacement_sigma: mc_atoms = list() if 'ligand' in atom_indices: mc_atoms = atom_indices['ligand'] # Set up simulation. # TODO: Support MPI initialization? logger.debug("Creating replica exchange object...") store_filename = os.path.join(self._store_directory, alchemical_phase.name + '.nc') self._store_filenames[alchemical_phase.name] = store_filename simulation = ModifiedHamiltonianExchange(store_filename) simulation.create(thermodynamic_state, alchemical_states, positions, displacement_sigma=self._mc_displacement_sigma, mc_atoms=mc_atoms, options=repex_parameters, metadata=metadata, fully_interacting_state=fully_interacting_state) # Initialize simulation. # TODO: Use the right scheme for initializing the simulation without running. #logger.debug("Initializing simulation...") #simulation.run(0) # Clean up simulation. del simulation # Add to list of phases that have been set up. self._phases.append(alchemical_phase.name) return
def _create_phase(self, phase, reference_system, positions, atom_indices, thermodynamic_state, protocols=None): """ Create a repex object for a specified phase. Parameters ---------- phase : str The phase being initialized (one of ['complex', 'solvent', 'vacuum']) reference_system : simtk.openmm.System The reference system object from which alchemical intermediates are to be construcfted. positions : list of simtk.unit.Qunatity objects containing (natoms x 3) positions (as np or lists) The list of positions to be used to seed replicas in a round-robin way. atom_indices : dict atom_indices[phase][component] is the set of atom indices associated with component, where component is ['ligand', 'receptor', 'complex', 'solvent', 'ligand_counterions'] thermodynamic_state : ThermodynamicState Thermodynamic state from which reference temperature and pressure are to be taken. protocols : dict of list of AlchemicalState, optional, default=None If specified, the alchemical protocol protocols[phase] will be used for phase 'phase' instead of the default. """ # We add default repex options only on creation, on resume repex will pick them from the store file repex_parameters = { 'number_of_equilibration_iterations': 0, 'number_of_iterations': 100, 'timestep': 2.0 * unit.femtoseconds, 'collision_rate': 5.0 / unit.picoseconds, 'minimize': False, 'show_mixing_statistics': True, # this causes slowdown with iteration and should not be used for production 'displacement_sigma': 1.0 * unit.nanometers # attempt to displace ligand by this stddev will be made each iteration } repex_parameters.update(self._repex_parameters) # Make sure positions argument is a list of coordinate snapshots. if hasattr(positions, 'unit'): # Wrap in list. positions = [positions] # Check the dimensions of positions. for index in range(len(positions)): # Make sure it is recast as a np array. positions[index] = unit.Quantity(np.array(positions[index] / positions[index].unit), positions[index].unit) [natoms, ndim] = (positions[index] / positions[index].unit).shape if natoms != reference_system.getNumParticles(): raise Exception("positions argument must be a list of simtk.unit.Quantity of (natoms,3) lists or np array with units compatible with nanometers.") # Create metadata storage. metadata = dict() # Make a deep copy of the reference system so we don't accidentally modify it. reference_system = copy.deepcopy(reference_system) # TODO: Use more general approach to determine whether system is periodic. is_periodic = self._is_periodic(reference_system) # Make sure pressure is None if not periodic. if not is_periodic: thermodynamic_state.pressure = None # Compute standard state corrections for complex phase. metadata['standard_state_correction'] = 0.0 # TODO: Do we need to include a standard state correction for other phases in periodic boxes? if phase == 'complex-implicit': # Impose restraints for complex system in implicit solvent to keep ligand from drifting too far away from receptor. logger.debug("Creating receptor-ligand restraints...") reference_positions = positions[0] if self._restraint_type == 'harmonic': restraints = HarmonicReceptorLigandRestraint(thermodynamic_state, reference_system, reference_positions, atom_indices['receptor'], atom_indices['ligand']) elif self._restraint_type == 'flat-bottom': restraints = FlatBottomReceptorLigandRestraint(thermodynamic_state, reference_system, reference_positions, atom_indices['receptor'], atom_indices['ligand']) else: raise Exception("restraint_type of '%s' is not supported." % self._restraint_type) force = restraints.getRestraintForce() # Get Force object incorporating restraints reference_system.addForce(force) metadata['standard_state_correction'] = restraints.getStandardStateCorrection() # standard state correction in kT elif phase == 'complex-explicit': # For periodic systems, we do not use a restraint, but must add a standard state correction for the box volume. # TODO: What if the box volume fluctuates during the simulation? box_vectors = reference_system.getDefaultPeriodicBoxVectors() box_volume = thermodynamic_state._volume(box_vectors) STANDARD_STATE_VOLUME = 1660.53928 * unit.angstrom**3 metadata['standard_state_correction'] = - np.log(STANDARD_STATE_VOLUME / box_volume) # Use default alchemical protocols if not specified. if not protocols: protocols = self.default_protocols # Create alchemically-modified states using alchemical factory. logger.debug("Creating alchemically-modified states...") try: alchemical_indices = atom_indices['ligand_counterions'] + atom_indices['ligand'] except KeyError: alchemical_indices = atom_indices['ligand'] factory = AbsoluteAlchemicalFactory(reference_system, ligand_atoms=alchemical_indices, **self._alchemy_parameters) alchemical_states = protocols[phase] alchemical_system = factory.alchemically_modified_system thermodynamic_state.system = alchemical_system # Check systems for finite energies. # TODO: Refactor this into another function. finite_energy_check = False if finite_energy_check: logger.debug("Checking energies are finite for all alchemical systems.") integrator = openmm.VerletIntegrator(1.0 * unit.femtosecond) context = openmm.Context(alchemical_system, integrator) context.setPositions(positions[0]) for alchemical_state in alchemical_states: AbsoluteAlchemicalFactory.perturbContext(context, alchemical_state) potential = context.getState(getEnergy=True).getPotentialEnergy() if np.isnan(potential / unit.kilocalories_per_mole): raise Exception("Energy for system %d is NaN." % index) del context, integrator logger.debug("All energies are finite.") # Randomize ligand position if requested, but only for implicit solvent systems. if self._randomize_ligand and (phase == 'complex-implicit'): logger.debug("Randomizing ligand positions and excluding overlapping configurations...") randomized_positions = list() nstates = len(alchemical_states) for state_index in range(nstates): positions_index = np.random.randint(0, len(positions)) current_positions = positions[positions_index] new_positions = ModifiedHamiltonianExchange.randomize_ligand_position(current_positions, atom_indices['receptor'], atom_indices['ligand'], self._randomize_ligand_sigma_multiplier * restraints.getReceptorRadiusOfGyration(), self._randomize_ligand_close_cutoff) randomized_positions.append(new_positions) positions = randomized_positions if self._randomize_ligand and (phase == 'complex-explicit'): logger.warning("Ligand randomization requested, but will not be performed for explicit solvent simulations.") # Identify whether any atoms will be displaced via MC, unless option is turned off. mc_atoms = None if self._mc_displacement_sigma: mc_atoms = list() if 'ligand' in atom_indices: mc_atoms = atom_indices['ligand'] # Set up simulation. # TODO: Support MPI initialization? logger.debug("Creating replica exchange object...") store_filename = os.path.join(self._store_directory, phase + '.nc') self._store_filenames[phase] = store_filename simulation = ModifiedHamiltonianExchange(store_filename) simulation.create(thermodynamic_state, alchemical_states, positions, displacement_sigma=self._mc_displacement_sigma, mc_atoms=mc_atoms, options=repex_parameters, metadata=metadata) # Initialize simulation. # TODO: Use the right scheme for initializing the simulation without running. #logger.debug("Initializing simulation...") #simulation.run(0) # Clean up simulation. del simulation # Add to list of phases that have been set up. self._phases.append(phase) return
def _create_phase(self, phase, reference_system, positions, atom_indices, thermodynamic_state, protocols=None, options=None, mpicomm=None): """ Create a repex object for a specified phase. Parameters ---------- phase : str The phase being initialized (one of ['complex', 'solvent', 'vacuum']) reference_system : simtk.openmm.System The reference system object from which alchemical intermediates are to be construcfted. positions : list of simtk.unit.Qunatity objects containing (natoms x 3) positions (as np or lists) The list of positions to be used to seed replicas in a round-robin way. atom_indices : dict atom_indices[phase][component] is the set of atom indices associated with component, where component is ['ligand', 'receptor'] thermodynamic_state : ThermodynamicState Thermodynamic state from which reference temperature and pressure are to be taken. protocols : dict of list of AlchemicalState, optional, default=None If specified, the alchemical protocol protocols[phase] will be used for phase 'phase' instead of the default. options : dict of str, optional, default=None If specified, these options will override default repex simulation options. """ # Make sure positions argument is a list of coordinate snapshots. if hasattr(positions, 'unit'): # Wrap in list. positions = [positions] # Check the dimensions of positions. for index in range(len(positions)): # Make sure it is recast as a np array. positions[index] = unit.Quantity( np.array(positions[index] / positions[index].unit), positions[index].unit) [natoms, ndim] = (positions[index] / positions[index].unit).shape if natoms != reference_system.getNumParticles(): raise Exception( "positions argument must be a list of simtk.unit.Quantity of (natoms,3) lists or np array with units compatible with nanometers." ) # Create metadata storage. metadata = dict() # Make a deep copy of the reference system so we don't accidentally modify it. reference_system = copy.deepcopy(reference_system) # TODO: Use more general approach to determine whether system is periodic. is_periodic = self._is_periodic(reference_system) # Make sure pressure is None if not periodic. if not is_periodic: thermodynamic_state.pressure = None # Compute standard state corrections for complex phase. metadata['standard_state_correction'] = 0.0 # TODO: Do we need to include a standard state correction for other phases in periodic boxes? if phase == 'complex-implicit': # Impose restraints for complex system in implicit solvent to keep ligand from drifting too far away from receptor. if self.verbose: print "Creating receptor-ligand restraints..." reference_positions = positions[0] if self.restraint_type == 'harmonic': restraints = HarmonicReceptorLigandRestraint( thermodynamic_state, reference_system, reference_positions, atom_indices['receptor'], atom_indices['ligand']) elif self.restraint_type == 'flat-bottom': restraints = FlatBottomReceptorLigandRestraint( thermodynamic_state, reference_system, reference_positions, atom_indices['receptor'], atom_indices['ligand']) else: raise Exception("restraint_type of '%s' is not supported." % self.restraint_type) force = restraints.getRestraintForce( ) # Get Force object incorporating restraints reference_system.addForce(force) metadata[ 'standard_state_correction'] = restraints.getStandardStateCorrection( ) # standard state correction in kT elif phase == 'complex-explicit': # For periodic systems, we do not use a restraint, but must add a standard state correction for the box volume. # TODO: What if the box volume fluctuates during the simulation? box_vectors = reference_system.getDefaultPeriodicBoxVectors() box_volume = thermodynamic_state._volume(box_vectors) STANDARD_STATE_VOLUME = 1660.53928 * unit.angstrom**3 metadata['standard_state_correction'] = np.log( STANDARD_STATE_VOLUME / box_volume) # TODO: Check sign. # Use default alchemical protocols if not specified. if not protocols: protocols = self.default_protocols # Create alchemically-modified states using alchemical factory. if self.verbose: print "Creating alchemically-modified states..." factory = AbsoluteAlchemicalFactory( reference_system, ligand_atoms=atom_indices['ligand']) systems = factory.createPerturbedSystems(protocols[phase]) # Randomize ligand position if requested, but only for implicit solvent systems. if self.randomize_ligand and (phase == 'complex-implicit'): if self.verbose: print "Randomizing ligand positions and excluding overlapping configurations..." randomized_positions = list() nstates = len(systems) for state_index in range(nstates): positions_index = np.random.randint(0, len(positions)) current_positions = positions[positions_index] new_positions = ModifiedHamiltonianExchange.randomize_ligand_position( current_positions, atom_indices['receptor'], atom_indices['ligand'], self.randomize_ligand_sigma_multiplier * restraints.getReceptorRadiusOfGyration(), self.randomize_ligand_close_cutoff) randomized_positions.append(new_positions) positions = randomized_positions # Identify whether any atoms will be displaced via MC. mc_atoms = list() if 'ligand' in atom_indices: mc_atoms = atom_indices['ligand'] # Combine simulation options with defaults. options = dict(self.default_options.items() + options.items()) # Set up simulation. # TODO: Support MPI initialization? if self.verbose: print "Creating replica exchange object..." store_filename = os.path.join(self._store_directory, phase + '.nc') self._store_filenames[phase] = store_filename simulation = ModifiedHamiltonianExchange(store_filename, mpicomm=mpicomm) simulation.create(thermodynamic_state, systems, positions, displacement_sigma=self.mc_displacement_sigma, mc_atoms=mc_atoms, options=options, metadata=metadata) simulation.verbose = self.verbose # Initialize simulation. # TODO: Use the right scheme for initializing the simulation without running. #if self.verbose: print "Initializing simulation..." #simulation.run(0) # TODO: Process user-supplied options. # Clean up simulation. del simulation return
def _create_phase(self, thermodynamic_state, alchemical_phase): """ Create a repex object for a specified phase. Parameters ---------- thermodynamic_state : ThermodynamicState (System need not be defined) Thermodynamic state from which reference temperature and pressure are to be taken. alchemical_phase : AlchemicalPhase The alchemical phase to be created. """ # We add default repex options only on creation, on resume repex will pick them from the store file repex_parameters = { 'number_of_equilibration_iterations': 0, 'number_of_iterations': 100, 'timestep': 2.0 * unit.femtoseconds, 'collision_rate': 5.0 / unit.picoseconds, 'minimize': False, 'show_mixing_statistics': True, # this causes slowdown with iteration and should not be used for production 'displacement_sigma': 1.0 * unit.nanometers # attempt to displace ligand by this stddev will be made each iteration } repex_parameters.update(self._repex_parameters) # Convenience variables positions = alchemical_phase.positions reference_system = copy.deepcopy(alchemical_phase.reference_system) atom_indices = alchemical_phase.atom_indices alchemical_states = alchemical_phase.protocol # If temperature and pressure are specified, make sure MonteCarloBarostat is attached. if thermodynamic_state.temperature and thermodynamic_state.pressure: forces = { reference_system.getForce(index).__class__.__name__ : reference_system.getForce(index) for index in range(reference_system.getNumForces()) } if 'MonteCarloAnisotropicBarostat' in forces: raise Exception('MonteCarloAnisotropicBarostat is unsupported.') if 'MonteCarloBarostat' in forces: logger.debug('MonteCarloBarostat found: Setting default temperature and pressure.') barostat = forces['MonteCarloBarostat'] # Set temperature and pressure. try: barostat.setDefaultTemperature(thermodynamic_state.temperature) except AttributeError: # versions previous to OpenMM7.1 barostat.setTemperature(thermodynamic_state.temperature) barostat.setDefaultPressure(state.pressure) else: # Create barostat and add it to the system if it doesn't have one already. logger.debug('MonteCarloBarostat not found: Creating one.') barostat = openmm.MonteCarloBarostat(thermodynamic_state.pressure, thermodynamic_state.temperature) reference_system.addForce(barostat) # Check the dimensions of positions. for index in range(len(positions)): n_atoms, _ = (positions[index] / positions[index].unit).shape if n_atoms != reference_system.getNumParticles(): err_msg = "Phase {}: number of atoms in positions {} and and " \ "reference system differ ({} and {} respectively)" err_msg.format(alchemical_phase.name, index, n_atoms, reference_system.getNumParticles()) logger.error(err_msg) raise RuntimeError(err_msg) # Inizialize metadata storage. metadata = dict() # Store a serialized copy of the reference system. metadata['reference_system'] = openmm.XmlSerializer.serialize(reference_system) metadata['topology'] = utils.serialize_topology(alchemical_phase.reference_topology) # TODO: Use more general approach to determine whether system is periodic. is_periodic = self._is_periodic(reference_system) is_complex_explicit = len(atom_indices['receptor']) > 0 and is_periodic is_complex_implicit = len(atom_indices['receptor']) > 0 and not is_periodic # Make sure pressure is None if not periodic. if not is_periodic: thermodynamic_state.pressure = None # Create a copy of the system for which the fully-interacting energy is to be computed. # For explicit solvent calculations, an enlarged cutoff is used to account for the anisotropic dispersion correction. fully_interacting_system = copy.deepcopy(reference_system) if is_periodic: # Expand cutoff to maximum allowed # TODO: Should we warn if cutoff can't be extended enough? # TODO: Should we extend to some minimum cutoff rather than the maximum allowed? box_vectors = fully_interacting_system.getDefaultPeriodicBoxVectors() max_allowed_cutoff = 0.499 * max([ max(vector) for vector in box_vectors ]) # TODO: Correct this for non-rectangular boxes logger.debug('Setting cutoff for fully interacting system to maximum allowed (%s)' % str(max_allowed_cutoff)) for force_index in range(fully_interacting_system.getNumForces()): force = fully_interacting_system.getForce(force_index) if hasattr(force, 'setCutoffDistance'): force.setCutoffDistance(max_allowed_cutoff) if hasattr(force, 'setCutoff'): force.setCutoff(max_allowed_cutoff) # Construct thermodynamic state fully_interacting_state = copy.deepcopy(thermodynamic_state) fully_interacting_state.system = fully_interacting_system # Compute standard state corrections for complex phase. metadata['standard_state_correction'] = 0.0 # TODO: Do we need to include a standard state correction for other phases in periodic boxes? if is_complex_implicit: # Impose restraints for complex system in implicit solvent to keep ligand from drifting too far away from receptor. logger.debug("Creating receptor-ligand restraints...") reference_positions = positions[0] if self._restraint_type == 'harmonic': restraints = HarmonicReceptorLigandRestraint(thermodynamic_state, reference_system, reference_positions, atom_indices['receptor'], atom_indices['ligand']) elif self._restraint_type == 'flat-bottom': restraints = FlatBottomReceptorLigandRestraint(thermodynamic_state, reference_system, reference_positions, atom_indices['receptor'], atom_indices['ligand']) else: raise Exception("restraint_type of '%s' is not supported." % self._restraint_type) force = restraints.getRestraintForce() # Get Force object incorporating restraints reference_system.addForce(force) metadata['standard_state_correction'] = restraints.getStandardStateCorrection() # standard state correction in kT elif is_complex_explicit: # For periodic systems, we do not use a restraint, but must add a standard state correction for the box volume. # TODO: What if the box volume fluctuates during the simulation? box_vectors = reference_system.getDefaultPeriodicBoxVectors() box_volume = thermodynamic_state._volume(box_vectors) STANDARD_STATE_VOLUME = 1660.53928 * unit.angstrom**3 metadata['standard_state_correction'] = - np.log(STANDARD_STATE_VOLUME / box_volume) # Create alchemically-modified states using alchemical factory. logger.debug("Creating alchemically-modified states...") try: alchemical_indices = atom_indices['ligand_counterions'] + atom_indices['ligand'] except KeyError: alchemical_indices = atom_indices['ligand'] factory = AbsoluteAlchemicalFactory(reference_system, ligand_atoms=alchemical_indices, **self._alchemy_parameters) alchemical_system = factory.alchemically_modified_system thermodynamic_state.system = alchemical_system # Check systems for finite energies. # TODO: Refactor this into another function. finite_energy_check = False if finite_energy_check: logger.debug("Checking energies are finite for all alchemical systems.") integrator = openmm.VerletIntegrator(1.0 * unit.femtosecond) context = openmm.Context(alchemical_system, integrator) context.setPositions(positions[0]) for alchemical_state in alchemical_states: AbsoluteAlchemicalFactory.perturbContext(context, alchemical_state) potential = context.getState(getEnergy=True).getPotentialEnergy() if np.isnan(potential / unit.kilocalories_per_mole): raise Exception("Energy for system %d is NaN." % index) del context, integrator logger.debug("All energies are finite.") # Randomize ligand position if requested, but only for implicit solvent systems. if self._randomize_ligand and is_complex_implicit: logger.debug("Randomizing ligand positions and excluding overlapping configurations...") randomized_positions = list() nstates = len(alchemical_states) for state_index in range(nstates): positions_index = np.random.randint(0, len(positions)) current_positions = positions[positions_index] new_positions = ModifiedHamiltonianExchange.randomize_ligand_position(current_positions, atom_indices['receptor'], atom_indices['ligand'], self._randomize_ligand_sigma_multiplier * restraints.getReceptorRadiusOfGyration(), self._randomize_ligand_close_cutoff) randomized_positions.append(new_positions) positions = randomized_positions if self._randomize_ligand and is_complex_explicit: logger.warning("Ligand randomization requested, but will not be performed for explicit solvent simulations.") # Identify whether any atoms will be displaced via MC, unless option is turned off. mc_atoms = None if self._mc_displacement_sigma: mc_atoms = list() if 'ligand' in atom_indices: mc_atoms = atom_indices['ligand'] # Set up simulation. # TODO: Support MPI initialization? logger.debug("Creating replica exchange object...") store_filename = os.path.join(self._store_directory, alchemical_phase.name + '.nc') self._store_filenames[alchemical_phase.name] = store_filename simulation = ModifiedHamiltonianExchange(store_filename) simulation.create(thermodynamic_state, alchemical_states, positions, displacement_sigma=self._mc_displacement_sigma, mc_atoms=mc_atoms, options=repex_parameters, metadata=metadata, fully_interacting_state=fully_interacting_state) # Initialize simulation. # TODO: Use the right scheme for initializing the simulation without running. #logger.debug("Initializing simulation...") #simulation.run(0) # Clean up simulation. del simulation # Add to list of phases that have been set up. self._phases.append(alchemical_phase.name) return