Fitting Interferometric Observables
In this tutorial, we fit a planet & orbit model to a sequence of interferometric observations. Closure phases and squared visibilities are supported.
We load the observations in OI-FITS format and model them as a point source orbiting a star.
Interferometer modelling is supported in Octofitter via the extension package OctofitterInterferometry. To install it, run pkg> add http://github.com/sefffal/Octofitter.jl:OctofitterInterferometry
using Octofitter
using OctofitterInterferometry
using Distributions
using CairoMakie
using PairPlots
┌ Warning: Module Octofitter with build ID fafbfcfd-a27b-b7ca-0000-006fbc8fe2e9 is missing from the cache.
│ This may mean Octofitter [daf3887e-d01a-44a1-9d7e-98f15c5d69c9] does not support precompilation but is imported by a module that does.
└ @ Base loading.jl:2011
Download simulated JWST AMI observations from our examples folder on GitHub:
download("https://github.com/sefffal/Octofitter.jl/raw/main/examples/AMI_data/Sim_data_2023_1_.oifits", "Sim_data_2023_1_.oifits")
download("https://github.com/sefffal/Octofitter.jl/raw/main/examples/AMI_data/Sim_data_2023_2_.oifits", "Sim_data_2023_2_.oifits")
download("https://github.com/sefffal/Octofitter.jl/raw/main/examples/AMI_data/Sim_data_2024_1_.oifits", "Sim_data_2024_1_.oifits")
"Sim_data_2024_1_.oifits"
Create the likelihood object:
vis_like = InterferometryLikelihood(
(; filename="Sim_data_2023_1_.oifits", epoch=mjd("2023-06-01"), spectrum_var=:contrast_F480M, use_vis2=false),
(; filename="Sim_data_2023_2_.oifits", epoch=mjd("2023-08-15"), spectrum_var=:contrast_F480M, use_vis2=false),
(; filename="Sim_data_2024_1_.oifits", epoch=mjd("2024-06-01"), spectrum_var=:contrast_F480M, use_vis2=false),
)
OctofitterInterferometry.InterferometryLikelihood Table with 14 columns and 3 rows:
filename epoch spectrum_var use_vis2 ⋯
┌───────────────────────────────────────────────────────────
1 │ Sim_data_2023_1_.oi… 60096.0 contrast_F480M false ⋯
2 │ Sim_data_2023_2_.oi… 60171.0 contrast_F480M false ⋯
3 │ Sim_data_2024_1_.oi… 60462.0 contrast_F480M false ⋯
Plot the closure phases:
fig = Makie.Figure()
ax = Axis(
fig[1,1],
xlabel="index",
ylabel="closure phase",
)
Makie.stem!(
vis_like.table.cps_data[1][:],
label="epoch 1",
)
Makie.stem!(
vis_like.table.cps_data[2][:],
label="epoch 2"
)
Makie.stem!(
vis_like.table.cps_data[3][:],
label="epoch 3"
)
Makie.Legend(fig[1,2], ax)
fig
@planet b Visual{KepOrbit} begin
a ~ truncated(Normal(2,0.1), lower=0.1)
e ~ truncated(Normal(0, 0.05),lower=0, upper=0.90)
i ~ Sine()
ω ~ UniformCircular()
Ω ~ UniformCircular()
# Our prior on the planet's photometry
# 0 +- 10% of stars brightness (assuming this is unit of data files)
contrast_F480M ~ truncated(Normal(0, 0.1),lower=0)
θ ~ UniformCircular()
tp = θ_at_epoch_to_tperi(system,b,60171) # reference epoch for θ. Choose an MJD date near your data.
end
@system Tutoria begin
M ~ truncated(Normal(1.5, 0.01), lower=0.1)
plx ~ truncated(Normal(100., 0.1), lower=0.1)
end vis_like b
System model Tutoria
Derived:
Priors:
M Truncated(Distributions.Normal{Float64}(μ=1.5, σ=0.01); lower=0.1)
plx Truncated(Distributions.Normal{Float64}(μ=100.0, σ=0.1); lower=0.1)
Planet b
Derived:
ω, Ω, θ, tp,
Priors:
a Truncated(Distributions.Normal{Float64}(μ=2.0, σ=0.1); lower=0.1)
e Truncated(Distributions.Normal{Float64}(μ=0.0, σ=0.05); lower=0.0, upper=0.9)
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)
contrast_F480M Truncated(Distributions.Normal{Float64}(μ=0.0, σ=0.1); lower=0.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)
OctofitterInterferometry.InterferometryLikelihood Table with 14 columns and 3 rows:
filename epoch spectrum_var use_vis2 ⋯
┌───────────────────────────────────────────────────────────
1 │ Sim_data_2023_1_.oi… 60096.0 contrast_F480M false ⋯
2 │ Sim_data_2023_2_.oi… 60171.0 contrast_F480M false ⋯
3 │ Sim_data_2024_1_.oi… 60462.0 contrast_F480M false ⋯
Create the model object and run octofit_pigeons
:
model = Octofitter.LogDensityModel(Tutoria)
using Pigeons
results,pt = octofit_pigeons(model, n_rounds=10);
┌ Warning: This model has priors that cannot be sampled IID.
└ @ OctofitterPigeonsExt ~/work/Octofitter.jl/Octofitter.jl/ext/OctofitterPigeonsExt/OctofitterPigeonsExt.jl:104
[ Info: Sampler running with multiple threads : true
[ Info: Likelihood evaluated with multiple threads: false
[ Info: Determining initial positions using pathfinder, around that location.
┌ Info: Found a sample of initial positions
└ initial_logpost_range = (190.80420261748498, 196.73373265958026)
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
scans restarts Λ Λ_var time(s) allc(B) log(Z₁/Z₀) min(α) mean(α) min(αₑ) mean(αₑ)
────────── ────────── ────────── ────────── ────────── ────────── ────────── ────────── ────────── ────────── ──────────
2 0 2.77 1.86 0.403 8.81e+07 -2.39e+04 0 0.851 1 1
4 0 4.12 3.77 0.467 1.59e+08 -137 0 0.746 1 1
8 0 5.25 4.47 0.909 3.18e+08 134 1.81e-41 0.686 1 1
16 0 5.61 4.89 1.78 6.37e+08 180 6.56e-05 0.661 1 1
32 0 6 5.46 3.51 1.26e+09 179 0.189 0.63 1 1
64 4 6.11 2.46 7.29 2.6e+09 176 0.339 0.724 1 1
128 9 6.16 2.76 14.2 5.24e+09 176 0.395 0.712 1 1
256 35 5.88 2.74 28.5 1.05e+10 176 0.484 0.722 1 1
512 76 6.2 2.62 56.8 2.1e+10 176 0.511 0.716 1 1
1.02e+03 133 6.07 2.66 114 4.19e+10 176 0.55 0.719 1 1
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Note that we use Pigeons paralell tempered sampling (octofit_pigeons
) instead of HMC (octofit
) because interferometry data is almost always multi-modal (or more precisely non-convex, there is often still a single mode that dominates).
Examine the recovered photometry posterior:
hist(results[:b_contrast_F480M][:], axis=(;xlabel="F480M"))
Determine the significance of the detection:
using Statistics
phot = results[:b_contrast_F480M][:]
snr = mean(phot)/std(phot)
7.108898728348015
Plot the resulting orbit:
octoplot(model, results)
Plot only the position at each epoch:
using PlanetOrbits
els = Octofitter.construct_elements(results,:b,:);
fig = Makie.Figure()
ax = Makie.Axis(
fig[1,1],
autolimitaspect = 1,
xreversed=true,
xlabel="ΔR.A. (mas)",
ylabel="ΔDec. (mas)",
)
for epoch in vis_like.table.epoch
Makie.scatter!(
ax,
raoff.(els, epoch)[:],
decoff.(els, epoch)[:],
label=string(mjd2date(epoch)),
markersize=1.5,
)
end
Makie.Legend(fig[1,2], ax, "date")
fig
Finally we can examine the joint photometry and orbit posterior as a corner plot:
using PairPlots
using CairoMakie: Makie
octocorner(model, results)