Ejemplo n.º 1
0
def test_position_output():
    """
    Test that the hybrid returns the correct positions for the new and old systems after construction
    """
    from perses.annihilation.new_relative import HybridTopologyFactory
    import numpy as np

    #generate topology proposal
    topology_proposal, old_positions, new_positions = utils.generate_vacuum_topology_proposal()

    factory = HybridTopologyFactory(topology_proposal, old_positions, new_positions)

    old_positions_factory = factory.old_positions(factory.hybrid_positions)
    new_positions_factory = factory.new_positions(factory.hybrid_positions)

    assert np.all(np.isclose(old_positions.in_units_of(unit.nanometers), old_positions_factory.in_units_of(unit.nanometers)))
    assert np.all(np.isclose(new_positions.in_units_of(unit.nanometers), new_positions_factory.in_units_of(unit.nanometers)))
Ejemplo n.º 2
0
def compute_nonalchemical_perturbation(
        equilibrium_result: EquilibriumResult,
        hybrid_factory: HybridTopologyFactory,
        nonalchemical_thermodynamic_state: states.ThermodynamicState,
        lambda_state: int):
    """
    Compute the perturbation of transforming the given hybrid equilibrium result into the system for the given nonalchemical_thermodynamic_state

    Parameters
    ----------
    equilibrium_result : EquilibriumResult
        Result of the equilibrium simulation
    hybrid_factory : HybridTopologyFactory
        Hybrid factory necessary for getting the positions of the nonalchemical system
    nonalchemical_thermodynamic_state : states.ThermodynamicState
        ThermodynamicState of the nonalchemical system
    lambda_state : int
        Whether this is lambda 0 or 1
    Returns
    -------
    work : float
        perturbation in kT from the hybrid system to the nonalchemical one
    """
    #get the objects we need to begin
    hybrid_reduced_potential = equilibrium_result.reduced_potential
    hybrid_sampler_state = equilibrium_result.sampler_state
    hybrid_positions = hybrid_sampler_state.positions

    #get the positions for the nonalchemical system
    if lambda_state == 0:
        nonalchemical_positions = hybrid_factory.old_positions(
            hybrid_positions)
    elif lambda_state == 1:
        nonalchemical_positions = hybrid_factory.new_positions(
            hybrid_positions)
    else:
        raise ValueError("lambda_state must be 0 or 1")

    nonalchemical_sampler_state = states.SamplerState(
        nonalchemical_positions, box_vectors=hybrid_sampler_state.box_vectors)

    nonalchemical_reduced_potential = compute_reduced_potential(
        nonalchemical_thermodynamic_state, nonalchemical_sampler_state)

    return hybrid_reduced_potential - nonalchemical_reduced_potential
Ejemplo n.º 3
0
def test_position_output():
    """
    Test that the hybrid returns the correct positions for the new and old systems after construction
    """
    from perses.annihilation.new_relative import HybridTopologyFactory
    import numpy as np

    #generate topology proposal
    topology_proposal, old_positions, new_positions = generate_topology_proposal(
    )

    factory = HybridTopologyFactory(topology_proposal, old_positions,
                                    new_positions)

    old_positions_factory = factory.old_positions(factory.hybrid_positions)
    new_positions_factory = factory.new_positions(factory.hybrid_positions)

    assert np.all(
        np.isclose(old_positions.in_units_of(unit.nanometers),
                   old_positions_factory.in_units_of(unit.nanometers)))
    assert np.all(
        np.isclose(new_positions.in_units_of(unit.nanometers),
                   new_positions_factory.in_units_of(unit.nanometers)))
Ejemplo n.º 4
0
def compute_nonalchemical_perturbation(equilibrium_result: EquilibriumResult, hybrid_factory: HybridTopologyFactory, nonalchemical_thermodynamic_state: states.ThermodynamicState, lambda_state: int):
    """
    Compute the perturbation of transforming the given hybrid equilibrium result into the system for the given nonalchemical_thermodynamic_state

    Parameters
    ----------
    equilibrium_result : EquilibriumResult
        Result of the equilibrium simulation
    hybrid_factory : HybridTopologyFactory
        Hybrid factory necessary for getting the positions of the nonalchemical system
    nonalchemical_thermodynamic_state : states.ThermodynamicState
        ThermodynamicState of the nonalchemical system
    lambda_state : int
        Whether this is lambda 0 or 1
    Returns
    -------
    work : float
        perturbation in kT from the hybrid system to the nonalchemical one
    """
    #get the objects we need to begin
    hybrid_reduced_potential = equilibrium_result.reduced_potential
    hybrid_sampler_state = equilibrium_result.sampler_state
    hybrid_positions = hybrid_sampler_state.positions

    #get the positions for the nonalchemical system
    if lambda_state==0:
        nonalchemical_positions = hybrid_factory.old_positions(hybrid_positions)
    elif lambda_state==1:
        nonalchemical_positions = hybrid_factory.new_positions(hybrid_positions)
    else:
        raise ValueError("lambda_state must be 0 or 1")

    nonalchemical_sampler_state = states.SamplerState(nonalchemical_positions, box_vectors=hybrid_sampler_state.box_vectors)

    nonalchemical_reduced_potential = compute_reduced_potential(nonalchemical_thermodynamic_state, nonalchemical_sampler_state)

    return hybrid_reduced_potential - nonalchemical_reduced_potential
Ejemplo n.º 5
0
    def make_alchemical_system(self, topology_proposal, current_positions,
                               new_positions):
        """
        Generate an alchemically-modified system at the correct atoms
        based on the topology proposal. This method generates a hybrid system using the new 
        HybridTopologyFactory. It memoizes so that calling multiple times (within a recent time period)
        will immediately return a cached object.

        Arguments
        ---------
        topology_proposal : perses.rjmc.TopologyProposal
            Unmodified real system corresponding to appropriate leg of transformation.
        current_positions : np.ndarray of float
            Positions of "old" system
        new_positions : np.ndarray of float
            Positions of "new" system atoms

        Returns
        -------
        hybrid_factory : perses.annihilation.new_relative.HybridTopologyFactory
            a factory object containing the hybrid system
        """
        try:
            hybrid_factory = self._hybrid_cache[topology_proposal]

            #If we've retrieved the factory from the cache, update it to include the relevant positions
            hybrid_factory._old_positions = current_positions
            hybrid_factory._new_positions = new_positions
            hybrid_factory._compute_hybrid_positions()
        except KeyError:
            try:
                hybrid_factory = HybridTopologyFactory(topology_proposal,
                                                       current_positions,
                                                       new_positions)
                self._hybrid_cache[topology_proposal] = hybrid_factory
            except:
                hybrid_factory = None

        return hybrid_factory
