From 7a446f52092539d8221f226fb8e10dfc0a01de3f Mon Sep 17 00:00:00 2001 From: Heidi Jiang Date: Wed, 25 Mar 2026 16:44:17 -0400 Subject: [PATCH 1/5] loss manager --- lang/src/js/trove/prototype.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 lang/src/js/trove/prototype.js diff --git a/lang/src/js/trove/prototype.js b/lang/src/js/trove/prototype.js new file mode 100644 index 000000000..dfdde819f --- /dev/null +++ b/lang/src/js/trove/prototype.js @@ -0,0 +1,13 @@ +function initIntervals(xMinValue, xMaxValue) { + const lossManager = new Map() + + for (let i = xMinValue; i < xMaxValue; i++) { + lossManager.set([i, i + 1], null) + } + + return lossManager; +} + +function recomputePoints() { + +} \ No newline at end of file From 5731d1ec6b28af0512b03da18f82a6caf73725d1 Mon Sep 17 00:00:00 2001 From: Heidi Jiang Date: Wed, 25 Mar 2026 17:06:53 -0400 Subject: [PATCH 2/5] map out overall flow + necessary functions --- lang/src/js/trove/prototype.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/lang/src/js/trove/prototype.js b/lang/src/js/trove/prototype.js index dfdde819f..eba614e6e 100644 --- a/lang/src/js/trove/prototype.js +++ b/lang/src/js/trove/prototype.js @@ -1,3 +1,6 @@ +/** + * initializes data points and intervals for loss manager + */ function initIntervals(xMinValue, xMaxValue) { const lossManager = new Map() @@ -8,6 +11,23 @@ function initIntervals(xMinValue, xMaxValue) { return lossManager; } -function recomputePoints() { +/** + * map f(x) to each x and stores points in data + */ +function recomputePoints(f, data) { +} + +/** + * flow: + * 1. take in a function + * 2. apply f(x) to each x (abstracted and will be done in Pyret) + * 3. compute loss over each interval + * 4. split interval w/ highest loss in half + * 5. recompute losses + * Stopping condition: threshold, numSamples + * To do: how to decide which loss function for a given f + */ +function runner() { + } \ No newline at end of file From ffcbccdf10bb424ee7a9b42a0574d3768092b19c Mon Sep 17 00:00:00 2001 From: Heidi Jiang Date: Mon, 30 Mar 2026 17:01:31 -0400 Subject: [PATCH 3/5] prototyped basic flow of an adaptive sampler with one loss function --- lang/src/js/trove/prototype.js | 111 ++++++++++++++++++++++++++------- 1 file changed, 89 insertions(+), 22 deletions(-) diff --git a/lang/src/js/trove/prototype.js b/lang/src/js/trove/prototype.js index eba614e6e..73341afd9 100644 --- a/lang/src/js/trove/prototype.js +++ b/lang/src/js/trove/prototype.js @@ -1,23 +1,3 @@ -/** - * initializes data points and intervals for loss manager - */ -function initIntervals(xMinValue, xMaxValue) { - const lossManager = new Map() - - for (let i = xMinValue; i < xMaxValue; i++) { - lossManager.set([i, i + 1], null) - } - - return lossManager; -} - -/** - * map f(x) to each x and stores points in data - */ -function recomputePoints(f, data) { - -} - /** * flow: * 1. take in a function @@ -28,6 +8,93 @@ function recomputePoints(f, data) { * Stopping condition: threshold, numSamples * To do: how to decide which loss function for a given f */ -function runner() { -} \ No newline at end of file +/** + * 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); +} + +function AdaptiveSampler(f, xMinValue, xMaxValue, lossFunction, numSamples) { + this.lossManager = []; + this.data = new Map(); + this.f = f; + this.xMinValue = xMinValue; + this.xMaxValue = xMaxValue; + this.lossFunction = lossFunction; + this.numSamples = numSamples; + this.pending = []; + + // initialize data by uniformly computing f(x) across the domain + this.initData = function() { + for (let i = xMinValue; i <= xMaxValue; i++) { + this.data.set(i, this.f(i)); + if (i < xMaxValue) { + this.pending.push([i, i + 1]); + } + } + }; + + // 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 + 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.f(m)); + this.lossManager.splice(maxIndex, 1); + this.pending.push([l, m], [m, r]); + }; + + this.runner = function() { + this.initData(); + this.computeLosses(); + while (this.data.size < this.numSamples) { + const maxLoss = this.getMaxLoss() + this.splitInterval(maxLoss.maxInterval, maxLoss.maxIndex) + this.computeLosses() + } + }; +} + +// testing +const learner = new AdaptiveSampler(x => x**2, 2, 5, defaultLoss, 10); +// learner.initData(); +// learner.computeLosses(); +// console.log(learner.data, learner.pending, learner.lossManager); +// const result = learner.getMaxLoss(); +// learner.splitInterval(result.maxInterval, result.maxIndex); +// console.log(learner.data, learner.pending, learner.lossManager); +learner.runner() +console.log(learner.data, learner.pending, learner.lossManager); \ No newline at end of file From f06dbf0b913d90a8f925a803d5a6a33efb3b4fc9 Mon Sep 17 00:00:00 2001 From: Heidi Jiang Date: Mon, 30 Mar 2026 23:47:39 -0400 Subject: [PATCH 4/5] added two other loss functions --- lang/src/js/trove/prototype.js | 44 ++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/lang/src/js/trove/prototype.js b/lang/src/js/trove/prototype.js index 73341afd9..d8305cbe4 100644 --- a/lang/src/js/trove/prototype.js +++ b/lang/src/js/trove/prototype.js @@ -1,14 +1,3 @@ -/** - * flow: - * 1. take in a function - * 2. apply f(x) to each x (abstracted and will be done in Pyret) - * 3. compute loss over each interval - * 4. split interval w/ highest loss in half - * 5. recompute losses - * Stopping condition: threshold, numSamples - * To do: how to decide which loss function for a given f - */ - /** * Euclidean distance between two points * @param {[number, number]} xs - a pair of x-values @@ -21,6 +10,29 @@ function defaultLoss(xs, ys) { 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 + * @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 absMinLogLoss(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); +} + function AdaptiveSampler(f, xMinValue, xMaxValue, lossFunction, numSamples) { this.lossManager = []; this.data = new Map(); @@ -52,6 +64,7 @@ function AdaptiveSampler(f, xMinValue, xMaxValue, lossFunction, numSamples) { }; // 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; @@ -77,6 +90,8 @@ function AdaptiveSampler(f, xMinValue, xMaxValue, lossFunction, numSamples) { this.pending.push([l, m], [m, r]); }; + // runs the adaptive sampler + // TODO: adding different stopping conditions this.runner = function() { this.initData(); this.computeLosses(); @@ -89,12 +104,11 @@ function AdaptiveSampler(f, xMinValue, xMaxValue, lossFunction, numSamples) { } // testing -const learner = new AdaptiveSampler(x => x**2, 2, 5, defaultLoss, 10); +// const learner = new AdaptiveSampler(x => x**2, 0, 5, absMinLogLoss, 10); // learner.initData(); // learner.computeLosses(); // console.log(learner.data, learner.pending, learner.lossManager); // const result = learner.getMaxLoss(); // learner.splitInterval(result.maxInterval, result.maxIndex); -// console.log(learner.data, learner.pending, learner.lossManager); -learner.runner() -console.log(learner.data, learner.pending, learner.lossManager); \ No newline at end of file +// learner.runner() +// console.log(learner.data, learner.pending, learner.lossManager); \ No newline at end of file From aefd069d90c50ee76d14779346709f4b5ee2c57d Mon Sep 17 00:00:00 2001 From: Heidi Jiang Date: Sun, 5 Apr 2026 21:40:01 -0400 Subject: [PATCH 5/5] integrated adaptive library --- lang/src/js/trove/adaptive.js | 134 ++++++++++++++++++++++++++++++++ lang/src/js/trove/charts-lib.js | 37 ++++----- lang/src/js/trove/prototype.js | 114 --------------------------- 3 files changed, 150 insertions(+), 135 deletions(-) create mode 100644 lang/src/js/trove/adaptive.js delete mode 100644 lang/src/js/trove/prototype.js diff --git a/lang/src/js/trove/adaptive.js b/lang/src/js/trove/adaptive.js new file mode 100644 index 000000000..9d2274421 --- /dev/null +++ b/lang/src/js/trove/adaptive.js @@ -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); + } +}) \ No newline at end of file diff --git a/lang/src/js/trove/charts-lib.js b/lang/src/js/trove/charts-lib.js index 18fdf9289..a02c4a8fa 100644 --- a/lang/src/js/trove/charts-lib.js +++ b/lang/src/js/trove/charts-lib.js @@ -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', @@ -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'; @@ -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); @@ -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); @@ -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 = [ @@ -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, @@ -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) => { diff --git a/lang/src/js/trove/prototype.js b/lang/src/js/trove/prototype.js deleted file mode 100644 index d8305cbe4..000000000 --- a/lang/src/js/trove/prototype.js +++ /dev/null @@ -1,114 +0,0 @@ -/** - * 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 - * @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 absMinLogLoss(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); -} - -function AdaptiveSampler(f, xMinValue, xMaxValue, lossFunction, numSamples) { - this.lossManager = []; - this.data = new Map(); - this.f = f; - this.xMinValue = xMinValue; - this.xMaxValue = xMaxValue; - this.lossFunction = lossFunction; - this.numSamples = numSamples; - this.pending = []; - - // initialize data by uniformly computing f(x) across the domain - this.initData = function() { - for (let i = xMinValue; i <= xMaxValue; i++) { - this.data.set(i, this.f(i)); - if (i < xMaxValue) { - this.pending.push([i, i + 1]); - } - } - }; - - // 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.f(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 < this.numSamples) { - const maxLoss = this.getMaxLoss() - this.splitInterval(maxLoss.maxInterval, maxLoss.maxIndex) - this.computeLosses() - } - }; -} - -// testing -// const learner = new AdaptiveSampler(x => x**2, 0, 5, absMinLogLoss, 10); -// learner.initData(); -// learner.computeLosses(); -// console.log(learner.data, learner.pending, learner.lossManager); -// const result = learner.getMaxLoss(); -// learner.splitInterval(result.maxInterval, result.maxIndex); -// learner.runner() -// console.log(learner.data, learner.pending, learner.lossManager); \ No newline at end of file