diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index ec7db9a5..103afc41 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -272,5 +272,5 @@ jobs: run: | cargo clippy cargo fmt --check - cargo run --example=make_model - cargo test --verbose \ No newline at end of file + cargo run --example=example + cargo test --verbose diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index bef1b8b4..a41b8cb4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -33,12 +33,19 @@ jobs: - name: Set up Julia uses: julia-actions/setup-julia@v1 + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: "15.0" + directory: ${{ runner.temp }}/llvm + - name: Update version numbers if: ${{ !inputs.is_rerun }} run: | sed -i 's/Version:.*/Version: ${{ inputs.new_version }}/' R/DESCRIPTION sed -i 's/version = .*/version = "${{ inputs.new_version }}"/' julia/Project.toml sed -i 's/__version__ = .*/__version__ = "${{ inputs.new_version }}"/' python/bridgestan/__version.py + sed -i 's/^version = .*/version = "${{ inputs.new_version }}"/' rust/Cargo.toml sed -i 's/#define BRIDGESTAN_MAJOR .*/#define BRIDGESTAN_MAJOR '"$(echo ${{ inputs.new_version }} | cut -d. -f1)"'/' src/version.hpp sed -i 's/#define BRIDGESTAN_MINOR .*/#define BRIDGESTAN_MINOR '"$(echo ${{ inputs.new_version }} | cut -d. -f2)"'/' src/version.hpp @@ -103,6 +110,17 @@ jobs: packages_dir: python/dist/ skip_existing: true + - name: Publish Rust crate + if: ${{ !inputs.dry_run }} + run: | + cd rust/ + cargo publish --token ${CRATES_TOKEN} + env: + # clang is necessary unless we want to do --no-verify + LIBCLANG_PATH: ${{ runner.temp }}/llvm/lib + LLVM_CONFIG_PATH: ${{ runner.temp }}/llvm/bin/llvm-config + CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} + - name: Create JuliaRegistration comment if: ${{ !inputs.dry_run }} uses: peter-evans/commit-comment@v2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 31f06a83..56cadf7b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,6 +73,9 @@ We use [Gnu make](https://www.gnu.org/software/make/) for builds. If you have p * Julia code is formatted using [JuliaFormatter](https://github.com/domluna/JuliaFormatter.jl). +### Rust development + +* Rust development is based on `cargo`, which should handle dependencies, testing, and formatting. ## Proposing a new interface language diff --git a/README.md b/README.md index c5bf4d57..011b6d83 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ to download the appropriate Stan compiler for your platform into ### Example programs This repository includes examples of calling Stan through BridgeStan -in Python, Julia, R, and C. +in Python, Julia, R, Rust, and C. * From Python: [`example.py`](python/example.py) @@ -68,6 +68,8 @@ in Python, Julia, R, and C. * From R: [`example.r`](R/example.R) +* From Rust: [`example.rs`](rust/examples/example.rs) + * From C: [`example.c`](c-example/example.c) Examples of other functionality can be found in the `test` folder for each interface. @@ -81,3 +83,6 @@ API, which in turn was derived from Thanks to Sebastian Weber (GitHub [@wds15](https://github.com/wds15)) for enabling multi-threaded calls from Julia to a single Stan model instance. + +Thanks to Adrian Seyboldt (GitHub [@aseyboldt](https://github.com/aseyboldt)) +for providing the Rust wrapper. diff --git a/docs/internals/ffi.rst b/docs/internals/ffi.rst index 9b244c42..528d7d85 100644 --- a/docs/internals/ffi.rst +++ b/docs/internals/ffi.rst @@ -63,6 +63,18 @@ as well as an extended Note: One quirk of the ``.C`` interface is the requirement that all inputs and return values are passed by pointers. This is the reason for the ``bridgestan_R`` files in the source. +Rust +____ + +The Rust interface uses two crates in addition to the built-in +`FFI types `__. +Some general guidance on Rust FFI can be found in the +`Rust Book `__. + +These are `bindgen `__, which generates the Rust bindings from the C headers, +and `libloading `__, which provides easy dynamic library loading functionality. + +Care is taken to provide "safe" Rust wrappers so that users of the crate do not need to use the `unsafe` keyword. General Problems ---------------- @@ -75,7 +87,8 @@ must also be freed on that side. This means special consideration is needed to pass strings back and forth between the languages, and inspired some of the design decisions behind ideas like returning the parameter names as a comma separated list, rather than the more "natural" -array of strings. +array of strings, and this is why error messages must be passed back to the +library in order to be freed. Output Streams ______________ @@ -85,3 +98,5 @@ This is particularly relevant for error messaging, which is printed to the stand error output ``stderr`` from C++. This does *not*, for example, correspond to the ``sys.stderr`` stream available from Python. +We tackle this problem through the use of an interface-provided callback function +when necessary. diff --git a/docs/languages.rst b/docs/languages.rst index bbdecfff..c52d1412 100644 --- a/docs/languages.rst +++ b/docs/languages.rst @@ -2,7 +2,7 @@ Language Interfaces =================== -BridgeStan currently has clients in three languages, a public C API +BridgeStan currently has clients in four languages, a public C API which underlies all the clients, and an example of a standalone program written in C. @@ -20,6 +20,7 @@ If you are missing these features in your favorite language, we would welcome a languages/python languages/julia languages/r + languages/rust languages/c-api diff --git a/docs/languages/rust.rst b/docs/languages/rust.rst new file mode 100644 index 00000000..a7652cce --- /dev/null +++ b/docs/languages/rust.rst @@ -0,0 +1,47 @@ +Rust Interface +============== + +`See the BridgeStan Crate documentation on docs.rs `__ + +---- + +Installation +------------ + +The BridgeStan Rust client is available on `crates.io `__ and via ``cargo``: + +.. code-block:: shell + + cargo add bridgestan + +To build and use BridgeStan models, a copy of the BridgeStan C++ source code +is required. Please follow the :doc:`Getting Started guide <../getting-started>` +or use the Rust client in tandem with an interface such as :doc:`Python <./python>` +which automates this process. + +``STAN_THREADS=true`` needs to be specified when compiling a model, for more +details see the `API reference `__. + +Example Program +--------------- + +An example program is provided alongside the Rust crate in ``examples/example.rs``: + +.. raw:: html + +
+ Show example.rs + + +.. literalinclude:: ../../rust/examples/example.rs + :language: Rust + +.. raw:: html + +
+ + +API Reference +------------- + +See docs.rs for the full API reference: ``__ diff --git a/rust/README.md b/rust/README.md index afdebc2d..c5a9fede 100644 --- a/rust/README.md +++ b/rust/README.md @@ -1,17 +1,43 @@ # BridgeStan from Rust -This is a Rust wrapper for [BridgeStan](https://roualdes.github.io/bridgestan/latest/). +[*View the BridgeStan documentation on Github Pages*](https://roualdes.github.io/bridgestan/latest/languages/rust.html). -It relies on [`bindgen`](https://docs.rs/bindgen/) and [`libloading`](https://docs.rs/libloading/). +This is a Rust wrapper for [BridgeStan](https://github.com/roualdes/bridgestan). It +allows users to evaluate the log likelihood and related functions for Stan models +natively from Rust. -The Rust wrapper does not have any functionality to compile Stan models. +Internally, it relies on [`bindgen`](https://docs.rs/bindgen/) and +[`libloading`](https://docs.rs/libloading/). + +## Compiling the model + +The Rust wrapper does not currently have any functionality to compile Stan models. Compiled shared libraries need to be built manually using `make` or with the Julia or Python bindings. +For safety reasons all Stan models need to be installed with `STAN_THREADS=true`. +When compiling a model using `make`, set the environment variable: + +```bash +STAN_THREADS=true make some_model +``` + +When compiling a Stan model in python, this has to be specified in the `make_args` +argument: + +```python +path = bridgestan.compile_model("stan_model.stan", make_args=["STAN_THREADS=true"]) +``` + +If `STAN_THREADS` was not specified while building the model, the Rust wrapper +will throw an error when loading the model. + ## Usage: +Run this example with `cargo run --example=example`. + ```rust -use std::ffi::{OsStr, CString}; +use std::ffi::CString; use std::path::Path; use bridgestan::{BridgeStanError, Model, open_library}; @@ -22,7 +48,7 @@ let path = Path::new(env!["CARGO_MANIFEST_DIR"]) .unwrap() .join("test_models/simple/simple_model.so"); -let lib = open_library(path).expect("Could not load compiled stan model."); +let lib = open_library(path).expect("Could not load compiled Stan model."); // The dataset as json let data = r#"{"N": 7}"#; @@ -35,7 +61,7 @@ let seed = 42; let model = match Model::new(&lib, Some(data), seed) { Ok(model) => { model }, Err(BridgeStanError::ConstructFailed(msg)) => { - panic!("Model initialization failed. Error message from stan was {}", msg) + panic!("Model initialization failed. Error message from Stan was {}", msg) }, _ => { panic!("Unexpected error") }, }; diff --git a/rust/examples/make_model.rs b/rust/examples/example.rs similarity index 97% rename from rust/examples/make_model.rs rename to rust/examples/example.rs index f45cfa69..0e32d213 100644 --- a/rust/examples/make_model.rs +++ b/rust/examples/example.rs @@ -10,7 +10,7 @@ fn main() { .unwrap() .join("test_models/simple/simple_model.so"); - let lib = open_library(path).expect("Could not load compiled stan model."); + let lib = open_library(path).expect("Could not load compiled Stan model."); // The dataset as json let data = r#"{"N": 7}"#; @@ -24,7 +24,7 @@ fn main() { Ok(model) => model, Err(BridgeStanError::ConstructFailed(msg)) => { panic!( - "Model initialization failed. Error message from stan was {}", + "Model initialization failed. Error message from Stan was {}", msg ) } diff --git a/rust/src/bs_safe.rs b/rust/src/bs_safe.rs index 1823165a..79726d5a 100644 --- a/rust/src/bs_safe.rs +++ b/rust/src/bs_safe.rs @@ -18,7 +18,7 @@ use std::time::Instant; // This is more or less equivalent to manually defining Display and From use thiserror::Error; -/// A loaded shared library for a stan model +/// A loaded shared library for a Stan model pub struct StanLibrary { lib: ManuallyDrop, id: u64, @@ -35,17 +35,17 @@ impl Drop for StanLibrary { } } -/// A callback for print statements in stan models +/// A callback for print statements in Stan models pub type StanPrintCallback = extern "C" fn(*const c_char, usize); impl StanLibrary { - /// Provide a callback function to be called when stan prints a message + /// Provide a callback function to be called when Stan prints a message /// /// # Safety /// /// The provided function must never panic. /// - /// Since the call is proteted by a mutex internally, it does not + /// Since the call is protected by a mutex internally, it does not /// need to be thread safe. pub unsafe fn set_print_callback(&mut self, callback: StanPrintCallback) -> Result<()> { let mut err = ErrorMsg::new(self); @@ -58,7 +58,7 @@ impl StanLibrary { } } - /// Unload the stan library. + /// Unload the Stan library. /// /// # Safety /// @@ -80,28 +80,35 @@ pub struct LoadingError(#[from] libloading::Error); #[derive(Error, Debug)] #[non_exhaustive] pub enum BridgeStanError { + /// The provided library could not be loaded. #[error(transparent)] InvalidLibrary(#[from] LoadingError), + /// The version of the Stan library does not match the version of the rust crate. #[error("Bad Stan library version: Got {0} but expected {1}")] BadLibraryVersion(String, String), + /// The Stan library could not be loaded because it was compiled without threading support. #[error("The Stan library was compiled without threading support. Config was {0}")] StanThreads(String), + /// Stan returned a string that couldn't be decoded using UTF8. #[error("Failed to decode string to UTF8")] InvalidString(#[from] Utf8Error), + /// The model could not be instanciated, possibly because if incorrect data. #[error("Failed to construct model: {0}")] ConstructFailed(String), + /// Stan returned an error while computing the density. #[error("Failed during evaluation: {0}")] EvaluationFailed(String), + /// Setting a print-callback failed. #[error("Failed to set a print-callback: {0}")] SetCallbackFailed(String), } type Result = std::result::Result; -/// Open a compiled stan library. +/// Open a compiled Stan library. /// -/// The library should have been compiled with bridgestan, -/// with the same version as the rust library. +/// The library should have been compiled with BridgeStan, +/// with the same version as the Rust library. pub fn open_library>(path: P) -> Result { let library = unsafe { libloading::Library::new(&path) }.map_err(LoadingError)?; let major: libloading::Symbol<*const c_int> = @@ -143,12 +150,17 @@ pub struct Model> { unsafe impl> Sync for Model {} unsafe impl> Send for Model {} -/// A random number generator for Stan +/// A random number generator for Stan models. +/// This is only used in the `param_contrain` method +/// of the model when requesting values from the `generated quantities` block. +/// Different threads should use different instances. pub struct Rng> { rng: NonNull, lib: T, } +// Use sites require exclusive reference which guarantees +// that the rng is not used in multiple threads concurrently. unsafe impl> Sync for Rng {} unsafe impl> Send for Rng {} @@ -204,7 +216,7 @@ impl<'lib> ErrorMsg<'lib> { /// Return the error message as a String. /// - /// Panics if there was no error message. + /// *Panics* if there was no error message. fn message(&self) -> String { NonNull::new(self.msg) .map(|msg| { @@ -244,7 +256,7 @@ impl> Model { if let Some(model) = NonNull::new(model) { drop(err); let model = Self { model, lib }; - // If STAN_THREADS is not true, the safty guaranties we are + // If STAN_THREADS is not true, the safety guaranties we are // making would be incorrect let info = model.info(); if !info.to_string_lossy().contains("STAN_THREADS=true") { @@ -259,11 +271,19 @@ impl> Model { } } - /// Return a reference to the underlying stan library + /// Return a reference to the underlying Stan library pub fn ref_library(&self) -> &StanLibrary { self.lib.borrow() } + /// Create a new `Rng` random number generator from the library underlying this model. + /// + /// This can be used in `param_constrain` when values from the `generated quantities` + /// block are desired. + /// + /// This instance can only be used with models from the same + /// Stan library. Invalid usage will otherwise result in a + /// panic. pub fn new_rng(&self, seed: u32) -> Result> { Rng::new(self.ref_library(), seed) } @@ -285,14 +305,14 @@ impl> Model { /// /// The parameters are returned in the order they are declared. /// Multivariate parameters are return in column-major (more - /// generally last-index major) order. Parameters are separated with - /// periods (`.`). For example, `a[3]` is written `a.3` and `b[2, - /// 3]` as `b.2.3`. The numbering follows Stan and is indexed from 1. + /// generally last-index major) order. Parameter indices are separated + /// with periods (`.`). For example, `a[3]` is written `a.3` and `b[2, 3]` + /// as `b.2.3`. The numbering follows Stan and is indexed from 1. /// - /// # Arguments - /// - /// `include_tp`: Include transformed parameters - /// `include_gp`: Include generated quantities + /// If `include_tp` is set the names will also include the transformed + /// parameters of the Stan model after the parameters. If `include_gq` is + /// set, we also include the names of the generated quantities at + /// the very end. pub fn param_names(&self, include_tp: bool, include_gq: bool) -> &str { let cstr = unsafe { CStr::from_ptr(self.ffi_lib().bs_param_names( @@ -311,7 +331,7 @@ impl> Model { /// /// The parameters are returned in the order they are declared. /// Multivariate parameters are return in column-major (more - /// generally last-index major) order. Parameters are separated with + /// generally last-index major) order. Parameter indices are separated with /// periods (`.`). For example, `a[3]` is written `a.3` and `b[2, /// 3]` as `b.2.3`. The numbering follows Stan and is indexed from 1. pub fn param_unc_names(&mut self) -> &str { @@ -322,7 +342,9 @@ impl> Model { } /// Number of parameters in the model on the constrained scale. - /// Will also count transformed parameters and generated quantities if requested + /// + /// Will also count transformed parameters (`include_tp`) and generated + /// quantities (`include_gq`) if requested. pub fn param_num(&self, include_tp: bool, include_gq: bool) -> usize { unsafe { self.ffi_lib() @@ -333,6 +355,7 @@ impl> Model { } /// Return the number of parameters on the unconstrained scale. + /// /// In particular, this is the size of the slice required by the log_density functions. pub fn param_unc_num(&self) -> usize { unsafe { self.ffi_lib().bs_param_unc_num(self.model.as_ptr()) } @@ -342,8 +365,9 @@ impl> Model { /// Compute the log of the prior times likelihood density /// - /// Drop jacobian determinant terms if `jacobian == false` and - /// drop constant terms of the density if `propto == true`. + /// Drop jacobian determinant terms of the transformation from unconstrained + /// to the constrained space if `jacobian == false` and drop terms + /// of the density that do not depend on the parameters if `propto == true`. pub fn log_density(&self, theta_unc: &[f64], propto: bool, jacobian: bool) -> Result { let n = self.param_unc_num(); assert_eq!( @@ -374,9 +398,14 @@ impl> Model { /// Compute the log of the prior times likelihood density and its gradient /// - /// Drop jacobian determinant terms if `jacobian == false` and - /// drop constant terms of the density if `propto == true`. - /// The gradient of the log density is stored in `grad`. + /// Drop jacobian determinant terms of the transformation from unconstrained + /// to the constrained space if `jacobian == false` and drop terms + /// of the density that do not depend on the parameters if `propto == true`. + /// + /// The gradient of the log density will be stored in `grad`. + /// + /// *Panics* if the provided buffer has incorrect shape. The gradient buffer `grad` + /// must have length `self.param_unc_num()`. pub fn log_density_gradient( &self, theta_unc: &[f64], @@ -418,12 +447,18 @@ impl> Model { } } - /// Compute the log of the prior times likelihood density and gradient and hessian. + /// Compute the log of the prior times likelihood density and its gradient and hessian. + /// + /// Drop jacobian determinant terms of the transformation from unconstrained + /// to the constrained space if `jacobian == false` and drop terms + /// of the density that do not depend on the parameters if `propto == true`. /// - /// Drop jacobian determinant terms if `jacobian == false` and - /// drop constant terms of the density if `propto == true`. - /// The gradient of the log density is stored in `grad`, the + /// The gradient of the log density will be stored in `grad`, the /// hessian is stored in `hessian`. + /// + /// *Panics* if the provided buffers have incorrect shapes. The gradient buffer `grad` + /// must have length `self.param_unc_num()` and the `hessian` buffer must + /// have length `self.param_unc_num() * self.param_unc_num()`. pub fn log_density_hessian( &self, theta_unc: &[f64], @@ -472,20 +507,17 @@ impl> Model { } } - /// Map a point in unconstrained parameter space to the constrained space - /// - /// # Arguments + /// Map a point in unconstrained parameter space to the constrained space. /// - /// `theta_unc`: The point in the unconstained parameter space. + /// `theta_unc` must contain the point in the unconstrained parameter space. /// - /// `include_tp`: Include transformed parameters + /// If `include_tp` is set the output will also include the transformed + /// parameters of the Stan model after the parameters. If `include_gq` is + /// set, we also include the generated quantities at the very end. /// - /// `include_gq`: Include generated quantities - /// - /// `out`: Array of length `self.param_num(include_tp, include_gp)`, where - /// the constrained parameters will be stored. - /// - /// `rng`: A Stan random number generator. Has to be provided if `include_gp`. + /// *Panics* if the provided buffer has incorrect shape. The length of the `out` buffer + /// `self.param_num(include_tp, include_gq)`. + /// *Panics* if `include_gq` is set but no random number generator is provided. pub fn param_constrain>( &self, theta_unc: &[f64], @@ -517,7 +549,7 @@ impl> Model { if let Some(rng) = &rng { assert!( rng.lib.borrow().id == self.lib.borrow().id, - "Rng and model must come from the same stan library" + "Rng and model must come from the same Stan library" ); } @@ -541,16 +573,7 @@ impl> Model { } } - /// Set the sequence of unconstrained parameters based on the - /// specified constrained parameters, and return a return code of 0 - /// for success and -1 for failure. Parameter order is as declared - /// in the Stan program, with multivariate parameters given in - /// last-index-major order. - /// - /// # Arguments - /// `theta`: The vector of constrained parameters - /// - /// `theta_unc` Vector of unconstrained parameters + /// Map a point in constrained parameter space to the unconstrained space. pub fn param_unconstrain(&self, theta: &[f64], theta_unc: &mut [f64]) -> Result<()> { assert_eq!( theta_unc.len(), @@ -611,7 +634,7 @@ impl> Model { } impl + Clone> Model { - /// Return a clone of the underlying stan library + /// Return a clone of the underlying Stan library pub fn clone_library_ref(&self) -> T { self.lib.clone() } diff --git a/rust/tests/common.rs b/rust/tests/common.rs index 8e54d0aa..5d8253e7 100644 --- a/rust/tests/common.rs +++ b/rust/tests/common.rs @@ -12,7 +12,7 @@ pub fn model_dir() -> PathBuf { .join("test_models") } -/// Load stan library and corresponding data if available +/// Load Stan library and corresponding data if available pub fn get_model>(name: S) -> (StanLibrary, Option) { let name = name.as_ref(); let mut base = model_dir();