Use Rcpp function as rstan density function

I have read some documentation but I am still confused, due to my limited knowledge of C++

I have an Rcpp function

I would like to go from

//This returns a probability density value [between 0 and 1] that I would need to log
.Call("_coga_dcoga_approx", PACKAGE = "coga", x, a, b)

to

y ~ coga_dcoga_approx(a, b)

Do I have to add an header to the function/cpp file? Could someone please provide an example with my function?

// [[Rcpp::export]]
NumericVector dcoga_approx(NumericVector x,
			   NumericVector shape,
			   NumericVector rate) 

Thanks a lot!

I tired to do this

> stan_model(file = "inst/stan/hierarchical.stan", allow_undefined = TRUE,
+            includes = paste0('\n#include "', 
+                              file.path(getwd(), 'src/approxcoga.cpp'), '"\n'))

With this dummy model

functions{
  vector pcoga_approx(vector x, vector shape, vector rate);
}
data {
  
	// Reference matrix inference
	int<lower=0> G;
	int<lower=0> S;
	int CL; // counts linear size
  int C;

  matrix[S, C+1] prop_template;

	// reference counts
 	int<lower=0> counts_linear[CL] ;

	// Non-centered param
	real lambda_mu_prior[2];
	real lambda_sigma_prior[2];
	real lambda_skew_prior[2];
	real sigma_intercept_prior[2];
	
	// vector[CL] exposure_rate;

}
transformed data{
  vector[3] x = [1,1,1]';
  vector[3] shape = [1,2,3]';
  vector[3] rate = [3,2,1]';
  
  print(pcoga_approx(x, shape, rate));
  
}
parameters {

// 	// Global properties
// 	real<offset=lambda_mu_prior[1],multiplier=lambda_mu_prior[2]>lambda_mu;
//   real<offset=lambda_sigma_prior[1],multiplier=lambda_sigma_prior[2]> lambda_sigma;
//   real<offset=lambda_skew_prior[1],multiplier=lambda_skew_prior[2]> lambda_skew;
// 
// 	// Sigma
// 	real<upper=0> sigma_slope;
// 	real<lower=0> sigma_sigma;
//   real<offset=sigma_intercept_prior[1],multiplier=sigma_intercept_prior[2]> sigma_intercept;

  // Local properties of the data
  matrix[C+1,G] lambda_log;
  matrix[C+1,G] sigma_inv_log;
  
  // vector[C+1] proportion;


}

model {

//   // Overall properties of the data
//   lambda_mu ~ normal(lambda_mu_prior[1],lambda_mu_prior[2]);
// 	lambda_sigma ~ normal(lambda_sigma_prior[1],lambda_sigma_prior[2]);
// 	lambda_skew ~ normal(lambda_skew_prior[1],lambda_skew_prior[2]);

  // sigma_intercept ~ normal(0,2);
  // sigma_slope ~ normal(0,2);
  // sigma_sigma ~ normal(0,2);


	// Means overdispersion reference
	for(c in 1:(C+1)) lambda_log[c] ~ normal(8, 3); // skew_normal(lambda_mu, exp(lambda_sigma), lambda_skew);
	for(c in 1:(C+1)) sigma_inv_log[c] ~ normal(0, 3); //~ normal(sigma_slope * lambda_log + sigma_intercept, sigma_sigma);

  counts_linear ~ neg_binomial_2( to_vector(prop_template * exp(lambda_log)), 1.0 ./ exp(to_vector(sigma_inv_log)));

}



And I get this error

> stan_model(file = "inst/stan/hierarchical.stan", allow_undefined = TRUE,
+            includes = paste0('\n#include "', 
+                              file.path(getwd(), 'src/approxcoga.cpp'), '"\n'))
Error in compileCode(f, code, language = language, verbose = verbose) : 
  Compilation ERROR, function(s)/method(s) not created! In file included from /stornext/Home/data/allstaff/m/mangiola.s/R/x86_64-pc-linux-gnu-library/4.0/RcppEigen/include/Eigen/Core:383,
                 from /stornext/Home/data/allstaff/m/mangiola.s/R/x86_64-pc-linux-gnu-library/4.0/RcppEigen/include/Eigen/Dense:1,
                 from /stornext/Home/data/allstaff/m/mangiola.s/R/x86_64-pc-linux-gnu-library/4.0/StanHeaders/include/stan/math/prim/mat/fun/Eigen.hpp:13,
                 from <command-line>:
