prophet/python/setup.py

273 lines
8.5 KiB
Python
Raw Normal View History

2019-05-21 18:40:04 +00:00
# Copyright (c) Facebook, Inc. and its affiliates.
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
import os
2017-02-22 23:59:43 +00:00
import sys
import platform
from pathlib import Path
from shutil import copy, copytree, rmtree
from typing import List
from pkg_resources import add_activation_listener, normalize_path, require, working_set
from setuptools import find_packages, setup, Extension
from setuptools.command.build_ext import build_ext
2017-02-22 23:59:43 +00:00
from setuptools.command.build_py import build_py
from setuptools.command.develop import develop
2017-02-22 23:59:43 +00:00
from setuptools.command.test import test as test_command
MODEL_DIR = "stan"
MODEL_TARGET_DIR = os.path.join("prophet", "stan_model")
CMDSTAN_VERSION = "2.26.1"
BINARIES_DIR = "bin"
BINARIES = ["diagnose", "print", "stanc", "stansummary"]
TBB_PARENT = "stan/lib/stan_math/lib"
TBB_DIRS = ["tbb", "tbb_2019_U8"]
def prune_cmdstan(cmdstan_dir: str) -> None:
"""
Keep only the cmdstan executables and tbb files (minimum required to run a cmdstanpy commands on a pre-compiled model).
"""
original_dir = Path(cmdstan_dir).resolve()
parent_dir = original_dir.parent
temp_dir = parent_dir / "temp"
if temp_dir.is_dir():
rmtree(temp_dir)
temp_dir.mkdir()
print("Copying ", original_dir, " to ", temp_dir, " for pruning")
copytree(original_dir / BINARIES_DIR, temp_dir / BINARIES_DIR)
for f in (temp_dir / BINARIES_DIR).iterdir():
if f.is_dir():
rmtree(f)
elif f.is_file() and f.stem not in BINARIES:
os.remove(f)
for tbb_dir in TBB_DIRS:
copytree(original_dir / TBB_PARENT / tbb_dir, temp_dir / TBB_PARENT / tbb_dir)
rmtree(original_dir)
temp_dir.rename(original_dir)
def repackage_cmdstan():
return os.environ.get("PROPHET_REPACKAGE_CMDSTAN", "").lower() not in ["false", "0"]
2021-10-03 23:16:55 +00:00
def maybe_install_cmdstan_toolchain():
2021-12-25 12:20:24 +00:00
"""Install C++ compilers required to build stan models on Windows machines."""
import cmdstanpy
2021-12-25 12:20:24 +00:00
from cmdstanpy.install_cxx_toolchain import main as _install_cxx_toolchain
try:
cmdstanpy.utils.cxx_toolchain_path()
except Exception:
_install_cxx_toolchain({"version": None, "dir": None, "verbose": True})
cmdstanpy.utils.cxx_toolchain_path()
2021-12-25 12:20:24 +00:00
def install_cmdstan_deps(cmdstan_dir: Path):
import cmdstanpy
from multiprocessing import cpu_count
if repackage_cmdstan():
if platform.platform().startswith("Win"):
maybe_install_cmdstan_toolchain()
print("Installing cmdstan to", cmdstan_dir)
if os.path.isdir(cmdstan_dir):
rmtree(cmdstan_dir)
if not cmdstanpy.install_cmdstan(
version=CMDSTAN_VERSION,
dir=cmdstan_dir.parent,
overwrite=True,
verbose=True,
cores=cpu_count(),
progress=True,
):
raise RuntimeError("CmdStan failed to install in repackaged directory")
2021-12-25 12:20:24 +00:00
2021-12-25 12:20:24 +00:00
def build_cmdstan_model(target_dir):
"""
Rebuild cmdstan in the build environment, then use this installation to compile the stan model.
The stan model is copied to {target_dir}/prophet_model.bin
The cmdstan files required to run cmdstanpy commands are copied to {target_dir}/cmdstan-{version}.
Parameters
----------
target_dir: Directory to copy the compiled model executable and core cmdstan files to.
"""
import cmdstanpy
2021-12-25 12:20:24 +00:00
cmdstan_dir = (Path(target_dir) / f"cmdstan-{CMDSTAN_VERSION}").resolve()
install_cmdstan_deps(cmdstan_dir)
model_name = "prophet.stan"
target_name = "prophet_model.bin"
sm = cmdstanpy.CmdStanModel(stan_file=os.path.join(MODEL_DIR, model_name))
copy(sm.exe_file, os.path.join(target_dir, target_name))
# Clean up
for f in Path(MODEL_DIR).iterdir():
if f.is_file() and f.name != model_name:
os.remove(f)
if repackage_cmdstan():
prune_cmdstan(cmdstan_dir)
2021-10-03 23:16:55 +00:00
def build_pystan_model(target_dir):
"""
Compile the stan model using pystan and pickle it. The pickle is copied to {target_dir}/prophet_model.pkl.
"""
import pystan
import pickle
model_name = "prophet.stan"
target_name = "prophet_model.pkl"
with open(os.path.join(MODEL_DIR, model_name)) as f:
model_code = f.read()
sm = pystan.StanModel(model_code=model_code)
with open(os.path.join(target_dir, target_name), "wb") as f:
pickle.dump(sm, f, protocol=pickle.HIGHEST_PROTOCOL)
2021-10-03 23:16:55 +00:00
def get_backends_from_env() -> List[str]:
return os.environ.get("STAN_BACKEND", "CMDSTANPY").split(",")
2021-10-03 23:16:55 +00:00
def build_models(target_dir):
for backend in get_backends_from_env():
print(f"Compiling {backend} model")
if backend == "CMDSTANPY":
build_cmdstan_model(target_dir)
2021-12-25 12:20:24 +00:00
elif backend == "PYSTAN" and PLATFORM != "win":
build_pystan_model(target_dir)
2017-02-22 23:59:43 +00:00
class BuildPyCommand(build_py):
"""Custom build command to pre-compile Stan models."""
def run(self):
if not self.dry_run:
target_dir = os.path.join(self.build_lib, MODEL_TARGET_DIR)
self.mkpath(target_dir)
build_models(target_dir)
2017-02-22 23:59:43 +00:00
build_py.run(self)
class BuildExtCommand(build_ext):
"""Ensure built extensions are added to the correct path in the wheel."""
def run(self):
pass
class DevelopCommand(develop):
"""Custom develop command to pre-compile Stan models in-place."""
def run(self):
if not self.dry_run:
target_dir = os.path.join(self.setup_path, MODEL_TARGET_DIR)
self.mkpath(target_dir)
build_models(target_dir)
develop.run(self)
2017-02-22 23:59:43 +00:00
class TestCommand(test_command):
user_options = [
("test-module=", "m", "Run 'test_suite' in specified module"),
(
"test-suite=",
"s",
"Run single test, case or suite (e.g. 'module.test_suite')",
),
("test-runner=", "r", "Test runner to use"),
("test-slow", "w", "Test slow suites (default off)"),
]
test_slow = None
def initialize_options(self):
super(TestCommand, self).initialize_options()
self.test_slow = False
def finalize_options(self):
super(TestCommand, self).finalize_options()
if self.test_slow is None:
self.test_slow = getattr(self.distribution, "test_slow", False)
2017-02-22 23:59:43 +00:00
"""We must run tests on the build directory, not source."""
def with_project_on_sys_path(self, func):
# Ensure metadata is up-to-date
self.reinitialize_command("build_py", inplace=0)
self.run_command("build_py")
2017-02-22 23:59:43 +00:00
bpy_cmd = self.get_finalized_command("build_py")
build_path = normalize_path(bpy_cmd.build_lib)
# Build extensions
self.reinitialize_command("egg_info", egg_base=build_path)
self.run_command("egg_info")
2017-02-22 23:59:43 +00:00
self.reinitialize_command("build_ext", inplace=0)
self.run_command("build_ext")
2017-02-22 23:59:43 +00:00
ei_cmd = self.get_finalized_command("egg_info")
old_path = sys.path[:]
old_modules = sys.modules.copy()
try:
sys.path.insert(0, normalize_path(ei_cmd.egg_base))
working_set.__init__()
add_activation_listener(lambda dist: dist.activate())
require("%s==%s" % (ei_cmd.egg_name, ei_cmd.egg_version))
2017-02-22 23:59:43 +00:00
func()
finally:
sys.path[:] = old_path
sys.modules.clear()
sys.modules.update(old_modules)
working_set.__init__()
with open("README.md", "r", encoding="utf-8") as f:
long_description = f.read()
with open("requirements.txt", "r") as f:
install_requires = f.read().splitlines()
2017-02-22 23:59:43 +00:00
setup(
name="prophet",
version="1.1",
description="Automatic Forecasting Procedure",
url="https://facebook.github.io/prophet/",
author="Sean J. Taylor <sjtz@pm.me>, Ben Letham <bletham@fb.com>",
author_email="sjtz@pm.me",
license="MIT",
2018-12-21 02:15:07 +00:00
packages=find_packages(),
install_requires=install_requires,
python_requires=">=3.6",
2017-02-22 23:59:43 +00:00
zip_safe=False,
2018-12-21 00:11:53 +00:00
include_package_data=True,
ext_modules=[Extension("prophet.stan_model", [])],
2017-02-22 23:59:43 +00:00
cmdclass={
"build_ext": BuildExtCommand,
"build_py": BuildPyCommand,
"develop": DevelopCommand,
"test": TestCommand,
2017-02-22 23:59:43 +00:00
},
test_suite="prophet.tests",
classifiers=[
2021-10-03 23:16:55 +00:00
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
],
long_description=long_description,
long_description_content_type="text/markdown",
2017-02-22 23:59:43 +00:00
)