Ejemplo n.º 6
0
def check_alchemical_hybrid_elimination_bar(topology_proposal, old_positions, new_positions, ncmc_nsteps=50, n_iterations=50, NSIGMA_MAX=6.0, geometry=False):
    """
    Check that the hybrid topology, where both endpoints are identical, returns a free energy within NSIGMA_MAX of 0.
    Parameters
    ----------
    topology_proposal
    positions
    ncmc_nsteps
    NSIGMA_MAX

    Returns
    -------

    """

    #make the hybrid topology factory:
    factory = HybridTopologyFactory(topology_proposal, old_positions, new_positions)

    platform = openmm.Platform.getPlatformByName("CUDA")

    hybrid_system = factory.hybrid_system
    hybrid_topology = factory.hybrid_topology
    initial_hybrid_positions = factory.hybrid_positions


    #alchemical functions
    functions = {
        'lambda_sterics' : '2*lambda * step(0.5 - lambda) + (1.0 - step(0.5 - lambda))',
        'lambda_electrostatics' : '2*(lambda - 0.5) * step(lambda - 0.5)',
        'lambda_bonds' : 'lambda',
        'lambda_angles' : 'lambda',
        'lambda_torsions' : 'lambda'
    }

    w_f = np.zeros(n_iterations)
    w_r = np.zeros(n_iterations)

    #make the alchemical integrators:
    forward_integrator = NCMCGHMCAlchemicalIntegrator(temperature, hybrid_system, functions, nsteps=ncmc_nsteps, direction='insert')
    forward_context = openmm.Context(hybrid_system, forward_integrator, platform)
    print("Minimizing for forward protocol...")
    forward_context.setPositions(initial_hybrid_positions)
    for parm in functions.keys():
        forward_context.setParameter(parm, 0.0)

    openmm.LocalEnergyMinimizer.minimize(forward_context, maxIterations=10)

    initial_state = forward_context.getState(getPositions=True, getEnergy=True)
    print("The initial energy after minimization is %s" % str(initial_state.getPotentialEnergy()))
    initial_forward_positions = initial_state.getPositions(asNumpy=True)
    equil_positions = simulate_hybrid(hybrid_system,functions, 0.0, initial_forward_positions)

    print("Beginning forward protocols")
    #first, do forward protocol (lambda=0 -> 1)
    with progressbar.ProgressBar(max_value=n_iterations) as bar:
        for i in range(n_iterations):
            equil_positions = simulate_hybrid(hybrid_system, functions, 0.0, equil_positions)
            forward_context.setPositions(equil_positions)
            forward_integrator.step(ncmc_nsteps)
            w_f[i] = -1.0 * forward_integrator.getLogAcceptanceProbability(forward_context)
            bar.update(i)

    del forward_context, forward_integrator

    reverse_integrator = NCMCGHMCAlchemicalIntegrator(temperature, hybrid_system, functions, nsteps=ncmc_nsteps, direction='delete')

    print("Minimizing for reverse protocol...")
    reverse_context = openmm.Context(hybrid_system, reverse_integrator, platform)
    reverse_context.setPositions(initial_hybrid_positions)
    for parm in functions.keys():
        reverse_context.setParameter(parm, 1.0)
    openmm.LocalEnergyMinimizer.minimize(reverse_context, maxIterations=10)
    initial_state = reverse_context.getState(getPositions=True, getEnergy=True)
    print("The initial energy after minimization is %s" % str(initial_state.getPotentialEnergy()))
    initial_reverse_positions = initial_state.getPositions(asNumpy=True)
    equil_positions = simulate_hybrid(hybrid_system,functions, 1.0, initial_reverse_positions, nsteps=1000)

    #now, reverse protocol
    print("Beginning reverse protocols...")
    with progressbar.ProgressBar(max_value=n_iterations) as bar:
        for i in range(n_iterations):
            equil_positions = simulate_hybrid(hybrid_system,functions, 1.0, equil_positions)
            reverse_context.setPositions(equil_positions)
            reverse_integrator.step(ncmc_nsteps)
            w_r[i] = -1.0 * reverse_integrator.getLogAcceptanceProbability(reverse_context)
            bar.update(i)
    del reverse_context, reverse_integrator

    from pymbar import BAR
    [df, ddf] = BAR(w_f, w_r)
    print("df = %12.6f +- %12.5f kT" % (df, ddf))
Ejemplo n.º 7
0
def compare_energies(mol_name="naphthalene", ref_mol_name="benzene"):
    """
    Make an atom map where the molecule at either lambda endpoint is identical, and check that the energies are also the same.
    """
    from perses.rjmc.topology_proposal import SmallMoleculeSetProposalEngine, TopologyProposal
    from perses.annihilation.new_relative import HybridTopologyFactory
    import simtk.openmm as openmm

    from perses.tests.utils import createOEMolFromIUPAC, createSystemFromIUPAC

    mol_name = "naphthalene"
    ref_mol_name = "benzene"

    mol = createOEMolFromIUPAC(mol_name)
    m, system, positions, topology = createSystemFromIUPAC(mol_name)

    refmol = createOEMolFromIUPAC(ref_mol_name)

    #map one of the rings
    atom_map = SmallMoleculeSetProposalEngine._get_mol_atom_map(mol, refmol)

    #now use the mapped atoms to generate a new and old system with identical atoms mapped. This will result in the
    #same molecule with the same positions for lambda=0 and 1, and ensures a contiguous atom map
    effective_atom_map = {value: value for value in atom_map.values()}

    #make a topology proposal with the appropriate data:
    top_proposal = TopologyProposal(new_topology=topology,
                                    new_system=system,
                                    old_topology=topology,
                                    old_system=system,
                                    new_to_old_atom_map=effective_atom_map,
                                    new_chemical_state_key="n1",
                                    old_chemical_state_key='n2')

    factory = HybridTopologyFactory(top_proposal, positions, positions)

    alchemical_system = factory.hybrid_system
    alchemical_positions = factory.hybrid_positions

    integrator = openmm.VerletIntegrator(1)
    platform = openmm.Platform.getPlatformByName("Reference")
    context = openmm.Context(alchemical_system, integrator, platform)

    context.setPositions(alchemical_positions)

    functions = {
        'lambda_sterics':
        '2*lambda * step(0.5 - lambda) + (1.0 - step(0.5 - lambda))',
        'lambda_electrostatics': '2*(lambda - 0.5) * step(lambda - 0.5)',
        'lambda_bonds': 'lambda',
        'lambda_angles': 'lambda',
        'lambda_torsions': 'lambda'
    }

    #set all to zero
    for parm in functions.keys():
        context.setParameter(parm, 0.0)

    initial_energy = context.getState(getEnergy=True).getPotentialEnergy()

    #set all to one
    for parm in functions.keys():
        context.setParameter(parm, 1.0)

    final_energy = context.getState(getEnergy=True).getPotentialEnergy()

    if np.abs(final_energy -
              initial_energy) > 1.0e-6 * unit.kilojoule_per_mole:
        raise Exception(
            "The energy at the endpoints was not equal for molecule %s" %
            mol_name)
