Skip to content

virgil.orbits

Keplerian orbits of a binary's secondary about its primary, in virgil's conventions: dra East, ddec North and dz away from the observer (mas); inc below 90° turns the position angle forward; Omega is the position angle of the node where the secondary recedes; omega is the secondary's argument of periastron; and times are days since a static float64 t_ref. Kepler's equation is solved by jaxoplanet, installed with pip install "virgil-astro[orbits]".

Radial velocities from several spectrographs can share an orbit fit without fitting their zero points: RVData(..., instrument=labels) with RVData.term(params, marginalize_offsets=(mean, sd)) marginalizes one velocity zero point per instrument analytically (Luger, Foreman-Mackey & Hogg 2017, arXiv:1710.11136), and term.posterior(values) reports them after the fit. With a broad prior this is the profile likelihood plus a log-determinant correction; only a finite prior width is supported. The prior must be stated: marginalize_offsets=True is an error, not a default of N(0, 1000²) km/s.

Angles without a wrap. orientation_priors() samples the node and periastron as angle vectors: 2Ω and ϖ = Ω + ω for positions alone, whose (Ω, ω) and (Ω + 180°, ω + 180°) then fall on one point, or Ω and ϖ (positions_only=False) when RVs fix the node. KeplerOrbit.from_varpi(..., varpi, a_mas, two_Omega=...) builds the orbit. Uniform angles with a prior uniform in cos i are the invariant prior on the orientation; the map to (2Ω, ϖ) has a constant Jacobian. The vectors follow Octofitter's UniformCircular and exoplanet's Angle (see virgil.angles).

The position angle at a reference epoch. For short arcs, KeplerOrbit.from_position_angle(period, theta, ecc, inc, omega, Omega, a_mas, t_ref) takes θ, the position angle at t_ref, which astrometry measures directly, in place of dt_peri (after Thompson et al. 2023). Sample θ as an angle vector, and add position_angle_prior(orbit_fn) to the likelihoods=. It adds log|∂M/∂θ|, so that the prior stays uniform in the time of periastron, which is the invariant prior, rather than uniform in θ. The map is singular at i = 90°, where the position angle takes only two values. Near edge-on, keep dt_peri or use StateVectorOrbit.

Positions measured by an instrument whose North or plate scale is uncertain take per-dataset calibration terms: PositionData.term(orbit, north_angle="north_b", plate_scale="scale_b") compares the data with m R(δ) times the orbit's positions, so that every measured position angle is the true one plus δ and every separation is m times the true one. "north_b" and "scale_b" are fitted values, with priors you supply (there is no default width; a Gaussian should come from the instrument's astrometric calibration). These terms follow Octofitter (Thompson et al. 2023, AJ 166, 164). The interferometric counterparts are the noise= terms north_angle and wavel_scale (see OIData.with_north_angle).

Credit and related software. Kepler's equation is solved by jaxoplanet (Hattori et al., doi:10.5281/zenodo.10736936), the JAX successor to exoplanet (Foreman-Mackey et al. 2021, JOSS 6, 3285); please cite it with virgil when you fit orbits. The Thiele–Innes solve in starting_orbits is the classical method (Thiele 1883, AN 104, 245; Hartkopf, McAlister & Franz 1989, AJ 98, 1014). Analytic marginalization of RV zero points is also done by orvara (Brandt et al. 2021, AJ 162, 186). For orbit fits to relative and absolute astrometry and RVs without an interferometric scene, mature codes exist: orbitize! (Blunt et al. 2020, AJ 159, 89), Octofitter (Thompson et al. 2023, AJ 166, 164; it also fits closure phases and kernel phases of point sources) and orvara (Brandt et al. 2021). They document the same conventions as virgil (the secondary's ω, +z away from the observer), not yet checked numerically. virgil's orbits exist to drive time-dependent scenes, extended components included, in JAX; for Hipparcos and Gaia absolute astrometry use one of those codes and bring the result in as a prior.

Keplerian orbits of a binary's secondary about its primary.

