EmissionAbsorptionModel Tutorial

Trey V. Wenger (c) July 2025

Here we demonstrate the basic features of the EmissionAbsorptionModel model. EmissionAbsorptionModel models both 21cm emission and absorption observations simultaneously.

[1]:
# General imports
import time

import matplotlib.pyplot as plt
import arviz as az
import pandas as pd
import numpy as np
import pymc as pm

print("pymc version:", pm.__version__)
print("arviz version:", az.__version__)

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

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

# Notebook configuration
pd.options.display.max_rows = None
pymc version: 5.22.0
arviz version: 0.22.0dev
bayes_spec version: 1.9.0
caribou_hi version: 4.1.0+3.g94a913e.dirty

Simulating Data

To test the model, we must simulate some data. We can do this with EmissionAbsorptionModel, but we must pack a “dummy” data structure first. The model expects the observations to be named "emission" and "absorption".

[2]:
from bayes_spec import SpecData

# spectral axes definitions
emission_axis = np.linspace(-60.0, 60.0, 200)  # km s-1
absorption_axis = np.linspace(-30.0, 30.0, 100)  # km s-1

# data noise can either be a scalar (assumed constant noise across the spectrum)
# or an array of the same length as the data
rms_emission = 0.1  # K
rms_absorption = 0.001  # 1 - exp(-tau)

# brightness data. In this case, we just throw in some random data for now
# since we are only doing this in order to simulate some actual data.
emission = rms_emission * np.random.randn(len(emission_axis))
absorption = rms_absorption * np.random.randn(len(absorption_axis))

dummy_data = {
    "emission": SpecData(
        emission_axis,
        emission,
        rms_emission,
        xlabel=r"$V_{\rm LSR}$ (km s$^{-1}$)",
        ylabel=r"$T_B$ (K)",
    ),
    "absorption": SpecData(
        absorption_axis,
        absorption,
        rms_absorption,
        xlabel=r"$V_{\rm LSR}$ (km s$^{-1}$)",
        ylabel=r"1 - exp(-$\tau$)",
    ),
}

# Plot dummy data
fig, axes = plt.subplots(2, layout="constrained", sharex=True)
axes[0].plot(dummy_data["emission"].spectral, dummy_data["emission"].brightness, "k-")
axes[0].plot(dummy_data["emission"].spectral, dummy_data["emission"].noise, "r-")
axes[1].plot(dummy_data["absorption"].spectral, dummy_data["absorption"].brightness, "k-", label="Data")
axes[1].plot(dummy_data["absorption"].spectral, dummy_data["absorption"].noise, "r-", label="Noise")
axes[1].set_xlabel(dummy_data["emission"].xlabel)
axes[0].set_ylabel(dummy_data["emission"].ylabel)
axes[1].set_ylabel(dummy_data["absorption"].ylabel)
_ = axes[1].legend(loc="lower left")
../_images/notebooks_emission_absorption_model_3_0.png

Now that we have a dummy data format, we can generate a simulated observation by evaluating the model. First we create the model.

[3]:
from caribou_hi import EmissionAbsorptionModel

# Initialize and define the model
n_clouds = 3
baseline_degree = 0
model = EmissionAbsorptionModel(
    dummy_data,
    n_clouds=n_clouds,
    baseline_degree=baseline_degree,
    bg_temp = 3.77, # assumed background temperature (K)
    seed=1234,
    verbose=True
)
model.add_priors(
    prior_filling_factor=[2.0, 1.0],  # filling factor prior shape
    prior_TB_fwhm=50.0, # peak brightness temperature x FWHM prior width (K km s-1)
    prior_tkin_factor=[2.0, 2.0], # kinetic temperature prior shape
    prior_sigma_log10_NHI=0.5, # log-normal column density distribution width
    prior_fwhm2=200.0, # FWHM^2 prior width (km2 s-2)
    prior_log10_nHI=[0.0, 1.5],  # log10(density) prior mean and width (cm-3)
    prior_velocity=[-15.0, 15.0],  # lower and upper limit of velocity prior (km/s)
    prior_log10_n_alpha=[-6.0, 1.0],  # log10(n_alpha) prior width (cm-3)
    prior_fwhm_L=None,  # Assume Gaussian line profile
    prior_baseline_coeffs=None,  # Default baseline priors
)
model.add_likelihood()
[4]:
from caribou_hi import physics

# Simulation parameters
filling_factor = np.array([0.2, 0.8, 1.0])
absorption_weight = np.array([0.9, 0.8, 1.0])

log10_nHI = np.array([1.25, 0.0, -0.5])
tkin = np.array([50.0, 300.0, 5000.0])
log10_n_alpha = np.array([-5.5, -6.0, -6.5])
depth = np.array([5.0, 25.0, 300.0])
nth_fwhm_1pc = np.array([1.25, 1.75, 1.5])
depth_nth_fwhm_power = np.array([0.2, 0.3, 0.4])

tspin = physics.calc_spin_temp(tkin, 10.0**log10_nHI, 10.0**log10_n_alpha).eval()
print("tspin", tspin)

fwhm_nonthermal = physics.calc_nonthermal_fwhm(depth, nth_fwhm_1pc, depth_nth_fwhm_power)
fwhm2_thermal = physics.calc_thermal_fwhm2(tkin)
fwhm = np.sqrt(fwhm_nonthermal**2.0 + fwhm2_thermal)
print("fwhm", fwhm)

log10_Pth = np.log10(tkin) + log10_nHI
print("log10_Pth", log10_Pth)

log10_NHI = log10_nHI + np.log10(depth) + 18.489351
print("log10_NHI", log10_NHI)

tau_total = physics.calc_tau_total(10.0**log10_NHI, tspin)
print("tau_total", tau_total)

