ALMA Data Example

Trey V. Wenger (c) December 2024

Here we test bayes_cn_hfs on some real ALMA data and demonstrate a general procedure for determining the carbon isotopic ratio from observations of CN and 13CN.

[1]:
# General imports
import os
import pickle
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__)

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

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

# Notebook configuration
pd.options.display.max_rows = None
pymc version: 5.19.1
bayes_spec version: 1.7.2
bayes_cn_hfs version: 1.1.1+2.g81bbbdf.dirty

Load the data

[2]:
from bayes_spec import SpecData

data_12CN = np.genfromtxt("data_CN.tsv")
data_13CN = np.genfromtxt("data_13CN.tsv")

# estimate noise
noise_12CN = 1.4826 * np.median(np.abs(data_12CN[:, 0] - np.median(data_12CN[:, 0])))
noise_13CN = 1.4826 * np.median(np.abs(data_13CN[:, 0] - np.median(data_13CN[:, 0])))

obs_12CN = SpecData(
    1000.0 * data_12CN[:, 0],
    data_12CN[:, 1],
    noise_12CN,
    xlabel=r"LSRK Frequency (MHz)",
    ylabel=r"$T_{B,\,\rm CN}$ (K)",
)
obs_13CN = SpecData(
    1000.0 * data_13CN[:, 0],
    data_13CN[:, 1],
    noise_13CN,
    xlabel=r"LSRK Frequency (MHz)",
    ylabel=r"$T_{B,\,^{13}\rm CN}$ (K)",
)
data = {"12CN": obs_12CN, "13CN": obs_13CN}

for label, dataset in data.items():
    print(label, len(dataset.spectral))
    # HACK: normalize data by noise
    dataset._brightness_offset = np.median(dataset.brightness)
    dataset._brightness_scale = dataset.noise

# subset of 12CN data
data_12CN = {
    label: data[label]
    for label in data.keys() if "12CN" in label
}

# Plot the data
fig, axes = plt.subplots(2, layout="constrained")
axes[0].plot(data["12CN"].spectral, data["12CN"].brightness, 'k-')
axes[1].plot(data["13CN"].spectral, data["13CN"].brightness, 'k-')
axes[0].set_xlabel(data["12CN"].xlabel)
axes[1].set_xlabel(data["13CN"].xlabel)
axes[0].set_ylabel(data["12CN"].ylabel)
_ = axes[1].set_ylabel(data["13CN"].ylabel)
12CN 1021
13CN 1021
../_images/notebooks_alma_data_3_1.png

Inspecting the CN data

Can we constrain the excitation temperature and optical depth? How about the hyperfine anomalies? It seems like there are two cloud components, so let’s explore the data with that assumption for now. We assume make the weak LTE assumption such that the kinetic temperature is equal to the mean cloud excitation temperature. We are otherwise unable to constrain the kinetic temperature because non-thermal broadening is important and we have poor spectral resolution.

[73]:
from bayes_cn_hfs.cn_model import CNModel
from bayes_cn_hfs import supplement_mol_data

mol_data_12CN, mol_weight_12CN = supplement_mol_data("CN")

n_clouds = 6
baseline_degree = 0
model = CNModel(
    data_12CN,
    molecule="CN", # molecule name
    mol_data=mol_data_12CN, # molecular data
    bg_temp = 2.7, # assumed background temperature (K)
    Beff=1.0, # Main beam efficiency
    Feff=1.0, # Forward efficiency
    n_clouds=n_clouds,
    baseline_degree=baseline_degree,
    seed=1234,
    verbose=True
)
model.add_priors(
    prior_log10_N = [14.0, 0.25], # mean and width of log10 total column density prior (cm-2)
    prior_log10_Tkin = None, # ignored because kinetic temperature is fixed
    prior_velocity = [-3.0, 5.0], # mean and width of velocity prior (km/s)
    prior_fwhm_nonthermal = 1.0, # width of non-thermal broadening prior (km/s)
    prior_fwhm_L = 1.0, # width of latent Lorentzian FWHM prior (km/s) TIP: set to typical feature separation
    prior_rms = None, # do not infer spectral rms
    prior_baseline_coeffs = None, # use default baseline priors
    assume_LTE = False, # do not assume LTE
    prior_log10_Tex = [0.75, 0.1], # mean and width of excitation temperature prior (K)
    assume_CTEX = False, # do not assume CTEX
    prior_LTE_precision = 1000.0, # width of LTE precision prior
    fix_log10_Tkin = 1.5, # fix the kinetic temperature (K)
    ordered = False, # do not assume optically-thin
)
model.add_likelihood()
[74]:
from bayes_spec.plots import plot_predictive

