virgil.ensemble
Ensembles of randomized image reconstructions in the manner of PYRA and MYTHRA (Drevon et al. 2025): draw regularizer families, weights, pixel sizes, fields and starting images; fit each group as an L-curve; select the members that fit the data and average them into a mean image with a per-pixel spread. See the tutorial Imaging, part 7.
Ensembles of randomized image reconstructions, averaged into one image.
A single regularized reconstruction depends on choices the data do not fix: the regularizer and its weight, the pixel size, the field and the starting image. Drevon et al. (2025, arXiv:2609.15365), who won the 2024 interferometric imaging contest, run many reconstructions with these choices drawn at random (their PYRA), keep those that fit the data, and average them while the average still fits (their MYTHRA). The mean is less sensitive to any one choice than a single reconstruction, and the spread of the members is a map of how much the image depends on those choices. This module does the same with virgil's own fits; it is written from the paper's description, not from their code.
The work is split so that a cluster can run it in parallel:
draw_groupsdraws the reconstruction settings. Each group has one geometry (pixel size and number of pixels), one regularizer family, one starting image and several weights.run_groupfits one group, as anl_curveover its weights. The weights are traced, so a group compiles once, and groups that share a geometry and a family share the compilation.combineselects the members and averages them into anEnsemble.
ensemble runs all three in turn. On a
cluster, run one group per array task (run_group(data,
draw_groups(data, n, key, spec)[task])), save the
Groups and combine them in one more job.
The ensemble's standard deviation is not a posterior uncertainty. It measures how much the image changes between reasonable reconstruction choices, not the noise in the data; for the posterior, sample an image (see the tutorial "Imaging, part 5").
FAMILIES = {'tv': TV, 'tsv': TSV, 'maxent': MaxEntropy, 'starlet': StarletL1}
module-attribute
EnsembleSpec
dataclass
What an ensemble draws at random, and how it selects members.
Attributes:
| Name | Type | Description |
|---|---|---|
families |
tuple of str
|
Regularizer families to draw from, uniformly: |
weight_ranges |
dict
|
For each family, the range |
n_weights |
int
|
Weights per group, at least three (the L-curve's corner needs them). |
oversample |
tuple of float
|
Pixels per Nyquist pixel
( |
field_factors |
tuple of float
|
The field is |
starts |
tuple of str
|
Starting images, drawn uniformly: |
max_npix |
int
|
Largest number of pixels on a side. A field that would need more
keeps its size and takes coarser pixels instead, so its pixels per
Nyquist pixel fall below |
window_dex |
float
|
Width of the window of weights kept in each group, in dex, from
|
max_chi2_red |
float
|
Drop a member if the raw χ² per data point of any dataset exceeds this. |
chi2_ratio |
float
|
Drop a member if, on any dataset, its χ² per data point exceeds this multiple of the best member's on that dataset. With miscalibrated errors no member reaches χ²/N ≈ 1, so a relative threshold is the one that bites. |
mad_cut |
float
|
Then drop members whose total χ² per data point lies more than this many robust standard deviations (1.4826 times the median absolute deviation) above the median. |
max_shift_mas |
float or None
|
Without a star, the members are recentred on the best one, searching shifts up to this (default: the beam's major axis). With a star, the star fixes the position and they are not shifted. |
mean_rtol |
float or None
|
A member joins the mean if, on every dataset, the mean's χ² stays
within this fraction of the best member's. The default, |
min_kept |
int
|
|
families = ('tv', 'tsv', 'maxent', 'starlet')
class-attribute
instance-attribute
weight_ranges = dataclasses.field(default_factory=_default_weight_ranges)
class-attribute
instance-attribute
n_weights = 8
class-attribute
instance-attribute
oversample = (2.0, 3.0, 4.0)
class-attribute
instance-attribute
field_factors = (1.0, 2.0, 4.0)
class-attribute
instance-attribute
starts = ('moments', 'flat')
class-attribute
instance-attribute
max_npix = 128
class-attribute
instance-attribute
window_dex = 1.0
class-attribute
instance-attribute
max_chi2_red = onp.inf
class-attribute
instance-attribute
chi2_ratio = 2.0
class-attribute
instance-attribute
mad_cut = 5.0
class-attribute
instance-attribute
max_shift_mas = None
class-attribute
instance-attribute
mean_rtol = None
class-attribute
instance-attribute
min_kept = 3
class-attribute
instance-attribute
__init__(families=('tv', 'tsv', 'maxent', 'starlet'), weight_ranges=_default_weight_ranges(), n_weights=8, oversample=(2.0, 3.0, 4.0), field_factors=(1.0, 2.0, 4.0), starts=('moments', 'flat'), max_npix=128, window_dex=1.0, max_chi2_red=onp.inf, chi2_ratio=2.0, mad_cut=5.0, max_shift_mas=None, mean_rtol=None, min_kept=3)
__post_init__()
Draw
dataclass
The settings of one group of reconstructions.
Attributes:
| Name | Type | Description |
|---|---|---|
index |
int
|
Position of the group in the ensemble. |
family |
str
|
The regularizer family. |
npix |
int
|
Pixels on a side. |
pixel_scale_mas |
float
|
Pixel size in mas. |
start |
str
|
The starting image. |
weights |
tuple of float
|
The regularizer weights, largest first. |
index
instance-attribute
family
instance-attribute
npix
instance-attribute
pixel_scale_mas
instance-attribute
start
instance-attribute
weights
instance-attribute
geometry
property
(family, npix, pixel_scale_mas): groups sharing it share a
compilation.
__init__(index, family, npix, pixel_scale_mas, start, weights)
Group
dataclass
Member
dataclass
One reconstruction in an ensemble.
Attributes:
| Name | Type | Description |
|---|---|---|
draw |
Draw
|
The settings of its group. |
weight |
float
|
Its regularizer weight. |
result |
FitResult
|
The fit. |
chi2_red |
tuple of float
|
Raw χ² per data point of each dataset (with the quoted errors). |
kept |
bool
|
Whether it is in the mean image. |
reason |
str or None
|
Why it was left out: |
draw
instance-attribute
weight
instance-attribute
result
instance-attribute
chi2_red
instance-attribute
kept = False
class-attribute
instance-attribute
reason = None
class-attribute
instance-attribute
total_chi2_red
property
χ² per data point over all datasets.
__init__(draw, weight, result, chi2_red, kept=False, reason=None)
Ensemble
dataclass
The result of combine.
Attributes:
| Name | Type | Description |
|---|---|---|
model |
System
|
The mean scene, exactly: the kept members' images on their own
grids ( |
mean |
Image
|
The mean extended emission, on the common grid (the finest pixels
and the largest field of the kept members). Its brightness sums to
one; with a star its |
std |
(array, shape(npix, npix))
|
Standard deviation across the kept members of the same quantity as
|
chi2_red |
tuple of float
|
Raw χ² per data point of the mean scene on each dataset. |
trace |
list of tuple
|
|
members |
list of Member
|
Every reconstruction, kept or not. |
groups |
list of Group
|
The groups, with their L-curves. |
model
instance-attribute
mean
instance-attribute
std
instance-attribute
chi2_red
instance-attribute
trace
instance-attribute
members
instance-attribute
groups
instance-attribute
kept
property
The members in the mean.
__init__(model, mean, std, chi2_red, trace, members, groups)
summary()
A short table: the groups, what was kept, and the mean's fit.
draw_groups(data, n_groups, key, spec=None)
Draw the settings of n_groups groups of reconstructions.
Each group draws a regularizer family, a pixel size (the Nyquist scale
over one of spec.oversample), a field (field_of_view(data)
times one of spec.field_factors, at most 1 on a uv lattice; beyond
spec.max_npix pixels the pixels are coarsened to fit it, down to
the Nyquist scale), a starting image and
spec.n_weights log-uniform weights, one in each equal bin of the
family's range in log w. The draws
depend only on key and spec, so every task of a cluster array
can draw them all and run its own.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
OIData or sequence of OIData
|
The data. |
required |
n_groups
|
int
|
Number of groups. |
required |
key
|
PRNGKey
|
The random key. |
required |
spec
|
EnsembleSpec
|
What to draw (default |
None
|
Returns:
| Type | Description |
|---|---|
list of Draw
|
Sorted by geometry, so that groups sharing a compilation run one after the other. |
reference_starts(data, star=True, starts=('moments', 'flat'))
The starting images that each group resamples onto its own grid.
Each is made once by starting_image,
which fits a star plus a Gaussian envelope: "moments" is that
Gaussian, "dirty" the positive part of the dirty image, and
"flat" a uniform image with the fitted flux.
Returns:
| Type | Description |
|---|---|
dict
|
Start name to an |
run_group(data, draw, *, star=True, starts=None, **fit_options)
Fit one group of an ensemble: an L-curve over its weights.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
OIData or sequence of OIData
|
The data. |
required |
draw
|
Draw
|
The group, from |
required |
star
|
bool
|
Whether the scene is an unresolved star at the origin plus the
image (default), or the image alone, centred by a
|
True
|
starts
|
dict
|
From |
None
|
**fit_options
|
{}
|
Returns:
| Type | Description |
|---|---|
Group
|
|
combine(data, groups, *, spec=None, star=True)
Select the members of an ensemble and average them.
The selection follows MYTHRA (Drevon et al. 2025):
- In each group, keep the weights from
spec.window_dexbelow the L-curve's corner up to the corner: the weights just before the turnover. - Drop members whose fit diverged (a non-finite χ²), then keep
members whose raw χ² per data point is below
spec.max_chi2_redand withinspec.chi2_ratioof the best member's on every dataset, then drop total-χ² outliers by their median absolute deviation (spec.mad_cut). - Resample the survivors' images to a common grid, the finest pixels
and the largest field among them, conserving flux; without a star,
recentre each on the best member
(
align). - In order of total χ², add members to a running mean one at a time,
keeping each only if the mean's χ² on every dataset stays within
spec.mean_rtol(by default the χ²/N noise, √(2/N)) of the best member's (so visibilities and closure phases, given as separate datasets, are judged separately). Judging against the best member, not the running mean, keeps the tolerance from compounding. The running mean is judged as the mixture of the members' images on their own grids, which is exact; resampling to the common grid smooths them, which on precise data can raise χ² several-fold and so let worse members through.
With a star, the mean is of the whole normalized sky, star included: the star's fraction of the flux is the members' mean, and the image's pixels the mean of their fluxes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
OIData or sequence of OIData
|
The data the groups were fitted to. |
required |
groups
|
sequence of Group
|
From |
required |
spec
|
EnsembleSpec
|
The selection settings (default |
None
|
star
|
bool
|
As for |
True
|
Returns:
| Type | Description |
|---|---|
Ensemble
|
|
ensemble(data, n_groups, key, *, spec=None, star=True, **fit_options)
Run, select and average an ensemble of randomized reconstructions.
draw_groups, then
run_group for each group in turn, then
combine. Each group is one L-curve, so the
ensemble has n_groups * spec.n_weights members, and compiles once
per distinct (family, npix, pixel_scale_mas): a handful, since
each is drawn from a short list.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
OIData or sequence of OIData
|
The data. Give visibilities and closure phases as separate datasets for the mean to be judged on each. |
required |
n_groups
|
int
|
Number of groups. |
required |
key
|
PRNGKey
|
The random key. |
required |
spec
|
EnsembleSpec
|
What to draw and how to select (default |
None
|
star
|
bool
|
Whether the scene has an unresolved star at the origin (default). |
True
|
**fit_options
|
Passed to |
{}
|
Returns:
| Type | Description |
|---|---|
Ensemble
|
|