In this tutorial you’ll use BioSimSpace to set up and run a simple metadyamics simulation to calculate the free energy as a function of the $${\phi}$$ and $${\psi}$$ dihedral angles for an alanine dipeptide molecule in vacuum.

Import BioiSmSpace using

>>> import BioSimSpace as BSS


Load a molecular system from a URL, via BioSimSpace.IO.readMolecules():

>>> url = BSS.tutorialUrl()


Note that this is a solvated system, which contains the alanine dipeptide and a bunch of water molecules. Since we want to study the molecule in vacuum, we will extract the alanine dipeptide using a search:

>>> search = system.search("resname ALA").molecules()
>>> print(search)
<BioSimSpace.SearchResult: nResults=1>


Note

Searches typically return the smallest unit, i.e. an atom, so we call molecules() to convert the result to a set of molecules that contain the search. BioSimSpace uses sire’s powerful search functionality.

We found a single matching result. Lets extract the result from the search object:

>>> molecule = search[0]


Let’s now examine each of the atoms in the molecule to make sure that it looks like we’d expect:

>>> for atom in molecule.getAtoms():
...     print(atom)
...
<BioSimSpace.Atom: name='HH31', molecule=2 index=0>
<BioSimSpace.Atom: name='CH3', molecule=2 index=1>
<BioSimSpace.Atom: name='HH32', molecule=2 index=2>
<BioSimSpace.Atom: name='HH33', molecule=2 index=3>
<BioSimSpace.Atom: name='C', molecule=2 index=4>
<BioSimSpace.Atom: name='O', molecule=2 index=5>
<BioSimSpace.Atom: name='N', molecule=2 index=6>
<BioSimSpace.Atom: name='H', molecule=2 index=7>
<BioSimSpace.Atom: name='CA', molecule=2 index=8>
<BioSimSpace.Atom: name='HA', molecule=2 index=9>
<BioSimSpace.Atom: name='CB', molecule=2 index=10>
<BioSimSpace.Atom: name='HB1', molecule=2 index=11>
<BioSimSpace.Atom: name='HB2', molecule=2 index=12>
<BioSimSpace.Atom: name='HB3', molecule=2 index=13>
<BioSimSpace.Atom: name='C', molecule=2 index=14>
<BioSimSpace.Atom: name='O', molecule=2 index=15>
<BioSimSpace.Atom: name='N', molecule=2 index=16>
<BioSimSpace.Atom: name='H', molecule=2 index=17>
<BioSimSpace.Atom: name='CH3', molecule=2 index=18>
<BioSimSpace.Atom: name='HH31', molecule=2 index=19>
<BioSimSpace.Atom: name='HH32', molecule=2 index=20>
<BioSimSpace.Atom: name='HH33', molecule=2 index=21>


The atoms that define the two dihedrals are:

$${\phi}$$: C index=4, N index=6, CA index=8, C index=14

$${\psi}$$: N index=6, CA index=8, C index=14, N index=16

Let’s store the indices:

>>> phi_idx = [4, 6, 8, 14]
>>> psi_idx = [6, 8, 14, 16]


Now we need to construct to collective variables that represent the dihedrals (torsions) above. These are the variables that will be sampled during our metadynamics simulation, allowing us to estimate the free energy of the system as a function of the dihedral angles.

>>> phi = BSS.Metadynamics.CollectiveVariable.Torsion(atoms=phi_idx)


Note how we passed the indices of the atoms involved in the torsion to the constructor. Since atoms are indexed relative to the molecule that they belong it is important that we get the absolute atom index within the system. In this case we have a single molecule, so all is okay, but in general you should be careful. For example, atom index 2 in molecule 2 won’t be atom index 2 in the system, i.e. you would have to shift the index by the number of atoms in molecule 1.

Thankfully we provide simple tools to compute this index for you. For example, for atom CA in the molecule:

>>> atom = molecule.search("atomname CA")[0]
>>> print(system.getIndex(atom))
8


(In this case we get the same index as the molecule since the alanine dipeptide is the first molecule in the system.)

There are many other options that can be set for collective variables, such as setting the width of the Gaussian hill that is used to bias a variable, specifying lower and upper bounds for the value of the variable, and sampling on a pre-defined grid to help speed up simulations. See BioSimSpace.Metadynamics.CollectiveVariable.Torsion for more information.

