Fitting Gaia DR4 IAD

This tutorial shows how you can use Octofitter to fit preliminary RV and absolute astrometry data from DR4, using the already published data for the Gaia-BH3 black hole.

The final format of the Gaia IAD data may change, in which case this tutorial will be updated.

using CairoMakie
using Octofitter
using Distributions
using CSV, DataFrames

As a first step, we will load the astrometry data for Gaia-BH3 and plot it:

headers = [
    :transit_id
    :ccd_id
    :obs_time_tcb
    :centroid_pos_al
    :centroid_pos_error_al
    :parallax_factor_al
    :scan_pos_angle
    :outlier_flag
]
df = CSV.read(joinpath(@__DIR__, "astrom.dat"), DataFrame, skipto=7, header=headers, delim=' ', ignorerepeated=true)
df.epoch = jd2mjd.(df.obs_time_tcb)

scatter(
    df.obs_time_tcb,
    df.centroid_pos_al,
)
Example block output

We can now construct a likelihood object for this data. We must also supply the Gaia ID, which will be used to query the full Gaia solution for this object (for now, using DR3):

gaiaIADlike = GaiaDR4Astrom(
    df,
    gaia_id=4318465066420528000,
    variables=@variables begin
        astrometric_jitter = 0.0 # [mas]. Could use e.g. `~ LogUniform(0.00001, 10)
    end
)
GaiaDR4Astrom Table with 9 columns and 622 rows:
      transit_id         ccd_id  obs_time_tcb  centroid_pos_al  ⋯
    ┌────────────────────────────────────────────────────────────
 1  │ 20114916805338633  1       2.45696e6     147.066          ⋯
 2  │ 20114916805338633  2       2.45696e6     146.696          ⋯
 3  │ 20114916805338633  3       2.45696e6     146.685          ⋯
 4  │ 20114916805338633  4       2.45696e6     146.557          ⋯
 5  │ 20114916805338633  5       2.45696e6     146.396          ⋯
 6  │ 20114916805338633  6       2.45696e6     146.374          ⋯
 7  │ 20114916805338633  7       2.45696e6     146.436          ⋯
 8  │ 20114916805338633  8       2.45696e6     146.06           ⋯
 9  │ 20114916805338633  9       2.45696e6     146.256          ⋯
 10 │ 20119009095238725  1       2.45696e6     146.244          ⋯
 11 │ 20119009095238725  2       2.45696e6     146.148          ⋯
 12 │ 20119009095238725  3       2.45696e6     146.192          ⋯
 13 │ 20119009095238725  4       2.45696e6     146.271          ⋯
 14 │ 20119009095238725  5       2.45696e6     146.047          ⋯
 15 │ 20119009095238725  6       2.45696e6     146.068          ⋯
 16 │ 20119009095238725  7       2.45696e6     146.023          ⋯
 17 │ 20119009095238725  8       2.45696e6     146.095          ⋯
 ⋮  │         ⋮            ⋮          ⋮               ⋮         ⋱

This object also has published RV data from Gaia, which we can load and use as normal:

using CSV, DataFrames
using OctofitterRadialVelocity

headers_rv = [
    :transit_id,
    :obs_time_tcb,
    :radial_velocity_kms,
    :radial_velocity_err_kms,
]
dfrv = CSV.read(joinpath(@__DIR__, "epochrv.dat"), DataFrame, skipto=7, header=headers_rv, delim=' ', ignorerepeated=true)
dfrv.epoch = jd2mjd.(dfrv.obs_time_tcb)
dfrv.rv = dfrv.radial_velocity_kms * 1e3
dfrv.σ_rv = dfrv.radial_velocity_err_kms * 1e3

# Calculate mean RV for the prior
mean_rv = mean(dfrv.rv)

rvlike = StarAbsoluteRVLikelihood(
    dfrv,
    name="GaiaRV",
    variables=@variables begin
        offset ~ Normal(mean_rv, 10_000)  # wide prior on RV offset centred on mean RV
        jitter ~ LogUniform(0.01, 100_000)  # RV jitter parameter
    end
)
errorbars(
    dfrv.obs_time_tcb,
    dfrv.rv,
    dfrv.σ_rv
)
Example block output

Now, we define a model that incorporates this data:

mjup2msol = Octofitter.mjup2msol
ref_epoch_mjd = Octofitter.meta_gaia_DR3.ref_epoch_mjd
orbit_ref_epoch = mean(gaiaIADlike.table.epoch)
DR4_REFERENCE_EPOCH = 2457936.875 # J2017.5

b = Planet(
    name="b",
    basis=Visual{KepOrbit},
    likelihoods=[],
    variables=@variables begin
        a ~ Uniform(0, 1000)
        e ~ Uniform(0, 0.99)
        ω ~ UniformCircular()
        i ~ Sine()
        Ω ~ UniformCircular()
        θ ~ UniformCircular()
        tp = θ_at_epoch_to_tperi(θ, $orbit_ref_epoch; M=system.M, e, a, i, ω, Ω)
        mass = system.M_sec / mjup2msol
    end
)
gaia_ra = 294.82786250815144
gaia_dec = 14.930979608612361

sys = System(
    name="gaiadr4test",
    companions=[b],
    likelihoods=[gaiaIADlike, rvlike],
    variables=@variables begin
        M_pri = 0.76
        M_sec ~ LogUniform(1, 1000) # Msol
        M = M_pri + M_sec
        plx ~ Uniform(0.01, 100)
        pmra ~ Uniform(-1000, 1000)
        pmdec ~ Uniform(-1000, 1000)

        # Put a prior of the catalog value +- 10,000 mas on position
        # We could just fit the deltas directly, but we can propagate
        # handling all non-linear effects if we know the actual ra,
        # dec, and rv.
        ra_offset_mas ~ Normal(0, 100)
        dec_offset_mas ~ Normal(0, 100)
        dec = $gaia_dec + ra_offset_mas / 60 / 60 / 1000
        ra = $gaia_ra + dec_offset_mas / 60 / 60 / 1000 / cosd(dec)
        ref_epoch = $DR4_REFERENCE_EPOCH
    end
)
model = Octofitter.LogDensityModel(sys, verbosity=4)
LogDensityModel for System gaiadr4test of dimension 17 and 642 epochs with fields .ℓπcallback and .∇ℓπcallback

We will initialize the model starting positions and visualize them:

init_chain = initialize!(model)
octoplot(model, init_chain, show_rv=true)
Example block output

Now, we can perform the fit. It is a little slow since we have many hundreds of RV and astrometry data points.

using Pigeons
chain, pt = octofit_pigeons(model, n_rounds=10) # might need more rounds to converge
(chain = MCMC chain (1024×32×1 Array{Float64, 3}), pt = PT(checkpoint = false, ...))

Finally, we can visualize the results:

octoplot(model, chain, show_rv=true, mark_epochs_mjd=mjd.([
    "2017"
    "2022"
    "2027"
]))
Example block output
octocorner(model, chain, small=true)
Example block output
Octofitter.rvpostplot(model, chain)
Example block output