Tutorial

[1]:
from __future__ import annotations

import getdist as gd
import matplotlib.pyplot as plt
import numpy as np

import mchammers

plt.style.use('light_mode.mplstyle')

Generate Artificial Data

[2]:
# set seed for reproducibility
np.random.seed(42)

# true parameter values
m = 1.0
b = 0.5

# number of data points
N = 20

# generate some synthetic data
xdata = np.random.rand(N)
ydata = m*xdata + b + np.random.normal(0, 0.1, N)
yerr  = 0.1
cov   = np.eye(N)*yerr**2 # diagonal covariance matrix for now
[3]:
plt.errorbar(xdata, ydata, yerr=yerr, fmt='o')
plt.xlabel("$x$")
plt.ylabel("$y$")
plt.show()
../_images/notebooks_tutorial_4_0.png

Running the Sampler

[4]:
# we specify the parameter names, their initial values, and their prior ranges
par_names = ['m', 'b']
par_defaults = [5.0, 0.1]
par_priors   = [(-10,10), (-10,10)]

par_defaults_dict = dict(zip(par_names, par_defaults))
par_priors_dict = dict(zip(par_names, par_priors))

# we write our model function f(x) = mx + b
def model(x, theta):
    """
    Theory model for the data.

    Parameters
    ----------
    x : array_like
        The x values at which the model is evaluated.
    theta : shape[num_dims] - array_like
        The model parameters.

    Returns
    -------
    y : array_like
        The model evaluated at x.
    """
    m, b = theta
    return m*x + b

We define our log probability function which will be fed into the mchammers.Sampler class.

[5]:
# - - - - - - - - - - - - - - - - - -
#          Log Likelihood
# - - - - - - - - - - - - - - - - - -

def log_likelihood(walker_state,
                    ydata, xdata, cov,
                    ):
    """
    This is the likelihood function for the MCMC sampler

    Arguments:
    -----------------
    walker_state: np.ndarray - shape(num_walkers, num_dims) - the current state of the walker
    ydata: np.ndarray - 1d - measured f(x) values
    xdata: np.ndarray - 1d - measured x values
    cov: np.ndarray - 2d - covariance matrix for errors in ydata

    Returns:
    -----------------
    log_likelihood: np.ndarray - shape(num_walkers, num_dims) - the log likelihood of the walker state
    """

    log_likelihood = np.zeros(walker_state.shape[0])
    for i in range(walker_state.shape[0]):
        μ = model(xdata, walker_state[i])
        diff = ydata - μ
        log_likelihood[i] = -0.5 * np.dot(diff, np.linalg.solve(cov, diff))

    return log_likelihood

# - - - - - - - - - - - - - - - - - -
#             Log Prior
# - - - - - - - - - - - - - - - - - -

def log_prior(walker_state):
    """
    MCMC samplers require a known prior for sampling
    This is the simplest case of a uniform flat prior

    Arguments:
    -----------------
    walker_state: np.ndarray - shape(num_walkers, num_dims) - the current state of the walker

    Returns:
    -----------------
    log_prior: np.ndarray - shape(num_walkers, num_dims) - the log prior of the walker state
    """

    log_prior = np.zeros(walker_state.shape[0])
    for i in range(walker_state.shape[0]):
        conditions = []
        for j, key in enumerate(par_priors_dict.keys()):
            conditions.append(par_priors_dict[key][0] < walker_state[i][j] < par_priors_dict[key][1])

        if np.all(conditions):
            log_prior[i] = 0.0
        else:
            log_prior[i] = -np.inf

    return log_prior


# - - - - - - - - - - - - - - - - - -
#          Log Probability
# - - - - - - - - - - - - - - - - - -

def log_probability(walker_state, ydata, xdata, cov):
    """
    This is the log probability function for the MCMC sampler

    Arguments:
    -----------------
    walker_state: np.ndarray - shape(num_walkers, num_dims) - the current state of the walker

    Returns:
    -----------------
    log_probability: np.ndarray - shape(num_walkers, num_dims) - the log probability of the walker state
    """

    lp = log_prior(walker_state)
    return lp + log_likelihood(walker_state, ydata, xdata, cov)

We first run the basic Metropolis-Hastings MCMC Sampler.

[15]:
NUM_STEP = 5000
NUM_WALKER = 16
NUM_DIM = len(par_defaults)

#initial state of walkers
initial_state = np.array(par_defaults) + np.random.normal(0, 0.1, (NUM_WALKER, NUM_DIM))

sampler = mchammers.SamplerBasic(
        num_step=NUM_STEP,
        num_walker=NUM_WALKER,
        num_dim=NUM_DIM,
        prior_bounds=par_priors,
        state_init=initial_state,
        log_prob_func=log_probability,
        args=[ydata, xdata, cov],
        std_rel_prop=0.004,
        flatten=False
    )

sampler.run()

