Skip to content

virgil.imaging

Regularizers, priors and helpers for image reconstruction with Image. Ways to choose a regularization weight are the L-curve's corner, the discrepancy principle, classic MaxEnt (LCurve.classic_maxent) and, for Gaussian-field images, the Laplace evidence (log_evidence).

Regularizers and helpers for image reconstruction.

An image fit is a call to fit whose model contains an Image, with a prior on its log_brightness (see :func:image_priors) and usually a regularizer, which adds a penalty on the pixel fluxes b to the loss:

Regularizer Penalty Least squares (LM)? A prior density?
TSV w Σ (Δx b)² + (Δy b)² yes no
TV w Σ √((Δx b)² + (Δy b)² + ε²) no no
MaxEntropy w Σ b log(b / q) no no
Laplacian w Σ (∇²b)² yes no
StarletL1 w Σ √(s² + ε²) over starlet details s no no
LogSum w Σ log(1 + b / (ε b̄)) no no
Centroid ½ |centroid / σ|² yes yes

Differences Δ are between neighbouring pixels, with zeros beyond the edges, so edge pixels are penalized too. TSV (total squared variation) and the Laplacian favour smooth images, TV (total variation) piecewise-flat ones, and maximum entropy images close to a default q. The weight w depends on the scene and the data; :func:l_curve sweeps it.

Sparse images, made of a few compact features, need a different penalty. An L1 norm of the pixels does not work: they are positive and sum to one, so Σ |b| = 1 for every image. StarletL1 is an L1 norm of the image's wavelet (starlet) coefficients instead, which favours images built from few compact structures at any scale. LogSum is a smooth surrogate for the number of bright pixels (the L0 "norm" of SQUEEZE). It is not convex, so the fit can stop in a local minimum: start it from a good image, such as a clean model.

clean builds a sparse image directly, from point components added one at a time where the gradient of χ² is steepest: CLEAN for any data, including closure and DISCO phases. With scales_mas the components can also be Gaussians of several widths (multi-scale CLEAN).

When fit's model function returns one model per dataset, every regularizer acts on the first model only. That is right when the later models are transformed copies of the same scene (e.g. a Rotated epoch), but an Image that appears only in a later model is not regularized at all.

Closure, kernel and DISCO phases do not fix an image's position. Something must: an analytic star at the origin, a Centroid prior, or a centred prior mean. diagnose checks a fit for this and other common pitfalls.

TSV

Bases: _ImageRegulariser

Total squared variation: weight * Σ (Δx b)² + (Δy b)².

A quadratic smoothness penalty, so it can be fitted by Levenberg–Marquardt.

Parameters:

Name Type Description Default
weight float

Strength of the penalty.

required
path str

Path of the Image in the model (e.g. "env"); None if the model is the Image itself.

None

weight = np.asarray(weight, dtype=float) instance-attribute

path = path instance-attribute

__init__(weight, path=None)

residuals(model)

value(model)

TV

Bases: _ImageRegulariser

Total variation: weight * Σ √((Δx b)² + (Δy b)² + ε²).

Favours piecewise-flat images with sharp edges. ε smooths the penalty where the image is flat, so that it is differentiable. The penalty is averaged over the four flips of the image, so it does not depend on which neighbour each forward difference pairs a pixel with, and is the same for an image and its mirror images. It is not isotropic: as for any total variation built from pixel differences, a sharp edge along a diagonal costs about a fifth more per unit length than one along a pixel axis.

Parameters:

Name Type Description Default
weight float

Strength of the penalty.

required
epsilon float

Smoothing scale, as a fraction of the mean pixel flux (default 1e-2).

0.01
path str

Path of the Image in the model.

None

weight = np.asarray(weight, dtype=float) instance-attribute

epsilon = np.asarray(epsilon, dtype=float) instance-attribute

path = path instance-attribute

__init__(weight, epsilon=0.01, path=None)

value(model)

MaxEntropy

Bases: _ImageRegulariser

Maximum entropy: weight * Σ b log(b / q), with b and q unit-sum.

The relative entropy of the image with respect to a default image q. It is zero for b = q and positive otherwise.

Parameters:

Name Type Description Default
weight float

Strength of the penalty.

required
prior array - like

The default image q, with the image's shape and positive where the image's support is; normalized here. Default: flat over the support.

None
path str

Path of the Image in the model.

None

weight = np.asarray(weight, dtype=float) instance-attribute

prior = None if prior is None else np.asarray(prior) instance-attribute

path = path instance-attribute

__init__(weight, prior=None, path=None)

value(model)

Laplacian

Bases: _ImageRegulariser

Squared Laplacian: weight * Σ (∇²b)².

∇²b is the five-point Laplacian, with zeros beyond the edges (so it is evaluated on a ring of pixels around the image too). A quadratic penalty on curvature, smoother than TSV, so it can be fitted by Levenberg–Marquardt.

Parameters:

Name Type Description Default
weight float

Strength of the penalty.

required
path str

Path of the Image in the model.

None

weight = np.asarray(weight, dtype=float) instance-attribute

path = path instance-attribute

__init__(weight, path=None)