Ejemplo n.º 8
0
def run_hybrid_endpoint_overlap(topology_proposal, current_positions,
                                new_positions):
    """
    Test that the variance of the perturbation from lambda={0,1} to the corresponding nonalchemical endpoint is not
    too large.

    Parameters
    ----------
    topology_proposal : perses.rjmc.TopologyProposal
         TopologyProposal object describing the transformation
    current_positions : np.array, unit-bearing
         Positions of the initial system
    new_positions : np.array, unit-bearing
         Positions of the new system

    Returns
    -------
    hybrid_endpoint_results : list
       list of [df, ddf, N_eff] for 1 and 0
    """
    #create the hybrid system:
    hybrid_factory = HybridTopologyFactory(topology_proposal,
                                           current_positions,
                                           new_positions,
                                           use_dispersion_correction=True)

    #get the relevant thermodynamic states:
    nonalchemical_zero_thermodynamic_state, nonalchemical_one_thermodynamic_state, lambda_zero_thermodynamic_state, lambda_one_thermodynamic_state = generate_thermodynamic_states(
        hybrid_factory.hybrid_system, topology_proposal)

    nonalchemical_thermodynamic_states = [
        nonalchemical_zero_thermodynamic_state,
        nonalchemical_one_thermodynamic_state
    ]

    alchemical_thermodynamic_states = [
        lambda_zero_thermodynamic_state, lambda_one_thermodynamic_state
    ]

    #create an MCMCMove, BAOAB with default parameters
    mc_move = mcmc.LangevinDynamicsMove()

    initial_sampler_state = SamplerState(
        hybrid_factory.hybrid_positions,
        box_vectors=hybrid_factory.hybrid_system.getDefaultPeriodicBoxVectors(
        ))

    hybrid_endpoint_results = []
    for lambda_state in (0, 1):
        result = run_endpoint_perturbation(
            alchemical_thermodynamic_states[lambda_state],
            nonalchemical_thermodynamic_states[lambda_state],
            initial_sampler_state,
            mc_move,
            100,
            hybrid_factory,
            lambda_index=lambda_state)
        print(result)

        hybrid_endpoint_results.append(result)

    return hybrid_endpoint_results
Ejemplo n.º 9
0
def check_hybrid_round_trip_elimination(topology_proposal,
                                        positions,
                                        ncmc_nsteps=50,
                                        NSIGMA_MAX=6.0):
    """
    Test the hybrid system by switching between lambda = 1 and lambda = 0, then using BAR to compute the free energy
    difference. As the test is designed so that both endpoints are the same, the free energy difference should be zero.

    Parameters
    ----------
    topology_proposal : TopologyProposal
        The topology proposal to test.
        This must be a null transformation, where topology_proposal.old_system == topology_proposal.new_system
    ncmc_steps : int, optional, default=50
        Number of NCMC switching steps, or 0 for instantaneous switching.
    NSIGMA_MAX : float, optional, default=6.0
    """
    functions = {
        'lambda_sterics': 'lambda',
        'lambda_electrostatics': 'lambda',
        'lambda_bonds': 'lambda',
        'lambda_angles': 'lambda',
        'lambda_torsions': 'lambda'
    }
    # Initialize engine
    from perses.annihilation import NCMCGHMCAlchemicalIntegrator
    from perses.annihilation.new_relative import HybridTopologyFactory

    #The current and "proposed" positions are the same, since the molecule is not changed.
    factory = HybridTopologyFactory(topology_proposal, positions, positions)

    forward_integrator = NCMCGHMCAlchemicalIntegrator(temperature,
                                                      factory.hybrid_system,
                                                      functions,
                                                      nsteps=ncmc_nsteps,
                                                      direction='insert')
    reverse_integrator = NCMCGHMCAlchemicalIntegrator(temperature,
                                                      factory.hybrid_system,
                                                      functions,
                                                      nsteps=ncmc_nsteps,
                                                      direction='delete')

    platform = openmm.Platform.getPlatformByName("Reference")

    forward_context = openmm.Context(factory.hybrid_system, forward_integrator,
                                     platform)
    reverse_context = openmm.Context(factory.hybrid_system, reverse_integrator,
                                     platform)

    # Make sure that old system and new system are identical.
    if not (topology_proposal.old_system == topology_proposal.new_system):
        raise Exception(
            "topology_proposal must be a null transformation for this test (old_system == new_system)"
        )
    for (k, v) in topology_proposal.new_to_old_atom_map.items():
        if k != v:
            raise Exception(
                "topology_proposal must be a null transformation for this test (retailed atoms must map onto themselves)"
            )

    nequil = 5  # number of equilibration iterations
    niterations = 50  # number of round-trip switching trials
    logP_work_n_f = np.zeros([niterations], np.float64)
    for iteration in range(nequil):
        positions = simulate_hybrid(factory.hybrid_system, functions, 0.0,
                                    factory.hybrid_positions)

    #do forward switching:
    for iteration in range(niterations):
        # Equilibrate
        positions = simulate_hybrid(factory.hybrid_system, functions, 0.0,
                                    factory.hybrid_positions)

        # Check that positions are not NaN
        if (np.any(np.isnan(positions / unit.angstroms))):
            raise Exception("Positions became NaN during equilibration")

        # Hybrid NCMC
        forward_integrator.reset()
        forward_context.setPositions(positions)
        forward_integrator.step(ncmc_nsteps)
        logP_work = forward_integrator.getTotalWork(forward_context)

        # Check that positions are not NaN
        if (np.any(np.isnan(positions / unit.angstroms))):
            raise Exception("Positions became NaN on Hybrid NCMC switch")

        # Store log probability associated with work
        logP_work_n_f[iteration] = logP_work

    logP_work_n_r = np.zeros([niterations], np.float64)

    for iteration in range(nequil):
        positions = simulate_hybrid(factory.hybrid_system, functions, 1.0,
                                    factory.hybrid_positions)

    #do forward switching:
    for iteration in range(niterations):
        # Equilibrate
        positions = simulate_hybrid(factory.hybrid_system, functions, 1.0,
                                    factory.hybrid_positions)

        # Check that positions are not NaN
        if (np.any(np.isnan(positions / unit.angstroms))):
            raise Exception("Positions became NaN during equilibration")

        # Hybrid NCMC
        reverse_integrator.reset()
        reverse_context.setPositions(positions)
        reverse_integrator.step(ncmc_nsteps)
        logP_work = reverse_integrator.getTotalWork(forward_context)

        # Check that positions are not NaN
        if (np.any(np.isnan(positions / unit.angstroms))):
            raise Exception("Positions became NaN on Hybrid NCMC switch")

        # Store log probability associated with work
        logP_work_n_r[iteration] = logP_work

    work_f = -logP_work_n_f
    work_r = -logP_work_n_r
    from pymbar import BAR
    [df, ddf] = BAR(work_f, work_r)
    print("df = %12.6f +- %12.5f kT" % (df, ddf))
    if (abs(df) > NSIGMA_MAX * ddf):
        msg = 'Delta F (%d steps switching) = %f +- %f kT; should be within %f sigma of 0\n' % (
            ncmc_nsteps, df, ddf, NSIGMA_MAX)
        msg += 'logP_work_n:\n'
        msg += str(work_f) + '\n'
        msg += str(work_r) + '\n'
        raise Exception(msg)
