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()
14:52: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
14:52:06 - cmdstanpy - ERROR - Stan program failed to compile:
14:52:06 - cmdstanpy - WARNING -
--- 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 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})
14:52: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
14:52:07 - cmdstanpy - ERROR - Stan program failed to compile:
14:52:07 - cmdstanpy - WARNING -
--- 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++ -march=native -mtune=native -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 lib/rapidjson_1.1.0/ -I lib/CLI11-1.9.1/ -I stan/lib/stan_math/ -I stan/lib/stan_math/lib/eigen_3.3.9 -I stan/lib/stan_math/lib/boost_1.75.0 -I stan/lib/stan_math/lib/sundials_6.0.0/include -I stan/lib/stan_math/lib/sundials_6.0.0/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
cc1plus: fatal error: /home/brian/Dev/py/cmdstanpy/docsrc/users-guide/examples/user_header.hpp: No such file or directory
compilation terminated.
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
14:52:07 - 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
14:52: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')
14:52:25 - 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


14:52:25 - cmdstanpy - INFO - CmdStan done processing.

[6]:
array([0.20418  , 0.27078  , 0.614841 , ..., 0.124882 , 0.0797855,
       0.165672 ])

The contents of this header file are a bit complicated unless you are familiar with the C++ internals of Stan, so they are presented without comment:

#include <boost/math/tools/promotion.hpp>
#include <ostream>

namespace bernoulli_model_namespace {
    template <typename T0__>  inline  typename
          boost::math::tools::promote_args<T0__>::type
          make_odds(const T0__& theta, std::ostream* pstream__) {
            return theta / (1 - theta);
       }
}