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. |
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 |
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 |
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, |
components |
(array, shape(npix, npix))
|
The components' total flux in each pixel of the grid,
|
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: |
components_by_scale |
(array, shape(n_scales, npix, npix))
|
The components' fluxes |
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, |
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'
|
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. |
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
|
50
|
stall_tolerance
|
int and float
|
Stop when χ² has fallen by less than a fraction
|
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.
|
None
|
refresh_norms
|
bool
|
Recompute the norms |
False
|
spectrum
|
Spectrum
|
The spectrum all the components share, e.g.
|
None
|
support
|
array-like of bool, shape (npix, npix)
|
Pixels allowed to receive components (default: all), e.g. a
|
None
|
init
|
(array - like, shape(npix, npix) or (n_scales, npix, npix))
|
Starting component fluxes, non-negative and zero outside
|
None
|
rotation_deg
|
float
|
Position angle of the grid's "up" axis, as for
|
0.0
|
dtype
|
(float64, float32)
|
Precision of the iterations, as for |
"float64"
|
scales_mas
|
sequence of float
|
FWHM in mas of the components' Gaussian shapes, distinct and
non-negative (default |
(0.0,)
|
scale_bias
|
float
|
In [0, 1): scales the scores of scale |
0.0
|
Returns:
| Type | Description |
|---|---|
CleanResult
|
The model, components and χ² history. The model can be polished
with |
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
oversampletimes finer than the Nyquist scale; - an
Imagewith 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 |
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
( |
None
|
Returns:
| Type | Description |
|---|---|
SourceModel
|
|
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
|
None
|
Returns:
| Type | Description |
|---|---|
(Array, shape(npix, npix))
|
In the orientation of |
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
|
required |
pixel_scale_mas
|
float
|
Pixel size in milliarcseconds. |
required |
beam
|
Beam
|
The beam, usually |
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 |
required | |
priors
|
As for |
required | |
data
|
As for |
required | |
regularizer
|
(TSV, TV, MaxEntropy, Laplacian, StarletL1 or LogSum)
|
The regularizer whose |
required |
weights
|
sequence of float
|
The weights to try. |
required |
others
|
sequence
|
Further regularizers kept fixed, e.g. a
|
()
|
warm_start
|
bool
|
Start each fit from the previous, stronger one (default), or every
fit from |
True
|
**fit_options
|
Passed to |
{}
|
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 |
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'
|
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 |
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'
|
npix
|
int
|
With |
None
|
fov_mas
|
float
|
The rendered field of view, in milliarcseconds. Give both or
neither of |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
|
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
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,
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 |
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'
|
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 |
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 ofLCurve.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
|
required |
data
|
OIData or sequence of OIData
|
The data. |
required |
regularizers
|
sequence
|
The regularizers of the fit; a
|
()
|
Returns:
| Type | Description |
|---|---|
Diagnosis
|
|
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.