We now need a protocol to describe our metadynamics simulation. Let’s go with the defaults, other than increasing the run time to 3 nanoseconds.

>>> protocol = BSS.Protocol.Metadynamics(collective_variable=[phi, psi], runtime=3 * BSS.Units.Time.nanosecond)


Finally, we need a process to actually run our metadynamics simulation. BioSimSpace will automatically configure this for you with the BioSimSpace.Metadynamics.run function. Note that the function expects a System and a Protocol as arguments, so we convert our alanine dipeptide molecule to a single-molecule system.

>>> process = BSS.Metadynamics.run(molecule.toSystem(), protocol, gpu_support=True)


All being well, we should now have a simulation process running in the background. Let’s check that it’s running:

>>> process.isRunning()
True


To see the PLUMED configuration file that was generated:

>>> process.getPlumedConfig()
['RESTART NO',
'\n# Define the molecular entities.',
'WHOLEMOLECULES ENTITY0=1-22',
'\n# Define the collective variable.',
't1: TORSION ATOMS=5,7,9,15',
'\n# Define the collective variable.',
't2: TORSION ATOMS=7,9,15,17',
'PRINT STRIDE=1000 ARG=* FILE=COLVAR']


If running interactively, e.g. within an IPython, console or Jupyter notebook, it’s possible to interact with the process directly. In this case, we will wait for the process to end, then analyse the results.

To wait for the process to finish, simply run:

>>> process.wait()


Assuming you are working interactively, we can then plot the time evolution of the two collective variables as follows:

>>> BSS.Notebook.plot(x=process.getTime(time_series=True), y=process.getCollectiveVariable(0, time_series=True))
>>> BSS.Notebook.plot(x=process.getTime(time_series=True), y=process.getCollectiveVariable(1, time_series=True))


Note

The getCollectiveVariable function takes the indices of the collective variables that we passed to the Protocol construtor, i.e. 0 = $${\phi}$$ and 1 = $${\psi}$$.

Note

BioSimSpace automatically writes labels for the axis of any plot based on the type of the input data.

It is also possible to compute the free energy estimate as a function of $${\phi}$$ and $${\psi}$$ and create a contour plot:

>>> free_nrg = process.getFreeEnergy(kt=BSS.Units.Energy.kt)
>>> BSS.Notebook.plotContour(free_nrg[0], free_nrg[1], free_nrg[2])


Note

The returned free_nrg object is a tuple, containing lists for the values of $${\phi}$$ and $${\psi}$$, and the corresponding free energy estimate.

If we are only interested in the free energy as a function of a single collective variable, then it’s possible to perform a projection by integrating out the other variables. We can do this by passing the index of the collective variable of interest to the getFreeEnergy function, along with an appropriate temperature factor (in energy units) for integrating out the other variables. For example, to get the free energy as a function of $${\phi}$$ only, then visualise as a simple $${x-y}$$ plot:

>>> free_nrg_phi = process.getFreeEnergy(index=0, kt=BSS.Units.Energy.kt)
>>> BSS.Notebook.plot(x=free_nrg_phi[0], y=free_nrg_phi[1])


Having successfully sampled the free energy landscape as a function of the $${\phi}$$ and $${\psi}$$ dihedral angles, we might next want to examine representative snapshots from the basins. To do this we can use the sampleConfigurations method of the process object. This takes a list of bounds for the values of the collective variables as an argument, along with the maximum number of snaphots that we would like. The method returns a list of randomly sampled molecular configurations that lie within the bounds, along with a list containing the corresponding collective variable values.

Let’s consider the basin just to the right in the middle of the free-energy contour plot above. This lies roughly at $${0.5\geq\phi\leq1.5}$$ and $${−1.5\geq\psi\leq0.5}$$.

>>> bounds = [(0.5 * BSS.Units.Angle.radian, 1.5 * BSS.Units.Angle.radian), (-1.5 * BSS.Units.Angle.radian, 0.5 * BSS.Units.Angle.radian)]
>>> configs, colvars = process.sampleConfigurations(bounds, 20)


Let’s examine the value of the collective variables for each sample to make sure they are in range:

>>> print(colvars)

We hope that you have enjoyed this tutorial. Please explore the metadynamics API documentation for further information.