Restraints#
It can be useful to add restraints to the system to hold things in place. For example, you might want to hold the protein fixed while allowing the ligand to move around. Or you may want to hold a ligand in place while it is being decoupled from the simulation. Or you may want to use restraints to control the distances between atoms or pull a molecule into a particular conformation.
You can specify the restraints you want to add to a system via the functions
in the sire.restraints
module. These functions return
Restraints
objects that contain all of the information that
needs to be passed to OpenMM to add the restraints to the system.
Positional Restraints#
The simplest type of restraint is a positional restraint, which holds a set of
atoms at a fixed position. You can create a positional restraint using the
positional()
function. This function takes the
set of molecules or system you want to simulate, together with a search
string, list of atom indexes, or molecule views holding the atoms that
you want to restrain. For example,
>>> import sire as sr
>>> mols = sr.load(sr.expand(sr.tutorial_url, "ala.top", "ala.crd"))
>>> restraints = sr.restraints.positional(mols, atoms="resname ALA and element C")
>>> print(restraints)
PositionalRestraints( size=3
0: PositionalRestraint( 8 => ( 16.5371, 5.02707, 15.812 ), k=150 kcal mol-1 Å-2 : r0=0 Å )
1: PositionalRestraint( 10 => ( 16.0464, 6.38937, 15.2588 ), k=150 kcal mol-1 Å-2 : r0=0 Å )
2: PositionalRestraint( 14 => ( 15.3698, 4.19397, 16.434 ), k=150 kcal mol-1 Å-2 : r0=0 Å )
)
will create positional restraints for the three carbon atoms in the alanine
residue. These carbon atoms are at indexes 8, 10 and 14 in mols.atoms()
,
e.g.
>>> print(mols.atoms([8, 10, 14]))
SireMol::SelectorM<SireMol::Atom>( size=3
0: MolNum(6) Atom( CA:9 [ 16.54, 5.03, 15.81] )
1: MolNum(6) Atom( CB:11 [ 16.05, 6.39, 15.26] )
2: MolNum(6) Atom( C:15 [ 15.37, 4.19, 16.43] )
)
You could have specified these indicies directly, e.g.
>>> restraints = sr.restraints.positional(mols, atoms=[8, 10, 14])
>>> print(restraints)
PositionalRestraints( size=3
0: PositionalRestraint( 8 => ( 16.5371, 5.02707, 15.812 ), k=150 kcal mol-1 Å-2 : r0=0 Å )
1: PositionalRestraint( 10 => ( 16.0464, 6.38937, 15.2588 ), k=150 kcal mol-1 Å-2 : r0=0 Å )
2: PositionalRestraint( 14 => ( 15.3698, 4.19397, 16.434 ), k=150 kcal mol-1 Å-2 : r0=0 Å )
)
or just passed in the atoms directly, e.g.
>>> atoms = mols.atoms([8, 10, 14])
>>> restraints = sr.restraints.positional(mols, atoms=atoms)
>>> print(restraints)
PositionalRestraints( size=3
0: PositionalRestraint( 8 => ( 16.5371, 5.02707, 15.812 ), k=150 kcal mol-1 Å-2 : r0=0 Å )
1: PositionalRestraint( 10 => ( 16.0464, 6.38937, 15.2588 ), k=150 kcal mol-1 Å-2 : r0=0 Å )
2: PositionalRestraint( 14 => ( 15.3698, 4.19397, 16.434 ), k=150 kcal mol-1 Å-2 : r0=0 Å )
)
The default force constant is 150 kcal mol-1 Å-2. By default, the
atoms are restrained to their current positions, and are held exactly in
those positions (hence why the r0=0 Å
in the output above).
You can set the force constant and width of the half-harmonic restraint
used to hold the atoms in position using the k
and r0
arguments, e.g.
>>> restraints = sr.restraints.positional(mols, atoms="resname ALA and element C",
... k="100 kcal mol-1 A-2", r0="1.5 A")
>>> print(restraints)
PositionalRestraints( size=3
0: PositionalRestraint( 8 => ( 16.5371, 5.02707, 15.812 ), k=100 kcal mol-1 Å-2 : r0=1.5 Å )
1: PositionalRestraint( 10 => ( 16.0464, 6.38937, 15.2588 ), k=100 kcal mol-1 Å-2 : r0=1.5 Å )
2: PositionalRestraint( 14 => ( 15.3698, 4.19397, 16.434 ), k=100 kcal mol-1 Å-2 : r0=1.5 Å )
)
By default, the atoms are restrained to their current positions. You can
specify a different position using the position
argument, e.g.
>>> restraints = sr.restraints.positional(mols, atoms="resname ALA and element C",
... position=[(0,0,0), (1,1,1), (2,2,2)])
>>> print(restraints)
PositionalRestraints( size=3
0: PositionalRestraint( 8 => ( 0, 0, 0 ), k=150 kcal mol-1 Å-2 : r0=0 Å )
1: PositionalRestraint( 10 => ( 1, 1, 1 ), k=150 kcal mol-1 Å-2 : r0=0 Å )
2: PositionalRestraint( 14 => ( 2, 2, 2 ), k=150 kcal mol-1 Å-2 : r0=0 Å )
)
Note
The number of positions must equal the number of atoms to be restrained, or equal to 1. If you only pass in a single position then this will be used for all of the restraints.
Sometimes it can be useful to use the same position for all of the restraints, e.g. in the case of a spherical half-harmonic restraint that holds molecules within a spherical bubble, e.g.
>>> center = mols[0].coordinates()
>>> restraints = sr.restraints.positional(mols,
... atoms=mols[f"atoms within 10 of {center}"],
... position=center,
... r0="10 A")
>>> print(restraints)
PositionalRestraints( size=358
0: PositionalRestraint( 0 => ( 16.5471, 4.50102, 15.6589 ), k=150 kcal mol-1 Å-2 : r0=10 Å )
1: PositionalRestraint( 1 => ( 16.5471, 4.50102, 15.6589 ), k=150 kcal mol-1 Å-2 : r0=10 Å )
2: PositionalRestraint( 2 => ( 16.5471, 4.50102, 15.6589 ), k=150 kcal mol-1 Å-2 : r0=10 Å )
3: PositionalRestraint( 3 => ( 16.5471, 4.50102, 15.6589 ), k=150 kcal mol-1 Å-2 : r0=10 Å )
4: PositionalRestraint( 4 => ( 16.5471, 4.50102, 15.6589 ), k=150 kcal mol-1 Å-2 : r0=10 Å )
...
353: PositionalRestraint( 1892 => ( 16.5471, 4.50102, 15.6589 ), k=150 kcal mol-1 Å-2 : r0=10 Å )
354: PositionalRestraint( 1893 => ( 16.5471, 4.50102, 15.6589 ), k=150 kcal mol-1 Å-2 : r0=10 Å )
355: PositionalRestraint( 1906 => ( 16.5471, 4.50102, 15.6589 ), k=150 kcal mol-1 Å-2 : r0=10 Å )
356: PositionalRestraint( 1907 => ( 16.5471, 4.50102, 15.6589 ), k=150 kcal mol-1 Å-2 : r0=10 Å )
357: PositionalRestraint( 1908 => ( 16.5471, 4.50102, 15.6589 ), k=150 kcal mol-1 Å-2 : r0=10 Å )
)
has created half-harmonic restraints that affect all atoms within 10 Å of the center of the first molecule, and that restrain those atoms to be within a sphere of radius 10 Å centered on that molecule.
Note that the restraints only contain the indicies of the atoms that are
restrained, not the atoms themselves. You can get the atoms by looking up
those indicies from the mols
molecular container from which the
atoms were selected. For example, here we get all of the atoms that are
subject to that spherical half-harmonic restraint;
>>> print(mols.atoms()[[restraint.atom() for restraint in restraints]])
SireMol::SelectorM<SireMol::Atom>( size=358
0: MolNum(6) Atom( HH31:1 [ 18.45, 3.49, 12.44] )
1: MolNum(6) Atom( CH3:2 [ 18.98, 3.45, 13.39] )
2: MolNum(6) Atom( HH32:3 [ 20.05, 3.63, 13.29] )
3: MolNum(6) Atom( HH33:4 [ 18.80, 2.43, 13.73] )
4: MolNum(6) Atom( C:5 [ 18.48, 4.55, 14.35] )
...
353: MolNum(630) Atom( H1:1893 [ 14.92, 1.28, 17.05] )
354: MolNum(630) Atom( H2:1894 [ 15.19, -0.21, 17.07] )
355: MolNum(623) Atom( O:1907 [ 21.65, 7.88, 9.79] )
356: MolNum(623) Atom( H1:1908 [ 22.33, 8.56, 9.83] )
357: MolNum(623) Atom( H2:1909 [ 21.07, 8.08, 10.53] )
)
Distance or Bond Restraints#
The sire.restraints.distance()
or sire.restraints.bond()
functions
are used to create bond or distance restraints. These are identical restraints,
so the functions are just synonyms of each other (they are the same function
with different names).
These functions take the set of molecules or system you want to simulate, plus two search strings, lists of atom indexes, or molecule views holding the pairs of atoms that you want to restrain, e.g.
>>> restraints = sr.restraints.distance(mols, atoms0=0, atoms1=1)
>>> print(restraints)
BondRestraints( size=1
0: BondRestraint( 0 <=> 1, k=150 kcal mol-1 Å-2 : r0=1.09 Å )
)
or
>>> restraints = sr.restraints.bond(mols, atoms0=0, atoms1=1)
>>> print(restraints)
BondRestraints( size=1
0: BondRestraint( 0 <=> 1, k=150 kcal mol-1 Å-2 : r0=1.09 Å )
)
creates a single harmonic distance (or bond) restraint that acts between atoms 0 and 1. By default, the equilibrium distance (r0) is the current distance between the atoms (1.09 Å), and the force constant (k) is 150 kcal mol-1 Å-2.
You can set these via the k
and r0
arguments, e.g.
>>> restraints = sr.restraints.bond(mols, atoms0=0, atoms1=1,
... k="100 kcal mol-1 A-2", r0="1.5 A")
>>> print(restraints)
BondRestraints( size=1
0: BondRestraint( 0 <=> 1, k=100 kcal mol-1 Å-2 : r0=1.5 Å )
)
You can specify as many atoms pairs as you like, e.g.
>>> restraints = sr.restraints.bond(mols, atoms0=[0, 1, 2], atoms1=[3, 4, 5])
>>> print(restraints)
BondRestraints( size=3
0: BondRestraint( 0 <=> 3, k=150 kcal mol-1 Å-2 : r0=1.71245 Å )
1: BondRestraint( 1 <=> 4, k=150 kcal mol-1 Å-2 : r0=1.54643 Å )
2: BondRestraint( 2 <=> 5, k=150 kcal mol-1 Å-2 : r0=2.48642 Å )
)
You can specify the atoms using a search string, passing the atoms themselves, or using the atom index, just as you could for the positional restraints.
>>> restraints = sr.restraints.bond(mols,
... atoms0=mols[0][0],
... atoms1=mols["water and element O"])
>>> print(restraints)
BondRestraints( size=630
0: BondRestraint( 0 <=> 22, k=150 kcal mol-1 Å-2 : r0=13.2847 Å )
1: BondRestraint( 0 <=> 25, k=150 kcal mol-1 Å-2 : r0=10.8445 Å )
2: BondRestraint( 0 <=> 28, k=150 kcal mol-1 Å-2 : r0=15.9183 Å )
3: BondRestraint( 0 <=> 31, k=150 kcal mol-1 Å-2 : r0=13.5108 Å )
4: BondRestraint( 0 <=> 34, k=150 kcal mol-1 Å-2 : r0=9.18423 Å )
...
625: BondRestraint( 0 <=> 1897, k=150 kcal mol-1 Å-2 : r0=9.52934 Å )
626: BondRestraint( 0 <=> 1900, k=150 kcal mol-1 Å-2 : r0=12.7207 Å )
627: BondRestraint( 0 <=> 1903, k=150 kcal mol-1 Å-2 : r0=10.8053 Å )
628: BondRestraint( 0 <=> 1906, k=150 kcal mol-1 Å-2 : r0=6.04142 Å )
629: BondRestraint( 0 <=> 1909, k=150 kcal mol-1 Å-2 : r0=17.1035 Å )
)
Note
If the number of atoms in atoms0
and atoms1
are not equal, then
the smaller list will be extended to match by appending multiple copies
of the last atom. In this case, this duplicates the single atom in
atoms0
, meaning that this has created distance restraints between
the first atom in the first molecule and the oxygen atoms in all of
the water molecules.
Using restraints in minimisation or dynamics#
You can use restraints in a minimisation or dynamics simulation by
passing them in via the restraints
argument, e.g.
>>> restraints = sr.restraints.positional(mols, atoms="resname ALA and element C")
>>> d = mols.dynamics(timestep="4fs", temperature="25oC",
... restraints=restraints)
>>> d.run("10ps")
>>> mols = d.commit()
runs a dynamics simulation using positional restraints on the carbon atoms
of the ALA
residue, while
>>> restraints = sr.restraints.distance(mols, atoms0=mols[0][0],
... atoms1=mols[1][0], r0="5 A")
>>> mols = mols.minimisation(restraints=restraints).run().commit()
performs a minimisation where a distance restraint is applied between the first atoms of the first two molecules, pulling them to be 5 Å apart.
You can pass in a list of restraints, meaning that you can use as many as you wish during a simulation.
>>> pos_rests = sr.restraints.positional(mols, atoms="resname ALA and element C")
>>> dst_rests = sr.restraints.distance(mols, atoms0=mols[0][0], atoms1=mols[1][0])
>>> mols = mols.minimisation(restraints=[pos_rests, dst_rests]).run().commit()
>>> d = mols.dynamics(timestep="4fs", temperature="25oC",
... restraints=[pos_rests, dst_rests])
>>> d.run("10ps")
>>> mols = d.commit()
Note
It is a good idea to run minimisation before dynamics whenever you add
restraints to a system. This is because the restraints could put a lot
of energy into the system, causing blow-ups or NaN
errors.
Using restraints in the low-level API#
You can use restraints in the low-level API by passing them in as a
map
parameter using the key restraints
, e.g.
>>> omm = sr.convert.to(mols, "openmm",
... map={"restraints": [pos_rests, dst_rests]})
>>> print(omm)
openmm::Context( num_atoms=1915 integrator=VerletIntegrator timestep=1.0 fs platform=HIP )
More information about the mappable options for the low-level API can be found in the detailed guide.
Fixing atoms in place#
As well as restraining atoms, you also have the option of fixing atoms in space during the simulation. Atoms which are fixed are not moved by minimisation or the dynamics integrator. This can be useful if you, e.g. want to freeze atoms outside a radius of a ligand binding site, or if you want to minimise the solvent while keeping the solute fixed.
You can fix atoms by passing in the set of atoms to fix as the
fixed
argument to the minimisation()
or
dynamics()
functions, e.g.
>>> mols = sr.load(sr.expand(sr.tutorial_url, "ala.top", "ala.crd"))
>>> d = mols.dynamics(timestep="4fs", temperature="25oC",
... fixed=mols[0])
>>> d.run("10ps")
>>> mols = d.commit()
will run dynamics where all of the atoms in mols[0]
(the solute)
are fixed. You can pass in a molecular container containing all of the
atoms that should be fixed, or a search string, or the atom indexes
of the atoms. Here,
>>> d = mols.dynamics(timestep="1fs", temperature="25oC",
... fixed="element C")
>>> d.run("10ps")
>>> mols = d.commit()
we have run dynamics where all of the carbon atoms are fixed, while
>>> d = mols.dynamics(timestep="1fs", temperature="25oC",
... fixed=[0, 2, 4, 6, 8])
>>> d.run("10ps")
>>> mols = d.commit()
would run dynamics where atoms at indicies 0, 2, 4, 6 and 8 are fixed.
You could even use search strings to fix atoms by distances. If you do this, it is best to also add half-harmonic positional restraints that hold nearby molecules within the sphere of mobile atoms, e.g.
>>> center = mols[0].coordinates()
>>> radius = "10 A"
>>> restraints = sr.restraints.positional(mols,
... atoms=mols[f"molecules within 10 of {center}"],
... position=center,
... r0=radius)
>>> d = mols.dynamics(timestep="1fs", temperature="25oC",
... fixed=f"not molecules within {radius} of {center}")
>>> d.run("10ps")
>>> mols = d.commit()
This implements the restraints in OpenMM by setting the masses of the fixed atoms to zero. This signals the OpenMM integrator to skip over these atoms and not move them.
Note
Be careful using constraints with fixed atoms. If a constraint involves
a fixed atom and a mobile atom, then there is a high risk that the
constraint won’t be able to be satisfied during dynamics, and
Particle coordinate is NaN
error or similar will occur.
It it safest to use a small timestep (e.g. 1 fs) when constraining
only parts of molecules.
Note
While the fixed atoms are not moved by the integrator, they are still included in the energy calculation. This means that the computational cost of the simulation will still be high, and also that energies will not be conserved. Make sure you use an integrator that provides a heat bath (e.g. NVT or NPT integrators).