Skip to content

virgil.orbit_search

Scoring candidate orbits on multi-epoch data with the nuisances an orbit shares across epochs shared in the score: one companion flux per band (with an optional chromatic slope) integrated out on a log-uniform grid, each dataset's error scales marginalized, calibration gains and closure-phase offsets profiled, and extra terms such as radial velocities added. Positions are evaluated at every sample's own time.

Details

For a scene linear in the companion's flux f, the complex visibility is V(f) = (A + f B) / (T₀ + f ΔT), with A and the total fluxes computed once per dataset and B from the visibilities of a unit companion.

The coarse flux grid only finds the peak. Newton steps in ln f (and the slope β) profile it, and Gauss–Legendre nodes on each side of it, out to a 12-nat drop or the prior's bounds, integrate it: a well-measured flux (σ_ln f ≈ 0.02) is far narrower than any affordable grid step.

With s_max=None a block's −(ν/2) ln χ² is unbounded as χ² → 0, so a block with few degrees of freedom (one triangle's closure phases, say) that a flux or slope fits almost exactly makes a narrow spike in the score, which can dominate the profile (profiled, flux, slope). The default s_max=5 leaves about twice the error inflation of typical interferometric data and bounds each block's gain at about ν ln s_max.

Scoring candidate orbits on multi-epoch data, with shared nuisances.

Provisional

May change in any 0.4.x release without a deprecation cycle.

score_orbits scores many candidate orbits exactly on all the data at once, sharing across epochs the nuisances that one orbit shares:

  • Flux. One companion flux per band, optionally with a chromatic power law f(lambda) = f0 (lambda/lambda0)^beta, integrated out under a log-uniform prior about its profiled peak. A flux per epoch is a strictly looser model that makes peaks no single-flux orbit visits, so it is never used to rank.
  • Error scales are integrated out block by block (marginal_loglike).
  • Gains and closure-phase offsets (OIData.with_gains, OIData.with_closure_offsets) are per epoch and linear, so they are profiled analytically, not marginalized, keeping a block's chi-squared proportional to 1/s^2.

The companion is placed at every sample's own time, so motion within a night needs no threshold. Extra terms such as radial velocities (RVData) or positions (PositionData) are added to the score.

SharedFlux dataclass

The companion flux shared across epochs, per band.

Parameters:

Name Type Description Default
grid (tuple, array - like or dict)

Flux grid at the reference wavelength: (lo, hi, n) (last element an integer) for n points uniform in ln f, or the flux values (a tuple whose last element is not an int; one value fixes the flux), or {band: either}. Default (1e-3, 1.0, 16); the prior is log-uniform over the grid's range.

(0.001, 1.0, 16)
bands dict

{epoch or dataset name: band} (a dataset's entry overrides its epoch's); datasets of a band share its flux. Default one band, "all".

None
reference optional

The band whose flux is at most 1 (default the first); it fixes which star is the companion in every band; the others may have grids above 1.

None
slope (tuple, array - like or dict)

The chromatic slope beta, (lo, hi, n) (uniform prior), values, or a dict per band; None fixes beta = 0.

None
wavel0 float or dict

Reference wavelength (m); default the geometric mean per band.

None
order int

Gauss-Legendre nodes per side of the marginal (16; each side runs to 12 nats below the peak, or the prior bound) and Newton steps to the profiled peak (8).

16
newton int

Gauss-Legendre nodes per side of the marginal (16; each side runs to 12 nats below the peak, or the prior bound) and Newton steps to the profiled peak (8).

16

grid = (0.001, 1.0, 16) class-attribute instance-attribute

bands = None class-attribute instance-attribute

reference = None class-attribute instance-attribute

slope = None class-attribute instance-attribute

wavel0 = None class-attribute instance-attribute

order = 16 class-attribute instance-attribute

newton = 8 class-attribute instance-attribute

__init__(grid=(0.001, 1.0, 16), bands=None, reference=None, slope=None, wavel0=None, order=16, newton=8)

__post_init__()

OrbitScores dataclass

The scores of candidate orbits, in the order they were given.

Returned by score_orbits; rank with :meth:order or rank_scores.

Attributes:

Name Type Description
score, profiled ndarray

