Skip to content

Commit d90af13

Browse files
committed
Use cpp-opts internally for user header, check clashes
1 parent 2ed87fe commit d90af13

4 files changed

Lines changed: 86 additions & 63 deletions

File tree

cmdstanpy/compiler_opts.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ def __init__(
7070
)
7171

7272
def __repr__(self) -> str:
73-
return 'stanc_options={}, cpp_options={}, user_header={}'.format(
74-
self._stanc_options, self._cpp_options, self._user_header
73+
return 'stanc_options={}, cpp_options={}'.format(
74+
self._stanc_options, self._cpp_options
7575
)
7676

7777
@property
@@ -84,11 +84,6 @@ def cpp_options(self) -> Dict[str, Union[bool, int]]:
8484
"""C++ compiler options."""
8585
return self._cpp_options
8686

87-
@property
88-
def user_header(self) -> str:
89-
"""The user header file if it exists, otherwise empty"""
90-
return self._user_header
91-
9287
def validate(self) -> None:
9388
"""
9489
Check compiler args.
@@ -184,9 +179,20 @@ def validate_user_header(self) -> None:
184179

185180
if ' ' in self._user_header:
186181
raise ValueError(
187-
"User header must be in a folder with no spaces in path!"
182+
"User header must be in a location with no spaces in path!"
188183
)
189184

185+
if (
186+
'USER_HEADER' in self._cpp_options
187+
and self._user_header != self._cpp_options['USER_HEADER']
188+
):
189+
raise ValueError(
190+
"Disagreement in user_header C++ options found!\n"
191+
f"{self._user_header}, {self._cpp_options['USER_HEADER']}"
192+
)
193+
194+
self._cpp_options['USER_HEADER'] = self._user_header
195+
190196
def add(self, new_opts: "CompilerOptions") -> None: # noqa: disable=Q000
191197
"""Adds options to existing set of compiler options."""
192198
if new_opts.stanc_options is not None:
@@ -201,8 +207,8 @@ def add(self, new_opts: "CompilerOptions") -> None: # noqa: disable=Q000
201207
if new_opts.cpp_options is not None:
202208
for key, val in new_opts.cpp_options.items():
203209
self._cpp_options[key] = val
204-
if new_opts.user_header != '' and self._user_header == '':
205-
self._user_header = new_opts.user_header
210+
if new_opts._user_header != '' and self._user_header == '':
211+
self._user_header = new_opts._user_header
206212

207213
def add_include_path(self, path: str) -> None:
208214
"""Adds include path to existing set of compiler options."""
@@ -233,6 +239,4 @@ def compose(self) -> List[str]:
233239
if self._cpp_options is not None and len(self._cpp_options) > 0:
234240
for key, val in self._cpp_options.items():
235241
opts.append(f'{key}={val}')
236-
if self._user_header:
237-
opts.append(f'USER_HEADER={self._user_header}')
238242
return opts

docsrc/env.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ dependencies:
66
- python=3.7
77
- ipykernel
88
- ipython
9+
- ipywidgets
910
- numpy>=1.15
1011
- pandas
1112
- xarray

docsrc/examples/Using External C++.ipynb

Lines changed: 56 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,103 +2,116 @@
22
"cells": [
33
{
44
"cell_type": "markdown",
5+
"metadata": {},
56
"source": [
67
"# Advanced Topic: Using External C++ Functions\n",
78
"\n",
89
"This is based on the relevant portion of the CmdStan documentation [here](https://mc-stan.org/docs/cmdstan-guide/using-external-cpp-code.html)"
9-
],
10-
"metadata": {}
10+
]
1111
},
1212
{
1313
"cell_type": "markdown",
14+
"metadata": {},
1415
"source": [
1516
"Consider the following Stan model, based on the bernoulli example."
16-
],
17-
"metadata": {}
17+
]
1818
},
1919
{
2020
"cell_type": "code",
2121
"execution_count": null,
22+
"metadata": {"nbsphinx": "hidden"},
23+
"outputs": [],
2224
"source": [
2325
"import os\n",
24-
"from cmdstanpy import cmdstan_path, CmdStanModel\n",
26+
"try:\n",
27+
" os.remove('bernoulli_external')\n",
28+
"except:\n",
29+
" pass"
30+
]
31+
},
32+
{
33+
"cell_type": "code",
34+
"execution_count": null,
35+
"metadata": {},
36+
"outputs": [],
37+
"source": [
38+
"from cmdstanpy import CmdStanModel\n",
2539
"model_external = CmdStanModel(stan_file='bernoulli_external.stan', compile=False)\n",
2640
"print(model_external.code())"
27-
],
28-
"outputs": [],
29-
"metadata": {}
41+
]
3042
},
3143
{
3244
"cell_type": "markdown",
45+
"metadata": {},
3346
"source": [
3447
"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. "
35-
],
36-
"metadata": {}
48+
]
3749
},
3850
{
3951
"cell_type": "code",
4052
"execution_count": null,
53+
"metadata": {},
54+
"outputs": [],
4155
"source": [
4256
"model_external.compile()"
43-
],
44-
"outputs": [],
45-
"metadata": {}
57+
]
4658
},
4759
{
4860
"cell_type": "markdown",
61+
"metadata": {},
4962
"source": [
5063
"Even enabling the `--allow_undefined` flag to stanc3 will not allow this model to be compiled quite yet."
51-
],
52-
"metadata": {}
64+
]
5365
},
5466
{
5567
"cell_type": "code",
5668
"execution_count": null,
69+
"metadata": {},
70+
"outputs": [],
5771
"source": [
5872
"model_external.compile(stanc_options={'allow_undefined':True})"
59-
],
60-
"outputs": [],
61-
"metadata": {}
73+
]
6274
},
6375
{
6476
"cell_type": "markdown",
77+
"metadata": {},
6578
"source": [
6679
"To resolve this, we need to both tell the Stan compiler an undefined function is okay **and** let C++ know what it should be. \n",
6780
"\n",
6881
"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. \n",
6982
"\n",
7083
"This will enables the `allow_undefined` flag automatically."
71-
],
72-
"metadata": {}
84+
]
7385
},
7486
{
7587
"cell_type": "code",
7688
"execution_count": null,
89+
"metadata": {},
90+
"outputs": [],
7791
"source": [
7892
"model_external.compile(user_header='make_odds.hpp')"
79-
],
80-
"outputs": [],
81-
"metadata": {}
93+
]
8294
},
8395
{
8496
"cell_type": "markdown",
97+
"metadata": {},
8598
"source": [
8699
"We can then run this model and inspect the output"
87-
],
88-
"metadata": {}
100+
]
89101
},
90102
{
91103
"cell_type": "code",
92104
"execution_count": null,
105+
"metadata": {},
106+
"outputs": [],
93107
"source": [
94108
"fit = model_external.sample(data={'N':10, 'y':[0,1,0,0,0,0,0,0,0,1]})\n",
95109
"fit.stan_variable('odds')"
96-
],
97-
"outputs": [],
98-
"metadata": {}
110+
]
99111
},
100112
{
101113
"cell_type": "markdown",
114+
"metadata": {},
102115
"source": [
103116
"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:\n",
104117
"\n",
@@ -114,32 +127,31 @@
114127
" }\n",
115128
"}\n",
116129
"```"
117-
],
118-
"metadata": {}
130+
]
119131
}
120132
],
121133
"metadata": {
122-
"orig_nbformat": 4,
134+
"interpreter": {
135+
"hash": "8765ce46b013071999fc1966b52035a7309a0da7551e066cc0f0fa23e83d4f60"
136+
},
137+
"kernelspec": {
138+
"display_name": "Python 3.9.5 64-bit ('stan': conda)",
139+
"name": "python3"
140+
},
123141
"language_info": {
124-
"name": "python",
125-
"version": "3.9.5",
126-
"mimetype": "text/x-python",
127142
"codemirror_mode": {
128143
"name": "ipython",
129144
"version": 3
130145
},
131-
"pygments_lexer": "ipython3",
146+
"file_extension": ".py",
147+
"mimetype": "text/x-python",
148+
"name": "python",
132149
"nbconvert_exporter": "python",
133-
"file_extension": ".py"
134-
},
135-
"kernelspec": {
136-
"name": "python3",
137-
"display_name": "Python 3.9.5 64-bit ('stan': conda)"
150+
"pygments_lexer": "ipython3",
151+
"version": "3.9.5"
138152
},
139-
"interpreter": {
140-
"hash": "d31ce8e45781476cfd394e192e0962028add96ff436d4fd4e560a347d206b9cb"
141-
}
153+
"orig_nbformat": 4
142154
},
143155
"nbformat": 4,
144156
"nbformat_minor": 2
145-
}
157+
}

test/test_compiler_opts.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ def test_opts_empty(self):
1414
opts = CompilerOptions()
1515
opts.validate()
1616
self.assertEqual(opts.compose(), [])
17-
self.assertEqual(
18-
opts.__repr__(), 'stanc_options={}, cpp_options={}, user_header='
19-
)
17+
self.assertEqual(opts.__repr__(), 'stanc_options={}, cpp_options={}')
2018

2119
stanc_opts = {}
2220
opts = CompilerOptions(stanc_options=stanc_opts)
@@ -31,9 +29,7 @@ def test_opts_empty(self):
3129
opts = CompilerOptions(stanc_options=stanc_opts, cpp_options=cpp_opts)
3230
opts.validate()
3331
self.assertEqual(opts.compose(), [])
34-
self.assertEqual(
35-
opts.__repr__(), 'stanc_options={}, cpp_options={}, user_header='
36-
)
32+
self.assertEqual(opts.__repr__(), 'stanc_options={}, cpp_options={}')
3733

3834
def test_opts_stanc(self):
3935
stanc_opts = {}
@@ -174,6 +170,13 @@ def test_user_header(self):
174170
with self.assertRaisesRegex(ValueError, "must end in .hpp"):
175171
opts.validate()
176172

173+
header_file = os.path.join(DATAFILES_PATH, 'return_one.hpp')
174+
opts = CompilerOptions(
175+
user_header=header_file, cpp_options={'USER_HEADER': 'foo'}
176+
)
177+
with self.assertRaisesRegex(ValueError, "Disagreement"):
178+
opts.validate()
179+
177180
def test_opts_add(self):
178181
stanc_opts = {'warn-uninitialized': True}
179182
cpp_opts = {'STAN_OPENCL': 'TRUE', 'OPENCL_DEVICE_ID': 1}
@@ -213,7 +216,10 @@ def test_opts_add(self):
213216
opts = CompilerOptions()
214217
opts.add(CompilerOptions(user_header=header_file))
215218
opts_list = opts.compose()
216-
self.assertTrue(len(opts_list) == 1)
219+
self.assertEqual(len(opts_list), 0)
220+
opts.validate()
221+
opts_list = opts.compose()
222+
self.assertEqual(len(opts_list), 2)
217223

218224

219225
if __name__ == '__main__':

0 commit comments

Comments
 (0)