residuals(model)

value(model)

StarletL1

Bases: _ImageRegulariser

L1 norm of the starlet details: weight * Σ √(s² + ε²).

s are the detail coefficients of :func:starlet, at every scale. An L1 norm favours few non-zero coefficients, so the image is built from few compact structures, of any size: a sparse image in the wavelet sense. The coarse plane, which carries the flux, is not penalized. ε smooths the penalty near zero, so that it is differentiable.

Parameters:

Name Type Description Default
weight float

Strength of the penalty.

required
scales int

Number of detail planes (default 4); the largest holds structure about 2**(scales - 1) pixels across.

4
epsilon float

Smoothing scale, as a fraction of the mean pixel flux (default 1e-2).

0.01
path str

Path of the Image in the model.

None

weight = np.asarray(weight, dtype=float) instance-attribute

scales = int(scales) class-attribute instance-attribute

epsilon = np.asarray(epsilon, dtype=float) instance-attribute

path = path instance-attribute

__init__(weight, scales=4, epsilon=0.01, path=None)

value(model)

LogSum

Bases: _ImageRegulariser

Log-sum sparsity: weight * Σ log(1 + b / (ε b̄)).

b̄ is the mean pixel flux over the support. A pixel costs about log(b / (ε b̄)) once it is brighter than ε b̄, and almost nothing when fainter, so as ε → 0 the penalty counts the bright pixels: a smooth surrogate for the L0 "norm" of SQUEEZE (Candès, Wakin & Boyd 2008). It favours images with few bright pixels. It is not convex, so start the fit from a good image.

Sweep its weight with l_curve(..., warm_start=False). By default :func:l_curve starts each fit from the previous, stronger one; a strong weight switches most pixels off, and pixels driven that dark (in log-brightness) cannot recover, so every weaker fit would inherit the collapse.

Parameters:

Name Type Description Default
weight float

Strength of the penalty.

required
epsilon float

The flux, as a fraction of the mean pixel flux, at which a pixel starts to count (default 1e-2).

0.01
path str

Path of the Image in the model.

None

weight = np.asarray(weight, dtype=float) instance-attribute

epsilon = np.asarray(epsilon, dtype=float) instance-attribute

path = path instance-attribute

__init__(weight, epsilon=0.01, path=None)

value(model)

CleanResult dataclass

The result of :func:clean.

Attributes:

Name Type Description
model SourceModel

