def test_setup_hybrid_system():
    alanine_topology, alanine_positions, leucine_topology, leucine_positions, atom_map = build_two_residues()

    alanine_system = forcefield.createSystem(alanine_topology)
    leucine_system = forcefield.createSystem(leucine_topology)

    atom_map = {value : key for key, value in atom_map.items()}
    hybrid = HybridTopologyFactory(leucine_system, alanine_system, leucine_topology, alanine_topology, leucine_positions, alanine_positions, atom_map, softening=0.0)
    [system, topology, positions, sys2_indices_in_system, sys1_indices_in_system] = hybrid.createPerturbedSystem()

    compute_alchemical_correction(leucine_system, alanine_system, system, leucine_positions, positions, positions, alanine_positions)
def test_generate_endpoint_thermodynamic_states():
    """
    test whether the hybrid system zero and one thermodynamic states have the appropriate lambda values
    """
    topology_proposal, current_positions, new_positions = utils.generate_solvated_hybrid_test_topology(
        current_mol_name='propane', proposed_mol_name='pentane', vacuum=False)
    hybrid_factory = HybridTopologyFactory(topology_proposal,
                                           current_positions,
                                           new_positions,
                                           use_dispersion_correction=True)

    #get the relevant thermodynamic states:
    _, _, lambda_zero_thermodynamic_state, lambda_one_thermodynamic_state = utils.generate_endpoint_thermodynamic_states(
        hybrid_factory.hybrid_system, topology_proposal)
    # check the parameters for each state
    lambda_protocol = [
        'lambda_sterics_core', 'lambda_electrostatics_core',
        'lambda_sterics_insert', 'lambda_electrostatics_insert',
        'lambda_sterics_delete', 'lambda_electrostatics_delete'
    ]
    for value in lambda_protocol:
        if getattr(lambda_zero_thermodynamic_state, value) != 0.:
            raise Exception(
                'Interaction {} not set to 0. at lambda = 0. {} set to {}'.
                format(value, value,
                       getattr(lambda_one_thermodynamic_state, value)))
        if getattr(lambda_one_thermodynamic_state, value) != 1.:
            raise Exception(
                'Interaction {} not set to 1. at lambda = 1. {} set to {}'.
                format(value, value,
                       getattr(lambda_one_thermodynamic_state, value)))
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 openmmtools import alchemy, states
    from perses.rjmc.topology_proposal import SmallMoleculeSetProposalEngine, TopologyProposal
    from perses.annihilation.relative import HybridTopologyFactory
    import simtk.openmm as openmm

    from perses.utils.openeye import createSystemFromIUPAC
    from openmoltools.openeye import iupac_to_oemol, generate_conformers

    mol = iupac_to_oemol(mol_name)
    mol = generate_conformers(mol, max_confs=1)
    m, system, positions, topology = createSystemFromIUPAC(mol_name)

    refmol = iupac_to_oemol(ref_mol_name)
    refmol = generate_conformers(refmol, max_confs=1)

    #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

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

    _, _, alch_zero_state, alch_one_state = utils.generate_endpoint_thermodynamic_states(
        alchemical_system, top_proposal)

    rp_list = []
    for state in [alch_zero_state, alch_one_state]:
        integrator = openmm.VerletIntegrator(1)
        context = state.create_context(integrator, platform)
        samplerstate = states.SamplerState(
            positions=alchemical_positions,
            box_vectors=alchemical_system.getDefaultPeriodicBoxVectors())
        samplerstate.apply_to_context(context)
        rp = state.reduced_potential(context)
        rp_list.append(rp)
        del context, integrator

    assert abs(rp_list[0] - rp_list[1]) < 1e-6
Beispiel #4
0
def generate_top_pos_sys(topology, old_oemol, new_oemol, system, positions,
                         system_generator, map_strength):
    """generate point mutation engine, geometry_engine, and conduct topology proposal, geometry propsal, and hybrid factory generation"""
    #create the point mutation engine
    print(f"generating point mutation engine")
    proposal_engine = SmallMoleculeSetProposalEngine(['CCCCO', 'CCCCS'],
                                                     system_generator,
                                                     map_strength=map_strength,
                                                     residue_name='MOL')

    #create a geometry engine
    print(f"generating geometry engine")
    geometry_engine = FFAllAngleGeometryEngine(metadata=None,
                                               use_sterics=False,
                                               n_bond_divisions=100,
                                               n_angle_divisions=180,
                                               n_torsion_divisions=360,
                                               verbose=True,
                                               storage=None,
                                               bond_softening_constant=1.0,
                                               angle_softening_constant=1.0,
                                               neglect_angles=False,
                                               use_14_nonbondeds=False)

    #create a top proposal
    print(f"making topology proposal")
    topology_proposal = proposal_engine.propose(system, topology, old_oemol,
                                                new_oemol)

    #make a geometry proposal forward
    print(f"making geometry proposal")
    forward_new_positions, logp_proposal = geometry_engine.propose(
        topology_proposal, positions, beta)

    #create a hybrid topology factory
    f"making forward hybridtopologyfactory"
    forward_htf = HybridTopologyFactory(topology_proposal=topology_proposal,
                                        current_positions=positions,
                                        new_positions=forward_new_positions,
                                        use_dispersion_correction=False,
                                        functions=None,
                                        softcore_alpha=None,
                                        bond_softening_constant=1.0,
                                        angle_softening_constant=1.0,
                                        soften_only_new=False,
                                        neglected_new_angle_terms=[],
                                        neglected_old_angle_terms=[],
                                        softcore_LJ_v2=True,
                                        softcore_electrostatics=True,
                                        softcore_LJ_v2_alpha=0.85,
                                        softcore_electrostatics_alpha=0.3,
                                        softcore_sigma_Q=1.0,
                                        interpolate_old_and_new_14s=False,
                                        omitted_terms=None)

    return topology_proposal, forward_new_positions, forward_htf
def test_position_output():
    """
    Test that the hybrid returns the correct positions for the new and old systems after construction
    """
    from perses.annihilation.relative import HybridTopologyFactory
    import numpy as np

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

    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)))
def test_setup_hybrid_system():
    alanine_topology, alanine_positions, leucine_topology, leucine_positions, atom_map = build_two_residues(
    )

    alanine_system = forcefield.createSystem(alanine_topology)
    leucine_system = forcefield.createSystem(leucine_topology)

    atom_map = {value: key for key, value in atom_map.items()}
    hybrid = HybridTopologyFactory(leucine_system,
                                   alanine_system,
                                   leucine_topology,
                                   alanine_topology,
                                   leucine_positions,
                                   alanine_positions,
                                   atom_map,
                                   softening=0.0)
    [
        system, topology, positions, sys2_indices_in_system,
        sys1_indices_in_system
    ] = hybrid.createPerturbedSystem()

    compute_alchemical_correction(leucine_system, alanine_system, system,
                                  leucine_positions, positions, positions,
                                  alanine_positions)
Beispiel #7
0
def test_create_endstates():
    """
    test the creation of unsampled endstates
    """
    from pkg_resources import resource_filename
    smiles_filename = resource_filename("perses",
                                        os.path.join("data", "test.smi"))
    fe_setup = RelativeFEPSetup(ligand_input=smiles_filename,
                                old_ligand_index=0,
                                new_ligand_index=1,
                                forcefield_files=[],
                                small_molecule_forcefield='gaff-2.11',
                                phases=['vacuum'])

    hybrid_factory = HybridTopologyFactory(
        topology_proposal=fe_setup._vacuum_topology_proposal,
        current_positions=fe_setup._vacuum_positions_old,
        new_positions=fe_setup._vacuum_positions_new,
        neglected_new_angle_terms=fe_setup._vacuum_forward_neglected_angles,
        neglected_old_angle_terms=fe_setup._vacuum_reverse_neglected_angles,
        softcore_LJ_v2=True,
        interpolate_old_and_new_14s=False)

    zero_state_error, one_state_error = validate_endstate_energies(
        fe_setup._vacuum_topology_proposal,
        hybrid_factory,
        added_energy=fe_setup._vacuum_added_valence_energy,
        subtracted_energy=fe_setup._vacuum_subtracted_valence_energy,
        beta=beta,
        platform=openmm.Platform.getPlatformByName('Reference'),
        ENERGY_THRESHOLD=ENERGY_THRESHOLD)

    lambda_alchemical_state = RelativeAlchemicalState.from_system(
        hybrid_factory.hybrid_system)
    lambda_protocol = LambdaProtocol(functions='default')
    lambda_alchemical_state.set_alchemical_parameters(0.0, lambda_protocol)
    thermodynamic_state = CompoundThermodynamicState(
        ThermodynamicState(hybrid_factory.hybrid_system,
                           temperature=temperature),
        composable_states=[lambda_alchemical_state])
    zero_endstate = copy.deepcopy(thermodynamic_state)
    one_endstate = copy.deepcopy(thermodynamic_state)
    one_endstate.set_alchemical_parameters(1.0, lambda_protocol)
    new_endstates = create_endstates(zero_endstate, one_endstate)
Beispiel #8
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.

        Parameters
        ----------
        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.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,
                    bond_softening_constant=self._bond_softening_constant,
                    angle_softening_constant=self._angle_softening_constant)
                self._hybrid_cache[topology_proposal] = hybrid_factory
            except:
                hybrid_factory = None

        return hybrid_factory