const = np.sqrt(2.0 * np.pi) / (2.0 * np.sqrt(2.0 * np.log(2.0)))
tau_peak = tau_total / fwhm / const
print("tau_peak", tau_peak)

TB_fwhm = filling_factor * tspin * (1.0 - np.exp(-tau_peak)) * fwhm
print("TB_fwhm", TB_fwhm)

wt_ff_tkin = absorption_weight / filling_factor / tkin
print("wt_ff_tkin", wt_ff_tkin)

sim_params = {
    "TB_fwhm": TB_fwhm,
    "tkin": tkin,
    "filling_factor": filling_factor,
    "wt_ff_tkin": wt_ff_tkin,
    "absorption_weight": absorption_weight,
    "fwhm2": fwhm**2.0,
    "log10_nHI": log10_nHI,
    "velocity": np.array([-5.0, 5.0, 10.0]),
    "log10_n_alpha": log10_n_alpha,
    "baseline_emission_norm": [0.0],
}

# add derived quantities to sim_params
for key in model.cloud_deterministics:
    if key not in sim_params.keys():
        sim_params[key] = model.model[key].eval(sim_params, on_unused_input="ignore")

# Evaluate and save simulated observation
emission = model.model["emission"].eval(sim_params, on_unused_input="ignore")
absorption = model.model["absorption"].eval(sim_params, on_unused_input="ignore")

data = {
    "emission": SpecData(
        emission_axis,
        emission,
        rms_emission,
        xlabel=r"$V_{\rm LSR}$ (km s$^{-1}$)",
        ylabel=r"$T_B$ (K)",
    ),
    "absorption": SpecData(
        absorption_axis,
        absorption,
        rms_absorption,
        xlabel=r"$V_{\rm LSR}$ (km s$^{-1}$)",
        ylabel=r"1 - exp(-$\tau$)",
    ),
}
tspin [  49.9920589   297.73994712 2795.52422645]
fwhm [ 2.29393108  5.90364916 21.08270953]
log10_Pth [2.94897    2.47712125 3.19897   ]
log10_NHI [20.438321   19.88729101 20.46647225]
tau_total [3.01140471 0.14216839 0.05745901]
tau_peak [1.23326541 0.02262301 0.00256035]
TB_fwhm [ 16.25359747  31.45536056 150.70696853]
wt_ff_tkin [0.09       0.00333333 0.0002    ]
[5]:
sim_params
[5]:
{'TB_fwhm': array([ 16.25359747,  31.45536056, 150.70696853]),
 'tkin': array([  50.,  300., 5000.]),
 'filling_factor': array([0.2, 0.8, 1. ]),
 'wt_ff_tkin': array([0.09      , 0.00333333, 0.0002    ]),
 'absorption_weight': array([0.9, 0.8, 1. ]),
 'fwhm2': array([  5.26211978,  34.85307344, 444.48064099]),
 'log10_nHI': array([ 1.25,  0.  , -0.5 ]),
 'velocity': array([-5.,  5., 10.]),
 'log10_n_alpha': array([-5.5, -6. , -6.5]),
 'baseline_emission_norm': [0.0],
 'tspin': array([  49.9920589 ,  297.73994712, 2795.52422645]),
 'tau_total': array([3.01140471, 0.14216839, 0.05745901]),
 'log10_Pth': array([2.94897   , 2.47712125, 3.19897   ]),
 'log10_NHI': array([20.438321  , 19.88729101, 20.46647225]),
 'log10_depth': array([0.69897   , 1.39794001, 2.47712125])}
[6]:
# Plot data
fig, axes = plt.subplots(2, layout="constrained", sharex=True)
axes[0].plot(data["emission"].spectral, data["emission"].brightness, "k-")
axes[0].plot(data["emission"].spectral, data["emission"].noise, "r-")
axes[1].plot(data["absorption"].spectral, data["absorption"].brightness, "k-", label="Data")
axes[1].plot(data["absorption"].spectral, data["absorption"].noise, "r-", label="Noise")
axes[1].set_xlabel(data["emission"].xlabel)
axes[0].set_ylabel(data["emission"].ylabel)
axes[1].set_ylabel(data["absorption"].ylabel)
_ = axes[1].legend(loc="upper left")
../_images/notebooks_emission_absorption_model_8_0.png

Model Definition

Finally, with our model definition and (simulated) data in hand, we can explore the capabilities of EmissionAbsorptionModel.

[7]:
# Initialize and define the model
model = EmissionAbsorptionModel(
    data,
    n_clouds=n_clouds,
    baseline_degree=baseline_degree,
    bg_temp = 3.77, # assumed background temperature (K)
    seed=1234,
    verbose=True
)
model.add_priors(
    prior_filling_factor=[2.0, 1.0],  # filling factor prior shape
    prior_TB_fwhm=200.0, # peak brightness temperature x FWHM prior width (K km s-1)
    prior_tkin_factor=[2.0, 2.0], # kinetic temperature prior shape
    prior_sigma_log10_NHI=0.5, # log-normal column density distribution width
    prior_fwhm2=200.0, # FWHM^2 prior width (km2 s-2)
    prior_log10_nHI=[0.0, 1.5],  # log10(density) prior mean and width (cm-3)
    prior_velocity=[-20.0, 20.0],  # lower and upper limit of velocity prior (km/s)
    prior_log10_n_alpha=[-6.0, 1.0],  # log10(n_alpha) prior width (cm-3)
    prior_fwhm_L=None,  # Assume Gaussian line profile
    prior_baseline_coeffs=None,  # Default baseline priors
)
model.add_likelihood()
[8]:
# Plot model graph
model.graph().render("emission_absorption_model", format="png")
model.graph()
[8]:
../_images/notebooks_emission_absorption_model_11_0.svg
[9]:
# model string representation
print(model.model.str_repr())
  baseline_emission_norm ~ Normal(0, 1)
