Installation

Dependencies

PackagePurpose
OIFITS.jlRead OIFITS v1/v2 files
NFFTNon-uniform fast Fourier transform for imaging
OptimPackNextGenVMLMB gradient optimizer for image reconstruction (Éric Thiébaut)
NearestNeighborsKD-tree UV deduplication
SparseArraysOI_CORR correlation matrix storage
NLoptLocal and global optimizers (Nelder–Mead, ISRES, …)
LsqFitLevenberg–Marquardt least-squares fitting
UltraNestBayesian model selection via nested sampling
SpecialFunctionsBessel functions for visibility calculations
PythonCallJulia↔Python bridge (UltraNest, matplotlib)
PythonPlotoptional — matplotlib plotting, see Plotting
Downloads(standard library) SIMBAD queries, over its TAP service
CrayonsColoured terminal output

Step 1: Python packages

Nothing to do. OITOOLS declares its Python dependencies in CondaPkg.toml, and PythonCall provisions them automatically into a project-local environment the first time you using OITOOLS:

PackageUsed by
matplotlib-basethe corner plot of a nested-sampling fit, and the plotting extension below
ultranestfit_model_nested's :ultranest backend — optional; Nautilus.jl needs no Python
scipyimported at module level by ultranest.plot

matplotlib-base, not matplotlib: on conda-forge the latter is a metapackage adding PySide6, which pulls in Qt, libclang and libLLVM — about 510 MB unpacked for a GUI binding nothing here imports. PythonPlot declares full matplotlib itself, so an interactive backend arrives with it if you want one.

Reusing an existing Python

Set JULIA_PYTHONCALL_EXE to your interpreter to reuse an existing installation. Note that doing so opts out of CondaPkg, so you become responsible for keeping matplotlib and ultranest installed and mutually consistent.

What each optimiser needs

using OITOOLS loads no Python and no plotting stack, so several engines live behind a package you have to load yourself. Each one below says exactly what.

An engine whose package is missing is not silently substituted: the function has no methods and the call raises a MethodError naming it, or — for the nested samplers, which share one entry point — an error listing what to load.

Model fitting

EngineCallLoad
NLopt, gradient and derivative-freefit_model(...; method = :LD_LBFGS) and any other NLopt symbolusing OITOOLS
Levenberg–Marquardtfit_model_lsqfitusing OITOOLS
Grid search over two parameterschi2_mapusing OITOOLS
Bootstrap resamplingbootstrap_fitusing OITOOLS
Nested sampling — pure Juliafit_model_nestedusing Nautilus
Nested sampling — UltraNestfit_model_nested(...; backend = :ultranest), or fit_model_ultranestusing PythonCall, plus the Python ultranest package CondaPkg installs

fit_model takes any NLopt method symbol — there is no whitelist — and use_grad follows the LD_ prefix. bootstrap_fit wraps an inner fitter (:lsqfit or :nlopt), so it needs nothing beyond what that fitter needs.

Both nested samplers fill the same fit_model_nested; nested_backend() reports which is in force and set_nested_backend! chooses when both are loaded. Their logz should agree within logzerr, which is the cheapest check that either has converged.

Imaging

EngineCallLoad
VMLMB + regularisersreconstructusing OITOOLS
Maximum entropy (BSMEM)reconstruct_bsmemusing OITOOLS
ADMM (BSDMM)reconstruct_bsdmmusing OITOOLS
SPARCO, chromatic hybridreconstruct_sparco, reconstruct_hybridusing OITOOLS
Simulated annealing (SQUEEZE)reconstruct_squeezeusing OITOOLS
Parallel tempering (SQUEEZE)reconstruct_squeeze_temperedusing Pigeons

Drawing what they produce

FigureCallLoad
Corner plot from any posteriorplot_corner_makieusing PairPlots and a Makie backend
Corner plot from an UltraNest resultplot_ultranest_cornerusing PythonPlot
Live SQUEEZE monitorreconstruct_squeeze(...; monitor = n)using PythonPlot
Every other figuresee Plottingusing PythonPlot for the matplotlib set, any Makie backend for the _makie set

The short version

