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; v3: Blunt et al. 2024, JOSS 9, 6756; the OFTI
sampler: Blunt et al. 2017),
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 position of the secondary relative to the primary is (dra, ddec, dz):
East and North in mas, and dz positive away from the observer. inc,
Omega and omega are the visual-binary angles (degrees; omega is
the secondary's, the spectroscopic one is omega - 180), period is in
days, a_mas the angular semimajor axis, and dt_peri the time of
periastron minus the static t_ref (days). See
conventions for the full definitions and
degeneracies, and orbit methods for the solvers and
references. Kepler's equation is solved by jaxoplanet, an optional
dependency (pip install "virgil-astro[orbits]").
KeplerOrbit
Bases: Base
A Keplerian orbit of the secondary relative to the primary.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
period
|
float
|
Orbital period (days) and time of periastron minus |
required |
dt_peri
|
float
|
Orbital period (days) and time of periastron minus |
required |
ecc
|
float
|
Eccentricity, |
required |
inc
|
float
|
Inclination ( |
required |
omega
|
float
|
Inclination ( |
required |
Omega
|
float
|
Inclination ( |
required |
a_mas
|
float
|
Angular semimajor axis (mas). |
required |
t_ref
|
float
|
Reference time (MJD, float64, static). |
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: astrometry measures the position
angle at an epoch directly
(derivation). Sample theta as an
AngleVector and add
position_angle_prior to the likelihood
terms so the prior stays uniform in the time of periastron.
Singular at i = 90° (a concrete inc of 90° is rejected) and badly
conditioned near edge-on: keep dt_peri or use
StateVectorOrbit.
Source: Thompson et al. (2023), references.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
theta
|
float
|
Position angle of the secondary at |
required |
period
|
As for |
required | |
ecc
|
As for |
required | |
inc
|
As for |
required | |
omega
|
As for |
required | |
Omega
|
As for |
required | |
a_mas
|
As for |
required | |
t_ref
|
As for |
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 |
required |
Returns:
| Type | Description |
|---|---|
tuple of arrays
|
|
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_primaryisline_pa + 180, wrapped to [0, 360).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 whoseincis 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: |
required |
dt_peri
|
float
|
As in :class: |
required |
ecc
|
float
|
As in :class: |
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 Keplerian elements are poorly determined.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dra
|
float
|
Position of the secondary at |
required |
ddec
|
float
|
Position of the secondary at |
required |
vra
|
float
|
Its velocity at |
required |
vdec
|
float
|
Its velocity at |
required |
dz
|
float
|
Line-of-sight position (mas) and velocity (mas/yr), positive away
from the observer; they and |
required |
vz
|
float
|
Line-of-sight position (mas) and velocity (mas/yr), positive away
from the observer; they and |
required |
mu
|
float
|
Gravitational parameter |
required |
t_ref
|
float
|
The epoch of the state (MJD, static float64). |
0.0
|
Notes
Only bound states (negative energy) are orbits; see
to_kepler.
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 |
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.
Source: Thompson et al. (2023), references.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
orbit
|
callable
|
Maps the fitted values (a dict, by path or keyword) to a
|
required |
north_angle
|
str
|
Keys of |
None
|
plate_scale
|
str
|
Keys of |
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
|
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.
See radial-velocity terms for the jitter and marginalized zero-point likelihoods.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
params
|
callable
|
Maps the fitted values to |
required |
jitter
|
str
|
Path of a fitted value holding an RV jitter |
None
|
marginalize_offsets
|
(mean, sd)
|
Analytically marginalize one velocity zero point per instrument,
with prior |
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); |
required |
kappa
|
float
|
Concentration (of the doubled angle); larger is tighter, with a
width of about |
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, uniform in 1/P, that stay in phase over the observations.
See period grids.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
times
|
array - like
|
Epoch times (days); their range is the baseline T. |
required |
p_min
|
float
|
The shortest and longest trial periods (days). |
required |
p_max
|
float
|
The shortest and longest trial periods (days). |
required |
k
|
float
|
Neighbouring periods drift apart by at most |
_PHASE_COHERENCE
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Periods (days), increasing from |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
starting_orbits(positions, periods, eccs=None, n_phase=36, n_best=5, sign_free=False, batch_size=None)
Good starting orbits for a set of positions, from a grid search.
Each grid point is an exact weighted least-squares solve; see starting orbits.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
positions
|
PositionData
|
The measured positions. |
required |
periods
|
array - like
|
Trial periods (days); see |
required |
eccs
|
array - like
|
Trial eccentricities (default 0 to 0.9 in steps of 0.05). |
None
|
n_phase
|
int
|
Trial times of periastron per period; orbits returned. |
36
|
n_best
|
int
|
Trial times of periastron per period; orbits returned. |
36
|
sign_free
|
bool
|
Positions known only up to a sign (nearly equal fluxes); the orbit
returned has the global sign of its best fit, and its reflection
( |
False
|
batch_size
|
int
|
Grid points solved together; bounds memory, not the result. Capped at the grid size (rounded up to a power of two) even when given. |
None
|
Returns:
| Type | Description |
|---|---|
list of (KeplerOrbit, float)
|
The best orbits and χ², best first, each with |
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.
With positions_only the keys are "two_Omega" (2Ω) and "varpi"
(ϖ = Ω + ω), merging the twin modes of a positions-only fit; otherwise
"Omega" and "varpi". Build the orbit with
KeplerOrbit.from_varpi; see
orientation priors.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
positions_only
|
bool
|
Whether only positions constrain the orbit (default |
True
|
prefix
|
str
|
Prepended to the keys, e.g. |
''
|
ring_width
|
float
|
Passed to |
0.25
|
inclination
|
bool
|
Also return |
False
|
Returns:
| Type | Description |
|---|---|
dict
|
|
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.