Real Data Example

Trey V. Wenger (c) November 2024

Here we demonstrate the basic amoeba2 workflow applied to some real OH absorption data.

[1]:
# General imports
import numpy as np
import matplotlib.pyplot as plt
import arviz as az
import pandas as pd

import pytensor
print("pytensor version:", pytensor.__version__)

import pymc
print("pymc version:", pymc.__version__)

import bayes_spec
print("bayes_spec version:", bayes_spec.__version__)

import amoeba2
print("amoeba2 version:", amoeba2.__version__)

# Notebook configuration
pd.options.display.max_rows = None
pytensor version: 2.26.3
pymc version: 5.18.2
bayes_spec version: 1.7.2
amoeba2 version: 1.0.1+7.gad490bf.dirty

Load the Data

Our data are stored in a pickle file.

[2]:
import pickle

with open("example_data.pkl", "rb") as f:
    real_data = pickle.load(f)

print(real_data.keys())
dict_keys(['coord', 'velocity_1612', 'absorption_1612', 'velocity_1665', 'absorption_1665', 'velocity_1667', 'absorption_1667', 'velocity_1720', 'absorption_1720'])

We pack the data into a SpecData dictionary.

[8]:
from bayes_spec import SpecData

data = {}
for transition in ["1612", "1665", "1667", "1720"]:
    # estimate rms
    med = np.median(real_data[f"absorption_{transition}"])
    rms = 1.4826 * np.median(np.abs(real_data[f"absorption_{transition}"] - med))
    data[f"absorption_{transition}"] = SpecData(
        real_data[f"velocity_{transition}"],
        real_data[f"absorption_{transition}"],
        rms,
        xlabel=r"$V_{\rm LSR}$ (km s$^{-1}$)",
        ylabel=r"$1 - e^{-\tau_{"+f"{transition}"+r"}}$"
    )

# Plot the data
fig, axes = plt.subplots(4, sharex=True, layout="constrained", figsize=(8, 8))
for ax, datum in zip(axes, data.values()):
    ax.plot(datum.spectral, datum.brightness, "k-")
    ax.set_ylabel(datum.ylabel)
_ = axes[-1].set_xlabel(datum.xlabel)
../_images/notebooks_real_data_5_0.png

Optimization

Now we determine the optimal number of clouds using the AbsorptionModel. It seems that the baselines are well-behaved, so we set baseline_degree=0. We update the velocity and rms priors to something reasonable for these data.

[11]:
from bayes_spec import Optimize
from amoeba2 import AbsorptionModel

# Initialize optimizer
opt = Optimize(
    AbsorptionModel,  # model definition
    data,  # data dictionary
    max_n_clouds=8,  # maximum number of clouds
    baseline_degree=0,  # polynomial baseline degree
    seed=1234,  # random seed
    verbose=True,  # verbosity
)

# Define each model
opt.add_priors(
    prior_tau = [0.1, 0.1], # mean and width of log10(tau) prior
    prior_log10_depth = [0.0, 0.25], # mean and width of log10(depth) prior (pc)
    prior_log10_Tkin = [2.0, 1.0], # mean and width of log10(Tkin) prior (K)
    prior_velocity = [70.0, 5.0], # mean and width of velocity prior (km/s)
    prior_log10_nth_fwhm_1pc = [0.2, 0.1], # mean and width of non-thermal FWHM prior (km/s)
    prior_depth_nth_fwhm_power = [0.4, 0.1], # mean and width of non-thermal FWHM exponent prior
    ordered = False, # do not assume optically-thin
    mainline_pos_tau = True, # force main line optical depths to be positive
)
opt.add_likelihood()

We will assume that we have thoroughly tested our model and determined effective VI and MCMC hyperparameters for our data. Below we use the same values as in the other tutorials, but this is just for demonstration. You should test these hyperparameters carefully. Now we sample every model with MCMC.

