PEO-LiTFSI with AmberTools¶
Type TFSI natively with AM1-BCC charges, build GAFF PEO chains from one antechamber-typed oligomer cut by prepgen and sequenced by tleap, merge the two force fields, and pack a PEO-LiTFSI electrolyte at target density for LAMMPS.
External dependencies
This guide requires AmberTools (via conda) and
molcrafts-molpack: sqm for TFSI's AM1-BCC charges, and antechamber,
parmchk2, prepgen and tleap for the chain. Only the inputs (TFSI's atom
types, Li⁺, the PEO oligomer and its cuts) run without them; every block
marked # docs: skip needs AmberTools or molpack.
Setting up AmberTools
Install AmberTools in a dedicated conda environment:
conda create -n AmberTools25 -c conda-forge ambertools=25
conda activate AmberTools25
# Verify installation
which antechamber # should print a path
which tleap # should print a path
Am1BccModel, AmberPolymerBuilder and MolPy's AmberTools typifiers activate the conda environment automatically when running commands, so you do not need to keep it active in your shell. Which environment they activate is a setting of molpy's configuration, shared by every AmberTools program ([wrapper]; see Configure and Log a Run). Put it in molpy.toml next to your scripts:
If you use a different environment name, change it there; nothing in the code below names it.
Workflow overview¶
A small molecule takes the native route, in a builder system as on its own:
AtdTypifier stamps antechamber's GAFF2 atom types, mp.ff.charge.Am1BccModel
assigns AM1-BCC charges with sqm as its one program, and GaffTypifier
assigns the bonded terms from gaff2.dat (see
Force Field Typification). Against AmberTools these are the
types and parameter values tleap gives the same molecule, and antechamber
-c bcc's charges to within sqm's rounding (0.001 e). The PEO chain needs
the AmberTools pipeline: it is cut by prepgen and joined by tleap, and its
force field is read back from tleap's prmtop.
The two force fields merge. molrs's ForceField.merge compares what each
style prices (defaults filled in, within a relative 1e-6), and native GAFF
names its terms and rounds its estimates as tleap's prmtop does, so a type or
term the chain and TFSI share (c3) is one definition.
Every GAFF entry point takes parameter_set ("gaff" or "gaff2", as
molrs names it); it has no default, so name the same set for every
component.
A polymer is not typed as one molecule. You build one oligomer in which the head, chain and tail monomers are already bonded, and you say how prepgen cuts it (AmberCut: the atoms each residue omits, and the atom across each junction). AmberPolymerBuilder then runs antechamber and parmchk2 on that oligomer, prepgen for each cut, and tleap sequence. Li⁺ gets a force field you define. The three force fields are merged, molpack places the molecules at the target density, and LammpsEngine writes the LAMMPS deck.
TFSI is typed natively with AM1-BCC charges¶
Atom types need no program. sqm, which computes the AM1 charges, needs 3D coordinates, so the anion is embedded first.
from pathlib import Path
import molpy as mp
from molpy.ff.typifier import AtdTypifier, GaffTypifier
output_dir = Path("13_output")
output_dir.mkdir(exist_ok=True)
tfsi = mp.io.smiles.read_smiles("O=S(=O)(C(F)(F)F)[N-]S(=O)(=O)C(F)(F)F").to_atomistic()
tfsi = mp.conformer.Conformer(add_hydrogens=False, seed=42).generate(tfsi)[0]
tfsi = AtdTypifier(parameter_set="gaff2").typify(tfsi) # GAFF2 atom types
print(sorted({atom["type"] for atom in tfsi.atoms})) # ['c3', 'f', 'ne', 'o', 's6', 'sy']
Am1BccModel reads the AmberTools environment from molpy.toml
(load_config() resolves it; passing nothing does the same). The net charge
sqm is given comes from the atoms' formal charges — here the [N-] of the
SMILES:
# docs: skip — needs AmberTools (sqm)
from molpy.config import load_config
from molpy.ff.charge import Am1BccModel
config = load_config() # defaults, then ~/.molcrafts/molpy/config/config.toml, then ./molpy.toml
am1bcc = Am1BccModel(parameter_set="bcc", work_dir=output_dir / "tfsi", config=config)
charges = am1bcc.assign(tfsi) # sqm's AM1 charges, then the native BCC correction
for atom, charge in zip(tfsi.atoms, charges):
atom["charge"] = float(charge)
print(round(float(charges.sum()), 3)) # -0.998
tfsi_gaff = GaffTypifier(parameter_set="gaff2")
tfsi = tfsi_gaff.typify(tfsi) # a typed copy: bonded terms from gaff2.dat
tfsi_ff = tfsi_gaff.forcefield() # the parameters of the types just assigned
The anion carries −0.998 e, not −1: sqm prints its Mulliken charges to three
decimals, and neither the BCC correction nor antechamber renormalises
(antechamber -c bcc gives the same charges). Four of TFSI's terms
(s6-ne-sy, f-c3-sy, o-s6-ne-sy, c3-s6-ne-sy) are not in gaff2.dat;
GaffTypifier estimates them as parmchk2 does and marks them estimated.
Li⁺ needs no charge calculation — you define its force field¶
Li⁺ has no bonded terms and no partial charges to compute, and GAFF has no lithium type (GaffTypifier refuses it: gaff2.dat has no exact parameter for 1 term(s): MASS Li). Build the atom with its type and charge, and define the force field it uses.
Li⁺ nonbond parameters — Åqvist (1990), J. Phys. Chem. 94, 8021–8024, DOI: 10.1021/j100384a009. These were fitted to hydration free energies and are the standard choice for polymer electrolyte simulations with GAFF.
| Parameter | Value |
|---|---|
| Rmin/2 | 1.137 Å |
| ε | 0.0183 kcal/mol |
li = mp.Atomistic()
li.add_atom(atomic_number=3, type="Li+", charge=1.0, mass=6.94, x=0.0, y=0.0, z=0.0)
li_ff = mp.ff.forcefield.ForceField("li", units="real")
li_type = li_ff.def_style("atom", "full").def_type("Li+", mass=6.94, charge=1.0)
# sigma = 2 * Rmin/2 / 2^(1/6)
li_ff.def_style("pair", "lj/cut").def_type("Li+", li_type, epsilon=0.0183, sigma=2.0259)
The chain is a tleap sequence, cut by prepgen¶
Assembler folds the whole leaving-group charge onto the anchor, so an
ether oxygen of about −0.42 e becomes about −0.20 e. A GAFF chain is built
the way AMBER residues are made instead: antechamber types one oligomer in
which the head, chain and tail monomers are already bonded, so every atom
it types sees its chain neighbours. prepgen cuts that oligomer into a head,
a chain and a tail residue (AmberCut: the atoms each residue omits, and
the atom across each junction), spreading the omitted charge over the atoms
it keeps; the builder does not edit the prepi afterwards. tleap sequence
joins the residues.
AmberPieces writes the oligomer from three SMILES — head, repeat and tail,
each in backbone order — and the three cuts with it: each residue keeps its
own monomer and omits the other two.
pieces = mp.builder.AmberPieces(head="COCC", repeat="OCC", tail="OCCOC")
oligomer, peo_cuts = pieces.oligomer(seed=42) # CH3O(CH2CH2O)3CH3, embedded
print(oligomer.n_atoms) # 30
print(peo_cuts["chain"].head, peo_cuts["chain"].tail) # O2 C5
print(len(peo_cuts["chain"].omit)) # 23: the head and tail monomers
{[#PEO]|10} is ten residues: the head cut, eight chain cuts and the tail
cut, CH3–(OCH2CH2)10–OCH3. See Polymer Topologies for
other architectures. The site graph is only that sequence; it must be one
linear path.
# docs: skip — needs AmberTools
built = mp.builder.AmberPolymerBuilder(
{"PEO": oligomer},
{"PEO": peo_cuts},
parameter_set="gaff2",
charge_method="bcc",
work_dir=output_dir / "peo",
config=config,
).assemble(sites)
peo = built.chain # typed graph: tleap's coordinates, types, charges and terms
peo_ff = built.forcefield # units real, AMBER 1-4 scaling declared
print(f"PEO 10-mer: {peo.n_atoms} atoms") # 79
antechamber runs on the oligomer once (-fo ac: types and charges from one
sqm run), and parmchk2 and prepgen read its ac file, under
work_dir/monomers/<label>/. They are not run on the assembled chain.
Junction terms come from leaprc.gaff2. PEO and TFSI use the same
GAFF generation, so their shared type c3 agrees when the force fields are
merged. A later assemble reuses what is
in work_dir: antechamber reruns only when the oligomer or the charge
settings change, prepgen when a cut changes, and tleap when the sequence or
a file it loads changes. A step that runs deletes its old outputs first, and
the typifiers rerun every step in a directory keyed by the molecule and every
setting, so a rerun never reads an earlier run's files.
Cutting an oligomer you built yourself¶
For an oligomer you prepared elsewhere, write the cuts by atom name; they
are the prepgen control files. GroPoB's ethyl PEO oligomer PEO.ac keeps
every monomer between two dummy methyls: the chain residue omits both, the
head residue only the tail methyl (C7, H15, H16, H17), the tail
residue only the head methyl (C3, H6, H7, H8). C1 and C6 are the
connection atoms. pre_head / post_tail name the atom across a junction;
its GAFF type, read from antechamber's ac file, becomes PRE_HEAD_TYPE /
POST_TAIL_TYPE. pre_head_type / post_tail_type write a type directly.
AmberCut = mp.builder.AmberCut
head_methyl = ("C3", "H6", "H7", "H8")
tail_methyl = ("C7", "H15", "H16", "H17")
gropob_cuts = {
"head": AmberCut(tail="C6", post_tail="C7", omit=tail_methyl),
"chain": AmberCut(
head="C1",
tail="C6",
pre_head="C3",
post_tail="C7",
omit=head_methyl + tail_methyl,
),
"tail": AmberCut(head="C1", pre_head="C3", omit=head_methyl),
}
An ac and frcmod you put in work_dir/monomers/<label>/ yourself (as
<label>.ac and <label>.frcmod) are used as they are: only prepgen and
tleap run.
Merging three force fields before packing prevents type conflicts¶
Merging is done before packing rather than after because packing operates on
coordinates only — it has no awareness of force field types. If two components
share a type name with different parameters, merge raises, so a collision is
an error before coordinates are generated. The chain's force field is tleap's
prmtop and TFSI's is native GAFF; they merge like any two force fields.
merge adds the other force field's styles and types to peo_ff in place and
returns it; where both define a style, peo_ff's is kept.
# docs: skip — needs AmberTools and molpack
from molpack import GencanPack, Target
ff = peo_ff.merge(tfsi_ff).merge(li_ff) # peo_ff, now holding all three
box_size = 60.0
box = mp.core.Cuboid([0.0, 0.0, 0.0], [box_size] * 3)
targets = [
Target(peo.to_frame(), count=3).with_restraint(box),
Target(li.to_frame(), count=10).with_restraint(box),
Target(tfsi.to_frame(), count=10).with_restraint(box),
]
system = GencanPack().with_seed(12345).run(targets, max_loops=200).frame
system.box = mp.Box(mp.core.Cell.lengths([box_size] * 3))
The LAMMPS deck sets the cutoff and the long-range solver¶
Neither the prmtop nor GAFF states a cutoff: it is a run setting, so the deck
names it. generate_inputs writes the data file, the force-field settings
(coefficients, AMBER's 1-4 special_bonds, pair_modify mix), the init
(units, atom_style, boundary, pair_style) and the input script. The
pair_style given here replaces the force field's cut-off Coulomb with
coul/long, whose PPPM solver the body sets:
# docs: skip — needs AmberTools and molpack
from molpy.engine import LammpsEngine
# full atom style needs mol_id: one per connected molecule
system["atoms"]["mol_id"] = mp.Topology.from_frame(system).connected_components() + 1
deck = LammpsEngine().generate_inputs(
system,
ff,
output_dir / "lammps",
pair_style="lj/cut/coul/long 12.0",
body="kspace_style pppm 1.0e-4\nthermo 100\nminimize 1.0e-4 1.0e-6 1000 10000\n",
)
print(sorted(deck)) # ['data', 'init', 'input', 'settings']
LammpsEngine().run(deck["input"], workdir=output_dir / "lammps") runs it;
it is the starting point for the equilibration you add to body. The four
terms GaffTypifier estimated for TFSI (it marks them estimated, with the
penalty, method and analog) export like any other: every molrs writer skips
the provenance and writes the parameters.
When the AmberTools typifiers are still the route¶
The native route covers GAFF / GAFF2 typing with AM1-BCC (or ABCG2) or Gasteiger charges, merged with a chain or not. The AmberTools pipeline, read back from tleap's prmtop, is kept for what it alone does:
- A GAFF polymer chain:
AmberPolymerBuilder(prepgen cuts, tleapsequence), above. - Parameters of your own on top of the leaprc:
TleapTypifier(forcefield=...)writes them as a frcmod that tleap loads afterleaprc.<parameter_set>, over a graph whose atoms already carry AMBER types and charges.GaffTypifiertakes no extra parameters, andmergerefuses a type it would redefine. - An antechamber charge method other than AM1-BCC / ABCG2:
AntechamberTypifier(charge_method=...)passes it to antechamber-c.
Troubleshooting¶
| Symptom | Check |
|---|---|
| Antechamber fails | The oligomer has 3D coordinates (embed it first) |
ValueError: … cut names [...] |
An atom name in a cut is not on the oligomer; AmberPieces names atoms element + count (C1, O2, …) |
Chain O near −0.20 e, or typed oh |
The chain was joined with Assembler (the leaving-group charge was folded onto the anchor). Build the oligomer with the monomers already bonded and cut it with prepgen |
| TFSI charge wrong | The formal charges sum to the net charge ([N-] gives −1). −0.998 e is sqm's three-decimal rounding; antechamber -c bcc gives the same |
| tleap fails on the chain | A junction term missing from the leaprc (leaprc.gaff2 here); the error carries tleap's output. The connection atoms are the ones in the oligomer you cut |
ValueError: … still has N ports |
TleapTypifier was given a template. A polymer goes through AmberPolymerBuilder |
| Polymer assembly fails | The site graph is one path of at least two sites, and every bead type has an oligomer and a cut for the residue that site uses |
ValueError: … both make tleap residue … |
Residue names are the first characters of the bead type (HPE, PEO, TPE for PEO); give the bead types distinct prefixes |
| Force field merge conflict | Every component names the same parameter_set; a type you define yourself (Li⁺) does not reuse a GAFF type name |
| Packing fails | Increase box size or reduce molecule count |
The raw subprocess wrappers (AntechamberWrapper, Parmchk2Wrapper,
TleapWrapper, …) stay in molpy.wrapper for scripts that drive the
executables directly.
See also: Force Field Typification, Wrapper and Adapter.