baseline_absorption_norm ~ Normal(0, 1)
              fwhm2_norm ~ Gamma(0.5, f())
          log10_nHI_norm ~ Normal(0, 1)
           velocity_norm ~ Beta(2, 2)
      log10_n_alpha_norm ~ Normal(0, 1)
            TB_fwhm_norm ~ HalfNormal(0, 1)
        tkin_factor_norm ~ Beta(2, 2)
          filling_factor ~ Beta(2, 1)
              wt_ff_tkin ~ LogNormal(f(tkin_factor_norm, fwhm2_norm, TB_fwhm_norm), 1.15)
                   fwhm2 ~ Deterministic(f(fwhm2_norm))
               log10_nHI ~ Deterministic(f(log10_nHI_norm))
                velocity ~ Deterministic(f(velocity_norm))
           log10_n_alpha ~ Deterministic(f(log10_n_alpha_norm))
                 TB_fwhm ~ Deterministic(f(TB_fwhm_norm))
                    tkin ~ Deterministic(f(tkin_factor_norm, fwhm2_norm, TB_fwhm_norm))
                   tspin ~ Deterministic(f(tkin_factor_norm, log10_n_alpha_norm, fwhm2_norm, TB_fwhm_norm, log10_nHI_norm))
               tau_total ~ Deterministic(f(fwhm2_norm, filling_factor, TB_fwhm_norm, tkin_factor_norm, log10_n_alpha_norm, log10_nHI_norm))
       absorption_weight ~ Deterministic(f(filling_factor, wt_ff_tkin, tkin_factor_norm, fwhm2_norm, TB_fwhm_norm))
               log10_Pth ~ Deterministic(f(log10_nHI_norm, tkin_factor_norm, fwhm2_norm, TB_fwhm_norm))
               log10_NHI ~ Deterministic(f(fwhm2_norm, filling_factor, tkin_factor_norm, log10_n_alpha_norm, TB_fwhm_norm, log10_nHI_norm))
             log10_depth ~ Deterministic(f(log10_nHI_norm, fwhm2_norm, filling_factor, tkin_factor_norm, log10_n_alpha_norm, TB_fwhm_norm))
              absorption ~ Normal(f(baseline_absorption_norm, filling_factor, wt_ff_tkin, fwhm2_norm, tkin_factor_norm, velocity_norm, TB_fwhm_norm, log10_n_alpha_norm, log10_nHI_norm), <constant>)
                emission ~ Normal(f(filling_factor, baseline_emission_norm, tkin_factor_norm, fwhm2_norm, log10_n_alpha_norm, TB_fwhm_norm, log10_nHI_norm, velocity_norm), <constant>)

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).

[10]:
from bayes_spec.plots import plot_predictive

# prior predictive check
prior = model.sample_prior_predictive(
    samples=1000,  # prior predictive samples
)
axes = plot_predictive(model.data, prior.prior_predictive.sel(draw=slice(None, None, 20)))
axes.ravel()[1].sharex(axes.ravel()[0])
Sampling: [TB_fwhm_norm, absorption, baseline_absorption_norm, baseline_emission_norm, emission, filling_factor, fwhm2_norm, log10_nHI_norm, log10_n_alpha_norm, tkin_factor_norm, velocity_norm, wt_ff_tkin]
../_images/notebooks_emission_absorption_model_14_1.png

Or we can inspect the prior distributions of the derived quantities to check that they are physically reasonable. The red points represent the simulation parameters.

[11]:
print(model.cloud_freeRVs)
print(model.cloud_deterministics)
['fwhm2_norm', 'log10_nHI_norm', 'velocity_norm', 'log10_n_alpha_norm', 'TB_fwhm_norm', 'tkin_factor_norm', 'filling_factor', 'wt_ff_tkin']
['fwhm2', 'log10_nHI', 'velocity', 'log10_n_alpha', 'TB_fwhm', 'tkin', 'tspin', 'tau_total', 'absorption_weight', 'log10_Pth', 'log10_NHI', 'log10_depth']
[12]:
from bayes_spec.plots import plot_pair

var_names = model.cloud_deterministics + [p for p in model.cloud_freeRVs if "_norm" not in p]
print(var_names)
_ = plot_pair(
    prior.prior, # samples
    var_names, # var_names to plot
    combine_dims=["cloud"], # concatenate clouds
    labeller=model.labeller, # label manager
    kind="scatter", # plot type
    reference_values=sim_params, # truths
)
['fwhm2', 'log10_nHI', 'velocity', 'log10_n_alpha', 'TB_fwhm', 'tkin', 'tspin', 'tau_total', 'absorption_weight', 'log10_Pth', 'log10_NHI', 'log10_depth', 'filling_factor', 'wt_ff_tkin']
../_images/notebooks_emission_absorption_model_17_1.png

Variational Inference

We can approximate the posterior distribution using variational inference.

