Open source · Python · CPU, GPU and Dask clusters

Radio imaging as an optimization problem

Pyralysis turns visibilities into images by minimizing an objective you write (χ², closure phases or the bispectrum, plus the priors you trust), with modern first-order solvers. Every step is explicit, testable and swappable. No black-box CLEAN.

pip install pyralysis latest release 3.1.0 · full install
Pyralysis reconstruction of a simulated protoplanetary disk Dirty image of the same observation Dirty image Pyralysis reconstruction
Drag to compare. Simulated ALMA Band 6 observation of a protoplanetary disk; MFISTA, χ² + L1, on a GPU. Each side has its own colour scale.
6χ² data terms, closures and bispectrum included
10regularizers and constraints
8 + 13line searches and step-size seeders
9gridding kernels
216antenna configurations, ALMA to SKA

Why Pyralysis

What makes it different

Most interferometric imaging still runs on CLEAN, a deconvolution heuristic. Pyralysis states the problem instead, and gives you the tools to solve it at scale.

f(x)

An objective you can read

The image minimizes χ²(x) + Σ λk Rk(x) under constraints you choose. Inspect every term's value at every iterate, test it, change it.

Objective functions →
φijk

Calibration-independent imaging

Closure phases, closure amplitudes and the bispectrum are first-class data terms, next to complex, amplitude and phase χ²: the EHT-style toolbox, in the same library.

Closures →
CPU→GPU

One code, any machine

The same script runs on NumPy, on CuPy on a GPU, or across a Dask cluster, with out-of-core Zarr spill when the data don't fit, in float32 or float64, end to end.

Array backends →
sky→uv

Simulate what you image

216 antenna configurations, parametric or FITS skies, protoplanetary disks, Faraday screens, thermal noise and gain errors: test a reconstruction against a known truth.

Simulation →
χ²≈N

Knows when it fits the noise

Every optimizer records each χ² and regularizer at every accepted iterate, and warns when the data are fitted to the thermal noise level: the discrepancy principle, built in.

Optimizers →
class

Made to be extended

Data terms, regularizers, kernels, line searches and optimizers are small Python classes. A new idea is a subclass, not a fork of a monolithic imager.

Extensibility →

Real data · EHT 2017 · M87*

Change one line, change the image

The same Event Horizon Telescope visibilities of M87*, reconstructed again and again. Swap the prior, or the forward model and the solver: the image and the line of code change together.

m87.py
terms = [
    ChiSquared(model_visibility=degridding, normalize=True),
]

Degridding with a PSWF1 kernel; SDMM with an inner Hager–Zhang solver (Hager–Zhang alone for χ² only and Entropy). Rebuilt by examples/scripts/m87_gallery_matrices.py.

M87* reconstruction with TSV
TSV (λ = 0.5)

Approach

CLEAN-style imaging and Pyralysis

CLEAN is effective and deeply tied to calibration practice. Pyralysis takes the other road: regularized maximum likelihood, written as an explicit optimization problem.

CLEAN-style deconvolutionPyralysis
What is solvedIterative peeling of bright components from the dirty imageAn explicit objective f(x) with its gradient or proximal map
Sky modelPoint or multi-scale components, convolved with a restoring beamA pixel image under the priors you choose; the model is not beam-convolved
DataComplex visibilitiesComplex, amplitude or phase χ²; closure phases, closure amplitudes, bispectrum
Prior knowledgeMasks or boxes, scales, loop gainL1, TSV, TV, entropy, Huber, Tikhonov, flux and positivity constraints
StoppingThreshold and iteration countConvergence tolerances, and each χ² against its noise level
Extending itInside the imaging packageSubclass a term, kernel, line search or optimizer in your own code

Background: the inverse problem · compressed sensing · why Pyralysis

Where it fits

A platform, not a single algorithm

Pyralysis sits between specialized imaging packages and domain-agnostic toolkits: it speaks interferometry (measurement sets, uvw, Stokes, closures) and keeps every method interchangeable.

Beyond greedy deconvolution

Regularization is an explicit part of the objective, not hidden in an algorithm's steps, so physical priors and constraints are principled, not ad hoc.

Beyond single-method imagers

Many optimizers, regularization families and forward models in one framework, instead of one method wired through the whole code.

Interpretable, not a black box

Classical optimization needs no training set, behaves predictably on a new observation, and every term of the result can be inspected.

Built for interferometry

Unlike general inverse-problem toolkits, the coordinates, measurement sets, polarization and closure quantities are first-class, and simulation lives next to reconstruction.

A great fit when you

  • want to try a new prior, data term or solver without rewriting an imager
  • image where calibration is uncertain, from closure quantities
  • need to test a method end to end on simulated data with a known truth
  • care about reproducible, inspectable, testable reconstructions

Worth knowing

  • the object model takes a little learning before it pays off
  • for the fastest routine survey imaging, a mature CLEAN pipeline may still be the pragmatic choice
  • Measurement Sets are the most mature input; other formats come through Io extensions