[12]:
fit_kwargs = {
    "rel_tolerance": 0.01,
    "abs_tolerance": 0.1,
    "learning_rate": 1e-2,
}
sample_kwargs = {
    "chains": 4,
    "cores": 4,
    "tune": 1000,
    "draws": 1000,
    "init_kwargs": fit_kwargs,
    "nuts_kwargs": {"target_accept": 0.8},
}
opt.optimize(bic_threshold=10.0, sample_kwargs=sample_kwargs, fit_kwargs=fit_kwargs, approx=False)
Null hypothesis BIC = 7.341e+03
Sampling n_cloud = 1 posterior...
Initializing NUTS using custom advi+adapt_diag strategy
Convergence achieved at 3600
Interrupted at 3,599 [3%]: Average Loss = -4,787.5
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [baseline_absorption_1612_norm, baseline_absorption_1665_norm, baseline_absorption_1667_norm, baseline_absorption_1720_norm, tau_1612_norm, tau_1665_norm, tau_1667_norm, log10_depth_norm, log10_Tkin_norm, velocity_norm, log10_nth_fwhm_1pc_norm, depth_nth_fwhm_power_norm]
Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 142 seconds.
Adding log-likelihood to trace
There were 26 divergences in converged chains.
GMM converged to unique solution
n_cloud = 1 solution = 0 BIC = -1.833e+04

Sampling n_cloud = 2 posterior...
Initializing NUTS using custom advi+adapt_diag strategy
Convergence achieved at 3600
Interrupted at 3,599 [3%]: Average Loss = -5,900.2
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [baseline_absorption_1612_norm, baseline_absorption_1665_norm, baseline_absorption_1667_norm, baseline_absorption_1720_norm, tau_1612_norm, tau_1665_norm, tau_1667_norm, log10_depth_norm, log10_Tkin_norm, velocity_norm, log10_nth_fwhm_1pc_norm, depth_nth_fwhm_power_norm]
Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 503 seconds.
Adding log-likelihood to trace
There were 112 divergences in converged chains.
GMM converged to unique solution
3 of 4 chains appear converged.
n_cloud = 2 solution = 0 BIC = -1.971e+04

Sampling n_cloud = 3 posterior...
Initializing NUTS using custom advi+adapt_diag strategy
Convergence achieved at 3800
Interrupted at 3,799 [3%]: Average Loss = -6,654.5
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [baseline_absorption_1612_norm, baseline_absorption_1665_norm, baseline_absorption_1667_norm, baseline_absorption_1720_norm, tau_1612_norm, tau_1665_norm, tau_1667_norm, log10_depth_norm, log10_Tkin_norm, velocity_norm, log10_nth_fwhm_1pc_norm, depth_nth_fwhm_power_norm]
Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 453 seconds.
Adding log-likelihood to trace
There were 212 divergences in converged chains.
GMM converged to unique solution
2 of 4 chains appear converged.
n_cloud = 3 solution = 0 BIC = -1.991e+04

Sampling n_cloud = 4 posterior...
Initializing NUTS using custom advi+adapt_diag strategy
Convergence achieved at 3800
Interrupted at 3,799 [3%]: Average Loss = -6,775.2
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [baseline_absorption_1612_norm, baseline_absorption_1665_norm, baseline_absorption_1667_norm, baseline_absorption_1720_norm, tau_1612_norm, tau_1665_norm, tau_1667_norm, log10_depth_norm, log10_Tkin_norm, velocity_norm, log10_nth_fwhm_1pc_norm, depth_nth_fwhm_power_norm]
Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 255 seconds.
Adding log-likelihood to trace
There were 68 divergences in converged chains.
GMM converged to unique solution
n_cloud = 4 solution = 0 BIC = -1.999e+04

Sampling n_cloud = 5 posterior...
Initializing NUTS using custom advi+adapt_diag strategy
Convergence achieved at 3600
Interrupted at 3,599 [3%]: Average Loss = -6,864.5
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [baseline_absorption_1612_norm, baseline_absorption_1665_norm, baseline_absorption_1667_norm, baseline_absorption_1720_norm, tau_1612_norm, tau_1665_norm, tau_1667_norm, log10_depth_norm, log10_Tkin_norm, velocity_norm, log10_nth_fwhm_1pc_norm, depth_nth_fwhm_power_norm]
Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 288 seconds.
Adding log-likelihood to trace
There were 2 divergences in converged chains.
GMM converged to unique solution
n_cloud = 5 solution = 0 BIC = -1.999e+04