Ejemplo n.º 10
0
def check_alchemical_hybrid_elimination_bar(topology_proposal,
                                            positions,
                                            ncmc_nsteps=50,
                                            NSIGMA_MAX=6.0,
                                            geometry=False):
    """
    Check that the hybrid topology, where both endpoints are identical, returns a free energy within NSIGMA_MAX of 0.
    Parameters
    ----------
    topology_proposal
    positions
    ncmc_nsteps
    NSIGMA_MAX

    Returns
    -------

    """
    from perses.annihilation import NCMCGHMCAlchemicalIntegrator
    from perses.annihilation.new_relative import HybridTopologyFactory

    #make the hybrid topology factory:
    factory = HybridTopologyFactory(topology_proposal, positions, positions)

    platform = openmm.Platform.getPlatformByName("Reference")

    hybrid_system = factory.hybrid_system
    hybrid_topology = factory.hybrid_topology
    initial_hybrid_positions = factory.hybrid_positions

    n_iterations = 50  #number of times to do NCMC protocol

    #alchemical functions
    functions = {
        'lambda_sterics':
        '2*lambda * step(0.5 - lambda) + (1.0 - step(0.5 - lambda))',
        'lambda_electrostatics': '2*(lambda - 0.5) * step(lambda - 0.5)',
        'lambda_bonds': 'lambda',
        'lambda_angles': 'lambda',
        'lambda_torsions': 'lambda'
    }

    w_f = np.zeros(n_iterations)
    w_r = np.zeros(n_iterations)

    #make the alchemical integrators:
    #forward_integrator = NCMCGHMCAlchemicalIntegrator(temperature, hybrid_system, functions, nsteps=ncmc_nsteps, direction='insert')
    #reverse_integrator = NCMCGHMCAlchemicalIntegrator(temperature, hybrid_system, functions, nsteps=ncmc_nsteps, direction='delete')

    #first, do forward protocol (lambda=0 -> 1)
    for i in range(n_iterations):
        forward_integrator = NCMCGHMCAlchemicalIntegrator(temperature,
                                                          hybrid_system,
                                                          functions,
                                                          nsteps=ncmc_nsteps,
                                                          direction='insert')
        equil_positions = simulate_hybrid(hybrid_system, functions, 0.0,
                                          initial_hybrid_positions)
        context = openmm.Context(hybrid_system, forward_integrator, platform)
        context.setPositions(equil_positions)
        forward_integrator.step(ncmc_nsteps)
        w_f[i] = -1.0 * forward_integrator.getLogAcceptanceProbability(context)
        print(i)
        del context, forward_integrator

    #now, reverse protocol
    for i in range(n_iterations):
        reverse_integrator = NCMCGHMCAlchemicalIntegrator(temperature,
                                                          hybrid_system,
                                                          functions,
                                                          nsteps=ncmc_nsteps,
                                                          direction='delete')
        equil_positions = simulate_hybrid(hybrid_system, functions, 1.0,
                                          initial_hybrid_positions)
        context = openmm.Context(hybrid_system, reverse_integrator, platform)
        context.setPositions(equil_positions)
        reverse_integrator.step(ncmc_nsteps)
        w_r[i] = -1.0 * reverse_integrator.getLogAcceptanceProbability(context)
        print(i)
        del context, reverse_integrator

    from pymbar import BAR
    [df, ddf] = BAR(w_f, w_r)
    print("df = %12.6f +- %12.5f kT" % (df, ddf))
    if (abs(df) > NSIGMA_MAX * ddf):
        msg = 'Delta F (%d steps switching) = %f +- %f kT; should be within %f sigma of 0\n' % (
            ncmc_nsteps, df, ddf, NSIGMA_MAX)
        msg += 'logP_work_n:\n'
        msg += str(w_f) + '\n'
        msg += str(w_r) + '\n'
        raise Exception(msg)