# prior predictive check
prior = model.sample_prior_predictive(
    samples=100,  # prior predictive samples
)
_ = plot_predictive(model.data, prior.prior_predictive)
Sampling: [12CN, LTE_precision, baseline_12CN_norm, fwhm_L_norm, fwhm_nonthermal_norm, log10_N_norm, log10_Tex_ul_norm, velocity_norm, weights]
../_images/notebooks_alma_data_6_1.png
[77]:
start = time.time()
model.fit(
    n = 20_000, # maximum number of VI iterations
    draws = 1_000, # number of posterior samples
    rel_tolerance = 0.01, # VI relative convergence threshold
    abs_tolerance = 0.05, # VI absolute convergence threshold
    learning_rate = 0.02, # VI learning rate
)
end = time.time()
print(f"Runtime: {(end-start)/60.0:.2f} minutes")
Finished [100%]: Average Loss = 533.67
Runtime: 2.68 minutes
[78]:
# ignore transition and state dependent parameters
var_names = [
    param for param in model.cloud_deterministics
    if not set(model.model.named_vars_to_dims[param]).intersection(set(["transition", "state"]))
]
pm.summary(model.trace.posterior, var_names=var_names + model.hyper_freeRVs + model.baseline_freeRVs)
arviz - WARNING - Shape validation failed: input_shape: (1, 1000), minimum_shape: (chains=2, draws=4)
[78]:
mean sd hdi_3% hdi_97% mcse_mean mcse_sd ess_bulk ess_tail r_hat
velocity[0] -2.841 0.020 -2.876 -2.804 0.001 0.000 872.0 907.0 NaN
velocity[1] -6.163 0.003 -6.169 -6.156 0.000 0.000 1221.0 944.0 NaN
velocity[2] -7.379 0.006 -7.391 -7.370 0.000 0.000 695.0 861.0 NaN
velocity[3] -4.291 0.005 -4.299 -4.282 0.000 0.000 1099.0 981.0 NaN
velocity[4] -2.317 0.004 -2.325 -2.311 0.000 0.000 999.0 832.0 NaN
velocity[5] -2.790 0.005 -2.799 -2.781 0.000 0.000 1007.0 919.0 NaN
fwhm_thermal[0] 0.236 0.000 0.236 0.236 0.000 0.000 1000.0 1000.0 NaN
fwhm_thermal[1] 0.236 0.000 0.236 0.236 0.000 0.000 1000.0 1000.0 NaN
fwhm_thermal[2] 0.236 0.000 0.236 0.236 0.000 0.000 1000.0 1000.0 NaN
fwhm_thermal[3] 0.236 0.000 0.236 0.236 0.000 0.000 1000.0 1000.0 NaN
fwhm_thermal[4] 0.236 0.000 0.236 0.236 0.000 0.000 1000.0 1000.0 NaN
fwhm_thermal[5] 0.236 0.000 0.236 0.236 0.000 0.000 1000.0 1000.0 NaN
fwhm_nonthermal[0] 8.264 0.038 8.183 8.326 0.001 0.001 932.0 967.0 NaN
fwhm_nonthermal[1] 1.540 0.008 1.527 1.556 0.000 0.000 982.0 870.0 NaN
fwhm_nonthermal[2] 2.482 0.010 2.461 2.500 0.000 0.000 907.0 983.0 NaN
fwhm_nonthermal[3] 1.618 0.007 1.606 1.631 0.000 0.000 1048.0 942.0 NaN
fwhm_nonthermal[4] 2.077 0.009 2.062 2.094 0.000 0.000 1063.0 973.0 NaN
fwhm_nonthermal[5] 7.238 0.007 7.227 7.251 0.000 0.000 956.0 983.0 NaN
fwhm[0] 8.267 0.038 8.187 8.330 0.001 0.001 932.0 967.0 NaN
fwhm[1] 1.558 0.008 1.545 1.574 0.000 0.000 982.0 870.0 NaN
fwhm[2] 2.493 0.010 2.473 2.511 0.000 0.000 907.0 983.0 NaN
fwhm[3] 1.636 0.007 1.623 1.648 0.000 0.000 1048.0 942.0 NaN
fwhm[4] 2.091 0.009 2.075 2.107 0.000 0.000 1063.0 973.0 NaN
fwhm[5] 7.242 0.007 7.230 7.254 0.000 0.000 956.0 983.0 NaN
log10_N[0] 14.005 0.002 14.003 14.009 0.000 0.000 977.0 872.0 NaN
log10_N[1] 13.503 0.002 13.499 13.507 0.000 0.000 1001.0 1015.0 NaN
log10_N[2] 13.659 0.001 13.657 13.662 0.000 0.000 939.0 990.0 NaN
log10_N[3] 13.385 0.002 13.381 13.389 0.000 0.000 971.0 826.0 NaN
log10_N[4] 13.751 0.001 13.748 13.753 0.000 0.000 1035.0 942.0 NaN
log10_N[5] 13.897 0.000 13.897 13.898 0.000 0.000 1101.0 942.0 NaN
log10_Tex_ul[0] 0.814 0.087 0.654 0.986 0.003 0.002 947.0 906.0 NaN
log10_Tex_ul[1] 0.515 0.054 0.418 0.618 0.002 0.001 1052.0 992.0 NaN
log10_Tex_ul[2] 0.503 0.062 0.390 0.621 0.002 0.001 948.0 896.0 NaN
log10_Tex_ul[3] 0.865 0.083 0.713 1.016 0.003 0.002 1024.0 983.0 NaN
log10_Tex_ul[4] 0.605 0.070 0.478 0.735 0.002 0.002 996.0 940.0 NaN
log10_Tex_ul[5] 0.891 0.093 0.708 1.065 0.003 0.002 934.0 975.0 NaN
tau_total[0] 2.209 0.016 2.178 2.237 0.000 0.000 986.0 941.0 NaN
tau_total[1] 1.727 0.017 1.696 1.756 0.001 0.000 1053.0 912.0 NaN
tau_total[2] 3.334 0.013 3.308 3.357 0.000 0.000 1059.0 867.0 NaN
tau_total[3] 0.367 0.005 0.358 0.377 0.000 0.000 961.0 1005.0 NaN
tau_total[4] 2.665 0.044 2.580 2.743 0.001 0.001 1064.0 1071.0 NaN
tau_total[5] -2.528 0.008 -2.543 -2.515 0.000 0.000 1012.0 941.0 NaN
fwhm_L_norm 0.025 0.008 0.012 0.040 0.000 0.000 955.0 779.0 NaN
baseline_12CN_norm[0] -1.471 0.034 -1.531 -1.403 0.001 0.001 1009.0 982.0 NaN
[79]:
posterior = model.sample_posterior_predictive(
    thin=100, # keep one in {thin} posterior samples
)
_ = plot_predictive(model.data, posterior.posterior_predictive)
Sampling: [12CN]
../_images/notebooks_alma_data_9_3.png