/stornext/Home/data/allstaff/m/mangiola.s/R/x86_64-pc-linux-gnu-library/4.0/RcppEigen/include/Eigen/src/Core/arch/SSE/PacketMath.h:60:39: warning: ignoring attributes on template argument ‘__m128’ {aka ‘__vector(4) float’} [-Wignored-attributes]
   60 | template<> struct is_arithmetic<__m128>  { enum { value = true }; };
      |                                       ^
/stornext/Home/data/allstaff/m/mangiola.s/R/x86_64-pc-linux-gnu-library/4.0/RcppEigen/include/Eigen/s
In addition: Warning message:
In system(cmd, intern = !verbose) :
  running command '/stornext/System/data/apps/R/R-4.0.2/lib64/R/bin/R CMD SHLIB file2f7328568eee.cpp 2> file2f7328568eee.cpp.err.txt' had status 1
Error in sink(type = "output") : invalid connection
> 

If I include the cpp script in src and add this line in stan_meta_header.hpp

#include <../src/approxcoga.cpp>

I also get error

I am not exactly sure what the NumericVector represents as the underlying type in RCpp.

This is a table showing what the underlying C++ types are for the basic Stan types.

Stan type Data - C++ type Parameter - C++ type
int int N/A
real double var
vector Eigen::Matrix<double, -1, 1> Eigen::Matrix<var, -1, 1>
row_vector Eigen::Matrix<double, 1, -1> Eigen::Matrix<var, 1, -1>
matrix Eigen::Matrix<double, -1, -1> Eigen::Matrix<var, -1, -1>
T[] std::vector< C++ type of T > std::vector< C++ type of T >

So you could write your function

vector pcoga_approx(vector x, vector shape, vector rate);

as

Eigen::Matrix<double, -1, 1> pcoga_approx(Eigen::Matrix<double, -1, 1> x, Eigen::Matrix<double, -1, 1> shape, Eigen::Matrix<double, -1, 1> rate) {
  Eigen::Matrix<stan::math::double, -1, 1> res;
  // ...
  return res;
}

but only if you know all variables will be data. If you know all variables will be parameter you could also write

Eigen::Matrix<stan::math::var, -1, 1> pcoga_approx(Eigen::Matrix<stan::math::var, -1, 1> x, Eigen::Matrix<stan::math::var, -1, 1> shape, Eigen::Matrix<stan::math::var, -1, 1> rate) {
  Eigen::Matrix<stan::math::var, -1, 1> res;
  // ...
  return res;
}

but if you want to make a general solution you need a templated signature. This is where it gets fun.

template <typename T0, typename T1, typename T2>
Eigen::Matrix<typename boost::math::tools::promote_args<T0, T1, T2>, -1, 1>
pcoga_approx(Eigen::Matrix<T0, -1, 1> x, Eigen::Matrix<T1, -1, 1> shape, Eigen::Matrix<T2, -1, 1> rate) {
  Eigen::Matrix<typename boost::math::tools::promote_args<T0, T1, T2>, -1, 1> res;
  // ...
  return res;
}

But then all the C++ functions you call inside, need to be templated as well. So basically all functions in the file need to be templated, no specific types. This is so that Stan can do autodifferentiation.

In most cases you want to write the functions with templates so that they can be used for both data and parameters. But if you know its only going to be data, then its waaaaaaay easier to not worry about templates and just use doubles.
But given that you want to use it in a sampling statement, that is not the case here.

The main question to ask is:

Does the pcoga_approx use any special code that would be hard to rewrite in Stan? Otherwise it might be easier to just rewrite the function in Stan. Especially if you are not well versed in C++ templating.

Also, if you wish to use it as y ~ pcoga_approx(); the function needs to have a _lpdf or _lpmf suffix. It might be easier to write it as target += pcoga_approx();.

2 Likes

To me, its easier to start from a non-toy example that works.
An example for C++ implementations for