using OITOOLS                                    # NLopt, LM, grid search, bootstrap, all imaging but tempering
using OITOOLS, Nautilus, PairPlots               # + nested sampling and its corner plot, no Python
using OITOOLS, PythonPlot                        # + matplotlib figures, UltraNest, the SQUEEZE monitor
using OITOOLS, Pigeons                           # + parallel-tempered SQUEEZE

Faster GUI startup: a sysimage

Most of the delay between launching the GUI and seeing a window is Julia compiling Makie, Qt and the reconstruction driver. A PackageCompiler sysimage holds that work already done:

julia --project=bin bin/compile.jl                  # tens of minutes, roughly 1 GB
julia --project=bin --sysimage bin/oitoolsgui.so bin/oitoolsgui.jl file.oifits

bin/trace.jl is what gets traced, and it is deliberately the work a cold start pays for: loading the stack, building every figure gui() builds, drawing into each one, one model fit, one χ² map, one nested-sampling run and two iterations of reconstruct.

Wayland and the sysimage

GLFW.jl initialises from its own __init__, which in a sysimage runs before any user code, so JULIA_GLFW_PLATFORM arrives too late and GLMakie stays on X11. The launcher notices and brings Qt to xcb to match, so the two halves still agree — but a sysimage run on Wayland uses XWayland where the plain launcher uses Wayland. Export the variable before Julia starts to get the native backend:

JULIA_GLFW_PLATFORM=wayland julia --project=bin --sysimage bin/oitoolsgui.so bin/oitoolsgui.jl
A stale sysimage runs the old code

The image contains the version of OITOOLS it was built from. Change the package, the Manifest or Julia itself and you must rebuild, or you will be running code that is no longer on disk — and nothing will tell you. This is the one failure mode of the approach worth being careful about.

No Python is in the image. Neither PythonPlot nor PythonCall is passed to create_sysimage and bin/trace.jl loads neither, so a session started on the sysimage carries no conda environment: nested sampling comes from Nautilus.jl and plotting from Makie. The matplotlib figures and plot_ultranest_corner are simply absent, and calling one raises a MethodError naming it. using PythonPlot in that session brings them back — the sysimage does not prevent it, it just does not carry it.

Step 2: Julia packages

Add the EmmtRegistry (required for OptimPackNextGen, OIFITS, and related packages), then install all dependencies:

using Pkg
pkg"registry add General"
pkg"registry add https://github.com/emmt/EmmtRegistry"
Pkg.add([
    "AstroTime", "CFITSIO", "Crayons", "Dates", "DelimitedFiles",
    "FFTW", "FITSIO", "Glob", "LaTeXStrings", "LinearAlgebra",
    "LsqFit", "Match", "NFFT", "NLopt", "NearestNeighbors",
    "Parameters", "Random", "SparseArrays",
    "SpecialFunctions", "Statistics", "UltraNest",
    "ArrayTools", "LazyAlgebra", "OptimPackNextGen", "OIFITS",
])
Pkg.add(url="https://github.com/fabienbaron/OITOOLS.jl.git")

Installation typically takes 2–10 minutes depending on your system.

Verify the installation:

using OITOOLS

Step 3: Plotting

The plotting functions live in a package extension and require PythonPlot. using OITOOLS alone gives you uvplot, plot_v2, imdisp and the rest as functions with no methods, and calling one raises a MethodError.

using Pkg
Pkg.add("PythonPlot")

using OITOOLS, PythonPlot     # both: PythonPlot is what activates the extension
uvplot(data)

The extension (OITOOLSPythonPlotExt) loads automatically once PythonPlot is present; there is nothing else to enable. It supplies every plotting entry point:

uvplot, plot_v2, plot_t3phi, plot_t3amp, plot_visamp, plot_visphi, plot_diffphi, plot_flux, plot_obs, plot_multi, the *_residuals family, plot_residuals, plot_v2_multifile, plot_facility, imdisp, imdisp_multi, plot_ultranest_corner, set_oiplot_defaults, and the observation planners gantt_onenight, obs_plan, chara_plan, empty_night.

Keeping them out of the core package means using OITOOLS starts no Python plotting stack: it does not import matplotlib, and it maps no Qt into the process — which matters on a headless machine, in CI, and beside any other Qt application.

