virgil.epochs
Datasets grouped into named epochs, with one snapshot of a moving scene per dataset, for orbit fits to visibilities and closure phases across epochs, and the tools that start and sample those fits: ranking trial orbits by the data, choosing distinct starts for each chain, and starting from a fit of positions.
Named epochs of data, with one snapshot of a moving scene per dataset.
An orbit fit to interferometric data across epochs predicts each epoch's
visibilities and closure phases from the scene at that epoch's time. When
nothing moves appreciably during an observation (a binary whose period is
much longer than a night), one snapshot of the scene per dataset is
enough: the orbit is solved once per dataset, and the snapshot is a static
model, evaluated on all of the dataset's samples at once on the fast path
(e.g. BinaryModelCartesian for
OrbitalBinary). That is much cheaper
than OIData.model's evaluation of a
time-dependent scene at every sample's own time, which remains the choice
when the scene moves within an observation.
Epochs keys epochs and datasets by name,
so that per-dataset nuisance terms (error scales, wavelength scales,
North angles) are attached to the right data however the list is
ordered, and it hands fit and
numpyro_model what they take: a
model function returning one snapshot per dataset, the datasets, and one
noise dict per dataset.
Fringe aliases make the likelihood of visibilities multimodal on the scale of the resolution λ/B, so an orbit fit needs a start in the right basin. The tools for starting and sampling such a fit are:
rank_orbits: the log likelihood of all the data for each of many trial orbits (e.g. fromstarting_orbits), best first;chain_starts: the best distinct orbits of a ranking, one per chain;epoch_positions: the companion's position in each dataset, from a grid and a binary fit;start_from_positions: the whole start: positions, starting orbits, ranking on the visibilities and refinement withfitfrom several distinct orbits, withOrbitStart.chain_valuesgiving one start per chain forchain_init_params.
Epochs
Datasets grouped into named epochs, each seen as one snapshot.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
epochs
|
dict
|
|
required |
at
|
(dataset, epoch)
|
Where each snapshot is taken: at the mean time of each dataset's samples (default), or at the mean time of the whole epoch, shared by its datasets. |
"dataset"
|
times
|
dict
|
|
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
names |
tuple of str
|
The epochs, in the order given. |
dataset_names |
tuple of str
|
The datasets, epoch by epoch. |
epoch_of |
tuple of str
|
The epoch of each dataset. |
data |
tuple of OIData
|
The datasets, in the order of |
times |
ndarray
|
The snapshot time of each dataset (MJD, float64). |
spread_days |
ndarray
|
The largest distance in time of each dataset's samples from its
snapshot (days). The scene should move by much less than the
resolution (λ/B) over this time; if it does not, evaluate the
dataset per sample instead (pass it to |
Examples:
>>> import numpy as onp
>>> from virgil.epochs import Epochs
>>> from virgil.oidata import OIData
>>> def night(mjd):
... return OIData({"u": [10.0, 20.0], "v": [5.0, -3.0],
... "wavel": 2.2e-6, "vis": [1.0, 1.0],
... "d_vis": [0.01, 0.01], "mjd": [mjd, mjd + 0.1]})
>>> epochs = Epochs({"2023": {"ut": night(60100.0), "at": night(60101.0)},
... "2024": night(60500.0)})
>>> epochs.dataset_names, epochs.epoch_of
(('ut', 'at', '2024'), ('2023', '2023', '2024'))
>>> [round(float(t), 2) for t in epochs.times]
[60100.05, 60101.05, 60500.05]
>>> epochs.noise({"2023": {"vis_scale": 2.0}, "2024": {"vis_scale": 3.0}})
[{'vis_scale': 2.0}, {'vis_scale': 2.0}, {'vis_scale': 3.0}]
names = tuple(epochs)
instance-attribute
dataset_names = tuple(names)
instance-attribute
epoch_of = tuple(epoch_of)
instance-attribute
data = tuple(data)
instance-attribute
at = at
instance-attribute
resolution_mas
property
The finest resolution λ/B_max over the datasets (mas).
Fringe aliases of a companion's position are spaced by about this much, so starting orbits closer together than a fraction of it are one mode.
__init__(epochs, at='dataset', times=None)
__len__()
__repr__()
index(name)
The position of dataset name in data (and in noise).
Fitted per-dataset noise terms are the sites
"noise[<index>].<term>" of fit and numpyro_model.
snapshots(scene)
The scene at each dataset's snapshot time, one model per dataset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scene
|
SourceModel or sequence of SourceModel
|
One scene for all datasets, or one per dataset (e.g. with per-dataset fluxes). A static scene is its own snapshot. |
required |
Returns:
| Type | Description |
|---|---|
list of SourceModel
|
Static models, in the order of |
model_fn(scene_fn)
A model function for fit/numpyro_model: the snapshots of
scene_fn(**values), one per dataset.
noise(terms)
One noise dict per dataset, from terms keyed by name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
terms
|
dict
|
|
required |
Returns:
| Type | Description |
|---|---|
list of dict
|
In the order of |
loglike(scene, noise=None)
The log likelihood of all the data, one snapshot per dataset.
The sum over datasets of
model_loglike of each
snapshot, with noise (values, keyed by name as for
:meth:noise) applied to each dataset. Traceable: it can be
jitted, differentiated or vmapped in the scene's parameters.
RankedOrbits
dataclass
Orbits ranked by the log likelihood of multi-epoch data, best first.
Returned by rank_orbits and
chain_starts. Iterating gives
(orbit, loglike) pairs, like the (orbit, χ²) pairs of
starting_orbits, and
ranked[k] is the k-th pair.
Attributes:
| Name | Type | Description |
|---|---|---|
orbits |
tuple
|
The orbits, best first. |
loglike |
ndarray
|
The log likelihood of all the data for each orbit (with the
|
order |
ndarray
|
The position of each orbit in the list that was ranked. |
times |
ndarray
|
The snapshot times of the datasets (MJD), at which
:meth: |
resolution_mas |
float
|
The finest resolution λ/B_max of the data (mas). |
orbits
instance-attribute
loglike
instance-attribute
order
instance-attribute
times
instance-attribute
resolution_mas
instance-attribute
best
property
The orbit with the highest log likelihood.
__init__(orbits, loglike, order, times, resolution_mas)
__len__()
__getitem__(k)
__iter__()
positions()
The companion's position (dra, ddec) (mas) of each orbit at
each snapshot time, shape (n_orbits, n_times, 2).
EpochPositions
dataclass
The companion's position in each dataset, from
epoch_positions.
Positions, covariances and gap_marginal come from the
scale-marginalized surface
(marginal_loglike), on which each
observable block's error scale is integrated out: they do not change
when a dataset's quoted errors, or only its closure-phase errors, are
multiplied by a constant.
Attributes:
| Name | Type | Description |
|---|---|---|
names |
tuple of str
|
The datasets. |
mjd |
ndarray
|
Each dataset's snapshot time (MJD). |
dra, ddec |
ndarray
|
The fitted positions (mas, East and North). |
cov |
ndarray
|
The covariance of each position, shape |
flux |
ndarray
|
The fitted companion/primary flux of each dataset (at most 1). |
gap |
ndarray
|
The gap on the quoted errors, as before |
gap_marginal |
ndarray
|
How decisive each dataset is, on the scale-marginalized surface:
the same difference between the best grid position and its best
rival more than |
chi2_raw |
tuple of dict
|
For each dataset, χ²/N on the quoted errors at the fitted
position, N = |
scale |
tuple of dict
|
For each dataset, the fitted error scale ŝ = √(χ²/ν) of each
block ( |
peaks |
tuple of EpochPeaks
|
For each dataset, the catalogue of its peaks, best first: the first is the fitted position. |
names
instance-attribute
mjd
instance-attribute
dra
instance-attribute
ddec
instance-attribute
cov
instance-attribute
flux
instance-attribute
gap
instance-attribute
gap_marginal
instance-attribute
chi2_raw
instance-attribute
scale
instance-attribute
peaks = ()
class-attribute
instance-attribute
edge
property
Whether each dataset's best peak started at the edge of the
position grid (see EpochPeaks.edge).
__init__(names, mjd, dra, ddec, cov, flux, gap, gap_marginal, chi2_raw, scale, peaks=())
decisive(min_gap)
Whether each dataset's gap_marginal exceeds min_gap.
positions(*, t_ref, min_gap=None)
The positions as PositionData,
for starting_orbits.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t_ref
|
float
|
The reference time (MJD) of the orbits fitted to them: give
the |
required |
min_gap
|
float
|
Keep only the datasets whose |
None
|
EpochPeaks
dataclass
The catalogue of one dataset's peaks, from
epoch_positions: the highest
distinct local maxima of its grid, each refined with refine,
best first.
Attributes:
| Name | Type | Description |
|---|---|---|
dra, ddec, flux |
ndarray
|
The peaks' positions (mas, East and North) and companion/primary fluxes, after refinement. |
loglike |
ndarray
|
The log likelihood of each peak on the quoted errors. |
marginal |
ndarray
|
The scale-marginalized score m of each peak
( |
weight |
ndarray
|
The posterior mass of each peak in this dataset alone, summing
to 1: the Laplace approximation w ∝ exp(m) det(H)^(-1/2), with H
the Hessian of -m at the peak over (dra, ddec, flux), so that a
broad peak outweighs a narrow one of the same height. Where
|
edge |
ndarray
|
Whether each peak started from a grid point on the edge of the position grid. A best peak at the edge may stand for a companion outside the grid: treat that dataset as ambiguous. |
laplace |
ndarray
|
Whether each peak's weight is its Laplace mass: false without
|
dra
instance-attribute
ddec
instance-attribute
flux
instance-attribute
loglike
instance-attribute
marginal
instance-attribute
weight
instance-attribute
edge
instance-attribute
laplace
instance-attribute
__init__(dra, ddec, flux, loglike, marginal, weight, edge, laplace)
__len__()
OrbitStart
dataclass
A start for a multi-epoch orbit fit, from
start_from_positions.
Attributes:
| Name | Type | Description |
|---|---|---|
positions |
EpochPositions
|
The per-dataset positions that seeded the orbits. |
candidates |
RankedOrbits
|
The starting orbits, ranked by the visibilities. |
fits |
tuple of FitResult
|
The fits to the visibilities from the distinct best candidates,
lowest loss first, and those with a non-finite loss last. Each
|
data |
Epochs
|
The data. |
failed |
tuple of (int, str)
|
The candidates whose refinement fit raised an error, with the error. |
seeded |
tuple of str
|
The datasets whose positions seeded the starting orbits (a
|
ambiguous |
tuple of str
|
The other datasets: their best peak has a rival within
|
positions
instance-attribute
candidates
instance-attribute
fits
instance-attribute
data
instance-attribute
failed = ()
class-attribute
instance-attribute
seeded = ()
class-attribute
instance-attribute
ambiguous = ()
class-attribute
instance-attribute
best
property
The fit with the lowest loss.
__init__(positions, candidates, fits, data, failed=(), seeded=(), ambiguous=())
modes(*, max_delta_loss=10.0, same_mode_chi2=1.0)
The distinct fits, lowest loss first.
Two fits are one mode when their predictions differ by less than
same_mode_chi2 in χ² (the sum of squared differences of their
whitened residuals, with the quoted errors). Fits whose loss is
more than max_delta_loss above the best's are dropped: a
difference Δ in loss is a factor of about e^Δ in posterior
density. Fits with a non-finite loss are left out.
chain_values(n_chains, **options)
One start (fitted values) per chain, cycling through the
distinct modes (see :meth:modes, which takes options).
Pass them to
chain_init_params to
start each chain of numpyro's MCMC in its own mode.
rank_orbits(model, data, orbits, *, scales=None, noise=None, dof=1.0, s_max=None, batch_size=None, dtype='float64')
Rank trial orbits by the log likelihood of multi-epoch data.
Each orbit's scene is evaluated at every dataset's snapshot time and
compared with all the data at once, by one compiled kernel mapped
over the orbits. This judges trial orbits, e.g. from a Thiele–Innes
grid on rough positions
(starting_orbits) or from prior
draws, by the interferometric data themselves rather than by the
positions that seeded them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
callable
|
Maps an orbit to a time-dependent scene, e.g.
|
required |
data
|
Epochs
|
The data, one snapshot per dataset. |
required |
orbits
|
sequence or orbit
|
A list of orbits, the |
required |
scales
|
(quoted, marginal)
|
How the errors are scaled. |
"quoted"
|
noise
|
dict
|
Noise values keyed by epoch or dataset name, as for
|
None
|
dof
|
optional
|
For |
1.0
|
s_max
|
optional
|
For |
1.0
|
batch_size
|
int
|
Orbits evaluated together by |
None
|
dtype
|
(float64, float32)
|
Precision of the evaluation (default float64, in a local
|
"float64"
|
Returns:
| Type | Description |
|---|---|
RankedOrbits
|
The orbits sorted by log likelihood, best first. An orbit whose
likelihood is not finite ranks last, with |
Examples:
>>> candidates = starting_orbits(positions, periods)
>>> ranked = rank_orbits(lambda o: OrbitalBinary(o, 0.1), epochs,
... candidates, scales="marginal")
>>> ranked.best, ranked.loglike[:3]
chain_starts(ranked, n_chains, *, min_distance_mas=None)
The best distinct orbits of a ranking, one per chain.
Starting every chain at one best fit hides other modes; starting
chains in different good modes (the best few distinct orbits) lets
the chains show whether the posterior is multimodal. Two orbits are
the same mode when the companion's positions at every snapshot time
agree to within min_distance_mas: the mirror orbit (Ω + 180°,
ω + 180°), which has the same sky motion, is the same mode, while a
fringe alias or a period alias is not.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ranked
|
RankedOrbits
|
From |
required |
n_chains
|
int
|
Number of starts. |
required |
min_distance_mas
|
float
|
The largest difference in position (mas) at any snapshot time for two orbits to count as one mode; by default half the finest resolution λ/B_max of the data. |
None
|
Returns:
| Type | Description |
|---|---|
RankedOrbits
|
|
epoch_positions(data, grid, *, gap_mas=None, refine=True, n_peaks=5, dof=1.0, s_max=None, batch_size=4096)
The companion's position in each dataset of a binary.
For each dataset: a static binary
(BinaryModelCartesian) on a
grid of positions and fluxes, scored on the scale-marginalized
surface m = -Σ_b (ν_b/2) ln χ²_b
(marginal_loglike), in which the
error scale of each observable block (visibilities, closure phases)
is integrated out under its Jeffreys prior. The n_peaks highest
distinct local maxima of the grid (more than gap_mas apart) are
each refined (with refine) by maximizing m, which is the fit with
a free vis_scale and phi_scale per dataset, profiled, and
ranked by their refined m: the best grid point is not always the best
peak once refined, since fringe peaks are often narrower than the
grid step. The best refined peak is the position, and the curvature
of m there gives its covariance. Every peak is kept in the catalogue
EpochPositions.peaks. The
positions, covariances and gap_marginal therefore do not depend
on how well each block's errors were quoted, and nights whose errors
are underestimated do not look more decisive than they are. These
positions are only a starting point for a fit of the orbit to the
visibilities: when the scene is more than two point stars they can be
biased.
The raw χ²/N on the quoted errors and the fitted scale of each block
are recorded, and a UserWarning names the datasets whose raw
χ²/N exceeds 4 (errors underestimated by more than 2): their orbit
fits need fitted error scales, and a rescaled χ²/N ≈ 1 does not make
them good fits.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Epochs
|
The data. Datasets with gains, closure-phase offsets or a
model-dependent covariance are refused (see |
required |
grid
|
dict
|
|
required |
gap_mas
|
float
|
The distance (mas) beyond which another peak counts as a rival
for |
None
|
refine
|
bool
|
Refine each grid peak by maximizing m (default). Otherwise the position is the best grid point, with a covariance of one grid step squared, and the peaks are the grid points. |
True
|
n_peaks
|
int
|
The peaks catalogued and refined per dataset (default 5). Each
refinement is a small fit, so the cost of |
5
|
dof
|
float or dict
|
The effective-dof fraction ν_eff/ν of each dataset's blocks (one
number, or a dict keyed by dataset or epoch name; default 1),
for errors correlated beyond the quoted ones (see
|
1.0
|
s_max
|
float
|
Bound the scales and integrate them out numerically (see
|
None
|
batch_size
|
int
|
Grid points evaluated together. |
4096
|
Returns:
| Type | Description |
|---|---|
EpochPositions
|
|
marginal_loglike(model, data, *, dof=1.0, s_max=None, **noise)
The log likelihood with each block's error scale marginalized.
Each observable block b of data (visibilities, phases, and each
extra observable) has an unknown factor s_b on its quoted errors.
Integrating it out under its Jeffreys prior 1/s_b gives, up to a
constant,
m = -Σ_b (ν_b/2) ln χ²_b,
where χ²_b is the block's χ² on the quoted errors (from
whitened_residuals) and
ν_b its number of independent observables
(n_independent, split by
block). Profiling s_b gives the same function, at
ŝ_b² = χ²_b/ν_b. m does not change when any block's errors are
multiplied by a constant, so its differences between models (e.g.
the gap between two peaks) do not depend on how well the errors were
quoted. Near a peak, m ≈ -χ²/(2ŝ²): differences are those of the
quoted-error log likelihood divided by ŝ².
Every block uses the Gaussian (small-σ) normalization, including
uncorrelated closure phases, whose likelihood in
model_loglike is a von Mises
density; for those the unbounded marginal would be improper as
s → ∞. m is a search surface, not a replacement for
model_loglike. Where sσ is not small (weak closure phases with a
large scale), pass s_max.
Data whose likelihood has a model-dependent normalization are refused
with a NotImplementedError: gains
(OIData.with_gains), closure-phase
offsets
(OIData.with_closure_offsets)
and extra observables with a model-dependent covariance. Their
marginalized nuisance covariance is not multiplied by an error scale,
so a block's χ² is not ∝ 1/s² and the scale would not be integrated
out. The same holds for epoch_positions and
rank_orbits(scales="marginal").
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
SourceModel
|
Model to evaluate. |
required |
data
|
OIData
|
Data to compare with. |
required |
dof
|
float
|
The effective number of degrees of freedom as a fraction of ν_b
(default 1). Errors correlated beyond the model of the quoted
errors carry fewer than ν_b degrees of freedom, and |
1.0
|
s_max
|
float
|
Bound each scale to [1/s_max, s_max] and integrate it out
numerically, with the exact von Mises normalization for
uncorrelated closure phases: |
None
|
**noise
|
Other noise terms (e.g. |
{}
|
Returns:
| Type | Description |
|---|---|
float
|
|
start_from_positions(model, priors, data, start_values, *, grid, periods, t_ref, scales=None, noise=None, dof=1.0, s_max=None, eccs=None, n_phase=36, n_candidates=200, n_refine=4, min_gap=5.0, refine_positions=True, n_peaks=5, batch_size=None, **fit_options)
Start an orbit fit to multi-epoch visibilities from a positions fit.
The likelihood of visibilities is multimodal on the scale of the resolution λ/B (fringe aliases), and a sampler started from default values can stick in an alias. This builds a start in four steps:
- Positions: the companion's position in each dataset, the
best of its refined grid peaks
(
epoch_positions). - Starting orbits: a Thiele–Innes grid over period, eccentricity
and time of periastron on the decisive datasets' positions
(
starting_orbits). An ambiguous dataset does not seed them; its other peaks are kept inpositions.peaks, but only its visibilities judge between them here. - Ranking of those orbits by the likelihood of all the
visibilities, with each orbit mapped to the model's parameters by
start_valuesand the flux at the median of the positions fits. - Refinement with
fitof the full model (noise terms included) from then_refinebest distinct orbits (as forchain_starts).
Only the start comes from positions: nothing here enters the
posterior. Sample from the best fit with numpyro's init_to_value,
or start one chain in each mode with
OrbitStart.chain_values and
chain_init_params.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
callable
|
The scene function, called with the parameters in |
required |
priors
|
dict
|
The priors of the fit, as for |
required |
data
|
Epochs
|
The data. |
required |
start_values
|
callable
|
|
required |
grid
|
dict
|
The per-dataset grid of positions and fluxes (see
|
required |
periods
|
array - like
|
Trial periods (days) for |
required |
t_ref
|
float
|
The reference time (MJD) of the starting orbits: the |
required |
scales
|
(quoted, marginal)
|
How step 3 ranks the candidates (see |
"quoted"
|
noise
|
dict or list of dict
|
Noise terms of the refinement fits, as for |
None
|
dof
|
optional
|
Passed to |
1.0
|
s_max
|
optional
|
Passed to |
1.0
|
eccs
|
optional
|
Passed to |
None
|
n_phase
|
optional
|
Passed to |
None
|
n_candidates
|
int
|
Starting orbits to rank (default 200). |
200
|
n_refine
|
int
|
Distinct orbits to refine (default 4). |
4
|
min_gap
|
float
|
Seed orbits only with datasets whose positions are decisive: a
|
5.0
|
refine_positions
|
bool
|
Refine each grid peak with a fit (see |
True
|
n_peaks
|
int
|
Peaks refined per dataset (see |
5
|
batch_size
|
int
|
Orbits ranked together (see |
None
|
**fit_options
|
Passed to the refinement fits (e.g. |
{}
|
Returns:
| Type | Description |
|---|---|
OrbitStart
|
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If every refinement fit raised an error (chained to the last). |