Advanced Topic: Using External C++ Functions

This is based on the relevant portion of the CmdStan documentation here

Consider the following Stan model, based on the bernoulli example.

[2]:
from cmdstanpy import CmdStanModel
model_external = CmdStanModel(stan_file='bernoulli_external.stan', compile=False)
print(model_external.code())
functions {
  real make_odds(real theta);
}
data {
  int<lower=0> N;
  array[N] int<lower=0, upper=1> y;
}
parameters {
  real<lower=0, upper=1> theta;
}
model {
  theta ~ beta(1, 1); // uniform prior on interval 0, 1
  y ~ bernoulli(theta);
}
generated quantities {
  real odds;
  odds = make_odds(theta);
}

As you can see, it features a function declaration for make_odds, but no definition. If we try to compile this, we will get an error.

[3]:
model_external.compile()
16:02:06 - cmdstanpy - INFO - compiling stan file /home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.stan to exe file /home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-3-ca7a518eb77d> in <module>
----> 1 model_external.compile()

~/Dev/py/cmdstanpy/cmdstanpy/model.py in compile(self, force, stanc_options, cpp_options, user_header, override_options)
    561                         "If the issue persists please open a bug report"
    562                     )
--> 563                 raise ValueError(
    564                     f"Failed to compile Stan model '{self._stan_file}'. "
    565                     f"Console:\n{console}"

ValueError: Failed to compile Stan model '/home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.stan'. Console:

--- Translating Stan model to C++ code ---
bin/stanc  --o=/home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.hpp /home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.stan
Semantic error in '/home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.stan', line 2, column 7 to column 16:
   -------------------------------------------------
     1:  functions {
     2:    real make_odds(real theta);
                ^
     3:  }
     4:  data {
   -------------------------------------------------

Function 'make_odds' is declared without specifying a definition.
make: *** [make/program:50: /home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.hpp] Error 1

Command ['make', '/home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external']
        error during processing No such file or directory

Even enabling the --allow-undefined flag to stanc3 will not allow this model to be compiled quite yet.

[4]:
model_external.compile(stanc_options={'allow-undefined':True})
16:02:10 - cmdstanpy - INFO - compiling stan file /home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.stan to exe file /home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-4-604c5622a59a> in <module>
----> 1 model_external.compile(stanc_options={'allow-undefined':True})

~/Dev/py/cmdstanpy/cmdstanpy/model.py in compile(self, force, stanc_options, cpp_options, user_header, override_options)
    561                         "If the issue persists please open a bug report"
    562                     )
--> 563                 raise ValueError(
    564                     f"Failed to compile Stan model '{self._stan_file}'. "
    565                     f"Console:\n{console}"

ValueError: Failed to compile Stan model '/home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.stan'. Console:

--- Translating Stan model to C++ code ---
bin/stanc --allow-undefined --o=/home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.hpp /home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.stan

--- Compiling, linking C++ code ---
g++ -std=c++1y -pthread -D_REENTRANT -Wno-sign-compare -Wno-ignored-attributes      -I stan/lib/stan_math/lib/tbb_2020.3/include    -O3 -I src -I stan/src -I stan/lib/rapidjson_1.1.0/ -I lib/CLI11-1.9.1/ -I stan/lib/stan_math/ -I stan/lib/stan_math/lib/eigen_3.4.0 -I stan/lib/stan_math/lib/boost_1.78.0 -I stan/lib/stan_math/lib/sundials_6.1.1/include -I stan/lib/stan_math/lib/sundials_6.1.1/src/sundials    -DBOOST_DISABLE_ASSERTS          -c -Wno-ignored-attributes   -include /home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/user_header.hpp -x c++ -o /home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.o /home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.hpp
/home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.hpp: In member function ‘void bernoulli_external_model_namespace::bernoulli_external_model::write_array_impl(RNG&, VecR&, VecI&, VecVar&, bool, bool, std::ostream*) const’:
/home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.hpp:156:14: error: there are no arguments to ‘make_odds’ that depend on a template parameter, so a declaration of ‘make_odds’ must be available [-fpermissive]
  156 |       odds = make_odds(theta, pstream__);
      |              ^~~~~~~~~
/home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.hpp:156:14: note: (if you use ‘-fpermissive’, G++ will accept your code, but allowing the use of an undeclared name is deprecated)
/home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.hpp: In instantiation of ‘void bernoulli_external_model_namespace::bernoulli_external_model::write_array_impl(RNG&, VecR&, VecI&, VecVar&, bool, bool, std::ostream*) const [with RNG = boost::random::additive_combine_engine<boost::random::linear_congruential_engine<unsigned int, 40014, 0, 2147483563>, boost::random::linear_congruential_engine<unsigned int, 40692, 0, 2147483399> >; VecR = Eigen::Matrix<double, -1, 1>; VecI = std::vector<int>; VecVar = Eigen::Matrix<double, -1, 1>; stan::require_vector_like_vt<std::is_floating_point, VecR>* <anonymous> = 0; stan::require_vector_like_vt<std::is_integral, VecI>* <anonymous> = 0; stan::require_vector_vt<std::is_floating_point, VecVar>* <anonymous> = 0; std::ostream = std::basic_ostream<char>]’:
/home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.hpp:275:21:   required from ‘void bernoulli_external_model_namespace::bernoulli_external_model::write_array(RNG&, Eigen::Matrix<double, -1, 1>&, Eigen::Matrix<double, -1, 1>&, bool, bool, std::ostream*) const [with RNG = boost::random::additive_combine_engine<boost::random::linear_congruential_engine<unsigned int, 40014, 0, 2147483563>, boost::random::linear_congruential_engine<unsigned int, 40692, 0, 2147483399> >; std::ostream = std::basic_ostream<char>]’
stan/src/stan/model/model_base_crtp.hpp:140:61:   required from ‘void stan::model::model_base_crtp<M>::write_array(boost::random::ecuyer1988&, Eigen::VectorXd&, Eigen::VectorXd&, bool, bool, std::ostream*) const [with M = bernoulli_external_model_namespace::bernoulli_external_model; boost::random::ecuyer1988 = boost::random::additive_combine_engine<boost::random::linear_congruential_engine<unsigned int, 40014, 0, 2147483563>, boost::random::linear_congruential_engine<unsigned int, 40692, 0, 2147483399> >; Eigen::VectorXd = Eigen::Matrix<double, -1, 1>; std::ostream = std::basic_ostream<char>]’
stan/src/stan/model/model_base_crtp.hpp:136:8:   required from here
/home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.hpp:156:23: error: ‘make_odds’ was not declared in this scope
  156 |       odds = make_odds(theta, pstream__);
      |              ~~~~~~~~~^~~~~~~~~~~~~~~~~~
/home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.hpp: In instantiation of ‘void bernoulli_external_model_namespace::bernoulli_external_model::write_array_impl(RNG&, VecR&, VecI&, VecVar&, bool, bool, std::ostream*) const [with RNG = boost::random::additive_combine_engine<boost::random::linear_congruential_engine<unsigned int, 40014, 0, 2147483563>, boost::random::linear_congruential_engine<unsigned int, 40692, 0, 2147483399> >; VecR = std::vector<double, std::allocator<double> >; VecI = std::vector<int>; VecVar = std::vector<double, std::allocator<double> >; stan::require_vector_like_vt<std::is_floating_point, VecR>* <anonymous> = 0; stan::require_vector_like_vt<std::is_integral, VecI>* <anonymous> = 0; stan::require_vector_vt<std::is_floating_point, VecVar>* <anonymous> = 0; std::ostream = std::basic_ostream<char>]’:
/home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.hpp:291:21:   required from ‘void bernoulli_external_model_namespace::bernoulli_external_model::write_array(RNG&, std::vector<double, std::allocator<double> >&, std::vector<int>&, std::vector<double, std::allocator<double> >&, bool, bool, std::ostream*) const [with RNG = boost::random::additive_combine_engine<boost::random::linear_congruential_engine<unsigned int, 40014, 0, 2147483563>, boost::random::linear_congruential_engine<unsigned int, 40692, 0, 2147483399> >; std::ostream = std::basic_ostream<char>]’
stan/src/stan/model/model_base_crtp.hpp:202:61:   required from ‘void stan::model::model_base_crtp<M>::write_array(boost::random::ecuyer1988&, std::vector<double, std::allocator<double> >&, std::vector<int>&, std::vector<double, std::allocator<double> >&, bool, bool, std::ostream*) const [with M = bernoulli_external_model_namespace::bernoulli_external_model; boost::random::ecuyer1988 = boost::random::additive_combine_engine<boost::random::linear_congruential_engine<unsigned int, 40014, 0, 2147483563>, boost::random::linear_congruential_engine<unsigned int, 40692, 0, 2147483399> >; std::ostream = std::basic_ostream<char>]’
stan/src/stan/model/model_base_crtp.hpp:198:8:   required from here
/home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.hpp:156:23: error: ‘make_odds’ was not declared in this scope
make: *** [make/program:58: /home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external] Error 1

Command ['make', 'STANCFLAGS+=--allow-undefined', '/home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external']
        error during processing No such file or directory

To resolve this, we need to both tell the Stan compiler an undefined function is okay and let C++ know what it should be.

We can provide a definition in a C++ header file by using the user_header argument to either the CmdStanModel constructor or the compile method.

This will enables the allow-undefined flag automatically.

[5]:
model_external.compile(user_header='make_odds.hpp')
assert model_external.exe_file is not None
16:03:06 - cmdstanpy - INFO - compiling stan file /home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external.stan to exe file /home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external
16:03:25 - cmdstanpy - INFO - compiled model executable: /home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/bernoulli_external

We can then run this model and inspect the output

[6]:
fit = model_external.sample(data={'N':10, 'y':[0,1,0,0,0,0,0,0,0,1]})
fit.stan_variable('odds')
16:03:31 - cmdstanpy - INFO - CmdStan start processing
chain 1 |          | 00:00 Status


chain 1 |██████████| 00:00 Sampling completed
chain 2 |██████████| 00:00 Sampling completed
chain 3 |██████████| 00:00 Sampling completed
chain 4 |██████████| 00:00 Sampling completed


16:03:31 - cmdstanpy - INFO - CmdStan done processing.

[6]:
array([0.28554 , 0.395692, 0.38404 , ..., 0.581201, 0.406603, 0.244602])

The contents of this header file are presented without comment:

#include <ostream>

double make_odds(const double& theta, std::ostream *pstream__) {
  return theta / (1 - theta);
}

Additional guidance, including on writing functions with known derivatives, can be found in the CmdStan documentation.