[13]:
start = time.time()
model.fit(
    n = 1_000_000, # maximum number of VI iterations
    draws = 1_000, # number of posterior samples
    rel_tolerance = 0.005, # VI relative convergence threshold
    abs_tolerance = 0.005, # VI absolute convergence threshold
    learning_rate = 0.001, # VI learning rate
    start = {"velocity_norm": np.linspace(0.1, 0.9, n_clouds)},
)
end = time.time()
print(f"Runtime: {(end-start)/60.0:.2f} minutes")
Convergence achieved at 53400
Interrupted at 53,399 [5%]: Average Loss = 3.0195e+05
Adding log-likelihood to trace
Runtime: 1.06 minutes
[14]:
pm.summary(model.trace.posterior)
arviz - WARNING - Shape validation failed: input_shape: (1, 1000), minimum_shape: (chains=2, draws=4)
[14]:
mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
baseline_emission_norm[0] -0.253 0.070 -0.375 -0.112 0.002 0.002 939.0 886.0 NaN
baseline_absorption_norm[0] 0.172 0.104 -0.022 0.358 0.003 0.002 1000.0 785.0 NaN
log10_nHI_norm[0] 0.202 0.960 -1.736 1.798 0.032 0.022 875.0 901.0 NaN
log10_nHI_norm[1] -2.525 0.023 -2.566 -2.479 0.001 0.001 973.0 945.0 NaN
log10_nHI_norm[2] 0.549 0.730 -0.965 1.837 0.024 0.016 945.0 937.0 NaN
log10_n_alpha_norm[0] 0.389 0.641 -0.736 1.649 0.021 0.014 937.0 1072.0 NaN
log10_n_alpha_norm[1] -3.295 0.001 -3.296 -3.294 0.000 0.000 809.0 1036.0 NaN
log10_n_alpha_norm[2] 0.668 0.659 -0.628 1.815 0.021 0.013 963.0 979.0 NaN
fwhm2_norm[0] 0.156 0.003 0.152 0.162 0.000 0.000 947.0 935.0 NaN
fwhm2_norm[1] 0.026 0.000 0.026 0.026 0.000 0.000 944.0 823.0 NaN
fwhm2_norm[2] 2.226 0.014 2.200 2.254 0.000 0.000 985.0 767.0 NaN
velocity_norm[0] 0.627 0.001 0.626 0.629 0.000 0.000 829.0 783.0 NaN
velocity_norm[1] 0.375 0.000 0.375 0.375 0.000 0.000 963.0 868.0 NaN
velocity_norm[2] 0.743 0.001 0.742 0.745 0.000 0.000 997.0 1022.0 NaN
TB_fwhm_norm[0] 0.141 0.001 0.139 0.143 0.000 0.000 812.0 965.0 NaN
TB_fwhm_norm[1] 0.097 0.000 0.097 0.097 0.000 0.000 1010.0 951.0 NaN
TB_fwhm_norm[2] 0.774 0.002 0.770 0.777 0.000 0.000 942.0 878.0 NaN
tkin_factor_norm[0] 0.549 0.111 0.325 0.741 0.004 0.002 991.0 909.0 NaN
tkin_factor_norm[1] 0.964 0.001 0.962 0.966 0.000 0.000 873.0 911.0 NaN
tkin_factor_norm[2] 0.334 0.157 0.073 0.620 0.005 0.003 945.0 956.0 NaN
filling_factor[0] 0.698 0.207 0.315 0.993 0.007 0.005 974.0 937.0 NaN
filling_factor[1] 0.878 0.004 0.870 0.885 0.000 0.000 1041.0 944.0 NaN
filling_factor[2] 0.676 0.231 0.239 0.992 0.008 0.005 948.0 826.0 NaN
wt_ff_tkin[0] 0.004 0.000 0.003 0.004 0.000 0.000 1090.0 981.0 NaN
wt_ff_tkin[1] 0.024 0.000 0.024 0.024 0.000 0.000 1030.0 978.0 NaN
wt_ff_tkin[2] 0.000 0.000 0.000 0.000 0.000 0.000 929.0 976.0 NaN
fwhm2[0] 31.299 0.541 30.351 32.347 0.018 0.012 947.0 935.0 NaN
fwhm2[1] 5.270 0.006 5.258 5.281 0.000 0.000 944.0 823.0 NaN
fwhm2[2] 445.179 2.797 440.058 450.835 0.089 0.069 985.0 767.0 NaN
log10_nHI[0] 0.302 1.441 -2.605 2.698 0.049 0.033 875.0 901.0 NaN
log10_nHI[1] -3.787 0.035 -3.849 -3.718 0.001 0.001 973.0 945.0 NaN
log10_nHI[2] 0.824 1.096 -1.448 2.756 0.036 0.023 945.0 937.0 NaN
velocity[0] 5.100 0.025 5.052 5.148 0.001 0.001 829.0 783.0 NaN
velocity[1] -5.002 0.001 -5.005 -4.999 0.000 0.000 963.0 868.0 NaN
velocity[2] 9.724 0.034 9.663 9.793 0.001 0.001 997.0 1022.0 NaN
log10_n_alpha[0] -5.611 0.641 -6.736 -4.351 0.021 0.014 937.0 1072.0 NaN
log10_n_alpha[1] -9.295 0.001 -9.296 -9.294 0.000 0.000 809.0 1036.0 NaN
log10_n_alpha[2] -5.332 0.659 -6.628 -4.185 0.021 0.013 963.0 979.0 NaN
TB_fwhm[0] 28.243 0.212 27.866 28.663 0.007 0.005 812.0 965.0 NaN
TB_fwhm[1] 19.418 0.019 19.382 19.452 0.001 0.000 1010.0 951.0 NaN
TB_fwhm[2] 154.726 0.415 153.992 155.481 0.014 0.009 942.0 878.0 NaN
tkin[0] 377.744 75.955 229.925 513.559 2.378 1.631 1017.0 919.0 NaN
tkin[1] 111.288 0.177 110.977 111.627 0.006 0.004 910.0 880.0 NaN
tkin[2] 3253.986 1523.913 719.562 6033.750 49.610 33.422 941.0 919.0 NaN
tspin[0] 374.911 74.828 236.967 516.922 2.360 1.636 1001.0 874.0 NaN
tspin[1] 25.911 0.036 25.842 25.978 0.001 0.001 1006.0 983.0 NaN
tspin[2] 3114.891 1431.307 826.523 5811.709 46.694 32.735 940.0 962.0 NaN
tau_total[0] 0.145 0.112 0.061 0.283 0.004 0.014 970.0 1017.0 NaN
tau_total[1] 1.137 0.007 1.123 1.149 0.000 0.000 981.0 862.0 NaN
tau_total[2] 0.131 0.153 0.024 0.313 0.005 0.016 932.0 900.0 NaN
absorption_weight[0] 0.927 0.337 0.331 1.596 0.011 0.007 980.0 1017.0 NaN
absorption_weight[1] 2.385 0.012 2.361 2.407 0.000 0.000 1034.0 1016.0 NaN
absorption_weight[2] 0.726 0.436 0.041 1.525 0.014 0.011 924.0 908.0 NaN
log10_Pth[0] 2.870 1.441 0.012 5.402 0.049 0.033 865.0 901.0 NaN
log10_Pth[1] -1.741 0.035 -1.804 -1.673 0.001 0.001 970.0 945.0 NaN
log10_Pth[2] 4.283 1.115 2.033 6.235 0.036 0.024 972.0 871.0 NaN
log10_NHI[0] 19.927 0.174 19.740 20.251 0.006 0.007 990.0 937.0 NaN
log10_NHI[1] 19.730 0.003 19.725 19.735 0.000 0.000 1024.0 907.0 NaN
log10_NHI[2] 20.687 0.209 20.480 21.099 0.007 0.009 946.0 826.0 NaN
log10_depth[0] 1.135 1.449 -1.365 3.987 0.049 0.033 867.0 850.0 NaN
log10_depth[1] 5.028 0.035 4.959 5.089 0.001 0.001 952.0 937.0 NaN
log10_depth[2] 1.374 1.120 -0.853 3.471 0.037 0.024 912.0 865.0 NaN
[15]:
posterior = model.sample_posterior_predictive(
    thin=10,  # keep one in {thin} posterior samples
)
axes = plot_predictive(model.data, posterior.posterior_predictive)
axes.ravel()[1].sharex(axes.ravel()[0])
Sampling: [absorption, emission]
../_images/notebooks_emission_absorption_model_21_3.png