Sampling n_cloud = 6 posterior...
Initializing NUTS using custom advi+adapt_diag strategy
Convergence achieved at 4300
Interrupted at 4,299 [4%]: Average Loss = -7,458.6
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [baseline_absorption_1612_norm, baseline_absorption_1665_norm, baseline_absorption_1667_norm, baseline_absorption_1720_norm, tau_1612_norm, tau_1665_norm, tau_1667_norm, log10_depth_norm, log10_Tkin_norm, velocity_norm, log10_nth_fwhm_1pc_norm, depth_nth_fwhm_power_norm]
Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 365 seconds.
Adding log-likelihood to trace
There were 41 divergences in converged chains.
No solution found!
0 of 4 chains appear converged.

Sampling n_cloud = 7 posterior...
Initializing NUTS using custom advi+adapt_diag strategy
Convergence achieved at 4300
Interrupted at 4,299 [4%]: Average Loss = -7,540.2
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [baseline_absorption_1612_norm, baseline_absorption_1665_norm, baseline_absorption_1667_norm, baseline_absorption_1720_norm, tau_1612_norm, tau_1665_norm, tau_1667_norm, log10_depth_norm, log10_Tkin_norm, velocity_norm, log10_nth_fwhm_1pc_norm, depth_nth_fwhm_power_norm]
Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 534 seconds.
Adding log-likelihood to trace
There were 55 divergences in converged chains.
No solution found!
0 of 4 chains appear converged.

Sampling n_cloud = 8 posterior...
Initializing NUTS using custom advi+adapt_diag strategy
Convergence achieved at 4100
Interrupted at 4,099 [4%]: Average Loss = -7,478.1
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [baseline_absorption_1612_norm, baseline_absorption_1665_norm, baseline_absorption_1667_norm, baseline_absorption_1720_norm, tau_1612_norm, tau_1665_norm, tau_1667_norm, log10_depth_norm, log10_Tkin_norm, velocity_norm, log10_nth_fwhm_1pc_norm, depth_nth_fwhm_power_norm]
Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 787 seconds.
Adding log-likelihood to trace
There were 12 divergences in converged chains.
No solution found!
0 of 4 chains appear converged.

