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.
Scoring candidate orbits on multi-epoch data, with shared nuisances.
The first stage of an automatic orbit search scores many candidate orbits
on all the data at once. score_orbits
does this exactly, on the visibilities and closure phases, with the
nuisances that a single orbit shares across epochs shared in the score:
- Flux. One companion flux per band (or instrument), the same at every epoch of that band, optionally with a chromatic power law f(λ) = f₀ (λ/λ₀)^β. It is integrated out under a log-uniform (Jeffreys) prior, adaptively about its profiled peak, and its profiled value and uncertainty are reported. Letting each epoch have its own flux is a strictly looser model, whose extra freedom moves each epoch's peaks and makes new ones that no orbit with one flux visits, so it is never used to rank.
- Error scales. Each dataset's error scale is integrated out block by
block, as in
marginal_loglike. - Calibration gains set with
OIData.with_gainsand closure-phase offsets set withOIData.with_closure_offsetsare truly per epoch. They enter the residuals linearly and are profiled analytically (each costs one degree of freedom), not marginalized, so that a block's χ² stays proportional to 1/s².
The companion's position is evaluated at every sample's own time, so
motion within a night needs no threshold. Extra likelihood terms, such as
radial velocities (RVData) or published
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)
|
The coarse flux grid at the reference wavelength λ₀: |
(0.001, 1.0, 16)
|
bands
|
dict
|
|
None
|
reference
|
optional
|
The band whose flux is at most 1 (default: the first band, in the order of the datasets). Bounding the flux in one band fixes which star is called the companion (a companion at r with flux f is the same in V² and closure phases as one at −r with flux 1/f), so that the labelling is the same in every band; the other bands may be given grids above 1. |
None
|
slope
|
(tuple, array - like or dict)
|
The chromatic slope β, as |
None
|
wavel0
|
float or dict
|
The reference wavelength λ₀ (metres), one or per band; by default the geometric mean of each band's wavelengths. |
None
|
order
|
int
|
Gauss–Legendre nodes per side of the adaptive marginal (default
16): each side of the profiled peak is integrated out to where the
score has fallen by 12 nats (grown from the curvature's length
scale, and at a prior bound also the slope's), cut at the prior's
bounds, with the nodes crowded towards the peak. With a slope,
|
16
|
newton
|
int
|
Newton steps from the best coarse grid point to the profiled peak (default 8), each at most one grid step and kept only if it raises the score. |
8
|
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
them with :meth:order (or
rank_scores).
Attributes:
| Name | Type | Description |
|---|---|---|
score |
ndarray
|
|
profiled |
ndarray
|
|
flux, flux_err |
ndarray
|
|
flux_at_edge |
ndarray
|
|
slope, slope_err, slope_at_edge |
ndarray
|
The same for the chromatic slope β (0 and NaN without a slope grid). With both free, the uncertainties are marginal (from the inverse of the 2 × 2 curvature). |
fallback |
ndarray
|
|
terms |
ndarray
|
|
scale |
ndarray
|
|
scale_at_bound |
ndarray
|
|
bands |
tuple
|
The band names, in column order; |
reference |
object
|
|
cost |
int
|
The work units spent: candidates × Σ over datasets of the score evaluations of the band's marginal (grid points, Newton steps with their derivatives, and nodes). |
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(epochs, model, 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.
For each candidate, the companion's position is computed at every
sample's own time, and the visibilities g of a unit companion
there once. Every evaluation at another flux is then cheap
arithmetic on g: for a scene linear in the companion's flux f
(a component weight), 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 g. With a slope, f
differs per sample: f = f₀ (λ/λ₀)^β.
For each dataset and flux the score is the scale-marginalized
log likelihood m = -Σ_b (ν_b/2) ln χ²_b of
marginal_loglike (or, with
s_max, its bounded form; with scales="quoted", -χ²/2).
Gains (OIData.with_gains) and
closure-phase offsets
(OIData.with_closure_offsets)
are profiled analytically inside χ²: their widths are ignored, and each
independent mode removes one degree of freedom from its block (a
single gain per dataset is with_gains(modes=onp.ones((1, n))), with
n the number of samples).
Each band's flux is then integrated out under its log-uniform prior.
The coarse grid only finds the peak: Newton steps in ln f (and β)
profile it, and Gauss–Legendre nodes on each side of it, out to a
12-nat drop or the prior's bounds, integrate it, so that the marginal does not
depend on where the peak falls between grid points (a well-measured
flux, σ_ln f ≈ 0.02, is far narrower than any affordable grid step).
Where the curvature at the peak is not usable, the coarse grid's
trapezoidal rule is used and OrbitScores.fallback is set. The
extra terms are then added.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
epochs
|
Epochs
|
The data. Datasets without sample times ( |
required |
model
|
callable
|
|
required |
orbits
|
sequence or orbit
|
A list of orbits (or |
required |
shared
|
SharedFlux
|
The shared flux grids, bands and slopes (default |
None
|
terms
|
sequence
|
Extra log likelihood terms: likelihood terms from
|
()
|
scales
|
(marginal, quoted)
|
Integrate each dataset's error scales out (default), or use the
quoted errors (the score is then |
"marginal"
|
dof
|
optional
|
The effective-dof fraction (one number, or a dict keyed by dataset
or epoch name) and the bound on the scales, as for
|
1.0
|
s_max
|
optional
|
The effective-dof fraction (one number, or a dict keyed by dataset
or epoch name) and the bound on the scales, as for
|
1.0
|
batch_size
|
int
|
Candidates evaluated together by |
None
|
max_evaluations
|
int
|
A budget in work units: candidates × Σ over datasets of the score
evaluations of the dataset's band's marginal (coarse grid points,
Newton steps counting each derivative as one, and nodes). The cost is predicted before
anything is evaluated, and a cost over budget raises a
|
None
|
dtype
|
(float64, float32)
|
Precision of the evaluation (default float64, in a local
|
"float64"
|
Returns:
| Type | Description |
|---|---|
OrbitScores
|
Scores in the order of |
Examples:
>>> scores = score_orbits(epochs, OrbitalBinary, candidates,
... shared=SharedFlux((0.01, 1.0, 16)))
>>> best = scores.order()[0]
>>> scores.flux[best], scores.flux_err[best]
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]