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()
INFO:cmdstanpy:compiling stan file /home/docs/checkouts/readthedocs.org/user_builds/cmdstanpy/checkouts/v1.0.0rc2/docsrc/examples/bernoulli_external.stan to exe file /home/docs/checkouts/readthedocs.org/user_builds/cmdstanpy/checkouts/v1.0.0rc2/docsrc/examples/bernoulli_external
ERROR:cmdstanpy:Stan program failed to compile:
WARNING:cmdstanpy:
--- Translating Stan model to C++ code ---
bin/stanc  --o=/home/docs/checkouts/readthedocs.org/user_builds/cmdstanpy/checkouts/v1.0.0rc2/docsrc/examples/bernoulli_external.hpp /home/docs/checkouts/readthedocs.org/user_builds/cmdstanpy/checkouts/v1.0.0rc2/docsrc/examples/bernoulli_external.stan
Semantic error in '/home/docs/checkouts/readthedocs.org/user_builds/cmdstanpy/checkouts/v1.0.0rc2/docsrc/examples/bernoulli_external.stan', line 2, column 2 to column 29:
   -------------------------------------------------
     1:  functions {
     2:    real make_odds(real theta);
           ^
     3:  }
     4:  data {
   -------------------------------------------------

Some function is declared without specifying a definition.
make: *** [make/program:50: /home/docs/checkouts/readthedocs.org/user_builds/cmdstanpy/checkouts/v1.0.0rc2/docsrc/examples/bernoulli_external.hpp] Error 1

Command ['make', '/home/docs/checkouts/readthedocs.org/user_builds/cmdstanpy/checkouts/v1.0.0rc2/docsrc/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})
INFO:cmdstanpy:compiling stan file /home/docs/checkouts/readthedocs.org/user_builds/cmdstanpy/checkouts/v1.0.0rc2/docsrc/examples/bernoulli_external.stan to exe file /home/docs/checkouts/readthedocs.org/user_builds/cmdstanpy/checkouts/v1.0.0rc2/docsrc/examples/bernoulli_external

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')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/tmp/ipykernel_27088/728714853.py in <module>
----> 1 model_external.compile(user_header='make_odds.hpp')

~/checkouts/readthedocs.org/user_builds/cmdstanpy/checkouts/v1.0.0rc2/cmdstanpy/model.py in compile(self, force, stanc_options, cpp_options, user_header, override_options)
    368                 user_header=user_header,
    369             )
--> 370             compiler_options.validate()
    371
    372             if compiler_options != self._compiler_options:

~/checkouts/readthedocs.org/user_builds/cmdstanpy/checkouts/v1.0.0rc2/cmdstanpy/compiler_opts.py in validate(self)
    110         self.validate_stanc_opts()
    111         self.validate_cpp_opts()
--> 112         self.validate_user_header()
    113
    114     def validate_stanc_opts(self) -> None:

~/checkouts/readthedocs.org/user_builds/cmdstanpy/checkouts/v1.0.0rc2/cmdstanpy/compiler_opts.py in validate_user_header(self)
    184                 and os.path.isfile(self._user_header)
    185             ):
--> 186                 raise ValueError(
    187                     f"User header file {self._user_header} cannot be found"
    188                 )

ValueError: User header file make_odds.hpp cannot be found

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')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/tmp/ipykernel_27088/4268543152.py in <module>
----> 1 fit = model_external.sample(data={'N':10, 'y':[0,1,0,0,0,0,0,0,0,1]})
      2 fit.stan_variable('odds')

~/checkouts/readthedocs.org/user_builds/cmdstanpy/checkouts/v1.0.0rc2/cmdstanpy/model.py in sample(self, data, chains, parallel_chains, threads_per_chain, seed, chain_ids, inits, iter_warmup, iter_sampling, save_warmup, thin, max_treedepth, metric, step_size, adapt_engaged, adapt_delta, adapt_init_phase, adapt_metric_window, adapt_step_size, fixed_param, output_dir, sig_figs, save_latent_dynamics, save_profile, show_progress, show_console, refresh, time_fmt, force_one_process_per_chain)
    920         )
    921         with MaybeDictToFilePath(data, inits) as (_data, _inits):
--> 922             args = CmdStanArgs(
    923                 self._name,
    924                 self._exe_file,

~/checkouts/readthedocs.org/user_builds/cmdstanpy/checkouts/v1.0.0rc2/cmdstanpy/cmdstan_args.py in __init__(self, model_name, model_exe, chain_ids, method_args, data, seed, inits, output_dir, sig_figs, save_latent_dynamics, save_profile, refresh)
    744             self.method = Method.VARIATIONAL
    745         self.method_args.validate(len(chain_ids) if chain_ids else None)
--> 746         self.validate()
    747
    748     def validate(self) -> None:

~/checkouts/readthedocs.org/user_builds/cmdstanpy/checkouts/v1.0.0rc2/cmdstanpy/cmdstan_args.py in validate(self)
    758             raise ValueError('no stan model specified')
    759         if self.model_exe is None:
--> 760             raise ValueError('model not compiled')
    761
    762         if self.chain_ids is not None:

ValueError: model not compiled

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);
       }
}