Number of cloud components

[80]:
from bayes_spec import Optimize

max_n_clouds = 10
baseline_degree = 0
opt = Optimize(
    CNModel,
    data_12CN,
    molecule="CN", # molecule name
    mol_data=mol_data_12CN, # molecular data
    bg_temp = 2.7, # assumed background temperature (K)
    Beff=1.0, # Main beam efficiency
    Feff=1.0, # Forward efficiency
    max_n_clouds=max_n_clouds,
    baseline_degree=baseline_degree,
    seed=1234,
    verbose=True
)
opt.add_priors(
    prior_log10_N = [14.0, 0.25], # mean and width of log10 total column density prior (cm-2)
    prior_log10_Tkin = None, # ignored because kinetic temperature is fixed
    prior_velocity = [-3.0, 5.0], # mean and width of velocity prior (km/s)
    prior_fwhm_nonthermal = 1.0, # width of non-thermal broadening prior (km/s)
    prior_fwhm_L = 1.0, # width of latent Lorentzian FWHM prior (km/s) TIP: set to typical feature separation
    prior_rms = None, # do not infer spectral rms
    prior_baseline_coeffs = None, # use default baseline priors
    assume_LTE = False, # do not assume LTE
    prior_log10_Tex = [0.75, 0.1], # mean and width of excitation temperature prior (K)
    assume_CTEX = False, # do not assume CTEX
    prior_LTE_precision = 1000.0, # width of LTE precision prior
    fix_log10_Tkin = 1.5, # fix the kinetic temperature (K)
    ordered = False, # do not assume optically-thin
)
opt.add_likelihood()
[81]:
from bayes_spec.plots import plot_predictive

