Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Go

on:
push:
branches: ["main"]
pull_request:

env:
CARGO_TERM_COLOR: always

permissions: {}

jobs:
go-bindings:
name: Go bindings
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false

- name: Install Rust toolchain
uses: moonrepo/setup-rust@ede6de059f8046a5e236c94046823e2af11ca670 # v1.2.2
with:
channel: nightly-2025-09-21
targets: x86_64-unknown-linux-gnu,wasm32-unknown-unknown
bins: just
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Install Go toolchain
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version: "1.25"
cache-dependency-path: go/go.sum

- name: Install gravity
# Use a custom fork of gravity with a bunch of extra features.
run: cargo install --git https://github.com/sd2k/gravity --rev 20dee8fd100bb70274c7c05f37cc8bde1059bae0

- name: Build Wasm components
run: just go/build-wasm

- name: Generate Go bindings
run: just go/generate

- name: Test Go bindings
run: just go/test

- name: Ensure no diffs
run: git diff --exit-code -- ./go/*/*.go
8 changes: 5 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [
"components/rust/*",
"crates/*",
"examples/*",
"js/*",
Expand Down Expand Up @@ -56,9 +57,9 @@ rand = "0.9.0"
rand_distr = "0.5.1"
roots = "0.0.8"
serde = { version = "1.0.166", features = ["derive"] }
statrs = "0.18.0"
serde_json = "1.0.128"
serde-wasm-bindgen = "0.6.0"
statrs = "0.18.0"
thiserror = "2.0.3"
tinyvec = "1.6.0"
tracing = "0.1.40"
Expand Down Expand Up @@ -97,8 +98,9 @@ debug = true

[patch.crates-io]
# rand 0.9 merged into main, waiting for a new release.
argmin = { git = "https://github.com/argmin-rs/argmin", branch = "main" }
argmin-math = { git = "https://github.com/argmin-rs/argmin", branch = "main" }
# meanwhile, had to fork to add a feature flag for timers
argmin = { git = "https://github.com/sd2k/argmin", branch = "timer-feature" }
argmin-math = { git = "https://github.com/sd2k/argmin", branch = "timer-feature" }

# Depends on rv PR and release (see below).
changepoint = { git = "https://github.com/sd2k/changepoint", branch = "patched-deps-to-bump-rand" }
Expand Down
7 changes: 7 additions & 0 deletions components/rust/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Rust Wasm Components

This directory contains various Wasm components implemented in Rust using the augurs library.

The resulting Wasm files can be used either directly or by generating host bindings for a specific language using e.g. [`gravity`](https://github.com/arcjet/gravity).

See the `go` directory for a Go package which is generated from these components. That directory also contains a `justfile` for building the components, which requires some specific `RUSTFLAGS`.
21 changes: 21 additions & 0 deletions components/rust/changepoint/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "augurs-changepoint-component"
license.workspace = true
authors.workspace = true
documentation.workspace = true
repository.workspace = true
version.workspace = true
edition.workspace = true
keywords.workspace = true
publish = false

[lib]
crate-type = ["cdylib"]

[dependencies]
augurs-changepoint.workspace = true
wit-bindgen.workspace = true
wit-component.workspace = true

[lints]
workspace = true
133 changes: 133 additions & 0 deletions components/rust/changepoint/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//! Implementation of a Wasm component that can perform changepoint detection on time series.

use std::{fmt, num::NonZeroUsize};

use augurs_changepoint::{
self, dist::NormalGamma, BocpdDetector, DefaultArgpcpDetector, DefaultArgpcpDetectorBuilder,
Detector,
};

// Wrap the wit-bindgen macro in a module so we don't get warned about missing docs in the generated trait.
mod bindings {
wit_bindgen::generate!({
world: "changepoint",
default_bindings_module: "bindings",
});
}
use crate::bindings::{
export,
grafana::augurs::types::{Algorithm, ArgpcpParams, Input, NormalGammaParams, Output},
Guest,
};

struct ChangepointWorld;
export!(ChangepointWorld);

impl Guest for ChangepointWorld {
fn detect(input: Input) -> Result<Output, String> {
detect(input).map_err(|e| e.to_string())
}
}

/// An error type for the changepoint detector.
#[derive(Debug)]
pub enum ChangepointError {
/// An invalid parameter was provided to the Normal Gamma distribution.
NormalGammaError(augurs_changepoint::dist::NormalGammaError),
/// An overflow occurred when converting an integer to a `NonZeroUsize`.
TryFromIntError(std::num::TryFromIntError),
/// An invalid parameter was provided to the max lag.
InvalidMaxLag(u32),
}

impl From<std::num::TryFromIntError> for ChangepointError {
fn from(value: std::num::TryFromIntError) -> Self {
Self::TryFromIntError(value)
}
}

impl From<augurs_changepoint::dist::NormalGammaError> for ChangepointError {
fn from(value: augurs_changepoint::dist::NormalGammaError) -> Self {
Self::NormalGammaError(value)
}
}

impl fmt::Display for ChangepointError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NormalGammaError(e) => write!(f, "invalid Normal Gamma distribution: {}", e),
Self::TryFromIntError(e) => write!(f, "overflow converting to usize: {}", e),
Self::InvalidMaxLag(ml) => write!(f, "invalid max lag: {}", ml),
}
}
}

impl std::error::Error for ChangepointError {}

impl TryFrom<ArgpcpParams> for DefaultArgpcpDetectorBuilder {
type Error = ChangepointError;

fn try_from(params: ArgpcpParams) -> Result<Self, Self::Error> {
let mut builder = DefaultArgpcpDetector::builder();
if let Some(cv) = params.constant_value {
builder = builder.constant_value(cv);
}
if let Some(ls) = params.length_scale {
builder = builder.length_scale(ls);
}
if let Some(nl) = params.noise_level {
builder = builder.noise_level(nl);
}
if let Some(ml) = params.max_lag {
let ml = NonZeroUsize::new(ml.try_into().map_err(ChangepointError::TryFromIntError)?)
.ok_or(ChangepointError::InvalidMaxLag(ml))?;
builder = builder.max_lag(ml);
}
if let Some(a0) = params.alpha0 {
builder = builder.alpha0(a0);
}
if let Some(b0) = params.beta0 {
builder = builder.beta0(b0);
}
if let Some(h) = params.logistic_hazard.h {
builder = builder.logistic_hazard_h(h);
}
if let Some(a) = params.logistic_hazard.a {
builder = builder.logistic_hazard_a(a);
}
if let Some(b) = params.logistic_hazard.b {
builder = builder.logistic_hazard_b(b);
}
Ok(builder)
}
}

fn convert_normal_gamma(
params: Option<NormalGammaParams>,
) -> Result<NormalGamma, ChangepointError> {
Ok(NormalGamma::new(
params.and_then(|p| p.mu).unwrap_or(0.0),
params.and_then(|p| p.rho).unwrap_or(1.0),
params.and_then(|p| p.s).unwrap_or(1.0),
params.and_then(|p| p.v).unwrap_or(1.0),
)?)
}

fn detect(input: Input) -> Result<Output, ChangepointError> {
match input.algorithm {
Algorithm::Argpcp(params) => Ok(DefaultArgpcpDetectorBuilder::try_from(params)?
.build()
.detect_changepoints(&input.data)
.into_iter()
.map(|i| i.try_into())
.collect::<Result<_, _>>()?),
Algorithm::Bocpd(params) => Ok(BocpdDetector::normal_gamma(
params.hazard_lambda.unwrap_or(250.0),
convert_normal_gamma(params.normal_gamma_params)?,
)
.detect_changepoints(&input.data)
.into_iter()
.map(|i| i.try_into())
.collect::<Result<_, _>>()?),
}
}
118 changes: 118 additions & 0 deletions components/rust/changepoint/wit/changepoint.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/// The augurs package.
package grafana:augurs;

/// Types used by the changepoint detector world.
interface types {
/// The input to changepoint detection.
record input {
/// The data to detect changepoints in.
data: list<f64>,

/// The algorithm to use.
algorithm: algorithm,
}

/// The indices of the most likely changepoints.
type output = list<u32>;

/// The parameters for the logistic hazard function.
///
/// Any unset parameters will use the default values.
record logistic-hazard {
/// Logit scaled, scaling factor for the logistic hazard function.
///
/// Increasing increases hazard over the whole space; more negative numbers
/// result in larger run lengths.
///
/// Defaults to -5.0.
h: option<f64>,
/// Scale term. Roughly, the slope of the logistic hazard function.
///
/// Higher numbers increase the variance of run-lengths.
///
/// Defaults to 1.0.
a: option<f64>,
/// Translation term (increasing moves the logistic to the left).
///
/// Defaults to 1.0.
b: option<f64>,
}

/// The parameters for the Autoregressive Gaussian Process Changepoint Detection (Argpcp) algorithm.
///
/// Any unset parameters will use the default values.
record argpcp-params {
/// The value of the constant kernel.
constant-value: option<f64>,
/// The length scale of the RBF kernel.
length-scale: option<f64>,
/// The noise level of the white kernel.
noise-level: option<f64>,
/// The maximum autoregressive lag.
max-lag: option<u32>,
/// The scale Gamma distribution alpha parameter.
alpha0: option<f64>,
/// The scale Gamma distribution beta parameter.
beta0: option<f64>,

/// The logistic hazard function parameters.
logistic-hazard: logistic-hazard,
}

/// The parameters for the Normal Gamma prior.
///
/// Any unset parameters will use the default values.
record normal-gamma-params {
/// The prior mean.
///
/// Defaults to 0.0.
mu: option<f64>,
/// The relative precision of μ versus data.
///
/// Defaults to 1.0.
rho: option<f64>,
/// The mean of rho (the precision) is v/s.
///
/// Defaults to 1.0.
s: option<f64>,
/// The degrees of freedom of precision of rho.
///
/// Defaults to 1.0.
v: option<f64>,
}

/// The parameters for the Bayesian Online Changepoint Detection (BOCPD) algorithm.
///
/// Any unset parameters will use the default values.
record bocpd-params {
/// The hazard lambda.
///
/// `1/hazard` is the probability of the next step being a changepoint.
/// Therefore, the larger the value, the lower the prior probability
/// is for the any point to be a change-point.
/// Mean run-length is lambda - 1.
///
/// Defaults to 250.0.
hazard-lambda: option<f64>,

/// The parameters of the Normal Gamma prior.
normal-gamma-params: option<normal-gamma-params>,
}

/// A changepoint detection algorithm.
variant algorithm {
/// The Autoregressive Gaussian Process Changepoint Detection (Argpcp) algorithm.
argpcp(argpcp-params),
/// The Bayesian Online Changepoint Detection (BOCPD) algorithm.
///
/// The algorithm is created with a Normal Gamma prior. Other priors
/// are not yet supported.
bocpd(bocpd-params),
}
}

world changepoint {
use types.{input, output};
/// Detect changepoints in the input.
export detect: func(input: input) -> result<output, string>;
}
21 changes: 21 additions & 0 deletions components/rust/outlier/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "augurs-outlier-component"
license.workspace = true
authors.workspace = true
documentation.workspace = true
repository.workspace = true
version.workspace = true
edition.workspace = true
keywords.workspace = true
publish = false

[lib]
crate-type = ["cdylib"]

[dependencies]
augurs-outlier.workspace = true
wit-bindgen.workspace = true
wit-component.workspace = true

[lints]
workspace = true
Loading
Loading