def run(self, niterations_to_run=None, mpicomm=None, options=None): """ Run a free energy calculation. Parameters ---------- niterations_to_run : int, optional, default=None If specified, only this many iterations will be run for each phase. This is useful for running simulation incrementally, but may incur a good deal of overhead. mpicomm : MPI communicator, optional, default=None If an MPI communicator is passed, an MPI simulation will be attempted. options : dict of str, optional, default=None If specified, these options will override any other options. """ # Make sure we've been properly initialized first. if not self._initialized: raise Exception("Yank must first be initialized by either resume() or create().") # Handle some logistics necessary for MPI. if mpicomm: # Turn off output from non-root nodes: if not (mpicomm.rank==0): self.verbose = False # Make sure each thread's random number generators have unique seeds. # TODO: Do we need to store seed in repex object? seed = np.random.randint(sys.maxint - mpicomm.size) + mpicomm.rank np.random.seed(seed) # Run all phases sequentially. # TODO: Divide up MPI resources among the phases so they can run simultaneously? for phase in self._phases: store_filename = self._store_filenames[phase] # Resume simulation from store file. simulation = ModifiedHamiltonianExchange(store_filename=store_filename, mpicomm=mpicomm) simulation.resume(options=options) # TODO: We may need to manually update run options here if options=options above does not behave as expected. simulation.run(niterations_to_run=niterations_to_run) # Clean up to ensure we close files, contexts, etc. del simulation return
def run(self, niterations_to_run=None): """ Run a free energy calculation. Parameters ---------- niterations_to_run : int, optional, default=None If specified, only this many iterations will be run for each phase. This is useful for running simulation incrementally, but may incur a good deal of overhead. """ # Make sure we've been properly initialized first. if not self._initialized: raise Exception("Yank must first be initialized by either resume() or create().") # Handle some logistics necessary for MPI. if self._mpicomm is not None: logger.debug("yank.run starting for MPI...") # Make sure each thread's random number generators have unique seeds. # TODO: Do we need to store seed in repex object? seed = np.random.randint(4294967295 - self._mpicomm.size) + self._mpicomm.rank np.random.seed(seed) # Run all phases sequentially. # TODO: Divide up MPI resources among the phases so they can run simultaneously? for phase in self._phases: store_filename = self._store_filenames[phase] # Resume simulation from store file. simulation = ModifiedHamiltonianExchange(store_filename=store_filename, mpicomm=self._mpicomm) simulation.resume(options=self._repex_parameters) # TODO: We may need to manually update run options here if options=options above does not behave as expected. simulation.run(niterations_to_run=niterations_to_run) # Clean up to ensure we close files, contexts, etc. del simulation return
def run(self, niterations=None, mpicomm=None, options=None): """ Run a free energy calculation. Parameters ---------- niterations : int, optional, default=None If specified, only this many iterations will be run for each phase. This is useful for running simulation incrementally, but may incur a good deal of overhead. mpicomm : MPI communicator, optional, default=None If an MPI communicator is passed, an MPI simulation will be attempted. options : dict of str, optional, default=None If specified, these options will override any other options. """ # Make sure we've been properly initialized first. if not self._initialized: raise Exception( "Yank must first be initialized by either resume() or create()." ) # Handle some logistics necessary for MPI. if mpicomm: # Turn off output from non-root nodes: if not (mpicomm.rank == 0): self.verbose = False # Make sure each thread's random number generators have unique seeds. # TODO: Do we need to store seed in repex object? seed = np.random.randint(sys.maxint - mpicomm.size) + mpicomm.rank np.random.seed(seed) # Run all phases sequentially. # TODO: Divide up MPI resources among the phases so they can run simultaneously? for phase in self._phases: store_filename = self._store_filenames[phase] # Resume simulation from store file. simulation = ModifiedHamiltonianExchange( store_filename=store_filename, mpicomm=mpicomm) simulation.resume(options=options) # TODO: We may need to manually update run options here if options=options above does not behave as expected. simulation.run(niterations_to_run=niterations) # Clean up to ensure we close files, contexts, etc. del simulation return
def run(self, niterations_to_run=None): """ Run a free energy calculation. Parameters ---------- niterations_to_run : int, optional, default=None If specified, only this many iterations will be run for each phase. This is useful for running simulation incrementally, but may incur a good deal of overhead. """ # Make sure we've been properly initialized first. if not self._initialized: raise Exception( "Yank must first be initialized by either resume() or create()." ) # Handle some logistics necessary for MPI. if self._mpicomm is not None: logger.debug("yank.run starting for MPI...") # Make sure each thread's random number generators have unique seeds. # TODO: Do we need to store seed in repex object? seed = np.random.randint(4294967295 - self._mpicomm.size) + self._mpicomm.rank np.random.seed(seed) # Run all phases sequentially. # TODO: Divide up MPI resources among the phases so they can run simultaneously? for phase in self._phases: store_filename = self._store_filenames[phase] # Resume simulation from store file. simulation = ModifiedHamiltonianExchange( store_filename=store_filename, mpicomm=self._mpicomm) simulation.resume(options=self._repex_parameters) # TODO: We may need to manually update run options here if options=options above does not behave as expected. simulation.run(niterations_to_run=niterations_to_run) # Clean up to ensure we close files, contexts, etc. del simulation return
def analyze(self): """ Programmatic interface to retrieve the results of a YANK free energy calculation. Returns ------- results : dict results[phase][component] is the estimate of 'component' of thermodynamic leg 'phase' 'component' can be one of ['DeltaF', 'dDeltaF', 'DeltaH', 'dDeltaH'] DeltaF is the estimated free energy difference dDeltaF is the statistical uncertainty in DeltaF (one standard error) DeltaH is the estimated enthalpy difference dDeltaH is the statistical uncertainty in DeltaH (one standard error) all quantites are reported in units are kT If simulation has not been initialized by a call to resume() or create(), None is returned. """ if not self._initialized: return None # TODO: Can we simplify this code by pushing more into analyze.py or repex.py? import analyze from pymbar import MBAR, timeseries import netCDF4 as netcdf # Storage for results. results = dict() logger.debug("Analyzing simulation data...") # Process each netcdf file in output directory. for phase in self._phases: fullpath = self._store_filenames[phase] # Skip if the file doesn't exist. if (not os.path.exists(fullpath)): continue # Analyze this leg. simulation = ModifiedHamiltonianExchange(store_filename=store_filename, mpicomm=mpicomm, options=options) analysis = simulation.analyze() del simulation # Store results. results[phase] = analysis # TODO: Analyze binding or hydration, depending on what phases are present. # TODO: Include effects of analytical contributions. phases_available = results.keys() if set(['solvent', 'vacuum']).issubset(phases_available): # SOLVATION FREE ENERGY results['solvation'] = dict() results['solvation']['Delta_f'] = results['solvent']['Delta_f'] + results['vacuum']['Delta_f'] results['solvation']['dDelta_f'] = np.sqrt(results['solvent']['dDelta_f']**2 + results['vacuum']['Delta_f']**2) if set(['ligand', 'complex']).issubset(phases_available): # BINDING FREE ENERGY results['binding'] = dict() # Read standard state correction free energy. Delta_f_restraints = 0.0 phase = 'complex' fullpath = os.path.join(source_directory, phase + '.nc') ncfile = netcdf.Dataset(fullpath, 'r') Delta_f_restraints = ncfile.groups['metadata'].variables['standard_state_correction'][0] ncfile.close() results['binding']['standard_state_correction'] = Delta_f_restraints # Compute binding free energy. results['binding']['Delta_f'] = results['solvent']['Delta_f'] - Delta_f_restraints - results['complex']['Delta_f'] results['binding']['dDelta_f'] = np.sqrt(results['solvent']['dDelta_f']**2 + results['complex']['dDelta_f']**2) return results
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 analyze(self): """ Programmatic interface to retrieve the results of a YANK free energy calculation. Returns ------- results : dict results[phase][component] is the estimate of 'component' of thermodynamic leg 'phase' 'component' can be one of ['DeltaF', 'dDeltaF', 'DeltaH', 'dDeltaH'] DeltaF is the estimated free energy difference dDeltaF is the statistical uncertainty in DeltaF (one standard error) DeltaH is the estimated enthalpy difference dDeltaH is the statistical uncertainty in DeltaH (one standard error) all quantites are reported in units are kT If simulation has not been initialized by a call to resume() or create(), None is returned. """ if not self._initialized: return None # TODO: Can we simplify this code by pushing more into analyze.py or repex.py? import analyze from pymbar import MBAR, timeseries import netCDF4 as netcdf # Storage for results. results = dict() logger.debug("Analyzing simulation data...") # Process each netcdf file in output directory. for phase in self._phases: fullpath = self._store_filenames[phase] # Skip if the file doesn't exist. if (not os.path.exists(fullpath)): continue # Read this phase. simulation = ModifiedHamiltonianExchange(fullpath) simulation.resume() # Analyze this phase. analysis = simulation.analyze() # Retrieve standard state correction. analysis['standard_state_correction'] = simulation.metadata[ 'standard_state_correction'] # Store results. results[phase] = analysis # Clean up. del simulation # TODO: Analyze binding or hydration, depending on what phases are present. # TODO: Include effects of analytical contributions. phases_available = results.keys() if set(['solvent', 'vacuum']).issubset(phases_available): # SOLVATION FREE ENERGY results['solvation'] = dict() results['solvation']['Delta_f'] = results['solvent'][ 'Delta_f'] + results['vacuum']['Delta_f'] # TODO: Correct in different ways depending on what reference conditions are desired. results['solvation']['Delta_f'] += results['solvent'][ 'standard_state_correction'] + results['vacuum'][ 'standard_state_correction'] results['solvation']['dDelta_f'] = np.sqrt( results['solvent']['dDelta_f']**2 + results['vacuum']['Delta_f']**2) if set(['ligand', 'complex']).issubset(phases_available): # BINDING FREE ENERGY results['binding'] = dict() # Compute binding free energy. results['binding']['Delta_f'] = ( results['solvent']['Delta_f'] + results['solvent']['standard_state_correction']) - ( results['complex']['Delta_f'] + results['complex']['standard_state_correction']) results['binding']['dDelta_f'] = np.sqrt( results['solvent']['dDelta_f']**2 + results['complex']['dDelta_f']**2) return results
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 analyze(self): """ Programmatic interface to retrieve the results of a YANK free energy calculation. Returns ------- results : dict results[phase][component] is the estimate of 'component' of thermodynamic leg 'phase' 'component' can be one of ['DeltaF', 'dDeltaF', 'DeltaH', 'dDeltaH'] DeltaF is the estimated free energy difference dDeltaF is the statistical uncertainty in DeltaF (one standard error) DeltaH is the estimated enthalpy difference dDeltaH is the statistical uncertainty in DeltaH (one standard error) all quantites are reported in units are kT If simulation has not been initialized by a call to resume() or create(), None is returned. """ if not self._initialized: return None # TODO: Can we simplify this code by pushing more into analyze.py or repex.py? import analyze from pymbar import MBAR, timeseries import netCDF4 as netcdf # Storage for results. results = dict() if self.verbose: print "Analyzing simulation data..." # Process each netcdf file in output directory. for phase in self._phases: fullpath = self._store_filenames[phase] # Skip if the file doesn't exist. if (not os.path.exists(fullpath)): continue # Analyze this leg. simulation = ModifiedHamiltonianExchange( store_filename=store_filename, mpicomm=mpicomm, options=options) analysis = simulation.analyze() del simulation # Store results. results[phase] = analysis # TODO: Analyze binding or hydration, depending on what phases are present. # TODO: Include effects of analytical contributions. phases_available = results.keys() if set(['solvent', 'vacuum']).issubset(phases_available): # SOLVATION FREE ENERGY results['solvation'] = dict() results['solvation']['Delta_f'] = results['solvent'][ 'Delta_f'] + results['vacuum']['Delta_f'] results['solvation']['dDelta_f'] = np.sqrt( results['solvent']['dDelta_f']**2 + results['vacuum']['Delta_f']**2) if set(['ligand', 'complex']).issubset(phases_available): # BINDING FREE ENERGY results['binding'] = dict() # Read standard state correction free energy. Delta_f_restraints = 0.0 phase = 'complex' fullpath = os.path.join(source_directory, phase + '.nc') ncfile = netcdf.Dataset(fullpath, 'r') Delta_f_restraints = ncfile.groups['metadata'].variables[ 'standard_state_correction'][0] ncfile.close() results['binding'][ 'standard_state_correction'] = Delta_f_restraints # Compute binding free energy. results['binding']['Delta_f'] = results['solvent'][ 'Delta_f'] - Delta_f_restraints - results['complex']['Delta_f'] results['binding']['dDelta_f'] = np.sqrt( results['solvent']['dDelta_f']**2 + results['complex']['dDelta_f']**2) return results
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, 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, 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