Fit Proper Motion Anomaly

Octofitter.jl supports fitting orbit models to astrometric motion in the form of GAIA-Hipparcos proper motion anomaly (HGCA; https://arxiv.org/abs/2105.11662). These data points are calculated by finding the difference between a long term proper motion of a star between the Hipparcos and GAIA catalogs, and their proper motion calculated within the windows of each catalog. This gives four data points that can constrain the dynamical mass & orbits of planetary companions (assuming we subtract out the net trend).

If your star of interest is in the HGCA, all you need is it's GAIA DR3 ID number. You can find this number by searching for your target on SIMBAD.

For this tutorial, we will examine the star and companion HD 91312 A & B discovered by SCExAO. We will use their published astrometry and proper motion anomaly extracted from the HGCA.

The first step is to find the GAIA source ID for your object. For HD 91312, SIMBAD tells us the GAIA DR3 ID is 756291174721509376.

Fitting Astrometric Motion Only

Initial setup:

using Octofitter, Distributions, Random

We begin by finding orbits that are consistent with the astrometric motion. Later, we will add in relative astrometry to the fit from direct imaging to further constrain the planet's orbit and mass.

Compared to previous tutorials, we will now have to add a few additional variables to our model. The first is a prior on the mass of the companion, called mass. The units used on this variable are Jupiter masses, in contrast to M, the primary's mass, in solar masses. A reasonable uninformative prior for mass is Uniform(0,1000) or LogUniform(1,1000) depending on the situation.

For this model, we also want to place a prior on the host star mass rather than system total mass. For exoplanets there is litte difference between these two values, but in this example we have a reasonably informative prior on the host mass, and know from the paper that the companion is has a non-neglible effect on the total system mass.

To make this parameterization change, we specify priors on both masses in the @system block, and connect it to the planet.

Planet Model

@planet B Visual{KepOrbit} begin
    a ~ LogUniform(0.1,20)
    e ~ Uniform(0,0.999)
    ω ~ UniformCircular()
    i ~ Sine() # The Sine() distribution is defined by Octofitter
    Ω ~ UniformCircular()

    mass = system.M_sec

    θ ~ UniformCircular()
    tp = θ_at_epoch_to_tperi(system,B,57423.0) # epoch of GAIA measurement
end
Planet B
Derived:
  ω, Ω, mass, θ, tp, 
Priors:
  a	Distributions.LogUniform{Float64}(a=0.1, b=20.0)
  e	Distributions.Uniform{Float64}(a=0.0, b=0.999)
  ωy	Distributions.Normal{Float64}(μ=0.0, σ=1.0)
  ωx	Distributions.Normal{Float64}(μ=0.0, σ=1.0)
  i	Sine()
  Ωy	Distributions.Normal{Float64}(μ=0.0, σ=1.0)
  Ωx	Distributions.Normal{Float64}(μ=0.0, σ=1.0)
  θy	Distributions.Normal{Float64}(μ=0.0, σ=1.0)
  θx	Distributions.Normal{Float64}(μ=0.0, σ=1.0)
Octofitter.UnitLengthPrior{:ωy, :ωx}: √(ωy^2+ωx^2) ~ LogNormal(log(1), 0.02)
Octofitter.UnitLengthPrior{:Ωy, :Ωx}: √(Ωy^2+Ωx^2) ~ LogNormal(log(1), 0.02)
Octofitter.UnitLengthPrior{:θy, :θx}: √(θy^2+θx^2) ~ LogNormal(log(1), 0.02)


System Model & Specifying Proper Motion Anomaly

Now that we have our planet model, we create a system model to contain it.

We specify priors on plx as usual, but here we use the gaia_plx helper function to read the parallax and uncertainty directly from the HGCA catalog using its source ID.

We also add parameters for the star's long term proper motion. This is usually close to the long term trend between the Hipparcos and GAIA measurements. If you're not sure what to use here, try Normal(0, 1000); that is, assume a long-term proper motion of 0 +- 1000 milliarcseconds / year.

@system HD91312 begin

    M_pri ~ truncated(Normal(1.61, 0.1), lower=0) # Msol
    M_sec ~ LogUniform(0.5, 1000) # MJup
    M = system.M_pri + system.M_sec*Octofitter.mjup2msol # Msol

    plx ~ gaia_plx(gaia_id=756291174721509376)

    # Priors on the center of mass proper motion
    pmra ~ Normal(-137, 10)
    pmdec ~ Normal(2,  10)
end HGCALikelihood(gaia_id=756291174721509376) B

model = Octofitter.LogDensityModel(HD91312)
LogDensityModel for System HD91312 of dimension 14 with fields .ℓπcallback and .∇ℓπcallback

After the priors, we add the proper motion anomaly measurements from the HGCA. If this is your first time running this code, you will be prompted to automatically download and cache the catalog which may take around 30 seconds.

Sampling from the posterior

Because proper motion anomaly data is quite sparse, it can often produce multi-modal posteriors. If your orbit already has several relative astrometry or RV data points, this is less of an issue. But in many cases it is recommended to use the Pigeons.jl sampler instead of Octofitter's default. This sampler is less efficient for unimodal distributions, but is more robust at exploring posteriors with distinct, widely separated peaks.

To install and use Pigeons.jl with Octofitter, type using Pigeons at in the terminal and accept the prompt to install the package. You may have to restart Julia.

Note

octofit_pigeons scales very well across multiple cores. Start julia with julia --threads=auto to make sure you have multiple threads available for sampling.

We now sample from our model using Pigeons:

using Pigeons
model = Octofitter.LogDensityModel(HD91312)
chain, pt = octofit_pigeons(model, n_rounds=10)
display(chain)
┌ Info: initialized chain
│   chain_no = 1
│   initial_logpost = -260.3386945842529
└   t = 0.827021895
┌ Info: initialized chain
│   chain_no = 2
│   initial_logpost = -251.0677651400995
└   t = 0.573317221
┌ Info: initialized chain
│   chain_no = 3
│   initial_logpost = -232.5993658182453
└   t = 0.629230751
┌ Info: initialized chain
│   chain_no = 4
│   initial_logpost = -236.03308636875187
└   t = 0.622582609
┌ Info: initialized chain
│   chain_no = 5
│   initial_logpost = -224.80261965371648
└   t = 0.635092337
┌ Info: initialized chain
│   chain_no = 6
│   initial_logpost = -156.0394777869163
└   t = 0.57933638
┌ Info: initialized chain
│   chain_no = 7
│   initial_logpost = -266.333678207752
└   t = 0.618272779
┌ Info: initialized chain
│   chain_no = 8
│   initial_logpost = -171.55481858639212
└   t = 0.626321614
─────────────────────────────────────────────────────────────────────────────────────────────────────────────
  scans     restarts      Λ        time(s)    allc(B)  log(Z₁/Z₀)   min(α)     mean(α)    min(αₑ)   mean(αₑ)
────────── ────────── ────────── ────────── ────────── ────────── ────────── ────────── ────────── ──────────
        2          0          4       6.69   3.28e+08  -2.14e+04          0      0.429      0.934      0.956
        4          0       1.95      0.338   6.37e+07   -3.6e+03          0      0.721      0.921      0.939
        8          0       4.93      0.566   1.38e+08      -26.6   4.82e-11      0.296      0.901      0.933
       16          0       5.72      0.938   2.42e+08      -53.1   6.86e-31      0.182      0.908      0.924
       32          0       5.71       2.53    6.3e+08      -31.3    3.9e-11      0.184      0.903      0.923
       64          0       6.08       3.59   9.11e+08      -24.6   2.33e-07      0.132      0.907      0.924
      128          0        6.2        9.2   2.31e+09      -20.7   0.000601      0.115      0.914      0.925
      256          0       6.21       18.1   4.56e+09      -20.3      0.015      0.113      0.905      0.919
      512          1       6.27       32.2   8.11e+09        -20     0.0195      0.104      0.908       0.92
 1.02e+03          5       6.22       66.3   1.67e+10      -20.4     0.0293      0.111      0.905      0.919
─────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note that octofit_pigeons took somewhat longer to run than octofit typically does; however, as we will see, it sampled successfully from severally completely disconnected modes in the posterior. That makes it a good fit for sampling from proper motion anomaly and relative astrometry with limited orbital coverage.

Analysis

The first step is to look at the table output above generated by MCMCChains.jl. The rhat column gives a convergence measure. Each parameter should have an rhat very close to 1.000. If not, you may need to run the model for more iterations or tweak the parameterization of the model to improve sampling. The ess column gives an estimate of the effective sample size. The mean and std columns give the mean and standard deviation of each parameter.

The second table summarizes the 2.5, 25, 50, 75, and 97.5 percentiles of each parameter in the model.

Pair Plot

If we wish to examine the covariance between parameters in more detail, we can construct a pair-plot (aka. corner plot).

# Create a corner plot / pair plot.
# We can access any property from the chain specified in Variables
using CairoMakie: Makie
using PairPlots
octocorner(model, chain, small=true)
Example block output

Notice how there are completely separated peaks? The default Octofitter sample (Hamiltonian Monte Carlo) is capabale of jumping 2-3σ gaps between modes, but such widely separated peaks can cause issues (hence why we used Pigeons in this example).

Posterior Mass vs. Semi-Major Axis

Given that this posterior is quite unconstrained, it is useful to make a simplified plot marginalizing over all orbital parameters besides semi-major axis. We can do this using PairPlots.jl:

using CairoMakie, PairPlots
pairplot(
    (; a=chain["B_a"][:], mass=chain["B_mass"][:]) =>
        (
            PairPlots.Scatter(color=:red),
            PairPlots.MarginHist(),
            PairPlots.MarginConfidenceLimits()
        ),
    labels=Dict(:mass=>"mass [Mⱼᵤₚ]", :a=>"sma. [au]"),
    axis = (;
        a = (;
            scale=Makie.pseudolog10,
            ticks=2 .^ (0:1:6)
        )
    )
)
Example block output

Adding Relative Astrometry

The first orbit fit to only Hipparcos/GAIA data was very unconstrained. We will now add six epochs of relative astrometry (measured from direct images) gathered from the discovery paper.

astrom_like = PlanetRelAstromLikelihood(
    (epoch=mjd("2016-12-15"), ra=133., dec=-174., σ_ra=07.0, σ_dec=07., cor=0.2),
    (epoch=mjd("2017-03-12"), ra=126., dec=-176., σ_ra=04.0, σ_dec=04., cor=0.3),
    (epoch=mjd("2017-03-13"), ra=127., dec=-172., σ_ra=04.0, σ_dec=04., cor=0.1),
    (epoch=mjd("2018-02-08"), ra=083., dec=-133., σ_ra=10.0, σ_dec=10., cor=0.4),
    (epoch=mjd("2018-11-28"), ra=058., dec=-122., σ_ra=10.0, σ_dec=20., cor=0.3),
    (epoch=mjd("2018-12-15"), ra=056., dec=-104., σ_ra=08.0, σ_dec=08., cor=0.2),
)
scatter(astrom_like.table.ra, astrom_like.table.dec)
Example block output

We use the same model as before, but now condition the planet model B on the astrometry data by adding astrom_like to the end of the @planet defintion.

@planet B Visual{KepOrbit} begin
    a ~ LogUniform(0.1,400)
    e ~ Uniform(0,0.999)
    ω ~ UniformCircular()
    i ~ Sine()
    Ω ~ UniformCircular()

    mass = system.M_sec

    θ ~ UniformCircular()
    tp = θ_at_epoch_to_tperi(system,B,57737.0) # epoch of astrometry
end astrom_like # Note the relative astrometry added here!

@system HD91312 begin

    M_pri ~ truncated(Normal(1.61, 0.1), lower=0)
    M_sec ~ LogUniform(0.5, 1000) # MJup
    M = system.M_pri + system.M_sec*Octofitter.mjup2msol

    plx ~ gaia_plx(gaia_id=756291174721509376)

    # Priors on the centre of mass proper motion
    pmra ~ Normal(-137, 10)
    pmdec ~ Normal(2,  10)
end HGCALikelihood(gaia_id=756291174721509376) B

model = Octofitter.LogDensityModel(HD91312)
LogDensityModel for System HD91312 of dimension 14 with fields .ℓπcallback and .∇ℓπcallback

Sample as before:

using Pigeons
model = Octofitter.LogDensityModel(HD91312)
Random.seed!(1)
chain, pt = octofit_pigeons(model, n_rounds=10)
display(chain)
┌ Info: initialized chain
│   chain_no = 1
│   initial_logpost = -1985.3155845133124
└   t = 0.842867366
┌ Info: initialized chain
│   chain_no = 2
│   initial_logpost = -1787.1135059081491
└   t = 0.742612808
┌ Info: initialized chain
│   chain_no = 3
│   initial_logpost = -734.7299534972204
└   t = 0.765393889
┌ Info: initialized chain
│   chain_no = 4
│   initial_logpost = -1082.0109774465461
└   t = 0.681099654
┌ Info: initialized chain
│   chain_no = 5
│   initial_logpost = -1730.994620464712
└   t = 0.727134914
┌ Info: initialized chain
│   chain_no = 6
│   initial_logpost = -1204.4976442209231
└   t = 0.740317872
┌ Info: initialized chain
│   chain_no = 7
│   initial_logpost = -1123.124329209282
└   t = 0.705504442
┌ Info: initialized chain
│   chain_no = 8
│   initial_logpost = -2397.128926317684
└   t = 0.72536417
─────────────────────────────────────────────────────────────────────────────────────────────────────────────
  scans     restarts      Λ        time(s)    allc(B)  log(Z₁/Z₀)   min(α)     mean(α)    min(αₑ)   mean(αₑ)
────────── ────────── ────────── ────────── ────────── ────────── ────────── ────────── ────────── ──────────
        2          0       4.45       6.84   3.22e+08  -2.39e+04          0      0.365      0.921      0.961
        4          0       2.22      0.377   5.79e+07  -2.61e+03          0      0.683      0.937       0.95
        8          0       6.06      0.588   1.14e+08       -225   8.76e-61      0.134      0.924      0.939
       16          0        5.5       1.36   2.63e+08       -469  2.92e-280      0.214      0.908      0.927
       32          0        5.3       2.65   5.23e+08       -589          0      0.243      0.933      0.936
       64          0       6.42       4.21   8.37e+08       -260  7.22e-131     0.0823       0.92      0.932
      128          0       6.27       8.27   1.64e+09       -133   7.83e-45      0.104      0.926      0.934
      256          0        6.1       16.9   3.34e+09      -75.8   9.99e-09      0.128      0.926      0.934
      512          0       6.26       34.4   6.79e+09      -75.1   5.58e-08      0.106      0.918      0.931
 1.02e+03          0       6.38       71.8   1.41e+10      -71.8   2.23e-05     0.0886      0.925      0.934
─────────────────────────────────────────────────────────────────────────────────────────────────────────────

The mass vs. semi-major axis posterior is now much more constrained:

using CairoMakie, PairPlots
pairplot(
    (; a=chain["B_a"][:], mass=chain["B_mass"][:]) =>
        (
            PairPlots.Scatter(color=:red),
            PairPlots.MarginHist(),
            PairPlots.MarginConfidenceLimits()
        ),
    labels=Dict(:mass=>"mass [Mⱼᵤₚ]", :a=>"sma. [au]"),
)
Example block output

It is now useful to display the orbits projected onto the plane of the sky using octoplot. This function produces a nine-panel figure showing posterior predictive distributions for velocity (in three dimensions), projected positions vs. time in the plane of the sky, and various other two and three-dimensional views.

octoplot(model, chain)
Example block output

Finally we display the posterior as a corner plot:

# Create a corner plot / pair plot.
# We can access any property from the chain specified in Variables
using CairoMakie: Makie
using PairPlots
octocorner(model, chain, small=true)
Example block output