Skip to content

Commit

Permalink
Merge pull request #700 from stan-dev/feature/update-constructor-depr…
Browse files Browse the repository at this point in the history
…ecations

Move compilation code, deprecate constructing a model without ever compiling
  • Loading branch information
WardBrian authored Sep 21, 2023
2 parents 13fe59f + 19ac74e commit 2cf6120
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 116 deletions.
4 changes: 2 additions & 2 deletions cmdstanpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def _cleanup_tmpdir() -> None:


from ._version import __version__ # noqa
from .compilation import compile_stan_file
from .install_cmdstan import rebuild_cmdstan
from .model import CmdStanModel
from .stanfit import (
Expand All @@ -31,7 +32,6 @@ def _cleanup_tmpdir() -> None:
CmdStanMLE,
CmdStanPathfinder,
CmdStanVB,
InferenceMetadata,
from_csv,
)
from .utils import (
Expand All @@ -49,14 +49,14 @@ def _cleanup_tmpdir() -> None:
'cmdstan_path',
'set_make_env',
'install_cmdstan',
'compile_stan_file',
'CmdStanMCMC',
'CmdStanMLE',
'CmdStanGQ',
'CmdStanVB',
'CmdStanLaplace',
'CmdStanPathfinder',
'CmdStanModel',
'InferenceMetadata',
'from_csv',
'write_stan_json',
'show_versions',
Expand Down
156 changes: 154 additions & 2 deletions cmdstanpy/compiler_opts.py → cmdstanpy/compilation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@
Makefile options for stanc and C++ compilers
"""

import io
import json
import os
import platform
import shutil
import subprocess
from copy import copy
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Union

from cmdstanpy.utils import get_logger
from cmdstanpy.utils.cmdstan import EXTENSION, cmdstan_path
from cmdstanpy.utils.command import do_command
from cmdstanpy.utils.filesystem import SanitizedOrTmpFilePath

STANC_OPTS = [
'O',
Expand Down Expand Up @@ -258,8 +266,9 @@ def add(self, new_opts: "CompilerOptions") -> None: # noqa: disable=Q000
else:
for key, val in new_opts.stanc_options.items():
if key == 'include-paths':
if isinstance(val, Iterable) \
and not isinstance(val, str):
if isinstance(val, Iterable) and not isinstance(
val, str
):
for path in val:
self.add_include_path(str(path))
else:
Expand Down Expand Up @@ -322,3 +331,146 @@ def compose(self, filename_in_msg: Optional[str] = None) -> List[str]:
for key, val in self._cpp_options.items():
opts.append(f'{key}={val}')
return opts


def src_info(
stan_file: str, compiler_options: CompilerOptions
) -> Dict[str, Any]:
"""
Get source info for Stan program file.
This function is used in the implementation of
:meth:`CmdStanModel.src_info`, and should not be called directly.
"""
cmd = (
[os.path.join(cmdstan_path(), 'bin', 'stanc' + EXTENSION)]
# handle include-paths, allow-undefined etc
+ compiler_options.compose_stanc(None)
+ ['--info', str(stan_file)]
)
proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
if proc.returncode:
raise ValueError(
f"Failed to get source info for Stan model "
f"'{stan_file}'. Console:\n{proc.stderr}"
)
result: Dict[str, Any] = json.loads(proc.stdout)
return result


def compile_stan_file(
src: Union[str, Path],
force: bool = False,
stanc_options: Optional[Dict[str, Any]] = None,
cpp_options: Optional[Dict[str, Any]] = None,
user_header: OptionalPath = None,
) -> str:
"""
Compile the given Stan program file. Translates the Stan code to
C++, then calls the C++ compiler.
By default, this function compares the timestamps on the source and
executable files; if the executable is newer than the source file, it
will not recompile the file, unless argument ``force`` is ``True``
or unless the compiler options have been changed.
:param src: Path to Stan program file.
:param force: When ``True``, always compile, even if the executable file
is newer than the source file. Used for Stan models which have
``#include`` directives in order to force recompilation when changes
are made to the included files.
:param stanc_options: Options for stanc compiler.
:param cpp_options: Options for C++ compiler.
:param user_header: A path to a header file to include during C++
compilation.
"""

src = Path(src).resolve()
if not src.exists():
raise ValueError(f'stan file does not exist: {src}')

compiler_options = CompilerOptions(
stanc_options=stanc_options,
cpp_options=cpp_options,
user_header=user_header,
)
compiler_options.validate()

exe_target = src.with_suffix(EXTENSION)
if exe_target.exists():
exe_time = os.path.getmtime(exe_target)
included_files = [src]
included_files.extend(
src_info(str(src), compiler_options).get('included_files', [])
)
out_of_date = any(
os.path.getmtime(included_file) > exe_time
for included_file in included_files
)
if not out_of_date and not force:
get_logger().debug('found newer exe file, not recompiling')
return str(exe_target)

compilation_failed = False
# if target path has spaces or special characters, use a copy in a
# temporary directory (GNU-Make constraint)
with SanitizedOrTmpFilePath(str(src)) as (stan_file, is_copied):
exe_file = os.path.splitext(stan_file)[0] + EXTENSION

hpp_file = os.path.splitext(exe_file)[0] + '.hpp'
if os.path.exists(hpp_file):
os.remove(hpp_file)
if os.path.exists(exe_file):
get_logger().debug('Removing %s', exe_file)
os.remove(exe_file)

get_logger().info(
'compiling stan file %s to exe file %s',
stan_file,
exe_target,
)

make = os.getenv(
'MAKE',
'make' if platform.system() != 'Windows' else 'mingw32-make',
)
cmd = [make]
cmd.extend(compiler_options.compose(filename_in_msg=src.name))
cmd.append(Path(exe_file).as_posix())

sout = io.StringIO()
try:
do_command(cmd=cmd, cwd=cmdstan_path(), fd_out=sout)
except RuntimeError as e:
sout.write(f'\n{str(e)}\n')
compilation_failed = True
finally:
console = sout.getvalue()

get_logger().debug('Console output:\n%s', console)
if not compilation_failed:
if is_copied:
shutil.copy(exe_file, exe_target)
get_logger().info('compiled model executable: %s', exe_target)
if 'Warning' in console:
lines = console.split('\n')
warnings = [x for x in lines if x.startswith('Warning')]
get_logger().warning(
'Stan compiler has produced %d warnings:',
len(warnings),
)
get_logger().warning(console)
if compilation_failed:
if 'PCH' in console or 'precompiled header' in console:
get_logger().warning(
"CmdStan's precompiled header (PCH) files "
"may need to be rebuilt."
"Please run cmdstanpy.rebuild_cmdstan().\n"
"If the issue persists please open a bug report"
)
raise ValueError(
f"Failed to compile Stan model '{src}'. " f"Console:\n{console}"
)
return str(exe_target)
Loading

0 comments on commit 2cf6120

Please sign in to comment.