We then run the Affine-Invariant Ensemble Sampler

[16]:
NUM_STEP = 5000
NUM_WALKER = 16
NUM_DIM = len(par_defaults)

#initial state of walkers
initial_state = np.array(par_defaults) + np.random.normal(0, 0.1, (NUM_WALKER, NUM_DIM))

sampler_stretch = mchammers.SamplerStretch(
        num_step=NUM_STEP,
        num_walker=NUM_WALKER,
        num_dim=NUM_DIM,
        prior_bounds=par_priors,
        state_init=initial_state,
        log_prob_func=log_probability,
        args=[ydata, xdata, cov],
        a=2,  # a=2 is implemented in GW10
        frac_burn=0.2,
        flatten=False
    )

sampler_stretch.run()
/Users/jamessunseri/Desktop/APC_524/Final_Project/mchammers/src/mchammers/hammer.py:500: RuntimeWarning: overflow encountered in exp
  q = (self.Z ** (self.num_dim - 1)) * np.exp(log_prob_prop - log_prob_curr)
[36]:
f, axes = plt.subplots(NUM_DIM, 1, figsize=(8, 8), sharex=True)
for i in range(NUM_DIM):
    mean_of_chains = np.mean(sampler.samples[::15, :, i], axis=1)
    std_of_chains = np.std(sampler.samples[::15, :, i], axis=1)
    axes[i].fill_between(range(len(mean_of_chains)),
                         mean_of_chains - std_of_chains,
                         mean_of_chains + std_of_chains,
                         alpha=0.5, color='dodgerblue')
    axes[i].plot(range(len(mean_of_chains)), mean_of_chains, color='dodgerblue')

    mean_of_chains = np.mean(sampler_stretch.samples[::15, :, i], axis=1)
    std_of_chains = np.std(sampler_stretch.samples[::15, :, i], axis=1)
    axes[i].fill_between(range(len(mean_of_chains)),
                         mean_of_chains - std_of_chains,
                         mean_of_chains + std_of_chains,
                         alpha=0.5, color='coral')
    axes[i].plot(range(len(mean_of_chains)), mean_of_chains, color='coral')

    axes[i].axhline([m, b][i], color='k', linestyle='--')
    axes[i].set_ylabel(f"${par_names[i]}$")
    axes[i].set_xlabel("step")
    axes[i].set_xlim(range(len(mean_of_chains))[0], range(len(mean_of_chains))[-1])

plt.tight_layout()

../_images/notebooks_tutorial_13_0.png

We note that our chains seem to be slightly biased in slope \(m\). We are not sure of why this is the case at the moment but with more time we can understand this in the future. Likely due to the way we generated fake data.

Plot Distribution

[34]:
samples_base_gd = gd.MCSamples(samples = sampler.samples[::15], # thinning
                          labels = ['m', 'b'],
                            names = ['m', 'b']
)

samples_stretch_gd = gd.MCSamples(samples = sampler_stretch.samples[::15], # thinning
                          labels = ['m', 'b'],
                            names = ['m', 'b']
)
Removed no burn in
Removed no burn in
[35]:
# Triangle plot

width = height = 15
g = gd.plots.get_subplot_plotter(width_inch=width)
g.settings.axes_labelsize = 36 ; g.settings.axes_fontsize = 32 ; g.settings.legend_fontsize = 32


##############################
#        CORNER PLOT
##############################
g.triangle_plot([
    samples_base_gd,
    samples_stretch_gd,
],
contour_colors=[
    'dodgerblue',
    'coral',
],
filled = True,
line_args=[
    {'ls':'-', 'lw':4, 'color':'dodgerblue'},
    {'ls':'--', 'lw':4, 'color':'coral'},
],
legend_labels=[
    'Metropolis-Hastings',
    'Affine-Invariant',
],
markers={'m':1.0, 'b':0.5}, marker_args={'lw': 4, 'color': 'black'},
)


../_images/notebooks_tutorial_17_0.png

Plot with Data

[44]:
par_means = np.mean(sampler.samples[::15].reshape(-1, NUM_DIM), axis=0)
par_stds = np.std(sampler.samples[::15].reshape(-1, NUM_DIM), axis=0)

print(f"m = {par_means[0]:0.2f} +- {par_stds[0]:0.2f}")
print(f"b = {par_means[1]:0.2f} +- {par_stds[1]:0.2f}")
m = 0.95 +- 0.07
b = 0.50 +- 0.04
[49]:
plt.errorbar(xdata, ydata, yerr=yerr, fmt='o', color='k')
plt.plot(xdata, model(xdata, [m, b]), label="Truth", color='green')
plt.plot(xdata, model(xdata, par_means), label="Fit", color='dodgerblue')
plt.xlabel("$x$")
plt.ylabel("$y$")
plt.legend()
plt.show()
../_images/notebooks_tutorial_20_0.png
[ ]: