Tutorial: a node that runs a GROMACS minimisation
Salpa does not ship a node for every calculation. When it lacks the one you need, you write it — and this page walks the whole way, from an empty directory to your own node running on the canvas.
The goal is concrete: energy-minimise a solvated water box with GROMACS. By the end you will
have a node that takes a structure, a topology and an .mdp, runs grompp and mdrun, and
hands the minimised structure downstream.
Everything below was measured on a real run, not sketched. Where a number appears — seven steps, five platforms, an energy of −1.8 × 10⁴ kJ/mol — it came from the run that also asserts this tutorial stays true.
What you need. Python 3.9+ and
pip install salpa-cli. Salpa itself for the last section. That is all — the CLI fetches GROMACS for you.This page covers this goal. For the general rules behind each step — what a manifest field means, how parameters work, how multi-node packages are laid out — run
salpa docsto list the authoring guides that ship with the CLI, andsalpa docs <name>to read one.
1 · Make a place for it
salpa new gmx-minimize -t individual-node \
-d "Energy-minimise a solvated system with GROMACS" \
--category "Molecular Dynamics" \
--hashtags "gromacs,md,minimisation,energy,structure"Half a second later you have a package that already validates. Not a blank file — a manifest, a node class, a test, and an environment definition:
gmx_minimize/
├── meta.toml what the app shows: name, description, category, hashtags
├── pixi.toml the environment: what your code needs to run
├── core.py your science. Plain Python, no Salpa imports
├── node.py the wrapper: parameters in, results out
├── demo_data/ inputs the node can run against, unattended
└── tests/ a real test, already passingYou passed gmx-minimize and got gmx_minimize/. Kebab is the package’s name; snake is the
directory, because Python cannot import through a hyphen.
The split between core.py and node.py is the one convention worth internalising now.
core.py is your computation and knows nothing about Salpa — you can run it, test it and debug it
as ordinary Python. node.py is a thin adapter: it reads parameters, calls core.py, and returns
a result. Keeping the science out of the wrapper is what makes it testable.
Before going further, prove the scaffold works:
cd gmx_minimize
pixi run testThe first run builds an environment, so give it a moment. Then 5 passed — including an end-to-end test — on a node you have not touched yet.
salpa docs node-package-structure— every file, and what belongs in it.
2 · It needs GROMACS
Your node shells out to gmx, so the environment has to contain it:
salpa add gromacs ok gromacs 2026.3 conda-forge · 5 platform(s)
ok linux-64
ok osx-64
ok osx-arm64
no win-64 no build for gromacs
added gromacs = ">=2026" to [dependencies]
dropped win-64 from platforms, with a note saying whyThree seconds, and read what happened. It resolved the package, checked every platform your
package claims, found that GROMACS has no Windows build at all, and narrowed your platform list
— leaving a comment in pixi.toml recording why:
# salpa add: win-64 removed — no build for gromacs.
platforms = ["linux-64", "osx-64", "osx-arm64"]That is the point of the command. You are on one machine; the claim “this node runs on Windows” is not something you can test from it. The solver can answer it for every platform at once, in seconds, and it just did.
It also declines to do something:
All of these also build for
linux-aarch64, which you do not declare.salpa addnever widens your platform list for you: adding a platform is a claim about testing, not only about solving.
Declaring a platform is a promise, not a solve. The tool will take one away on evidence and will never add one on your behalf.
salpa docs dependencies-and-platforms— whypixi.tomlis the only file that installs anything, and what the other places you might write a dependency actually do.
3 · Write the science
Put the computation in core.py. This is the version most people write first:
import os
import subprocess
def minimize(gro_file, top_file, mdp_file, output_dir, run_label="em"):
os.makedirs(output_dir, exist_ok=True)
tpr = os.path.join(output_dir, run_label + ".tpr")
rc, out = _run(f"gmx grompp -f {mdp_file} -c {gro_file} -p {top_file} -o {tpr} -maxwarn 10",
cwd=output_dir)
if rc != 0:
return MinimizeResult(log=out)
rc, out = _run(f"gmx mdrun -s {tpr} -deffnm {os.path.join(output_dir, run_label)} -ntmpi 1",
cwd=output_dir)
...Then point node.py at it and declare what the node runs on by default:
DEMO_CONFIG = {
"input_gro_file": "demo_data/water_box/water_box.gro",
"input_top_file": "demo_data/water_box/topol.top",
"input_mdp_file": "demo_data/water_box/em.mdp",
}DEMO_CONFIG is you telling the tools which of your own demo files goes with which parameter. It
cannot be guessed: a parameter’s type gives its shape and never its value, and nothing can tell
a .top from a .gro by looking at the type. Declare it once and both salpa smoke and your
node’s own test use it.
salpa docs node-parameters— the parameter types and how the app renders each one.
4 · Find out you were wrong
The code above works. Run it and it minimises. Ship it and it breaks for everyone who installs it — and the reason is one you cannot see from your own machine.
salpa env install
salpa smokeok GmxMinimize on demo_data/water_box/em.mdp, demo_data/water_box/topol.top,
demo_data/water_box/water_box.gro
returned: output_gro, run_label, working_path
also: refused bad input · same answer twice
safe to re-run
not path-independent — the same input read from a different directory gave a different
answer
why: the node raised when the same input was read from a different directory
(NodeException). The moved path contains a space, so check for an unquoted path in a
shell command before a hardcoded pathsmoke does not just run your node. It runs it again from a different directory — one
whose name contains a space — and this time the node raised.
Look back at the command string. f"gmx grompp -f {mdp_file} …" interpolates a path into a
shell command with nothing around it. On your machine the path is /home/you/gmx_minimize/…
and it works perfectly. Once the app installs your node, the path on macOS is:
~/Library/Application Support/…/gmx_minimize/demo_data/water_box/water_box.groApplication Support has a space in it. The shell splits the command there, grompp
receives a truncated filename, and your node fails with an error mentioning neither paths nor
quoting.
The fix is one import:
import shlex
rc, out = _run(
"gmx grompp -f {} -c {} -p {} -o {} -maxwarn 10".format(
shlex.quote(mdp_file), shlex.quote(gro_file),
shlex.quote(top_file), shlex.quote(tpr)),
cwd=output_dir)salpa smokeok GmxMinimize on demo_data/water_box/em.mdp, demo_data/water_box/topol.top,
demo_data/water_box/water_box.gro
returned: output_gro, run_label, working_path
also: refused bad input · same answer twice
safe to re-run · works from anywhereFour checks, and none of them needed to know the right answer. They follow from what a node is — run it twice and it should agree with itself; run it somewhere else and it should agree with itself; take its input away and it must not claim success. That is why they work on anybody’s node, including yours, without anyone writing down an expected result.
Read the report, do not just read the verdict. Notice that the failing run still says
okat the top. These four relations report rather than refuse — they are new enough that blocking on them could reject correct code, and a node that legitimately samples random numbers would fail two of them for good reasons. So the verdict stays with you:smokewill tell you what it found and name the likely cause, and it is your call what to do about it.The one above is not a judgement call. A node that gives a different answer from a different directory is broken, and this is the bug most likely to make it through your own testing intact.
What
smokedoes not tell you. It says the node runs and refuses bad input. It does not say the science is right. That question is yours, and the next section is where you answer it.
salpa docs testing-and-loading-your-node—validateversussmoke, and what each can see.
5 · Put it in the app
salpa push gmx_minimizeOn the shelf: gmx-minimize (1 node, linked in place)
Install it from Marketplace > Browse in the app.Open Salpa, go to Marketplace → Browse, and your package is there beside everything else. Install it and its node joins the library — nothing marks it as homemade.
Drag it onto a canvas, point it at your files, and run.
The payoff
Steepest Descents converged to Fmax < 1000 in 7 steps
Potential Energy = -1.8052e+04 kJ/molThe system started at roughly +3.4 × 10³ kJ/mol — a clashing configuration — and finished at
−1.8 × 10⁴. The minimisation resolved it in seven steps, and em.gro came back with exactly
as many atoms as it went in with.
That is your node, doing your science, in the app, on a structure you chose.
Changed your mind? salpa unpush gmx_minimize takes it back off the shelf. It will refuse while
the package is still installed, and tell you to uninstall it first — removing the source under an
installed copy would leave the app pointing at something that no longer exists.
salpa docs publishing-to-your-app— push, unpush, and what each one touches.
Where to go next
salpa dev— the same loop as a checklist that recomputes itself. It shows what is done, what is outstanding, and runs the next step for you. Useful once the shape is familiar and you stop wanting to hold the order in your head.- Build Custom Nodes — the reference companion to this page, dissecting a simpler node file by file.
salpa docs— lists every authoring guide bundled with the CLI. They are the what; this page was the how, once.
A note on what this tutorial is
Every command, output and number above comes from a walk that runs unattended and asserts its own
claims — that the minimisation converged, that the energy fell, that em.gro is a real structure,
and that all three are still true when the app runs the node from its own install path.
If any of that stopped being true, the walk would fail before this page could mislead you.