virgil.oidata
Data containers and observable helpers for interferometric observations.
OIData maps each supported data product into a likelihood comparison vector.
For ordinary OIFITS-style data, this standardized vector concatenates the
visibility and phase observables. For AMIGO mixed-DISCO products, it is the
single mixed log-complex vector defined by the stored log-amplitude and phase
projection operators. In this context, "standardize" means "put data and model
predictions into the same comparison basis", not z-score normalization.
Every (baseline, wavelength) sample is one entry of u, v and wavel, so
data with several wavelength channels need no special handling in models.
Flagged samples are left out of the observables (see vis_index and
phi_index). residuals wraps phase residuals into [-π, π) for display.
Likelihoods and fits use
whitened_residuals instead. For
ordinary unprojected phases, chord residuals 2 sin(Δ/2) give a squared
contribution that is smooth across phase wraps. Correlated closure phases are
the exception: their residuals are wrapped into [-π, π), combined, and
whitened together as sines sin Δ, with a periodic penalty
2 sin²(Δ/2)/σ per closure phase, so the likelihood is continuous
everywhere (the correlated Gaussian for small residuals, with no false
minimum at Δ = π).
Closure phases from four or more telescopes are correlated: the triangles
of one frame and channel share baselines, and only some of them are
independent (three of four, for four telescopes). The likelihood keeps only
the independent combinations and whitens them with their covariance, built
from independent noise on the baseline phases. n_independent counts the
observables that remain (the degrees of freedom); n_residuals is the
length of whitened_residuals, which adds the penalty residuals.
Calibration nuisances per dataset. with_wavelength_scale and
with_north_angle are what the noise= terms wavel_scale and
north_angle of fit and
numpyro_model apply. A North angle δ
means the data see every position angle as the true one plus δ, which is
a rotation of (u, v) by −δ. A plate scale needs no term of its own: a sky
magnified by m is wavel_scale = 1/m. Neither has a default prior. Their
invariant priors (uniform on the circle for δ) leave them degenerate with
the scene's orientation and size for a single dataset, so a Gaussian prior
is strong information and its width should come from the instrument's
astrometric calibration. The same terms for published positions are in
PositionData.term; they follow
Octofitter (Thompson et al. 2023, AJ 166, 164).
The bundled data/calibrated_visibility.npy fixture is synthetic; see the
AMIGO DISCO tutorial and virgil.amigo for loading it.
Classes
Bases: Base
Store and transform optical-interferometry observables.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
dict, str, os.PathLike, astropy.io.fits.HDUList, or a list of files
|
An OIFITS file (a path, or a file opened with |
required |
target
|
str or int
|
For OIFITS input, the target to keep (by name or |
None
|
Notes
Every (baseline, wavelength) sample is one element of the flat u,
v (metres) and wavel (metres) arrays; wavel has a single
element when all samples share one wavelength. Models are evaluated on
these samples. The observables are:
vis/d_vis: squared visibilities (v2_flag=True) or amplitudes, or their projection throughvis_mat.phi/d_phi: closure phases (cp_flag=True) built from the samplesi_cps1 + i_cps2 - i_cps3, or absolute phases; always in radians, optionally projected throughphi_mat.
Visibility-only data (no OI_T3 or VISPHI, no phi in a
dictionary, or every closure phase flagged) have an empty phase block:
has_phases is False and fits use the visibilities alone.
Flagged samples are left out of the observables. vis_index (and
phi_index for absolute phases) then lists the samples that are
observed; they are None when every sample is used.
uv_grid is a UVGrid for AMIGO DISCO
products, whose samples lie on a regular (possibly rotated) lattice,
and None for all other data, even if their samples happen to lie
on a lattice: virgil does not search ordinary data for one. Models
that can use the lattice, such as an Image
with matching rotation_deg, then evaluate faster; the results are
the same. To use it for other lattice-sampled data, set it with
find_uv_grid, e.g.
eqx.tree_at(lambda d: d.uv_grid, data, find_uv_grid(data.u, data.v),
is_leaf=lambda x: x is None).
Data read from OIFITS also keep their time and exposure per sample:
frame numbers the exposures (frames), :attr:mjd gives each
sample's time, and :meth:epochs and :meth:split_by_epoch group the
frames into nights. The time is stored as dt, days since the static
float64 t_ref, so that models of time see small numbers that keep
their precision in float32 (about 5 s over 1000 days). stations
holds each sample's station pair (STA_INDEX).
gains holds calibration gains correlated across channels
(GainModes, set with
with_gains), which the likelihood
marginalizes; None by default. phase_offsets likewise holds
closure-phase offsets per frame
(ClosureOffsets, set with
with_closure_offsets).
extras holds further observables, read with extras= (OI_FLUX,
T3AMP, VISAMP beside V², differential VISPHI beside closure phases):
a tuple of blocks from virgil.observables,
which follow the phases in the data vector, in the order of
observables.KINDS. It is empty by default.
has_phases
property
Whether the data hold any phase observables. Visibility-only data (e.g. V² without closure phases) have an empty phase block.
n_independent
property
Number of independent observables, for degrees of freedom.
Equal to the size of :meth:flatten_data, except for closure phases
from four or more telescopes, where only the independent
combinations count (three of the four triangles of a frame and
channel, for four telescopes). The residual vector of
whitened_residuals is
longer for such data; see :attr:n_residuals.
n_residuals
property
Length of whitened_residuals.
Equal to n_independent, except for correlated closure phases
(four or more telescopes), which add one periodic penalty residual
per closure phase that keeps the likelihood continuous where a
residual crosses ±π. Use n_independent for degrees of freedom.
has_model_covariance
property
Whether the likelihood's covariance depends on the model: with
gains, or with marginalized flux scales (extras). Least squares
then does not give the likelihood, so fit uses L-BFGS.
mjd
property
Time of each sample (days, float64), or None if unknown.
__init__(data, target=None, extras=())
Initialize from an OIFITS file or explicit arrays.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
dict, str, os.PathLike, astropy.io.fits.HDUList, or a list of files
|
An OIFITS file or a list of them, read with
A record with |
required |
target
|
str or int
|
For OIFITS input, the target to keep. |
None
|
extras
|
sequence of str
|
For OIFITS input, the extra observables to read (see
|
()
|
flatten_data()
Return the data vector and its uncertainties.
Returns:
| Type | Description |
|---|---|
tuple[array - like, array - like]
|
The visibility observables followed by the phases (radians),
in the order of
|
standardize_model(cvis)
Map model complex visibilities (one per sample) to the data vector.
The result lines up with the first vector of :meth:flatten_data.
to_vis(cvis)
Convert model complex visibilities to the visibility observables.
The channel follows vis_mode (V², amplitude or log-amplitude);
flagged samples are dropped, and vis_mat is applied if set.
to_phases(cvis)
Convert complex visibilities to closure or absolute phases in radians.
model(model)
Compute the model visibilities and phases for the given model object.
A model that changes with time (see
SourceModel.at) is evaluated at
each sample's own time, which the data must have (mjd), with the
direct Fourier transform: a uv_grid (AMIGO DISCO data, which
carry no times) is not used for it.
residuals(prediction, reference=None)
Return prediction - reference with phase residuals wrapped.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prediction
|
array - like
|
Model vector, e.g. from |
required |
reference
|
array - like
|
Vector to compare against; by default the data
(the first vector of :meth: |
None
|
Returns:
| Type | Description |
|---|---|
array - like
|
Residual vector. Unprojected phase residuals are wrapped into
|
Notes
This is for display. Likelihoods and fits use
whitened_residuals,
which is smooth where phases wrap.
with_model(model, key=None, noise_scale=1.0)
Return a copy populated from a model with optional Gaussian noise.
Sampling, uncertainties, conventions, closure indices, and linear
observable operators are preserved from this object. With key,
marginalized gains and extra-observable nuisance modes are drawn
along with diagonal noise.
with_error_scale(factor)
A copy of the data with its uncertainties multiplied by factor.
Use it when the error bars are known to be too large or too small,
for example with a factor from
error_scale. The uncertainties
are those of the observables as fitted (after any projection), so
the whitened residuals simply scale by 1 / factor.
A dictionary scales each kind of observable by its own factor, as
returned by error_scale(..., by_observable=True): "vis"
(d_vis), "phi" (d_phi) and the kinds of extras
("flux", "nflux", "corrflux", "visamp",
"t3amp", "visphi"). Kinds missing from the dictionary are
left unchanged, and so are kinds these data lack, so one
dictionary can rescale several datasets.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
factor
|
float or dict
|
Positive scale for every uncertainty, or a dictionary of positive scales by kind of observable. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
For a factor that is not finite and positive, or a dictionary key that is not a kind of observable. |
Examples:
>>> data = OIData({"u": [1.0, 2.0], "v": [0.0, 1.0], "wavel": 1e-6,
... "vis": [0.9, 0.5], "d_vis": [0.01, 0.02]})
>>> scaled = data.with_error_scale({"vis": 3.0})
>>> [round(float(e), 3) for e in scaled.d_vis]
[0.03, 0.06]
with_error_floor(absolute=None, relative=None)
A copy of the data with a minimum uncertainty per observable.
Each uncertainty becomes max(σ, absolute, relative × |data|),
as PMOIRED's min error and min relative error do. This is a
fixed change to the data; the fitted counterpart, added in
quadrature and relative to the model, is the noise= terms
(see inflated_errors).
Both use one rule, _utils.inflate_errors.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
absolute
|
dict
|
Floors per observable: |
None
|
relative
|
dict
|
Floors per observable: |
None
|
Returns:
| Type | Description |
|---|---|
OIData
|
|
Notes
Closure phases from four or more telescopes are floored before they are whitened: the whitened rows use the floored errors, while their periodic penalty rows keep their effective error 1/√(2π) (the penalty itself is 2 sin²(Δ/2)/σ with the floored σ). The differential phases are floored per channel, before their covariance N D Nᵀ is formed.
Examples:
>>> import numpy as onp
>>> data = OIData({"u": [1.0, 2.0], "v": [0.0, 1.0], "wavel": 1e-6,
... "vis": [0.9, 0.5], "d_vis": [0.001, 0.02]})
>>> floored = data.with_error_floor(relative={"vis": 0.01})
>>> [round(float(e), 4) for e in floored.d_vis]
[0.009, 0.02]
with_continuum(continuum=None, lines=None, order=None, prior_width=None)
A copy with the continuum and line windows of the extra spectra.
They set how differential phases ("visphi") and normalized
spectra ("nflux") are normalized: see
DifferentialPhase.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
continuum
|
sequence of (lo, hi)
|
Wavelength ranges (metres). Differential phases are fitted over the continuum and kept in the lines (each defaults to the complement of the other); normalized spectra use the continuum. |
None
|
lines
|
sequence of (lo, hi)
|
Wavelength ranges (metres). Differential phases are fitted over the continuum and kept in the lines (each defaults to the complement of the other); normalized spectra use the continuum. |
None
|
order
|
int
|
The continuum polynomial in wavenumber: 1 (a mean and a slope) by default for differential phases, 0 (a mean) for spectra. |
None
|
prior_width
|
float or (float, float)
|
For differential phases: marginalize each baseline and frame's
offset (and slope) under Gaussian priors of these widths
(radians), using the channels of both windows, instead of
projecting them out as the pipeline does (the default). See
|
None
|
Examples:
Keep the differential phase across Brγ, normalized on either side:
data.with_continuum([(2.150e-6, 2.162e-6), (2.170e-6, 2.180e-6)],
lines=[(2.163e-6, 2.169e-6)]).
with_flux_scale(scale=None, per='dataset', poly_order=0, poly_width=0.1, kinds=None)
A copy with new grey-scale nuisances for the extra spectra.
Each spectrum (OI_FLUX, or correlated fluxes) is known up to a
scale, marginalized analytically under a Gaussian prior that you
state (see FluxSpectrum). It
is never taken from the data, and it is a proposal for the scale's
Jeffreys prior 1/k (see virgil.observables).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scale
|
(float, float)
|
The prior |
None
|
per
|
(dataset, row, frame, station)
|
One scale for the whole dataset (default), or one per spectrum (row), per exposure, or per telescope (baseline, for correlated fluxes). Fibre injection varies per telescope and exposure, so per row is the safest for uncalibrated spectra. |
"dataset"
|
poly_order
|
int
|
Also marginalize a polynomial in λ of this order times the model spectrum (a chromatic calibration). |
0
|
poly_width
|
float
|
Prior width of each polynomial coefficient, relative to the scale's prior mean. |
0.1
|
kinds
|
sequence of str
|
Which blocks to change: by default |
None
|
with_gains(telescope=None, baseline=None, chromatic=None, modes=None)
A copy of the data with calibration gains correlated across channels.
The likelihood then marginalizes gains on log |V| per frame
analytically (see virgil.gains), with these widths
unless they are fitted as the noise terms vis_gain_telescope,
vis_gain_baseline, vis_gain_chromatic or vis_gain_modes
(scale parameters: give them log-uniform priors on stated bounds,
e.g. dist.LogUniform(1e-4, 0.3)).
The covariance then depends on the model, so fits use L-BFGS.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
telescope
|
float
|
Widths (1σ on log |V|; 0.01 is a 1% amplitude or 2% V² gain)
of gains per (frame, telescope), per (frame, baseline), and per
(frame, baseline) shaped (λ_ref/λ)², a coherence loss. |
None
|
baseline
|
float
|
Widths (1σ on log |V|; 0.01 is a 1% amplitude or 2% V² gain)
of gains per (frame, telescope), per (frame, baseline), and per
(frame, baseline) shaped (λ_ref/λ)², a coherence loss. |
None
|
chromatic
|
float
|
Widths (1σ on log |V|; 0.01 is a 1% amplitude or 2% V² gain)
of gains per (frame, telescope), per (frame, baseline), and per
(frame, baseline) shaped (λ_ref/λ)², a coherence loss. |
None
|
modes
|
array - like
|
Further modes, |
None
|
Returns:
| Type | Description |
|---|---|
OIData
|
The data with |
with_wavelength_scale(scale=1.0, offset=0.0)
A copy of the data whose wavelengths are scale · λ + offset.
A wavelength calibration error: models are evaluated at the
corrected wavelengths, which rescales the spatial frequencies
(u and v are in metres) and moves the spectra together, as
a wrong wavelength scale does. Angular sizes scale with it, so with
a single dataset the scale is degenerate with every size, and its
prior is the systematic error. Its effect on the spatial
frequencies is that of magnifying the sky by 1 / scale, so it
is also the plate-scale term for interferometric data (see
with_north_angle).
Fit it with the noise terms
wavel_scale and wavel_offset (e.g.
noise={"wavel_scale": dist.Normal(1.0, 2e-4)}, about right for
GRAVITY), which call this.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scale
|
float
|
Factor on the wavelengths (default 1). |
1.0
|
offset
|
float
|
Shift added after scaling, in metres (default 0). |
0.0
|
Returns:
| Type | Description |
|---|---|
OIData
|
The data with |
with_north_angle(angle)
A copy of the data that see the sky rotated by angle degrees.
An error in the instrument's North: every position angle these
data measure is the true one plus angle (North through East).
A source at (dra, ddec) appears at R(angle) (dra, ddec),
with R(δ) = [[cos δ, sin δ], [-sin δ, cos δ]], so a companion
at PA θ appears at PA θ + angle. Because virgil's Fourier
kernel is u dra + v ddec, rotating the sky rotates (u, v)
the same way, and models are evaluated at R(-angle) (u, v), as
for Rotated(model, angle). Fit it
with the noise term north_angle, which calls this.
Its prior must be stated, as for every noise term: there is no
default width. The invariant prior of a rotation is uniform on the
circle, under which, with one dataset, the angle is degenerate with
every position angle in the scene. A Gaussian prior (e.g.
noise=[{}, {"north_angle": dist.Normal(0.0, 0.5)}]) is strong
information, and its width should come from the instrument's
astrometric calibration. Long-baseline interferometers know their
baselines' orientation geometrically, so leave the term out there;
it is for imaging, masking and kernel-phase data.
A plate scale needs no term of its own: a sky magnified by a factor
m is with_wavelength_scale(1 / m) (the noise term
wavel_scale). Per-dataset North-angle and plate-scale
calibration terms follow Octofitter (Thompson et al. 2023, AJ 166,
164).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
angle
|
float
|
Rotation of the sky as these data see it, in degrees, North through East. |
required |
Returns:
| Type | Description |
|---|---|
OIData
|
The data with |
with_closure_offsets(baseline=None, triangle=None, modes=None)
A copy of the data with closure-phase offsets per frame.
Calibration can leave closure phases that do not close. The
likelihood then marginalizes offsets common to the channels of a
frame analytically (see
ClosureOffsets), with these widths
unless they are fitted as the noise terms phi_offset_baseline,
phi_offset_triangle or phi_offset_modes (scale parameters:
log-uniform priors on stated bounds). Use them only if
calibrators show such offsets: they are off by default. Needs
closure phases from four or more telescopes.
This is a small-offset approximation: an offset δ changes the whitened sine sin Δ by about δ cos Δ, which is treated as linear in δ. It holds while the offsets (and the residuals) are small, about δ ≲ 0.3 rad (17°); larger widths are not marginalized exactly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
baseline
|
float
|
Width (radians) of a phase offset per (frame, baseline), which reaches the closure phases through the triangles' signs. |
None
|
triangle
|
float
|
Width (radians) of an offset per (frame, triangle). |
None
|
modes
|
array - like
|
Further modes, |
None
|
Returns:
| Type | Description |
|---|---|
OIData
|
The data with |
epochs(gap_days=0.5)
Label each sample with its epoch: a run of frames with no gap.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gap_days
|
float
|
Frames whose mean times are more than this far apart (days) are in different epochs; the default separates nights. A frame is never split between epochs. |
0.5
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Integer epoch of each sample, numbered in time order from 0. |
split_by_epoch(gap_days=0.5)
One OIData per epoch, in time order.
See :meth:epochs. Each part keeps its own samples, observables and
closure phases, so it can be fitted on its own or with a model per
epoch. Not available for projected (kernel, DISCO) observables, nor
for data with a gain mode shared between epochs (a supplied mode
spanning frames): the parts' likelihoods would then not add up to
the whole.
select(wavel_min=None, wavel_max=None, *, ranges=None, exclude=None, observables=None)
These data restricted to wavelength windows and observables.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
wavel_min
|
float
|
Keep the samples with |
None
|
wavel_max
|
float
|
Keep the samples with |
None
|
ranges
|
sequence of (lo, hi)
|
Keep the samples in any of these wavelength ranges (metres,
inclusive), instead of |
None
|
exclude
|
sequence of (lo, hi)
|
Then drop the samples in any of these ranges (metres, inclusive), e.g. a line or a telluric band. |
None
|
observables
|
str or sequence of str
|
The observables to keep: |
None
|
Returns:
| Type | Description |
|---|---|
OIData
|
The samples in range, with their observables and closure phases (a closure triangle's legs share a wavelength, so triangles are kept whole). Not available for projected (kernel, DISCO) observables, or after closure-phase offsets are added. |
Examples:
Keep the K-band continuum of a GRAVITY file but not the Brγ window:
data.select(2.05e-6, 2.16e-6), or the continuum on both sides
of the line: data.select(exclude=[(2.162e-6, 2.170e-6)]).
Closure phases alone, between 2.05 and 2.18 µm:
data.select(2.05e-6, 2.18e-6, observables="phi").
uv grids
AMIGO DISCO data are sampled on a regular uv lattice; OIData.uv_grid
records it, so that a matching Image can use an exact matrix Fourier
transform. Other data get uv_grid=None, even on a lattice; set it with
find_uv_grid (see OIData) to use the fast transform for them.
virgil.oidata.UVGrid
Bases: Module
uv samples that lie on a regular lattice, possibly rotated on the sky.
AMI data, for example, are sampled on the detector's Fourier grid,
which is rotated on the sky by the parallactic angle. The sample k
is at grid-frame coordinates
(u_axis[index[k] % nu], v_axis[index[k] // nu]) (metres), and on
the sky at rotate(u_g, v_g, rotation_deg). Found from the samples by
:func:find_uv_grid.
u_axis
instance-attribute
v_axis
instance-attribute
index
instance-attribute
rotation_deg = eqx.field(static=True)
class-attribute
instance-attribute
virgil.oidata.find_uv_grid(u, v, tolerance=1e-06)
Return the lattice that (u, v) samples lie on, or None.
The lattice axes are found from the shortest separation between
samples, and every sample must lie within tolerance of a cell of a
lattice point (so that a Fourier transform onto the lattice is exact).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
u
|
array - like
|
Sample coordinates (concrete arrays, e.g. in metres). |
required |
v
|
array - like
|
Sample coordinates (concrete arrays, e.g. in metres). |
required |
tolerance
|
float
|
Largest allowed offset from a lattice point, in cells. |
1e-06
|
Returns:
| Type | Description |
|---|---|
UVGrid or None
|
|
Functions
closure_phases(cvis, index_cps1, index_cps2, index_cps3)
Calculate closure phases from complex visibilities.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cvis
|
array - like
|
Complex visibilities, one per sample. |
required |
index_cps1
|
array - like
|
For each closure phase, the sample of baseline |
required |
index_cps2
|
array - like
|
For each closure phase, the sample of baseline |
required |
index_cps3
|
array - like
|
For each closure phase, the sample of baseline |
required |
Returns:
| Type | Description |
|---|---|
array - like
|
Closure phases |
Notes
This helper returns radians for internal modeling consistency. Convert to
degrees before writing OIFITS phase columns (e.g., T3PHI).
cp_indices(vis_sta_index, cp_sta_index)
Map closure-triangle station indices to baseline indices.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vis_sta_index
|
array - like
|
Station index pairs |
required |
cp_sta_index
|
array - like
|
Station index triplets |
required |
Returns:
| Type | Description |
|---|---|
tuple[ndarray, ndarray, ndarray]
|
Arrays |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a triangle needs a baseline that is missing, or stored only in the reversed orientation. |
Notes
Baselines are matched on station indices alone. For data with several
epochs or wavelength channels use virgil.oifits.read_oifits,
which also matches on instrument and MJD.