def aux_var_model(f, K, sn, g=None):
    """Implementation of auxiliary variable model inside sds sampling method

    Reference: http://papers.nips.cc/paper/4114-slice-sampling-covariance-hyperparameters-of-latent-gaussian-models.pdf

    P(f|g,theta) = N(f; m_theta_g, R_theta_g)

    Parameters
    ----------
    f: ndarray with shape (n_samples,)
        latent samples
    K: ndarray with shape (n_samples, n_samples)
        covariance of training samples
    sn: float
        noise
    g: ndarray with shape (n_samples,)
        auxiliary values with P(g|theta) = N(g; 0, Sigma_theta + S_theta)
    """
    n = K.shape[0]
    Kii = np.diagonal(K)
    K_ii_inv = 1./(Kii)
    v_1 = (sn**2)**(-1) + K_ii_inv        # v-1 = variance of posterior P(f|L, theta) according to Laplace Approximation
    Sii = 1./(v_1 - K_ii_inv)
    S = np.zeros_like(K)
    np.fill_diagonal(S, Sii)
    S = np.maximum(S, 0.)

    if g is None:
        # g = np.dot(tools.jitchol(K+S), np.random.normal(size=(n,)))
        g = np.random.multivariate_normal(f, S, 1).T.reshape((n,))

    L = tools.jitchol(K+S)
    V = np.linalg.solve(L, K)           # V = L-1 * K, V.T*V = K.T * (K+S)-1 * K
    R_theta = K - np.dot(V.T, V)
    # R_theta = K - np.dot(np.dot(K, np.linalg.inv(K+S)), K)

    # LS = np.linalg.cholesky(S)
    # beta = tools.solve_chol(LS.T, g)    # beta = S-1 * g
    # m_theta_g = np.dot(R_theta, beta)
    m_theta_g = np.dot(np.dot(R_theta, np.linalg.inv(S)), g)
    chol_R_theta = tools.jitchol(R_theta+np.eye(n)*1e-11)

    return g, K+S, m_theta_g, chol_R_theta, L
def inf_mcmc(f, model, ys=0):
    """Inference of fs|f

    Parameters
    ----------
    f: ndarray with shape (n_samples, n_mcmc_iters)
        latent samples from MCMC
    model: GP instance
        Trained Gaussian process model, which has x, y, mean, cov & llk func's
    ys: ndarray with shape (n_samples,)
        testing target, for the purpose of computing log-likelihood
    """
    x = model.x
    y = model.y
    xs= model.xs
    my = np.mean(y)
    n_samples = f.shape[1]
    ns  = xs.shape[0]

    n, D = x.shape
    m = np.tile(model.meanfunc.getMean(x), (1, n_samples))
    K = model.covfunc.getCovMatrix(x=x, mode='train')
    sn2   = model.likfunc.sn**2.
    L     = tools.jitchol(K/sn2+np.eye(n)).T
    alpha = tools.solve_chol(L, f-m)/sn2            # np.dot(Sigma**(-1), f-m)
    sW    = np.ones((n, 1))/np.sqrt(sn2)

    Ltril = np.all(np.tril(L, -1) == 0)                         # is L an upper triangular matrix?
    kss = model.covfunc.getCovMatrix(z=xs, mode='self_test')    # this only contains the diagonal terms
    Ks  = model.covfunc.getCovMatrix(x=x, z=xs, mode='cross')

    ms  = model.meanfunc.getMean(xs)
    Fmu = np.tile(ms, (1, n_samples)) + np.dot(Ks.T, alpha)     # conditional mean fs|f

    if Ltril: # L is triangular => use Cholesky parameters (alpha, sW, L)
        V   = np.linalg.solve(L.T, np.tile(sW, (1, ns))*Ks)                     # Ks has shape (n, ns),
        fs2 = kss - np.array([(V*V).sum(axis=0)]).T
    else:     # L is not triangular => use alternative parametrization
        fs2 = kss + np.array([(Ks*np.dot(L, Ks)).sum(axis=0)]).T

    # variance can only be >= 0
    Fs2 = np.maximum(fs2, 0)
    # Fs2 = np.tile(fs2, (1, n_samples))
    Fmu = np.mean(Fmu, axis=1, keepdims=True)

    Ymu, Lower, Upper = model.likfunc.evaluate(mu=Fmu, s2=Fs2)
    ym  = np.reshape(np.mean(Ymu, axis=1), (ns, 1)) + my
    ys_lw = np.reshape(np.mean(Lower, axis=1), (ns, 1)) + my
    ys_up = np.reshape(np.mean(Upper, axis=1), (ns, 1)) + my

    return ym, ys_lw, ys_up, Fs2