def test_create_endstates():
    """
    test the creation of unsampled endstates
    """
    fe_setup = RelativeFEPSetup(ligand_input=f"{os.getcwd()}/test.smi",
                                old_ligand_index=0,
                                new_ligand_index=1,
                                forcefield_files=['gaff.xml'],
                                phases=['vacuum'])

    hybrid_factory = HybridTopologyFactory(
        topology_proposal=fe_setup._vacuum_topology_proposal,
        current_positions=fe_setup._vacuum_positions_old,
        new_positions=fe_setup._vacuum_positions_new,
        neglected_new_angle_terms=fe_setup._vacuum_forward_neglected_angles,
        neglected_old_angle_terms=fe_setup._vacuum_reverse_neglected_angles,
        softcore_LJ_v2=True,
        interpolate_old_and_new_14s=False)

    zero_state_error, one_state_error = validate_endstate_energies(
        fe_setup._vacuum_topology_proposal,
        hybrid_factory,
        added_energy=fe_setup._vacuum_added_valence_energy,
        subtracted_energy=fe_setup._vacuum_subtracted_valence_energy,
        beta=beta,
        ENERGY_THRESHOLD=ENERGY_THRESHOLD)

    lambda_alchemical_state = RelativeAlchemicalState.from_system(
        hybrid_factory.hybrid_system)
    lambda_protocol = LambdaProtocol(functions='default')
    lambda_alchemical_state.set_alchemical_parameters(0.0, lambda_protocol)
    thermodynamic_state = CompoundThermodynamicState(
        ThermodynamicState(hybrid_factory.hybrid_system,
                           temperature=temperature),
        composable_states=[lambda_alchemical_state])
    zero_endstate = copy.deepcopy(thermodynamic_state)
    one_endstate = copy.deepcopy(thermodynamic_state)
    one_endstate.set_alchemical_parameters(1.0, lambda_protocol)
    new_endstates = create_endstates(zero_endstate, one_endstate)
Beispiel #10
0
def sMC_setup():
    """
    function to setup local sMC
    """
    from pkg_resources import resource_filename
    smiles_filename = resource_filename("perses",
                                        os.path.join("data", "test.smi"))
    fe_setup = RelativeFEPSetup(ligand_input=smiles_filename,
                                old_ligand_index=0,
                                new_ligand_index=1,
                                forcefield_files=[],
                                small_molecule_forcefield='gaff-2.11',
                                phases=['vacuum'])

    hybrid_factory = HybridTopologyFactory(
        topology_proposal=fe_setup._vacuum_topology_proposal,
        current_positions=fe_setup._vacuum_positions_old,
        new_positions=fe_setup._vacuum_positions_new,
        neglected_new_angle_terms=fe_setup._vacuum_forward_neglected_angles,
        neglected_old_angle_terms=fe_setup._vacuum_reverse_neglected_angles,
        softcore_LJ_v2=True,
        interpolate_old_and_new_14s=False)

    zero_state_error, one_state_error = validate_endstate_energies(
        fe_setup._vacuum_topology_proposal,
        hybrid_factory,
        added_energy=fe_setup._vacuum_added_valence_energy,
        subtracted_energy=fe_setup._vacuum_subtracted_valence_energy,
        beta=beta,
        platform=openmm.Platform.getPlatformByName('Reference'),
        ENERGY_THRESHOLD=ENERGY_THRESHOLD)
    ne_fep = SequentialMonteCarlo(factory=hybrid_factory,
                                  lambda_protocol=lambda_protocol,
                                  temperature=temperature,
                                  trajectory_directory=trajectory_directory,
                                  trajectory_prefix=trajectory_prefix,
                                  atom_selection=atom_selection,
                                  timestep=timestep,
                                  eq_splitting_string=eq_splitting_string,
                                  neq_splitting_string=neq_splitting_string,
                                  collision_rate=collision_rate,
                                  ncmc_save_interval=ncmc_save_interval,
                                  external_parallelism=None,
                                  internal_parallelism=None)

    assert ne_fep.external_parallelism is False and ne_fep.internal_parallelism is True, f"all parallelism should be None"
    assert ne_fep.workers == 0, f"local annealing definition only allows 0 workers"
    assert ne_fep.parallelism_parameters == {
        'library': None,
        'num_processes': None
    }, f"the parallelism_parameters are not supported"

    #get reduced energies
    ne_fep.minimize_sampler_states()
    ne_fep.equilibrate(n_equilibration_iterations=10,
                       n_steps_per_equilibration=1,
                       endstates=[0, 1],
                       decorrelate=True,
                       timer=True,
                       minimize=False)
    assert all(
        sum([
            ne_fep._eq_dict[state][i][-1]
            for i in range(len(ne_fep._eq_dict[state]))
        ]) == 10
        for state in [0, 1]), f"there should be 10 snapshots per endstate"
    assert all(
        len(ne_fep._eq_dict[f"{state}_reduced_potentials"]) == 10 for state in
        [0, 1]), f"there should be 10 reduced potentials per endstate"
    assert all(
        len(ne_fep._eq_dict[f"{state}_decorrelated"]) <= 10
        for state in [0, 1]
    ), f"the decorrelated indices must be less than or equal to the total number of snapshots"

    #now to check for decorrelation in ne_fep._eq_files_dict...
    _filenames_0, _filenames_1 = [
        ne_fep._eq_dict[0][i][0] for i in range(len(ne_fep._eq_dict[0]))
    ], [ne_fep._eq_dict[1][i][0] for i in range(len(ne_fep._eq_dict[1]))]
    decorrelated_0 = [
        item for sublist in
        [ne_fep._eq_files_dict[0][filename] for filename in _filenames_0]
        for item in sublist
    ]
    decorrelated_1 = [
        item for sublist in
        [ne_fep._eq_files_dict[1][filename] for filename in _filenames_1]
        for item in sublist
    ]
    decorrelated_0_files = [
        subitem for ssublist in [
            item for sublist in [list(ne_fep._eq_files_dict[0].values())]
            for item in sublist
        ] for subitem in ssublist
    ]
    decorrelated_1_files = [
        subitem for ssublist in [
            item for sublist in [list(ne_fep._eq_files_dict[1].values())]
            for item in sublist
        ] for subitem in ssublist
    ]
    assert decorrelated_0 == sorted(
        decorrelated_0_files
    ), f"there is a discrepancy between the decorrelated 0 equilibrium states and the decorrelated equilibria saved to disk"
    assert decorrelated_1 == sorted(
        decorrelated_1_files
    ), f"there is a discrepancy between the decorrelated 1 equilibrium states and the decorrelated equilibria saved to disk"
    return ne_fep
Beispiel #11
0
def generate_top_pos_sys(topology, new_res, system, positions,
                         system_generator):
    """generate point mutation engine, geometry_engine, and conduct topology proposal, geometry propsal, and hybrid factory generation"""
    #create the point mutation engine
    print(f"generating point mutation engine")
    point_mutation_engine = PointMutationEngine(
        wildtype_topology=topology,
        system_generator=system_generator,
        chain_id=
        '1',  #denote the chain id allowed to mutate (it's always a string variable)
        max_point_mutants=1,
        residues_allowed_to_mutate=['2'],  #the residue ids allowed to mutate
        allowed_mutations=[
            ('2', new_res)
        ],  #the residue ids allowed to mutate with the three-letter code allowed to change
        aggregate=True)  #always allow aggregation

    #create a geometry engine
    print(f"generating geometry engine")
    geometry_engine = FFAllAngleGeometryEngine(metadata=None,
                                               use_sterics=False,
                                               n_bond_divisions=100,
                                               n_angle_divisions=180,
                                               n_torsion_divisions=360,
                                               verbose=True,
                                               storage=None,
                                               bond_softening_constant=1.0,
                                               angle_softening_constant=1.0,
                                               neglect_angles=False,
                                               use_14_nonbondeds=False)

    #create a top proposal
    print(f"making topology proposal")
    topology_proposal, local_map_stereo_sidechain, new_oemol_sidechain, old_oemol_sidechain = point_mutation_engine.propose(
        current_system=system, current_topology=topology)

    #make a geometry proposal forward
    print(f"making geometry proposal")
    forward_new_positions, logp_proposal = geometry_engine.propose(
        topology_proposal, positions, beta)

    #create a hybrid topology factory
    f"making forward hybridtopologyfactory"
    forward_htf = HybridTopologyFactory(topology_proposal=topology_proposal,
                                        current_positions=positions,
                                        new_positions=forward_new_positions,
                                        use_dispersion_correction=False,
                                        functions=None,
                                        softcore_alpha=None,
                                        bond_softening_constant=1.0,
                                        angle_softening_constant=1.0,
                                        soften_only_new=False,
                                        neglected_new_angle_terms=[],
                                        neglected_old_angle_terms=[],
                                        softcore_LJ_v2=True,
                                        softcore_electrostatics=True,
                                        softcore_LJ_v2_alpha=0.85,
                                        softcore_electrostatics_alpha=0.3,
                                        softcore_sigma_Q=1.0,
                                        interpolate_old_and_new_14s=False,
                                        omitted_terms=None)

    return topology_proposal, forward_new_positions, forward_htf, local_map_stereo_sidechain, old_oemol_sidechain, new_oemol_sidechain