The conventions are:

  • dra is positive East and ddec positive North (mas), as everywhere in virgil, and dz is positive away from the observer, so that (dra, ddec, dz) is right-handed and dz grows while the secondary recedes. The vector runs from the primary (the scene's reference component) to the secondary.
  • inc in [0°, 180°): below 90° the position angle increases with time (counterclockwise on the sky, North through East).
  • Omega: the position angle of the ascending node, the node where the secondary recedes. Positions alone fix it only modulo 180°.
  • omega: the secondary's argument of periastron (the visual-binary convention); the spectroscopic ω of the primary is omega - 180°.
  • dt_peri: the time of periastron minus the static float64 t_ref (days), so that float32 keeps it precise; period in days; a_mas the angular semimajor axis of the relative orbit.

Kepler's equation is solved by jaxoplanet (an optional dependency, pip install "virgil-astro[orbits]"), with its exact derivatives; the positions follow from the Thiele–Innes constants (§2.4). jaxoplanet's own conventions differ (its ω is the primary's, and its third axis points toward the observer) and stay inside :meth:KeplerOrbit.to_jaxoplanet and :meth:KeplerOrbit.from_jaxoplanet.

KeplerOrbit

Bases: Base

A Keplerian orbit of the secondary relative to the primary.

Parameters:

Name Type Description Default
period float

Orbital period (days).

required
dt_peri float

Time of periastron minus t_ref (days).

required
ecc float

Eccentricity, 0 <= ecc < 1.

required
inc float

Inclination (degrees, 0 <= inc < 180; below 90 the position angle increases with time).

required
omega float

The secondary's argument of periastron (degrees), measured from the ascending node in the direction of motion.

required
Omega float

Position angle of the ascending node, where the secondary recedes (degrees, North through East).

required
a_mas float

Angular semimajor axis of the relative orbit (mas).

required
t_ref float

Reference time (MJD, float64, static). Times are measured from it, so that float32 keeps them precise.

0.0
Notes

(Omega + 180°, omega + 180°) gives the same sky positions with dz reversed: positions alone cannot tell them apart.

period = np.asarray(period, dtype=float) instance-attribute

dt_peri = np.asarray(dt_peri, dtype=float) instance-attribute

ecc = np.asarray(ecc, dtype=float) instance-attribute

inc = np.asarray(inc, dtype=float) instance-attribute

omega = np.asarray(omega, dtype=float) instance-attribute

Omega = np.asarray(Omega, dtype=float) instance-attribute

a_mas = np.asarray(a_mas, dtype=float) instance-attribute

t_ref = float(t_ref) class-attribute instance-attribute

__init__(period, dt_peri, ecc, inc, omega, Omega, a_mas, t_ref=0.0)

__check_init__()

from_varpi(period, dt_peri, ecc, inc, varpi, a_mas, *, Omega=None, two_Omega=None, t_ref=0.0) classmethod

The orbit with longitude of periastron varpi = Ω + ω.

Give two_Omega (2Ω, degrees) for positions alone, which fix Ω only modulo 180°, or Omega when other data (RVs) fix the node; see orientation_priors, which samples them as angle vectors. omega is varpi - Omega, the secondary's argument of periastron, as everywhere in virgil.

from_position_angle(period, theta, ecc, inc, omega, Omega, a_mas, t_ref=0.0) classmethod

The orbit whose position angle at t_ref is theta.

An alternative to dt_peri for short arcs, after Thompson et al. (2023, AJ 166, 164): astrometry measures the position angle at an epoch directly. With φ = θ - Ω, the argument of latitude u = ω + f at t_ref follows from (cos φ, sin φ) ∝ (cos u, sin u cos i): u = atan2(sin φ / cos i, cos φ); then the true anomaly f = u - ω, the eccentric and mean anomalies, and dt_peri = -M P / 2π (in [-P/2, P/2)). Sample theta as an AngleVector, and add position_angle_prior to the likelihood terms, so that the prior stays uniform in the time of periastron.

Singular at i = 90°, where the position angle takes only two values (Ω and Ω + 180°) and does not fix the phase; a concrete inc of 90° is rejected. Near edge-on the map is badly conditioned: keep dt_peri, or use StateVectorOrbit.

Parameters:

Name Type Description Default
theta float

Position angle of the secondary at t_ref (degrees, North through East).

required
period

As for KeplerOrbit.

required
ecc

As for KeplerOrbit.

required
inc

As for KeplerOrbit.

required
omega

As for KeplerOrbit.

required
Omega

As for KeplerOrbit.

required
a_mas

As for KeplerOrbit.

required
t_ref

As for KeplerOrbit.

required

thiele_innes()

The Thiele–Innes constants (A, B, F, G, C, H) (mas).

ddec = A X + F Y, dra = B X + G Y and dz = C X + H Y, with X = cos E - e and Y = √(1 - e²) sin E.

relative(mjd)

Position of the secondary relative to the primary (mas).

Parameters:

Name Type Description Default
mjd array - like

Times (MJD). Concrete times are offset from t_ref in float64 first. Under jit the offset is taken in the traced precision, which in float32 is good to only about 0.004 d near MJD 60000.

required

Returns:

Type Description
tuple of arrays

(dra, ddec, dz), each shaped like mjd.

relative_velocity(mjd)

d(dra, ddec, dz)/dt (mas per day), exactly.

frame(mjd)

Angles of the binary frame at mjd (degrees), by name.

  • line_pa: position angle of the line of centres, primary to secondary; towards_primary is line_pa + 180.
  • line_tilt: the line of centres' elevation out of the sky, arcsin(dz / |r|), positive when the secondary is farther.
  • node_pa: Omega, the line of nodes of the orbital plane.
  • inc: the orbit's inclination (0–180); apparent_inc: arccos|cos inc| (0–90), the projected tilt, for components whose inc is an apparent inclination.

separation_pa(mjd)

Separation (mas) and position angle (degrees, North through East, in [0, 360)) of the secondary from the primary.

to_thiele_innes()

The same sky orbit as a :class:ThieleInnesOrbit.

to_jaxoplanet()

A jaxoplanet OrbitalBody for this orbit, and the factor that turns its relative positions into mas.

jaxoplanet's times are days since t_ref, its axes are (X, Y, Z) = (North, East, toward the observer), and its ω is the primary's: (dra, ddec, dz) = (Y, X, -Z) * scale.

from_jaxoplanet(body, a_mas, t_ref=0.0) classmethod

The orbit of a jaxoplanet OrbitalBody whose times are days since t_ref, with angular semimajor axis a_mas.

ThieleInnesOrbit

Bases: Base

A sky orbit in Thiele–Innes form: linear in A, B, F, G.

ddec = A X + F Y and dra = B X + G Y, with X = cos E - e and Y = √(1 - e²) sin E. For fixed (period, dt_peri, ecc) the positions are linear in the four constants, so a starting orbit is a linear least-squares solve on a grid of those three.

Parameters:

Name Type Description Default
period float

As in :class:KeplerOrbit.

required
dt_peri float

As in :class:KeplerOrbit.

required
ecc float

As in :class:KeplerOrbit.

required
A float

Thiele–Innes constants (mas).

required
B float

Thiele–Innes constants (mas).

required
F float

Thiele–Innes constants (mas).

required
G float

Thiele–Innes constants (mas).

required
t_ref float

Reference time (MJD, static float64).

0.0

period = np.asarray(period, dtype=float) instance-attribute

dt_peri = np.asarray(dt_peri, dtype=float) instance-attribute

ecc = np.asarray(ecc, dtype=float) instance-attribute

A = np.asarray(A, dtype=float) instance-attribute

B = np.asarray(B, dtype=float) instance-attribute

F = np.asarray(F, dtype=float) instance-attribute

G = np.asarray(G, dtype=float) instance-attribute

t_ref = float(t_ref) class-attribute instance-attribute

__init__(period, dt_peri, ecc, A, B, F, G, t_ref=0.0)

__check_init__()

sky(mjd)

(dra, ddec) of the secondary from the primary (mas).

to_kepler()

The :class:KeplerOrbit with these sky positions.

Positions fix Omega only modulo 180°: the result has 0 <= Omega < 180, and (Omega + 180, omega + 180) is the other solution, with dz reversed.

StateVectorOrbit

Bases: Base

An orbit given by the relative position and velocity at t_ref.

For short arcs, where the measured quantities (the position and its rate of change) are well determined but the Keplerian elements are not: sampling these instead of the elements avoids long curved degeneracies. The line-of-sight position and velocity, and the gravitational parameter, carry the physical priors.

Parameters:

Name Type Description Default
dra float

Position of the secondary from the primary at t_ref (mas).

required
ddec float

Position of the secondary from the primary at t_ref (mas).

required
vra float

Its velocity at t_ref (mas/yr).

required
vdec float

Its velocity at t_ref (mas/yr).

required
dz float

Line-of-sight position (mas) and velocity (mas/yr), positive away from the observer.

required
vz float

Line-of-sight position (mas) and velocity (mas/yr), positive away from the observer.

required
mu float

Gravitational parameter in angular units, 4π² a_mas³ / P² with P in years (mas³/yr²). It is free of distance.

required
t_ref float

The epoch of the state (MJD, static float64).

0.0
Notes

Use to_kepler for the elements. Only bound states (negative energy) are orbits.

dra = np.asarray(dra, dtype=float) instance-attribute

ddec = np.asarray(ddec, dtype=float) instance-attribute

vra = np.asarray(vra, dtype=float) instance-attribute

vdec = np.asarray(vdec, dtype=float) instance-attribute

dz = np.asarray(dz, dtype=float) instance-attribute

vz = np.asarray(vz, dtype=float) instance-attribute

mu = np.asarray(mu, dtype=float) instance-attribute

t_ref = float(t_ref) class-attribute instance-attribute

__init__(dra, ddec, vra, vdec, dz, vz, mu, t_ref=0.0)

__check_init__()

from_kepler(orbit) classmethod

The state of orbit at its t_ref.

to_kepler()

The KeplerOrbit of this state.

The periastron and the direction of motion there give the Thiele–Innes constants directly, which fixes the angles in virgil's conventions; the line-of-sight components then pick the node, which positions alone leave ambiguous by 180°. A circular orbit (e = 0) has no periastron: the position at t_ref is used instead.

relative(mjd)

(dra, ddec, dz) (mas), as for KeplerOrbit.relative.

PositionData

Bases: Base

Measured positions of the secondary relative to the primary.

For starting orbits from per-epoch binary fits, and for published positions with no raw data. It ignores the scene: when the source is more than two point stars, positions fitted at about λ/D resolution can be biased, and the orbit should be fitted to the visibilities.

Parameters:

Name Type Description Default
mjd array - like

Time of each position (MJD).

required
dra array - like

Positions (mas), East and North.

required
ddec array - like

Positions (mas), East and North.

required
cov array - like

Covariance of (dra, ddec) at each epoch, shape (n, 2, 2) (mas²), e.g. from laplace_cov.

required
t_ref float

Reference time (MJD, static float64); by default the first epoch.

None

t_ref = float(mjd.min() if t_ref is None else t_ref) class-attribute instance-attribute

dt = np.asarray(mjd - self.t_ref) instance-attribute

dra = np.asarray(dra) instance-attribute

ddec = np.asarray(ddec) instance-attribute

whitener = np.asarray(onp.linalg.inv(onp.linalg.cholesky(cov))) instance-attribute

__init__(mjd, dra, ddec, cov, t_ref=None)

from_sep_pa(mjd, sep, pa, sep_err, pa_err, t_ref=None) classmethod

Positions given as separation (mas) and position angle (degrees, North through East), with independent errors on each.

model(orbit, north_angle=None, plate_scale=None)

The positions (dra, ddec) these data would measure (mas).

The orbit's sky positions, seen through this dataset's astrometric calibration: m R(δ) (dra, ddec) with δ = north_angle (degrees), m = plate_scale and R(δ) = [[cos δ, sin δ], [-sin δ, cos δ]]. So a companion at true PA θ and separation ρ is measured at PA θ + δ and separation m ρ: north_angle is the error added to every measured position angle, as in OIData.with_north_angle. None (the default) leaves the positions untouched.

whitened_residuals(orbit, north_angle=None, plate_scale=None)

L⁻¹ (data - model) for every epoch, flattened (2n,).

north_angle and plate_scale are this dataset's calibration terms (see model).

loglike(orbit, north_angle=None, plate_scale=None)

Gaussian log-likelihood of the positions under orbit.

term(orbit, north_angle=None, plate_scale=None)

A likelihood term for fit's likelihoods.

Parameters:

Name Type Description Default
orbit callable

Maps the fitted values (a dict, by path or keyword) to a KeplerOrbit.

required
north_angle str

Names of fitted values holding this dataset's North angle δ (degrees, added to every measured position angle) and plate scale m (a factor, 1 when calibrated), as in model. As for RVData.term's jitter, each is a key of priors (which a function model must accept and may ignore); give each dataset its own names. Their priors must be stated: there is no default width. The invariant priors (uniform on the circle for δ, log-uniform for m) leave each one degenerate with the orbit's orientation and size if a single dataset is fitted; a Gaussian, e.g. Normal(0, 0.1) for δ and Normal(1, 1e-3) for m, is strong information and should come from the instrument's astrometric calibration. Per-dataset plate-scale and North-angle terms follow Octofitter (Thompson et al. 2023, AJ 166, 164). They do not change the likelihood's normalization, so a least-squares fit keeps its form.

None
plate_scale str

Names of fitted values holding this dataset's North angle δ (degrees, added to every measured position angle) and plate scale m (a factor, 1 when calibrated), as in model. As for RVData.term's jitter, each is a key of priors (which a function model must accept and may ignore); give each dataset its own names. Their priors must be stated: there is no default width. The invariant priors (uniform on the circle for δ, log-uniform for m) leave each one degenerate with the orbit's orientation and size if a single dataset is fitted; a Gaussian, e.g. Normal(0, 0.1) for δ and Normal(1, 1e-3) for m, is strong information and should come from the instrument's astrometric calibration. Per-dataset plate-scale and North-angle terms follow Octofitter (Thompson et al. 2023, AJ 166, 164). They do not change the likelihood's normalization, so a least-squares fit keeps its form.

None

RVData

Bases: Base

Radial velocities of one star of the binary.

Radial velocities are the only data that fix the node absolutely: from positions alone (Omega + 180, omega + 180) fits equally well. They need a physical scale, the distance, to turn the orbit's angular velocities into km/s, and the mass ratio to share the motion between the stars.

Parameters:

Name Type Description Default
mjd array - like

Times (MJD).

required
rv array - like

Radial velocities and their errors (km/s, positive receding).

required
d_rv array - like

Radial velocities and their errors (km/s, positive receding).

required
star (primary, secondary)

Which star they are of.

"primary"
t_ref float

Reference time (MJD, static float64); by default the first epoch.

None
instrument array - like

One label per epoch naming the spectrograph. Only used when term marginalizes the zero points; by default there is a single instrument.

None

t_ref = float(mjd.min() if t_ref is None else t_ref) class-attribute instance-attribute

dt = np.asarray(mjd - self.t_ref) instance-attribute

rv = np.asarray(rv) instance-attribute

d_rv = np.asarray(d_rv) instance-attribute

star = star class-attribute instance-attribute

instruments = labels class-attribute instance-attribute

inst = np.asarray(codes.reshape(mjd.shape)) instance-attribute

__init__(mjd, rv, d_rv, star='primary', t_ref=None, instrument=None)

model(orbit, q, gamma, distance_pc)

Predicted radial velocities (km/s).

Parameters:

Name Type Description Default
orbit KeplerOrbit

The relative orbit (secondary about primary).

required
q float

Mass ratio, secondary / primary.

required
gamma float

Systemic velocity (km/s).

required
distance_pc float

Distance (pc), which turns mas/day into km/s.

required

errors(jitter=0.0)

Effective errors sqrt(d_rv² + jitter²) (km/s).

whitened_residuals(orbit, q, gamma, distance_pc, jitter=0.0)

(rv - model) / sqrt(d_rv² + jitter²) for every epoch.

loglike(orbit, q, gamma, distance_pc, jitter=0.0)

Gaussian log-likelihood of the velocities.

jitter (km/s) adds an extra scatter in quadrature to every error; the normalization then depends on it.

marginal_whitened_residuals(orbit, q, gamma, distance_pc, jitter=0.0, prior=None)

Whitened residuals of the zero-point-marginalized Gaussian.

The data are d ~ N(m + Aμ, C + AΛAᵀ) with m the Keplerian model (gamma included), A the indicator matrix of the instruments and w ~ N(μ, Λ) the zero points. Returns u of length N with uᵀu = rᵀ(C + AΛAᵀ)⁻¹r, r = d - m - Aμ, by the dense Woodbury whitening of virgil._linear (an exact square root, with no eigendecomposition).

prior is (mean, sd) per instrument, as term builds it.

marginal_log_norm(jitter=0.0, prior=None)

½ log det(C + AΛAᵀ).

This is the normalization that depends on the jitter, in the same convention as the plain Σ log σ_eff.

marginal_loglike(orbit, q, gamma, distance_pc, jitter=0.0, prior=None)

Normalized log density of the zero-point-marginalized Gaussian.

Equal to a dense N(m + Aμ, C + AΛAᵀ) log density, computed in O(N k²). A flat prior is the limit Λ → ∞ only up to a constant (-½ Σ log Λ); the finite prior width is part of the model.

zero_point_posterior(orbit, q, gamma, distance_pc, jitter=0.0, prior=None)

Mean and covariance of the zero points w given the orbit.

w | d, θ ~ N(S⁻¹(Λ⁻¹μ + AᵀC⁻¹(d - m)), S⁻¹) with S = Λ⁻¹ + AᵀC⁻¹A. Entry j is the velocity zero point of self.instruments[j]; m includes gamma, so with gamma = 0 they are the systemic velocity seen by each instrument, and differences between them are the offsets.

term(params, jitter=None, marginalize_offsets=None)

A likelihood term for fit's likelihoods.

Parameters:

Name Type Description Default
params callable

Maps the fitted values to (orbit, q, gamma, distance_pc).

required
jitter str

Path of a fitted value holding an RV jitter s (km/s), which inflates the errors to sqrt(d_rv² + s²). As with fitted noise= terms, the likelihood's normalization Σ log σ_eff then depends on a parameter, so the term reports it (log_norm) and fit adds it to the loss, defaulting to L-BFGS (no least-squares form). Give jitter a prior with positive support. The jitter is a scale, so the default is dist.LogUniform(lo, hi) (the Jeffreys prior; pick lo well below the smallest plausible scatter and hi above the largest). dist.HalfNormal is a deliberate informative choice (scale about the expected scatter, a few km/s for a spotted star). The likelihood depends on s² only.

None
marginalize_offsets (mean, sd)

Analytically marginalize one velocity zero point per instrument (Luger, Foreman-Mackey & Hogg 2017, arXiv:1710.11136). The model is m_kepler + A w with A the indicator matrix of instrument= and w_j ~ N(mean_j, sd_j²). One zero point per instrument, with no separate γ, is the parameterization without a degeneracy: w_j is the systemic velocity as measured by instrument j, and offsets are differences w_j - w_0. Return gamma = 0 from params (a nonzero value just shifts the prior mean). mean and sd are scalars or one per instrument (in RVData.instruments order, km/s); True is an error, because the prior must be stated. The density is the dense N(m + Aμ, C + AΛAᵀ) log density with C = diag(σ_eff²), evaluated by the Woodbury identity and the matrix-determinant lemma in O(N k²), so the jitter enters the full log-determinant. For a broad prior this is the profile likelihood plus a log-determinant correction; a flat prior is the Λ → ∞ limit up to a Λ-dependent constant, and only finite sd is supported. The term's residuals have length N and its log_norm carries the marginal log-determinant. term.posterior(values) returns the zero points' conditional mean and covariance, to report after a fit.

None

AxialVonMises

Bases: Distribution

A prior on an angle (degrees) known only modulo 180°.

For a node position angle from a source whose convention is in doubt: the density is a von Mises in 2θ, so θ and θ + 180° are equally likely, exp(kappa cos 2(θ - mean)), normalized over [0, 360).

Parameters:

Name Type Description Default
mean float

Mean angle (degrees); mean + 180 is equivalent.

required
kappa float

Concentration (of the doubled angle); larger is tighter, with a width of about 28.6 / sqrt(kappa) degrees.

required

arg_constraints = {'mean_deg': constraints.real, 'kappa': constraints.positive} class-attribute instance-attribute

support = constraints.interval(0.0, 360.0) class-attribute instance-attribute

mean_deg = np.asarray(mean, dtype=float) instance-attribute

kappa = np.asarray(kappa, dtype=float) instance-attribute

__init__(mean, kappa, *, validate_args=None)

log_prob(value)

sample(key, sample_shape=())

period_grid(times, p_min, p_max, k=_PHASE_COHERENCE)

Trial periods that stay in phase across the observations.

A period that is wrong by δP drifts by T δP / P² cycles over the time baseline T, so trial periods must be spaced by no more than δP = P²/(kT) for the best of them to stay within a fraction of a cycle of the truth at every epoch. That is a uniform grid in frequency 1/P, with a step of 1/(kT): many more periods at short periods than a log-spaced grid, which misses short-period orbits on a long baseline.

Parameters:

Name Type Description Default
times array - like

The times (days, e.g. MJD) of the epochs that seed the orbits; their range is the baseline T.

required
p_min float

The shortest and longest trial periods (days), e.g. from the period prior.

required
p_max float

The shortest and longest trial periods (days), e.g. from the period prior.

required
k float

Neighbouring trial periods drift apart by at most 1/k of a cycle over the baseline (default 9; the true period then drifts by at most 1/36 of a cycle from the nearest once its phase is centred, a step of starting_orbits' default phase grid). The default is finer than ARMADA's grid, P = 2fT/n with f = 3 (k = 6), and The Joker's resolution, δP = 4P²/(2πT) (k = π/2).

_PHASE_COHERENCE

Returns:

Type Description
ndarray

The periods (days), increasing from p_min to p_max, for starting_orbits.

starting_orbits(positions, periods, eccs=None, n_phase=36, n_best=5)

Good starting orbits for a set of positions, from a grid search.

At fixed period, eccentricity and time of periastron the positions are linear in the Thiele–Innes constants, so each grid point is an exact weighted least-squares solve. This is the classical way to start an orbit fit: it needs no random restarts and handles the several minima of a short arc. Refine the best orbits with a fit to the visibilities or to the positions.

Parameters:

Name Type Description Default
positions PositionData

The measured positions.

required
periods array - like

Trial periods (days), dense enough to stay in phase across the positions' time baseline: see period_grid.

required
eccs array - like

Trial eccentricities; by default 0 to 0.9 in steps of 0.05.

None
n_phase int

Number of trial times of periastron, spread over each period.

36
n_best int

Number of orbits to return.

5

Returns:

Type Description
list of (KeplerOrbit, float)

The best orbits and their χ², best first. Each has 0 <= Omega < 180; (Omega + 180, omega + 180) fits equally well.

total_mass(orbit, distance_pc)

Total mass (solar masses) from the orbit at a distance (pc).

Kepler's third law, M = 4π² a³ / (G P²), with a in au (a_mas · D / 1000) and P in days, and the IAU 2015 nominal G M☉ = 1.3271244e20 m³ s⁻², au = 149597870700 m and day = 86400 s. (The shortcut M = a³ / P² with P in Julian years holds only for the Gaussian year, 365.256898 d, and is 4e-5 off in M.) Report it as a function of distance, or with the distance's uncertainty: positions alone do not fix it.

distance_pc(orbit, total_mass)

The distance (pc) at which orbit has this total mass (M☉): the dynamical parallax, the inverse of total_mass (same constants).

orientation_priors(positions_only=True, *, prefix='', ring_width=0.25, inclination=False)

Angle-vector priors for an orbit's node and periastron.

Vectors remove the wrap at 0°/360° (see AngleVector); these choose which angles to sample, so that exact symmetries become single points rather than separate modes:

  • positions_only=True: "two_Omega" (2Ω, i.e. Ω modulo 180°) and "varpi" (ϖ = Ω + ω, the longitude of periastron). Positions alone cannot tell (Ω, ω) from (Ω + 180°, ω + 180°); both have the same 2Ω and ϖ, so the two modes collapse to one.
  • positions_only=False, when RVs or other data fix the node: "Omega" and "varpi".

Near face-on, positions fix ϖ but not Ω and ω separately; ϖ is then the well-measured angle and Ω the broad one. Build the orbit with KeplerOrbit.from_varpi.

The angles are uniform, which with a prior uniform in cos i is the invariant (Haar) prior on the orbit's orientation: (Ω, ω) → (2Ω, ϖ) is linear with a constant Jacobian, so uniform (Ω, ω) is uniform (2Ω, ϖ).

With inclination=True the inclination prior IsotropicInclination (density ∝ sin i on 0° to 180°) is added under "inc", so one call gives the full Haar (isotropic) prior on the orientation. It is off by default so that existing code, which fixes or places its own prior on inc, keeps getting exactly the keys it did.

Parameters:

Name Type Description Default
positions_only bool

Whether only positions constrain the orbit (default True).

True
prefix str

Prepended to the keys, e.g. "orbit.".

''
ring_width float

Passed to AngleVector.

0.25
inclination bool

Also return IsotropicInclination() under prefix + "inc" (default False).

False

Returns:

Type Description
dict

Priors keyed prefix + "two_Omega" (or "Omega"), prefix + "varpi" and, if inclination, prefix + "inc".

Examples:

>>> priors = {**orientation_priors(), "ecc": dist.Uniform(0.0, 0.9)}
>>> def orbit_fn(v):
...     return KeplerOrbit.from_varpi(
...         400.0, 30.0, v["ecc"], 60.0, v["varpi"], 20.0,
...         two_Omega=v["two_Omega"], t_ref=60500.0,
...     )

orientation_from_varpi(varpi, *, Omega=None, two_Omega=None)

(omega, Omega) (degrees) from ϖ = Ω + ω and the node.

Give exactly one of Omega and two_Omega. From two_Omega, Ω is reported in [0°, 180°), as by starting_orbits; ω is in [0°, 360°).

position_angle_prior(orbit_fn)

The prior term that keeps a θ-sampled orbit uniform in t_peri.

Pass it in likelihoods= to fit or numpyro_model, beside the data terms, when orbit_fn(values) builds its orbit with KeplerOrbit.from_position_angle. It adds position_angle_log_jacobian at the orbit's position angle at t_ref (its θ) to the log posterior. It has no least-squares form, so fit then defaults to L-BFGS; its χ² in info is 0 over 0 points.

Examples:

>>> priors = {"theta": AngleVector(), "ecc": dist.Uniform(0.0, 0.9)}
>>> def orbit_fn(v):
...     return KeplerOrbit.from_position_angle(
...         400.0, v["theta"], v["ecc"], 60.0, 40.0, 110.0, 20.0,
...         t_ref=60500.0,
...     )
>>> terms = [positions.term(orbit_fn), position_angle_prior(orbit_fn)]

position_angle_log_jacobian(theta, ecc, inc, omega, Omega)

log|∂M/∂θ| at fixed (e, i, ω, Ω), for a prior uniform in t_peri.

The invariant prior on the epoch is uniform in the time of periastron, i.e. in the mean anomaly M at t_ref (a translation), not in the position angle θ there. Sampling θ uniformly (an AngleVector) and adding this term gives back the uniform prior in M:

log|∂M/∂θ| = 3/2 log(1 - e²) - 2 log(1 + e cos f) + log|cos i|
             - log(cos²φ cos²i + sin²φ),  φ = θ - Ω,

from dM/df = (1 - e²)^{3/2}/(1 + e cos f)² and du/dφ = cos i / (cos²φ cos²i + sin²φ). Over a full turn of θ it integrates to 2π, so the prior stays normalized. It diverges at i = 90° (see KeplerOrbit.from_position_angle). All angles in degrees.