Detailed Example

🎊 Congratulations! 🎊

It seems you really want to learn how to use protex! Keep going! 😄

Setup

Now we will dive a little bit deeper in all of the functions and methods protex offers. First and foremost we need to make sure to have valid input files! The usual way is to provide a psf and crd file alongside the toppar files. The topology and parameter files (toppar) contain all the residues needed for the system setup. It is really important, that the corresponding residues prone to (de-)protonation have the same number of atoms in the same order. So, i.e if we have methylimidazolium (im1h) and methylimidazole (im1) the missing hydrogen for im1 needs to be a dummy atom. The same is true for acetate/acetic acid. Additionally, make sure that also atoms in the coordinate files match the residue description.

Attention

It is VERY important that the atom order of the protonated and deprotonated residues match exactly between the coordinate as well as topology/psf files!

Examplary snippet of the RESI section of a CHARMM input structure. Please note the same atom ordering between the residues. Additionally the coordinate files need to have the same ordering as well.

RESI  IM1    0.0000     ||      RESI  IM1H  1.0000
GROUP                   ||      GROUP
ATOM  C1     CD33G      ||      ATOM  C1     CD33F
ATOM  H1     HDA3A      ||      ATOM  H1     HDA3A
ATOM  H2     HDA3A      ||      ATOM  H2     HDA3A
ATOM  H3     HDA3A      ||      ATOM  H3     HDA3A
ATOM  N1     ND2R5A     ||      ATOM  N1     ND2R5C
ATOM  C2     CD2R5A     ||      ATOM  C2     CD2R5D
ATOM  H4     HDR5A      ||      ATOM  H4     HDR5D
ATOM  C3     CD2R5A     ||      ATOM  C3     CD2R5D
ATOM  H5     HDR5A      ||      ATOM  H5     HDR5D
ATOM  C4     CD2R5B     ||      ATOM  C4     CD2R5E
ATOM  H6     HDR5B      ||      ATOM  H6     HDR5E
ATOM  N2     ND2R5B     ||      ATOM  N2     ND2R5C
ATOM  H7     DUMMY H    ||      ATOM  H7     HDP1A
ATOM  LPN21  LPD        ||      ATOM  LPN21  LPD

Now, lets start building our system. The first part, getting the OpenMM simulation object is nothing protex specific and we won’t need any protex functions for it. Nevertheless, here is one possible way to do it, if you are not that familiar with OpenMM.

Note

All the files we need can be found in the protex directory.

from openmm import Platform
from openmm.app import PME, CharmmCrdFile, CharmmParameterSet, CharmmCrdFile, Simulation
from openmm.unit import angstroms, kelvin, picoseconds
from velocityverletplugin import VVIntegrator

protex_dir = "/path/to/your/protex/directory"

print("Loading CHARMM files...")
PARA_FILES = ["toppar_drude_master_protein_2013f_lj025.str", "hoac_d.str", "im1h_d.str", "im1_d.str", "oac_d.str"]
params = CharmmParameterSet(*[f"{protex_dir}/forcefield/toppar/{para_files}" for para_files in PARA_FILES])
psf = CharmmPsfFile(f"{protex_dir}/forcefield/im1h_oac_150_im1_hoac_350.psf")
psf.setBox(48*angstroms, 48*angstroms, 48*angstroms)
crd = CharmmCrdFile(f"{protex_dir}/forcefield/im1h_oac_150_im1_hoac_350.crd")

print("Create system")
system = psf.createSystem(params, nonbondedMethod=PME, nonbondedCutoff=11.0 * angstroms, switchDistance=10 * angstroms, constraints=None)

print("Setup simulation")
integrator = VVIntegrator(300 * kelvin, 10 / picoseconds, 1 * kelvin, 100 / picoseconds, 0.0005 * picoseconds)
integrator.setMaxDrudeDistance(0.25 * angstroms)
platform = Platform.getPlatformByName("CUDA")
prop = dict(CudaPrecision="single")
simulation = Simulation(psf.topology, system, integrator, platform=platform, platformProperties=prop)
simulation.context.setPositions(crd.positions)

Now we have the simulation object ready. In principle we did, what was done with generate_im1h_oac_system(). For advanced usage of this function see the Advanced Setup section.

Next, we construct the IonicLiquidTemplates class, which will be needed beside the simulation object to build the IonicLiquidSystem. Two parts are needed. On the one hand a dictionary, with the settings for the possible transfers. The key is always a frozenset of the transfer reaction, while the value is another dictionary with the keywords “r_max” and “prob” corresponding values for the maximum distance (in Angstrom) and the probability for this transfer. 0 means the reaction should never happen, 1 every time “r_max” is fullfilled. Note that it is equivalent to write frozenset(["IM1H", "OAC"]) or frozenset(["OAC", "IM1H"]).

The second ingredient is another dictionary specifiying the acceptor/donor atom name. So in our example from above, we want the hydrogen H7 from IM1H to be transfered to the nitrogen N2 of IM1. This information belongs together, so it is grouped in one dictionary, as can be seen in the next code snippet. “canonical_name” is deprecated.