vector solve_quadprog(matrix a, vector b, matrix c, vector d, matrix e, vector f);
vector solve_quadprog_chol(matrix a, vector b, matrix c, vector d, matrix e, vector f);

we did with @spinkney based on initial work from @charlesm93 and @shoshievass is here.

Maybe this will help you.

2 Likes

Wow OK it’s involved. I was naively thinking that one function that outputs a log probability was enough to stan.

likelihood = function_that_outputs_between_0_and_1();
target += log(likelihood);

I will try if I can transform all to stan, but it’s like 300 lines (if I’m not missing some other library)

// [[Rcpp::interfaces(r, cpp)]]
#include <Rcpp.h>

using namespace Rcpp;


// [[Rcpp::export]]
double get_mu(NumericVector alpha, NumericVector beta) {
  double beta1 = min(beta);
  double result = 0;
  int n = alpha.size();
  
  for(int i = 0; i < n; ++i) {
    result += (beta[i] / beta1) * (1 - (beta1 / beta[i])) * alpha[i];
  }

  return result;
}


// [[Rcpp::export]]
double get_mu2(NumericVector alpha, NumericVector beta) {
  double beta1 = min(beta);
  double result = 0;
  int n = alpha.size();
  
  for(int i = 0; i < n; ++i) {
    result += pow(beta[i] / beta1, 2) * (1 - (beta1 / beta[i])) * alpha[i];
  }

  return result;
}

// [[Rcpp::export]]
double get_mu3(NumericVector alpha, NumericVector beta) {
  double beta1 = min(beta);
  double result = 0;
  int n = alpha.size();
  
  for(int i = 0; i < n; ++i) {
    result += pow(beta[i] / beta1, 3) * (1 - (beta1 / beta[i]))
      * (2 - beta1 / beta[i]) * alpha[i];
  }

  return result;
}

// [[Rcpp::export]]
double get_A(double mu, double mu2, double mu3) {
  double result = pow(mu * mu3 - 3 * pow(mu2, 2), 2);
  result /= mu * pow(mu2, 3);
  result -= 2;
  return result;
}

// [[Rcpp::export]]
double get_p_GNB(double A) {
  double result = 1 - (A / 2) + pow(pow(A, 2) / 4 - 1, 0.5);
  return result;
}

// [[Rcpp::export]]
double get_b_GNB(double p, double mu, double mu2) {
  double cart = mu * (1 - p) / mu2;
  double result = (1 - pow(cart, 0.5)) / p;
  return result;
}

// [[Rcpp::export]]
double get_r_GNB(double mu, double p, double b) {
  double result = mu * (1 - p * b) / p;
  return result;
}

// [[Rcpp::export]]
double get_p_NB(double mu, double mu2) {
  double result = 1 - mu / mu2;
  return result;
}

// [[Rcpp::export]]
double get_r_NB(double p, double mu) {
  double result = (1 - p) * mu / p;
  return result;
}

// [[Rcpp::export]]
double GNB(double k, double r, double p, double b, double mu) {
  // input check
  if (p < 0 || p > 1 || (p * b) > 1 || (p * b) < -1) warning("out of control");
  //if (b < 1 && b > 0) stop("GNB do not exit");
  if(k >= mu && (r + b * mu) < 0) return 0;

  double result = r / (r  + b * k) * R::choose(r + b * k, k);
  result *= pow(p, k) * pow(1 - p, r + b * k - k);
  return result;
}

// [[Rcpp::export]]
double NB(double k, double r, double p) {
  double result = r / (r + k) * R::choose(r + k, k);
  result *= pow(p, k) * pow(1 - p, r);
  return result;
}

double get_rho_approx(NumericVector alpha) {
  int n = alpha.size();
  double out = 0;
  for(int i = 0; i < n; ++i) {
    out += alpha[i];
  }
  return out;
}