To pick a non-interactive backend, for scripts or CI, set MPLBACKEND before loading PythonPlot:

ENV["MPLBACKEND"] = "Agg"
using OITOOLS, PythonPlot

Observable metadata — OBS_PLOT_SPECS, oiplot_colors, canonical_color — stays in the core package, so a front-end that draws with something other than matplotlib can share the axis labels, groupings and palette without loading Python at all.

Step 4: The GUI (optional)

The GUI is a separate stack — GLMakie for drawing, Qt/QML for the window — and none of it is loaded by using OITOOLS. It costs about 6.4 s of load time against the package's own 1.7 s, so it is gated behind its own dependencies and no script, reduction or CI run pays for it.

From a clone of the repository:

julia --project=bin -e 'using Pkg; Pkg.instantiate()'   # first time only
julia --project=bin bin/oitoolsgui.jl                   # every time

An OIFITS file given on the command line is opened at startup:

julia --project=bin bin/oitoolsgui.jl demos/data/2004-data1.oifits

Three things about that command are deliberate.

--project=bin, not --project=.. GLMakie, QMLMakie and QML are weak dependencies of OITOOLS, and weak dependencies are not loadable from the package's own environment. bin/Project.toml declares them as ordinary ones. Under --project=. they either fail to load or, worse, resolve from your default environment at whatever versions happen to be there.

Run it from the repository root. bin/Project.toml reaches the package with [sources] OITOOLS = {path = ".."}, which is relative to bin/.

bin/Manifest.toml is not in the repository. The first instantiate resolves the GL and Qt stack itself, within the bounds Project.toml declares (GLMakie 0.13, Makie 0.24, QML 0.13, QMLMakie 0.3, GLFW_jll 3.4). Keep the resulting manifest if you want the same versions back later.

Starting it from your own session

using OITOOLS
configure_graphics!()                                   # before the first OpenGL context

using GLFW_jll
wl = prefer_native_wayland!()                           # before `using GLMakie`
configure_qt_platform!(; match_x11 = !wl.applied)       # before Qt starts

using GLMakie, QMLMakie, QML     # these activate the GUI extension
gui()

The session decides the windowing system. On Wayland both Qt and GLMakie use Wayland; on X11 both use X11. Neither of the last two calls does anything in the ordinary case — Qt already follows the session on its own, and the only reason prefer_native_wayland! is needed at all is that GLFW.jl hard-codes PLATFORM_X11 on Linux (glfw3.jl:499), so GLMakie would land on XWayland under a Wayland compositor. It sets JULIA_GLFW_PLATFORM=wayland, which GLFW.jl reads at its own Init.

What must not happen is one half on each: two EGL display connections and two surface lifetimes in one process. Only GLMakie's choice can fail, so Qt is the one that yields — that is what match_x11 = !wl.applied says. GLMakie's choice fails in two cases: OITOOLSGUI_GLFW_X11 is set, or GLFW is already initialised, which is what a sysimage does.

The order is not stylistic. Mesa reads its driver variables when the first OpenGL context is created, and GLFW.jl reads JULIA_GLFW_PLATFORM at its Init, so both calls have to happen above using GLMakie. That is also why neither lives in the GUI extension: loading GLMakie is what creates that extension, so anything inside it would already be too late. configure_graphics! is in the core package and needs nothing; prefer_native_wayland! needs only libglfw and lives in a one-function extension that GLFW_jll alone activates.

configure_graphics! acts only on WSL, where there is no /dev/dri render node and Mesa would otherwise fall through to software rendering.

Qt used to be pinned to XWayland

Qt's Wayland backend does not release the windows it opens, and a QtQuick.Dialogs.FileDialog stayed on screen after closing — measured in test/gui/filepicker_min.jl, where leaving the dialog to manage itself, forcing DontUseNativeDialog and destroying the QML object outright all failed, and only QT_QPA_PLATFORM=xcb worked. The GUI draws its own picker now and no QML imports QtQuick.Dialogs, so the pin is gone. It cost nothing on the GPU either way — measured on WSL, GL_RENDERER is the same D3D12/NVIDIA adapter on both backends — but XWayland gives up application-side fractional scaling.

Any of it can be switched off:

