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 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.
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 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 smokex 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.groApplication 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 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. 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
okwith a failed check listed under it:smoketells 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
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
From the package directory, where every command since step 1 has run:
salpa pushOn 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/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 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.