Skip to Content
Tutorial: A GROMACS Node

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 docs to list the authoring guides that ship with the CLI, and salpa 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 passing

You 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 test

The 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 why

Three 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 add never 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 — why pixi.toml is 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 smoke
ok 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 path

smoke 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.gro

Application 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 smoke
ok 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 anywhere

Four 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 ok at 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: smoke will 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 smoke does 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-nodevalidate versus smoke, and what each can see.

5 · Put it in the app

salpa push gmx_minimize
On 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/mol

The 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.