Posterior Sampling: MCMC

We can sample from the posterior distribution using MCMC. We increase target_accept because this model has some degeneracies.

[16]:
start = time.time()
model.sample(
    init="advi+adapt_diag",  # initialization strategy
    tune=1000,  # tuning samples
    draws=1000,  # posterior samples
    chains=8,  # number of independent chains
    cores=8,  # number of parallel chains
    init_kwargs={
        "rel_tolerance": 0.005,
        "abs_tolerance": 0.005,
        "learning_rate": 0.001,
        "start": {"velocity_norm": np.linspace(0.1, 0.9, n_clouds)},
    },  # VI initialization arguments
    nuts_kwargs={"target_accept": 0.9},  # NUTS arguments
)
end = time.time()
print(f"Runtime: {(end-start)/60.0:.2f} minutes")
Initializing NUTS using custom advi+adapt_diag strategy
Convergence achieved at 53400
Interrupted at 53,399 [5%]: Average Loss = 3.0195e+05
Multiprocess sampling (8 chains in 8 jobs)
NUTS: [baseline_emission_norm, baseline_absorption_norm, fwhm2_norm, log10_nHI_norm, velocity_norm, log10_n_alpha_norm, TB_fwhm_norm, tkin_factor_norm, filling_factor, wt_ff_tkin]
Sampling 8 chains for 1_000 tune and 1_000 draw iterations (8_000 + 8_000 draws total) took 350 seconds.
Adding log-likelihood to trace
There were 136 divergences in converged chains.
Runtime: 7.22 minutes
[17]:
model.solve(
    init_params="random_from_data", # GMM initialization strategy
    n_init=10, # number of GMM initilizations
    max_iter=1_000, # maximum number of GMM iterations
    kl_div_threshold=0.1, # covergence threshold
)
GMM converged to unique solution

Check that the effective sample sizes are large and the covergence statistic r_hat is close to 1! If not, you may have to increase the number of tuning steps (tune=2000) or the NUTS acceptance rate (target_accept=0.9).

