Roll your helix.
Interactive triage of helical-filament segments from a Dynamo or RELION 5
subtomogram-averaging project. It reconstructs the per-filament helical roll check
(subunit roll vs. real position along the filament axis, compared to the known screw) and
lets you mark bad segments for removal. Built to run over ssh -XY and stay responsive
(PyQt6 + pyqtgraph, raster renderer, no OpenGL).
See docs/README.md for the full method.
For one tomogram it loads either input — the two refined_table_ref_00X_iteYYYY.tbl tables
from a Dynamo averages folder, or a RELION 5 tomography particles .star — and for each
filament:
- fits the axis through the 3D coordinates (origin = centroid, the mean of all segments, not the middle one; direction = least-squares fit through every segment),
- projects each segment onto the axis → real position (Å, via the pixel size),
- reads the subunit roll about the axis,
- compares to the screw model
roll = (twist/rise)·pos + phase.
Inputs. Dynamo: concatenates and sorts the ref tables by tag, groups filaments by
column 23, poses from the ZXZ-extrinsic Eulers. Point it at the project folder
(…/abp_align_eo) and it auto-finds results/ite_*/averages/, uses the last real
iteration as the working set (the empty N+1 placeholder is skipped), and keeps the earlier
iterations for the per-segment convergence paths; a single averages/ folder also works
(no paths). RELION: groups by _rlnHelicalTubeID,
coordinates from _rlnCoordinateX/Y/Z, and converts the poses
(_rlnTomoSubtomogram* combined with _rlnAngle*, or _rlnAngle*Prior when no refined
angles exist) back to the Dynamo convention so both paths share one fit. The pixel size is
read from the star optics block (override with --pixelsize). Point it at a Refine3D job
folder (containing run_data.star + run_it*_data.star) and it reads run_data.star as
the working set and the run_it* files as the per-iteration history (run_it000 = start);
a single .star also works (no paths).
A segment whose alignment drifted lands at its true (wrong) position and visibly leaves the model line — that's what you mark.
- A scrollable grid of per-filament panels (roll vs position + dashed model).
- The tomogram XY map on the right.
- Hover a panel → that filament lights up on the map.
- Click a panel → open its detail window.
- Panels with marked segments turn red.
- Three linked plots: roll vs position, residual to model, XY map.
- Point at a segment in any plot → it highlights in all three, with a readout (tag, position, roll, delta).
- Mark for removal: left-drag a rubber-band box to mark, right-drag to unmark, click Select all in filament, or click individual points to toggle. Clear filament unmarks the whole tube. Marks are red everywhere and saved immediately.
- Iteration paths (Dynamo project folder, or RELION Refine3D job folder): a thin grey
trail per segment showing how its roll moved from the starting value (grey dot,
iteration 0 — Dynamo
ite_0001/starting_values/, RELIONrun_it000_data.star) through each refinement iteration to the final pose (the colored dot). Intermediate iterations are line-only. The trail is drawn in the same unwrapped frame as the panel: each iteration's roll is placed on the turn nearest that segment's final value, so a trail always ends on its own dot and never wanders more than half a turn from it. A segment whose trail wanders never settled. Toggle it off in the toolbar; solid line (vs the dashed model), beneath the dots.
An iteration slider under the toolbar scrubs every panel back through the refinement.
Reading all the iterations takes a few seconds, so it is not done at startup: point the tool
at the refinement job folder and press load iterations next to the slider (or launch
with --iteration-paths to have it ready immediately). If the slider reads n/a with no
load button, the path has no history — you are pointing at a single .star rather than the
job folder that holds run_it*_data.star.
Drag it and the whole grid redraws as the rolls were at
that iteration; the readout beside it reports the twist measured from the rolls on screen
(the same robust per-filament slope vote the auto-fit uses), against iteration 0 and the
final value — so you can watch, and quantify, the screw flattening as refinement proceeds.
This is the intended way to see a measured twist decay across refinement. Two distinct
causes produce that decay, and the job's note.txt tells you which one you have:
- Symmetry applied (
--helixwithout--ignore_helical_symmetry) — the case in Where the method does not apply (docs/README.md§13): rot is defined only modulo the screw operator, so the accumulated phase is folded and the slope is attenuated. - Symmetry ignored, rot unrestrained (
--ignore_helical_symmetrywith--sigma_rot 0) — nothing ties consecutive segments' rot to their position, so the screw present in the picking geometry simply washes out toward zero.
Either way the late-iteration twist is not the filament's twist; iteration 0 is the honest one.
Coordinates never move between iterations — only the poses — so positions, tags and the fitted axis are shared, and only the roll and its colouring change. Notes:
- Marks apply to segments, not to iterations. auto-exclude and exclude bad tilt judge the iteration on screen, so marking at several iterations simply accumulates into the one remove list. That is expected and supported.
- Flips are disabled off the final iteration — a flip rewrites the pose, which is only meaningful on the working set. Press final first. For the same reason, committed flips are shown only at the final iteration.
- Opening a filament (clicking a panel) always shows the final iteration; the detail window's own Iteration paths trail is the per-segment view of the same history.
Both windows carry twist / rise / pixel size spin boxes (rise in Å, pixel size in Å/px). Retune any of them and every model overlay refits instantly at real Å scale; the two windows stay in sync. Use this to dial the screw onto the data before triaging.
The View 3D button lives in the per-filament detail window (click a panel first), so
the 3D scene is always scoped to one filament — which keeps it fast, since only that
filament's density is ever built. It shows the filament's particles as points colored by
position (marked-for-removal in red, live), the 3D backbone, and a pose triad per
particle so you can see the orientations screw along the axis. If you pass a refined average
map (--map run_class001.mrc, or load one from the 3D window's "Load map…"), its
isosurface is placed and oriented at every particle of that filament, with a threshold
slider and a convention toggle (as-is vs inverse) to confirm the placement looks continuous.
The app launches in 2D; 3D is opened on demand from a filament. It needs OpenGL
(PyOpenGL), which the 2D windows avoid. Forwarded X (ssh -XY) usually has no usable GLX,
and Qt would otherwise abort at startup trying to initialize it — so over SSH the app
disables the xcb GLX integration (2D runs fine) and greys out the View 3D button. To use
3D, run locally, or force it with --gl (e.g. under VirtualGL / vglrun); --no-gl forces
it off. The OpenGL/mrc packages are imported only when you actually open the 3D view.
Marked segments are written continuously to a plain text file (default
<dir>/remove_list.txt), one id per line, sorted — the Dynamo tag (column 1) for
Dynamo, or the _rlnTomoParticleId for RELION. This file is:
- the resume list — reloaded automatically on the next launch, and
- the export — feed it to your own
.tbl/.starcleanup (the app does not edit the input).
For Dynamo it also writes temp.tbl (the sorted working rows for the chosen tomogram),
matching the notebook; disable with --no-temp.
Dependencies (both methods below install all of them):
| packages | needed for | |
|---|---|---|
| core | numpy, scipy, PyQt6, pyqtgraph | the 2D app |
| RELION | eulerangles | --relion (RELION→Dynamo pose conversion) |
| 3D view | PyOpenGL, mrcfile | the per-filament "View 3D" (OpenGL + reading the map) |
PyQt6 comes from PyPI (conda-forge ships only PyQt5), so the GUI stack is pip-installed
on every route — including inside the conda env.
cd rohlex
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt # PyQt6 + pyqtgraph + scipy/numpy + eulerangles + PyOpenGL + mrcfileOn macOS (incl. Apple Silicon) and Windows this just works — native windowing + OpenGL,
no extra system libraries. On Linux it relies on libxcb-cursor0 (PyQt6 ≥ 6.5) and
libGL/GLX being present — see the fallback below if launching errors out.
On a shared Linux cluster where you can't sudo apt install, let conda supply the missing
system libraries (the Qt binding still comes via pip per environment.yml):
cd ~/LBEM/rohlex
conda env create -f environment.yml # python + numpy/scipy (conda), Qt stack (pip)
conda activate rohlex
conda install -c conda-forge xcb-util-cursor # provides libxcb-cursor.so.0 (Linux only)If a pip-venv launch fails with "xcb-cursor0 or libxcb-cursor0 is needed to load the Qt xcb
platform plugin", either sudo apt install libxcb-cursor0 (one-time, all users), or — with
no root — drop the single library into your home dir and point LD_LIBRARY_PATH at it:
cd /tmp && apt-get download libxcb-cursor0 && dpkg -x libxcb-cursor0_*.deb x
mkdir -p ~/.local/lib && cp x/usr/lib/*/libxcb-cursor.so.0* ~/.local/lib/
export LD_LIBRARY_PATH=$HOME/.local/lib:$LD_LIBRARY_PATHThe bundled run.sh sets that LD_LIBRARY_PATH for you and runs the venv Python. None of this
applies on macOS/Windows. The 2D app needs no OpenGL — the 3D extras are only touched when you
click View 3D — so a missing OpenGL stack never blocks 2D triage.
The GUI needs a display, so connect with X11 forwarding first:
ssh -XY user@cluster
conda activate rohlex # or: source .venv/bin/activate
cd ~/LBEM/rohlex
# Dynamo project folder (auto-finds results/ite_*/averages, shows iteration paths)
python rohlex.py \
/mnt/.../dynamo_project_b4/abp_align_eo \
--tomo 1 --twist -1.4 --rise 4.75 --pixelsize 7.92
# RELION 5 (auto-detected from the .star extension)
python rohlex.py \
/mnt/.../warp/relion_b4_clean/particles_new_relion5.star \
--twist -1.4 --rise 4.75 --out ~/relion_remove_list.txtWith the pip venv (no conda) on this cluster, swap the launch line for ./run.sh <path> [options], which adds the local libxcb-cursor shim automatically.
The format is inferred from path (.star → RELION, folder → Dynamo); force it with
--dynamo / --relion.
Options: --tomo (Dynamo col-20 id or RELION _rlnTomoName; else only/first or a chooser),
--twist (°/subunit), --rise (Å/subunit), --pixelsize (Å/px; RELION default from the
star optics, Dynamo default 7.92), --map (reference average .mrc/.em for the 3D
density overlay), --cols (overview columns), --out (remove-list path; point it somewhere
writable if the input sits on read-only storage), --no-temp, --iteration-paths (read
every iteration up front; enables the overview iteration slider and the detail-window
convergence trails). Twist, rise and pixel size are also live in the GUI.
Requires Python 3.9+. --relion additionally needs the eulerangles package (included in
both environment.yml and requirements.txt).