[13]:
az.summary(opt.best_model.trace.solution_0)
[13]:
mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
baseline_absorption_1612_norm[0] -0.230 0.021 -0.268 -0.189 0.000 0.000 2744.0 2465.0 1.00
baseline_absorption_1665_norm[0] -0.352 0.011 -0.373 -0.332 0.000 0.000 1903.0 2553.0 1.00
baseline_absorption_1667_norm[0] -0.377 0.009 -0.393 -0.361 0.000 0.000 2451.0 1897.0 1.00
baseline_absorption_1720_norm[0] -0.063 0.029 -0.117 -0.009 0.001 0.001 1606.0 668.0 1.00
tau_1612_norm[0] 2.969 0.388 2.270 3.743 0.011 0.008 1100.0 1406.0 1.00
tau_1612_norm[1] -2.222 0.151 -2.519 -1.953 0.003 0.002 2034.0 1596.0 1.00
tau_1612_norm[2] -0.678 0.615 -1.871 0.413 0.021 0.015 860.0 1229.0 1.00
tau_1612_norm[3] 1.984 0.418 1.264 2.746 0.015 0.011 863.0 1109.0 1.00
log10_depth_norm[0] 0.181 0.628 -1.055 1.305 0.019 0.013 1102.0 1371.0 1.00
log10_depth_norm[1] 1.266 0.624 0.043 2.414 0.026 0.018 679.0 265.0 1.00
log10_depth_norm[2] 0.698 0.618 -0.441 1.907 0.021 0.015 911.0 1141.0 1.00
log10_depth_norm[3] 0.462 0.693 -0.910 1.674 0.021 0.015 1078.0 1487.0 1.00
log10_Tkin_norm[0] -0.093 0.911 -1.621 1.523 0.021 0.017 1921.0 1556.0 1.00
log10_Tkin_norm[1] 0.251 1.015 -1.442 1.760 0.030 0.026 682.0 287.0 1.00
log10_Tkin_norm[2] 0.120 0.968 -1.489 1.675 0.027 0.020 1174.0 794.0 1.00
log10_Tkin_norm[3] 0.075 0.959 -1.507 1.765 0.022 0.017 1733.0 1137.0 1.00
velocity_norm[0] -0.994 0.020 -1.030 -0.958 0.001 0.000 1176.0 937.0 1.00
velocity_norm[1] -0.339 0.017 -0.370 -0.308 0.001 0.000 842.0 622.0 1.00
velocity_norm[2] -1.222 0.035 -1.287 -1.155 0.001 0.001 1354.0 1381.0 1.00
velocity_norm[3] -1.702 0.036 -1.763 -1.631 0.001 0.001 889.0 1047.0 1.00
log10_nth_fwhm_1pc_norm 2.655 0.558 1.542 3.631 0.017 0.012 1110.0 1398.0 1.00
depth_nth_fwhm_power_norm 0.024 1.000 -1.970 1.837 0.026 0.024 1573.0 818.0 1.00
tau_1665_norm[0] 5.843 0.588 4.714 6.963 0.013 0.009 1919.0 2131.0 1.00
tau_1665_norm[1] 5.998 0.315 5.450 6.625 0.009 0.007 1099.0 722.0 1.00
tau_1665_norm[2] 6.323 0.593 5.245 7.510 0.014 0.010 1729.0 2051.0 1.00
tau_1665_norm[3] 1.970 0.358 1.313 2.651 0.010 0.007 1394.0 1688.0 1.01
tau_1667_norm[0] 7.402 0.682 6.134 8.624 0.019 0.013 1383.0 2497.0 1.00
tau_1667_norm[1] 10.207 0.463 9.286 11.041 0.016 0.011 843.0 588.0 1.00
tau_1667_norm[2] 7.317 0.761 5.826 8.733 0.021 0.015 1359.0 1434.0 1.00
tau_1667_norm[3] 4.787 0.591 3.674 5.871 0.019 0.013 1030.0 1496.0 1.00
tau_1612[0] 0.397 0.039 0.327 0.474 0.001 0.001 1100.0 1406.0 1.00
tau_1612[1] -0.122 0.015 -0.152 -0.095 0.000 0.000 2034.0 1596.0 1.00
tau_1612[2] 0.032 0.062 -0.087 0.141 0.002 0.001 860.0 1229.0 1.00
tau_1612[3] 0.298 0.042 0.226 0.375 0.001 0.001 863.0 1109.0 1.00
tau_1665[0] 0.584 0.059 0.471 0.696 0.001 0.001 1919.0 2131.0 1.00
tau_1665[1] 0.600 0.031 0.545 0.662 0.001 0.001 1099.0 722.0 1.00
tau_1665[2] 0.632 0.059 0.524 0.751 0.001 0.001 1729.0 2051.0 1.00
tau_1665[3] 0.197 0.036 0.131 0.265 0.001 0.001 1394.0 1688.0 1.01
tau_1667[0] 0.740 0.068 0.613 0.862 0.002 0.001 1383.0 2497.0 1.00
tau_1667[1] 1.021 0.046 0.929 1.104 0.002 0.001 843.0 588.0 1.00
tau_1667[2] 0.732 0.076 0.583 0.873 0.002 0.001 1359.0 1434.0 1.00
tau_1667[3] 0.479 0.059 0.367 0.587 0.002 0.001 1030.0 1496.0 1.00
tau_1720[0] -0.198 0.041 -0.274 -0.122 0.001 0.001 1198.0 1266.0 1.00
tau_1720[1] 0.356 0.016 0.325 0.384 0.000 0.000 2127.0 1821.0 1.00
tau_1720[2] 0.176 0.059 0.070 0.286 0.002 0.001 922.0 1068.0 1.00
tau_1720[3] -0.206 0.037 -0.279 -0.145 0.001 0.001 915.0 806.0 1.00
log10_depth[0] 0.045 0.157 -0.264 0.326 0.005 0.003 1102.0 1371.0 1.00
log10_depth[1] 0.316 0.156 0.011 0.603 0.006 0.005 679.0 265.0 1.00
log10_depth[2] 0.175 0.155 -0.110 0.477 0.005 0.004 911.0 1141.0 1.00
log10_depth[3] 0.115 0.173 -0.227 0.419 0.005 0.004 1078.0 1487.0 1.00
log10_Tkin[0] 1.907 0.911 0.379 3.523 0.021 0.015 1921.0 1556.0 1.00
log10_Tkin[1] 2.251 1.015 0.558 3.760 0.030 0.026 682.0 287.0 1.00
log10_Tkin[2] 2.120 0.968 0.511 3.675 0.027 0.021 1174.0 794.0 1.00
log10_Tkin[3] 2.075 0.959 0.493 3.765 0.022 0.017 1733.0 1137.0 1.00
velocity[0] 65.028 0.100 64.849 65.212 0.003 0.002 1176.0 937.0 1.00
velocity[1] 68.306 0.084 68.148 68.462 0.003 0.002 842.0 622.0 1.00
velocity[2] 63.888 0.177 63.566 64.225 0.005 0.003 1354.0 1381.0 1.00
velocity[3] 61.489 0.179 61.183 61.844 0.006 0.004 889.0 1047.0 1.00
log10_nth_fwhm_1pc 0.466 0.056 0.354 0.563 0.002 0.001 1110.0 1398.0 1.00
depth_nth_fwhm_power 0.402 0.100 0.203 0.584 0.003 0.002 1573.0 818.0 1.00
fwhm_thermal[0] 0.602 0.558 0.008 1.757 0.013 0.010 1921.0 1556.0 1.00
fwhm_thermal[1] 0.968 0.893 0.012 2.739 0.039 0.032 682.0 287.0 1.01
fwhm_thermal[2] 0.812 0.768 0.015 2.392 0.026 0.020 1174.0 794.0 1.00
fwhm_thermal[3] 0.765 0.743 0.006 2.300 0.021 0.016 1733.0 1137.0 1.00
fwhm_nonthermal[0] 3.095 0.259 2.541 3.577 0.007 0.005 1425.0 1270.0 1.00
fwhm_nonthermal[1] 3.935 0.357 3.133 4.413 0.018 0.012 646.0 238.0 1.01
fwhm_nonthermal[2] 3.465 0.379 2.685 4.087 0.013 0.009 985.0 876.0 1.00
fwhm_nonthermal[3] 3.309 0.429 2.528 4.149 0.014 0.010 961.0 1252.0 1.00
fwhm[0] 3.207 0.179 2.899 3.556 0.005 0.004 1181.0 1639.0 1.01
fwhm[1] 4.163 0.129 3.938 4.430 0.003 0.002 1593.0 1925.0 1.00
fwhm[2] 3.652 0.246 3.201 4.109 0.007 0.005 1329.0 1552.0 1.00
fwhm[3] 3.487 0.337 2.872 4.124 0.012 0.008 891.0 1115.0 1.00

We check that our prior distributions are reasonable by drawing prior predictive checks. Each colored line is a simulated “observation” with parameters drawn from the prior distributions. You should check that these simulated observations at least somewhat overlap your actual observation (black line).

[14]:
from bayes_spec.plots import plot_predictive

posterior = opt.best_model.sample_posterior_predictive(
    thin=100, # keep one in {thin} posterior samples
)
axes = plot_predictive(opt.best_model.data, posterior.posterior_predictive)
axes.ravel()[0].figure.set_size_inches(12, 12)
Sampling: [absorption_1612, absorption_1665, absorption_1667, absorption_1720]
../_images/notebooks_real_data_12_3.png
[15]:
from bayes_spec.plots import plot_pair

axes = plot_pair(
    opt.best_model.trace.solution_0, # samples
    opt.best_model.cloud_freeRVs, # var_names to plot
    labeller=opt.best_model.labeller, # label manager
)
axes.ravel()[0].figure.set_size_inches(12, 12)
../_images/notebooks_real_data_13_0.png
[ ]: