Skip to content
Draft
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
134 changes: 134 additions & 0 deletions lang/src/js/trove/adaptive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
({
requires: [],
nativeRequires: [],
provides: {
values: {},
types: {}
},
theModule: function(RUNTIME, NAMESPACE, uri) {
/**
* Euclidean distance between two points
* @param {[number, number]} xs - a pair of x-values
* @param {[number, number]} ys - a corresponding pair of y-values
* @returns {number} - the loss for the interval
*/
function defaultLoss(xs, ys) {
const dx = xs[1] - xs[0];
const dy = ys[1] - ys[0];
return Math.hypot(dx, dy);
}

/**
* Distance between two x-values
* @param {[number, number]} xs - a pair of x-values
* @param {[number, number]} ys - unused (only passed in for consistency with other loss functions)
* @returns {number} - the loss for the interval
*/
function uniformLoss(xs, ys) {
return xs[1] - xs[0];
}

/**
* Penalizes y-values close to 0
* Implementation of abs_min_log_loss from adaptive (min not required for scalars)
* @param {[number, number]} xs - a pair of x-values
* @param {[number, number]} ys - a corresponding pair of y-values
* @returns {number} - the loss for the interval
*/
function absLogLoss(xs, ys) {
// bound the transformed y-value in case y = 0
const lowerBound = -1e12;
const ysLog = ys.map(y => Math.max(lowerBound, Math.log(Math.abs(y))));
return defaultLoss(xs, ysLog);
}

/**
* Flow:
* 1. Initialize data
* 2. Compute losses
* 3. Identify interval with max loss
* 4. Split interval in half
* 5. Repeat steps 2-4 until stopping condition
* @param {Function} func - function to plot
* @param {number} xMinValue - min x-value
* @param {number} xMaxValue - max x-value
* @param {Function} lossFunction - loss function
* @param {number} numSamples - max number of data points to sample
*/
function AdaptiveSampler(func, xMinValue, xMaxValue, lossFunction, numSamples) {
this.lossManager = [];
this.data = new Map();
this.func = func;
this.xMinValue = xMinValue;
this.xMaxValue = xMaxValue;
this.lossFunction = lossFunction;
this.numSamples = numSamples;
this.pending = [];

// initialize data by computing f(x) for endpoints
this.initData = function() {
this.data.set(xMinValue, this.func.app(xMinValue));
this.data.set(xMaxValue, this.func.app(xMaxValue));
this.pending.push([xMinValue, xMaxValue]);
};

// compute loss for each interval in pending
this.computeLosses = function() {
while (this.pending.length > 0) {
const xs = this.pending.pop();
const ys = [this.data.get(xs[0]), this.data.get(xs[1])]
const loss = this.lossFunction(xs, ys)
this.lossManager.push([...xs, loss])
}
};

// get the interval with the max loss
// TODO: handling multiple intervals with the same max loss (particularly important for uniform loss)
this.getMaxLoss = function() {
let maxLoss = -Infinity;
let maxInterval = null;
let maxIndex = null;

for (let i = 0; i < this.lossManager.length; i++) {
const item = this.lossManager[i];
if (item[2] > maxLoss) {
maxLoss = item[2];
maxInterval = item.slice(0, 2);
maxIndex = i;
}
}
return { maxInterval, maxIndex };
};

// split an interval in half, compute y-value of the midpoint, and add new intervals to pending
this.splitInterval = function(maxInterval, maxIndex) {
const [l, r] = maxInterval;
const m = (l + r) / 2;
this.data.set(m, this.func.app(m));
this.lossManager.splice(maxIndex, 1);
this.pending.push([l, m], [m, r]);
};

// runs the adaptive sampler
// TODO: adding different stopping conditions
this.runner = function() {
this.initData();
this.computeLosses();
while (this.data.size < 10) {
const maxLoss = this.getMaxLoss()
this.splitInterval(maxLoss.maxInterval, maxLoss.maxIndex)
this.computeLosses()
}
};
}

var internal = {
defaultLoss: defaultLoss,
uniformLoss: uniformLoss,
absLogLoss: absLogLoss,
AdaptiveSampler: AdaptiveSampler
};

return RUNTIME.makeModuleReturn({}, {}, internal);
}
})
37 changes: 16 additions & 21 deletions lang/src/js/trove/charts-lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
requires: [
{ 'import-type': 'builtin', 'name': 'image-lib' },
{ "import-type": "builtin", 'name': "charts-util" },
{ "import-type": "builtin", 'name': "adaptive" }
],
nativeRequires: [
'pyret-base/js/js-numbers',
Expand All @@ -20,7 +21,7 @@
'plot': "tany",
}
},
theModule: function (RUNTIME, NAMESPACE, uri, IMAGELIB, CHARTSUTILLIB, jsnums, vega, canvasLib) {
theModule: function (RUNTIME, NAMESPACE, uri, IMAGELIB, CHARTSUTILLIB, ADAPTIVELIB, jsnums, vega, canvasLib) {
'use strict';


Expand Down Expand Up @@ -61,6 +62,7 @@

var IMAGE = get(IMAGELIB, "internal");
var CHARTSUTIL = get(CHARTSUTILLIB, "values");
var ADAPTIVE = get(ADAPTIVELIB, "internal");

const ann = function(name, pred) {
return RUNTIME.makePrimitiveAnn(name, pred);
Expand Down Expand Up @@ -2767,20 +2769,18 @@


// NOTE: Must be run on Pyret stack
function recomputePoints(func, samplePoints, then) {
function recomputePoints(func, xMinValue, xMaxValue, numSamples, then) {
const loss = ADAPTIVE.defaultLoss;
const sampler = new ADAPTIVE.AdaptiveSampler(func, xMinValue, xMaxValue, loss, numSamples);
return RUNTIME.safeCall(() => {
return RUNTIME.raw_array_map(RUNTIME.makeFunction((sample) => {
return RUNTIME.execThunk(RUNTIME.makeFunction(() => func.app(sample)));
}), samplePoints);
}, (funcVals) => {
sampler.runner();
return sampler.data;
}, (dataMap) => {
const dataValues = [];
funcVals.forEach((result, idx) => {
cases(RUNTIME.ffi.isEither, 'Either', result, {
left: (value) => dataValues.push({
x: toFixnum(samplePoints[idx]),
y: toFixnum(value)
}),
right: () => {}
dataMap.forEach((yVal, xVal) => {
dataValues.push({
x: toFixnum(xVal),
y: toFixnum(yVal)
})
});
return then(dataValues);
Expand All @@ -2801,8 +2801,6 @@
const xAxisType = globalOptions['x-axis-type'];
const yAxisType = globalOptions['y-axis-type'];

const fraction = (xMaxValue - xMinValue) / (numSamples - 1);

const data = [ { name: `${prefix}table` } ];

const signals = [
Expand Down Expand Up @@ -2852,9 +2850,7 @@

addCrosshairs(prefix, ['Dots'], signals, marks, pointColor);

const samplePoints = [...Array(numSamples).keys().map((i) => (xMinValue + (fraction * i)))];

return recomputePoints(func, samplePoints, (dataValues) => {
return recomputePoints(func, xMinValue, xMaxValue, numSamples, (dataValues) => {
data[0].values = dataValues;
return {
prefix,
Expand All @@ -2869,14 +2865,13 @@
const numSamples = globalOptions.numSamples;
const xMinValue = globalOptions.xMinValue;
const xMaxValue = globalOptions.xMaxValue;
const fraction = (xMaxValue - xMinValue) / (numSamples - 1);
const samplePoints = [...Array(numSamples).keys().map((i) => (xMinValue + (fraction * i)))];

RUNTIME.runThunk(() => {
// NOTE(Ben): We can use view.data(`${prefix}rawTable`, ...newData...)
// to replace the existing data points in the _current_ view, so that
// we do not have to reconstruct a new vega.View or restart the rendering process.
// See https://vega.github.io/vega/docs/api/view/#view_data
return recomputePoints(func, samplePoints, (dataValues) => {
return recomputePoints(func, xMinValue, xMaxValue, numSamples, (dataValues) => {
view.data(data[0].name, dataValues);
});
}, (res) => {
Expand Down