[18]:
print("solutions:", model.solutions)
az.summary(model.trace["solution_0"])
# this also works: az.summary(model.trace.solution_0)
solutions: [0]
[18]:
mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
baseline_emission_norm[0] -0.234 0.092 -0.408 -0.061 0.002 0.001 3105.0 4297.0 1.00
baseline_absorption_norm[0] 0.172 0.148 -0.107 0.454 0.002 0.002 4444.0 4414.0 1.00
log10_nHI_norm[0] 0.004 1.001 -1.750 1.989 0.020 0.011 2591.0 4311.0 1.00
log10_nHI_norm[1] -0.002 1.007 -1.797 1.964 0.015 0.012 4435.0 4228.0 1.00
log10_nHI_norm[2] -0.074 0.981 -1.861 1.793 0.020 0.011 2428.0 4434.0 1.00
log10_n_alpha_norm[0] -0.013 1.009 -1.881 1.862 0.028 0.017 1333.0 1164.0 1.01
log10_n_alpha_norm[1] 0.001 1.009 -1.854 1.962 0.016 0.011 4019.0 4748.0 1.00
log10_n_alpha_norm[2] -0.060 0.957 -1.789 1.791 0.019 0.010 2406.0 4808.0 1.00
fwhm2_norm[0] 0.161 0.005 0.152 0.171 0.000 0.000 2028.0 2756.0 1.00
fwhm2_norm[1] 0.026 0.000 0.026 0.026 0.000 0.000 4155.0 4852.0 1.00
fwhm2_norm[2] 2.195 0.023 2.151 2.237 0.000 0.000 2728.0 4441.0 1.00
velocity_norm[0] 0.627 0.001 0.626 0.628 0.000 0.000 2791.0 3841.0 1.00
velocity_norm[1] 0.375 0.000 0.375 0.375 0.000 0.000 5145.0 5413.0 1.00
velocity_norm[2] 0.746 0.001 0.743 0.749 0.000 0.000 2016.0 4059.0 1.00
TB_fwhm_norm[0] 0.148 0.005 0.139 0.158 0.000 0.000 1108.0 809.0 1.01
TB_fwhm_norm[1] 0.097 0.008 0.083 0.113 0.000 0.000 739.0 1554.0 1.02
TB_fwhm_norm[2] 0.764 0.005 0.754 0.774 0.000 0.000 1765.0 3306.0 1.00
tkin_factor_norm[0] 0.434 0.212 0.097 0.833 0.006 0.003 1073.0 818.0 1.00
tkin_factor_norm[1] 0.154 0.091 0.044 0.315 0.004 0.004 731.0 1351.0 1.02
tkin_factor_norm[2] 0.453 0.213 0.099 0.854 0.004 0.002 3272.0 3858.0 1.00
filling_factor[0] 0.669 0.231 0.261 1.000 0.004 0.002 3333.0 3019.0 1.00
filling_factor[1] 0.577 0.199 0.250 0.967 0.007 0.003 723.0 1407.0 1.02
filling_factor[2] 0.671 0.235 0.243 1.000 0.003 0.002 4633.0 3316.0 1.00
wt_ff_tkin[0] 0.003 0.000 0.003 0.004 0.000 0.000 624.0 333.0 1.02
wt_ff_tkin[1] 0.080 0.008 0.066 0.094 0.000 0.000 882.0 1692.0 1.01
wt_ff_tkin[2] 0.000 0.000 0.000 0.000 0.000 0.000 1561.0 1298.0 1.01
fwhm2[0] 32.188 1.038 30.349 34.284 0.023 0.013 2028.0 2756.0 1.00
fwhm2[1] 5.266 0.013 5.242 5.291 0.000 0.000 4155.0 4852.0 1.00
fwhm2[2] 439.012 4.592 430.204 447.420 0.088 0.052 2728.0 4441.0 1.00
log10_nHI[0] 0.006 1.502 -2.625 2.983 0.030 0.016 2591.0 4311.0 1.00
log10_nHI[1] -0.004 1.511 -2.695 2.946 0.023 0.018 4435.0 4228.0 1.00
log10_nHI[2] -0.110 1.471 -2.792 2.690 0.030 0.016 2428.0 4434.0 1.00
velocity[0] 5.076 0.029 5.024 5.132 0.001 0.000 2791.0 3841.0 1.00
velocity[1] -5.001 0.001 -5.003 -4.998 0.000 0.000 5145.0 5413.0 1.00
velocity[2] 9.837 0.060 9.726 9.950 0.001 0.001 2016.0 4059.0 1.00
log10_n_alpha[0] -6.013 1.009 -7.881 -4.138 0.028 0.017 1333.0 1164.0 1.01
log10_n_alpha[1] -5.999 1.009 -7.854 -4.038 0.016 0.011 4019.0 4748.0 1.00
log10_n_alpha[2] -6.060 0.957 -7.789 -4.209 0.019 0.010 2406.0 4808.0 1.00
TB_fwhm[0] 29.631 1.077 27.858 31.605 0.040 0.059 1108.0 809.0 1.01
TB_fwhm[1] 19.469 1.680 16.543 22.580 0.061 0.024 739.0 1554.0 1.02
TB_fwhm[2] 152.833 1.040 150.854 154.827 0.025 0.015 1765.0 3306.0 1.00
tkin[0] 309.585 150.276 56.278 576.770 4.071 1.771 1098.0 811.0 1.00
tkin[1] 24.983 9.251 14.790 41.329 0.365 0.425 733.0 1370.0 1.02
tkin[2] 4352.617 2049.009 922.855 8166.648 35.636 18.975 3267.0 3856.0 1.00
tspin[0] 301.464 144.863 55.614 557.850 3.802 1.663 1149.0 815.0 1.00
tspin[1] 24.946 9.215 14.834 41.290 0.348 0.420 732.0 1373.0 1.02
tspin[2] 3254.774 1744.473 399.499 6409.431 32.124 18.229 2687.0 3298.0 1.00
tau_total[0] 0.269 0.260 0.047 0.719 0.008 0.013 1397.0 1521.0 1.00
tau_total[1] 2.674 0.257 2.180 3.146 0.003 0.003 6137.0 5240.0 1.00
tau_total[2] 0.142 0.192 0.020 0.380 0.004 0.019 3263.0 4129.0 1.00
absorption_weight[0] 0.691 0.442 0.051 1.507 0.010 0.005 1400.0 1410.0 1.00
absorption_weight[1] 1.023 0.101 0.846 1.219 0.001 0.001 6111.0 5318.0 1.00
absorption_weight[2] 0.748 0.511 0.043 1.725 0.008 0.006 3271.0 4169.0 1.00
log10_Pth[0] 2.436 1.523 -0.408 5.303 0.030 0.017 2535.0 3980.0 1.00
log10_Pth[1] 1.372 1.513 -1.432 4.228 0.023 0.018 4366.0 4884.0 1.00
log10_Pth[2] 3.467 1.471 0.806 6.342 0.028 0.017 2683.0 4907.0 1.00
log10_NHI[0] 19.980 0.202 19.742 20.359 0.003 0.004 3820.0 3413.0 1.00
log10_NHI[1] 20.060 0.137 19.841 20.315 0.005 0.003 787.0 1469.0 1.01
log10_NHI[2] 20.687 0.213 20.468 21.091 0.003 0.005 4708.0 3382.0 1.00
log10_depth[0] 1.485 1.510 -1.392 4.247 0.030 0.016 2535.0 4475.0 1.00
log10_depth[1] 1.575 1.520 -1.386 4.317 0.023 0.018 4331.0 4159.0 1.00
log10_depth[2] 2.308 1.489 -0.548 5.034 0.030 0.017 2478.0 4579.0 1.00