The IonicLiquidTemplates class accepts now a list, of all dictionaries with the specified atoms, as well as the allowed_updates dictionary.

from protex.system import ProtexTemplates

allowed_updates = {}
allowed_updates[frozenset(["IM1H", "OAC"])] = {"r_max": 0.16, "prob": 0.994}
allowed_updates[frozenset(["IM1", "HOAC"])] = {"r_max": 0.16, "prob": 0.098}

IM1H_IM1 = {"IM1H": {"atom_name": "H7", "canonical_name": "IM1"},
             "IM1": {"atom_name": "N2", "canonical_name": "IM1"}}

OAC_HOAC = {"OAC" : {"atom_name": "O2", "canonical_name": "OAC"},
            "HOAC": {"atom_name": "H", "canonical_name": "OAC"}}

templates = ProtexTemplates([OAC_HOAC, IM1H_IM1], allowed_updates)

Now we have everything to build the ProtexSystem:

from protex.system import ProtexSystem

ionic_liquid = ProtexSystem(simulation, templates)

Next define the update method. Currently there is one available update method called NaiveMCUpdate. It uses the information passes before, to determine the distance criterion for the specific update paris and the probability. NaiveMCUpdate accepts to more keywords:

NaiveMCUpdate
parameters
ionic_liquid: ProtexSystem

The Protex system

all_forces: bool = True

Wether to change all forces during an update (default), or just the non bonded force (all_force=False)

to_adapt: list[tuple[str, int, frozenset[str]]] = None

This option is used to keep certain residues around an equilibrium value. The tuple consists of the name of the residue, the amount of molecules, and the update set, for which the probability will be accordingly altered.

Important: If using this option in consecutive runs, consider using the save_updates and load_updates methods of ionic liquid to get the current probability values.

from protex.update import NaiveMCUpdate, StateUpdate

to_adapt = [("IM1H", 150, frozenset(["IM1H", "OAC"])), ("IM1", 350, frozenset(["IM1", "HOAC"]))]
update = NaiveMCUpdate(ionic_liquid, all_forces=True, to_adapt=to_adapt)
state_update = StateUpdate(update)

Optionally you can define reporters for the simulation. Protex has a built in ChargeReporter to report the current charges of all molecules which can just be added to the simulation like all other OpenMM reporters. You can define an additional header line with arbitrary informtion, e.g. on system settings.

from protex.reporter import ChargeReporter

save_freq = 200
infos={f"Put whatever additional infos you would like the charge reporter to store here, e.g. save_freq: {save_freq}"}
charge_reporter = ChargeReporter(f"path/to/outfile", save_freq, ionic_liquid, header_data=infos)
ionic_liquid.simulation.reporters.append(charge_reporter)

You can add additional OpenMM reporters:

from openmm.app import StateDataReporter, DCDReporter

report_frequency = 200
ionic_liquid.simulation.reporters.append(DCDReporter(f"traj.dcd", report_frequency))
state_data_reporter= StateDataReporter(sys.stdout,
    report_frequency,
    step=True,
    time=True,
    potentialEnergy=True,
    kineticEnergy=True,
    totalEnergy=True,
    temperature=True,
    volume=True,
    density=False,
)
ionic_liquid.simulation.reporters.append(state_data_reporter)

Now you are ready to run the simulation and just call the update method whenever you like. The state_update.update() method an integer as argument specifying the intermediate lambda-states for an update. 2 means no intermediate steps, just one before and one after the update. Consequently every number n, means n-2 actual intermediate steps.

You can also save a psf file at any point during the simulation or store the current update values for the probability. Due to current limitations on the conversion of Drude OpenMM toplogoies to ParmEd structures, the user has to supply a reference psf file. This can just be the initial psf file used for the system creation.

ionic_liquid.simulation.step(1000)
state_update.update(2)
ionic_liquid.save_updates("updates.txt")
ionic_liquid.write_psf("im1h_oac_150_im1_hoac_350.psf", "new.psf")

Advanced Setup

One usual way might be to do multiple runs, which means restarting the simulation after some time. There are some options in protex which should help. Use the psf_file argument to load the current psf and the restart_file argument to load the current restart file. Alternatively new coordinates can also be specified via load_checkpoint()

from protex.testsystems import generate_im1h_oac_system

psf_file = "psf_file_from_previous_run.psf"
restart_file = "restart_file.rst"
simulation = generate_im1h_oac_system(psf_file=psf_file,restart_file=restart_file)

...

ionic_liquid.load_updates("updates.txt")
ionic_liquid.loadCheckpoint("nvt_checkpoint.rst")

...

ionic_liquid.save_updates("updates.txt")
ionic_liquid.write_psf("im1h_oac_150_im1_hoac_350.psf", "nvt.psf")
ionic_liquid.saveCheckpoint("nvt_checkpoint.rst")