Whitepaper Extensions - ULJ-Yale/qunexsdk GitHub Wiki
QuNex can be extended. You can write your own commands — in Python, in MATLAB, or as
bash scripts — put them in a folder of their own, and have them appear as QuNex commands
alongside the ones the suite ships with. They are run the same way, they take parameters
the same way, they are logged the same way, and they can be sent to a scheduler the same
way. A command you wrote this morning is invoked as qunex my_command --parameter=value
this afternoon.
This paper explains how that works, end to end: what an extension is made of, how QuNex finds one, how your commands and their parameters are declared, what happens on the way from a command line to your function, and how an extension can replace a command that QuNex itself provides.
It is written for developers who are about to write or maintain an extension, and it assumes no previous acquaintance with the extension code. Two shorter documents sit alongside it. Extensions is the working guide — the same material condensed to what you need while writing one. Command registry and command types describes the docstring format and the command types in detail, and everything it says applies to an extension's commands exactly as it does to the suite's own.
Everything described here is the Python implementation, which is where every QuNex command is dispatched.
An extension is a directory whose name begins with qx_, placed somewhere QuNex looks.
Inside it are up to three folders of code — python, matlab and bash — plus a couple
of optional ones. That is the whole of it. There is no manifest to write, no
registration call to make, and no installation step beyond putting the folder in place
and running one command.
What makes a function inside that folder a QuNex command is its docstring. A
function whose docstring carries a .. qx_command: block is a command; a function
whose docstring does not is ordinary code that your commands can call. Nothing else
distinguishes the two, which means that turning a working function into a QuNex command
is a matter of writing its documentation properly.
Because your commands are described by the same declarations as the suite's own, they inherit everything QuNex does around a command: the parameter tiers, the batch file, the per-run logging, the scheduler, the parallelisation over sessions. You write the part that is yours, and the framework around it is already there.
Almost everything that can go wrong with an extension comes from misunderstanding this section, so it is worth reading slowly. There are only two conditions, and both are simple.
QuNex has to be able to see the folder. Every time it runs, QuNex looks in a small,
fixed set of places — its search roots — and takes every qx_* folder it finds there
to be an extension. Section 3 lists the roots.
The registry has to know your commands. A registry is the list of what a body of
code offers, built by scanning that code for qx_command docstrings. The suite has one;
your extension gets one of its own, a file called qx_commands.yaml sitting in the
extension folder, written by qunex build_qx_extensions. Every time QuNex runs, the
suite's registry and every extension's registry are loaded and merged into one list, and
that merged list decides whether qunex my_command is a command at all.
Everything else follows from those two. A registry is read from a known place, so QuNex
knows which folder each command came from, and it uses that to find the code: your
Python modules are put on the Python path, your MATLAB folder is added to MATLABPATH
for the duration of a MATLAB call, and your bash scripts are looked up under your own
bash folder. Your extension's bin folder goes on PATH, and <EXT>PATH, <EXT>LIB
and <EXT>BIN are set, for the QuNex process and everything it starts. None of that
needs anything set up in advance — putting the folder somewhere QuNex looks is the whole
of the installation.
So there is one habit to keep, and this paper returns to it repeatedly:
Run
qunex build_qx_extensions --extensions=<name>after you add, rename or re-document a command. Nothing rebuilds the registry for you, and the registry — not your source — is what QuNex reads. Section 7 covers the command and how to name what it should build.
Sourcing qunex_environment.sh also walks the extensions, and it is worth knowing what
that adds. It exports the same per-extension variables into your shell rather than
into QuNex's, puts each bin folder on your interactive PATH, and prints a report of
what it found. That report is a useful check, and the shell variables are convenient
when you are working on an extension by hand — but QuNex does not depend on any of it.
This matters most inside a container, where the environment is sourced once at start-up
and cannot be sourced again: see Extensions in a
container.
QuNex searches three places, in this order:
-
$QUNEXPATH/qx_extensions— inside the QuNex installation itself; -
$TOOLS/qx_extensions— beside it, in the folder that hosts the installation; - every folder listed in the environment variable
QUNEXEXTENSIONSFOLDERS, which is a colon-separated list, in the style ofPATH.
Each of these is a search root — a folder that contains extensions, not an extension itself. A root holding two extensions looks like this:
/opt/software/qx_extensions/ # a search root
├── qx_example/ # one extension
└── qx_mylab/ # another
Any subdirectory of a root whose name starts with qx_ is taken to be an extension. The
part after qx_ is the extension's name: qx_example is the extension example.
The first two roots are always searched and need nothing set up. The third is how you keep an extension outside the QuNex installation — under your home directory, on a shared filesystem, wherever the code you maintain actually lives:
# a colon-separated list, like PATH; it just has to be exported in the shell
# you run QuNex from
export QUNEXEXTENSIONSFOLDERS="$HOME/qunex_extensions:/data/shared/qx_extensions"
# QuNex reads it on every call -- nothing has to be re-sourced
qunex example_hello --example_name=QuNexIf you name a folder that does not exist, QuNex says so and carries on with the rest:
WARNING: extensions folder '/data/shared/qx_extensions' does not exist or is not a folder, skipping it.
The order of the roots matters when the same extension name appears in more than one of them: the later root wins. That gives you a way to try a modified copy of a shared extension without touching the shared one.
An older spelling of this variable,
QUNEXEXTENSIONFOLDERS— without the secondS— is still read, so an installation that sets it keeps working. It prints a notice namingQUNEXEXTENSIONSFOLDERSas the spelling to move to, and a folder named under both is registered once.
Sourcing qunex_environment.sh walks the same roots and reports what it found:
QuNex extensions identified
---> Registering extension qx_example
... setting QXEXAMPLEPATH to '/opt/software/qx_extensions/qx_example'
... setting QXEXAMPLEBIN to '/opt/software/qx_extensions/qx_example/bin'
... added /opt/software/qx_extensions/qx_example/bin to PATH
... added /opt/software/qx_extensions/qx_example/python to QXEXTENSIONSPY
... added /opt/software/qx_extensions/qx_example/matlab to MATLABPATH
This listing is the quickest confirmation that a search root is set the way you think it is, and the per-folder lines tell you which parts of your extension were recognised. Read it rather than scrolling past it — but do not treat its absence as the problem. QuNex finds an extension by itself, so a command from an extension the environment never saw still runs.
The concrete difference is whose environment gets the variables. When you source the
script, they are set in your shell, which is what makes an extension's bin scripts
callable by name at your own prompt. When QuNex runs, it sets the same variables inside
its own process, which is what makes them available to your command's code and to
anything that code starts. Neither can set the other's.
Three variables are set per extension for your own use, named after the extension folder
with the underscores removed and the rest upper-cased — qx_example gives QXEXAMPLE:
| Variable | Set to |
|---|---|
<EXT>PATH |
the extension folder itself |
<EXT>BIN |
its bin folder, when it has one |
<EXT>LIB |
its lib folder, when it has one |
So qx_example's code can refer to $QXEXAMPLEPATH/templates/mask.nii.gz and find its
own data wherever the extension has been installed.
QuNex is often run from a container, and containers have one property that shapes how an extension is deployed: the QuNex environment is sourced once, when the container starts, and sourcing it again does nothing. An extension installed inside a running container therefore cannot be picked up by the environment script, no matter how many times you source it.
QuNex works anyway, for exactly the reason section 2
gives — it finds the extension itself, and resolves your Python, MATLAB and bash code
from the registry rather than from the environment. Everything QuNex runs works. The one
thing that does not is calling an extension's bin script by name at the container's
interactive prompt, since that shell's PATH was fixed before the extension existed.
Add the folder yourself if you want it there:
export PATH=/path/to/qx_example/bin:$PATHThe tidier arrangement, and the one to prefer, is to prepare the extension outside the container and bind it in when the container starts. Build its registry on the host:
# the variable names the folder that holds qx_example; build_qx_extensions
# leaves the installation's own registry alone
QUNEXEXTENSIONSFOLDERS=/path/to/extensions \
qunex build_qx_extensions --extensions=exampleThe registry that writes holds only paths relative to the extension, so the same file
serves at whatever mount point the container gives it. Then bind the folder that holds
the qx_* folders onto $TOOLS/qx_extensions, which inside a container is
/opt/qx_extensions — one of the two roots QuNex always searches, so no environment
variable is involved at all:
qunex_container --interactive \
--container=/path/to/qunex_suite-latest.sif \
--bind=/path/to/extensions:/opt/qx_extensionsBecause the extension is in place before the container starts, the container's own
sourcing of the environment registers it, and everything works — including the bin
scripts at the interactive prompt.
Preparing the extension on the host suits a finished extension being deployed. While you are still writing one, it is easier to bind the folder in and work inside the container, rebuilding as you change things:
# the extension lives on the host and is bound in, so edits and rebuilds
# both land on the host and survive the container
qunex build_qx_extensions --extensions=example
qunex example_hello --example_name=ContainerThis works because an extension's registry is written beside its own code. The code is on a bound folder, so the registry lands there too, and the next container you start already has it. Nothing has to be copied back out.
The one thing you cannot rebuild from inside is the suite's registry. It lives on the
container image, which is mounted read-only, so qunex build_qx_registry cannot write it.
Rather than failing on whatever the filesystem reports, it says what has happened and what
to do instead:
---> ERROR in completing build_qx_registry:
Registry location is not writable
Could not write the registry to: /opt/qunex/qx_commands.yaml
The installation's own registry cannot be written, which is normal inside a
container: it lives on the container image, and that is read only.
Registering an extension does not need it. Build the extension instead:
qunex build_qx_extensions --extensions=<name>
Nothing about registering an extension needs that registry rebuilt, which is the reason
build_qx_extensions exists as a command of its own: the distinction between the suite's
registry and an extension's is QuNex's business, not something an extension author should
have to know about in order to say they are not touching it.
The same message appears if the extension folder itself is read-only — an extension baked into an image rather than bound in — and the answer there is to bind it in from the host, which is what the arrangement above does.
Here is a complete extension. Every entry is explained below, and none of them is
compulsory — an extension with nothing but a python folder is perfectly valid.
qx_example/
├── qx_commands.yaml # the registry, written for you by build_qx_extensions
├── python/
│ ├── qx_modules # optional: modules QuNex imports on every call
│ ├── qx_example_options.py # the parameters the commands take
│ └── qx_example_commands.py# the commands themselves
├── matlab/
│ ├── matlabpaths # optional: further folders to put on MATLABPATH
│ └── example_matlab_greet.m
├── bash/
│ └── example_bash_greet.sh # bash scripts that are QuNex commands
├── bin/
│ └── example_helper.sh # plain scripts, on PATH, not QuNex commands
└── lib/ # anything else your code needs; exported as <EXT>LIB
qx_commands.yaml is generated. You never write or edit it, and you do not need to
read it, although it is plain YAML and reading it is a good way to check that a command
was registered the way you meant. It is described in
section 7.
python/ holds Python commands and Python library code. The folder is put on the
Python path for you, and qx_modules — an optional file naming the modules QuNex should
import on every call — is covered in section 6.
matlab/ holds MATLAB functions. The folder is added to MATLABPATH as a whole, so
every function in it is callable from MATLAB whether or not it is a QuNex command. If
your MATLAB code is organised into subfolders, list them — one per line, relative to the
matlab folder — in a file called matlabpaths, and they are added too.
bash/ holds bash scripts that are QuNex commands, and bin/ holds bash scripts
that are not. The distinction is genuine and is explained in
section 8.
lib/ has no special meaning beyond being exported as <EXT>LIB. Templates,
reference data, compiled helpers — anything your commands need at run time can live
there.
A function becomes a QuNex command by carrying a .. qx_command: block in its
docstring. The format is the same one the suite's own commands use, and Command registry
and command types is where it is specified in full. This section
covers what you need to write one; read that page as well before writing many.
A registrable docstring has four parts, and this Python command shows all of them:
def example_hello(example_name="world", example_times=1):
"""
``example_hello [--example_name=<name>] [--example_times=<n>]``
Greets whoever is named -- the smallest useful command an extension can
ship.
.. qx_command:
type: utility
Parameters:
--example_name (str, default 'world'):
Who to greet.
--example_times (int, default 1):
How many times to greet them.
Returns:
--greeting (str):
The greeting, which is also printed.
"""
greeting = "\n".join(["Hello, %s!" % example_name] * int(example_times))
print(greeting)
return greetingReading it from the top:
- The call line. The first line of the docstring, wrapped in double backticks. It is the example invocation shown in the command's help, and it is required — a docstring without one is not registered.
- The description. The paragraph immediately after the call line, up to the first blank line. It becomes the command's one-line description in listings.
-
The
qx_commandblock. A reStructuredText comment, so it does not appear in rendered documentation. It must give atype:; it may also givename:,aliases:— each of which is a second name the command answers to,qunex <alias>running it exactly asqunex <name>does — and, for MATLAB and bash,language:. Thetypeis what tells QuNex how to run the command, and the choices —utility,qa,matlab, and the threeprocessing.*types — are described on Command registry and command types. -
Parameters:andReturns:. One entry per parameter, written as--name (type[, default <value>]):with the description indented beneath it. The trailing colon on the entry line is required. These entries are not decoration: section 9 explains what QuNex does with them.
The command above is a utility command — it takes its parameters as ordinary function
arguments, does its work, and returns. A processing command is the other common
shape: QuNex hands it one session at a time, along with the merged options, and expects
a log object back.
import qx_utilities.general.log as gl
def example_greet_sessions(sinfo, options, overwrite=False, thread=0):
"""
``example_greet_sessions [... processing options]``
Greets every session in the batch file, once per session -- the shape of a
processing command, with none of the processing.
.. qx_command:
type: processing.session
Parameters:
--batchfile (str, default ''):
The batch file listing the sessions to greet.
--sessionsfolder (str, default '.'):
The path to the study sessions folder.
--example_greeting (str, default 'Hello'):
The greeting to use.
--example_shout (flag, default False):
Greet in capitals.
Returns:
--log (SessionLog):
The command's log, carrying its report and its status.
"""
# the log this command reports into; it writes its own header as it is built
log = gl.SessionLog(sinfo, options, "Example greeting")
# sinfo is this one session; options are the merged parameters for it
greeting = "%s, %s!" % (options["example_greeting"], sinfo["id"])
if options["example_shout"]:
greeting = greeting.upper()
log.step(greeting)
# the summary, and the log itself, which is what a processing command returns
return log.finish("session greeted")The signature — (sinfo, options, overwrite=False, thread=0) — is fixed, and QuNex calls
your function once per session, running several sessions concurrently when the run asks
for it. What arrives as sinfo depends on the processing.* type you chose, and
Command registry and command types sets out all three. The log object
and what to do with it are described on Logging.
Notice that example_greet_sessions declares batchfile and sessionsfolder among its
parameters even though it never reads them directly. A processing command should: it is
how the run's session list and study folder reach it, and a command that does not declare
them cannot be given them.
Python is the language QuNex itself is written in and the one an extension will most
often use. Very little is asked of you: put your modules in the extension's python
folder, and QuNex adds that folder to the Python path on every call. A command's module
is imported the first time the command is actually run, so nothing you write costs
anything until it is used.
Some of what an extension declares has to be in memory before any command runs —
notably the parameter defaults described in section 9, which
QuNex needs while it is merging the parameters for the run. qx_modules is how you say
which modules those are. It is an optional file in the extension's python folder,
listing one module name per line:
# The python modules QuNex imports when it starts. One name per line; a line
# starting with '#' is a comment, an empty line is skipped.
qx_example_options
A name may be a module (qx_example_options for qx_example_options.py) or a package
(a folder with an __init__.py). Each one is imported on every qunex call, whether
or not the command being run has anything to do with your extension.
That is the whole of its job, and it is worth being clear about what does not belong in
it. The modules holding your commands should not be listed: QuNex imports a command's
module when the command is run, so naming it here only makes every unrelated call a
little slower. And an extension that declares no parameters of its own needs no
qx_modules file at all — its commands are found, imported and run without one.
Your extension's code can import from the QuNex Python library directly:
import qx_utilities.general.log as gl # the log classes
import qx_utilities.general.exceptions as ge # CommandError, CommandFailedThe suite's python folder is already on the path by the time your module is imported,
so no path manipulation is needed. Developer utilities and classes
describes what is available.
Once the docstrings are written, build the registry. As an extension author the command
you want is build_qx_extensions, which builds the registries of the extensions you name
and leaves the QuNex installation's own registry untouched — it is faster, it works on an
installation you have no write access to, and it keeps your work out of a file that is
not yours:
# rebuild the registry of the extension called qx_example
qunex build_qx_extensions --extensions=exampleYou have to say what to build. The name is taken with or without the qx_ prefix,
several may be given separated by commas, and all means every extension QuNex can find:
qunex build_qx_extensions --extensions=qx_example,mytools
qunex build_qx_extensions --extensions=allA name that matches no extension stops the build and lists the ones that were found,
rather than quietly building nothing — a misspelt name would otherwise look exactly like
a build that worked, and only show up much later as
Requested command is not supported.
The command reports each command as it registers it, and closes with a summary of the extensions it built:
--> Leaving the core command registry as it is: /opt/qunex/qx_commands.yaml
-> registering qx_example_commands.example_hello
adding options: ['batchfile', 'sessionsfolder', 'example_greeting', 'example_shout']
-> registering qx_example_commands.example_greet_sessions
-> registering example_matlab_greet.m
-> registering example_bash_greet.sh
----------------------------------------------------------------
Registry built!
--> Built 1 extension registry:
- extension:example: /opt/software/qx_extensions/qx_example/qx_commands.yaml
--extensions=check builds nothing. It lists the extensions QuNex can see instead, each
with the folder it was found in and whether it has a registry yet, and then says how to
name them:
--> Extensions QuNex can see (2):
qx_example /opt/qx_extensions/qx_example [registry built]
qx_mytools /opt/qx_extensions/qx_mytools [no registry yet]
--> Name the ones to build:
qunex build_qx_extensions --extensions=<name>[,<name>...]
or build every one of them:
qunex build_qx_extensions --extensions=all
This is the quickest way to confirm that QuNex is looking where you think it is. An empty
listing almost always means QUNEXEXTENSIONSFOLDERS names the extension folder itself
rather than the folder containing it, and the command says as much when it finds nothing.
qunex build_qx_registry is the command underneath, and it rebuilds the suite's registry
as well as the extensions', which is what an installation needs after QuNex itself has
been changed. It takes the same --extensions to limit which extensions it covers,
--build_extensions=no to leave them out entirely, and --build_core=no to skip the
suite's registry — which is what build_qx_extensions does for you.
qunex build_qx_registry --help lists the rest.
Both commands check where the registry is going before writing it, because the two ways it can go wrong have very different answers.
If the location cannot be written, that is an error. For the suite's own registry it is also the normal state of affairs inside a container, where the registry sits on a read-only image, so the message says so and names what to run instead:
---> ERROR in completing build_qx_registry:
Registry location is not writable
Could not write the registry to: /opt/qunex/qx_commands.yaml
The installation's own registry cannot be written, which is normal inside a
container: it lives on the container image, and that is read only.
Registering an extension does not need it. Build the extension instead:
qunex build_qx_extensions --extensions=<name>
The same message appears outside a container on a shared installation you have no write access to, and for an extension whose own folder is read-only.
A registry that is already current is never rewritten, so rebuilding one that happens to live somewhere read-only is not an error — there is simply nothing to do. An extension shipped inside a container image with its registry already built rebuilds cleanly for exactly that reason.
The second case is a location that can be written but does not outlive the container — a writable scratch layer rather than a folder bound in from the host. That is a warning rather than an error: the registry is written and works for as long as the container runs, and is gone afterwards.
An extension's registry is written beside the extension, as qx_commands.yaml in
the extension folder, and it records paths relative to that folder rather than absolute
ones. It therefore travels with the extension: copy the folder somewhere else, or mount
it at a different path inside a container, and the same file still resolves. This is why
an extension can be prepared on one machine and used on another with no build step in
between.
If the build finds no commands anywhere, it says so plainly:
--> No extension registries were built: no extension was found with commands in it.
which almost always means the search root names the extension folder itself rather than the folder containing it.
Read this output. It is where a malformed docstring shows up, and the failure is quiet
by design — a function that is not a command is not an error, so a command that fails to
register is simply absent from the listing. A command is excluded when its docstring has
no call line, or no qx_command block, or no type: in that block. Warnings are printed
for each and collected at the end of the run.
Two clashes are treated more firmly. Within a single registry — yours, or the suite's — every command name and every alias must be unique, and a duplicate stops the build with an error naming both claimants. Across registries a shared name is not a clash at all; it is an override, which section 10 covers.
Rebuild after every change to a command's declaration: a new command, a removed one, a
renamed one, an edit to the qx_command block, the call line, the description, or the
Parameters: entries. Nothing rebuilds automatically, and the registry, not the source,
is what QuNex reads.
The generated file is worth one look, because it shows you exactly what QuNex now believes about your command:
- name: example_hello
aliases: []
path: qx_example_commands.example_hello # how the code is found
language: python
call: example_hello [--example_name=<name>] [--example_times=<n>]
description: Greets whoever is named -- the smallest useful command an extension can ship.
type: utility
args: # from the function signature
- name: example_name
type: str
default: world
description: Who to greet.
- name: example_times
type: int
default: '1'
description: How many times to greet them.
options: []
returns:
- name: greeting
type: str
default: null
description: The greeting, which is also printed.
origin: extension:example # which registry it came fromA command does not have to be written in Python. The docstring format is the same; only where the docstring lives and how QuNex calls the code differ.
A MATLAB command is a .m file in your extension's matlab folder, with the
qx_command block in the help comment block that follows the function line:
function [] = example_matlab_greet(name, times)
%``example_matlab_greet(name, times)``
%
% Greets whoever is named, from MATLAB.
%
% .. qx_command:
% type: matlab
%
% Parameters:
% --name (str, default 'world'):
% Who to greet.
%
% --times (int, default 1):
% How many times to greet them.
%
if nargin < 2 || isempty(times), times = 1; end
if nargin < 1 || isempty(name), name = 'world'; end
for n = 1:times
fprintf('Hello, %s! (from MATLAB)\n', name);
endThe command's name and its arguments are read from the function line, and QuNex calls
the function by name. It adds the extension's matlab folder — and whatever
matlab/matlabpaths lists beside it — to MATLABPATH for the duration of the call,
working from the registry record, so the call resolves wherever the extension is
installed. Arguments are passed positionally, in the order of the function line, and each one is
quoted according to the type you documented for it — a str is quoted, an int is not,
an empty value becomes []. Documenting the type of every argument therefore matters
more here than anywhere else: an argument with no documented type is passed through
as written, with a warning, and one badly quoted argument shifts every argument after
it.
Because the arguments are positional, give every one of them a sensible default inside
the function, as the two nargin lines above do. QuNex passes a value for every declared
argument, using an empty value for the ones the user did not name.
A bash command is a .sh script in your extension's bash folder. Its docstring is the
text inside the script's usage() heredoc, and its qx_command block must declare
language: bash:
#!/bin/bash
usage() {
cat << EOF
``example_bash_greet``
Greets whoever is named, from a bash script.
.. qx_command:
type: utility
language: bash
Parameters:
--example_name (str, default 'world'):
Who to greet.
EOF
exit 0
}
if [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then usage; fi
example_name="world"
# QuNex passes every declared parameter as --name='value'
for arg in "$@"; do
case ${arg} in
--example_name=*) example_name="${arg#*=}" ;;
esac
done
echo "Hello, ${example_name}! (from bash)"QuNex calls the script with each declared parameter as --name='value'. A bash script
has no signature for QuNex to read, so the Parameters: block is the script's
interface: a parameter the docstring does not declare is never passed, and one it
declares must be spelled exactly as the script reads it.
The two folders are for different things, and the difference is easy to trip over.
-
bash/holds QuNex commands. A script here is registered from its docstring and invoked asqunex example_bash_greet --example_name=QuNex. -
bin/holds plain scripts. The folder is put onPATH, so a script here is callable by its own name from your extension's Python, MATLAB or bash code, and from anything those start. It is not scanned for docstrings and is not a QuNex command.
Use bin for the helper that your own command shells out to, and bash for the script
that is the command.
QuNex puts bin on the PATH of its own process, which covers every command it runs.
Getting it onto the PATH of the terminal you are typing in is a separate thing, and
it happens when qunex_environment.sh is sourced with the extension already in place —
see section 3. Remember also that a script has to
be executable: a copy made through an editor or an archive that drops file modes fails
with Permission denied rather than command not found.
Every command declares what it accepts, and this is where an extension most often goes subtly wrong, so it is worth being precise about the two declarations involved and what each one does.
The Parameters: block of a command's docstring, together with its function signature,
is the definitive list of what that command may be given. QuNex uses it on every run:
-
For a utility command it is a gate. A parameter that is neither in the signature nor among the run-wide parameters QuNex itself understands is rejected before anything is logged:
ERROR: Extra argument bogus is not valid! Please check your command! -
For every command it is what narrows the parameter tiers. QuNex merges defaults, the batch file header and the command line into one set of parameters for the run, and what your command is actually called with is the part of that set your command declares.
-
For MATLAB and bash commands it is the whole interface, since there is no Python signature to read.
-
It is also what the parameter report prints before every run, so a declared parameter is a parameter the user can see the value and the origin of.
For a Python command, parameters that appear in the docstring but not in the signature
are attached to the command as options — but only if the signature has an options
argument, which processing commands do and utility commands do not. A utility command
that documents a parameter it does not take gets a build warning and the parameter is
dropped.
The docstring declares the parameter; it does not give it a value. That is arglist,
a list in a module named in your qx_modules:
# qx_example_options.py -- listed in qx_modules, so QuNex imports it at start-up
def is_set(value):
"""Reads a flag's value: the flag itself, or the usual spellings of yes."""
return value in (True, "yes", "true", "TRUE", "True", "1")
# each entry is [name, default, converter], and may carry a description as a fourth
arglist = [
["# ---- qx_example settings"], # a one-element entry is a heading
["example_name", "world", str], # default "world", read as a string
["example_greeting", "Hello", str, "The greeting to use."],
["example_times", 1, int], # "3" on the command line becomes 3
["example_shout", False, is_set], # read by the function above
]Each entry gives three things: the parameter's name, its default, and a callable that converts what was written into what the command should see. A fourth element, a description, is accepted and may be written for the reader's benefit; QuNex ignores it. A one-element entry is a heading, used only to group the listing.
The converter is usually str, int or float, and it can be any function of one
argument, as is_set shows. It has to be something QuNex can call: a type annotation
such as Optional[str] looks like a type but cannot be applied to a value, and QuNex
reports it and leaves the parameter as it was written rather than failing the run:
WARNING: parameter `example_times` declares `Optional[int]` as its convert function,
which can not be called. The parameter keeps the value it was given, unconverted.
What arglist buys you is the default and the conversion. A parameter with an arglist
entry has a value on every run, whether or not anybody named it, and that value has been
through the converter. A parameter without one has a value only when somebody names it,
and that value is the string they typed.
For a utility command this rarely matters: the parameter is a function argument with a default in the signature, and that default applies. For a processing command it matters a great deal, because a processing command reads its parameters out of the options dictionary:
greeting = options["example_greeting"] # KeyError, if nothing declared a defaultSo the rule is: a parameter that a processing command reads from options needs an
arglist entry. Declare it in the docstring so the command accepts it and the user can
see it; declare it in arglist so it is always there.
A flag is a parameter given without a value. Declare it in flaglist, alongside
arglist, as the name and the value the flag sets:
# `--example_shout` means `--example_shout=True`
flaglist = [
["example_shout", True],
]Give a flag an arglist entry as well, so that it has a value — normally False — on
the runs where it is not given.
The same module can carry a few more lists and dictionaries, all optional, all collected across every extension QuNex loads. They are how an extension takes part in the bookkeeping the suite does for its own parameters:
| Name | Kind | What it does |
|---|---|---|
arglist |
list | parameter defaults and converters |
flaglist |
list | parameters that may be given without a value |
extra_parameters |
list | parameters accepted by every command, not just yours |
deprecated_commands |
dict | old command name → the name that replaces it |
deprecated_parameters |
dict | old parameter name → its replacement |
deprecated_values |
dict | old parameter value → its replacement |
to_impute |
list | parameters whose value is filled in from another when unset |
towarn_parameters |
dict | parameters that produce a warning when used |
logskip_commands |
list | commands that write no log |
Most extensions need only the first two.
When the registries are merged, commands are keyed by name, and the extension's entry
wins. An extension that ships a command called check_study replaces the QuNex
command of that name for every run in that environment:
$ qunex check_study
...
THIS IS THE EXTENSION'S check_study, not the core one.
This is deliberate, and it is the intended way to change a command's behaviour for a
study, a site or an experiment without modifying the QuNex installation. Nothing is
altered on disk: the suite's command is untouched, and removing the extension — or
removing its root from QUNEXEXTENSIONSFOLDERS — restores it.
Three things follow, and the third is the one to remember.
Your replacement is a whole command. It has its own docstring, its own parameters and its own type, and it need have nothing in common with the command it replaces beyond the name. QuNex will run it exactly as the declaration describes.
Overriding applies between extensions too. Two extensions offering the same command name are resolved by search-root order, later winning — which is what makes a local copy of a shared extension a usable way to test a change.
An override announces itself in the run's own record. Above the parameter table — once per run, not once per session — QuNex says which extension the command came from, and, when it stands in for a command of the same name, what it replaced:
---> Command check_study is provided by extension example, replacing the core command of the same name
---> Parameters for check_study
...
A command that comes from an extension without replacing anything gets the first half of that line on its own:
---> Command example_hello is provided by extension example
and a command from the suite itself prints no such line at all. Because the line goes into the run's log as well as onto the screen, a study whose results differ from another's leaves an answer behind rather than a mystery.
Even so, override sparingly and deliberately. To see what an override actually does before running it, ask for its help — that renders the docstring of whichever implementation is in effect:
qunex check_study --helpThe extension used throughout this paper is real and is shipped with this wiki, in
Examples/qx_example. This section installs it and runs everything in it.
# clone this wiki, if you have not already
git clone [email protected]:ULJ-Yale/qunexsdk.wiki.git
# put the extension in a search root of your own -- a folder that holds
# qx_<name> folders, not the extension folder itself
mkdir -p $HOME/qunex_extensions
cp -r qunexsdk.wiki/Examples/qx_example $HOME/qunex_extensions/
# tell QuNex where to look
export QUNEXEXTENSIONSFOLDERS="$HOME/qunex_extensions"That is the installation. The example ships with its registry already built, so its commands are available immediately:
qunex --a | grep example- example_bash_greet: Greets whoever is named, from a bash script.
- example_greet_sessions: Greets every session in the batch file, once per session.
- example_hello: Greets whoever is named -- the smallest useful command an extension can ship.
- example_matlab_greet: Greets whoever is named, from MATLAB.
If nothing comes back, the search root is wrong — QUNEXEXTENSIONSFOLDERS has to name
$HOME/qunex_extensions and not $HOME/qunex_extensions/qx_example.
The shipped registry describes the example as it stands. The moment you change a docstring — which you will, as soon as you use the example as a starting point — rebuild it:
qunex build_qx_extensions --extensions=exampleIts output is the listing shown in section 7: four commands registered, one extension registry written, and the QuNex installation's own registry left alone.
qunex example_hello --example_name=QuNex --example_times=2--- Full QuNex call for command: example_hello
qunex example_hello --example_name="QuNex" --example_times="2"
---------------------------------------------------------
---> Command example_hello is provided by extension example
---> Parameters for example_hello
parameter source value
------------------------------------
example_name command line QuNex
example_times command line 2
------------------------------------
started running example_hello at 2026-08-21 16:25:07, track progress in
/home/user/qunex_logs/2026-08-21_16.25.07_example_hello/comlogs/tmp_example_hello_....log
call: gmri example_hello example_name="QuNex" example_times="2"
-----------------------------------------
Hello, QuNex!
Hello, QuNex!
-----------------------------------------
Finished at 2026-08-21 16:25:07
---> Successful completion of task at 2026-08-21 16:25:07
Everything around your two lines of output — the line naming the extension the command came from, the call echo, the parameter table with the origin of each value, the comlog, the closing status — is what the command inherited by being a QuNex command. None of it is in the extension's code.
qunex example_greet_sessions \
--sessions="s01,s02" \
--sessionsfolder=/data/study/sessions \
--example_greeting=Hi \
--example_shout---> Command example_greet_sessions is provided by extension example
---> Parameters for example_greet_sessions
parameter source value
---------------------------------------
batchfile default
example_greeting command line Hi
example_shout command line True
overwrite default False
sessionsfolder command line /data/study/sessions
---------------------------------------
Starting multiprocessing sessions in s01,s02 with a pool of 1 concurrent processes
Starting processing of session s01 at Friday, 21. August 2026 16:25:51
------------------------------------------------------------
Session id: s01
[started on Friday, 21. August 2026 16:25:51]
Running Example greeting [HCPStyleData] ...
---> HI, S01!
Example greeting completed on Friday, 21. August 2026 16:25:51
------------------------------------------------------------
...
---> Final report for command example_greet_sessions
2 run, 2 successful, 0 failed, 0 did not complete
Successful:
... s01 ---> session greeted
... s02 ---> session greeted
---> Successful completion of all tasks
The session loop, the concurrency, the per-session log with its header and footer, and
the final report across sessions are all provided. The extension supplied one line of
greeting per session, and --example_shout — the flag declared in flaglist and given a
default by arglist — arrived as True and was read by the extension's own converter.
qunex example_matlab_greet --name=QuNex --times=2Running:
>>> example_matlab_greet('QuNex', 2)
Hello, QuNex! (from MATLAB)
Hello, QuNex! (from MATLAB)
---> Successful completion of task
The line beginning >>> is the MATLAB call QuNex composed from your declaration: the
arguments in the order of the function line, the string quoted and the integer not,
exactly as their documented types said.
qunex example_bash_greet --example_name=QuNexRunning:
>>> bash /home/user/qunex_extensions/qx_example/bash/example_bash_greet.sh --example_name='QuNex'
Hello, QuNex! (from bash)
---> Successful completion of task
Here too the >>> line shows what QuNex composed: the script found under the
extension's own bash folder, and each declared parameter passed as --name='value'.
And the script in bin, which is not a QuNex command, is on the path of anything QuNex
runs — so a command of yours can simply call it by name:
example_helper.shexample_helper.sh: Fri Aug 21 16:27:59 UTC 2026
At your own shell prompt it is on the path only if qunex_environment.sh was sourced
with the extension already in place; otherwise add
$HOME/qunex_extensions/qx_example/bin to PATH yourself.
Four failures account for most of the trouble, and each has a distinctive symptom.
"Requested command is not supported."
ERROR: example_hello ---> Requested command is not supported. Refer to general QuNex usage.
The registry does not have your command. Either build_qx_registry has not been run
since you wrote it, or it was run and your command was not registered — check the build
output for your command's name, and check the docstring for the call line and the
qx_command block with its type:. If no extension at all was found, the search root is
the thing to check: QUNEXEXTENSIONSFOLDERS must name the folder containing
qx_example, not qx_example itself.
ModuleNotFoundError
ModuleNotFoundError: No module named 'nibabel'
The command was found and dispatched, and something it imports was not there. Read the module name in the message: if it is one of your own, the registry is describing source that has since been renamed or moved, and a rebuild fixes it. If it is a third-party package, your extension depends on something the QuNex installation does not have, and it has to be installed into the Python environment QuNex runs under.
KeyError on one of your own parameters
File ".../qx_example_commands.py", line 76, in example_greet_sessions
greeting = "%s, %s!" % (options["example_greeting"], sinfo["id"])
KeyError: 'example_greeting'
A processing command read a parameter out of options that has no arglist entry, on a
run where nobody named it. Add the entry, and check that the module holding it is named
in qx_modules — a declaration in a module QuNex never imports is a declaration that
never happens.
A parameter is rejected
ERROR: Extra argument bogus is not valid! Please check your command!
A utility command was given a parameter it does not declare. Add it to the signature and
to the Parameters: block, and rebuild the registry — the registry, not the source, is
what the check reads, so a parameter you added this minute is not accepted until the
build has run.
Before asking why an extension does not work, walk this list:
- Is the extension folder named
qx_something, and is it inside a search root rather than being one? - Is
QUNEXEXTENSIONSFOLDERSexported in the shell you are runningqunexfrom, and does QuNex report no warning about the folder it names? - Has
qunex build_qx_extensions --extensions=<name>been run since the last docstring change, and did it list your command? If you are unsure QuNex can see the extension at all,qunex build_qx_extensions --extensions=checklists what it finds. - Does every command's docstring have a call line, a
qx_commandblock and atype:? - Does every parameter a processing command reads from
optionshave anarglistentry, in a module named inpython/qx_modules? - For MATLAB commands, does every argument have a documented type?
- For bash commands and
binscripts, is the file executable?