We generate posterior predictive checks as well as a trace plot of the individual chains. In the posterior predictive plot, we show each chain as a different color. Each line is one posterior sample.

[19]:
posterior = model.sample_posterior_predictive(
    thin=10,  # keep one in {thin} posterior samples
)
axes = plot_predictive(model.data, posterior.posterior_predictive)
axes.ravel()[0].sharex(axes.ravel()[1])
Sampling: [absorption, emission]
../_images/notebooks_emission_absorption_model_28_3.png
[20]:
from bayes_spec.plots import plot_traces

_ = plot_traces(model.trace.posterior, model.cloud_freeRVs + model.baseline_freeRVs + model.hyper_freeRVs)
../_images/notebooks_emission_absorption_model_29_0.png

We can inspect the posterior distribution pair plots. First, the free parameters for all clouds. Keep an eye out for any strong degeneracies or non-linear correlations. If present, then these features can cause the posterior sampling to be inefficient. It may be worth re-parameterizing your model to remove these effects. Alternatively, increasing tune and target_accept can help.

[21]:
_ = plot_pair(
    model.trace.solution_0.sel(draw=slice(None, None, 10)), # samples
    model.cloud_freeRVs, # var_names to plot
    combine_dims=["cloud"], # concatenate clouds
    kind="scatter", # plot type
)
../_images/notebooks_emission_absorption_model_31_0.png

Notice that there are three posterior modes. These correspond to the three clouds of the model.

[22]:
_ = plot_pair(
    model.trace.solution_0.sel(draw=slice(None, None, 10)), # samples
    ["velocity", "fwhm2"], # var_names to plot
    combine_dims=None, # do not concatenate clouds
    kind="scatter", # plot type
)
../_images/notebooks_emission_absorption_model_33_0.png

We can plot the posterior distributions of the deterministic quantities for a single cloud.

[23]:
_ = plot_pair(
    model.trace.solution_0.sel(cloud=0, draw=slice(None, None, 10)), # samples
    model.cloud_freeRVs + model.hyper_freeRVs + model.baseline_freeRVs, # var_names to plot
    kind="scatter", # plot type
)
../_images/notebooks_emission_absorption_model_35_0.png
[24]:
_ = plot_pair(
    model.trace.solution_0.sel(cloud=1, draw=slice(None, None, 10)), # samples
    model.cloud_freeRVs + model.hyper_freeRVs + model.baseline_freeRVs, # var_names to plot
    kind="scatter", # plot type
)
../_images/notebooks_emission_absorption_model_36_0.png
[25]:
_ = plot_pair(
    model.trace.solution_0.sel(cloud=2, draw=slice(None, None, 10)), # samples
    model.cloud_freeRVs + model.hyper_freeRVs + model.baseline_freeRVs, # var_names to plot
    kind="scatter", # plot type
)
../_images/notebooks_emission_absorption_model_37_0.png

And the deterministic quantities. The red points represent the simulation parameters.

[26]:
_ = plot_pair(
    model.trace.solution_0.sel(draw=slice(None, None, 10)), # samples
    var_names, # var_names to plot
    combine_dims=["cloud"], # concatenate clouds
    labeller=model.labeller, # label manager
    kind="scatter", # plot type
    reference_values=sim_params, # truths
)
../_images/notebooks_emission_absorption_model_39_0.png

Notice that there are three posterior modes. These correspond to the three clouds of the model. We can plot the posterior distributions of the deterministic quantities for a single cloud.

[27]:
# identify simulation cloud corresponding to each posterior cloud
sim_cloud_map = {}
for i in range(n_clouds):
    posterior_velocity = model.trace.solution_0['velocity'].sel(cloud=i).data.mean()
    match = np.argmin(np.abs(sim_params["velocity"] - posterior_velocity))
    sim_cloud_map[i] = match
sim_cloud_map
[27]:
{0: np.int64(1), 1: np.int64(0), 2: np.int64(2)}
[28]:
print("cloud freeRVs", model.cloud_freeRVs)
print("cloud deterministics", model.cloud_deterministics)
print("hyper freeRVs", model.hyper_freeRVs)
print("hyper deterministics", model.hyper_deterministics)
cloud freeRVs ['fwhm2_norm', 'log10_nHI_norm', 'velocity_norm', 'log10_n_alpha_norm', 'TB_fwhm_norm', 'tkin_factor_norm', 'filling_factor', 'wt_ff_tkin']
cloud deterministics ['fwhm2', 'log10_nHI', 'velocity', 'log10_n_alpha', 'TB_fwhm', 'tkin', 'tspin', 'tau_total', 'absorption_weight', 'log10_Pth', 'log10_NHI', 'log10_depth']
hyper freeRVs []
hyper deterministics []
[29]:
cloud = 0

# subset of sim_params
my_sim_params = {}
for var_name in var_names:
    my_sim_params[var_name] = sim_params[var_name][sim_cloud_map[cloud]]

_ = plot_pair(
    model.trace.solution_0.sel(cloud=cloud, draw=slice(None, None, 10)), # samples
    var_names, # var_names to plot
    labeller=model.labeller, # label manager
    kind="scatter", # plot type
    reference_values=my_sim_params, # truths
)
../_images/notebooks_emission_absorption_model_43_0.png
[30]:
cloud = 1