Ejemplo n.º 11
0
    def __init__(self,
                 topology_proposal,
                 pos_old,
                 new_positions,
                 use_dispersion_correction=False,
                 forward_functions=None,
                 ncmc_nsteps=100,
                 nsteps_per_iteration=1,
                 concurrency=4,
                 platform_name="OpenCL",
                 temperature=300.0 * unit.kelvin,
                 trajectory_directory=None,
                 trajectory_prefix=None):

        #construct the hybrid topology factory object
        self._factory = HybridTopologyFactory(
            topology_proposal,
            pos_old,
            new_positions,
            use_dispersion_correction=use_dispersion_correction)

        #use default functions if none specified
        if forward_functions == None:
            self._forward_functions = self.default_forward_functions
        else:
            self._forward_functions = forward_functions

        #reverse functions to get a symmetric protocol
        self._reverse_functions = {
            param: param_formula.replace("lambda", "(1-lambda)")
            for param, param_formula in self._forward_functions.items()
        }

        #set up some class attributes
        self._hybrid_system = self._factory.hybrid_system
        self._initial_hybrid_positions = self._factory.hybrid_positions
        self._concurrency = concurrency
        self._ncmc_nsteps = ncmc_nsteps
        self._nsteps_per_iteration = nsteps_per_iteration
        self._trajectory_prefix = trajectory_prefix
        self._trajectory_directory = trajectory_directory
        self._zero_endpoint_n_atoms = topology_proposal.n_atoms_old
        self._one_endpoint_n_atoms = topology_proposal.n_atoms_new

        #initialize lists for results
        self._forward_nonequilibrium_trajectories = []
        self._reverse_nonequilibrium_trajectories = []
        self._forward_nonequilibrium_cumulative_works = []
        self._reverse_nonequilibrium_cumulative_works = []
        self._forward_nonequilibrium_results = []
        self._reverse_nonequilibrium_results = []
        self._forward_total_work = []
        self._reverse_total_work = []
        self._lambda_zero_reduced_potentials = []
        self._lambda_one_reduced_potentials = []
        self._nonalchemical_zero_endpt_reduced_potentials = []
        self._nonalchemical_one_endpt_reduced_potentials = []
        self._nonalchemical_zero_results = []
        self._nonalchemical_one_results = []

        #Set the number of times that the nonequilbrium move will have to be run in order to complete a protocol:
        if self._ncmc_nsteps % self._nsteps_per_iteration != 0:
            logging.warning(
                "The number of ncmc steps is not divisible by the number of steps per iteration. You may not have a full protocol."
            )
        self._n_iterations_per_call = self._ncmc_nsteps // self._nsteps_per_iteration

        #create the thermodynamic state
        lambda_zero_alchemical_state = alchemy.AlchemicalState.from_system(
            self._hybrid_system)
        lambda_one_alchemical_state = copy.deepcopy(
            lambda_zero_alchemical_state)

        #ensure their states are set appropriately
        lambda_zero_alchemical_state.set_alchemical_parameters(0.0)
        lambda_one_alchemical_state.set_alchemical_parameters(0.0)

        #create the base thermodynamic state with the hybrid system
        self._thermodynamic_state = ThermodynamicState(self._hybrid_system,
                                                       temperature=temperature)

        #Create thermodynamic states for the nonalchemical endpoints
        self._nonalchemical_zero_thermodynamic_state = ThermodynamicState(
            topology_proposal.old_system, temperature=temperature)
        self._nonalchemical_one_thermodynamic_state = ThermodynamicState(
            topology_proposal.new_system, temperature=temperature)

        #Now create the compound states with different alchemical states
        self._lambda_zero_thermodynamic_state = CompoundThermodynamicState(
            self._thermodynamic_state,
            composable_states=[lambda_zero_alchemical_state])
        self._lambda_one_thermodynamic_state = CompoundThermodynamicState(
            self._thermodynamic_state,
            composable_states=[lambda_one_alchemical_state])

        #create the forward and reverse integrators
        self._forward_integrator = AlchemicalNonequilibriumLangevinIntegrator(
            alchemical_functions=self._forward_functions,
            nsteps_neq=ncmc_nsteps,
            temperature=temperature)
        self._reverse_integrator = AlchemicalNonequilibriumLangevinIntegrator(
            alchemical_functions=self._reverse_functions,
            nsteps_neq=ncmc_nsteps,
            temperature=temperature)

        #create the forward and reverse MCMoves
        self._forward_ne_mc_move = NonequilibriumSwitchingMove(
            self._forward_integrator, self._nsteps_per_iteration)
        self._reverse_ne_mc_move = NonequilibriumSwitchingMove(
            self._reverse_integrator, self._nsteps_per_iteration)

        #create the equilibrium MCMove
        self._equilibrium_mc_move = mcmc.LangevinSplittingDynamicsMove()

        #set the SamplerState for the lambda 0 and 1 equilibrium simulations
        self._lambda_one_sampler_state = SamplerState(
            self._initial_hybrid_positions,
            box_vectors=self._hybrid_system.getDefaultPeriodicBoxVectors())
        self._lambda_zero_sampler_state = copy.deepcopy(
            self._lambda_one_sampler_state)

        #initialize by minimizing
        self.minimize()

        #initialize the trajectories for the lambda 0 and 1 equilibrium simulations
        a_0, b_0, c_0, alpha_0, beta_0, gamma_0 = mdtrajutils.unitcell.box_vectors_to_lengths_and_angles(
            *self._lambda_zero_sampler_state.box_vectors)
        a_1, b_1, c_1, alpha_1, beta_1, gamma_1 = mdtrajutils.unitcell.box_vectors_to_lengths_and_angles(
            *self._lambda_one_sampler_state.box_vectors)

        self._lambda_zero_traj = md.Trajectory(
            np.array(self._lambda_zero_sampler_state.positions),
            self._factory.hybrid_topology,
            unitcell_lengths=[a_0, b_0, c_0],
            unitcell_angles=[alpha_0, beta_0, gamma_0])
        self._lambda_one_traj = md.Trajectory(
            np.array(self._lambda_one_sampler_state.positions),
            self._factory.hybrid_topology,
            unitcell_lengths=[a_1, b_1, c_1],
            unitcell_angles=[alpha_1, beta_1, gamma_1])