(n,): the log likelihood with the shared flux (and slope) integrated out (-inf where not finite), and at the profiled flux.

flux, flux_err, flux_at_edge, slope, slope_err, slope_at_edge ndarray

(n, n_band): each band's profiled flux and chromatic slope (0 without a slope grid), their uncertainties (NaN with one grid point or non-positive curvature; marginal when both are free) and whether they sit at a prior bound.

fallback ndarray

(n, n_band) bool: the curvature at the peak was unusable, so the marginal is the coarse grid's trapezoidal rule.

terms ndarray

(n, n_terms): each extra term's log likelihood (in score).

scale, scale_at_bound ndarray

(n, n_dataset): each dataset's largest error scale at the profiled flux, and whether it presses against s_max (the score is then penalized; always False with s_max=None).

bands, reference, cost (tuple, object, int)

The band names, the reference band, and the work units spent (candidates times each dataset's band-marginal score evaluations).

score instance-attribute

profiled instance-attribute

flux instance-attribute

flux_err instance-attribute

flux_at_edge instance-attribute

slope instance-attribute

slope_err instance-attribute

slope_at_edge instance-attribute

fallback instance-attribute

terms instance-attribute

scale instance-attribute

scale_at_bound instance-attribute

bands instance-attribute

reference instance-attribute

cost instance-attribute

__init__(score, profiled, flux, flux_err, flux_at_edge, slope, slope_err, slope_at_edge, fallback, terms, scale, scale_at_bound, bands, reference, cost)

__len__()

order(quantum=_QUANTUM)

Candidate indices, best first, ties broken by index (see rank_scores).

score_orbits(model, epochs, orbits, *, shared=None, terms=(), scales='marginal', dof=1.0, s_max=5.0, batch_size=None, max_evaluations=None, dtype='float64')

Score candidate orbits on all epochs, with the flux shared.

Unlike rank_orbits, each companion is placed at every sample's own time. The score is the scale-marginalized log likelihood with the flux integrated out and the extra terms added (details).

Parameters:

Name Type Description Default
model callable

model(orbit, flux) returns the scene, e.g. OrbitalBinary; it must be linear in flux and independent of the orbit at flux 0 (both are checked).

required
epochs Epochs

The data; datasets without sample times use Epochs.times.

required
orbits sequence or orbit

Orbits (or starting_orbits pairs) of one class and t_ref, or one orbit with a leading candidate axis.

required
shared SharedFlux

Flux grids, bands and slopes (default one band, f in [1e-3, 1]).

None
terms sequence

Extra log likelihood terms or callables orbit -> log likelihood.

()
scales optional

As for rank_orbits; s_max defaults to 5, and a candidate needing more is penalized (OrbitScores.scale_at_bound).

'marginal'
dof optional

As for rank_orbits; s_max defaults to 5, and a candidate needing more is penalized (OrbitScores.scale_at_bound).

'marginal'
s_max optional

As for rank_orbits; s_max defaults to 5, and a candidate needing more is penalized (OrbitScores.scale_at_bound).

'marginal'
batch_size optional

Candidates per step (never changes the result); a budget in score evaluations (over it raises ValueError); precision.

None
max_evaluations optional

Candidates per step (never changes the result); a budget in score evaluations (over it raises ValueError); precision.

None
dtype optional

Candidates per step (never changes the result); a budget in score evaluations (over it raises ValueError); precision.

None

Returns:

Type Description
OrbitScores

In the order of orbits; rank with .order().

rank_scores(score, quantum=_QUANTUM)

Indices that sort score from best to worst, deterministically.

Scores are first rounded to multiples of quantum (nats), and equal rounded scores rank by index, so that two candidates differing only by rounding error (e.g. mirror-image grid cells) rank the same way on every platform and in either precision. Non-finite scores rank last.

Parameters:

Name Type Description Default
score array - like

One score per candidate, higher is better.

required
quantum float

The rounding step (default 1e-3).

_QUANTUM

Returns:

Type Description
ndarray

Integer indices, best first.

Examples:

>>> rank_scores([1.0, 3.0, 3.0 + 1e-9, -float("inf"), 2.0]).tolist()
[1, 2, 4, 0, 3]