Skip to content

Commit

Permalink
feat: add 'transforms' JS crate and include in augurs JS bindings
Browse files Browse the repository at this point in the history
This provides a way to access the power transform functions from JS.

I'd like to add the Forecaster functionality to the Prophet bindings
somehow but that limits the Prophet APIs to only return yhat and
the bounds. That's probably enough for many cases but it's not ideal.

This is a quick workaround to get the transform functionality into the
JS package really.
  • Loading branch information
sd2k committed Dec 11, 2024
1 parent bfd842a commit b8013fd
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 4 deletions.
4 changes: 2 additions & 2 deletions crates/augurs-forecaster/src/transforms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ impl Transform {
}

/// Apply the transformation to the given time series.
pub(crate) fn transform<'a, T>(&'a self, input: T) -> Box<dyn Iterator<Item = f64> + 'a>
pub fn transform<'a, T>(&'a self, input: T) -> Box<dyn Iterator<Item = f64> + 'a>
where
T: Iterator<Item = f64> + 'a,
{
Expand All @@ -166,7 +166,7 @@ impl Transform {
}

/// Apply the inverse transformation to the given time series.
pub(crate) fn inverse_transform<'a, T>(&'a self, input: T) -> Box<dyn Iterator<Item = f64> + 'a>
pub fn inverse_transform<'a, T>(&'a self, input: T) -> Box<dyn Iterator<Item = f64> + 'a>
where
T: Iterator<Item = f64> + 'a,
{
Expand Down
34 changes: 34 additions & 0 deletions js/augurs-transforms-js/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[package]
name = "augurs-transforms-js"
version.workspace = true
authors.workspace = true
documentation.workspace = true
repository.workspace = true
license.workspace = true
edition.workspace = true
keywords.workspace = true
description = "JavaScript bindings for augurs' data transformations."
publish = false

[lib]
bench = false
crate-type = ["cdylib", "rlib"]
doc = false
doctest = false
test = false

[dependencies]
augurs-core-js.workspace = true
augurs-forecaster.workspace = true
getrandom.workspace = true
serde.workspace = true
serde-wasm-bindgen.workspace = true
tsify-next.workspace = true
wasm-bindgen.workspace = true

[package.metadata.wasm-pack.profile.release]
# previously had just ['-O4']
wasm-opt = ['-O4', '--enable-bulk-memory', '--enable-threads']

[lints]
workspace = true
63 changes: 63 additions & 0 deletions js/augurs-transforms-js/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//! Javascript bindings for augurs transformations, such as power transforms, scaling, etc.
use serde::Deserialize;
use tsify_next::Tsify;
use wasm_bindgen::prelude::*;

use augurs_core_js::VecF64;
use augurs_forecaster::transforms::Transform;

/// A power transform.
///
/// This transform applies the power function to each item.
///
/// If all values are positive, it will use the Box-Cox transform.
/// If any values are negative or zero, it will use the Yeo-Johnson transform.
///
/// The optimal value of the `lambda` parameter is calculated from the data
/// using maximum likelihood estimation.
#[derive(Debug)]
#[wasm_bindgen]
pub struct PowerTransform {
inner: Transform,
}

#[wasm_bindgen]
impl PowerTransform {
/// Create a new power transform for the given data.
#[wasm_bindgen(constructor)]
pub fn new(opts: PowerTransformOptions) -> Result<PowerTransform, JsError> {
Ok(PowerTransform {
inner: Transform::power_transform(&opts.data)
.map_err(|e| JsError::new(&e.to_string()))?,
})
}

/// Transform the given data.
#[wasm_bindgen]
pub fn transform(&self, data: VecF64) -> Result<Vec<f64>, JsError> {
Ok(self
.inner
.transform(data.convert()?.iter().copied())
.collect())
}

/// Inverse transform the given data.
#[wasm_bindgen(js_name = "inverseTransform")]
pub fn inverse_transform(&self, data: VecF64) -> Result<Vec<f64>, JsError> {
Ok(self
.inner
.inverse_transform(data.convert()?.iter().copied())
.collect())
}
}

/// Options for the power transform.
#[derive(Debug, Default, Deserialize, Tsify)]
#[serde(rename_all = "camelCase")]
#[tsify(from_wasm_abi)]
pub struct PowerTransformOptions {
/// The data to transform. This is used to calculate the optimal value of 'lambda'.
#[tsify(type = "number[] | Float64Array")]
pub data: Vec<f64>,
}
3 changes: 2 additions & 1 deletion js/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ build: \
(build-inner "mstl") \
(build-inner "outlier") \
(build-inner "prophet") \
(build-inner "seasons")
(build-inner "seasons") \
(build-inner "transforms")
just fix-package-json

build-inner target args='':
Expand Down
3 changes: 2 additions & 1 deletion js/package.json.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"./mstl": "./mstl.js",
"./prophet": "./prophet.js",
"./outlier": "./outlier.js",
"./seasons": "./seasons.js"
"./seasons": "./seasons.js",
"./transforms": "./transforms.js"
},
"types": "augurs.d.ts",
"sideEffects": [
Expand Down
37 changes: 37 additions & 0 deletions js/testpkg/transforms.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { webcrypto } from 'node:crypto'
import { readFileSync } from "node:fs";

import { PowerTransform, initSync } from '@bsull/augurs/transforms';

import { describe, expect } from 'vitest';

// Required for Rust's `rand::thread_rng` to support NodeJS modules.
// See https://docs.rs/getrandom#nodejs-es-module-support.
// @ts-ignore
globalThis.crypto = webcrypto

initSync({ module: readFileSync('node_modules/@bsull/augurs/transforms_bg.wasm') });

describe('transforms', () => {
const y = [
0.1, 0.3, 0.8, 0.5,
0.1, 0.31, 0.79, 0.48,
0.09, 0.29, 0.81, 0.49,
0.11, 0.28, 0.78, 0.53,
0.1, 0.3, 0.8, 0.5,
0.1, 0.31, 0.79, 0.48,
0.09, 0.29, 0.81, 0.49,
0.11, 0.28, 0.78, 0.53,
];

describe('power transform', () => {
const pt = new PowerTransform({ data: y });
const transformed = pt.transform(y);
expect(transformed).toBeInstanceOf(Float64Array);
expect(transformed).toHaveLength(y.length);
const inverse = pt.inverseTransform(transformed);
expect(inverse).toBeInstanceOf(Float64Array);
expect(inverse).toHaveLength(y.length);
expect(new Array(inverse)).toEqual(y);
})
})

0 comments on commit b8013fd

Please sign in to comment.