From 25e2b887652f950956f3236c0a666d605b23748e Mon Sep 17 00:00:00 2001 From: Mikhail Popov Date: Tue, 28 Apr 2020 16:06:17 -0400 Subject: [PATCH] {rstantools}-based compilation + cleans up Stan model compilation by switching over to the rstantools-based workflow (see https://mc-stan.org/rstantools/articles/minimal-rstan-package.html for more info) + minor documentation change: {devtools} -> {remotes}, which is better for end-users + adds RStudio project file which makes it easier for community to get started with contributing to the package --- R/.Rbuildignore | 2 ++ R/.gitignore | 54 +++++++++++++++++++++++++++++ R/DESCRIPTION | 21 +++++++---- R/NAMESPACE | 1 + R/R/prophet.R | 51 ++++----------------------- R/R/stanmodels.R | 25 +++++++++++++ R/configure | 4 +++ R/configure.win | 4 +++ R/inst/include/stan_meta_header.hpp | 1 + R/man/compile_stan_model.Rd | 15 -------- R/man/generated_holidays.Rd | 4 ++- R/prophet.Rproj | 21 +++++++++++ R/src/Makevars | 15 ++++++++ R/src/Makevars.win | 14 ++++++++ README.md | 15 ++++---- 15 files changed, 173 insertions(+), 74 deletions(-) create mode 100644 R/.gitignore create mode 100644 R/R/stanmodels.R create mode 100755 R/configure create mode 100755 R/configure.win create mode 100644 R/inst/include/stan_meta_header.hpp delete mode 100644 R/man/compile_stan_model.Rd create mode 100644 R/prophet.Rproj create mode 100644 R/src/Makevars create mode 100644 R/src/Makevars.win diff --git a/R/.Rbuildignore b/R/.Rbuildignore index 96b8450..24215b4 100644 --- a/R/.Rbuildignore +++ b/R/.Rbuildignore @@ -2,3 +2,5 @@ ^libs$ ^doc$ ^Meta$ +^.*\.Rproj$ +^\.Rproj\.user$ diff --git a/R/.gitignore b/R/.gitignore new file mode 100644 index 0000000..7bb5058 --- /dev/null +++ b/R/.gitignore @@ -0,0 +1,54 @@ +.DS_Store + +# History files +.Rhistory +.Rapp.history + +# Session Data files +.RData + +# User-specific files +.Ruserdata + +# Example code in package build process +*-Ex.R + +# Output files from R CMD build +/*.tar.gz + +# Output files from R CMD check +/*.Rcheck/ + +# RStudio files +.Rproj.user/ + +# produced vignettes +vignettes/*.html +vignettes/*.pdf + +# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 +.httr-oauth + +# knitr and R markdown default cache directories +*_cache/ +/cache/ + +# Temporary files created by R markdown +*.utf8.md +*.knit.md + +# R Environment Variables +.Renviron +inst/doc + +# Stan +exec/*.rda +src/*.o +src/*.h +src/*.cc +src/*.cpp +src/*.hpp +src/prophet.so +src/prophet.dll +src/init.o + diff --git a/R/DESCRIPTION b/R/DESCRIPTION index 4b21935..bbf8cc5 100644 --- a/R/DESCRIPTION +++ b/R/DESCRIPTION @@ -12,8 +12,10 @@ Description: Implements a procedure for forecasting time series data based on that have strong seasonal effects and several seasons of historical data. Prophet is robust to missing data and shifts in the trend, and typically handles outliers well. +URL: https://github.com/facebook/prophet +BugReports: https://github.com/facebook/prophet/issues Depends: - R (>= 3.2.3), + R (>= 3.4.0), Rcpp (>= 0.12.0), rlang (>= 0.3.0.1) Imports: @@ -22,7 +24,9 @@ Imports: extraDistr, ggplot2, grid, - rstan (>= 2.14.0), + methods, + rstan (>= 2.18.1), + rstantools (>= 2.0.0), scales, stats, tidyr (>= 0.6.1), @@ -32,11 +36,16 @@ Suggests: testthat, readr, rmarkdown +SystemRequirements: GNU make, C++11 +Biarch: true License: MIT + file LICENSE -URL: https://github.com/facebook/prophet -BugReports: https://github.com/facebook/prophet/issues +LinkingTo: + BH (>= 1.66.0), + Rcpp (>= 0.12.0), + RcppEigen (>= 0.3.3.3.0), + rstan (>= 2.18.1), + StanHeaders (>= 2.18.0) LazyData: true -RoxygenNote: 7.0.2 VignetteBuilder: knitr -SystemRequirements: C++11 Encoding: UTF-8 +RoxygenNote: 7.1.0 diff --git a/R/NAMESPACE b/R/NAMESPACE index fcbd445..4cbc600 100644 --- a/R/NAMESPACE +++ b/R/NAMESPACE @@ -19,3 +19,4 @@ export(prophet_plot_components) import(Rcpp) import(rlang) importFrom(dplyr,"%>%") +useDynLib(prophet, .registration = TRUE) diff --git a/R/R/prophet.R b/R/R/prophet.R index ab6ce22..67e6245 100644 --- a/R/R/prophet.R +++ b/R/R/prophet.R @@ -80,6 +80,7 @@ globalVariables(c( #' @importFrom dplyr "%>%" #' @import Rcpp #' @import rlang +#' @useDynLib prophet, .registration = TRUE prophet <- function(df = NULL, growth = 'linear', changepoints = NULL, @@ -236,44 +237,6 @@ validate_column_name <- function( } } -#' Compile Stan model -#' -#' @return Stan model. -#' -#' @keywords internal -compile_stan_model <- function() { - packageStartupMessage('Compiling model (this will take a few minutes...)') - packageStartupMessage( - 'If this is the first time fitting a model since package install, this is normal. ', - 'You should not see this message more than once after install.' - ) - - if (.Platform$OS.type == "windows") { - dest <- file.path(system.file(package="prophet"), 'libs', .Platform$r_arch) - } else { - dest <- file.path(system.file(package="prophet"), 'libs') - } - dir.create(dest, recursive = TRUE, showWarnings = FALSE) - - packageStartupMessage(paste('Writing model to:', dest)) - packageStartupMessage(paste('Compiling using binary:', R.home('bin'))) - - fn <- 'stan/prophet.stan' - stan.src <- system.file(fn, package = 'prophet', mustWork = TRUE) - model.binary <- file.path(dest, 'prophet_stan_model.RData') - - stanc <- rstan::stanc(stan.src) - model.stanm <- rstan::stan_model( - stanc_ret = stanc, - model_name = 'prophet_model' - ) - save('model.stanm', file = model.binary) - packageStartupMessage('------ Model successfully compiled!') - packageStartupMessage('You can ignore any compiler warnings above.') - assign(".prophet.stan.model", model.stanm, envir = prophet_model_env) - return(model.stanm) -} - #' Convert date vector #' #' Convert the date to POSIXct object @@ -365,7 +328,7 @@ setup_dataframe <- function(m, df, initialize_scales = FALSE) { df[[condition.name]] <- as.logical(df[[condition.name]]) } } - + df <- df %>% dplyr::arrange(ds) @@ -724,7 +687,7 @@ add_regressor <- function( #' specified, m$seasonality.mode will be used (defaults to 'additive'). #' Additive means the seasonality will be added to the trend, multiplicative #' means it will multiply the trend. -#' +#' #' If condition.name is provided, the dataframe passed to `fit` and `predict` #' should have a column with the specified condition.name containing booleans #' which decides when to apply seasonality. @@ -741,7 +704,7 @@ add_regressor <- function( #' #' @export add_seasonality <- function( - m, name, period, fourier.order, prior.scale = NULL, mode = NULL, + m, name, period, fourier.order, prior.scale = NULL, mode = NULL, condition.name = NULL ) { if (!is.null(m$history)) { @@ -1231,7 +1194,7 @@ fit.prophet <- function(m, df, ...) { if (exists(".prophet.stan.model", where = prophet_model_env)) { model <- get('.prophet.stan.model', envir = prophet_model_env) } else { - model <- compile_stan_model() + model <- stanmodels$prophet } stan_init <- function() { @@ -1280,7 +1243,7 @@ fit.prophet <- function(m, df, ...) { m$params <- m$stan.fit$par n.iteration <- 1 } - + # Cast the parameters to have consistent form, whether full bayes or MAP for (name in c('delta', 'beta')){ m$params[[name]] <- matrix(m$params[[name]], nrow = n.iteration) @@ -1340,7 +1303,7 @@ predict.prophet <- function(object, df = NULL, ...) { } else { intervals <- NULL } - + # Drop columns except ds, cap, floor, and trend cols <- c('ds', 'trend') if ('cap' %in% colnames(df)) { diff --git a/R/R/stanmodels.R b/R/R/stanmodels.R new file mode 100644 index 0000000..9e45354 --- /dev/null +++ b/R/R/stanmodels.R @@ -0,0 +1,25 @@ +# Generated by rstantools. Do not edit by hand. + +# names of stan models +stanmodels <- c("prophet") + +# load each stan module +Rcpp::loadModule("stan_fit4prophet_mod", what = TRUE) + +# instantiate each stanmodel object +stanmodels <- sapply(stanmodels, function(model_name) { + # create C++ code for stan model + stan_file <- if(dir.exists("stan")) "stan" else file.path("inst", "stan") + stan_file <- file.path(stan_file, paste0(model_name, ".stan")) + stanfit <- rstan::stanc_builder(stan_file, + allow_undefined = TRUE, + obfuscate_model_name = FALSE) + stanfit$model_cpp <- list(model_cppname = stanfit$model_name, + model_cppcode = stanfit$cppcode) + # create stanmodel object + methods::new(Class = "stanmodel", + model_name = stanfit$model_name, + model_code = stanfit$model_code, + model_cpp = stanfit$model_cpp, + mk_cppmodule = function(x) get(paste0("model_", model_name))) +}) diff --git a/R/configure b/R/configure new file mode 100755 index 0000000..1c04798 --- /dev/null +++ b/R/configure @@ -0,0 +1,4 @@ +# Generated by rstantools. Do not edit by hand. + +#! /bin/sh +"${R_HOME}/bin/Rscript" -e "rstantools::rstan_config()" diff --git a/R/configure.win b/R/configure.win new file mode 100755 index 0000000..94d77bd --- /dev/null +++ b/R/configure.win @@ -0,0 +1,4 @@ +# Generated by rstantools. Do not edit by hand. + +#! /bin/sh +"${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" -e "rstantools::rstan_config()" diff --git a/R/inst/include/stan_meta_header.hpp b/R/inst/include/stan_meta_header.hpp new file mode 100644 index 0000000..3b914da --- /dev/null +++ b/R/inst/include/stan_meta_header.hpp @@ -0,0 +1 @@ +// Insert all #include statements here diff --git a/R/man/compile_stan_model.Rd b/R/man/compile_stan_model.Rd deleted file mode 100644 index 2e33c40..0000000 --- a/R/man/compile_stan_model.Rd +++ /dev/null @@ -1,15 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/prophet.R -\name{compile_stan_model} -\alias{compile_stan_model} -\title{Compile Stan model} -\usage{ -compile_stan_model() -} -\value{ -Stan model. -} -\description{ -Compile Stan model -} -\keyword{internal} diff --git a/R/man/generated_holidays.Rd b/R/man/generated_holidays.Rd index 9795099..0e087f9 100644 --- a/R/man/generated_holidays.Rd +++ b/R/man/generated_holidays.Rd @@ -4,7 +4,9 @@ \name{generated_holidays} \alias{generated_holidays} \title{holidays table} -\format{A data frame with five variables: ds, holiday, country, year} +\format{ +A data frame with five variables: ds, holiday, country, year +} \usage{ generated_holidays } diff --git a/R/prophet.Rproj b/R/prophet.Rproj new file mode 100644 index 0000000..88ff2b5 --- /dev/null +++ b/R/prophet.Rproj @@ -0,0 +1,21 @@ +Version: 1.0 + +RestoreWorkspace: Default +SaveWorkspace: Default +AlwaysSaveHistory: Default + +EnableCodeIndexing: Yes +UseSpacesForTab: Yes +NumSpacesForTab: 2 +Encoding: UTF-8 + +RnwWeave: knitr +LaTeX: pdfLaTeX + +AutoAppendNewline: Yes +StripTrailingWhitespace: Yes + +BuildType: Package +PackageUseDevtools: Yes +PackageInstallArgs: --no-multiarch --with-keep.source +PackageRoxygenize: rd,collate,namespace diff --git a/R/src/Makevars b/R/src/Makevars new file mode 100644 index 0000000..268a9c1 --- /dev/null +++ b/R/src/Makevars @@ -0,0 +1,15 @@ +# Generated by rstantools. Do not edit by hand. + +STANHEADERS_SRC = $(shell "$(R_HOME)/bin$(R_ARCH_BIN)/Rscript" -e "message()" -e "cat(system.file('include', 'src', package = 'StanHeaders', mustWork = TRUE))" -e "message()" | grep "StanHeaders") + +PKG_CPPFLAGS = -I"../inst/include" -I"$(STANHEADERS_SRC)" -DBOOST_DISABLE_ASSERTS -DEIGEN_NO_DEBUG -DBOOST_MATH_OVERFLOW_ERROR_POLICY=errno_on_error + +CXX_STD = CXX14 + +all: $(SHLIB) + @if test -e "/usr/bin/install_name_tool" && test -e "/usr/local/clang4/lib/libc++.1.dylib" && test -e "/usr/lib/libc++.1.dylib"; then /usr/bin/install_name_tool -change /usr/local/clang4/lib/libc++.1.dylib /usr/lib/libc++.1.dylib $(SHLIB); fi + +clean: + rm -rf *.so *.o + +.phony: all clean diff --git a/R/src/Makevars.win b/R/src/Makevars.win new file mode 100644 index 0000000..1851eb4 --- /dev/null +++ b/R/src/Makevars.win @@ -0,0 +1,14 @@ +# Generated by rstantools. Do not edit by hand. + +STANHEADERS_SRC = $(shell "$(R_HOME)/bin$(R_ARCH_BIN)/Rscript" -e "message()" -e "cat(system.file('include', 'src', package = 'StanHeaders', mustWork = TRUE))" -e "message()" | grep "StanHeaders") + +PKG_CPPFLAGS = -I"../inst/include" -I"$(STANHEADERS_SRC)" -DBOOST_DISABLE_ASSERTS -DEIGEN_NO_DEBUG + +CXX_STD = CXX14 + +all: $(SHLIB) + +clean: + RM -rf *.so *.o + +.phony: all clean diff --git a/README.md b/README.md index 360f17e..a24468e 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,11 @@ Prophet is [open source software](https://code.facebook.com/projects/) released ## Installation in R -Prophet is temporarily not on CRAN so you can use the `devtools` package to install it directly from github: +Prophet is temporarily not on CRAN so you can use the `devtools` package to install it directly from GitHub: -``` -# R -> devtools::install_github("facebook/prophet", subdir='R', ref="8306ae3") +```R +# install.packages("remotes") +remotes::install_github("facebook/prophet", subdir = "R") ``` After installation, you can [get started!](https://facebook.github.io/prophet/docs/quick_start.html#r-api) @@ -38,11 +38,10 @@ If you have custom Stan compiler settings, install from source rather than the C ## Installation in Python -Prophet is on PyPI, so you can use pip to install it: +Prophet is on PyPI, so you can use `pip` to install it: -``` -# bash -$ pip install fbprophet +```bash +pip install fbprophet ``` The default dependency that Prophet has is `pystan`. PyStan has its own [installation instructions](http://pystan.readthedocs.io/en/latest/installation_beginner.html). Install pystan with pip before using pip to install fbprophet.