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+, pip install salpa-cli, and pixi , which builds the environments. The Salpa app installs a pixi of its own, so if you have the app you have one. You need the app itself only for the last section, and 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.

If your shell answers command not found: pixi, the pixi you have is the app’s, and it is not on your PATH. salpa new printed where it is: use that path in place of pixi, or add its directory to PATH.

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
x GmxMinimize — execute() raised on its input: bocoflow_core.node.NodeException: gmx_minimize: Minimisation failed: … In command-line option -p File '/…/salpa-stage-…/Application' does not exist or is not accessible. … In command-line option -p File 'Support/gmx_minimize/demo_data/water_box/topol.top' does not exist or is not accessible. … why: it fails from a path containing a space, as the app installs it, and works from where you keep it, so the space is the cause. It also fails when only its input is moved under such a path, so look for an unquoted path in a shell command (use shlex.quote, or pass an argv list) smoke supplied: input_gro_file=water_box.gro, input_top_file=topol.top, input_mdp_file=em.mdp, output_dir=salpa-smoke-case-…, run_label=em ran with .pixi/envs/test/bin/python · from an install path containing a space Some nodes did not behave.

smoke does not run your node where you keep it. It copies the package into a folder whose name contains a space, as the app installs it, and runs it there, and there the node raised. From where you keep it, it works, which is how smoke knows the space is the cause.

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, and grompp receives two halves of a filename, the …/Application and Support/… in the output above. The error is GROMACS’s, about files that do not exist, and it says nothing about 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)

Quote the mdrun line’s paths the same way. They are in the output folder, which smoke names without a space, so it cannot catch them there, but a user’s folder may have one. Or leave the shell out entirely: a list of arguments, subprocess.run(["gmx", "grompp", "-f", mdp_file, …]), needs no quoting at all.

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. The first character is the verdict, and it answers one question: does the node run, from where the app puts it, and refuse bad input? The checks under it — same answer twice, safe to re-run, works from anywhere — 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 a node can say ok with a failed check listed under it: smoke tells you what it found and names the likely cause, and it is your call what to do about it.

The failure above is not a judgement call. A node that breaks in the folder the app installs it into is broken for everyone, 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-node — validate versus smoke, and what each can see.

5 · Put it in the app

From the package directory, where every command since step 1 has run:

salpa push
On the shelf: gmx-minimize (1 node, linked in place) Install it from Marketplace > Browse in the app.

The first time, it asks to move gmx_minimize/ into a packages/ directory beside it, which is the layout the app reads a package source from. Say yes. Your shell stays in the package, which is now packages/gmx_minimize/, and the next push asks nothing.

On Windows, run this first push from the folder above: cd .., then salpa push gmx_minimize. Windows will not move a folder that a running program started in, so from inside the package push stops, changes nothing and tells you this. Then cd packages\gmx_minimize, and later pushes from there ask nothing.

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