# prior predictive check
prior = opt.models[1].sample_prior_predictive(
    samples=100,  # prior predictive samples
)
_ = plot_predictive(opt.models[1].data, prior.prior_predictive)
Sampling: [12CN, LTE_precision, baseline_12CN_norm, fwhm_L_norm, fwhm_nonthermal_norm, log10_N_norm, log10_Tex_ul_norm, velocity_norm, weights]
../_images/notebooks_alma_data_12_1.png

Approximate all models with variational inference.

[82]:
start = time.time()
fit_kwargs = {
    "n": 20_000,
    "rel_tolerance": 0.01,
    "abs_tolerance": 0.05,
    "learning_rate": 0.02,
}
opt.fit_all(**fit_kwargs)
end = time.time()
print(f"Runtime: {(end-start)/60.0:.2f} minutes")
Null hypothesis BIC = 9.837e+05
Approximating n_cloud = 1 posterior...
Finished [100%]: Average Loss = 25,926
n_cloud = 1 BIC = 5.952e+04

Approximating n_cloud = 2 posterior...
Finished [100%]: Average Loss = 14,433
n_cloud = 2 BIC = 3.122e+04

Approximating n_cloud = 3 posterior...
Finished [100%]: Average Loss = 6,979.5
n_cloud = 3 BIC = 1.077e+04

Approximating n_cloud = 4 posterior...
Finished [100%]: Average Loss = 5,383.5
n_cloud = 4 BIC = 6.806e+03

Approximating n_cloud = 5 posterior...
Finished [100%]: Average Loss = 1,669.4
n_cloud = 5 BIC = 1.830e+03

Approximating n_cloud = 6 posterior...
Finished [100%]: Average Loss = 533.67
n_cloud = 6 BIC = -1.968e+00

Approximating n_cloud = 7 posterior...
Finished [100%]: Average Loss = 1,500.3
n_cloud = 7 BIC = 1.939e+03

Approximating n_cloud = 8 posterior...
Finished [100%]: Average Loss = 343.3
n_cloud = 8 BIC = -3.888e+02

Approximating n_cloud = 9 posterior...
Finished [100%]: Average Loss = -361.63
n_cloud = 9 BIC = -1.662e+03

Approximating n_cloud = 10 posterior...
Finished [100%]: Average Loss = -521.79
n_cloud = 10 BIC = -1.525e+03

Runtime: 25.33 minutes
[83]:
null_bic = opt.models[1].null_bic()
n_clouds = np.arange(max_n_clouds+1)
bics_vi = np.array([null_bic] + [model.bic(chain=0) for model in opt.models.values()])
print(bics_vi)

plt.plot(n_clouds[1:], bics_vi[1:], 'ko')
plt.xlabel("Number of Clouds")
_ = plt.ylabel("BIC")
[ 9.83688523e+05  5.95200559e+04  3.12208929e+04  1.07687710e+04
  6.80598056e+03  1.82960053e+03 -1.96767126e+00  1.93949664e+03
 -3.88770599e+02 -1.66173758e+03 -1.52473788e+03]
../_images/notebooks_alma_data_15_1.png

The strange baseline structure makes it too difficult to identify the optimal number of clouds.