# subset of sim_params
my_sim_params = {}
for var_name in var_names:
    my_sim_params[var_name] = sim_params[var_name][sim_cloud_map[cloud]]

_ = plot_pair(
    model.trace.solution_0.sel(cloud=cloud, draw=slice(None, None, 10)), # samples
    var_names, # var_names to plot
    labeller=model.labeller, # label manager
    kind="scatter", # plot type
    reference_values=my_sim_params, # truths
)
../_images/notebooks_emission_absorption_model_44_0.png
[31]:
cloud = 2

# subset of sim_params
my_sim_params = {}
for var_name in var_names:
    my_sim_params[var_name] = sim_params[var_name][sim_cloud_map[cloud]]

_ = plot_pair(
    model.trace.solution_0.sel(cloud=cloud, draw=slice(None, None, 10)), # samples
    var_names, # var_names to plot
    labeller=model.labeller, # label manager
    kind="scatter", # plot type
    reference_values=my_sim_params, # truths
)
../_images/notebooks_emission_absorption_model_45_0.png

Finally, we can get the posterior statistics, Bayesian Information Criterion (BIC), etc.

[32]:
point_stats = az.summary(model.trace.solution_0, kind="stats", hdi_prob=0.68)
print("BIC:", model.bic())
display(point_stats)
BIC: -1322.9379586390694
mean sd hdi_16% hdi_84%
baseline_emission_norm[0] -0.234 0.092 -0.322 -0.141
baseline_absorption_norm[0] 0.172 0.148 0.016 0.309
log10_nHI_norm[0] 0.004 1.001 -0.969 1.032
log10_nHI_norm[1] -0.002 1.007 -1.025 0.960
log10_nHI_norm[2] -0.074 0.981 -1.147 0.761
log10_n_alpha_norm[0] -0.013 1.009 -0.938 1.074
log10_n_alpha_norm[1] 0.001 1.009 -1.012 0.978
log10_n_alpha_norm[2] -0.060 0.957 -1.049 0.832
fwhm2_norm[0] 0.161 0.005 0.156 0.166
fwhm2_norm[1] 0.026 0.000 0.026 0.026
fwhm2_norm[2] 2.195 0.023 2.173 2.218
velocity_norm[0] 0.627 0.001 0.626 0.628
velocity_norm[1] 0.375 0.000 0.375 0.375
velocity_norm[2] 0.746 0.001 0.744 0.747
TB_fwhm_norm[0] 0.148 0.005 0.143 0.151
TB_fwhm_norm[1] 0.097 0.008 0.088 0.105
TB_fwhm_norm[2] 0.764 0.005 0.759 0.769
tkin_factor_norm[0] 0.434 0.212 0.158 0.599
tkin_factor_norm[1] 0.154 0.091 0.054 0.172
tkin_factor_norm[2] 0.453 0.213 0.162 0.620
filling_factor[0] 0.669 0.231 0.564 1.000
filling_factor[1] 0.577 0.199 0.328 0.754
filling_factor[2] 0.671 0.235 0.576 1.000
wt_ff_tkin[0] 0.003 0.000 0.003 0.004
wt_ff_tkin[1] 0.080 0.008 0.070 0.086
wt_ff_tkin[2] 0.000 0.000 0.000 0.000
fwhm2[0] 32.188 1.038 31.184 33.200
fwhm2[1] 5.266 0.013 5.253 5.279
fwhm2[2] 439.012 4.592 434.568 443.582
log10_nHI[0] 0.006 1.502 -1.453 1.548
log10_nHI[1] -0.004 1.511 -1.538 1.440
log10_nHI[2] -0.110 1.471 -1.721 1.142
velocity[0] 5.076 0.029 5.046 5.103
velocity[1] -5.001 0.001 -5.002 -5.000
velocity[2] 9.837 0.060 9.777 9.895
log10_n_alpha[0] -6.013 1.009 -6.938 -4.926
log10_n_alpha[1] -5.999 1.009 -7.012 -5.022
log10_n_alpha[2] -6.060 0.957 -7.049 -5.168
TB_fwhm[0] 29.631 1.077 28.543 30.230
TB_fwhm[1] 19.469 1.680 17.503 21.100
TB_fwhm[2] 152.833 1.040 151.827 153.854
tkin[0] 309.585 150.276 108.635 419.805
tkin[1] 24.983 9.251 15.469 26.431
tkin[2] 4352.617 2049.009 1549.440 5951.917
tspin[0] 301.464 144.863 104.366 404.395
tspin[1] 24.946 9.215 15.473 26.403
tspin[2] 3254.774 1744.473 971.164 4375.793
tau_total[0] 0.269 0.260 0.053 0.271
tau_total[1] 2.674 0.257 2.403 2.906
tau_total[2] 0.142 0.192 0.025 0.132
absorption_weight[0] 0.691 0.442 0.092 0.869
absorption_weight[1] 1.023 0.101 0.909 1.100
absorption_weight[2] 0.748 0.511 0.103 0.948
log10_Pth[0] 2.436 1.523 0.921 3.964
log10_Pth[1] 1.372 1.513 -0.066 2.912
log10_Pth[2] 3.467 1.471 1.922 4.788
log10_NHI[0] 19.980 0.202 19.759 20.031
log10_NHI[1] 20.060 0.137 19.882 20.130
log10_NHI[2] 20.687 0.213 20.471 20.713
log10_depth[0] 1.485 1.510 0.103 3.127
log10_depth[1] 1.575 1.520 0.046 3.027
log10_depth[2] 2.308 1.489 0.992 3.895
[ ]: