From 7a446f52092539d8221f226fb8e10dfc0a01de3f Mon Sep 17 00:00:00 2001 From: Heidi Jiang Date: Wed, 25 Mar 2026 16:44:17 -0400 Subject: [PATCH 1/9] 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/9] 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/9] 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/9] 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/9] 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 From 7b61fa6ac674401355845e40fcbb0b8015212f16 Mon Sep 17 00:00:00 2001 From: Heidi Jiang Date: Wed, 8 Apr 2026 13:59:40 -0400 Subject: [PATCH 6/9] started working on edge cases --- lang/examples/___charts.arr | 10 +++++++ lang/src/js/trove/adaptive.js | 52 ++++++++++++++++++++++++++++------- 2 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 lang/examples/___charts.arr diff --git a/lang/examples/___charts.arr b/lang/examples/___charts.arr new file mode 100644 index 000000000..842074902 --- /dev/null +++ b/lang/examples/___charts.arr @@ -0,0 +1,10 @@ +include charts +include image +import color as C + +fun some-fun(x): 1 / x end +a-series = from-list.function-plot(some-fun) + .color(C.purple) + +img = render-chart(a-series).get-image() +save-image(img, "chart.png") \ No newline at end of file diff --git a/lang/src/js/trove/adaptive.js b/lang/src/js/trove/adaptive.js index 9d2274421..44a2ca316 100644 --- a/lang/src/js/trove/adaptive.js +++ b/lang/src/js/trove/adaptive.js @@ -42,6 +42,17 @@ return defaultLoss(xs, ysLog); } + // handles if val is a number or Roughnum + function unwrapNum(val) { + if (typeof(val) == "number") { + return val; + } else if (typeof(val) == "object") { + return val.n; + } else { + throw new Error("Invalid type:", typeof(val)) + } + } + /** * Flow: * 1. Initialize data @@ -65,20 +76,40 @@ this.numSamples = numSamples; this.pending = []; + // handles zero division error + // TODO: janky error handling + this.runFunc = function(input, offset=1e-6) { + let x = input; + let y; + try { + y = unwrapNum(this.func.app(x)); + } catch (e) { + x = offset; + y = unwrapNum(this.func.app(x)); + } + return { x, y } + }; + // 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)); + lower = this.runFunc(xMinValue); + upper = this.runFunc(xMaxValue); + this.data.set(lower.x, lower.y); + this.data.set(upper.x, upper.y); this.pending.push([xMinValue, xMaxValue]); }; // compute loss for each interval in pending + // TODO: janky error handling 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]) + const ys = [this.data.get(xs[0]), this.data.get(xs[1])]; + if (ys[0] == null || ys[1] == null) { + continue; + } + const loss = this.lossFunction(xs, ys); + this.lossManager.push([...xs, loss]); } }; @@ -104,7 +135,8 @@ this.splitInterval = function(maxInterval, maxIndex) { const [l, r] = maxInterval; const m = (l + r) / 2; - this.data.set(m, this.func.app(m)); + coord = this.runFunc(m); + this.data.set(coord.x, coord.y); this.lossManager.splice(maxIndex, 1); this.pending.push([l, m], [m, r]); }; @@ -114,10 +146,10 @@ this.runner = function() { this.initData(); this.computeLosses(); - while (this.data.size < 10) { - const maxLoss = this.getMaxLoss() - this.splitInterval(maxLoss.maxInterval, maxLoss.maxIndex) - this.computeLosses() + while (this.data.size < numSamples) { + const maxLoss = this.getMaxLoss(); + this.splitInterval(maxLoss.maxInterval, maxLoss.maxIndex); + this.computeLosses(); } }; } From 3f2687094035ef94b07604e788d2f8ed1122fa7d Mon Sep 17 00:00:00 2001 From: Heidi Jiang Date: Sat, 11 Apr 2026 19:28:33 -0400 Subject: [PATCH 7/9] added types for debugging --- lang/src/js/trove/adaptive.js | 148 ++++++++++++++++++++-------------- lang/src/types.d.ts | 2 +- 2 files changed, 88 insertions(+), 62 deletions(-) diff --git a/lang/src/js/trove/adaptive.js b/lang/src/js/trove/adaptive.js index 44a2ca316..a620496c9 100644 --- a/lang/src/js/trove/adaptive.js +++ b/lang/src/js/trove/adaptive.js @@ -1,11 +1,14 @@ +/** @satisfies {PyretModule} */ ({ requires: [], - nativeRequires: [], + nativeRequires: [ + 'pyret-base/js/js-numbers', + ], provides: { values: {}, types: {} }, - theModule: function(RUNTIME, NAMESPACE, uri) { + theModule: function(RUNTIME, NAMESPACE, uri, jsnums) { /** * Euclidean distance between two points * @param {[number, number]} xs - a pair of x-values @@ -39,19 +42,10 @@ // 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); + return defaultLoss(xs, (/** @type {[number, number]} */ (ysLog))); } - // handles if val is a number or Roughnum - function unwrapNum(val) { - if (typeof(val) == "number") { - return val; - } else if (typeof(val) == "object") { - return val.n; - } else { - throw new Error("Invalid type:", typeof(val)) - } - } + /** @typedef {{x: number[], y: number[]}} Foo */ /** * Flow: @@ -60,54 +54,82 @@ * 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 = []; - - // handles zero division error - // TODO: janky error handling - this.runFunc = function(input, offset=1e-6) { + class AdaptiveSampler { + /** + * @param {PyretFunction} 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 + */ + constructor(func, xMinValue, xMaxValue, lossFunction, numSamples) { + // format: [[x1, x2, loss]] + this.lossManager = (/** @type {[number, number, number][]} */ ([])); + // format: {x1: y1, x2: y2, ...} + this.data = new Map(); + this.func = func; + this.xMinValue = xMinValue; + this.xMaxValue = xMaxValue; + this.lossFunction = lossFunction; + this.numSamples = numSamples; + this.pending = (/** @type {[number, number][]} */ ([])); + } + // runs the function more safely (handles zero division and Pyret nums) + // FIX: janky error handling + /** + * @param {Number} input + * @returns {Foo} + */ + runFunc(input, offset = 1e-6) { let x = input; - let y; try { - y = unwrapNum(this.func.app(x)); + // const y = RUNTIME.safeCall(() => { + // return this.func.app(x); + // }, (output) => { + // return typeof(output) == "number" ? output : jsnums.toFixnum(output) + // }, "runFunc") + const output = this.func.app(x); + const y = typeof (output) == "number" ? output : jsnums.toFixnum(output); + return { "x": [x], "y": [y] }; } catch (e) { - x = offset; - y = unwrapNum(this.func.app(x)); + if ((/** @type {ABI.FailureResult} */ (e))?.exn?.dict?.message?.includes("division by zero")) { + const x1 = x - offset; + const x2 = x + offset; + + // FIX: recursive calls are probably not the best option + const y1 = this.runFunc(x1).y[0]; + const y2 = this.runFunc(x2).y[0]; + + // (x1, y1) offset to the left of x + // (x2, y2) offset to the right of x + return { "x": [x1, x2], "y": [y1, y2] }; + } + else { + throw e; + } } - return { x, y } }; // initialize data by computing f(x) for endpoints - this.initData = function() { - lower = this.runFunc(xMinValue); - upper = this.runFunc(xMaxValue); - this.data.set(lower.x, lower.y); - this.data.set(upper.x, upper.y); - this.pending.push([xMinValue, xMaxValue]); + initData() { + const lower = this.runFunc(this.xMinValue); + const upper = this.runFunc(this.xMaxValue); + + lower.x.forEach((xi, i) => this.data.set(xi, lower.y[i])); + upper.x.forEach((xi, i) => this.data.set(xi, upper.y[i])); + + // adds lower x1 and upper x2 (outer bounds) if there is more than one x returned + this.pending.push([lower.x[0], (/** @type {number} */ (upper.x.at(-1)))]); }; // compute loss for each interval in pending - // TODO: janky error handling - this.computeLosses = function() { + // FIX: janky error handling + computeLosses() { while (this.pending.length > 0) { - const xs = this.pending.pop(); + const xs = /** @type {[number, number]} */ (this.pending.pop()); const ys = [this.data.get(xs[0]), this.data.get(xs[1])]; - if (ys[0] == null || ys[1] == null) { - continue; - } + if (ys.includes(undefined)) continue; const loss = this.lossFunction(xs, ys); this.lossManager.push([...xs, loss]); } @@ -115,7 +137,7 @@ // get the interval with the max loss // TODO: handling multiple intervals with the same max loss (particularly important for uniform loss) - this.getMaxLoss = function() { + getMaxLoss() { let maxLoss = -Infinity; let maxInterval = null; let maxIndex = null; @@ -132,35 +154,39 @@ }; // split an interval in half, compute y-value of the midpoint, and add new intervals to pending - this.splitInterval = function(maxInterval, maxIndex) { + /** + * @param {number[]} maxInterval + * @param {number} maxIndex + */ + splitInterval(maxInterval, maxIndex) { const [l, r] = maxInterval; const m = (l + r) / 2; - coord = this.runFunc(m); - this.data.set(coord.x, coord.y); + const coord = this.runFunc(m); + coord.x.forEach((xi, i) => this.data.set(xi, coord.y[i])); this.lossManager.splice(maxIndex, 1); - this.pending.push([l, m], [m, r]); + this.pending.push([l, coord.x[0]], [/** @type {number} */ (coord.x.at(-1)), r]); }; // runs the adaptive sampler - // TODO: adding different stopping conditions - this.runner = function() { + // TODO: adding different stopping conditions (e.g. error threshold) + runner() { this.initData(); this.computeLosses(); - while (this.data.size < numSamples) { + while (this.data.size < this.numSamples) { + console.log(this.data); const maxLoss = this.getMaxLoss(); this.splitInterval(maxLoss.maxInterval, maxLoss.maxIndex); this.computeLosses(); } }; - } - var internal = { + } + + return RUNTIME.makeModuleReturn({}, {}, { defaultLoss: defaultLoss, uniformLoss: uniformLoss, absLogLoss: absLogLoss, AdaptiveSampler: AdaptiveSampler - }; - - return RUNTIME.makeModuleReturn({}, {}, internal); + }); } }) \ No newline at end of file diff --git a/lang/src/types.d.ts b/lang/src/types.d.ts index dfe98eac6..334b6fb20 100644 --- a/lang/src/types.d.ts +++ b/lang/src/types.d.ts @@ -224,7 +224,7 @@ declare namespace ABI { type __ErrorExt = { pyretStack?: string[]; exn?: val; - } + } & PyretObject; class FailureResult { exn: Error & __ErrorExt; From 98a05245fa7666cc92d089e124913675efe36faa Mon Sep 17 00:00:00 2001 From: Heidi Jiang Date: Sat, 11 Apr 2026 20:52:36 -0400 Subject: [PATCH 8/9] claude fix --- lang/chart.png | Bin 0 -> 5739 bytes lang/examples/___charts.arr | 1 - lang/src/js/trove/adaptive copy.js | 185 +++++++++++++++++++++++++++++ lang/src/js/trove/adaptive.js | 115 +++++++++++------- lang/src/js/trove/prototype.js | 132 ++++++++++++++++++++ package-lock.json | 6 + 6 files changed, 392 insertions(+), 47 deletions(-) create mode 100644 lang/chart.png create mode 100644 lang/src/js/trove/adaptive copy.js create mode 100644 lang/src/js/trove/prototype.js create mode 100644 package-lock.json diff --git a/lang/chart.png b/lang/chart.png new file mode 100644 index 0000000000000000000000000000000000000000..deb6706a433900fdf65cd3331b8b4d0e5df113e7 GIT binary patch literal 5739 zcmeI0X;f3!7RTdI<0>lhEcl{=wg#x8wGdIl5S(K~#A3@lv`ru)7$!*w5Mr$sl?ZxM zWD1F|R!~7k2_PX@mB_^qM3BJ{Ky#B6On?MQ3`wA|tE+uq-q-hud-ghKt$ptq?(g2c z_J8e5$B$uGe!T8u7!0-&cldxm47Oww47NCE`7*Poh;1ZDqW7?W$Y#7h<|}^{!5^@^wXd+^AlPwZbV*HvH(yb!8*a&$pz1P2 zO%rcjlZlir+N_8|SFlD527^h2{@eiC=ZDoMC`u^&OuC7;ayEw~q!I zTty5YD1dI^<{ZE*rtuWui?Z#W)g&Xt>nM}e$W{P~XJ#P5Teu1D9;;4{FD{~vX3#2p z*>LtQsk#E5< zF01Yf;~79#lF}E9(}d!3*s28iD<>wazC+b@Ov7iG>hkT;sj{`oDi~E*Jp8T{Nrf(p zHx@%b;>ZrvSYYE(&7A3ddaJGHmv}|=eqiLN<^qyl2xSq`w^K%6AX|TiECHSuK5MrA z!;2*P3%Fd(TnC$LPdx@aV;a8#%81);pBXtB!#9zD>0R-|#xZ=Z*DlG`d{bTP?haBE#Z&qvtFD=qi$at)}|XIGY3M zfQ*y1Zc|GThMFv4!qMrIc%X;L`wUb&VH)1E^lBu%poXsX&^_5ib_g!WhznIWQKFVU z{pBVwR0v@wF8xrko0*-G=>T>j<_ohiMNlUS(aS0p4%XoKc`|W#GBeDXxwo^)dhG>? z6X@u|K9BJzw(4vJS=%uYUYTGS`>HyK@?x97HOmfg#`=XMSBQrY8ZmxJsPH3xRz#oCi66_-1de;4n62k*e&? z0M7%g_aTYV`swF$3Gx>)I1=dAM()l+1+2_1058f%tPr}^-0e-BVnUH+wz9e-{ zuW*#|sp=I^@7wWqN`L;PiyM zrNmobLYa1*&x77lrdzUeS2t8OnnW9d6feBor1$f^hB6H*@bfkWQh!{~@p>grgwsgT z34wQVJJu;~52wAohfCrx9{)!{WCb>E0n-8AW>6P!$c;Y1ag7*#eLa+f6P#)j1FS-* zyyd&j`pqaCoHk*)b9WVQEC$ivIaB~K7}m}LTD%NzocEnOlQ}o+HN!xO4huwY{0OYR zhwN3NM()X2!S}Z56b{I7bU>2~T9t>Nr}_zz7#92P+BWY~AjvxqYDC5l)J6+H{_1ytQ_Z zmr7b&;Y?Jp(-zL{?3zL?~x8%_aen7uP-y z8nZ`WH|F!LUavvPyUQ=!Q}GcVy&jRK@xUDfz3W-!*bw6c{);0T4TyiW4~Wc(J<_i^ z%Rr}*Yn34n{h~azHD!-cHj5$(hFpnR(c2nZ2|r1Th@4I!c2DJl@w;sX`Tl?mNg&Fm zf3mj0v1g47nGtAsKNZ2J1Pa#l;PZ#LTkvi3d)()zE(POPanuYVQi_|MM$CVgJ?9G7 zKC|ZJxyqHiX_1d$%Zl}Np09QhDtMVFcxj|?U9$FWV9Qp7u1ST^jjeGBn6oT2nubyT zp7eZQZzho+P9A*IA9gKqBaHww5cGErF=A)!iX`RtKsRg6EMsl|w<(jtpeS0eMa0xO zLR-uVCM(CwpyX>%Yb_$(-PF!?1_3X!#Ef&`i4it9t#83rw936&Ga zv84jW+m7EZbME{G9=RZsE9l#0mUHy)~#j#N1`GH&&giF9lg8K;p#smO)!m_QrB{ z02Su$CUu@$`DY^4Jlhf3K2RCqEjItmSmL(p#^b=6O8)Hl1?6yM*m=GsX6gsaRO<@b zbPhx3(=-n`tAY__f(_n}ojaT9U2eAw&q0=#v-EcR9f~0wUKxemq}NeMI+Lux;mGXF*|A8mIWYDKiv0$!MTjlxa?5tr zxi6~i$`LlorR%Y_DZTpyk0f0p^5NvkL|nZ~$7br_|L4{j9> z_O43r&W?D;JuT)IHf$OEQ~QvI@&Y58R%1!JmjYz&gEyC*Y-$A-dDRc>b)72_Wl}ca z8j*T;W~)8t%FDhMy}y=MUDQm$0(ADwrn5ujcimWe;&gPG^znuV>R*}1j_^8O@gJZ- zM?Z`nBIwfJeOVHY!0zpXtixfjv+EWuhQWH5uZF>H96D>(j;?=8roYtBQnQdczIo5W zqYDHU2rLj-Ah1ASfxrTR1p*5M76|-L0_aq_=|*@nxy4-ZtXOj4zsr-A;mwu9_hWk0 tAIdHG(0>Y-Z1fVd0RO*VXJ 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) { + // format: [[x1, x2, loss]] + this.lossManager = []; + // format: {x1: y1, x2: y2, ...} + this.data = new Map(); + this.func = func; + this.xMinValue = xMinValue; + this.xMaxValue = xMaxValue; + this.lossFunction = lossFunction; + this.numSamples = numSamples; + this.pending = []; + + // runs the function more safely (handles zero division and Pyret nums) + // FIX: janky recursive calls + this.runFunc = function(x, offset=1e-6) { + return RUNTIME.safeCall( + () => RUNTIME.execThunk(RUNTIME.makeFunction(() => this.func.app(x))), + (result) => cases(RUNTIME.ffi.isEither, "Either", result, { + left: (value) => { + const y = typeof value === 'number' ? value : jsnums.toFixnum(value); + return {"x": [x], "y": [y]}; + }, + right: () => { + const x1 = x - offset; + const x2 = x + offset; + return RUNTIME.safeCall( + () => this.runFunc(x1), + (r1) => RUNTIME.safeCall( + () => this.runFunc(x2), + (r2) => ({"x": [x1, x2], "y": [r1.y[0], r2.y[0]]}), + "runFunc" + ), + "runFunc" + ); + } + }), + "runFunc" + ) + }; + + // initialize data by computing f(x) for endpoints + this.initData = function() { + return RUNTIME.safeCall( + () => this.runFunc(xMinValue), + (lower) => RUNTIME.safeCall( + () => this.runFunc(xMaxValue), + (upper) => { + lower.x.forEach((xi, i) => this.data.set(xi, lower.y[i])); + upper.x.forEach((xi, i) => this.data.set(xi, upper.y[i])); + // adds lower x1 and upper x2 (outer bounds) if there is more than one x returned + this.pending.push([lower.x[0], upper.x.at(-1)]); + } + ), + "initData" + ) + }; + + // compute loss for each interval in pending + // FIX: janky error handling + 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])]; + if (ys.includes(undefined)) continue; + 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; + return RUNTIME.safeCall( + () => this.runFunc(m), + (coord) => { + coord.x.forEach((xi, i) => this.data.set(xi, coord.y[i])); + this.lossManager.splice(maxIndex, 1); + this.pending.push([l, coord.x[0]], [coord.x.at(-1), r]); + }, + "splitInterval" + ); + }; + + // runs the adaptive sampler + // TODO: adding different stopping conditions (e.g. error threshold) + this.runner = function() { + this.initData(); + this.computeLosses(); + while (this.data.size < numSamples) { + console.log(this) + 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/adaptive.js b/lang/src/js/trove/adaptive.js index a620496c9..316a0bf35 100644 --- a/lang/src/js/trove/adaptive.js +++ b/lang/src/js/trove/adaptive.js @@ -83,44 +83,48 @@ */ runFunc(input, offset = 1e-6) { let x = input; - try { - // const y = RUNTIME.safeCall(() => { - // return this.func.app(x); - // }, (output) => { - // return typeof(output) == "number" ? output : jsnums.toFixnum(output) - // }, "runFunc") - const output = this.func.app(x); - const y = typeof (output) == "number" ? output : jsnums.toFixnum(output); - return { "x": [x], "y": [y] }; - } catch (e) { - if ((/** @type {ABI.FailureResult} */ (e))?.exn?.dict?.message?.includes("division by zero")) { - const x1 = x - offset; - const x2 = x + offset; - - // FIX: recursive calls are probably not the best option - const y1 = this.runFunc(x1).y[0]; - const y2 = this.runFunc(x2).y[0]; - - // (x1, y1) offset to the left of x - // (x2, y2) offset to the right of x - return { "x": [x1, x2], "y": [y1, y2] }; - } - else { - throw e; - } - } + return RUNTIME.safeCall( + () => RUNTIME.execThunk(RUNTIME.makeFunction(() => this.func.app(x))), + (result) => { + if (result.$name === "left") { + const output = RUNTIME.getField(result, "v"); + const y = typeof output == "number" ? output : jsnums.toFixnum(output); + return { "x": [x], "y": [y] }; + } else { + // Error (e.g. division by zero) - evaluate at nearby points + const x1 = x - offset; + const x2 = x + offset; + return RUNTIME.safeCall( + () => this.runFunc(x1), + (r1) => RUNTIME.safeCall( + () => this.runFunc(x2), + (r2) => ({"x": [x1, x2], "y": [r1.y[0], r2.y[0]]}), + "runFunc-offset-2" + ), + "runFunc-offset-1" + ); + } + }, + "runFunc" + ); }; // initialize data by computing f(x) for endpoints initData() { - const lower = this.runFunc(this.xMinValue); - const upper = this.runFunc(this.xMaxValue); - - lower.x.forEach((xi, i) => this.data.set(xi, lower.y[i])); - upper.x.forEach((xi, i) => this.data.set(xi, upper.y[i])); - - // adds lower x1 and upper x2 (outer bounds) if there is more than one x returned - this.pending.push([lower.x[0], (/** @type {number} */ (upper.x.at(-1)))]); + return RUNTIME.safeCall( + () => this.runFunc(this.xMinValue), + (lower) => RUNTIME.safeCall( + () => this.runFunc(this.xMaxValue), + (upper) => { + lower.x.forEach((xi, i) => this.data.set(xi, lower.y[i])); + upper.x.forEach((xi, i) => this.data.set(xi, upper.y[i])); + // adds lower x1 and upper x2 (outer bounds) if there is more than one x returned + this.pending.push([lower.x[0], (/** @type {number} */ (upper.x.at(-1)))]); + }, + "initData-upper" + ), + "initData-lower" + ); }; // compute loss for each interval in pending @@ -161,23 +165,42 @@ splitInterval(maxInterval, maxIndex) { const [l, r] = maxInterval; const m = (l + r) / 2; - const coord = this.runFunc(m); - coord.x.forEach((xi, i) => this.data.set(xi, coord.y[i])); - this.lossManager.splice(maxIndex, 1); - this.pending.push([l, coord.x[0]], [/** @type {number} */ (coord.x.at(-1)), r]); + return RUNTIME.safeCall( + () => this.runFunc(m), + (coord) => { + coord.x.forEach((xi, i) => this.data.set(xi, coord.y[i])); + this.lossManager.splice(maxIndex, 1); + this.pending.push([l, coord.x[0]], [/** @type {number} */ (coord.x.at(-1)), r]); + }, + "splitInterval" + ); }; // runs the adaptive sampler // TODO: adding different stopping conditions (e.g. error threshold) runner() { - this.initData(); - this.computeLosses(); - while (this.data.size < this.numSamples) { - console.log(this.data); - const maxLoss = this.getMaxLoss(); - this.splitInterval(maxLoss.maxInterval, maxLoss.maxIndex); - this.computeLosses(); - } + const iterate = () => { + if (this.data.size >= this.numSamples) { + return RUNTIME.nothing; + } + const { maxInterval, maxIndex } = this.getMaxLoss(); + return RUNTIME.safeCall( + () => this.splitInterval(maxInterval, maxIndex), + () => { + this.computeLosses(); + return iterate(); + }, + "runner-iterate" + ); + }; + return RUNTIME.safeCall( + () => this.initData(), + () => { + this.computeLosses(); + return iterate(); + }, + "runner-init" + ); }; } diff --git a/lang/src/js/trove/prototype.js b/lang/src/js/trove/prototype.js new file mode 100644 index 000000000..09291c1cf --- /dev/null +++ b/lang/src/js/trove/prototype.js @@ -0,0 +1,132 @@ +/** + * 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 + */ +export 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 + */ +export 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); +} + +/** + * 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 + */ +export 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 uniformly computing f(x) across the domain + this.initData = function() { + for (let i = xMinValue; i <= xMaxValue; i++) { + this.data.set(i, this.func(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.func(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 => 1 / x, -5, 5, absMinLogLoss, 100); +// 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); + +const map = new Map(); +map.set(1, 1); +console.log(map.get(1)); +console.log(map.get(2)) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..bf6a0578f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "pyret", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From 15fdb8cd426b8137bf99a9b12e9925c312a61281 Mon Sep 17 00:00:00 2001 From: Heidi Jiang Date: Sat, 11 Apr 2026 21:23:49 -0400 Subject: [PATCH 9/9] more claude fixes --- lang/chart.png | Bin 5739 -> 0 bytes lang/src/arr/compiler/cli-module-loader.arr | 2 +- lang/src/js/trove/adaptive copy.js | 185 -------------------- lang/src/js/trove/adaptive.js | 63 +++++-- lang/src/js/trove/charts-lib.js | 6 +- lang/src/types.d.ts | 7 +- 6 files changed, 56 insertions(+), 207 deletions(-) delete mode 100644 lang/chart.png delete mode 100644 lang/src/js/trove/adaptive copy.js diff --git a/lang/chart.png b/lang/chart.png deleted file mode 100644 index deb6706a433900fdf65cd3331b8b4d0e5df113e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5739 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 tAIdHG(0>Y-Z1fVd0RO*VXJ 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) { - // format: [[x1, x2, loss]] - this.lossManager = []; - // format: {x1: y1, x2: y2, ...} - this.data = new Map(); - this.func = func; - this.xMinValue = xMinValue; - this.xMaxValue = xMaxValue; - this.lossFunction = lossFunction; - this.numSamples = numSamples; - this.pending = []; - - // runs the function more safely (handles zero division and Pyret nums) - // FIX: janky recursive calls - this.runFunc = function(x, offset=1e-6) { - return RUNTIME.safeCall( - () => RUNTIME.execThunk(RUNTIME.makeFunction(() => this.func.app(x))), - (result) => cases(RUNTIME.ffi.isEither, "Either", result, { - left: (value) => { - const y = typeof value === 'number' ? value : jsnums.toFixnum(value); - return {"x": [x], "y": [y]}; - }, - right: () => { - const x1 = x - offset; - const x2 = x + offset; - return RUNTIME.safeCall( - () => this.runFunc(x1), - (r1) => RUNTIME.safeCall( - () => this.runFunc(x2), - (r2) => ({"x": [x1, x2], "y": [r1.y[0], r2.y[0]]}), - "runFunc" - ), - "runFunc" - ); - } - }), - "runFunc" - ) - }; - - // initialize data by computing f(x) for endpoints - this.initData = function() { - return RUNTIME.safeCall( - () => this.runFunc(xMinValue), - (lower) => RUNTIME.safeCall( - () => this.runFunc(xMaxValue), - (upper) => { - lower.x.forEach((xi, i) => this.data.set(xi, lower.y[i])); - upper.x.forEach((xi, i) => this.data.set(xi, upper.y[i])); - // adds lower x1 and upper x2 (outer bounds) if there is more than one x returned - this.pending.push([lower.x[0], upper.x.at(-1)]); - } - ), - "initData" - ) - }; - - // compute loss for each interval in pending - // FIX: janky error handling - 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])]; - if (ys.includes(undefined)) continue; - 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; - return RUNTIME.safeCall( - () => this.runFunc(m), - (coord) => { - coord.x.forEach((xi, i) => this.data.set(xi, coord.y[i])); - this.lossManager.splice(maxIndex, 1); - this.pending.push([l, coord.x[0]], [coord.x.at(-1), r]); - }, - "splitInterval" - ); - }; - - // runs the adaptive sampler - // TODO: adding different stopping conditions (e.g. error threshold) - this.runner = function() { - this.initData(); - this.computeLosses(); - while (this.data.size < numSamples) { - console.log(this) - 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/adaptive.js b/lang/src/js/trove/adaptive.js index 316a0bf35..9ca3f2610 100644 --- a/lang/src/js/trove/adaptive.js +++ b/lang/src/js/trove/adaptive.js @@ -81,7 +81,7 @@ * @param {Number} input * @returns {Foo} */ - runFunc(input, offset = 1e-6) { + runFuncSafe(input) { let x = input; return RUNTIME.safeCall( () => RUNTIME.execThunk(RUNTIME.makeFunction(() => this.func.app(x))), @@ -89,21 +89,37 @@ if (result.$name === "left") { const output = RUNTIME.getField(result, "v"); const y = typeof output == "number" ? output : jsnums.toFixnum(output); - return { "x": [x], "y": [y] }; + return {"x": [x], "y": [y]}; } else { - // Error (e.g. division by zero) - evaluate at nearby points - const x1 = x - offset; - const x2 = x + offset; - return RUNTIME.safeCall( - () => this.runFunc(x1), - (r1) => RUNTIME.safeCall( - () => this.runFunc(x2), - (r2) => ({"x": [x1, x2], "y": [r1.y[0], r2.y[0]]}), - "runFunc-offset-2" - ), - "runFunc-offset-1" - ); + return null; + } + }, + "runFuncSafe" + ); + }; + + runFunc(input, offset = 1e-6) { + let x = input; + console.log("[runFunc] calling func with x =", x); + return RUNTIME.safeCall( + () => this.runFuncSafe(x), + (result) => { + if (result !== null) { + console.log("[runFunc] success: x =", x, "y =", result.y[0]); + return result; } + console.log("[runFunc] error for x =", x, "trying offsets"); + const x1 = x - offset; + const x2 = x + offset; + return RUNTIME.safeCall( + () => this.runFunc(x1), + (r1) => RUNTIME.safeCall( + () => this.runFunc(x2), + (r2) => ({"x": [x1, x2], "y": [r1.y[0], r2.y[0]]}), + "runFunc-offset-2" + ), + "runFunc-offset-1" + ); }, "runFunc" ); @@ -111,18 +127,23 @@ // initialize data by computing f(x) for endpoints initData() { + console.log("[initData] starting with xMin =", this.xMinValue, "xMax =", this.xMaxValue); return RUNTIME.safeCall( () => this.runFunc(this.xMinValue), - (lower) => RUNTIME.safeCall( + (lower) => { + console.log("[initData] lower result:", lower); + return RUNTIME.safeCall( () => this.runFunc(this.xMaxValue), (upper) => { + console.log("[initData] upper result:", upper); lower.x.forEach((xi, i) => this.data.set(xi, lower.y[i])); upper.x.forEach((xi, i) => this.data.set(xi, upper.y[i])); // adds lower x1 and upper x2 (outer bounds) if there is more than one x returned this.pending.push([lower.x[0], (/** @type {number} */ (upper.x.at(-1)))]); + console.log("[initData] done, data size =", this.data.size, "pending =", this.pending); }, "initData-upper" - ), + );}, "initData-lower" ); }; @@ -179,11 +200,20 @@ // runs the adaptive sampler // TODO: adding different stopping conditions (e.g. error threshold) runner() { + console.log("[runner] starting, numSamples =", this.numSamples); + /** @returns {PyretNothing} */ const iterate = () => { + console.log("[iterate] data size =", this.data.size, "/ target =", this.numSamples); if (this.data.size >= this.numSamples) { + console.log("[iterate] done, final data size =", this.data.size); return RUNTIME.nothing; } const { maxInterval, maxIndex } = this.getMaxLoss(); + if (maxInterval === null || maxIndex === null) { + console.log("[iterate] no intervals found in lossManager, stopping early"); + return RUNTIME.nothing; + } + console.log("[iterate] splitting interval", maxInterval, "at index", maxIndex); return RUNTIME.safeCall( () => this.splitInterval(maxInterval, maxIndex), () => { @@ -196,6 +226,7 @@ return RUNTIME.safeCall( () => this.initData(), () => { + console.log("[runner] initData complete, computing initial losses"); this.computeLosses(); return iterate(); }, diff --git a/lang/src/js/trove/charts-lib.js b/lang/src/js/trove/charts-lib.js index 3b47f2fd2..a116630de 100644 --- a/lang/src/js/trove/charts-lib.js +++ b/lang/src/js/trove/charts-lib.js @@ -2786,9 +2786,9 @@ const loss = ADAPTIVE.defaultLoss; const sampler = new ADAPTIVE.AdaptiveSampler(func, xMinValue, xMaxValue, loss, numSamples); return RUNTIME.safeCall(() => { - sampler.runner(); - return sampler.data; - }, (dataMap) => { + return sampler.runner(); + }, () => { + const dataMap = sampler.data; const dataValues = []; dataMap.forEach((yVal, xVal) => { dataValues.push({ diff --git a/lang/src/types.d.ts b/lang/src/types.d.ts index 334b6fb20..953ec82e9 100644 --- a/lang/src/types.d.ts +++ b/lang/src/types.d.ts @@ -244,6 +244,8 @@ type PyretMethod = val & ABI.PMethod; type PyretTuple = val & ABI.PTuple; +type PyretEither = val & { $name: "left" | "right" }; + // TODO: type RunOptions = { sync: boolean; @@ -267,6 +269,7 @@ interface PyretRuntime { options?: RunOptions, ): void; safeCall(fun: () => T, after: (result: T) => U, stackFrame: string): U; + execThunk(thunk: PyretFunction): PyretEither; ffi: PyretFFI; @@ -313,7 +316,7 @@ interface PyretRuntime { makeNumberFromString(s: string): val; makeBoolean(b: boolean): PyretBoolean; makeString(s: string): PyretString; - makeFunction(fun: func, name: string): PyretFunction; + makeFunction(fun: func, name?: string): PyretFunction; makeMethod(meth: func, full_meth: func, name: string): PyretMethod; // ... makeTuple(tup: val[]): PyretTuple; @@ -363,7 +366,7 @@ interface PyretRuntime { checkOpaque(v: val): void; checkPyretVal(v: unknown): void; - nothing: val; + nothing: PyretNothing; toRepr(v: val): val; makeSrcloc(srcloc: SrcLocJs): val;