OITOOLSGUI_NO_GPU_SETUP=1      # skip the Mesa setup
OITOOLSGUI_GLFW_X11=1          # keep GLMakie on X11; Qt follows it to xcb
QT_QPA_PLATFORM=wayland        # say so explicitly; an existing setting is never touched
QSG_INFO=1                     # make Qt report which GL renderer it actually got
OITOOLS.guiFunction
gui()
gui(session)

Open the OITOOLS window: four perspectives — Exploring, Observing, Modeling, Imaging — over one session, so a dataset moves between them without being written out and read back.

Defined by OITOOLSGUIExt, so the GUI stack has to be loaded for it to have any methods:

using OITOOLS, GLMakie, QMLMakie, QML
gui()

With no argument it builds its own session. Pass one to open on datasets already loaded, which is what bin/oitoolsgui.jl does with the files named on its command line.

A function can be declared here and given methods by an extension; a TYPE cannot, which is why Session, ShellState and LiveCanvas stay inside the extension and are reached through Base.get_extension(OITOOLS, :OITOOLSGUIExt).

See also configure_graphics!, which must run before the first OpenGL context exists and therefore before using GLMakie — not from inside the window.

source
OITOOLS.configure_graphics!Function
configure_graphics!(; verbose = true) -> NamedTuple

Give Mesa the one hint it cannot work out for itself on WSL, and do nothing anywhere else.

Returns (applied, reason, vars): whether anything was set, the one-line explanation of why or why not, and the variables set. Call it before the first OpenGL context exists — bin/oitoolsgui.jl calls it above using GLMakie, and gui calls it again as a backstop for sessions started by hand. The second call is a no-op, since by then the variables are set and set variables are left alone.

Nothing here is forced on you. Any of

OITOOLSGUI_NO_GPU_SETUP=1     # skip all of it
GALLIUM_DRIVER=llvmpipe       # or any other explicit choice
LIBGL_ALWAYS_SOFTWARE=1       # compare against software rendering

wins, which is what you want when the question is whether a bug is the driver's fault.

To see which renderer you actually ended up on, start the GUI with QSG_INFO=1; Qt reports the GL vendor and renderer as it builds the scene graph.

source
OITOOLS.configure_qt_platform!Function
configure_qt_platform!(; match_x11 = false, verbose = true) -> (; applied, reason, vars)

Keep Qt on the same windowing system as GLMakie.

The rule is simply that the session decides: on Wayland both halves use Wayland, on X11 both use X11, and this function does nothing in either case — Qt already follows the session on its own. It acts only when GLMakie could not, and pins QT_QPA_PLATFORM=xcb so that Qt comes back to X11 to match.

That happens in two situations, and bin/oitoolsgui.jl reports both by passing match_x11 = !prefer_native_wayland!().applied:

  • $FORCE_X11 is set, asking for XWayland deliberately;
  • GLFW is already initialised, which is what a PackageCompiler sysimage does — GLFW.jl initialises from its own __init__, before any user code, and glfwInit cannot be undone.

Why it matters that they agree: one half on Wayland and one on X11 means two EGL display connections and two surface lifetimes in one process. An explicit QT_QPA_PLATFORM is always left alone.

This used to pin xcb unconditionally

Qt's Wayland plugin does not release the windows it opens, and a QtQuick.Dialogs.FileDialog stayed on screen after closing — measured in test/gui/filepicker_min.jl, where open(), DontUseNativeDialog and destroy() all failed and only QT_QPA_PLATFORM=xcb worked. The GUI draws its own picker now (src/gui/qml/FilePicker.qml) and no QML imports QtQuick.Dialogs, so the failure is unreachable and the pin is gone. Measured on WSL, the two backends are equivalent on the GPU — GL_RENDERER is the same D3D12/NVIDIA adapter either way — and XWayland's only real cost is that the compositor does fractional scaling instead of the application.

source

The GUI's own types (Session, LiveCanvas, ShellState) are defined inside the extension, since a function can be declared in the core package and given methods later but a type cannot. gui() builds its own Session, so most callers never need to name one; scripts that do reach them through the extension module:

const GUI = Base.get_extension(OITOOLS, :OITOOLSGUIExt)
using .GUI