Skip to content

Commit

Permalink
Support for USER_HEADER in build system (#32)
Browse files Browse the repository at this point in the history
* Support for USER_HEADER

* Clean up
  • Loading branch information
WardBrian committed May 7, 2024
1 parent 70a1f64 commit 0bea9b9
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 17 deletions.
50 changes: 35 additions & 15 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## include paths
# include paths
TINYSTAN_ROOT ?= .

# user customization
Expand All @@ -10,29 +10,32 @@ STANC ?= $(TINYSTAN_ROOT)/bin/stanc$(EXE)
MATH ?= $(STAN)lib/stan_math/
RAPIDJSON ?= $(STAN)lib/rapidjson_1.1.0/

## required C++ includes
# required C++ includes
INC_FIRST ?= -I $(STAN)src -I $(RAPIDJSON)

# TinyStan always wants multithreading support
STAN_THREADS=true

## makefiles needed for math library
# We can bump to C++17, even if Stan hasn't yet
CXXFLAGS_LANG ?= -std=c++17

# makefiles needed for math library
include $(MATH)make/compiler_flags
include $(MATH)make/libraries
include $(MATH)make/dependencies

## Set -fPIC globally since we're always building a shared library
# Set -fPIC globally since we're always building a shared library
override CXXFLAGS += -fPIC -fvisibility=hidden -fvisibility-inlines-hidden
override CXXFLAGS_SUNDIALS += -fPIC
override CPPFLAGS += -DTINYSTAN_EXPORT

ifeq ($(OS),Windows_NT)
CXXFLAGS += -Wa,-mbig-obj
override CXXFLAGS += -Wa,-mbig-obj
endif

## set flags for stanc compiler (math calls MIGHT? set STAN_OPENCL)
ifdef STAN_OPENCL
STANCFLAGS += --use-opencl
# set flags for stanc compiler
override STANCFLAGS += --use-opencl
STAN_FLAG_OPENCL=_opencl
else
STAN_FLAG_OPENCL=
Expand All @@ -50,25 +53,41 @@ $(TINYSTAN_O) : $(TINYSTAN_DEPS)
$(COMPILE.cpp) $(OUTPUT_OPTION) $(LDLIBS) $<


ifneq ($(findstring allow-undefined,$(STANCFLAGS)),)

USER_HEADER ?= $(dir $(MAKECMDGOALS))user_header.hpp
USER_INCLUDE = -include $(USER_HEADER)
# Give a better error message if the USER_HEADER is not found
$(USER_HEADER):
@echo 'ERROR: Missing user header.'
@echo 'Because --allow-undefined is set, we need a C++ header file to include.'
@echo 'We tried to find the user header at:'
@echo ' $(USER_HEADER)'
@echo ''
@echo 'You can also set the USER_HEADER variable to the path of your C++ file.'
@exit 1
endif

## generate .hpp file from .stan file using stanc
# generate .hpp file from .stan file using stanc
%.hpp : %.stan $(STANC)
@echo ''
@echo '--- Translating Stan model to C++ code ---'
$(STANC) $(STANCFLAGS) --o=$(subst \,/,$@) $(subst \,/,$<)

%.o : %.hpp
%.o : %.hpp $(USER_HEADER)
@echo '--- Compiling C++ code ---'
$(COMPILE.cpp) -x c++ -o $(subst \,/,$*).o $(subst \,/,$<)
$(COMPILE.cpp) $(USER_INCLUDE) -x c++ -o $(subst \,/,$*).o $(subst \,/,$<)

## builds executable (suffix depends on platform)
# builds executable (suffix depends on platform)
%_model.so : %.o $(TINYSTAN_O) $(SUNDIALS_TARGETS) $(MPI_TARGETS) $(TBB_TARGETS)
@echo '--- Linking C++ code ---'
$(LINK.cpp) -shared -lm -o $(patsubst %.o, %_model.so, $(subst \,/,$<)) $(subst \,/,$*.o) $(TINYSTAN_O) $(LDLIBS) $(SUNDIALS_TARGETS) $(MPI_TARGETS) $(TBB_TARGETS)

# build all test models at once
TEST_MODEL_NAMES = $(patsubst $(TINYSTAN_ROOT)/test_models/%/, %, $(sort $(dir $(wildcard $(TINYSTAN_ROOT)/test_models/*/))))
TEST_MODEL_NAMES := $(filter-out syntax_error, $(TEST_MODEL_NAMES))
ALL_TEST_MODEL_NAMES = $(patsubst $(TINYSTAN_ROOT)/test_models/%/, %, $(sort $(dir $(wildcard $(TINYSTAN_ROOT)/test_models/*/))))
# these are for compilation testing in the interfaces
SKIPPED_TEST_MODEL_NAMES = syntax_error external
TEST_MODEL_NAMES := $(filter-out $(SKIPPED_TEST_MODEL_NAMES), $(ALL_TEST_MODEL_NAMES))
TEST_MODEL_LIBS = $(join $(addprefix test_models/, $(TEST_MODEL_NAMES)), $(addsuffix _model.so, $(addprefix /, $(TEST_MODEL_NAMES))))

.PHONY: test_models
Expand All @@ -86,8 +105,9 @@ format:
.PHONY: clean
clean:
$(RM) $(SRC)/*.o
$(RM) test_models/**/*.so
$(RM) $(join $(addprefix $(BS_ROOT)/test_models/, $(TEST_MODEL_NAMES)), $(addsuffix .hpp, $(addprefix /, $(TEST_MODEL_NAMES))))
$(RM) bin/stanc$(EXE)
$(RM) $(TEST_MODEL_LIBS)

.PHONY: stan-update stan-update-version
stan-update:
Expand All @@ -101,7 +121,7 @@ stan-update-remote:
compile_info:
@echo '$(LINK.cpp) $(STANC_O) $(LDLIBS) $(SUNDIALS_TARGETS) $(MPI_TARGETS) $(TBB_TARGETS)'

## print value of makefile variable (e.g., make print-TBB_TARGETS)
# print value of makefile variable (e.g., make print-TBB_TARGETS)
.PHONY: print-%
print-% : ; @echo $* = $($*) ;

Expand Down
19 changes: 19 additions & 0 deletions clients/python/tests/test_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,25 @@ def test_compile_good():
assert lib.exists()


def test_compile_user_header():
stanfile = STAN_FOLDER / "external" / "external.stan"
lib = tinystan.compile.generate_so_name(stanfile)
lib.unlink(missing_ok=True)

with pytest.raises(RuntimeError, match=r"declared without specifying a definition"):
tinystan.compile_model(stanfile)

with pytest.raises(RuntimeError, match=r"USER_HEADER"):
tinystan.compile_model(stanfile, stanc_args=["--allow-undefined"])

header = stanfile.parent / "make_odds.hpp"
res = tinystan.compile_model(
stanfile, stanc_args=["--allow-undefined"], make_args=[f"USER_HEADER={header}"]
)
assert lib.samefile(res)
assert lib.exists()


def test_compile_bad_ext():
not_stanfile = STAN_FOLDER / "bernoulli" / "bernoulli.data.json"
with pytest.raises(ValueError, match=r".stan"):
Expand Down
13 changes: 13 additions & 0 deletions docs/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,19 @@ Flags for :command:`stanc3` can also be set here
# pedantic mode and level 1 optimization
STANCFLAGS+= --warn-pedantic --O1
Using External C++ Code
_______________________

TinyStan supports the same `capability to plug in external C++ code as CmdStan <https://mc-stan.org/docs/cmdstan-guide/external_code.html>`_.

Namely, you can declare a function in your Stan model and then define it in a separate C++ file.
This requires passing the ``--allow-undefined`` flag to the Stan compiler when building your model.
The :makevar:`USER_HEADER` variable must point to the C++ file containing the function definition.
By default, this will be the file :file:`user_header.hpp` in the same directory as the Stan model.

For a more complete example, consult the `CmdStan documentation <https://mc-stan.org/docs/cmdstan-guide/external_code.html>`_.

Using Pre-Existing Stan Installations
_____________________________________

Expand Down
4 changes: 2 additions & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

Welcome to TinyStan's documentation!
====================================
TinyStan -- A lightweight Stan interface
========================================

.. warning::
This project is still under active development. The API is not yet stable, and the documentation is incomplete. Using before a 1.0 release is not recommended for most users.
Expand Down
18 changes: 18 additions & 0 deletions test_models/external/external.stan
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
functions {
real make_odds(data 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);
}
5 changes: 5 additions & 0 deletions test_models/external/make_odds.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#include <ostream>

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

0 comments on commit 0bea9b9

Please sign in to comment.