Module Developer Guide
This document describes the internals of the QIT module framework for developers who need to extend, maintain, or understand the codebase. For a user-facing overview, see modules.md.
Architecture Overview
The module framework has four layers:
- Annotations (
qit.base.annot.*) — declarative metadata on classes and fields - Module interface (
qit.base.Module) — the contract every module implements - Reflection engine (
qit.base.utils.ModuleUtils) — discovers modules, extracts field metadata, validates, serializes - CLI binding (
qit.base.cli.*) — wraps modules for command-line execution with auto-generated help
┌──────────────┐
│ QitMain │ entry point, module discovery
└──────┬───────┘
│
┌──────▼───────┐
│ CliModule │ CLI wrapper, arg parsing, I/O
└──────┬───────┘
│
┌──────▼───────┐
│ ModuleUtils │ reflection, validation, field access
└──────┬───────┘
│
┌─────────▼─────────┐
│ Module + Annots │ user-written module code
└───────────────────┘
Design Philosophy
QIT modules are pure data transforms: they read from input fields, compute a result, and write to output fields. A module should not manage file discovery, iterate over subjects, handle dependencies between processing steps, or implement retry logic. These concerns belong to external orchestration tools.
This is a deliberate design choice. By keeping modules stateless and single-purpose, they remain composable across many different execution contexts:
- Command line — chain modules with shell scripts or Makefiles
- LONI Pipeline — QIT modules integrate directly with the LONI Pipeline workflow engine, which handles execution graphs, parallelism, and provenance tracking
- Workflow managers — tools like Nextflow, Snakemake, or Nipype can invoke
qit <Module> ...as a process step, managing dependencies and scheduling - qitview — the interactive viewer wraps modules in a GUI, letting users apply transforms to loaded data without writing scripts
Workflow orchestration tools evolve independently and have their own strengths (DAG scheduling, cluster support, checkpointing, provenance). By not reimplementing these capabilities inside QIT, modules stay simple, testable, and durable — they will work with whatever orchestration tools exist today or in the future.
Rule of thumb: if you find yourself adding loops over subjects, file globbing, or conditional branching inside a module, that logic should live in an external script or workflow definition instead.
Annotations Reference
All annotations are in qit/base/annot/. All use @Retention(RUNTIME) and
@Inherited.
Class-level annotations
| Annotation | Target | Purpose |
|---|---|---|
@ModuleDescription("...") |
Class or Field | Plain-text description shown in help |
@ModuleAuthor("...") |
Class | Author attribution |
@ModuleCitation("...") |
Class | Publication to cite when using the module |
@ModuleUnlisted |
Class | Hides the module from --list and help output |
Field-level annotations
Every annotated field must have exactly one role annotation:
| Annotation | Role | Typical types |
|---|---|---|
@ModuleInput |
Data consumed by the module | Dataset subclasses: Volume, Mask, Curves, Mesh, Vects, Table, Matrix, Affine, Gradients, Deformation, Neuron, Solids |
@ModuleOutput |
Data produced by the module | Same Dataset subclasses |
@ModuleParameter |
Configuration value | Double, Integer, String, Boolean, boolean, enums |
Field modifiers (combinable with a role annotation):
| Annotation | Effect |
|---|---|
@ModuleOptional |
Field may be null; not required on the command line |
@ModuleAdvanced |
Grouped separately in help output; not typically modified |
@ModuleExpert |
Only visible when --expert flag is passed |
Validation rules
ModuleUtils.validate() enforces:
- Each annotated field has exactly one role (
@ModuleInputXOR@ModuleParameterXOR@ModuleOutput) @ModuleOutputfields are not marked@ModuleAdvanced- Input/output field types are valid Dataset subclasses
Module Interface
public interface Module {
Module run() throws IOException;
}
Modules must:
- Implement a public no-argument constructor
- Declare annotated
publicfields for inputs, parameters, and outputs - Implement
run()which reads from input fields, writes to output fields, and returnsthis
The framework populates input and parameter fields before calling run(), and
reads output fields after it returns.
How Module Discovery Works
QitMain.main() → ModuleUtils.list():
- Uses
org.reflections.Reflectionsto scan the"qit"package at runtime - Finds all classes implementing
Module - Filters out inner classes and
@ModuleUnlistedclasses - Maps simple class names (e.g.
VolumeThreshold) to classes - Also discovers
CliMainimplementations for batch processing tools
A user invokes a module by name:
qit VolumeThreshold --input in.nii.gz --output out.nii.gz
QitMain looks up VolumeThreshold, wraps it in CliModule, and calls run().
CLI Binding Flow
CliModule is the adapter between Module and the command line:
1. Build CLI specification (cli() method)
For each field returned by ModuleUtils.fields(module):
- Check
@ModuleExpertvisibility (skip if not in expert mode) - Determine role from
@ModuleInput/@ModuleParameter/@ModuleOutput - Read
@ModuleDescriptionfor help text - Read
@ModuleOptional/@ModuleAdvancedfor categorization - For
booleanfields: create a flag (no argument value needed) - For
enumfields: auto-generate option list from enum constants - For Dataset fields: use the type name as the argument placeholder
- Build a
CliOptionand add it toCliSpecification
2. Parse arguments
CliValues parses --key value pairs and positional arguments from String[] args.
3. Read inputs (read() method)
For each input/parameter field:
- Get the string value from
CliValues - For Dataset types: call the type’s static
read(filename)method via reflection - For primitives: parse from string (
Double.valueOf(), etc.) - For enums:
Enum.valueOf() - For booleans: presence of the flag means
true
4. Execute
Call module.run().
5. Write outputs (write() method)
For each @ModuleOutput field:
- Get the filename from
CliValues - Call the Dataset’s
write(filename)method via reflection
Writing a New Module
Minimal example
@ModuleDescription("Scale a volume by a constant factor")
@ModuleAuthor("Your Name")
public class VolumeScale implements Module
{
@ModuleInput
@ModuleDescription("input volume")
public Volume input;
@ModuleParameter
@ModuleDescription("scale factor")
public Double factor = 1.0;
@ModuleOutput
@ModuleDescription("output volume")
public Volume output;
@Override
public VolumeScale run()
{
this.output = this.input.copy();
// ... scale voxel values by this.factor ...
return this;
}
}
This automatically provides:
qit VolumeScale --helpwith formatted help textqit VolumeScale --input in.nii.gz --factor 2.0 --output out.nii.gz- Availability in
qitviewmodule panel - JSON serialization of parameters via
--save/--load
Conventions
- Place modules in
qit/data/modules/<category>/(e.g.volume/,mask/,curves/) - Class name should be
<DataType><Action>(e.g.VolumeThreshold,MaskInvert,CurvesTransform) - Always set default values for
@ModuleParameterfields - Mark non-essential inputs with
@ModuleOptional - Return
thisfromrun()
Using @ModuleOptional inputs
@ModuleInput
@ModuleOptional
@ModuleDescription("optional mask to restrict processing")
public Mask mask;
public MyModule run()
{
for (Sample sample : this.input.getSampling())
{
if (this.input.valid(sample, this.mask)) // mask can be null
{
// process voxel
}
}
return this;
}
Using enum parameters
public enum InterpolationType { NEAREST, LINEAR, CUBIC }
@ModuleParameter
@ModuleDescription("interpolation method")
public InterpolationType interp = InterpolationType.LINEAR;
The CLI will automatically show: --interp <InterpolationType> (Options: NEAREST, LINEAR, CUBIC) (Default: LINEAR)
Batch Processing Tools
For operations across multiple subjects/files, create a CliMain implementation
in qit/main/:
@ModuleDescription("Run VolumeThreshold on many subjects")
@ModuleAuthor("Your Name")
public class VolumeThresholdBatch implements CliMain
{
public void execute(CliValues args) throws Exception
{
// Custom batch logic using args.keyed("--input"), etc.
}
}
These are discovered alongside Modules and invoked the same way:
qit VolumeThresholdBatch --help
Module Serialization
Modules can be saved and loaded as JSON:
qit VolumeThreshold --input in.nii.gz --threshold 0.3 --output out.nii.gz --save params.json
qit VolumeThreshold --load params.json --input other.nii.gz --output other_out.nii.gz
ModuleUtils.write() serializes annotated field values to JSON.
ModuleUtils.read() deserializes and populates fields.
Key Source Files
| File | Role |
|---|---|
qit/base/Module.java |
Module interface (10 lines) |
qit/base/annot/*.java |
All annotation definitions |
qit/base/utils/ModuleUtils.java |
Reflection engine: discovery, validation, field access, serialization |
qit/base/cli/CliModule.java |
CLI adapter: arg parsing, file I/O, help generation |
qit/base/cli/CliSpecification.java |
Help text and Markdown generation |
qit/base/cli/CliOption.java |
Single CLI option metadata |
qit/base/cli/CliValues.java |
Parsed command-line arguments |
qit/main/QitMain.java |
Entry point, module discovery and dispatch |