Ejemplo n.º 12
0
class NonequilibriumSwitchingFEP(object):
    """
    This class manages Nonequilibrium switching based relative free energy calculations, carried out on a distributed computing framework.
    """

    default_forward_functions = {
        'lambda_sterics': 'lambda',
        'lambda_electrostatics': 'lambda',
        'lambda_bonds': 'lambda',
        'lambda_angles': 'lambda',
        'lambda_torsions': 'lambda'
    }

    def __init__(self,
                 topology_proposal,
                 pos_old,
                 new_positions,
                 use_dispersion_correction=False,
                 forward_functions=None,
                 ncmc_nsteps=100,
                 nsteps_per_iteration=1,
                 concurrency=4,
                 platform_name="OpenCL",
                 temperature=300.0 * unit.kelvin,
                 trajectory_directory=None,
                 trajectory_prefix=None):

        #construct the hybrid topology factory object
        self._factory = HybridTopologyFactory(
            topology_proposal,
            pos_old,
            new_positions,
            use_dispersion_correction=use_dispersion_correction)

        #use default functions if none specified
        if forward_functions == None:
            self._forward_functions = self.default_forward_functions
        else:
            self._forward_functions = forward_functions

        #reverse functions to get a symmetric protocol
        self._reverse_functions = {
            param: param_formula.replace("lambda", "(1-lambda)")
            for param, param_formula in self._forward_functions.items()
        }

        #set up some class attributes
        self._hybrid_system = self._factory.hybrid_system
        self._initial_hybrid_positions = self._factory.hybrid_positions
        self._concurrency = concurrency
        self._ncmc_nsteps = ncmc_nsteps
        self._nsteps_per_iteration = nsteps_per_iteration
        self._trajectory_prefix = trajectory_prefix
        self._trajectory_directory = trajectory_directory
        self._zero_endpoint_n_atoms = topology_proposal.n_atoms_old
        self._one_endpoint_n_atoms = topology_proposal.n_atoms_new

        #initialize lists for results
        self._forward_nonequilibrium_trajectories = []
        self._reverse_nonequilibrium_trajectories = []
        self._forward_nonequilibrium_cumulative_works = []
        self._reverse_nonequilibrium_cumulative_works = []
        self._forward_nonequilibrium_results = []
        self._reverse_nonequilibrium_results = []
        self._forward_total_work = []
        self._reverse_total_work = []
        self._lambda_zero_reduced_potentials = []
        self._lambda_one_reduced_potentials = []
        self._nonalchemical_zero_endpt_reduced_potentials = []
        self._nonalchemical_one_endpt_reduced_potentials = []
        self._nonalchemical_zero_results = []
        self._nonalchemical_one_results = []

        #Set the number of times that the nonequilbrium move will have to be run in order to complete a protocol:
        if self._ncmc_nsteps % self._nsteps_per_iteration != 0:
            logging.warning(
                "The number of ncmc steps is not divisible by the number of steps per iteration. You may not have a full protocol."
            )
        self._n_iterations_per_call = self._ncmc_nsteps // self._nsteps_per_iteration

        #create the thermodynamic state
        lambda_zero_alchemical_state = alchemy.AlchemicalState.from_system(
            self._hybrid_system)
        lambda_one_alchemical_state = copy.deepcopy(
            lambda_zero_alchemical_state)

        #ensure their states are set appropriately
        lambda_zero_alchemical_state.set_alchemical_parameters(0.0)
        lambda_one_alchemical_state.set_alchemical_parameters(0.0)

        #create the base thermodynamic state with the hybrid system
        self._thermodynamic_state = ThermodynamicState(self._hybrid_system,
                                                       temperature=temperature)

        #Create thermodynamic states for the nonalchemical endpoints
        self._nonalchemical_zero_thermodynamic_state = ThermodynamicState(
            topology_proposal.old_system, temperature=temperature)
        self._nonalchemical_one_thermodynamic_state = ThermodynamicState(
            topology_proposal.new_system, temperature=temperature)

        #Now create the compound states with different alchemical states
        self._lambda_zero_thermodynamic_state = CompoundThermodynamicState(
            self._thermodynamic_state,
            composable_states=[lambda_zero_alchemical_state])
        self._lambda_one_thermodynamic_state = CompoundThermodynamicState(
            self._thermodynamic_state,
            composable_states=[lambda_one_alchemical_state])

        #create the forward and reverse integrators
        self._forward_integrator = AlchemicalNonequilibriumLangevinIntegrator(
            alchemical_functions=self._forward_functions,
            nsteps_neq=ncmc_nsteps,
            temperature=temperature)
        self._reverse_integrator = AlchemicalNonequilibriumLangevinIntegrator(
            alchemical_functions=self._reverse_functions,
            nsteps_neq=ncmc_nsteps,
            temperature=temperature)

        #create the forward and reverse MCMoves
        self._forward_ne_mc_move = NonequilibriumSwitchingMove(
            self._forward_integrator, self._nsteps_per_iteration)
        self._reverse_ne_mc_move = NonequilibriumSwitchingMove(
            self._reverse_integrator, self._nsteps_per_iteration)

        #create the equilibrium MCMove
        self._equilibrium_mc_move = mcmc.LangevinSplittingDynamicsMove()

        #set the SamplerState for the lambda 0 and 1 equilibrium simulations
        self._lambda_one_sampler_state = SamplerState(
            self._initial_hybrid_positions,
            box_vectors=self._hybrid_system.getDefaultPeriodicBoxVectors())
        self._lambda_zero_sampler_state = copy.deepcopy(
            self._lambda_one_sampler_state)

        #initialize by minimizing
        self.minimize()

        #initialize the trajectories for the lambda 0 and 1 equilibrium simulations
        a_0, b_0, c_0, alpha_0, beta_0, gamma_0 = mdtrajutils.unitcell.box_vectors_to_lengths_and_angles(
            *self._lambda_zero_sampler_state.box_vectors)
        a_1, b_1, c_1, alpha_1, beta_1, gamma_1 = mdtrajutils.unitcell.box_vectors_to_lengths_and_angles(
            *self._lambda_one_sampler_state.box_vectors)

        self._lambda_zero_traj = md.Trajectory(
            np.array(self._lambda_zero_sampler_state.positions),
            self._factory.hybrid_topology,
            unitcell_lengths=[a_0, b_0, c_0],
            unitcell_angles=[alpha_0, beta_0, gamma_0])
        self._lambda_one_traj = md.Trajectory(
            np.array(self._lambda_one_sampler_state.positions),
            self._factory.hybrid_topology,
            unitcell_lengths=[a_1, b_1, c_1],
            unitcell_angles=[alpha_1, beta_1, gamma_1])

    def minimize(self, max_steps=50):
        """
        Minimize both end states. This method updates the _sampler_state attributes for each lambda

        Parameters
        ----------
        max_steps : int, default 50
            max number of steps for openmm minimizer.
        """
        #Asynchronously invoke the tasks
        minimized_lambda_zero_result = feptasks.minimize.delay(
            self._lambda_zero_thermodynamic_state,
            self._lambda_zero_sampler_state,
            self._equilibrium_mc_move,
            max_iterations=max_steps)
        minimized_lambda_one_result = feptasks.minimize.delay(
            self._lambda_one_thermodynamic_state,
            self._lambda_one_sampler_state,
            self._equilibrium_mc_move,
            max_iterations=max_steps)

        #now synchronously retrieve the results and save the sampler states.
        self._lambda_zero_sampler_state = minimized_lambda_zero_result.get()
        self._lambda_one_sampler_state = minimized_lambda_one_result.get()

    def run(self, n_iterations=5, concurrency=1):
        """
        Run one iteration of the nonequilibrium switching free energy calculations. This entails:

        - 1 iteration of equilibrium at lambda=0 and lambda=1
        - concurrency (parameter) many nonequilibrium trajectories in both forward and reverse
           (e.g., if concurrency is 5, then 5 forward and 5 reverse protocols will be run)
        - 1 iteration of equilibrium at lambda=0 and lambda=1

        Parameters
        ----------
        n_iterations : int, optional, default 5
            The number of times to run the entire sequence described above
        concurrency: int, default 1
            The number of concurrent nonequilibrium protocols to run; note that with greater than one,
            error estimation may be more complicated.
        """
        for i in range(n_iterations):
            self._run_equilibrium()
            self._run_nonequilibrium(concurrency=concurrency,
                                     n_iterations=self._n_iterations_per_call)
            self._run_equilibrium()

        if self._trajectory_directory:
            self._write_equilibrium_trajectories(self._trajectory_directory,
                                                 self._trajectory_prefix)

    def _run_equilibrium(self, n_iterations=1):
        """
        Run one iteration of equilibrium at lambda=1 and lambda=0, and replace the current equilibrium sampler states
        with the results of the equilibrium calculation, as well as extend the current equilibrium trajectories.

        Parameters
        ----------
        n_iterations : int, default 1
            How many times to run the n_steps of equilibrium
        """
        #run equilibrium for lambda=0 and lambda=1
        lambda_zero_result = feptasks.run_equilibrium.delay(
            self._lambda_zero_thermodynamic_state,
            self._lambda_zero_sampler_state, self._equilibrium_mc_move,
            self._factory.hybrid_topology, n_iterations)
        lambda_one_result = feptasks.run_equilibrium.delay(
            self._lambda_one_thermodynamic_state,
            self._lambda_one_sampler_state, self._equilibrium_mc_move,
            self._factory.hybrid_topology, n_iterations)

        #retrieve the results of the calculation
        self._lambda_zero_sampler_state, traj_zero_result, lambda_zero_reduced_potential = lambda_zero_result.get(
        )
        self._lambda_one_sampler_state, traj_one_result, lambda_one_reduced_potential = lambda_one_result.get(
        )

        #append the potential energies of the final frame of the trajectories
        self._lambda_zero_reduced_potentials.append(
            lambda_zero_reduced_potential)
        self._lambda_one_reduced_potentials.append(
            lambda_one_reduced_potential)

        #Now create SamplerStates to generate the data for endpoint perturbations:
        final_hybrid_positions_zero = self._lambda_zero_sampler_state.positions
        final_hybrid_positions_one = self._lambda_one_sampler_state.positions

        positions_zero = self._factory.old_positions(
            final_hybrid_positions_zero)
        positions_one = self._factory.new_positions(final_hybrid_positions_one)

        #Create sampler states for each of these:
        sampler_state_zero = SamplerState(
            positions_zero,
            box_vectors=self._lambda_zero_sampler_state.box_vectors)
        sampler_state_one = SamplerState(
            positions_one,
            box_vectors=self._lambda_one_sampler_state.box_vectors)

        #launch a task to compute the reduced potentials at these endpoints
        self._nonalchemical_zero_results.append(
            feptasks.compute_reduced_potential.delay(
                self._nonalchemical_zero_thermodynamic_state,
                sampler_state_zero))
        self._nonalchemical_one_results.append(
            feptasks.compute_reduced_potential.delay(
                self._nonalchemical_one_thermodynamic_state,
                sampler_state_one))

        #join the trajectories to the reference trajectories, if the object exists,
        #otherwise, simply create it
        if self._lambda_zero_traj:
            self._lambda_zero_traj = self._lambda_zero_traj.join(
                traj_zero_result, check_topology=False)
        else:
            self._lambda_zero_traj = traj_zero_result

        if self._lambda_one_traj:
            self._lambda_one_traj = self._lambda_one_traj.join(
                traj_one_result, check_topology=False)
        else:
            self._lambda_one_traj = traj_one_result

    def _run_nonequilibrium(self, concurrency=1, n_iterations=1):
        """
        Run concurrency-many nonequilibrium protocols in both directions. This method stores the result object, but does
        not retrieve the results. Note that n_iterations is important, since in order to perform an entire switching trajectory
        (from 0 to 1 or vice versa), we require that n_steps*n_iterations = protocol length

        Parameters
        ----------
        concurrency : int, default 1
            The number of protocols to run in each direction simultaneously
        n_iterations : int, default 1
            The number of times to have the NE move applied. Note that as above if n_steps*n_iterations!=ncmc_nsteps,
            the protocol will not be run properly.
        """

        #set up the group object that will be used to compute the nonequilibrium results.
        forward_protocol_group = celery.group(
            feptasks.run_protocol.s(
                self._lambda_zero_thermodynamic_state,
                self._lambda_zero_sampler_state, self._forward_ne_mc_move,
                self._factory.hybrid_topology, n_iterations)
            for i in range(concurrency))
        reverse_protocol_group = celery.group(
            feptasks.run_protocol.s(
                self._lambda_one_thermodynamic_state,
                self._lambda_one_sampler_state, self._reverse_ne_mc_move,
                self._factory.hybrid_topology, n_iterations)
            for i in range(concurrency))

        #get the result objects:
        self._forward_nonequilibrium_results.append(
            forward_protocol_group.apply_async())
        self._reverse_nonequilibrium_results.append(
            reverse_protocol_group.apply_async())

    def retrieve_nonequilibrium_results(self):
        """
        Retrieve any pending results that were generated by computations from the run() call. Note that this will block
        until all have completed. This method will update the list of trajectories as well as the nonequilibrium work values.
        """
        for result in self._forward_nonequilibrium_results:
            result_group = result.join()
            for result in result_group:
                traj, cum_work = result

                #we can take the final element as the total work
                self._forward_total_work.append(cum_work[-1])

                #we'll append the cumulative work and the trajectory to the appropriate lists
                self._forward_nonequilibrium_cumulative_works.append(cum_work)
                self._forward_nonequilibrium_trajectories.append(traj)

        for result in self._reverse_nonequilibrium_results:
            result_group = result.join()
            for result in result_group:
                traj, cum_work = result

                #we can take the final element as the total work
                self._reverse_total_work.append(cum_work[-1])

                #we'll append the cumulative work and the trajectory to the appropriate lists
                self._reverse_nonequilibrium_cumulative_works.append(cum_work)
                self._reverse_nonequilibrium_trajectories.append(traj)

    def write_nonequilibrium_trajectories(self, directory, file_prefix):
        """
        Write out an MDTraj h5 file for each nonequilibrium trajectory. The files will be placed in
        [directory]/file_prefix-[forward, reverse]-index.h5. This method will ensure that all pending
        results are collected.

        Parameters
        ----------
        directory : str
            The directory in which to place the files
        file_prefix : str
            A prefix for the filenames
        """
        self.retrieve_nonequilibrium_results()

        #loop through the forward trajectories
        for index, forward_trajectory in enumerate(
                self._forward_nonequilibrium_trajectories):

            #construct the name for this file
            full_filename = os.path.join(
                directory, file_prefix + "forward" + str(index) + ".h5")

            #save the trajectory
            forward_trajectory.save_hdf5(full_filename)

        #repeat for the reverse trajectories:
        for index, reverse_trajectory in enumerate(
                self._reverse_nonequilibrium_trajectories):

            #construct the name for this file
            full_filename = os.path.join(
                directory, file_prefix + "reverse" + str(index) + ".h5")

            #save the trajectory
            reverse_trajectory.save_hdf5(full_filename)

    def _write_equilibrium_trajectories(self, directory, file_prefix):
        """
        Write out an MDTraj h5 file for each nonequilibrium trajectory. The files will be placed in
        [directory]/file_prefix-[lambda0, lambda1].h5.

        Parameters
        ----------
        directory : str
            The directory in which to place the files
        file_prefix : str
            A prefix for the filenames
        """
        lambda_zero_filename = os.path.join(
            directory, file_prefix + "-" + "lambda0" + ".h5")
        lambda_one_filename = os.path.join(
            directory, file_prefix + "-" + "lambda1" + ".h5")

        filenames = [lambda_zero_filename, lambda_one_filename]
        trajs = [self._lambda_zero_traj, self._lambda_one_traj]

        #open the existing file if it exists, and append. Otherwise create it
        for filename, traj in zip(filenames, trajs):
            if not os.path.exists(filename):
                traj.save_hdf5(filename)
            else:
                written_traj = md.load(filename)
                concatenated_traj = written_traj.join(traj)
                concatenated_traj.save_hdf5(filename)

        #delete the trajectories.
        self._lambda_one_traj = None
        self._lambda_zero_traj = None

    def retrieve_nonalchemical_results(self):
        """
        Call this to retrieve the reduced potential results for the nonalchemical endpoints
        """
        for nonalchemical_result_zero, nonalchemical_result_one in zip(
                self._nonalchemical_zero_results,
                self._nonalchemical_one_results):
            self._nonalchemical_zero_endpt_reduced_potentials.append(
                nonalchemical_result_zero.get())
            self._nonalchemical_one_endpt_reduced_potentials.append(
                nonalchemical_result_one.get())

        self._nonalchemical_zero_results = []
        self._nonalchemcal_one_results = []

    @property
    def zero_endpoint_perturbation(self):
        hybrid_reduced_potentials = np.array(
            self._lambda_zero_reduced_potentials)
        nonalchemical_reduced_potentials = np.array(
            self._nonalchemical_zero_endpt_reduced_potentials)
        [df, ddf] = pymbar.EXP(nonalchemical_reduced_potentials -
                               hybrid_reduced_potentials)
        return [df, ddf]

    @property
    def one_endpoint_perturbation(self):
        hybrid_reduced_potentials = np.array(
            self._lambda_one_reduced_potentials)
        nonalchemical_reduced_potentials = np.array(
            self._lambda_zero_reduced_potentials)
        [df, ddf] = pymbar.EXP(nonalchemical_reduced_potentials -
                               hybrid_reduced_potentials)
        return [df, ddf]

    @property
    def lambda_zero_equilibrium_trajectory(self):
        return self._lambda_zero_traj

    @property
    def lambda_one_equilibrium_trajectory(self):
        return self._lambda_one_traj

    @property
    def forward_nonequilibrium_trajectories(self):
        return self._forward_nonequilibrium_trajectories

    @property
    def reverse_nonequilibrium_trajectories(self):
        return self._reverse_nonequilibrium_trajectories

    @property
    def forward_cumulative_works(self):
        return self._forward_nonequilibrium_cumulative_works

    @property
    def reverse_cumulative_works(self):
        return self._reverse_nonequilibrium_cumulative_works

    @property
    def current_free_energy_estimate(self):
        [df, ddf] = pymbar.BAR(self._forward_total_work,
                               self._reverse_total_work)
        return [df, ddf]