Tutorial: Solvating Caffeine in Saline Water, Force Field Included#
This tutorial solvates 3 caffeine molecules in a sodium chloride solution. The molecule is read from a moltemplate file produced by the Automated Topology Builder, so it arrives with its topology and its force-field assignment already attached: bonds, angles, dihedrals, impropers, GROMOS atom-type keys and ATB partial charges. Nothing has to be guessed afterwards.
Prerequisites#
from cemd import AtomicSystem
from cemd.build import SolutionBuilder
Step 1: Read the molecule and its topology#
caffeine = AtomicSystem.from_file("caffeine.lt")
print(caffeine)
print(caffeine.atom_types)
<AtomicSystem with 24 atoms, 25 bonds, 43 angles, 54 dihedrals, 8 impropers>
['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'H1', 'H10', 'H2', 'H3',
'H4', 'H5', 'H6', 'H7', 'H8', 'H9', 'N1', 'N2', 'N3', 'N4', 'O1', 'O2']
The connectivity is complete straight away – compare this with a
structure fetched from PubChem, which carries coordinates and bonds but no
angles or dihedrals and would need
guess_connections().
Each atom is its own type (C1, C2, … rather than a single C),
because the ATB assigns a distinct partial charge to every position:
print(f"{caffeine.total_charge:+.4f} e")
print(caffeine.charges["C1"], caffeine.charges["N1"], caffeine.charges["O1"])
+0.0000 e
0.536 -0.358 -0.583
Step 2: Inspect what the file already provides#
The moltemplate file also names, for every atom and every interaction, the force-field entry it should use – the ff keys. It does not carry the parameters themselves:
print(len(caffeine.ff_keys.atom), len(caffeine.ff_keys.bond),
len(caffeine.ff_keys.angle), len(caffeine.ff_keys.dihedral))
print(list(caffeine.ff_keys.atom.items())[:3])
print(len(caffeine.ff_params.bond), len(caffeine.ff_params.angle))
24 25 43 54
[('C1', 'gromos.CAro'), ('C2', 'gromos.COpt'), ('C3', 'gromos.CAro')]
0 0
Keys but no parameters: that gap is what
set_ff_from_database() fills in
at step 5, once the whole system is assembled.
Step 3: Describe the solution#
Ask for 3 caffeine molecules and 10 Na+ / 10 Cl- at a
target density of 1.0 g/cm³. Species that are not plain elements are
supplied through structures:
blueprint = SolutionBuilder(
density=1.0,
counts={"caffeine": 3, "Na": 10, "Cl": 10},
structures={"caffeine": caffeine},
)
print(blueprint)
<SolutionBuilder>
┌─ Composition
│ density: 1.00 g/cm³
│ type: counts
│ caffeine: 3
│ Na: 10
│ Cl: 10
│
├─ Custom structures
│ caffeine: 24 atoms
Step 4: Build the box#
The builder works out how much water is needed to reach the target density once the solutes are accounted for, then packs everything with Packmol:
system = blueprint.build(box=[30.0, 30.0, 30.0])
print(system)
print(system.get_count("Ow"), "water molecules")
<AtomicSystem with 2606 atoms, 1751 bonds, 967 angles, 162 dihedrals, 24 impropers>
838 water molecules
The caffeine topology was replicated for each copy: 3 × 54 = 162 dihedrals and 3 × 8 = 24 impropers survive the packing, and so do the ATB charges.
print(system.charges["C1"], system.charges["Ow"], system.charges["Hw"])
0.536 -0.892 0.446
Step 5: Give the ions their charge#
Monoatomic species are generated on the fly by the builder, so they come out neutral – the builder has no way to know which oxidation state you mean. Set them explicitly:
system.set_charges({"Na": 1.0, "Cl": -1.0})
print(f"{system.total_charge:+.4f} e")
+0.0000 e
Warning
Check this before running anything. A silently neutral Na+ is easy to miss and produces a physically meaningless trajectory.
Step 6: Resolve the force-field parameters#
With the ff keys already in place, the database lookup needs no arguments:
system.set_ff_from_database()
print(len(system.ff_params.bond), len(system.ff_params.angle),
len(system.ff_params.dihedral), len(system.ff_params.improper))
print(list(system.ff_params.bond.items())[:1])
26 43 42 8
[('C1-C3', HarmonicBondParams(k=500.678776290631, r0=1.38, ref='', model='gromos'))]
Every bond, angle, dihedral and improper type in the box is parameterized, and the partial charges are untouched – GROMOS defines its charges per atom in the topology rather than per atom type, so the lookup leaves them alone.
Step 7: Export#
system.view()
system.write("caffeine_nacl.data")
Note
Starting from PubChem instead.
from_pubchem() fetches a 3D
structure – from_pubchem(2519) for caffeine, or no argument to
browse – which is the quickest route when you only need geometry. It returns atoms and bonds only, so you then have to call
guess_connections() for the
angles and dihedrals and assign the force-field keys yourself. The
moltemplate route used above skips both steps.