Beispiel #12
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)
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)
    hybrid_factory = HybridTopologyFactory(
        topology_proposal,
        current_positions,
        new_positions,
        use_dispersion_correction=False)  # DEBUG

    #get the relevant thermodynamic states:
    nonalchemical_zero_thermodynamic_state, nonalchemical_one_thermodynamic_state, lambda_zero_thermodynamic_state, lambda_one_thermodynamic_state = utils.generate_endpoint_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 (but don't restart if we encounter a NaN)
    mc_move = mcmc.LangevinDynamicsMove(n_restart_attempts=0, n_steps=100)

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

    hybrid_endpoint_results = []
    all_results = []
    for lambda_state in (0, 1):
        result, non, hybrid = 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)
        all_results.append(non)
        all_results.append(hybrid)
        print('lambda {} : {}'.format(lambda_state, result))

        hybrid_endpoint_results.append(result)
    calculate_cross_variance(all_results)
    return hybrid_endpoint_results
Beispiel #14
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
    -------

    """
    #TODO this is a test
    #this code is out of date

    #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)
    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...")
    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))
    def __init__(
            self,
            receptor_filename,
            ligand_filename,
            mutation_chain_id,
            mutation_residue_id,
            proposed_residue,
            phase='complex',
            conduct_endstate_validation=False,
            ligand_index=0,
            forcefield_files=[
                'amber14/protein.ff14SB.xml', 'amber14/tip3p.xml'
            ],
            barostat=openmm.MonteCarloBarostat(1.0 * unit.atmosphere,
                                               temperature, 50),
            forcefield_kwargs={
                'removeCMMotion': False,
                'ewaldErrorTolerance': 1e-4,
                'nonbondedMethod': app.PME,
                'constraints': app.HBonds,
                'hydrogenMass': 4 * unit.amus
            },
            small_molecule_forcefields='gaff-2.11',
            **kwargs):
        """
        arguments
            receptor_filename : str
                path to receptor; .pdb
            ligand_filename : str
                path to ligand of interest; .sdf or .pdb
            mutation_chain_id : str
                name of the chain to be mutated
            mutation_residue_id : str
                residue id to change
            proposed_residue : str
                three letter code of the residue to mutate to
            phase : str, default complex
                if phase == vacuum, then the complex will not be solvated with water; else, it will be solvated with tip3p
            conduct_endstate_validation : bool, default True
                whether to conduct an endstate validation of the hybrid topology factory
            ligand_index : int, default 0
                which ligand to use
            forcefield_files : list of str, default ['amber14/protein.ff14SB.xml', 'amber14/tip3p.xml']
                forcefield files for proteins and solvent
            barostat : openmm.MonteCarloBarostat, default openmm.MonteCarloBarostat(1.0 * unit.atmosphere, 300 * unit.kelvin, 50)
                barostat to use
            forcefield_kwargs : dict, default {'removeCMMotion': False, 'ewaldErrorTolerance': 1e-4, 'nonbondedMethod': app.NoCutoff, 'constraints' : app.HBonds, 'hydrogenMass' : 4 * unit.amus}
                forcefield kwargs for system parametrization
            small_molecule_forcefields : str, default 'gaff-2.11'
                the forcefield string for small molecule parametrization

        TODO : allow argument for separate apo structure if it exists separately
               allow argument for specator ligands besides the 'ligand_filename'
        """
        from openforcefield.topology import Molecule
        from openmmforcefields.generators import SystemGenerator

        # first thing to do is make a complex and apo...
        pdbfile = open(receptor_filename, 'r')
        pdb = app.PDBFile(pdbfile)
        pdbfile.close()
        receptor_positions, receptor_topology, receptor_md_topology = pdb.positions, pdb.topology, md.Topology.from_openmm(
            pdb.topology)
        receptor_topology = receptor_md_topology.to_openmm()
        receptor_n_atoms = receptor_md_topology.n_atoms

        molecules = []
        ligand_mol = createOEMolFromSDF(ligand_filename, index=ligand_index)
        ligand_mol = generate_unique_atom_names(ligand_mol)
        molecules.append(
            Molecule.from_openeye(ligand_mol, allow_undefined_stereo=False))
        ligand_positions, ligand_topology = extractPositionsFromOEMol(
            ligand_mol), forcefield_generators.generateTopologyFromOEMol(
                ligand_mol)
        ligand_md_topology = md.Topology.from_openmm(ligand_topology)
        ligand_n_atoms = ligand_md_topology.n_atoms

        #now create a complex
        complex_md_topology = receptor_md_topology.join(ligand_md_topology)
        complex_topology = complex_md_topology.to_openmm()
        complex_positions = unit.Quantity(np.zeros(
            [receptor_n_atoms + ligand_n_atoms, 3]),
                                          unit=unit.nanometers)
        complex_positions[:receptor_n_atoms, :] = receptor_positions
        complex_positions[receptor_n_atoms:, :] = ligand_positions

        #now for a system_generator
        self.system_generator = SystemGenerator(
            forcefields=forcefield_files,
            barostat=barostat,
            forcefield_kwargs=forcefield_kwargs,
            small_molecule_forcefield=small_molecule_forcefields,
            molecules=molecules,
            cache=None)

        #create complex and apo inputs...
        complex_topology, complex_positions, complex_system = self._solvate(
            complex_topology, complex_positions, 'tip3p', phase=phase)
        apo_topology, apo_positions, apo_system = self._solvate(
            receptor_topology, receptor_positions, 'tip3p', phase='phase')

        geometry_engine = FFAllAngleGeometryEngine(
            metadata=None,
            use_sterics=False,
            n_bond_divisions=100,
            n_angle_divisions=180,
            n_torsion_divisions=360,
            verbose=True,
            storage=None,
            bond_softening_constant=1.0,
            angle_softening_constant=1.0,
            neglect_angles=False,
            use_14_nonbondeds=True)

        #run pipeline...
        htfs = []
        for (top, pos, sys) in zip([complex_topology, apo_topology],
                                   [complex_positions, apo_positions],
                                   [complex_system, apo_system]):
            point_mutation_engine = PointMutationEngine(
                wildtype_topology=top,
                system_generator=self.system_generator,
                chain_id=
                mutation_chain_id,  #denote the chain id allowed to mutate (it's always a string variable)
                max_point_mutants=1,
                residues_allowed_to_mutate=[
                    mutation_residue_id
                ],  #the residue ids allowed to mutate
                allowed_mutations=[
                    (mutation_residue_id, proposed_residue)
                ],  #the residue ids allowed to mutate with the three-letter code allowed to change
                aggregate=True)  #always allow aggregation

            topology_proposal = point_mutation_engine.propose(sys, top)

            new_positions, logp_proposal = geometry_engine.propose(
                topology_proposal, pos, beta)
            logp_reverse = geometry_engine.logp_reverse(
                topology_proposal, new_positions, pos, beta)

            forward_htf = HybridTopologyFactory(
                topology_proposal=topology_proposal,
                current_positions=pos,
                new_positions=new_positions,
                use_dispersion_correction=False,
                functions=None,
                softcore_alpha=None,
                bond_softening_constant=1.0,
                angle_softening_constant=1.0,
                soften_only_new=False,
                neglected_new_angle_terms=[],
                neglected_old_angle_terms=[],
                softcore_LJ_v2=True,
                softcore_electrostatics=True,
                softcore_LJ_v2_alpha=0.85,
                softcore_electrostatics_alpha=0.3,
                softcore_sigma_Q=1.0,
                interpolate_old_and_new_14s=False,
                omitted_terms=None)

            if not topology_proposal.unique_new_atoms:
                assert geometry_engine.forward_final_context_reduced_potential == None, f"There are no unique new atoms but the geometry_engine's final context reduced potential is not None (i.e. {self._geometry_engine.forward_final_context_reduced_potential})"
                assert geometry_engine.forward_atoms_with_positions_reduced_potential == None, f"There are no unique new atoms but the geometry_engine's forward atoms-with-positions-reduced-potential in not None (i.e. { self._geometry_engine.forward_atoms_with_positions_reduced_potential})"
                vacuum_added_valence_energy = 0.0
            else:
                added_valence_energy = geometry_engine.forward_final_context_reduced_potential - geometry_engine.forward_atoms_with_positions_reduced_potential

            if not topology_proposal.unique_old_atoms:
                assert geometry_engine.reverse_final_context_reduced_potential == None, f"There are no unique old atoms but the geometry_engine's final context reduced potential is not None (i.e. {self._geometry_engine.reverse_final_context_reduced_potential})"
                assert geometry_engine.reverse_atoms_with_positions_reduced_potential == None, f"There are no unique old atoms but the geometry_engine's atoms-with-positions-reduced-potential in not None (i.e. { self._geometry_engine.reverse_atoms_with_positions_reduced_potential})"
                subtracted_valence_energy = 0.0
            else:
                subtracted_valence_energy = geometry_engine.reverse_final_context_reduced_potential - geometry_engine.reverse_atoms_with_positions_reduced_potential

            if conduct_endstate_validation:
                zero_state_error, one_state_error = validate_endstate_energies(
                    forward_htf._topology_proposal,
                    forward_htf,
                    added_valence_energy,
                    subtracted_valence_energy,
                    beta=beta,
                    ENERGY_THRESHOLD=ENERGY_THRESHOLD)
            else:
                pass

            htfs.append(forward_htf)

        self.complex_htf = htfs[0]
        self.apo_htf = htfs[1]
Beispiel #16
0
def generate_dipeptide_top_pos_sys(topology,
                                   new_res,
                                   system,
                                   positions,
                                   system_generator,
                                   conduct_geometry_prop=True,
                                   conduct_htf_prop=False):
    """generate point mutation engine, geometry_engine, and conduct topology proposal, geometry propsal, and hybrid factory generation"""
    from perses.tests.utils import validate_endstate_energies
    if conduct_htf_prop:
        assert conduct_geometry_prop, f"the htf prop can only be conducted if there is a geometry proposal"
    #create the point mutation engine
    from perses.rjmc.topology_proposal import PointMutationEngine
    point_mutation_engine = PointMutationEngine(
        wildtype_topology=topology,
        system_generator=system_generator,
        chain_id=
        '1',  #denote the chain id allowed to mutate (it's always a string variable)
        max_point_mutants=1,
        residues_allowed_to_mutate=['2'],  #the residue ids allowed to mutate
        allowed_mutations=[
            ('2', new_res)
        ],  #the residue ids allowed to mutate with the three-letter code allowed to change
        aggregate=True)  #always allow aggregation

    #create a top proposal
    print(f"making topology proposal")
    topology_proposal = point_mutation_engine.propose(
        current_system=system, current_topology=topology)

    if not conduct_geometry_prop:
        return topology_proposal

    if conduct_geometry_prop:
        #create a geometry engine
        print(f"generating geometry engine")
        from perses.rjmc.geometry import FFAllAngleGeometryEngine
        geometry_engine = FFAllAngleGeometryEngine(
            metadata=None,
            use_sterics=False,
            n_bond_divisions=100,
            n_angle_divisions=180,
            n_torsion_divisions=360,
            verbose=True,
            storage=None,
            bond_softening_constant=1.0,
            angle_softening_constant=1.0,
            neglect_angles=False,
            use_14_nonbondeds=True)

        #make a geometry proposal forward
        print(
            f"making geometry proposal from {list(topology.residues())[1].name} to {new_res}"
        )
        forward_new_positions, logp_proposal = geometry_engine.propose(
            topology_proposal, positions, beta)
        logp_reverse = geometry_engine.logp_reverse(topology_proposal,
                                                    forward_new_positions,
                                                    positions, beta)

    if not conduct_htf_prop:
        return (topology_proposal, forward_new_positions, logp_proposal,
                logp_reverse)

    if conduct_htf_prop:
        #create a hybrid topology factory
        from perses.annihilation.relative import HybridTopologyFactory
        forward_htf = HybridTopologyFactory(
            topology_proposal=topology_proposal,
            current_positions=positions,
            new_positions=forward_new_positions,
            use_dispersion_correction=False,
            functions=None,
            softcore_alpha=None,
            bond_softening_constant=1.0,
            angle_softening_constant=1.0,
            soften_only_new=False,
            neglected_new_angle_terms=[],
            neglected_old_angle_terms=[],
            softcore_LJ_v2=True,
            softcore_electrostatics=True,
            softcore_LJ_v2_alpha=0.85,
            softcore_electrostatics_alpha=0.3,
            softcore_sigma_Q=1.0,
            interpolate_old_and_new_14s=False,
            omitted_terms=None)

        if not topology_proposal.unique_new_atoms:
            assert geometry_engine.forward_final_context_reduced_potential == None, f"There are no unique new atoms but the geometry_engine's final context reduced potential is not None (i.e. {self._geometry_engine.forward_final_context_reduced_potential})"
            assert geometry_engine.forward_atoms_with_positions_reduced_potential == None, f"There are no unique new atoms but the geometry_engine's forward atoms-with-positions-reduced-potential in not None (i.e. { self._geometry_engine.forward_atoms_with_positions_reduced_potential})"
            vacuum_added_valence_energy = 0.0
        else:
            added_valence_energy = geometry_engine.forward_final_context_reduced_potential - geometry_engine.forward_atoms_with_positions_reduced_potential

        if not topology_proposal.unique_old_atoms:
            assert geometry_engine.reverse_final_context_reduced_potential == None, f"There are no unique old atoms but the geometry_engine's final context reduced potential is not None (i.e. {self._geometry_engine.reverse_final_context_reduced_potential})"
            assert geometry_engine.reverse_atoms_with_positions_reduced_potential == None, f"There are no unique old atoms but the geometry_engine's atoms-with-positions-reduced-potential in not None (i.e. { self._geometry_engine.reverse_atoms_with_positions_reduced_potential})"
            subtracted_valence_energy = 0.0
        else:
            subtracted_valence_energy = geometry_engine.reverse_final_context_reduced_potential - geometry_engine.reverse_atoms_with_positions_reduced_potential

        zero_state_error, one_state_error = validate_endstate_energies(
            forward_htf._topology_proposal,
            forward_htf,
            added_valence_energy,
            subtracted_valence_energy,
            beta=1.0 / (kB * temperature),
            ENERGY_THRESHOLD=ENERGY_THRESHOLD,
            platform=openmm.Platform.getPlatformByName('Reference'))
        print(f"zero state error : {zero_state_error}")
        print(f"one state error : {one_state_error}")

        return forward_htf
def HybridTopologyFactory_energies(
        current_mol='toluene',
        proposed_mol='1,2-bis(trifluoromethyl) benzene'):
    """
    Test whether the difference in the nonalchemical zero and alchemical zero states is the forward valence energy.  Also test for the one states.
    """
    from perses.tests.utils import generate_solvated_hybrid_test_topology, generate_endpoint_thermodynamic_states
    import openmmtools.cache as cache

    #Just test the solvated system
    top_proposal, old_positions, _ = generate_solvated_hybrid_test_topology(
        current_mol_name=current_mol, proposed_mol_name=proposed_mol)

    #remove the dispersion correction
    top_proposal._old_system.getForce(3).setUseDispersionCorrection(False)
    top_proposal._new_system.getForce(3).setUseDispersionCorrection(False)

    # run geometry engine to generate old and new positions
    _geometry_engine = FFAllAngleGeometryEngine(metadata=None,
                                                use_sterics=False,
                                                n_bond_divisions=100,
                                                n_angle_divisions=180,
                                                n_torsion_divisions=360,
                                                verbose=True,
                                                storage=None,
                                                bond_softening_constant=1.0,
                                                angle_softening_constant=1.0,
                                                neglect_angles=False)
    _new_positions, _lp = _geometry_engine.propose(top_proposal, old_positions,
                                                   beta)
    _lp_rev = _geometry_engine.logp_reverse(top_proposal, _new_positions,
                                            old_positions, beta)

    # make the hybrid system, reset the CustomNonbondedForce cutoff
    HTF = HybridTopologyFactory(top_proposal, old_positions, _new_positions)
    hybrid_system = HTF.hybrid_system
    nonalch_zero, nonalch_one, alch_zero, alch_one = generate_endpoint_thermodynamic_states(
        hybrid_system, top_proposal)

    # compute reduced energies
    #for the nonalchemical systems...
    attrib_list = [(nonalch_zero, old_positions,
                    top_proposal._old_system.getDefaultPeriodicBoxVectors()),
                   (alch_zero, HTF._hybrid_positions,
                    hybrid_system.getDefaultPeriodicBoxVectors()),
                   (alch_one, HTF._hybrid_positions,
                    hybrid_system.getDefaultPeriodicBoxVectors()),
                   (nonalch_one, _new_positions,
                    top_proposal._new_system.getDefaultPeriodicBoxVectors())]

    rp_list = []
    for (state, pos, box_vectors) in attrib_list:
        context, integrator = cache.global_context_cache.get_context(state)
        samplerstate = SamplerState(positions=pos, box_vectors=box_vectors)
        samplerstate.apply_to_context(context)
        rp = state.reduced_potential(context)
        rp_list.append(rp)

    #valence energy definitions
    forward_added_valence_energy = _geometry_engine.forward_final_context_reduced_potential - _geometry_engine.forward_atoms_with_positions_reduced_potential
    reverse_subtracted_valence_energy = _geometry_engine.reverse_final_context_reduced_potential - _geometry_engine.reverse_atoms_with_positions_reduced_potential

    nonalch_zero_rp, alch_zero_rp, alch_one_rp, nonalch_one_rp = rp_list[
        0], rp_list[1], rp_list[2], rp_list[3]
    # print(f"Difference between zeros: {nonalch_zero_rp - alch_zero_rp}; forward added: {forward_added_valence_energy}")
    # print(f"Difference between ones: {nonalch_zero_rp - alch_zero_rp}; forward added: {forward_added_valence_energy}")

    assert abs(
        nonalch_zero_rp - alch_zero_rp + forward_added_valence_energy
    ) < ENERGY_THRESHOLD, f"The zero state alchemical and nonalchemical energy absolute difference {abs(nonalch_zero_rp - alch_zero_rp + forward_added_valence_energy)} is greater than the threshold of {ENERGY_THRESHOLD}."
    assert abs(
        nonalch_one_rp - alch_one_rp + reverse_subtracted_valence_energy
    ) < ENERGY_THRESHOLD, f"The one state alchemical and nonalchemical energy absolute difference {abs(nonalch_one_rp - alch_one_rp + reverse_subtracted_valence_energy)} is greater than the threshold of {ENERGY_THRESHOLD}."

    print(
        f"Abs difference in zero alchemical vs nonalchemical systems: {abs(nonalch_zero_rp - alch_zero_rp + forward_added_valence_energy)}"
    )
    print(
        f"Abs difference in one alchemical vs nonalchemical systems: {abs(nonalch_one_rp - alch_one_rp + reverse_subtracted_valence_energy)}"
    )
Beispiel #18
0
def compute_nonalchemical_perturbation(
        alchemical_thermodynamic_state: states.ThermodynamicState,
        growth_thermodynamic_state: states.ThermodynamicState,
        hybrid_sampler_state: states.SamplerState,
        hybrid_factory: HybridTopologyFactory,
        nonalchemical_thermodynamic_state: states.ThermodynamicState,
        lambda_state: int) -> tuple:
    """
    Compute the perturbation of transforming the given hybrid equilibrium result into the system for the given nonalchemical_thermodynamic_state

    Parameters
    ----------
    alchemical_thermodynamic_state: states.ThermodynamicState
        alchemical thermostate
    growth_thermodynamic_state : states.ThermodynamicState
    hybrid_sampler_state: states.SamplerState
        sampler state for the alchemical thermodynamic_state
    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
    -------
    valence_energy: float
        reduced potential energy of the valence contribution of the alternate endstate
    nonalchemical_reduced_potential : float
        reduced potential energy of the nonalchemical endstate
    hybrid_reduced_potential: float
        reduced potential energy of the alchemical endstate
    """
    #get the objects we need to begin
    hybrid_reduced_potential = compute_reduced_potential(
        alchemical_thermodynamic_state, hybrid_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)
        nonalchemical_alternate_positions = hybrid_factory.new_positions(
            hybrid_positions)
    elif lambda_state == 1:
        nonalchemical_positions = hybrid_factory.new_positions(
            hybrid_positions)
        nonalchemical_alternate_positions = hybrid_factory.old_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_alternate_sampler_state = states.SamplerState(
        nonalchemical_alternate_positions,
        box_vectors=hybrid_sampler_state.box_vectors)

    nonalchemical_reduced_potential = compute_reduced_potential(
        nonalchemical_thermodynamic_state, nonalchemical_sampler_state)

    #now for the growth system (set at lambda 0 or 1) so we can get the valence energy
    if growth_thermodynamic_state:
        valence_energy = compute_reduced_potential(
            growth_thermodynamic_state, nonalchemical_alternate_sampler_state)
    else:
        valence_energy = 0.0

    #now, the corrected energy of the system (for dispersion correction) is the nonalchemical_reduced_potential + valence_energy
    return (valence_energy, nonalchemical_reduced_potential,
            hybrid_reduced_potential)
Beispiel #19
0
def compare_energies(mol_name="naphthalene",
                     ref_mol_name="benzene",
                     atom_expression=['Hybridization'],
                     bond_expression=['Hybridization']):
    """
    Make an atom map where the molecule at either lambda endpoint is identical, and check that the energies are also the same.
    """
    from openmmtools.constants import kB
    from openmmtools import alchemy, states
    from perses.rjmc.topology_proposal import SmallMoleculeSetProposalEngine
    from perses.annihilation.relative import HybridTopologyFactory
    from perses.rjmc.geometry import FFAllAngleGeometryEngine
    import simtk.openmm as openmm
    from perses.utils.openeye import iupac_to_oemol, extractPositionsFromOEMol, generate_conformers
    from perses.utils.openeye import generate_expression
    from openmmforcefields.generators import SystemGenerator
    from openmoltools.forcefield_generators import generateTopologyFromOEMol
    from perses.tests.utils import validate_endstate_energies
    temperature = 300 * unit.kelvin
    # Compute kT and inverse temperature.
    kT = kB * temperature
    beta = 1.0 / kT
    ENERGY_THRESHOLD = 1e-6

    atom_expr, bond_expr = generate_expression(
        atom_expression), generate_expression(bond_expression)

    mol = iupac_to_oemol(mol_name)
    mol = generate_conformers(mol, max_confs=1)

    refmol = iupac_to_oemol(ref_mol_name)
    refmol = generate_conformers(refmol, max_confs=1)

    from openforcefield.topology import Molecule
    molecules = [Molecule.from_openeye(oemol) for oemol in [refmol, mol]]
    barostat = None
    forcefield_files = ['amber14/protein.ff14SB.xml', 'amber14/tip3p.xml']
    forcefield_kwargs = {
        'removeCMMotion': False,
        'ewaldErrorTolerance': 1e-4,
        'nonbondedMethod': app.NoCutoff,
        'constraints': app.HBonds,
        'hydrogenMass': 4 * unit.amus
    }

    system_generator = SystemGenerator(forcefields=forcefield_files,
                                       barostat=barostat,
                                       forcefield_kwargs=forcefield_kwargs,
                                       small_molecule_forcefield='gaff-2.11',
                                       molecules=molecules,
                                       cache=None)

    topology = generateTopologyFromOEMol(refmol)
    system = system_generator.create_system(topology)
    positions = extractPositionsFromOEMol(refmol)

    proposal_engine = SmallMoleculeSetProposalEngine([refmol, mol],
                                                     system_generator)
    proposal = proposal_engine.propose(system,
                                       topology,
                                       atom_expr=atom_expr,
                                       bond_expr=bond_expr)
    geometry_engine = FFAllAngleGeometryEngine()
    new_positions, _ = geometry_engine.propose(
        proposal, positions, beta=beta, validate_energy_bookkeeping=False)
    _ = geometry_engine.logp_reverse(proposal, new_positions, positions, beta)
    #make a topology proposal with the appropriate data:

    factory = HybridTopologyFactory(proposal, positions, new_positions)
    if not proposal.unique_new_atoms:
        assert geometry_engine.forward_final_context_reduced_potential == None, f"There are no unique new atoms but the geometry_engine's final context reduced potential is not None (i.e. {self._geometry_engine.forward_final_context_reduced_potential})"
        assert geometry_engine.forward_atoms_with_positions_reduced_potential == None, f"There are no unique new atoms but the geometry_engine's forward atoms-with-positions-reduced-potential in not None (i.e. { self._geometry_engine.forward_atoms_with_positions_reduced_potential})"
        vacuum_added_valence_energy = 0.0
    else:
        added_valence_energy = geometry_engine.forward_final_context_reduced_potential - geometry_engine.forward_atoms_with_positions_reduced_potential

    if not proposal.unique_old_atoms:
        assert geometry_engine.reverse_final_context_reduced_potential == None, f"There are no unique old atoms but the geometry_engine's final context reduced potential is not None (i.e. {self._geometry_engine.reverse_final_context_reduced_potential})"
        assert geometry_engine.reverse_atoms_with_positions_reduced_potential == None, f"There are no unique old atoms but the geometry_engine's atoms-with-positions-reduced-potential in not None (i.e. { self._geometry_engine.reverse_atoms_with_positions_reduced_potential})"
        subtracted_valence_energy = 0.0
    else:
        subtracted_valence_energy = geometry_engine.reverse_final_context_reduced_potential - geometry_engine.reverse_atoms_with_positions_reduced_potential

    zero_state_error, one_state_error = validate_endstate_energies(
        factory._topology_proposal,
        factory,
        added_valence_energy,
        subtracted_valence_energy,
        beta=1.0 / (kB * temperature),
        ENERGY_THRESHOLD=ENERGY_THRESHOLD,
        platform=openmm.Platform.getPlatformByName('Reference'))
    return factory
def run_setup(setup_options, serialize_systems=True, build_samplers=True):
    """
    Run the setup pipeline and return the relevant setup objects based on a yaml input file.
    Parameters
    ----------
    setup_options : dict
        result of loading yaml input file
    Returns
    -------
    setup_dict: dict
        {'topology_proposals': top_prop, 'hybrid_topology_factories': htf, 'hybrid_samplers': hss}
        - 'topology_proposals':
    """
    phases = setup_options['phases']
    known_phases = ['complex', 'solvent', 'vacuum']
    for phase in phases:
        assert (
            phase in known_phases
        ), f"Unknown phase, {phase} provided. run_setup() can be used with {known_phases}"

    if 'use_given_geometries' not in list(setup_options.keys()):
        use_given_geometries = False
    else:
        assert type(setup_options['use_given_geometries']) == type(True)
        use_given_geometries = setup_options['use_given_geometries']

    if 'complex' in phases:
        _logger.info(f"\tPulling receptor (as pdb or mol2)...")
        # We'll need the protein PDB file (without missing atoms)
        try:
            protein_pdb_filename = setup_options['protein_pdb']
            assert protein_pdb_filename is not None
            receptor_mol2 = None
        except KeyError:
            try:
                receptor_mol2 = setup_options['receptor_mol2']
                assert receptor_mol2 is not None
                protein_pdb_filename = None
            except KeyError as e:
                print(
                    "Either protein_pdb or receptor_mol2 must be specified if running a complex simulation"
                )
                raise e
    else:
        protein_pdb_filename = None
        receptor_mol2 = None

    # And a ligand file containing the pair of ligands between which we will transform
    ligand_file = setup_options['ligand_file']
    _logger.info(f"\tdetected ligand file: {ligand_file}")

    # get the indices of ligands out of the file:
    old_ligand_index = setup_options['old_ligand_index']
    new_ligand_index = setup_options['new_ligand_index']
    _logger.info(
        f"\told ligand index: {old_ligand_index}; new ligand index: {new_ligand_index}"
    )

    _logger.info(f"\tsetting up forcefield files...")
    forcefield_files = setup_options['forcefield_files']

    if "timestep" in setup_options:
        if isinstance(setup_options['timestep'], float):
            timestep = setup_options['timestep'] * unit.femtoseconds
        else:
            timestep = setup_options['timestep']
        _logger.info(f"\ttimestep: {timestep}.")
    else:
        timestep = 1.0 * unit.femtoseconds
        _logger.info(f"\tno timestep detected: setting default as 1.0fs.")

    if "neq_splitting" in setup_options:
        neq_splitting = setup_options['neq_splitting']
        _logger.info(f"\tneq_splitting: {neq_splitting}")

        try:
            eq_splitting = setup_options['eq_splitting']
            _logger.info(f"\teq_splitting: {eq_splitting}")
        except KeyError as e:
            print(
                "If you specify a nonequilibrium splitting string, you must also specify an equilibrium one."
            )
            raise e

    else:
        eq_splitting = "V R O R V"
        neq_splitting = "V R O R V"
        _logger.info(
            f"\tno splitting strings specified: defaulting to neq: {neq_splitting}, eq: {eq_splitting}."
        )

    if "measure_shadow_work" in setup_options:
        measure_shadow_work = setup_options['measure_shadow_work']
        _logger.info(f"\tmeasuring shadow work: {measure_shadow_work}.")
    else:
        measure_shadow_work = False
        _logger.info(
            f"\tno measure_shadow_work specified: defaulting to False.")
    if isinstance(setup_options['pressure'], float):
        pressure = setup_options['pressure'] * unit.atmosphere
    else:
        pressure = setup_options['pressure']
    if isinstance(setup_options['temperature'], float):
        temperature = setup_options['temperature'] * unit.kelvin
    else:
        temperature = setup_options['temperature']
    if isinstance(setup_options['solvent_padding'], float):
        solvent_padding_angstroms = setup_options[
            'solvent_padding'] * unit.angstrom
    else:
        solvent_padding_angstroms = setup_options['solvent_padding']
    if isinstance(setup_options['ionic_strength'], float):
        ionic_strength = setup_options['ionic_strength'] * unit.molar
    else:
        ionic_strength = setup_options['ionic_strength']
    _logger.info(f"\tsetting pressure: {pressure}.")
    _logger.info(f"\tsetting temperature: {temperature}.")
    _logger.info(f"\tsetting solvent padding: {solvent_padding_angstroms}A.")
    _logger.info(f"\tsetting ionic strength: {ionic_strength}M.")

    setup_pickle_file = setup_options[
        'save_setup_pickle_as'] if 'save_setup_pickle_as' in list(
            setup_options) else None
    _logger.info(f"\tsetup pickle file: {setup_pickle_file}")
    trajectory_directory = setup_options['trajectory_directory']
    _logger.info(f"\ttrajectory directory: {trajectory_directory}")
    try:
        atom_map_file = setup_options['atom_map']
        with open(atom_map_file, 'r') as f:
            atom_map = {
                int(x.split()[0]): int(x.split()[1])
                for x in f.readlines()
            }
        _logger.info(f"\tsucceeded parsing atom map.")
    except Exception:
        atom_map = None
        _logger.info(f"\tno atom map specified: default to None.")

    if 'topology_proposal' not in list(setup_options.keys(
    )) or setup_options['topology_proposal'] is None:
        _logger.info(
            f"\tno topology_proposal specified; proceeding to RelativeFEPSetup...\n\n\n"
        )
        if 'set_solvent_box_dims_to_complex' in list(setup_options.keys(
        )) and setup_options['set_solvent_box_dims_to_complex']:
            set_solvent_box_dims_to_complex = True
        else:
            set_solvent_box_dims_to_complex = False

        _logger.info(
            f'Box dimensions: {setup_options["complex_box_dimensions"]} and {setup_options["solvent_box_dimensions"]}'
        )
        fe_setup = RelativeFEPSetup(
            ligand_file,
            old_ligand_index,
            new_ligand_index,
            forcefield_files,
            phases=phases,
            protein_pdb_filename=protein_pdb_filename,
            receptor_mol2_filename=receptor_mol2,
            pressure=pressure,
            temperature=temperature,
            solvent_padding=solvent_padding_angstroms,
            spectator_filenames=setup_options['spectators'],
            map_strength=setup_options['map_strength'],
            atom_expr=setup_options['atom_expr'],
            bond_expr=setup_options['bond_expr'],
            atom_map=atom_map,
            neglect_angles=setup_options['neglect_angles'],
            anneal_14s=setup_options['anneal_1,4s'],
            small_molecule_forcefield=setup_options[
                'small_molecule_forcefield'],
            small_molecule_parameters_cache=setup_options[
                'small_molecule_parameters_cache'],
            trajectory_directory=trajectory_directory,
            trajectory_prefix=setup_options['trajectory_prefix'],
            nonbonded_method=setup_options['nonbonded_method'],
            complex_box_dimensions=setup_options['complex_box_dimensions'],
            solvent_box_dimensions=setup_options['solvent_box_dimensions'],
            ionic_strength=ionic_strength,
            remove_constraints=setup_options['remove_constraints'],
            use_given_geometries=use_given_geometries)

        _logger.info(f"\twriting pickle output...")
        if setup_pickle_file is not None:
            with open(
                    os.path.join(os.getcwd(), trajectory_directory,
                                 setup_pickle_file), 'wb') as f:
                try:
                    pickle.dump(fe_setup, f)
                    _logger.info(f"\tsuccessfully dumped pickle.")
                except Exception as e:
                    print(e)
                    print("\tUnable to save setup object as a pickle")

            _logger.info(
                f"\tsetup is complete.  Writing proposals and positions for each phase to top_prop dict..."
            )
        else:
            _logger.info(
                f"\tsetup is complete.  Omitted writing proposals and positions for each phase to top_prop dict..."
            )

        top_prop = dict()
        for phase in phases:
            top_prop[f'{phase}_topology_proposal'] = getattr(
                fe_setup, f'{phase}_topology_proposal')
            top_prop[f'{phase}_geometry_engine'] = getattr(
                fe_setup, f'_{phase}_geometry_engine')
            top_prop[f'{phase}_old_positions'] = getattr(
                fe_setup, f'{phase}_old_positions')
            top_prop[f'{phase}_new_positions'] = getattr(
                fe_setup, f'{phase}_new_positions')
            top_prop[f'{phase}_added_valence_energy'] = getattr(
                fe_setup, f'_{phase}_added_valence_energy')
            top_prop[f'{phase}_subtracted_valence_energy'] = getattr(
                fe_setup, f'_{phase}_subtracted_valence_energy')
            top_prop[f'{phase}_logp_proposal'] = getattr(
                fe_setup, f'_{phase}_logp_proposal')
            top_prop[f'{phase}_logp_reverse'] = getattr(
                fe_setup, f'_{phase}_logp_reverse')
            top_prop[f'{phase}_forward_neglected_angles'] = getattr(
                fe_setup, f'_{phase}_forward_neglected_angles')
            top_prop[f'{phase}_reverse_neglected_angles'] = getattr(
                fe_setup, f'_{phase}_reverse_neglected_angles')

        top_prop['ligand_oemol_old'] = fe_setup._ligand_oemol_old
        top_prop['ligand_oemol_new'] = fe_setup._ligand_oemol_new
        top_prop[
            'non_offset_new_to_old_atom_map'] = fe_setup.non_offset_new_to_old_atom_map
        _logger.info(f"\twriting atom_mapping.png")
        atom_map_outfile = os.path.join(os.getcwd(), trajectory_directory,
                                        'atom_mapping.png')

        if 'render_atom_map' in list(
                setup_options.keys()) and setup_options['render_atom_map']:
            render_atom_mapping(atom_map_outfile, fe_setup._ligand_oemol_old,
                                fe_setup._ligand_oemol_new,
                                fe_setup.non_offset_new_to_old_atom_map)

    else:
        _logger.info(f"\tloading topology proposal from yaml setup options...")
        top_prop = np.load(setup_options['topology_proposal']).item()

    n_steps_per_move_application = setup_options[
        'n_steps_per_move_application']
    _logger.info(
        f"\t steps per move application: {n_steps_per_move_application}")
    trajectory_directory = setup_options['trajectory_directory']

    trajectory_prefix = setup_options['trajectory_prefix']
    _logger.info(f"\ttrajectory prefix: {trajectory_prefix}")

    if 'atom_selection' in setup_options:
        atom_selection = setup_options['atom_selection']
        _logger.info(f"\tatom selection detected: {atom_selection}")
    else:
        _logger.info(f"\tno atom selection detected: default to all.")
        atom_selection = 'all'

    if setup_options['fe_type'] == 'neq':
        _logger.info(f"\tInstantiating nonequilibrium switching FEP")
        n_equilibrium_steps_per_iteration = setup_options[
            'n_equilibrium_steps_per_iteration']
        ncmc_save_interval = setup_options['ncmc_save_interval']
        write_ncmc_configuration = setup_options['write_ncmc_configuration']
        if setup_options['LSF']:
            _internal_parallelism = {
                'library': ('dask', 'LSF'),
                'num_processes': setup_options['processes']
            }
        else:
            _internal_parallelism = None

        ne_fep = dict()
        for phase in phases:
            _logger.info(f"\t\tphase: {phase}")
            hybrid_factory = HybridTopologyFactory(
                top_prop['%s_topology_proposal' % phase],
                top_prop['%s_old_positions' % phase],
                top_prop['%s_new_positions' % phase],
                neglected_new_angle_terms=top_prop[
                    f"{phase}_forward_neglected_angles"],
                neglected_old_angle_terms=top_prop[
                    f"{phase}_reverse_neglected_angles"],
                softcore_LJ_v2=setup_options['softcore_v2'],
                interpolate_old_and_new_14s=setup_options['anneal_1,4s'])

            if build_samplers:
                ne_fep[phase] = SequentialMonteCarlo(
                    factory=hybrid_factory,
                    lambda_protocol=setup_options['lambda_protocol'],
                    temperature=temperature,
                    trajectory_directory=trajectory_directory,
                    trajectory_prefix=f"{trajectory_prefix}_{phase}",
                    atom_selection=atom_selection,
                    timestep=timestep,
                    eq_splitting_string=eq_splitting,
                    neq_splitting_string=neq_splitting,
                    collision_rate=setup_options['ncmc_collision_rate_ps'],
                    ncmc_save_interval=ncmc_save_interval,
                    internal_parallelism=_internal_parallelism)

        print("Nonequilibrium switching driver class constructed")

        return {'topology_proposals': top_prop, 'ne_fep': ne_fep}

    else:
        _logger.info(f"\tno nonequilibrium detected.")
        htf = dict()
        hss = dict()
        _logger.info(f"\tcataloging HybridTopologyFactories...")

        for phase in phases:
            _logger.info(f"\t\tphase: {phase}:")
            #TODO write a SAMSFEP class that mirrors NonequilibriumSwitchingFEP
            _logger.info(
                f"\t\twriting HybridTopologyFactory for phase {phase}...")
            htf[phase] = HybridTopologyFactory(
                top_prop['%s_topology_proposal' % phase],
                top_prop['%s_old_positions' % phase],
                top_prop['%s_new_positions' % phase],
                neglected_new_angle_terms=top_prop[
                    f"{phase}_forward_neglected_angles"],
                neglected_old_angle_terms=top_prop[
                    f"{phase}_reverse_neglected_angles"],
                softcore_LJ_v2=setup_options['softcore_v2'],
                interpolate_old_and_new_14s=setup_options['anneal_1,4s'])

        for phase in phases:
            # Define necessary vars to check energy bookkeeping
            _top_prop = top_prop['%s_topology_proposal' % phase]
            _htf = htf[phase]
            _forward_added_valence_energy = top_prop['%s_added_valence_energy'
                                                     % phase]
            _reverse_subtracted_valence_energy = top_prop[
                '%s_subtracted_valence_energy' % phase]

            if not use_given_geometries:
                zero_state_error, one_state_error = validate_endstate_energies(
                    _top_prop,
                    _htf,
                    _forward_added_valence_energy,
                    _reverse_subtracted_valence_energy,
                    beta=1.0 / (kB * temperature),
                    ENERGY_THRESHOLD=ENERGY_THRESHOLD
                )  #, trajectory_directory=f'{xml_directory}{phase}')
                _logger.info(f"\t\terror in zero state: {zero_state_error}")
                _logger.info(f"\t\terror in one state: {one_state_error}")
            else:
                _logger.info(
                    f"'use_given_geometries' was passed to setup; skipping endstate validation"
                )

            #TODO expose more of these options in input
            if build_samplers:

                n_states = setup_options['n_states']
                _logger.info(f"\tn_states: {n_states}")
                if 'n_replicas' not in setup_options:
                    n_replicas = n_states
                else:
                    n_replicas = setup_options['n_replicas']

                checkpoint_interval = setup_options['checkpoint_interval']

                # generating lambda protocol
                lambda_protocol = LambdaProtocol(
                    functions=setup_options['protocol-type'])
                _logger.info(
                    f'Using lambda protocol : {setup_options["protocol-type"]}'
                )

                if atom_selection:
                    selection_indices = htf[phase].hybrid_topology.select(
                        atom_selection)
                else:
                    selection_indices = None

                storage_name = str(trajectory_directory) + '/' + str(
                    trajectory_prefix) + '-' + str(phase) + '.nc'
                _logger.info(f'\tstorage_name: {storage_name}')
                _logger.info(f'\tselection_indices {selection_indices}')
                _logger.info(f'\tcheckpoint interval {checkpoint_interval}')
                reporter = MultiStateReporter(
                    storage_name,
                    analysis_particle_indices=selection_indices,
                    checkpoint_interval=checkpoint_interval)

                if phase == 'vacuum':
                    endstates = False
                else:
                    endstates = True

                if setup_options['fe_type'] == 'fah':
                    _logger.info('SETUP FOR FAH DONE')
                    return {
                        'topology_proposals': top_prop,
                        'hybrid_topology_factories': htf
                    }

                if setup_options['fe_type'] == 'sams':
                    hss[phase] = HybridSAMSSampler(
                        mcmc_moves=mcmc.LangevinSplittingDynamicsMove(
                            timestep=timestep,
                            collision_rate=1.0 / unit.picosecond,
                            n_steps=n_steps_per_move_application,
                            reassign_velocities=False,
                            n_restart_attempts=20,
                            constraint_tolerance=1e-06),
                        hybrid_factory=htf[phase],
                        online_analysis_interval=setup_options['offline-freq'],
                        online_analysis_minimum_iterations=10,
                        flatness_criteria=setup_options['flatness-criteria'],
                        gamma0=setup_options['gamma0'])
                    hss[phase].setup(n_states=n_states,
                                     n_replicas=n_replicas,
                                     temperature=temperature,
                                     storage_file=reporter,
                                     lambda_protocol=lambda_protocol,
                                     endstates=endstates)
                elif setup_options['fe_type'] == 'repex':
                    hss[phase] = HybridRepexSampler(
                        mcmc_moves=mcmc.LangevinSplittingDynamicsMove(
                            timestep=timestep,
                            collision_rate=1.0 / unit.picosecond,
                            n_steps=n_steps_per_move_application,
                            reassign_velocities=False,
                            n_restart_attempts=20,
                            constraint_tolerance=1e-06),
                        hybrid_factory=htf[phase],
                        online_analysis_interval=setup_options['offline-freq'])
                    hss[phase].setup(n_states=n_states,
                                     temperature=temperature,
                                     storage_file=reporter,
                                     lambda_protocol=lambda_protocol,
                                     endstates=endstates)
            else:
                _logger.info(f"omitting sampler construction")

            if serialize_systems:
                # save the systems and the states
                pass

                _logger.info('WRITING OUT XML FILES')
                #old_thermodynamic_state, new_thermodynamic_state, hybrid_thermodynamic_state, _ = generate_endpoint_thermodynamic_states(htf[phase].hybrid_system, _top_prop)

                xml_directory = f'{setup_options["trajectory_directory"]}/xml/'
                if not os.path.exists(xml_directory):
                    os.makedirs(xml_directory)
                from perses.utils import data
                _logger.info('WRITING OUT XML FILES')
                _logger.info(f'Saving the hybrid, old and new system to disk')
                data.serialize(
                    htf[phase].hybrid_system,
                    f'{setup_options["trajectory_directory"]}/xml/{phase}-hybrid-system.gz'
                )
                data.serialize(
                    htf[phase]._old_system,
                    f'{setup_options["trajectory_directory"]}/xml/{phase}-old-system.gz'
                )
                data.serialize(
                    htf[phase]._new_system,
                    f'{setup_options["trajectory_directory"]}/xml/{phase}-new-system.gz'
                )

        return {
            'topology_proposals': top_prop,
            'hybrid_topology_factories': htf,
            'hybrid_samplers': hss
        }
    def __init__(
            self,
            protein_filename,
            mutation_chain_id,
            mutation_residue_id,
            proposed_residue,
            phase='complex',
            conduct_endstate_validation=True,
            ligand_file=None,
            ligand_index=0,
            water_model='tip3p',
            ionic_strength=0.15 * unit.molar,
            forcefield_files=[
                'amber14/protein.ff14SB.xml', 'amber14/tip3p.xml'
            ],
            barostat=openmm.MonteCarloBarostat(1.0 * unit.atmosphere,
                                               temperature, 50),
            forcefield_kwargs={
                'removeCMMotion': False,
                'ewaldErrorTolerance': 0.00025,
                'constraints': app.HBonds,
                'hydrogenMass': 4 * unit.amus
            },
            periodic_forcefield_kwargs={'nonbondedMethod': app.PME},
            nonperiodic_forcefield_kwargs=None,
            small_molecule_forcefields='gaff-2.11',
            complex_box_dimensions=None,
            apo_box_dimensions=None,
            **kwargs):
        """
        arguments
            protein_filename : str
                path to protein (to mutate); .pdb
            mutation_chain_id : str
                name of the chain to be mutated
            mutation_residue_id : str
                residue id to change
            proposed_residue : str
                three letter code of the residue to mutate to
            phase : str, default complex
                if phase == vacuum, then the complex will not be solvated with water; else, it will be solvated with tip3p
            conduct_endstate_validation : bool, default True
                whether to conduct an endstate validation of the hybrid topology factory
            ligand_file : str, default None
                path to ligand of interest (i.e. small molecule or protein); .sdf or .pdb
            ligand_index : int, default 0
                which ligand to use
            water_model : str, default 'tip3p'
                solvent model to use for solvation
            ionic_strength : float * unit.molar, default 0.15 * unit.molar
                the total concentration of ions (both positive and negative) to add using Modeller.
                This does not include ions that are added to neutralize the system.
                Note that only monovalent ions are currently supported.
            forcefield_files : list of str, default ['amber14/protein.ff14SB.xml', 'amber14/tip3p.xml']
                forcefield files for proteins and solvent
            barostat : openmm.MonteCarloBarostat, default openmm.MonteCarloBarostat(1.0 * unit.atmosphere, 300 * unit.kelvin, 50)
                barostat to use
            forcefield_kwargs : dict, default {'removeCMMotion': False, 'ewaldErrorTolerance': 1e-4, 'constraints' : app.HBonds, 'hydrogenMass' : 4 * unit.amus}
                forcefield kwargs for system parametrization
            periodic_forcefield_kwargs : dict, default {'nonbondedMethod': app.PME}
                periodic forcefield kwargs for system parametrization
            nonperiodic_forcefield_kwargs : dict, default None
                non-periodic forcefield kwargs for system parametrization
            small_molecule_forcefields : str, default 'gaff-2.11'
                the forcefield string for small molecule parametrization
            complex_box_dimensions : Vec3, default None
                define box dimensions of complex phase;
                if none, padding is 1nm
            apo_box_dimensions :  Vec3, default None
                define box dimensions of apo phase phase;
                if None, padding is 1nm

        TODO : allow argument for spectator ligands besides the 'ligand_file'

        """

        # First thing to do is load the apo protein to mutate...
        protein_pdbfile = open(protein_filename, 'r')
        protein_pdb = app.PDBFile(protein_pdbfile)
        protein_pdbfile.close()
        protein_positions, protein_topology, protein_md_topology = protein_pdb.positions, protein_pdb.topology, md.Topology.from_openmm(
            protein_pdb.topology)
        protein_topology = protein_md_topology.to_openmm()
        protein_n_atoms = protein_md_topology.n_atoms

        # Load the ligand, if present
        molecules = []
        if ligand_file:
            if ligand_file.endswith('.sdf'):  # small molecule
                ligand_mol = createOEMolFromSDF(ligand_file,
                                                index=ligand_index)
                ligand_mol = generate_unique_atom_names(ligand_mol)
                molecules.append(
                    Molecule.from_openeye(ligand_mol,
                                          allow_undefined_stereo=False))
                ligand_positions, ligand_topology = extractPositionsFromOEMol(
                    ligand_mol
                ), forcefield_generators.generateTopologyFromOEMol(ligand_mol)
                ligand_md_topology = md.Topology.from_openmm(ligand_topology)
                ligand_n_atoms = ligand_md_topology.n_atoms

            elif ligand_file.endswith('pdb'):  # protein
                ligand_pdbfile = open(ligand_file, 'r')
                ligand_pdb = app.PDBFile(ligand_pdbfile)
                ligand_pdbfile.close()
                ligand_positions, ligand_topology, ligand_md_topology = ligand_pdb.positions, ligand_pdb.topology, md.Topology.from_openmm(
                    ligand_pdb.topology)
                ligand_n_atoms = ligand_md_topology.n_atoms

            else:
                _logger.warning(
                    f'ligand filetype not recognised. Please provide a path to a .pdb or .sdf file'
                )
                return

            # Now create a complex
            complex_md_topology = protein_md_topology.join(ligand_md_topology)
            complex_topology = complex_md_topology.to_openmm()
            complex_positions = unit.Quantity(np.zeros(
                [protein_n_atoms + ligand_n_atoms, 3]),
                                              unit=unit.nanometers)
            complex_positions[:protein_n_atoms, :] = protein_positions
            complex_positions[protein_n_atoms:, :] = ligand_positions

        # Now for a system_generator
        self.system_generator = SystemGenerator(
            forcefields=forcefield_files,
            barostat=barostat,
            forcefield_kwargs=forcefield_kwargs,
            periodic_forcefield_kwargs=periodic_forcefield_kwargs,
            nonperiodic_forcefield_kwargs=nonperiodic_forcefield_kwargs,
            small_molecule_forcefield=small_molecule_forcefields,
            molecules=molecules,
            cache=None)

        # Solvate apo and complex...
        apo_input = list(
            self._solvate(protein_topology, protein_positions, water_model,
                          phase, ionic_strength, apo_box_dimensions))
        inputs = [apo_input]
        if ligand_file:
            inputs.append(
                self._solvate(complex_topology, complex_positions, water_model,
                              phase, ionic_strength, complex_box_dimensions))

        geometry_engine = FFAllAngleGeometryEngine(
            metadata=None,
            use_sterics=False,
            n_bond_divisions=100,
            n_angle_divisions=180,
            n_torsion_divisions=360,
            verbose=True,
            storage=None,
            bond_softening_constant=1.0,
            angle_softening_constant=1.0,
            neglect_angles=False,
            use_14_nonbondeds=True)

        # Run pipeline...
        htfs = []
        for (top, pos, sys) in inputs:
            point_mutation_engine = PointMutationEngine(
                wildtype_topology=top,
                system_generator=self.system_generator,
                chain_id=
                mutation_chain_id,  # Denote the chain id allowed to mutate (it's always a string variable)
                max_point_mutants=1,
                residues_allowed_to_mutate=[
                    mutation_residue_id
                ],  # The residue ids allowed to mutate
                allowed_mutations=[
                    (mutation_residue_id, proposed_residue)
                ],  # The residue ids allowed to mutate with the three-letter code allowed to change
                aggregate=True)  # Always allow aggregation

            topology_proposal = point_mutation_engine.propose(sys, top)

            # Only validate energy bookkeeping if the WT and proposed residues do not involve rings
            old_res = [
                res for res in top.residues() if res.id == mutation_residue_id
            ][0]
            validate_bool = False if old_res.name in ring_amino_acids or proposed_residue in ring_amino_acids else True
            new_positions, logp_proposal = geometry_engine.propose(
                topology_proposal,
                pos,
                beta,
                validate_energy_bookkeeping=validate_bool)
            logp_reverse = geometry_engine.logp_reverse(
                topology_proposal,
                new_positions,
                pos,
                beta,
                validate_energy_bookkeeping=validate_bool)

            forward_htf = HybridTopologyFactory(
                topology_proposal=topology_proposal,
                current_positions=pos,
                new_positions=new_positions,
                use_dispersion_correction=False,
                functions=None,
                softcore_alpha=None,
                bond_softening_constant=1.0,
                angle_softening_constant=1.0,
                soften_only_new=False,
                neglected_new_angle_terms=[],
                neglected_old_angle_terms=[],
                softcore_LJ_v2=True,
                softcore_electrostatics=True,
                softcore_LJ_v2_alpha=0.85,
                softcore_electrostatics_alpha=0.3,
                softcore_sigma_Q=1.0,
                interpolate_old_and_new_14s=False,
                omitted_terms=None)

            if not topology_proposal.unique_new_atoms:
                assert geometry_engine.forward_final_context_reduced_potential == None, f"There are no unique new atoms but the geometry_engine's final context reduced potential is not None (i.e. {self._geometry_engine.forward_final_context_reduced_potential})"
                assert geometry_engine.forward_atoms_with_positions_reduced_potential == None, f"There are no unique new atoms but the geometry_engine's forward atoms-with-positions-reduced-potential in not None (i.e. { self._geometry_engine.forward_atoms_with_positions_reduced_potential})"
            else:
                added_valence_energy = geometry_engine.forward_final_context_reduced_potential - geometry_engine.forward_atoms_with_positions_reduced_potential

            if not topology_proposal.unique_old_atoms:
                assert geometry_engine.reverse_final_context_reduced_potential == None, f"There are no unique old atoms but the geometry_engine's final context reduced potential is not None (i.e. {self._geometry_engine.reverse_final_context_reduced_potential})"
                assert geometry_engine.reverse_atoms_with_positions_reduced_potential == None, f"There are no unique old atoms but the geometry_engine's atoms-with-positions-reduced-potential in not None (i.e. { self._geometry_engine.reverse_atoms_with_positions_reduced_potential})"
                subtracted_valence_energy = 0.0
            else:
                subtracted_valence_energy = geometry_engine.reverse_final_context_reduced_potential - geometry_engine.reverse_atoms_with_positions_reduced_potential

            if conduct_endstate_validation:
                zero_state_error, one_state_error = validate_endstate_energies(
                    forward_htf._topology_proposal,
                    forward_htf,
                    added_valence_energy,
                    subtracted_valence_energy,
                    beta=beta,
                    ENERGY_THRESHOLD=ENERGY_THRESHOLD)
                if zero_state_error > ENERGY_THRESHOLD:
                    _logger.warning(
                        f"Reduced potential difference of the nonalchemical and alchemical Lambda = 0 state is above the threshold ({ENERGY_THRESHOLD}): {zero_state_error}"
                    )
                if one_state_error > ENERGY_THRESHOLD:
                    _logger.warning(
                        f"Reduced potential difference of the nonalchemical and alchemical Lambda = 1 state is above the threshold ({ENERGY_THRESHOLD}): {one_state_error}"
                    )
            else:
                pass

            htfs.append(forward_htf)

        self.apo_htf = htfs[0]
        self.complex_htf = htfs[1] if ligand_file else None