// [[Rcpp::export]]
double dcoga_approx_nv(double x, NumericVector alpha, NumericVector beta) {
  double mu = get_mu(alpha, beta);
  double mu2 = get_mu2(alpha, beta);
  double mu3 = get_mu3(alpha, beta);
  double valueA = get_A(mu, mu2, mu3);
  double rho = get_rho_approx(alpha);
  double beta1 = min(beta);
  // double cart = exp(- x / beta1);
  double step = 0;

  double result = 0;
  int k = 0;

  if (valueA < 2) {
    double valuep = get_p_NB(mu, mu2);
    double valuer = get_r_NB(valuep, mu);
    while(TRUE) {
      step = NB(k, valuer, valuep);
      step *= R::dgamma(x, rho + k, beta1, 0);
      // step /= pow(beta1, rho + k);
      // step *= pow(x, rho + k - 1);
      // step /= exp(R::lgammafn(rho + k));
      if(step == R_PosInf || R_IsNaN(step)) {
	warning("Inf or NaN happened, not converge!");
	break;
      }
      if(step == 0) break;
      result += step;
      k++;
    }
  } else {
    double valuep = get_p_GNB(valueA);
    double valueb = get_b_GNB(valuep, mu, mu2);
    double valuer = get_r_GNB(mu, valuep, valueb);
    while(TRUE) {
      step = GNB(k, valuer, valuep, valueb, mu);
      step *= R::dgamma(x, rho + k, beta1, 0);
      // step /= pow(beta1, rho + k);
      // step *= pow(x, rho + k - 1);
      // step /= exp(R::lgammafn(rho + k));
      if(step == R_PosInf || R_IsNaN(step)) {
	warning("Inf or NaN happened, not converge!");
	break;
      }
      if(step == 0) break;
      result += step;
      k++;
    }
  }

  return result;
}

// do recycling x to follw y's size
// the same function as recycling with diff name
NumericVector recycling2(NumericVector x, NumericVector y) {
  // recycling x to y
  int nx = x.size();
  int ny = y.size();
  int mx = x.size();
  while(TRUE) {
    for(int i = 0; i < nx; ++i) {
      x.push_back(x[i]);
      mx = x.size();
      if (mx == ny) break;
    }
    if (mx == ny) break;
  }
  return x;
}



//' Convolution of Gamma distribuitons (Approximation Method)
//'
//' Density and distribution function of convolution of gamma distributions
//' are calculated based on approximation method from Barnabani(2017), which
//' gives us the approximate result and faster evaluation than \code{dcoga}
//' and \code{pcoga} during three or more variables case. **So, we recommend
//' these functions for three or more varibales case with approximate result.**
//'
//' @param x Quantiles.
//' @param shape Numerical vector of shape parameters for each gamma distributions,
//' all shape parameters should be larger than or equal to 0, with at least three
//' non-zero.
//' @param rate Numerical vector of rate parameters for each gamma distributions,
//' all rate parameters should be larger than 0.
//'
//' @references
//' Barnabani, M. (2017). An approximation to the convolution of gamma
//' distributions. Communications in Statistics - Simulation and Computation
//' 46(1), 331-343.
//'
//' @examples
//' ## Example 1: Correctness check
//' set.seed(123)
//' ## do grid
//' y <- rcoga(100000, c(3,4,5), c(2,3,4))
//' grid <- seq(0, 15, length.out=100)
//' ## calculate pdf and cdf
//' pdf <- dcoga_approx(grid, shape=c(3,4,5), rate=c(2,3,4))
//' cdf <- pcoga_approx(grid, shape=c(3,4,5), rate=c(2,3,4))
//'
//' ## plot pdf
//' plot(density(y), col="blue")
//' lines(grid, pdf, col="red")
//'
//' ## plot cdf
//' plot(ecdf(y), col="blue")
//' lines(grid, cdf, col="red")
//'
//' ## Example 2: Show parameter recycling
//' ## these pairs give us the same results
//' dcoga_approx(1:5, c(1, 2), c(1, 3, 4, 2, 5))
//' dcoga_approx(1:5, c(1, 2, 1, 2, 1), c(1, 3, 4, 2, 5))
//'
//' pcoga_approx(1:5, c(1, 3, 5, 2, 2), c(3, 5))
//' pcoga_approx(1:5, c(1, 3, 5, 2, 2), c(3, 5, 3, 5, 3))
//'
//' @author Chaoran Hu
//'
//' @export
// [[Rcpp::export]]
NumericVector dcoga_approx(NumericVector x,
			   NumericVector shape,
			   NumericVector rate) {
  // input check
  if (is_true(any(rate <= 0))) stop("all rate should be larger than 0.");
  if (is_true(any(shape < 0))) stop("all shape should be larger than or equal to 0, with at least three non-zero.");
  // handle recycle
  if (shape.size() != rate.size()) {
    if (shape.size() < rate.size()) {
      if (rate.size() % shape.size() != 0) warning("number of rate is not a multiple of shape.");
      shape = recycling2(shape, rate);
    } else {
      if (shape.size() % rate.size() != 0) warning("number of shape is not a multiple of rate.");
      rate = recycling2(rate, shape);
    }
  }
  rate = rate[shape > 0];
  shape = shape[shape > 0];
  if (shape.size() < 3) stop("all shape should be larger than or equal to 0, with at least three non-zero.");
  // input check

  NumericVector beta = 1 / rate;
  int n = x.size();
  NumericVector result(n);
  for (int i = 0; i < n; ++i) {
    result[i] = dcoga_approx_nv(x[i], shape, beta);
  }
  return result;
}


