Wenuray
Radio-interferometric self-calibration
Wenuray is a Python framework for radio-interferometric self-calibration. Instead of rewriting a calibration script for each dataset, you describe the procedure once, as a short TOML recipe or a few Python objects, and Wenuray runs it: imaging with CASA tclean, Pyralysis or your own imager, solving phases and amplitudes round after round, and keeping only the rounds that improve the image. It works with any radio interferometer.
About the name
Wenuray (also written Huenuray) is a Mapuche woman's name. In Mapudungun it means "flower of the sky": wenu, the sky, and rayen, flower. The radio sky is what Wenuray brings into focus, and its roots are in Chile, home of many of the world's radio telescopes.
Wenuray is dedicated to the women of radio astronomy. Each major release carries one of their names, beginning with v1.0 "Payne-Scott", after Ruby Payne-Scott (1912–1981), the first woman radio astronomer and a pioneer of radio interferometry.
Wenuray was called snow (caSa pythoN self-calibratiOn frameWork) before version 1.0.
Features
- Object-oriented design: Clean, modular architecture for easy customization
- Multiple imagers supported: CASA tclean, Pyralysis, or your own
- Flexible self-calibration: Phase-only, amplitude-only, or combined amplitude-phase calibration
- CASA integration: Built on CASA (Common Astronomy Software Applications) for radio astronomy data processing
- Reproducible workflows: Version-controlled calibration pipelines
- Docker support: Pre-built container images for easy deployment
Requirements
- Python:
>=3.12, <3.14, the versions with both CASA wheels and Pyralysis support - Dependencies: casatools, casatasks (CASA >= 6.7), numpy, astropy and reproject. The
package declares lower bounds;
requirements.txtpins the set tested in CI and used in the Docker images. - CASA data: CASA downloads its measures data the first time
casatoolsis imported (see External Data). Without network access, pointmeasurespathto an existing copy.
On Debian, Ubuntu or Arch, the manylinux_2_34 casatools wheel that pip picks fails to import
(OPENSSL_3.0.1 not found). Install from requirements.txt, which uses the working
manylinux_2_28 wheel, or install that wheel first.
Installation
Option 1: From PyPI (Recommended)
pip install wenuray
Release candidates are on PyPI too, and pip installs them only when asked:
pip install --pre wenuray.
Option 2: From GitHub
pip install -U git+https://github.com/miguelcarcamov/wenuray.git
Option 3: Using Conda
Create a conda environment from the provided environment file:
conda env create -f environment.yml
conda activate wenuray-env
The environment provides Python 3.12 and the pinned dependencies from requirements.txt; then
install Wenuray itself with pip install -e . (from a clone) or pip install wenuray.
Option 4: From Source
git clone https://github.com/miguelcarcamov/wenuray
cd wenuray
pip install .
Option 5: Development Installation
For development work with editable installation:
git clone https://github.com/miguelcarcamov/wenuray
cd wenuray
pip install -e .
Option 6: Docker Container
Pull the pre-built Docker image:
docker pull ghcr.io/miguelcarcamov/wenuray:latest
The image has Python 3.12, Wenuray with its pinned dependencies, and CASA's measures data and
observatory table in /opt/casa/data, readable by any user (also under Apptainer/Singularity). To stay small it has
no CUDA, and it skips the rest of casarundata (antenna responses and simulator files, used by
e.g. the awproject gridder); mount a full copy at /opt/casa/data if you need it:
docker run --rm -v "$PWD":/work -w /work ghcr.io/miguelcarcamov/wenuray:latest python main_ALMA.py target.ms out
CASA needs a writable home directory. Apptainer/Singularity provides yours; with docker run -u,
also pass -e HOME=/tmp.
wenuray.utils.reproject() needs the optional reproject package: pip install "wenuray[reproject]".
Quick Start
Basic Usage Example
Phase, then amplitude-phase self-calibration of an ALMA dataset. This is
examples/alma/selfcal.py; a test keeps the two identical.
"""
Phase, then amplitude-phase self-calibration of an ALMA dataset imaged with tclean.
Usage: python selfcal.py <visfile> <output_directory> [--plot]
"""
import argparse
import logging
from casatasks import split
from wenuray.config import ImageGeometry, SolveOptions, Weighting
from wenuray.imaging import Tclean
from wenuray.selfcalibration import AmpPhasecal, GainPlotObserver, LoggingObserver, Phasecal, Selfcal
def parse_arguments():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("visfile", help="Measurement set with the calibrated data in DATA")
parser.add_argument("output", help="Directory for the images, tables and the result record")
parser.add_argument("--plot", action="store_true", help="Plot the gain solutions")
return parser.parse_args()
if __name__ == '__main__':
args = parse_arguments()
# Wenuray logs each self-calibration round's metrics at INFO
logging.basicConfig(level=logging.INFO)
# tclean's own parameter names. Automasking values for long baselines:
# https://casaguides.nrao.edu/index.php/Automasking_Guide
imager = Tclean(
ImageGeometry(imsize=1024, cell="0.005arcsec"),
Weighting("briggs", robust=0.5),
niter=100,
threshold="0.1mJy",
specmode="mfs",
deconvolver="hogbom",
gridder="standard",
usemask="auto-multithresh",
sidelobethreshold=3.0,
noisethreshold=5.0,
minbeamfrac=0.3,
lownoisethreshold=1.5,
negativethreshold=0.0,
interactive=False # True to edit the masks by hand
)
# Phases combined over spectral windows, then amplitudes and phases per spectral window,
# cleaned deeper. Each spwmap is derived from combine.
steps = [
Phasecal("inf", solve={"combine": "spw"}),
Phasecal("600s", solve={"combine": "spw"}),
AmpPhasecal("inf", imaging={"threshold": "0.025mJy"}),
]
selfcal = Selfcal(
args.visfile,
imager,
steps,
solve=SolveOptions(refant="DA51", minblperant=6, minsnr=3.0, gaintype="T"),
output=args.output,
)
observers = [LoggingObserver()] + ([GainPlotObserver()] if args.plot else [])
result = selfcal.run(observers)
# The self-calibrated data, for final imaging
split(vis=result.ms, outputvis=args.output + "/selfcal.ms", datacolumn="corrected")
Run the script:
python examples/alma/selfcal.py target.ms target_selfcal
The same run as a TOML recipe
A recipe is the same objects written as TOML: each table is a constructor and its keys are that
class's arguments, so a recipe and a script build equal objects and raise the same errors. The
imager, the steps, the acceptance policies and the flagging name their class with type.
ms = "target.ms" # paths are relative to the recipe
output = "target_selfcal"
[solve]
refant = "DA51"
minblperant = 6
[imager]
type = "tclean"
niter = 100
threshold = "0.1mJy"
deconvolver = "hogbom"
usemask = "auto-multithresh"
geometry = { imsize = 1024, cell = "0.005arcsec" }
weighting = { weighting = "briggs", robust = 0.5 }
[[steps]]
type = "phase" # Phasecal; "amp" is Ampcal, "ampphase" AmpPhasecal
solint = "inf"
solve = { combine = "spw" }
[[steps]]
type = "phase"
solint = "600s"
solve = { combine = "spw" }
[[steps]]
type = "ampphase"
solint = "inf"
imaging = { threshold = "0.025mJy" }
selfcal = Selfcal.from_toml("recipe.toml") # an ordinary Selfcal: inspect it, change it, run it
selfcal.to_toml("copy.toml") # and back; values equal to the defaults are left out
Or from the command line:
wenuray init > recipe.toml # a generic recipe to start from
wenuray validate recipe.toml # builds the run and checks it, without touching the data
wenuray show --defaults recipe.toml # the resolved recipe: presets laid in, every value shown
wenuray run recipe.toml --plots # runs it, plotting the gain solutions
wenuray run recipe.toml --ms other.ms # the same procedure on another dataset
Errors are the same as from Python, e.g. a misspelt refant or an amplitude step before any
phase step, and wenuray exits with status 1.
Reproducing a run
Every run records how to repeat it in its output directory:
recipe.toml: the run as a resolved recipe, written before anything runs, with presets laid in and every default written out, so later changes to Wenuray's defaults can't change it.selfcal.toml: the rounds and accepted tables, plus a[provenance]table with the start time, a fingerprint of the input measurement set (a hash of its metadata and of sampledDATAandFLAGrows) and the versions of Python, Wenuray, CASA and the imager's packages.
wenuray run target_selfcal/recipe.toml --ms fresh_copy.ms --output rerun
reruns it on a fresh copy of the input. A run Wenuray can't write as a recipe, e.g. one with a
Check(name, lambda ...) policy, still runs; it logs a warning and records no recipe.
preset = "group.toml" lays a recipe over another TOML file, e.g. the solve options and imager
settings your group shares: tables merge key by key and the recipe wins. Wenuray ships no presets,
since its defaults come from theory or the data rather than from an instrument. A class of your
own, e.g. an acceptance policy, is named as type = "package.module:Class". Policies built with
Check(name, function) can't be written to a recipe; make them a class.
How a run is described:
- One
Selfcal, a list of steps. Each step is one round:Phasecal,AmpcalorAmpPhasecalwith its solution interval. A step can override the run's solve options (solve={"minsnr": 5.0}), the imager's options (imaging={"nsigma": 2.0}) or the acceptance policy (acceptance=...), and can flag residual outliers before imaging (flagging=ResidualFlagging("rflag"), off by default). - Invalid schedules fail before any I/O. An amplitude step needs a phase solution before it,
in the schedule or in
initial_stack; otherwiseSelfcal(...)raisesScheduleError. - Every accepted table stays applied. Each solve pre-applies all accepted tables, each with
its own
spwmap(derived fromcombine) andinterp. - A round is kept only if it improves the image. By default the PSNR must rise and every solution interval must keep enough antennas (3 for phases, 4 for amplitudes), and amplitude rounds must not lose model flux. A rejected round is rolled back and stops the run.
- Each solve is checked first. Before
gaincal, Wenuray predicts a solution's S/N for the step's solint and combine from the data: the median model amplitude over the noise of one visibility, times √(N_ant − 1) · √(n_int · n_chan · n_pol). It warns when the prediction is belowminsnr, giving the shortest solint that would reach it, and records it aspredicted_snr. - Continue later with
Selfcal.resume(result_or_output_directory, imager, more_steps), or start from tables of your own withinitial_stack=CalStack([CalTable("pcal", "p")]). result.finalis the last accepted round; every round's metrics, decision and reasons are inresult.roundsand in<output>/selfcal.toml.
What the input must look like:
- Calibrated data in
DATA.gaincalsolves againstDATA, while the imager imagesCORRECTED_DATA. Split the calibrated target first (split(vis=..., outputvis=..., datacolumn="corrected")). A run resetsCORRECTED_DATAtoDATA(plusinitial_stack) and warns if it held something else. - One selection.
Selection(field=..., spw=..., uvrange=...)is used by the solve, the apply and the imager. applymode. The defaultApplyOptions(applymode="calflag")flags data whose solutions failminsnr."calonly"leaves those data uncalibrated but unflagged.
More examples, for the VLA and e-MERLIN too, are in examples/, each as a recipe and
the script that builds the same run. Code written for snow, the name before
Wenuray 1.0, raises an error that shows the new call; see the migration guide.
API Overview
Self-calibration (wenuray.selfcalibration)
Selfcal: the self-calibration loop;run()returns aSelfcalResult, andresume()continues one.Phasecal,Ampcal,AmpPhasecal: calibration steps.- Acceptance policies:
PSNRImproves,EnoughAntennas,FluxNotLost,BeamStable,FlaggedFractionBelow,Check(name, predicate)for your own, combined withAllOf/AnyOf. - Observers:
LoggingObserver(the default) andGainPlotObserver, passed torun().
Configuration (wenuray.config)
Frozen objects with CASA's parameter names: Selection, SolveOptions, ApplyOptions,
ImageGeometry and Weighting (natural by default).
Imaging (wenuray.imaging)
An imager images CORRECTED_DATA and writes its model to MODEL_DATA; Imager.image() enforces
both and raises ImagerContractError otherwise. It also raises ValueError, before imaging, when
the cell is coarser than the Nyquist cell of the selected data: the image grid can't hold the
longest baselines, so the model written for them would mislead the next solve. The message gives
the largest cell that works, and the Selection(uvrange=...) that leaves those baselines out of
the solve, the apply and the image alike.
-
Tclean(geometry, weighting, **tclean_options): CASA tclean, with tclean's own parameter names. -
Pyralysis(geometry, weighting, terms=[...], optimizer=..., **pyralysis_options): optimization-based reconstruction with Pyralysis, with its own names for terms ("chi_squared","l1norm:1e-3"), the optimizer ("lbfgsb","fista") and options (max_iter,mask_mode, ...). Each reconstruction runs in a separate process, since Pyralysis's casacore and CASA's can't share one. Give it a mask (mask_mode="file",mask_path=...): a model free in every pixel can absorb the gain errors self-calibration should solve for. Wenuray needs Pyralysis 3.2.1 or later, from PyPI; itsska-ost-array-configdependency comes from SKA's index:pip install "wenuray[pyralysis]" \ --extra-index-url https://artefact.skao.int/repository/pypi-internal/simple
A
Selection(uvrange=...)works with Pyralysis in metres or km (e.g."<3090m", the form the Nyquist check suggests); a range in wavelengths is refused, since it depends on each channel's frequency.Numba needs OpenMP's runtime (
libgomp1, present on most systems and in Wenuray's images) or TBB for Pyralysis's parallel kernels; Wenuray checks for it when the imager is created. -
Your own imager: subclass
Imagerand implement_reconstruct, wrap a function inCallableImager, or register it under thewenuray.imagersentry point to build it by name.wenuray.testing.ImagerContractSuitechecks that it follows the contract.
Project Structure
wenuray/
├── src/wenuray/
│ ├── config.py # Selection, SolveOptions, ApplyOptions, ImageGeometry, Weighting
│ ├── imaging/ # Imager contract, tclean backend, image statistics
│ ├── selfcalibration/ # The Selfcal loop, steps, schedule rules, acceptance, observers
│ ├── testing.py # Simulated data and the imager contract suite
│ └── utils/ # Utility functions
├── examples/ # Recipe + script pairs (ALMA, VLA, e-MERLIN)
├── docs/ # Sphinx documentation and the migration guide
├── requirements.txt # Pinned dependencies tested in CI
└── pyproject.toml # Project metadata and build configuration
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the terms specified in the LICENSE file.
Citation
If you use Wenuray in your research, please cite:
@software{wenuray,
author = {Cárcamo, Miguel},
title = {Wenuray: radio-interferometric self-calibration},
url = {https://github.com/miguelcarcamov/wenuray},
version = {<version>},
year = {<year>}
}
Support
- Issues: Report bugs or request features on GitHub Issues
- Documentation: See the CASA documentation for CASA-specific questions
- Contact: miguel.carcamo@usach.cl
Acknowledgments
Wenuray is built on top of CASA (Common Astronomy Software Applications), developed by the National Radio Astronomy Observatory (NRAO).
Metadata
Release files for wenuray 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| wenuray-1.0.0.tar.gz | 172.2 kB | With provenance | Details for wenuray-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| wenuray-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for wenuray-1.0.0-py3-none-any.whl |
Total release size: 259.6 kB
Release files / wenuray-1.0.0.tar.gz
| Download URL | wenuray-1.0.0.tar.gz |
|---|---|
| Size | 172.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c9f4db4f3e7281569e3a70db3b8530bd408adfe1d799d41ad3b723b3d3b97057
|
|
BLAKE2b-256 checksum How to use checksums |
4112ab9522df657ad68ec7ad99374568cf4a6a7b51182dabae63ba62eab01603
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logRelease files / wenuray-1.0.0-py3-none-any.whl
| Download URL | wenuray-1.0.0-py3-none-any.whl |
|---|---|
| Size | 87.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
57580bedad6edcacb53d8511c9d28e7e5e92606c86ce8062a39bdcea2c26a52f
|
|
BLAKE2b-256 checksum How to use checksums |
82fda2fb4bdca0e6a38f9c5cf04c8956e6d578113e5a02ef58b666a4ad22ba0b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logSecurity for wenuray 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for wenuray-1.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.