LAMMPS from scratch · 20 min
Styles: the plug-in design
How LAMMPS can have 1800 styles without the main loop knowing any of them: every style is a C++ class that fills the same set of sockets (settings, coeff, init_one, compute, single). You follow one pair style through its sockets in the order LAMMPS calls them, then use pair_write to ask a style for its energy and force at chosen distances and match every number by hand, for three different styles.
- A base class declares the sockets; a style fills them (inheritance and virtual functions, in plain words)
- How a style registers its name, and how pair_style finds it
- The order the sockets are called: settings → coeff → init_one → compute every step
- Call single() with pair_write and match energy and force for lj/cut, lj/expand and morse by hand
1 · Understand: sockets and plugs
Think of a wall socket. Any lamp, kettle or charger works in it, because the socket fixes the shape and every device is made to fit. In LAMMPS the socket is a base class such as Pair; each pair style is a plug: a class built from Pair that fills in its functions. The main loop only ever talks to the socket.
The socket is a list of functions in src/pair.h:158. Those ending in = 0 have no default: every style must write them.
158 virtual void compute(int, int) = 0; // all forces, every step
164 virtual double single(int, int, int, int, double, ...) // one pair (default: "not available")
187 virtual void settings(int, char **) = 0; // the pair_style arguments
188 virtual void coeff(int, char **) = 0; // the pair_coeff arguments
192 virtual double init_one(int, int) { return 0.0; } // set up one type pair, return its cutoff
The plug says which socket it fits and which functions it fills, in src/pair_lj_cut.h:27:
27 class PairLJCut : public Pair { // "built from Pair"
31 void compute(int, int) override; // "my own version of compute"
32 void settings(int, char **) override;
33 void coeff(int, char **) override;
35 double init_one(int, int) override;
42 double single(int, int, int, int, double, double, double, double &) override;
| Base class (socket) | File | Styles (plugs) in 22 Jul 2025 |
|---|---|---|
Pair | pair.h | pair_style: lj/cut, morse, eam, tersoff … (734) |
Bond, Angle, Dihedral, Improper | bond.h … | harmonic, fene, opls … (209) |
KSpace | kspace.h | ewald, pppm … (37) |
Fix | fix.h | nve, nvt, npt, langevin, rigid … (440) |
Compute | compute.h | temp, pe, rdf, msd … (210) |
AtomVec, Region, Dump, Integrate, Min, Command | … | the rest of the 1807 (page B.1) |
2 · Registration: how "lj/cut" finds PairLJCut
- The header has one line:
PairStyle(lj/cut,PairLJCut);(src/pair_lj_cut.h:16). - At build time CMake collects these lines from every header of the core and the switched-on packages into one generated file,
style_pair.h. A package that is off contributes nothing: that is whylj/smooth/linearwas missing on page B.1. - When LAMMPS starts, src/force.cpp:92 turns each line into an entry of a dictionary: name → "a function that does
new PairLJCut(lmp)" (src/force.cpp:41). pair_style lj/cut 2.5looks the name up (src/force.cpp:270). Found: the class is created and itssettings()receives2.5. Not found: the "Unrecognized pair style" error.
So adding a new style means: write one class, add one PairStyle line, rebuild. No other file of LAMMPS changes. Part E does exactly that.
3 · The order LAMMPS calls the sockets
Step through the life of pair_style lj/cut in in.melt.
4 · Modify: ask a style for E and F with pair_write
pair_write calls single() at evenly spaced distances and writes r, E, F to a file. It needs only a box, no atoms. Choose a plug, change its numbers, and predict the table by hand. The hand side below runs the same arithmetic as the C++ init_one() and single().
A surprise: lj/expand with shift yes
Choose lj/expand, tick shift yes and run. The energies do not match. The hand side subtracts the energy at the cutoff, which is what shift yes is meant to do (the manual: the energy goes to 0 at the cutoff). For lj/expand the cutoff is r = rc + Δ, where r − Δ = rc, so the offset should be 4ε[(σ/rc)¹² − (σ/rc)⁶].
The code computes something else (src/pair_lj_expand.cpp:239):
239 double ratio = sigma[i][j] / (cut[i][j] + shift[i][j]); // σ/(rc + Δ), not σ/rc 240 offset[i][j] = 4.0 * epsilon[i][j] * (pow(ratio, 12.0) - pow(ratio, 6.0));
Tick "use the offset exactly as written" and the hand values match LAMMPS to 15 digits. With ε = σ = 1, Δ = 0.2, rc = 2.3, the shifted energy just inside the cutoff (r = 2.4999) is −0.0105 instead of 0, while lj/cut goes to 0 as it should. The forces are not affected, only the reported energy. The same line is in the current development version. This is what a hand calculation is for: it finds the one line where the code and the formula disagree.
What the sockets show
- The last row is always 0 when r reaches the cutoff: LAMMPS calls
single()only if r² < rc² (src/pair.cpp:1943). At exactly rc the pair does not interact. shift yessubtracts one constant, computed once ininit_one(), from every energy. The force column does not change at all: a constant has no slope.- The F column is the force itself. Inside
compute()LAMMPS uses F/r (calledfpair), so it can multiply by Δx, Δy, Δz without a square root;pair_writemultiplies back by r. - Three different plugs, one socket:
pair_write,compute()'s callers and the rest of LAMMPS never needed to know which one was in.
Your turn
1. lj/cut with ε = 1, σ = 1, rc = 2.5, shift yes. Predict E at r = 1.0 before running.
2. For morse, which number in the table tells you r₀ without looking at the input?
3. A new style forgets to write init_one(). What goes wrong?
settings, coeff and compute is forced by "= 0"; init_one is not forced but is needed in practice.