Real data

Public ALMA and VLA observations

Each image was made with the Pyralysis optimization CLI on one workstation. Drag to compare the dirty image with the reconstruction; each side has its own colour scale.

Settings for every dataset, and every variant tried: gallery/generate.py.

Under the hood

Engineered like research software should be

Three core frameworks, Dataset, Optimization and Simulation, share one data model, organized with classic design patterns so new ideas plug in instead of being bolted on.

Strategy

Optimizers, line searches, step-size seeders, estimators and kernels are interchangeable objects.

Composite

An objective is a sum of terms: χ² variants, regularizers and constraints, combined freely.

Template Method

Solvers share one iteration skeleton and override only their step, so logging, stopping and snapshots come for free.

Observer

An interferometer follows its antenna array: filter antennas by radius, dish size or ID, and what depends on them updates.

Pyralysis architecture: how you use it, what it does, how it scales, and the libraries underneath
12,700+unit and integration tests; every merge request runs the fast suite on Python 3.11, 3.12 and 3.13
Open sourceGPL-3.0, developed in the open: issues and merge requests welcome, see how to contribute
Reproducibleevery figure on this page is regenerated by a script or notebook in the repository

Heritage

Pyralysis grew out of gpuvmem, a C++/CUDA maximum-entropy imager that ran 1000 to 5000 times faster than a sequential CPU version. Pyralysis keeps that optimization-first approach and rebuilds it in Python, so it is open to extension, to the community, and to the data volumes of the next generation of telescopes.

Cárcamo, Román, Casassus, Moral & Rannou (2018), Multi-GPU maximum entropy image synthesis for radio astronomy, Astronomy and Computing 22, 16–27.

In code

Compose, don't configure

Objects you put together, not a parameter file: an array, a sky, noise, a weighting and an imager.

dirty_image.py
from importlib.resources import files
from astropy import units as u
from pyralysis.io.antenna_config_io import AntennaConfigurationIo
from pyralysis.simulation import Simulator
from pyralysis.models.sky import PointSource
from pyralysis.injectors import ThermalNoiseInjector
from pyralysis.transformers.weighting_schemes import Natural
from pyralysis.transformers import DirtyMapper

cfg = files("pyralysis.simulation") / "antenna_configs" / "vla.c.cfg"
interferometer = AntennaConfigurationIo(input_name=str(cfg)).read()
interferometer.configure_observation(
    min_frequency=1e9 * u.Hz, max_frequency=1.1e9 * u.Hz, frequency_step=1e7 * u.Hz,
    right_ascension="12h00m00s", declination="45d00m00s",
    integration_time=10 * u.s, observation_time="1h",
)
source = PointSource(
    reference_intensity=1.0 * u.Jy, sky_position="12h00m00s 45d00m00s",
    reference_frequency=1e9 * u.Hz,
)
dataset = Simulator(interferometer=interferometer, sources=source).simulate(create_dataset=True)
dataset = ThermalNoiseInjector(system_temperature=50, integration_time=10, channel_bandwidth=1e6).apply(dataset)

imsize, cellsize = 128, dataset.theo_resolution / 3.5
Natural(imsize=imsize, cellsize=cellsize, input_data=dataset).transform()
dataset.calculate_psf()
dirty, _beam = DirtyMapper(input_data=dataset, imsize=imsize, cellsize=cellsize, stokes=["I"]).transform()

Install

Python 3.11–3.13. A few dependencies come from the SKA package index.

shell
pip install --extra-index-url https://artefact.skao.int/repository/pypi-internal/simple pyralysis[all]

GPU: micromamba create -f envs/cuda13.yml (Maxwell/Pascal/Volta: cuda12.yml, Kepler: cuda11.yml). See the installation guide.

Built on the scientific Python stack

Project activity

4GitLab stars
13forks
398PyPI downloads, last 30 days
GitLab stars over time Public forks over time PyPI downloads per day

Updated 2026-10-20 09:08 UTC, daily and on every merge. Who forks Pyralysis →

Contribute

Build Pyralysis with us

Pyralysis is developed in the open on GitLab. A new regularizer, data term, kernel, line search or solver is a small class with its own tests, so a first contribution can be a focused one. Bug reports, documentation and reviews count too.

development setup
git clone https://gitlab.com/clirai/pyralysis.git && cd pyralysis
uv venv --python 3.12   # or 3.11 / 3.13
uv pip install -e ".[test]"
pre-commit install
pytest -n auto -m "not gpu and not slow" tests/unit/

Extending Pyralysis: write your own term or strategy. Coding conventions: CONVENTIONS.md.

Cite Pyralysis

If Pyralysis helps your research, please cite it.

BibTeX
@software{carcamo2021pyralysis,
  author = {Miguel Cárcamo},
  title = {Pyralysis: A Python framework for radio interferometric imaging and simulation},
  year = {2021},
  url = {https://gitlab.com/clirai/pyralysis},
  note = {https://pyralysis.readthedocs.io/}
}