AnDi-J uses deep-learning models at different levels:
- Trajectory Analysis — ML is optional for computing the anomalous exponent α and required for predicting the diffusion model.
- Trajectory Segmentation — all predictions here are made with ML models: per-frame α, log₁₀(D), diffusive state and change-point detection along each trajectory.
Both tasks come with bundled default models that work out of the box. You can also use your own models for any of the tasks above (see Custom models for Trajectory Analysis and Custom models for Trajectory Segmentation).
Benchmark your custom models with this online tool.
Bundled models
Two models ship with AnDi-J:
| Model | Used in | Output | Reference |
|---|---|---|---|
| andi-j-analysis | Anomalous exponent (ML), Diffusion Model | one α per trajectory and the diffusion-model class (two networks in one model file) | Based on the convolutional networks of Granik et al. (2019). |
| andi-j-segmentation | Trajectory Segmentation | per-frame α, D and diffusive state, plus change points | Based on U-AnD-ME. |
Inference runs on CPU or GPU. See Installation for details.
andi-j-analysis
Two convolutional networks with the same architecture, after Granik et al. (2019): five parallel blocks of dilated causal convolutions, max-pooled over time and followed by dense layers. One predicts the anomalous exponent α as a probability distribution over the 40 values 0.05, 0.10, …, 2.00; the other the diffusion-model class (ATTM, CTRW, FBM, LW, SBM). Both ship in a single .onnx file that follows the same input contract as custom models (raw (1, T, 2) positions) and returns two outputs, alpha (1, 1) and model (1, 5).
Inside the model, each trajectory is:
- cut to its last 1000 positions and shifted to start at the origin;
- zero-padded at the front, turned into displacements and divided by the mean step size;
- run through both networks for the 8 symmetric views of the trajectory (sign flips of x and y, and swapping x and y), whose probabilities are averaged;
- read out: α is the median of the exponent distribution; the class scores of ATTM and CTRW are multiplied by the probability that α ≤ 1, and that of LW by the probability that α > 1.
On the 2D trajectories of the 1st AnDi Challenge benchmark it reaches an α MAE of 0.137 and a classification F1 of 0.892, above every challenge participant in both tasks. Compute grows with the trajectory length, and the plugin runs one trajectory per CPU core in parallel (on an 8-core CPU, about 35 ms per trajectory for lengths of 10–400 frames).
andi-j-segmentation
A 1-D U-Net 3+ (U-AnD-ME): an encoder–decoder with full-scale skip connections (6 depth levels) on the trajectory’s displacements, with dedicated heads for α (including a temporal branch of dilated convolutions), log₁₀(D) and the diffusive state. It follows the same contract as custom models: raw (1, T, 2) positions in, every output below out, with all processing inside the model:
- Test-time augmentation, always on: the network runs on the 8 symmetric views of the trajectory (4 rotations × 2 reflections) and the per-frame predictions are averaged, so the result does not depend on the orientation of your data.
- Per frame: α (clipped to [0, 1.999]), log₁₀(D) and the probabilities of the four diffusive states (0 = immobile, 1 = confined, 2 = free diffusion, 3 = directed); frames with α > 1.9 are directed.
- Change points: the local maxima of the change-point probability above 0.32, one flag at the first frame of each new segment.
- Segments (
seg_*outputs): each segment’s α, log₁₀(D) and diffusive-state probabilities are the median over its frames; its state is the most probable one. - Immobile segments have α = 0, for the segment and for each of its frames, as in the labels of the 2nd AnDi Challenge. Their D is the predicted one: the apparent diffusion coefficient of the remaining motion, typically localization noise.
On the challenge-phase data of the 2nd AnDi Challenge (single-trajectory task) it reaches JI (CP) 0.684, RMSE (CP) 1.755, MAE (α) 0.175, MSLE (D) 0.018 and F1 (diffusive state) 0.919, second in the overall ranking (see the benchmark website).
The plugin runs 4 trajectories at a time in parallel. On an 8-core CPU this takes about 175 ms per trajectory of 10–400 frames; compute grows with the trajectory length.
What is ONNX (and why)
ONNX (Open Neural Network Exchange) is a portable, framework-independent file format for a trained neural network: the graph of operations plus the learned weights, in a single .onnx file. It allows you to train in whichever Python framework you like (PyTorch, TensorFlow, scikit-learn…), then export the trained model to ONNX.
The bundled models have all been trained using PyTorch. However, AnDi-J runs .onnx files with ONNX Runtime (the Java build), so at inference time no Python is needed — the model runs inside Fiji. This also means that when developing a custom model, you must export it to .onnx afterwards. Below you will find how to do that for the different options available.
Custom models for Trajectory Analysis
AnDi-J ships with a default deep-learning model, but you can plug in your own model for either per-trajectory prediction task:
- Anomalous exponent (Anomalous exponent tab → Type: Load) — a model that predicts the exponent α for a trajectory.
- Diffusion model classification (Diffusion Model tab → Type: Load) — a model that classifies a trajectory into one of the five diffusion models.
The bundled Default model predicts α and the diffusion model in one run. A model you Load is single-purpose: an α model updates only α; a classifier model updates only the diffusion-model tab.
Input
For every trajectory the plugin builds one input tensor and calls your model:
| Shape | (1, T, 2) — batch 1, one row per frame, two features |
| Type | float32 |
| Dynamic axis | the T (sequence-length) axis must be dynamic |
The two features are the raw trajectory coordinates:
- column
0=x - column
1=y
i.e. exactly the (x, y) positions from your loaded data, one row per frame. The plugin feeds these directly — it does no preprocessing.
Choose your own representation inside the model. If your method works on displacements, relative-polar features (dr, dθ), images, etc., compute that transform as the first layers of your model (or in the exported graph). That way training and inference stay identical and the plugin’s contract stays simple. The bundled Default model, for example, shifts each trajectory to the origin and converts it to normalised displacements internally. That choice lives inside the model, not the plugin.
Output
Anomalous-exponent model
| Shape | (1, 1) (or (1,)) |
| Meaning | the predicted exponent α; the plugin reads element [0] |
α should lie in [0, 2] (values outside are dropped from the histogram). The α prediction is a single value for the 2D trajectory, so the X / Y component buttons are disabled while ML α results are shown.
Diffusion-model classifier
| Shape | (1, K) — one score/logit per class |
| Meaning | predicted class = argmax(output) |
K = 5 and the class order must be:
0 = ATTM 1 = CTRW 2 = FBM 3 = LW 4 = SBM
(A scalar output (1, 1) is accepted too and taken as a rounded class index.)
Exporting an analysis model to ONNX (PyTorch example)
import torch
model = MyModel() # your trained nn.Module
model.load_state_dict(torch.load("my_weights.pth", map_location="cpu"))
model.eval()
dummy = torch.zeros(1, 50, 2) # (1, T, 2) raw (x, y); 50 is arbitrary here
torch.onnx.export(
model, dummy, "my_alpha.onnx",
input_names=["input"],
output_names=["output"],
dynamic_axes={"input": {0: "batch", 1: "seq_len"}, # length must be dynamic!
"output": {0: "batch"}},
opset_version=13,
do_constant_folding=True,
)
The dynamic_axes entry for the sequence-length axis is essential — trajectories have different lengths, and without it the exported graph is locked to 50.
Watch out: values derived from the sequence length. If your forward uses the length as a Python integer — e.g. T = x.shape[1] then math.log(T), 1/T, or slicing like x[:, T//2:] — the ONNX tracer bakes those in as constants for the dummy length, and the model is then wrong for other lengths. Fixes: derive such quantities from the tensor shape at runtime (e.g. build a length tensor with torch.ones_like(...).sum(dim=1) and a position index with torch.cumsum(torch.ones_like(...), dim=1) - 1, then use masks instead of fixed slices), or expose the extra scalar as an additional model input.
Verify an analysis model before loading
Check the exported graph in Python with ONNX Runtime, at two different lengths, before loading it in Fiji:
import numpy as np, onnxruntime as ort
sess = ort.InferenceSession("my_alpha.onnx")
for T in (12, 137): # different trajectory lengths
x = np.random.randn(1, T, 2).astype(np.float32) # (1, T, 2) raw (x, y)
out = sess.run(["output"], {"input": x})[0]
print(T, out.shape, out.ravel()[:5])
You want the output shape to be (1, 1) for an α model or (1, 5) for a classifier, and no error at either length.
A ready-made pair of test models (an α estimator and a classifier) is provided in utils/pythons_helpers/make_traj_analysis_test_onnx_models.py:
python make_traj_analysis_test_onnx_models.py # -> test_alpha.onnx, test_model.onnx
Use them to confirm the Load path works end-to-end before wiring your own model.
Loading an analysis model in the plugin
- Run Perform Analysis so the trajectories are loaded.
- Open the relevant tab:
- Anomalous exponent → set Type: Load → Choose model… → pick your α
.onnx→ Compute α. - Diffusion Model → set Type: Load → Choose model… → pick your classifier
.onnx→ Compute models (ML).
- Anomalous exponent → set Type: Load → Choose model… → pick your α
- Each runs with a progress bar over the number of trajectories.
Switching Type back to Default restores the bundled model (which fills both α and the diffusion model, and keeps the two tabs in sync).
Custom models for Trajectory Segmentation
Trajectory Segmentation can run your own ONNX model(s) instead of the bundled one. Pick Load in the ML model options box of the segmentation Data Manager, then either:
- Combined model — one
.onnxthat outputs α, log₁₀(D), change points and the diffusive state, and optionally its own segments (see Model segments), or - Per output — one
.onnxper output (α, D, Change points, Diffusive state); any of α, D or change points you leave as default is produced by the bundled model. The diffusive state is the exception: it is only backed by the bundled model when the bundled model is already running for one of the other three; if you supply custom α, D and change-point models and simply leave diffusive state unset, segments get no diffusive state rather than triggering an extra model run just for that.
The plugin does no pre- or post-processing for your model: it feeds the raw trajectory in and reads your outputs out verbatim. Any processing must be added within your .onnx file.
Input: the raw trajectory
| name | shape | dtype | meaning |
|---|---|---|---|
input |
(1, T, 2) |
float32 | raw positions — column 0 = x, column 1 = y, one row per frame |
T (the number of frames) is dynamic — export the length axis as dynamic so your model accepts any trajectory length. If your architecture needs displacements, a fixed length, or padding, do it inside your model.
Required outputs: per-frame α, log D, change points and diffusive state
All four outputs below are required. Each is per frame, length T, in the same frame order as the input. Shape (1, T) or (1, T, 1) are both accepted ((1, T, 4) for diffusive_state, see below).
| output | shape | meaning | used as-is? |
|---|---|---|---|
alpha |
(1, T) |
anomalous exponent α per frame | yes, verbatim (the plots cover α in [0, 2]) |
logD |
(1, T) |
log₁₀(D) per frame | yes, verbatim (use NaN for undefined/immobile) |
cp |
(1, T) |
change-point flag per frame, 0 or 1 | the plugin marks a change point at every interior frame where cp ≥ 0.5 |
diffusive_state |
(1, T, 4) or (1, T) |
per-frame diffusive state: either class probabilities over the four states (0 = immobile, 1 = confined, 2 = free diffusion, 3 = directed), or the class index directly |
(1, T, 4) → per-frame argmax; (1, T) → per-frame rounded value |
Notes:
- D must be output as log₁₀(D), not linear D — that is exactly what the plugin stores and plots, so nothing is transformed. If a frame has no meaningful D (immobile / trapped), emit
NaN; it will be shown as a gap and left out of segment means. cpis a 0/1 flag, one per frame. Frame 0 and the last frame are ignored (they are trajectory ends, not change points). Emit1at the first frame of each new segment, and apply whatever peak-picking or thresholding your method needs inside the model.diffusive_stateis shown per frame in the Visualization tab’s state strip, so every predicted state is visible even when it is short-lived.
Output names. Name the outputs alpha, logD, cp and diffusive_state. If unnamed, the plugin falls back to output order 0 = alpha, 1 = logD, 2 = cp, 3 = diffusive_state. A per-output model exposes a single output and its name does not matter.
Optional outputs: segment-level predictions
A combined model may also report its own segments, for methods whose segment-level values are not a simple summary of the per-frame ones. The Model segments checkbox, shown under Combined model in the ML model options box and ticked by default, decides how segments are built:
- ticked: the plugin reads the
seg_*outputs below and uses them verbatim as the segments.seg_startandseg_lengthmust be present; if either is missing, the plugin derives the segments fromcpas when unticked. - unticked: the plugin ignores any
seg_*output and derives the segments fromcp, taking each segment’s α and log₁₀(D) as the interval mean and its state as the majority vote of the frames it covers.
Per output mode always derives the segments from cp. S is the number of segments and is dynamic.
| output | shape | meaning |
|---|---|---|
seg_start |
(1, S) |
first frame of each segment, 0-based |
seg_length |
(1, S) |
number of frames in each segment |
seg_alpha |
(1, S) |
anomalous exponent α of each segment |
seg_logD |
(1, S) |
log₁₀(D) of each segment |
seg_state |
(1, S) |
diffusive-state class index of each segment, 0–3 |
seg_startandseg_lengthare what identify a segment; a row whoseseg_lengthis≤ 0, or whoseseg_startfalls outside the trajectory, is skipped, so the arrays may be padded to a fixed length. A segment that runs past the last frame is cut at the end of the trajectory.seg_alpha,seg_logDandseg_stateare optional: a missing value is left empty for that segment (not plotted, no diffusive state).seg_stateis rounded to the nearest class and limited to 0–3.- Integer (
int32/int64) or float arrays are both accepted.
Inference runs one trajectory per call, so the batch axis is always 1.
Exporting a segmentation model to ONNX (PyTorch example)
import torch, torch.nn as nn
class CombinedSeg(nn.Module):
def forward(self, x): # x: (1, T, 2) raw x,y
# ... your architecture; must handle a dynamic T ...
alpha = ... # (1, T)
logD = ... # (1, T) = log10(D)
cp = ... # (1, T) in {0,1}
diffusive_state = ... # (1, T, 4) softmax over the 4 states
return alpha, logD, cp, diffusive_state
m = CombinedSeg().eval()
dummy = torch.zeros(1, 200, 2) # any length; the axis is dynamic
torch.onnx.export(
m, dummy, "my_seg.onnx",
input_names=["input"],
output_names=["alpha", "logD", "cp", "diffusive_state"],
dynamic_axes={"input": {1: "T"}, "alpha": {1: "T"}, "logD": {1: "T"},
"cp": {1: "T"}, "diffusive_state": {1: "T"}},
opset_version=13, do_constant_folding=True,
)
A model that also reports its own segments returns the seg_* tensors after these four, naming them in output_names and marking their length axis dynamic with {1: "S"}.
For a per-output model, return a single (1, T) (or (1, T, 4) for the diffusive-state slot) tensor and give it one output_names / dynamic_axes entry.
Length-derived-constants pitfall. Do not bake the dummy length (200 in the example above) into any reshape or constant. Derive T from the input at runtime (x.shape[1]) so the exported graph is truly length-agnostic.
Verify a segmentation model before loading
import numpy as np, onnxruntime as ort
s = ort.InferenceSession("my_seg.onnx", providers=["CPUExecutionProvider"])
for T in (37, 128, 240):
out = s.run(None, {"input": np.random.randn(1, T, 2).astype("float32")})
for name, arr in zip([o.name for o in s.get_outputs()], out):
print(T, name, arr.shape) # (1, T) or (1, T, 1); (1, T, 4) for diffusive_state
If every output is length T for every T, it will load in the plugin.
utils/pythons_helpers/make_traj_seg_test_onnx_models.py builds small dummy models (seg_alpha.onnx, seg_logd.onnx, seg_cp.onnx, seg_state.onnx, and seg_combined.onnx, the last with all four required outputs) that follow this contract, so you can exercise both the Combined and Per output Load modes before dropping in your real models.
Troubleshooting
Applies to both analysis and segmentation models.
| Symptom | Likely cause |
|---|---|
| “Output ‘…’ not found” / wrong values | output not named as expected, or wrong shape |
| “Output ‘…’ has N values for a trajectory of T frames” | a per-frame output is not length T: e.g. one value per displacement (T−1) or a length fixed at export |
| Works for one dataset, wrong for another | sequence-length axis not dynamic, or length-derived constants baked in |
| Classes look shuffled | classifier class order ≠ ATTM, CTRW, FBM, LW, SBM |
| α histogram empty | α outside [0, 2], or NaNs — the plugin uses custom α verbatim, so clip/validate it inside your model |
| Load fails immediately | not a valid .onnx, or exported with an opset newer than the bundled ONNX Runtime 1.26 supports — re-export with a lower opset_version |
Contributing a model
Models developed for AnDi-J can be shared with the community: once approved, they are listed with the bundled models in contributions/list.md, together with their scores and download links, and shown on the benchmark website. The maintainers review every contribution.
- Export your model to ONNX following the contract of its module (analysis or segmentation), and check that it runs in Fiji with Load.
- Score it on the Submit pages of the benchmark website.
- Upload the
.onnxfile(s) to a public record with a DOI (for example Zenodo). The code of the method must be open source. - Copy
contributions/template.ymltocontributions/<id>.yml, fill it in, and open a pull request that adds it. The file gives the method’s id and version, authors, contact, license, code repository, optional reference, the ONNX record(s) and the scores, namedTA.alpha_mae,TA.model_f1(Trajectory Analysis) andTS.cp_ji,TS.cp_rmse,TS.alpha_mae,TS.D_msle,TS.state_f1(Trajectory Segmentation). The pull request template lists the checks.
The maintainers then download and benchmark the model, and add it to the list, the GitHub wiki and the benchmark website.