////////////////////////////////////////////


// [[Rcpp::export]]
double pcoga_approx_nv(double x, NumericVector alpha, NumericVector beta) {
  double mu = get_mu(alpha, beta);
  double mu2 = get_mu2(alpha, beta);
  double mu3 = get_mu3(alpha, beta);
  double valueA = get_A(mu, mu2, mu3);
  double rho = get_rho_approx(alpha);
  double beta1 = min(beta);

  double step = 0;
  double result = 0;
  int k = 0;

  if (valueA < 2) {
    double valuep = get_p_NB(mu, mu2);
    double valuer = get_r_NB(valuep, mu);
    while(TRUE) {
      step = NB(k, valuer, valuep);
      step *= R::pgamma(x / beta1, rho + k, 1, 1, 0);
      if(step == R_PosInf || R_IsNaN(step)) {
	warning("Inf or NaN happened, not converge!");
	break;
      }
      if(step == 0) break;
      result += step;
      k++;
    }
  } else {
    double valuep = get_p_GNB(valueA);
    double valueb = get_b_GNB(valuep, mu, mu2);
    double valuer = get_r_GNB(mu, valuep, valueb);
    while(TRUE) {
      step = GNB(k, valuer, valuep, valueb, mu);
      step *= R::pgamma(x / beta1, rho + k, 1, 1, 0);
      if(step == R_PosInf || R_IsNaN(step)) {
	warning("Inf or NaN happened, not converge!");
	break;
      }
      if(step == 0) break;
      result += step;
      k++;
    }
  }

  return result;
}

//' @rdname dcoga_approx
//' @export
// [[Rcpp::export]]
NumericVector pcoga_approx(NumericVector x,
			   NumericVector shape,
			   NumericVector rate) {
  // input check
  if (is_true(any(rate <= 0))) stop("all rate should be larger than 0.");
  if (is_true(any(shape < 0))) stop("all shape should be larger than or equal to 0, with at least three non-zero.");
  // handle recycle
  if (shape.size() != rate.size()) {
    if (shape.size() < rate.size()) {
      if (rate.size() % shape.size() != 0) warning("number of rate is not a multiple of shape.");
      shape = recycling2(shape, rate);
    } else {
      if (shape.size() % rate.size() != 0) warning("number of shape is not a multiple of rate.");
      rate = recycling2(rate, shape);
    }
  }
  rate = rate[shape > 0];
  shape = shape[shape > 0];
  if (shape.size() < 3) stop("all shape should be larger than or equal to 0, with at least three non-zero.");
  // input check
  
  NumericVector beta = 1 / rate;
  int n = x.size();
  NumericVector result(n);
  for (int i = 0; i < n; ++i) {
    result[i] = pcoga_approx_nv(x[i], shape, beta);
  }
  return result;
}

Yeah its a bit more involved as Stan’s sampler requires the gradient for all parameter inputs of the function as well.

And the easiest way is to properly template all arguments and functions. In that case Stan’s backend can obtain the gradient with the Stan Math autodiff.

The other way is providing a manual gradient (which also involves C++).