From b124b26c88e7321651adae03433ab0e552b88e77 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Sat, 9 Nov 2024 12:49:36 +0000 Subject: [PATCH 1/7] Add Prophet WASM example --- examples/forecasting/Cargo.toml | 10 ++- .../forecasting/examples/prophet_wasmstan.rs | 61 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 examples/forecasting/examples/prophet_wasmstan.rs diff --git a/examples/forecasting/Cargo.toml b/examples/forecasting/Cargo.toml index 1ccf8114..15e9a265 100644 --- a/examples/forecasting/Cargo.toml +++ b/examples/forecasting/Cargo.toml @@ -11,6 +11,14 @@ keywords.workspace = true publish = false [dependencies] -augurs = { workspace = true, features = ["ets", "mstl", "forecaster", "prophet", "prophet-cmdstan", "seasons"] } +augurs = { workspace = true, features = [ + "ets", + "mstl", + "forecaster", + "prophet", + "prophet-cmdstan", + "prophet-wasmstan", + "seasons", +] } tracing.workspace = true tracing-subscriber = { workspace = true, default-features = true } diff --git a/examples/forecasting/examples/prophet_wasmstan.rs b/examples/forecasting/examples/prophet_wasmstan.rs new file mode 100644 index 00000000..518280aa --- /dev/null +++ b/examples/forecasting/examples/prophet_wasmstan.rs @@ -0,0 +1,61 @@ +//! Example of using the Prophet model with the cmdstan optimizer. +//! +//! To run this example, you must first download the Prophet Stan model +//! and libtbb shared library into the `prophet_stan_model` directory. +//! The easiest way to do this is to run the `download-stan-model` +//! binary in the `augurs-prophet` crate: +//! +//! ```sh +//! $ cargo run --features download --bin download-stan-model +//! $ cargo run --example prophet_cmdstan +//! ``` +use std::{collections::HashMap, time::Duration}; + +use augurs::prophet::{cmdstan::CmdstanOptimizer, Prophet, Regressor, TrainingData}; + +fn main() -> Result<(), Box> { + tracing_subscriber::fmt::init(); + tracing::info!("Running Prophet example"); + + let ds = vec![ + 1704067200, 1704871384, 1705675569, 1706479753, 1707283938, 1708088123, 1708892307, + 1709696492, 1710500676, 1711304861, 1712109046, 1712913230, 1713717415, + ]; + let y = vec![ + 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, + ]; + let data = TrainingData::new(ds, y.clone())? + .with_regressors(HashMap::from([ + ( + "foo".to_string(), + vec![ + 1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, + ], + ), + ( + "bar".to_string(), + vec![ + 4.0, 5.0, 6.0, 4.0, 5.0, 6.0, 4.0, 5.0, 6.0, 4.0, 5.0, 6.0, 4.0, + ], + ), + ])) + .unwrap(); + + let cmdstan = CmdstanOptimizer::with_prophet_path("prophet_stan_model/prophet_model.bin")? + .with_poll_interval(Duration::from_millis(100)) + .with_refresh(50); + // If you were using the embedded version of the cmdstan model, you'd use this: + // let cmdstan = CmdstanOptimizer::new_embedded(); + + let mut prophet = Prophet::new(Default::default(), cmdstan); + prophet.add_regressor("foo".to_string(), Regressor::additive()); + prophet.add_regressor("bar".to_string(), Regressor::additive()); + + prophet.fit(data, Default::default())?; + let predictions = prophet.predict(None)?; + assert_eq!(predictions.yhat.point.len(), y.len()); + assert!(predictions.yhat.lower.is_some()); + assert!(predictions.yhat.upper.is_some()); + println!("Predicted values: {:#?}", predictions.yhat); + Ok(()) +} From 345e46484ae876b4721a81de41685c1a152fecaa Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Sat, 9 Nov 2024 13:12:25 +0000 Subject: [PATCH 2/7] Add some comments to rust workflow --- .github/workflows/rust.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 6c8a2d0d..f7e140e0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,7 +2,7 @@ name: Rust on: push: - branches: [ "main" ] + branches: ["main"] pull_request: env: @@ -39,15 +39,17 @@ jobs: with: bins: cargo-nextest,just + # Download the Prophet Stan model since an example requires it. - name: Download Prophet Stan model - # Download the Prophet Stan model since an example requires it. run: just download-prophet-stan-model + # Download the Prophet wasmstan module since an example and some doctests require it. - name: Download Prophet WASMStan component artifact uses: actions/download-artifact@v4 with: name: prophet-wasmstan.wasm path: crates/augurs-prophet + - name: Run cargo nextest run: just test-all - name: Run doc tests From 1813f8ebc4c4b8569643a904a9d89a7958a50cf7 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Sat, 9 Nov 2024 13:18:12 +0000 Subject: [PATCH 3/7] Fix build failures now that we have a WASM example --- crates/augurs-prophet/Cargo.toml | 11 ++++++----- crates/augurs-prophet/build.rs | 31 +++++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/crates/augurs-prophet/Cargo.toml b/crates/augurs-prophet/Cargo.toml index e44f5a49..4ebb72c2 100644 --- a/crates/augurs-prophet/Cargo.toml +++ b/crates/augurs-prophet/Cargo.toml @@ -45,15 +45,16 @@ bytemuck = ["dep:bytemuck"] cmdstan = ["dep:tempfile", "dep:serde_json", "serde"] compile-cmdstan = ["cmdstan", "dep:tempfile"] download = ["dep:ureq", "dep:zip"] -# Ignore cmdstan compilation in the build script. -# This should only be used for developing the library, not by -# end users, or you may end up with a broken build where the -# Prophet model isn't available to be compiled into the binary. -internal-ignore-cmdstan-failure = [] serde = ["dep:serde"] wasmstan = ["wasmstan-min"] wasmstan-min = ["dep:serde_json", "dep:wasmtime", "dep:wasmtime-wasi", "serde"] +# Ignore cmdstan compilation or wasmstan copying in the build script. +# This should only be used for developing the library, not by +# end users, or you may end up with a broken build where the +# Prophet model isn't available to be compiled into the binary. +internal-ignore-build-failures = [] + [lib] bench = false diff --git a/crates/augurs-prophet/build.rs b/crates/augurs-prophet/build.rs index d68dca6b..7652354c 100644 --- a/crates/augurs-prophet/build.rs +++ b/crates/augurs-prophet/build.rs @@ -88,19 +88,19 @@ fn handle_cmdstan() -> Result<(), Box> { // This will cause things to fail at runtime if there isn't a Stan // installation, but that's okay because no-one should ever use this // feature. - #[cfg(feature = "internal-ignore-cmdstan-failure")] + #[cfg(feature = "internal-ignore-build-failures")] if _result.is_err() { create_empty_files(&["prophet", "libtbb.so.12"])?; } // Do the same thing in docs.rs builds. - #[cfg(not(feature = "internal-ignore-cmdstan-failure"))] + #[cfg(not(feature = "internal-ignore-build-failures"))] if std::env::var("DOCS_RS").is_ok() { create_empty_files(&["prophet", "libtbb.so.12"])?; } // If we're not in a docs.rs build and we don't have the 'ignore' // feature enabled, then we should fail if there's an error. - #[cfg(not(feature = "internal-ignore-cmdstan-failure"))] + #[cfg(not(feature = "internal-ignore-build-failures"))] if std::env::var("DOCS_RS").is_err() { _result?; } @@ -129,13 +129,28 @@ fn handle_wasmstan() -> Result<(), Box> { let _result = Ok::<(), Box>(()); #[cfg(feature = "wasmstan")] let _result = copy_wasmstan(); - + // This is a complete hack but lets us get away with still using + // the `--all-features` flag of Cargo without everything failing + // if there isn't a WASM module built, which takes a while in CI + // and isn't available in docs.rs. + // Basically, if have this feature enabled, skip any failures in + // the build process and just create some empty files. + // This will cause things to fail at runtime if there isn't a WASM module + // present, but that's okay because no-one should ever use this feature. + #[cfg(feature = "internal-ignore-build-failures")] + if _result.is_err() { + create_empty_files(&["prophet-wasmstan.wasm"])?; + } + // Do the same thing in docs.rs builds. + #[cfg(not(feature = "internal-ignore-build-failures"))] if std::env::var("DOCS_RS").is_ok() { - // In docs.rs we won't have (or need) the wasmstan file in the current directory, - // so we should just create an empty one so the build doesn't fail. create_empty_files(&["prophet-wasmstan.wasm"])?; - } else { - // Otherwise, fail the build if there was an error. + } + + // If we're not in a docs.rs build and we don't have the 'ignore' + // feature enabled, then we should fail if there's an error. + #[cfg(not(feature = "internal-ignore-build-failures"))] + if std::env::var("DOCS_RS").is_err() { _result?; } Ok(()) From fa6bc96537edbeb377d88e36798d402bd5281bb9 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Sat, 9 Nov 2024 13:32:32 +0000 Subject: [PATCH 4/7] Enable all targets and features for check/clippy --- .github/workflows/rust.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f7e140e0..dc85bacb 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -20,7 +20,7 @@ jobs: uses: moonrepo/setup-rust@v1 - name: Run cargo check - run: cargo check --all-targets + run: cargo check --all-targets --all-features wasmstan: name: Prophet WASMStan component @@ -83,4 +83,4 @@ jobs: components: clippy - name: Run cargo clippy - run: cargo clippy -- -D warnings + run: cargo clippy --all-features --all-targets -- -D warnings From 40899f4360197dc9f0e5e57e8a80f18bf3d24775 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Sat, 9 Nov 2024 14:23:34 +0000 Subject: [PATCH 5/7] Improve build script error messages --- crates/augurs-prophet/build.rs | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/crates/augurs-prophet/build.rs b/crates/augurs-prophet/build.rs index 7652354c..0975c54d 100644 --- a/crates/augurs-prophet/build.rs +++ b/crates/augurs-prophet/build.rs @@ -49,13 +49,29 @@ fn compile_cmdstan_model() -> Result<(), Box> { // Copy the executable to the final location. let dest_exe_path = build_dir.join("prophet"); - std::fs::copy(tmp_exe_path, &dest_exe_path)?; + std::fs::copy(&tmp_exe_path, &dest_exe_path).map_err(|e| { + eprintln!( + "error copying prophet binary from {} to {}: {}", + tmp_exe_path.display(), + dest_exe_path.display(), + e + ); + e + })?; eprintln!("Copied prophet exe to {}", dest_exe_path.display()); // Copy libtbb to the final location. let libtbb_path = stan_path.join("lib/libtbb.so.12"); let dest_libtbb_path = build_dir.join("libtbb.so.12"); - std::fs::copy(&libtbb_path, &dest_libtbb_path)?; + std::fs::copy(&libtbb_path, &dest_libtbb_path).map_err(|e| { + eprintln!( + "error copying libtbb from {} to {}: {}", + libtbb_path.display(), + dest_libtbb_path.display(), + e + ); + e + })?; eprintln!( "Copied libtbb.so from {} to {}", libtbb_path.display(), @@ -109,6 +125,8 @@ fn handle_cmdstan() -> Result<(), Box> { #[cfg(feature = "wasmstan")] fn copy_wasmstan() -> Result<(), Box> { + println!("cargo::rerun-if-changed=prophet-wasmstan.wasm"); + let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?); let prophet_path = std::path::PathBuf::from(concat!( env!("CARGO_MANIFEST_DIR"), @@ -116,7 +134,15 @@ fn copy_wasmstan() -> Result<(), Box> { )) .canonicalize()?; let wasmstan_path = out_dir.join("prophet-wasmstan.wasm"); - std::fs::copy(&prophet_path, &wasmstan_path)?; + std::fs::copy(&prophet_path, &wasmstan_path).map_err(|e| { + eprintln!( + "error copying prophet-wasmstan from {} to {}: {}", + prophet_path.display(), + wasmstan_path.display(), + e + ); + e + })?; eprintln!( "Copied prophet-wasmstan.wasm from {} to {}", prophet_path.display(), From c9b989d12f7461be3e59e125d26fd65c800a7d44 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Sat, 9 Nov 2024 14:23:42 +0000 Subject: [PATCH 6/7] Explicitly include prophet-wasmstan.wasm in published package --- crates/augurs-prophet/Cargo.toml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/augurs-prophet/Cargo.toml b/crates/augurs-prophet/Cargo.toml index 4ebb72c2..76dd496c 100644 --- a/crates/augurs-prophet/Cargo.toml +++ b/crates/augurs-prophet/Cargo.toml @@ -8,6 +8,23 @@ version.workspace = true edition.workspace = true keywords.workspace = true description = "Prophet: time-series forecasting at scale, in Rust." +include = [ + ".gitignore", + "Cargo.toml", + "README.md", + "LICENSE-APACHE", + "LICENSE-MIT", + "CHANGELOG.md", + "build.rs", + "data", + "src/**/*", + "examples", + "tests", + "benches", + "prophet-wasmstan.wit", + "prophet-wasmstan.wasm", + "prophet.stan", +] [dependencies] anyhow.workspace = true From 4aba998af490af1a1e007485f78575c3222c9bfa Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Sat, 9 Nov 2024 14:40:20 +0000 Subject: [PATCH 7/7] Reorder download steps --- .github/workflows/rust.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index dc85bacb..ef01d728 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -39,10 +39,6 @@ jobs: with: bins: cargo-nextest,just - # Download the Prophet Stan model since an example requires it. - - name: Download Prophet Stan model - run: just download-prophet-stan-model - # Download the Prophet wasmstan module since an example and some doctests require it. - name: Download Prophet WASMStan component artifact uses: actions/download-artifact@v4 @@ -50,6 +46,10 @@ jobs: name: prophet-wasmstan.wasm path: crates/augurs-prophet + # Download the Prophet Stan model since an example requires it. + - name: Download Prophet Stan model + run: just download-prophet-stan-model + - name: Run cargo nextest run: just test-all - name: Run doc tests