The base scene with the components, System(base=base, clean=Image(...)), or for a System base its components and clean side by side. The Image is non-zero only on the components, and its flux is their total relative to the base (with the components' spectrum, if one was given). Without a base scene, the Image alone; with no components, the base alone.

components (array, shape(npix, npix))

The components' total flux in each pixel of the grid, Σ_s G_s * F_s summed over the scales, relative to the base scene's weight (without a base scene, normalized to unit sum).

pixel_scale_mas float

Pixel size in milliarcseconds.

chi2_red array

χ² per data point before each iteration; the last entry is that of the final model.

stop str

Why CLEAN stopped: "target" (χ² per point reached target_chi2_red), "stalled" (χ² stopped falling, even after a major cycle) or "max_iterations".

components_by_scale (array, shape(n_scales, npix, npix))

The components' fluxes F_s at each scale, at the pixel at the centre of their shape, in the units of components. Each sums to that scale's share of the total flux.

scales_mas tuple of float

The FWHM of each scale's Gaussian shape (0 for a point).

model instance-attribute

components instance-attribute

pixel_scale_mas instance-attribute

chi2_red instance-attribute

stop instance-attribute

components_by_scale = None class-attribute instance-attribute

scales_mas = (0.0,) class-attribute instance-attribute

__init__(model, components, pixel_scale_mas, chi2_red, stop, components_by_scale=None, scales_mas=(0.0,))

restored(beam)

The components convolved with beam: the "restored" image.

In the orientation of the pixel grid, in the units of components. Unlike radio astronomy's restored image, it does not include the residuals.

Centroid

Bases: _ImageRegulariser

A Gaussian prior centring an Image's flux on the origin.

The penalty is ½ |c / sigma_mas|², where c is the flux-weighted centroid of the Image on the sky, including its dra/ddec. This is a genuine prior density, so it may be used when sampling.

Parameters:

Name Type Description Default
sigma_mas float

Standard deviation of the centroid, in milliarcseconds.

required
path str

Path of the Image in the model.

None

probabilistic = True class-attribute instance-attribute

weight = 1.0 instance-attribute

sigma_mas = np.asarray(sigma_mas, dtype=float) instance-attribute

path = path instance-attribute

__init__(sigma_mas, path=None)

centroid(model)

The Image's flux-weighted centroid (dra, ddec) in mas.

residuals(model)

value(model)

Beam dataclass

A Gaussian approximation to the core of the dirty beam.

Attributes:

Name Type Description
major_mas, minor_mas float

Full widths at half maximum along the major and minor axes, in mas.

pa_deg float

Position angle of the major axis, North to East, in degrees.

major_mas instance-attribute

minor_mas instance-attribute

pa_deg instance-attribute

__init__(major_mas, minor_mas, pa_deg)

LCurve dataclass

The result of :func:l_curve.

Attributes:

Name Type Description
weights array

The regularization weights, in the order fitted (largest first).

chi2 array

Total χ² of each fit.

chi2_red (array, shape(n_weights, n_datasets))

χ² per data point of each dataset.

penalty array

The unweighted regularizer, value / weight, of each fit.

results list of FitResult

The fits.

weights instance-attribute

chi2 instance-attribute

chi2_red instance-attribute

penalty instance-attribute

results instance-attribute

__init__(weights, chi2, chi2_red, penalty, results)

corner()

The weight at the L-curve's corner, where it bends most sharply.

The curve is (log χ², log penalty) parametrized by log w; the corner is the interior point of largest curvature (Hansen & O'Leary 1993). Check it by eye: the curvature of a sparse or noisy sweep is itself noisy, and a smooth curve has no clear corner. A fit that diverged (a non-finite χ² or penalty) is left out, so that its NaN cannot spread to the curvature of its neighbours.

discrepancy(target=1.0)

The weight at which χ² per data point reaches target.

This is Morozov's discrepancy principle: regularize as strongly as the data allow. With several datasets, the binding one (the largest χ² per point) must reach the target, so that a well-fitted dataset cannot hide a badly fitted one. It interpolates linearly in log w between the fitted weights, and returns None if the sweep never crosses the target. The truth itself has χ² per point of 1 ± √(2/N), so the default target of 1 is the natural one. It relies on correct error bars; with underestimated errors it over-regularizes.

classic_maxent(data, path='env')

The maximum-entropy weight of Gull and Skilling's "classic MaxEnt".

For a sweep of MaxEntropy fits, this is the weight w at which -2 w S equals the number of well-measured directions in the image, N = Σ λ / (λ + w) (Gull 1989; Skilling 1989). Here S is the entropy (minus the sweep's penalty), and the λ are the eigenvalues of the data's Gauss–Newton curvature in the entropy metric, diag(√b) JᵀJ diag(√b), with J the Jacobian of the whitened residuals with respect to the brightness b. It is the stationary point of the Laplace-approximated evidence in w, and so assumes correct error bars, like the discrepancy principle.

Both sides are evaluated at each fit in the sweep, and the crossing is interpolated linearly in log w. Returns None if the sweep does not bracket it. Like :func:log_evidence, it uses the data's quoted errors, so it rejects a sweep fitted with noise= terms or with one model per dataset.

Parameters:

Name Type Description Default
data OIData or sequence of OIData

The data the sweep was fitted to.

required
path str

Path of the regularized Image in the model (default "env").

'env'
References
  • S. F. Gull (1989), "Developments in maximum entropy data analysis", in Maximum Entropy and Bayesian Methods, Kluwer, 53–71.
  • J. Skilling (1989), "Classic maximum entropy", in Maximum Entropy and Bayesian Methods, Kluwer, 45–52. (Skilling & Bryan 1984 is the earlier "historic" MaxEnt, which stops at χ² = N.)

Diagnosis dataclass

The result of :func:diagnose.

Attributes:

Name Type Description
checks dict

Check name to value. Checks made per dataset or per Image are lists, in the order of the data or of the Images in the model.

warnings list of str

One sentence for each problem found, saying what to do about it. Empty if none.

checks instance-attribute

warnings instance-attribute

__init__(checks, warnings)

__str__()

starlet(image, scales=4)

The starlet (isotropic undecimated wavelet) transform of an image.

The à-trous algorithm with a B3-spline kernel (Starck, Murtagh & Fadili 2010): the image is smoothed scales times, the kernel's taps twice as far apart each time, and each detail plane is the difference between successive smoothings. Detail plane j holds structure about 2**j pixels across. Beyond the edges the image is zero.

Parameters:

Name Type Description Default
image (array - like, shape(ny, nx))

The image.

required
scales int

Number of detail planes (default 4).

4

Returns:

Name Type Description
details (Array, shape(scales, ny, nx))

The detail planes, finest first.

coarse (Array, shape(ny, nx))

What is left after the last smoothing. details.sum(0) + coarse is the image.

clean(data, npix, pixel_scale_mas, base=None, *, gain=0.1, max_iterations=1000, target_chi2_red=1.0, refit_every=50, stall_window=50, stall_tolerance=0.001, refresh_norms=False, base_priors=None, spectrum=None, support=None, init=None, rotation_deg=0.0, dtype='float64', scales_mas=(0.0,), scale_bias=0.0)

Build an image from components, one at a time: gradient CLEAN.

Högbom's CLEAN repeatedly finds the peak of the residual dirty image and adds a fraction (the loop gain) of a point source there. For data that are linear in the image, the residual dirty image is proportional to -∂χ²/∂c, the gradient of χ² with respect to the flux c_p of a point at each pixel. This function uses that gradient directly, so it works for any data virgil can fit: closure phases, kernel or DISCO phases, squared visibilities, or a mix. J e_p is the change in the whitened residuals per unit flux at pixel p. Each iteration picks the pixel whose Gauss–Newton step would lower χ² most, the largest g_p² / |J e_p|² with g_p < 0, and adds gain times that step, -g_p / (2 |J e_p|²). This is matching pursuit; for linear data, where |J e_p| is the same everywhere, it is exactly Högbom's CLEAN. The norms |J e_p| are computed once, at the start, with one Jacobian–vector product per pixel. Normalizing by them matters next to an analytic star, where flux in a pixel is nearly the same as the star's: the gradient there is small, but so is |J e_p|, and an unnormalized search would pile flux beside the star.

Components are added to a fixed base scene, usually an analytic star at flux 1; fit its parameters first. Their fluxes are relative to the base's, like a companion's in a System; for a System base they are siblings of its components, relative to its total. By default the components are grey, the same fraction of the base's flux at every wavelength; give a spectrum to make them follow one, as the image does in SPARCO (e.g. a star with its own spectral index and an environment with another). Without a base, the components alone make the image, starting from one at the centre of the grid (closure phases do not fix the position, so this is also the anchor).

Each iteration only adds flux, so an early step that overshoots cannot be undone by later ones. Every refit_every iterations a major cycle (as in Clark and Cotton–Schwab CLEAN) refits the fluxes of all the components at once, by non-negative least squares on the linearized residuals: flux can move between components, and components whose flux falls to zero are removed. The fluxes stay non-negative. A last major cycle runs when CLEAN stops at the target or at max_iterations, so that the fluxes it returns are refitted even when it stops early (when the data start close to the target, a major cycle may otherwise never run).

The base is fixed unless base_priors names some of its parameters, e.g. a companion's position or a star's diameter. Each major cycle then fits those together with the components' fluxes, with fit: fitting them in turn instead would let the components absorb an error in the base, or let the base absorb flux it does not model. A companion much closer to the star than λ/B has its flux and separation nearly degenerate, whatever fits it.

Multi-scale CLEAN (Cornwell 2008, IEEE JSTSP 2, 793): with several scales_mas, each component is a pixel convolved with a circular Gaussian of FWHM s (s = 0 is a point), and the image is Σ_s G_s * F_s. The search runs over every (scale, pixel), with the same score g² / |J e|², and the major cycles refit the components at all scales together. An extended source then takes a few broad components instead of many points. Each shape is cut to the support and renormalized there, so a component's flux is the flux it puts in the image. Cornwell multiplies the peak residual at each scale by a bias 1 - 0.6 s / s_max that favours small scales, because the peak of a smoothed residual grows with the scale's area. The score here is instead the χ² decrease of each component's Gauss–Newton step, which does not depend on its size, so no bias is needed; scale_bias applies one, 1 - scale_bias s / s_max, if the broad components grab flux that belongs to points.

Iteration stops when χ² per data point reaches target_chi2_red (the discrepancy principle; it relies on correct error bars), or after max_iterations. It also stops when χ² has stalled: when no pixel lowers it, or when it has fallen by less than a fraction stall_tolerance over the last stall_window iterations, and a major cycle does not help. That happens when noise puts the truth's own χ² per point above the target; chi2_red shows how close it came.

Parameters:

Name Type Description Default
data OIData or sequence of OIData

The data, fitted jointly.

required
npix int

Pixels on a side of the grid.

required
pixel_scale_mas float

Pixel size in milliarcseconds.

required
base SourceModel

A fixed scene the components are added to (default: none). A System base must not be offset (put any offset on its components).

None
gain float

Loop gain, the fraction of each step taken (default 0.1). Smaller is slower but less likely to put flux in the wrong place.

0.1
max_iterations int

Iteration limit (default 1000).

1000
target_chi2_red float

χ² per data point at which to stop (default 1).

1.0
refit_every int

Iterations between major cycles (default 50); 0 for none.

50
stall_window int and float

Stop when χ² has fallen by less than a fraction stall_tolerance (default 1e-3) over the last stall_window (default 50) iterations, even after a major cycle.

50
stall_tolerance int and float

Stop when χ² has fallen by less than a fraction stall_tolerance (default 1e-3) over the last stall_window (default 50) iterations, even after a major cycle.

50
base_priors dict

Priors (numpyro distributions) on parameters of the base to fit at every major cycle, keyed by their paths in the base (e.g. {"comp.dra": dist.Normal(30.0, 5.0)} for a System base, whose paths must start with a component's name, or {"diameter": ...} for a single component). Needs a base and major cycles (refit_every > 0). The fits run in dtype.

None
refresh_norms bool

Recompute the norms |J e_p| at every major cycle (default False). For non-linear data they change as the image does, but each refresh costs one Jacobian–vector product per pixel, computed a row of pixels at a time.

False
spectrum Spectrum

The spectrum all the components share, e.g. PowerLaw(1.0, index, wavel0); only its shape matters, and the component fluxes are at its reference wavelength. Default: grey. Needs a base.

None
support array-like of bool, shape (npix, npix)

Pixels allowed to receive components (default: all), e.g. a circular_support with a hole under the star.

None
init (array - like, shape(npix, npix) or (n_scales, npix, npix))

Starting component fluxes, non-negative and zero outside support (default: none with a base, and without one a single component of the smallest scale on the supported pixel nearest the centre). An (npix, npix) array is point components, and needs 0 among the scales_mas.

None
rotation_deg float

Position angle of the grid's "up" axis, as for Image; match the data's uv lattice (data.uv_grid.rotation_deg) for the fast exact transform.

0.0
dtype (float64, float32)

Precision of the iterations, as for fit.

"float64"
scales_mas sequence of float

FWHM in mas of the components' Gaussian shapes, distinct and non-negative (default (0.0,): points only, Högbom-like CLEAN). E.g. (0.0, 2 * beam_fwhm) for points and broad emission.

(0.0,)
scale_bias float

In [0, 1): scales the scores of scale s by 1 - scale_bias s / max(scales_mas) (default 0, no bias).

0.0

Returns:

Type Description
CleanResult

The model, components and χ² history. The model can be polished with fit on the components' support, used as a starting image for a regularized fit, or restored with the beam.

starting_image(data, star=True, oversample=4.0, largest_mas=None, start='moments', hole_mas=None)

A starting model for an image fit, sized from the data.

It fits a quick parametric model, an analytic star (if star) plus a circular Gaussian envelope with free flux and width, a low-order description of the visibilities, from a few starting widths. Then it chooses the image:

  • a field of about six FWHMs of the envelope, but at least 500 mas, and never larger than the interferometric field of view (λ / B_min) or largest_mas;
  • pixels oversample times finer than the Nyquist scale;
  • an Image with the fitted flux, whose pixels are the fitted Gaussian (start="moments") or the positive part of the :func:dirty_image (start="dirty", with the star removed). A dirty start is better when the Fourier coverage is dense (it already has the right shape) and worse when it is sparse (its sidelobes dominate).

Fit it with fit(start, image_priors(start), data, regularizers), adding a prior on the image's flux, "env.flux" (or "flux" with star=False), to fit it too: with a star, this stops the fit from parking excess flux next to it.

Parameters:

Name Type Description Default
data OIData or sequence of OIData

The data.

required
star bool

Whether the scene has an unresolved star at the origin (which also fixes the image's position). Without one, the result is the Image alone, and a Centroid prior should fix its position.

True
oversample float

Pixels per Nyquist pixel.

4.0
largest_mas float

A cap on the field of view.

None
start (moments, dirty)

The starting pixels, as above.

"moments"
hole_mas float

Radius of a hole in the image's support under the star. Extended flux within a fraction of a beam of the star is nearly indistinguishable from the star's own, so without a hole the fit can trade the two and bias the flux ratio; half the beam's minor axis (0.5 * beam(data).minor_mas) is a good choice.

None

Returns:

Type Description
SourceModel

System(star=PointSource(), env=Image(...)), or the Image alone.

dirty_image(data, npix, pixel_scale_mas, flux_ratio=None)

The dirty image: direct synthesis of the visibilities, unregularized.

I(x) = Σ_k w_k Re[V_k exp(+2πi u_k · x)] / Σ_k w_k, summing over the samples (each standing for itself and its conjugate) with uniform weights on the informed ones: the image convolved with the dirty beam, plus noise. It peaks at one for a lone point source. For AMIGO DISCO data the visibilities are the least-squares estimate from the modes; since the modes are blind to the total flux, the estimate is normalized to |V| = 1 on average on the shortest baselines.

Parameters:

Name Type Description Default
data OIData or sequence of OIData

The data; see the error raised for data without phases.

required
npix int

Number of pixels on a side.

required
pixel_scale_mas float

Pixel size in mas.

required
flux_ratio float

If the scene is a star at the origin plus extended emission with this flux relative to the star, the star is removed first, so the map shows the extended emission alone. The star is removed by subtracting the best-fitting point source at the origin (the weighted mean of the visibilities), then scaling by (1 + flux_ratio) / flux_ratio. That is robust to the normalization of the visibilities, which matters because subtracting a bright star amplifies any error in it by 1 / flux_ratio.

None

Returns:

Type Description
(Array, shape(npix, npix))

In the orientation of render (East left, North up). It has negative sidelobes.

image_priors(scene)

Priors on the log-brightness of every Image in a scene.

Returns a priors dict for fit. An Image with a plain log-brightness array gets a flat prior, {"env.log_brightness": ImproperUniform(...)}, so that its pixels are constrained only by the data and the regularizers. An Image with a GaussianField gets standard-normal priors on the field's latents, {"env.log_brightness.latent": Normal(0, 1)}: the Gaussian-process prior, which needs no regularizer. Add priors for any other free parameters (fluxes, offsets) to the dict.

nyquist_pixel_scale(data)

The largest pixel scale (mas) that samples the data's finest fringes.

That is λ / (2 B) for the longest baseline B in units of wavelength, over all samples of data (an OIData or a sequence of them). Reconstructions normally use pixels 2–4 times smaller.

field_of_view(data, largest_mas=500.0)

The default field of view for an image of data, in mas.

The smaller of largest_mas (500 by default) and the interferometric field of view, the largest λ / B over all samples with a non-zero baseline B (the shortest baseline at the longest wavelength, where they are observed together): structure larger than that is not measured, and on a uv lattice a larger field would alias. Use it with nyquist_pixel_scale to choose the image's size and pixels.

beam(data)

The resolution of a dataset: the FWHM ellipse of its beam's core.

The dirty beam is the image of a point source made by the data's uv sampling. Near its peak it falls off as 1 - 2π² xᵀ M x, where M is the weighted mean of u uᵀ over the samples, which matches a Gaussian of covariance M⁻¹ / 4π². Its FWHM ellipse is the standard "beam" drawn on interferometric images: roughly λ/B, and elongated where the coverage is. For complicated coverage the real beam can look quite different, but the ellipse still shows the size and shape of a resolution element.

Every sample that carries information is weighted equally ("uniform weighting"); in AMIGO DISCO data those are the uv points on which the modes have weight (at least 1e-3 of the largest), i.e. the splodges. Weighting by information instead would let the low-frequency central splodge dominate, and give a broader beam.

Parameters:

Name Type Description Default
data OIData or sequence of OIData

The data.

required

Returns:

Type Description
Beam

convolve_beam(image, pixel_scale_mas, beam)

An image convolved with a Gaussian beam: the image at the data's resolution.

Reconstructed images are super-resolved. A regularized image can put structure on scales finer than the beam, where the data constrain it only weakly, so it often looks clumpy or streaky. Convolved with the beam (the "restored" image of radio astronomy), it shows only what the data resolve. That is the fair way to compare two reconstructions, or a reconstruction with a model: convolve both.

The kernel is an elliptical Gaussian with the beam's FWHMs and position angle (North to East), normalized to unit sum. It is sampled on an odd grid centred on a pixel, so the convolution does not shift the image. Flux beyond the edge of the image is taken to be zero, and the result is cropped to the image, so the total flux is kept only for structure more than about a beam from the edge; structure nearer the edge is dimmed.

Parameters:

Name Type Description Default
image (array - like, shape(ny, nx))

The image, in the orientation of render (East left, North up).

required
pixel_scale_mas float

Pixel size in milliarcseconds.

required
beam Beam

The beam, usually beam(data).

required

Returns:

Type Description
(Array, shape(ny, nx))

The convolved image.

l_curve(model, priors, data, regularizer, weights, others=(), *, warm_start=True, **fit_options)

Fit a model over a range of weights for one regularizer.

The weights are fitted from largest to smallest, each starting from the previous solution, which is faster and more stable than starting every fit afresh. Plot penalty against chi2 (both on log axes) to see the trade-off between fitting the data and regularizing the image, and compare LCurve.corner and LCurve.discrepancy with the images either side: there is usually a wide range of good weights.

Parameters:

Name Type Description Default
model

As for fit.

required
priors

As for fit.

required
data

As for fit.

required
regularizer (TSV, TV, MaxEntropy, Laplacian, StarletL1 or LogSum)

The regularizer whose weight is swept (its own weight is ignored).

required
weights sequence of float

The weights to try.

required
others sequence

Further regularizers kept fixed, e.g. a Centroid prior.

()
warm_start bool

Start each fit from the previous, stronger one (default), or every fit from model (and init). Use False for penalties that switch pixels off, such as LogSum: pixels a strong weight has driven dark cannot recover in the weaker fits that follow.

True
**fit_options

Passed to fit.

{}

Returns:

Type Description
LCurve

log_evidence(model, data, path='env')

Laplace-approximated log evidence of a Gaussian-field image fit.

For an Image whose log-brightness is a GaussianField with standard-normal latents z, at the MAP model from fit,

log Z ≈ -½ χ² - L - ½ |z|² - ½ log det(I + JᵀJ),

up to a constant that depends only on the data (see below). J is the Jacobian of the whitened residuals with respect to z, so JᵀJ is the Gauss–Newton curvature of the likelihood. Other fitted parameters (fluxes, spectra) are held at their MAP values. Compare it across fits with different hyperparameters and choose the largest, as MacKay's evidence framework does; it is exact for a linear model, and assumes correct error bars. It uses the data's quoted errors, so it does not support fits with noise= terms, nor fits with one model per dataset.

Normalization. -½ χ² - L is model_loglike at the MAP less its data-only normalization, -Σ log σ - ½ n log 2π (in its von Mises form for unprojected phases, and with the correlated closure phases' determinant), which is the same for every model and hyperparameter on a given dataset and so cancels in differences of log Z, the only meaningful quantity. For plain data L = 0. L is the rest of the likelihood's normalizer, which can depend on the model and on nuisance widths: for data with calibration gains (OIData.with_gains) or closure offsets (OIData.with_closure_offsets), which the likelihood marginalizes, it is ½ log det of their covariance factor, and for extra observables (OIData.extras) the log ratio of their effective to quoted errors. It is evaluated at the MAP exactly as the likelihood evaluates it, so that evidences for data with different gain or offset widths can be compared; its curvature with respect to z is left out of J, as Gauss–Newton leaves out the residuals' own second derivatives.

Parameters:

Name Type Description Default
model SourceModel or FitResult

The MAP model, or better the FitResult itself, so that fitted noise= terms are caught (a ValueError). A list of models raises a TypeError.

required
data OIData or sequence of OIData

The data it was fitted to.

required
path str

Path of the Image in the model (default "env").

'env'

Returns:

Type Description
float

laplace_samples(model, data, n, key, path='env', npix=None, fov_mas=None)

Draws from the Laplace posterior of a Gaussian-field image.

For an Image whose log-brightness is a GaussianField, the prior on its whitened latents z is N(0, I), so about the MAP z₀ the Gauss–Newton (Laplace) posterior is

z ~ N(z₀, (I + JᵀJ)⁻¹),

with J the Jacobian of the whitened residuals with respect to z: the same approximation as log_evidence. With the thin SVD J = U S Vᵀ, a draw is z₀ + V diag((1 + s²)^-½) ε₁ + (I - V Vᵀ) ε₂ with standard-normal ε₁ and ε₂, so directions the data do not constrain keep the prior's unit width. The draws show which features of the image the data support. Only the latents vary: every other fitted parameter (fluxes, the star, spectra) is held at its MAP value, so the spread is narrower than the full posterior's where they are correlated with the image. Like log_evidence, it uses the data's quoted errors.

Parameters:

Name Type Description Default
model SourceModel or FitResult

The MAP model, or the FitResult itself (fits with fitted noise= terms raise a ValueError, lists of models a TypeError).

required
data OIData or sequence of OIData

The data it was fitted to.

required
n int

Number of draws.

required
key Array

A JAX random key.

required
path str

Path of the Image in the model (default "env").

'env'
npix int

With fov_mas, also render the whole model for each draw (see SourceModel.render).

None
fov_mas float

The rendered field of view, in milliarcseconds. Give both or neither of npix and fov_mas (one alone raises a ValueError).

None

Returns:

Type Description
dict

"latents", a NumPy array (n, *latent.shape), and, given npix and fov_mas, "images", (n, npix, npix): unit-sum images of the whole model.

error_scale(model, data, path='env', *, by_observable=False)

Re-estimate the scale of the error bars from a Gaussian-field fit.

What it does. It estimates the factor s by which every error bar should be multiplied for the data to be consistent with the fit. s < 1 means the error bars are too large; s > 1 that they are too small, or that the model is missing something.

The idea. In MacKay's evidence framework, the level of the noise is a hyperparameter, like the prior's sigma and length_mas. Write the noise precision as β = 1/s², so that the likelihood is exp(-β χ²/2), with χ² computed with the quoted errors. The Laplace-approximated evidence, as a function of β, is maximized when

\[\frac{1}{\beta} = s^2 = \frac{\chi^2}{N - \gamma}, \qquad \gamma = \sum_i \frac{\beta\lambda_i}{1 + \beta\lambda_i}.\]

N is the number of data. γ is the effective number of parameters the data measure: the λ_i are the eigenvalues of the Gauss–Newton curvature JᵀJ of the likelihood (with the quoted errors) in the field's whitened latents, in which the prior's curvature is the identity, so that βλ_i are those of the rescaled likelihood. A direction with βλ ≫ 1 is fixed by the data and counts as one parameter; one with βλ ≪ 1 is fixed by the prior and counts as none. Since γ depends on β, the equation is solved for β at the fitted model (by Newton's method, which converges monotonically from β = 0 because γ is concave in β).

Each measured parameter uses up one datum's worth of scatter. So of the N residuals, only N − γ are free to scatter, and an honest error bar gives χ² ≈ N − γ, not N. The ordinary "χ² per point" estimate, s² = χ²/N, is biased low for the same reason as the 1/N estimate of a sample variance; this is its Bayesian, nonlinear generalization. It is MacKay's re-estimation formula for β (MacKay 1992, eq. 4.10, with γ from eq. 4.9; Bishop 2006, eqs. 3.91–3.95).

How to use it. Fit at your chosen hyperparameters, call this, rescale the data with OIData.with_error_scale, and refit. The fit itself depends on the errors, so in principle fitting and rescaling is a fixed-point iteration; in practice one rescaling usually suffices. Error bars that are too large make the discrepancy principle, classic MaxEnt and the evidence all over-regularize, so rescale before choosing hyperparameters with any of them. The estimate assumes the model is adequate: if the data contain structure the model cannot fit, s absorbs it.

Only the field's latents are counted in γ. Each other fitted parameter (the image's flux, a star's position, a spectral index) that the data measure lowers N − γ by about one more, and so raises s by a fraction of about 1/(2N). That is negligible while such parameters are few compared with the data, as in every SPARCO fit.

One scale per observable. A single s assumes every error bar is off by the same factor. Often it is not: the V² and closure phases of one instrument are calibrated differently, and in a MATISSE N-band contest file the V² gave χ² per point 0.005 while the closure phases gave 0.49. One scale between the two leaves the V² overweighted and the phases underweighted. With by_observable=True each kind of observable b ("vis", "phi" and each kind in extras, pooled over the datasets) gets its own precision β_b on its own rows of the residual vector. The evidence is then maximized where, for every block,

\[\frac{1}{\beta_b} = s_b^2 = \frac{\chi^2_b}{N_b - \gamma_b}, \qquad \gamma_b = \beta_b \operatorname{tr}(A^{-1} G_b), \qquad A = I + \sum_b \beta_b G_b,\]

with G_b = J_bᵀJ_b the curvature from block b's rows of the Jacobian: MacKay's re-estimation of β applied to each block (MacKay 1992, eqs. 4.9–4.10; Bishop 2006, §§3.5.2–3.5.3). Equivalently, γ_b is the sum over the block's rows of the diagonal of the hat matrix B½ J A⁻¹ Jᵀ B½ (B the diagonal matrix of each row's β_b): the share of the measured parameters that block b pays for. The γ_b add up to the total γ = tr(A⁻¹(A − I)). They are coupled, so the equations are solved together by iterating β_b ← N_b / (χ²_b + γ_b/β_b) (the same fixed point as MacKay's β_b ← (N_b − γ_b)/χ²_b, but monotone, so it cannot oscillate) from β_b = N_b/χ²_b to a relative change of 1e-10. With one block it reproduces the single scale. The blocks' covariances must be s_b² D: data with calibration gains, closure-phase offsets, marginalized flux scales or differential phases with a finite prior_width add nuisance covariance that does not scale with the quoted errors, and raise a ValueError (the single scale makes the same assumption, so treat it with care for such data). N_b counts independent data: closure phases from four or more telescopes count their independent combinations, while their periodic penalty residuals (see whitened_residuals) belong to the block's χ² and γ_b but are not counted as data.

Use it when the blocks' χ² per point differ markedly, e.g. V² and closure phases from different calibrations, or OI_FLUX spectra beside interferometry. Rescale with the dictionary it returns, data.with_error_scale(scales), and refit. Prefer the single scale when a block has few data: its s_b scatters by about 1/√(2(N_b − γ_b)). Like the single scale, it absorbs into s_b any structure in block b the model cannot fit.

It uses the data's quoted errors, so it does not support fits with noise= terms (which estimate the errors another way), nor fits with one model per dataset.

Parameters:

Name Type Description Default
model SourceModel or FitResult

The MAP model, whose Image at path has a GaussianField log-brightness, or better the FitResult itself, so that fitted noise= terms are caught (a ValueError). A list of models raises a TypeError.

required
data OIData or sequence of OIData

The data it was fitted to.

required
path str

Path of the Image in the model (default "env").

'env'
by_observable bool

Estimate one scale per kind of observable (default False: one scale for all the data).

False

Returns:

Type Description
float or dict

The scale s, or with by_observable=True a dictionary from each kind of observable present ("vis", "phi", and e.g. "flux" or "visphi") to its scale s_b, ready for OIData.with_error_scale. A warning is issued if the block scales do not converge.

References
  • D. J. C. MacKay (1992), "Bayesian interpolation", Neural Computation 4, 415–447, doi:10.1162/neco.1992.4.3.415. It introduces the evidence framework, γ, and the re-estimation of α and β.
  • C. M. Bishop (2006), Pattern Recognition and Machine Learning, §3.5, "The evidence approximation" (free PDF): the same results for linear models, eqs. 3.91–3.95.
  • S. F. Gull (1989), "Developments in maximum entropy data analysis", in Maximum Entropy and Bayesian Methods, Kluwer, 53–71: the same N − γ argument for maximum entropy, the basis of LCurve.classic_maxent.

diagnose(model, data, regularizers=())

Check a model and its data for common imaging pitfalls.

Nothing is printed or warned: print the returned Diagnosis to read it. Run it on the fitted model, with the regularizers used in the fit.

Parameters:

Name Type Description Default
model SourceModel

The model, normally containing at least one Image (the Image checks are skipped otherwise).

required
data OIData or sequence of OIData

The data.

required
regularizers sequence

The regularizers of the fit; a Centroid fixes the position.

()

Returns:

Type Description
Diagnosis

checks holds, per dataset, chi2_red (χ² per data point) and phase_regime (fraction of model visibilities with |arg V| > 0.8π or |V| < 0.05, for projected phases only); per Image, pixel_scale_mas, edge_flux (fraction of the flux in the outer 2 pixels) and centroid_mas (East, North offset in mas); and anchored (whether something fixes the position), flip_dchi2 (Δχ² when every Image is rotated by 180° about the origin).

Sky-plane and uv-plane geometry shared by the source models.

Position angles follow the on-sky convention used throughout virgil: North is +y (up), East is +x (left, i.e. x decreases left-to-right across an image array's columns), and position angle is measured North to East, i.e. counter-clockwise from the top in a plot with East to the left. This matches the coordinate grid produced by :func:image_coordinates.

pixel_offsets(npix, pixel_scale_mas)

Sky offsets (mas) of the pixel centres along one image axis.

Along columns this is dra, decreasing to the right (East left); along rows it is ddec, decreasing downwards (North up). Both are (centre - index) * pixel_scale_mas with the centre at index (npix - 1) / 2, as in :func:image_coordinates.