From 531df57242c468ff39e4aa4594f893a567cca64f Mon Sep 17 00:00:00 2001 From: Roman Galochkin Date: Thu, 27 Feb 2020 14:32:05 +0300 Subject: [PATCH 1/4] Init --- package.json | 5 +- src/components/editor/distribution.js | 181 +++++++++++++++++ src/components/editor/index.js | 31 +++ src/components/editor/main.js | 278 ++++++++++++++++++++++++++ src/components/editor/parse.js | 119 +++++++++++ yarn.lock | 50 ++++- 6 files changed, 656 insertions(+), 8 deletions(-) create mode 100644 src/components/editor/distribution.js create mode 100644 src/components/editor/index.js create mode 100644 src/components/editor/main.js create mode 100644 src/components/editor/parse.js diff --git a/package.json b/package.json index a0a7200f..12536a55 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,10 @@ "react-dom": "16.12.0", "reason-react": ">=0.7.0", "reschema": "1.3.0", - "tailwindcss": "1.2.0" + "tailwindcss": "1.2.0", + "binary-search-tree": "0.2.6", + "jstat": "1.9.2", + "mathjs": "6.6.0" }, "alias": { "react": "./node_modules/react", diff --git a/src/components/editor/distribution.js b/src/components/editor/distribution.js new file mode 100644 index 00000000..02ef8c5c --- /dev/null +++ b/src/components/editor/distribution.js @@ -0,0 +1,181 @@ +// This module defines an abstract BinnedDistribution class, which +// should be implemented for each distribution. You need to decide +// how to bin the distribution (use _adabin unless there's a nicer +// way for your distr) and how to choose the distribution's support. +const _math = require("mathjs"); +const math = _math.create(_math.all); +const jStat = require("jstat"); + +math.import({ + normal: jStat.normal, + beta: jStat.beta, + lognormal: jStat.lognormal, + uniform: jStat.uniform +}); + +class BaseDistributionBinned { + constructor(args) { + this._set_props(); + this.max_bin_size = 0.5; + this.min_bin_size = 0; + this.increment = 0.001; + this.desired_delta = 0.01; + this.start_bin_size = 0.01; + + [this.params, this.pdf_func, this.sample] = this.get_params_and_pdf_func( + args + ); + + [this.start_point, this.end_point] = this.get_bounds(); + [this.pdf_vals, this.divider_pts] = this.bin(); + } + + _set_props() { + // this is hacky but class properties aren't always supported + throw new Error("NotImplementedError"); + } + + _adabin() { + let point = this.start_point; + let vals = [this.pdf_func(point)]; + let divider_pts = [point]; + let support = this.end_point - this.start_point; + let bin_size = this.start_bin_size * support; + + while (point < this.end_point) { + let val = this.pdf_func(point + bin_size); + if (Math.abs(val - vals[vals.length - 1]) > this.desired_delta) { + while ( + (Math.abs(val - vals[vals.length - 1]) > this.desired_delta) & + (bin_size - this.increment * support > this.min_bin_size) + ) { + bin_size -= this.increment; + val = this.pdf_func(point + bin_size); + } + } else if (Math.abs(val - vals[vals.length - 1]) < this.desired_delta) { + while ( + (Math.abs(val - vals[vals.length - 1]) < this.desired_delta) & + (bin_size < this.max_bin_size) + ) { + bin_size += this.increment; + val = this.pdf_func(point + bin_size); + } + } + point += bin_size; + vals.push(val); + divider_pts.push(point); + } + vals = vals.map((_, idx) => vals[idx] / 2 + vals[idx + 1] / 2); + vals = vals.slice(0, -1); + return [vals, divider_pts]; + } + + bin() { + throw new Error("NotImplementedError"); + } + + get_bounds() { + throw new Error("NotImplementedError"); + } + + get_params_and_pdf_func(args) { + let args_str = args.toString() + ")"; + let substr = this.name + ".pdf(x, " + args_str; + let compiled = math.compile(substr); + function pdf_func(x) { + return compiled.evaluate({ x: x }); + } + let mc_compiled = math.compile(this.name + ".sample(" + args_str); + let kv_pairs = this.param_names.map((val, idx) => [val, args[idx]]); + let params = Object.fromEntries(new Map(kv_pairs)); + return [params, pdf_func, mc_compiled.evaluate]; + } +} + +class NormalDistributionBinned extends BaseDistributionBinned { + _set_props() { + this.name = "normal"; + this.param_names = ["mean", "std"]; + } + get_bounds() { + return [ + this.params.mean - 4 * this.params.std, + this.params.mean + 4 * this.params.std + ]; + } + bin() { + return this._adabin(this.params.std); + } +} + +class UniformDistributionBinned extends BaseDistributionBinned { + _set_props() { + this.name = "uniform"; + this.param_names = ["start_point", "end_point"]; + this.num_bins = 200; + } + get_bounds() { + return [this.params.start_point, this.params.end_point]; + } + bin() { + let divider_pts = evenly_spaced_grid( + this.params.start_point, + this.params.end_point, + this.num_bins + ); + let vals = divider_pts.map(x => + this.pdf_func(this.params.start_point / 2 + this.params.end_point / 2) + ); + vals = vals.slice(0, -1); + return [vals, divider_pts]; + } +} + +class LogNormalDistributionBinned extends BaseDistributionBinned { + _set_props() { + this.name = "lognormal"; + this.param_names = ["normal_mean", "normal_std"]; + this.n_bounds_samples = 1000; + this.n_largest_bound_sample = 10; + } + + _nth_largest(samples, n) { + var largest_buffer = Array(n).fill(-Infinity); + for (const sample of samples) { + if (sample > largest_buffer[n - 1]) { + var i = n; + while ((i > 0) & (sample > largest_buffer[i - 1])) { + i -= 1; + } + largest_buffer[i] = sample; + } + } + return largest_buffer[n - 1]; + } + get_bounds() { + let samples = Array(this.n_bounds_samples) + .fill(0) + .map(() => this.sample()); + return [ + math.min(samples), + this._nth_largest(samples, this.n_largest_bound_sample) + ]; + } + bin() { + return this._adabin(); + } +} + +function evenly_spaced_grid(start, stop, numel) { + return Array(numel) + .fill(0) + .map((_, idx) => start + (idx / numel) * (stop - start)); +} + +const distrs = { + normal: NormalDistributionBinned, + lognormal: LogNormalDistributionBinned, + uniform: UniformDistributionBinned +}; + +exports.distrs = distrs; diff --git a/src/components/editor/index.js b/src/components/editor/index.js new file mode 100644 index 00000000..64de1ba4 --- /dev/null +++ b/src/components/editor/index.js @@ -0,0 +1,31 @@ +import "./styles.css"; +const embed = require("vega-embed").embed; +const get_pdf_from_user_input = require("./main.js").get_pdf_from_user_input; + +let [y, x] = get_pdf_from_user_input("normal(1, 1) / normal(10, 1)"); + +let pdf = x.map((val, idx) => ({ x: val, pdf: y[idx] })); + +let spec = { + data: { + values: pdf + }, + mark: { type: "area", line: true }, + encoding: { + x: { field: "x", type: "quantitative" }, + y: { + field: "pdf", + type: "quantitative", + scale: { domain: [0, 3 * Math.max(...y)] } + } + }, + width: 500 +}; + +embed("#viz", spec); + +console.log(y.reduce((a, b) => a + b)); + +document.getElementById("app").innerHTML = ` +
+`; diff --git a/src/components/editor/main.js b/src/components/editor/main.js new file mode 100644 index 00000000..3ff9b355 --- /dev/null +++ b/src/components/editor/main.js @@ -0,0 +1,278 @@ +// The main algorithmic work is done by functions in this module. +// It also contains the main function, taking the user's string +// and returning pdf values and x's. + +const distrs = require("./distribution.js").distrs; +const parse = require("./parse.js"); +const _math = require("mathjs"); +const math = _math.create(_math.all); +const bst = require("binary-search-tree"); + +const NUM_MC_SAMPLES = 300; +const OUTPUT_GRID_NUMEL = 300; + +function evenly_spaced_grid(start, stop, numel) { + return Array(numel) + .fill(0) + .map((_, idx) => start + (idx / numel) * (stop - start)); +} + +function get_distributions(substrings) { + // Takes an array of strings like "normal(0, 1)" and + // returns the corresponding distribution objects + let names_and_args = substrings.map(parse.get_distr_name_and_args); + let pdfs = names_and_args.map(x => new distrs[x[0]](x[1])); + return pdfs; +} + +function update_transformed_divider_points_bst( + transform_func, + deterministic_pdf, + mc_distrs, + track_idx, + num_mc_samples, + bst_pts_and_idxs +) { + // update the binary search tree with bin points of + // deterministic_pdf transformed by tansform func + // (transfrom func can be a stocahstic func with parameters + // sampled from mc_distrs) + var transformed_pts = []; + var pdf_inner_idxs = []; + var factors = []; + var start_pt = Infinity; + var end_pt = -Infinity; + let use_mc = mc_distrs.length > 0; + var num_outer_iters = use_mc ? num_mc_samples : 1; + + for (let sample_idx = 0; sample_idx < num_outer_iters; ++sample_idx) { + var this_transformed_pts = deterministic_pdf.divider_pts; + if (use_mc) { + let samples = mc_distrs.map(x => x.sample()); + this_transformed_pts = this_transformed_pts.map(x => + transform_func([x].concat(samples)) + ); + } else { + this_transformed_pts = this_transformed_pts.map(x => transform_func([x])); + } + var this_transformed_pts_paired = []; + for (let tp_idx = 0; tp_idx < this_transformed_pts.length - 1; tp_idx++) { + let sorted = [ + this_transformed_pts[tp_idx], + this_transformed_pts[tp_idx + 1] + ].sort((a, b) => a - b); + if (sorted[0] < start_pt) { + start_pt = sorted[0]; + } + if (sorted[1] > end_pt) { + end_pt = sorted[1]; + } + this_transformed_pts_paired.push(sorted); + } + + transformed_pts = transformed_pts.concat(this_transformed_pts_paired); + + pdf_inner_idxs = pdf_inner_idxs.concat([ + ...Array(this_transformed_pts_paired.length).keys() + ]); + var this_factors = []; + for (let idx = 0; idx < this_transformed_pts_paired.length; idx++) { + this_factors.push( + (deterministic_pdf.divider_pts[idx + 1] - + deterministic_pdf.divider_pts[idx]) / + (this_transformed_pts_paired[idx][1] - + this_transformed_pts_paired[idx][0]) + ); + } + factors = factors.concat(this_factors); + } + for (let i = 0; i < transformed_pts.length; ++i) { + bst_pts_and_idxs.insert(transformed_pts[i][0], { + start: transformed_pts[i][0], + end: transformed_pts[i][1], + idx: [track_idx, pdf_inner_idxs[i]], + factor: factors[i] / num_outer_iters + }); + } + return [start_pt, end_pt]; +} + +function get_final_pdf(pdf_vals, bst_pts_and_idxs, output_grid) { + // Take the binary search tree with transformed bin points, + // and an array of pdf values associated with the bins, + // and return a pdf over an evenly spaced grid + var offset = output_grid[1] / 2 - output_grid[0] / 2; + var active_intervals = new Map(); + var active_endpoints = new bst.AVLTree(); + var final_pdf_vals = []; + for ( + let out_grid_idx = 0; + out_grid_idx < output_grid.length; + ++out_grid_idx + ) { + let startpoints_within_bin = bst_pts_and_idxs.betweenBounds({ + $gte: output_grid[out_grid_idx] - offset, + $lt: output_grid[out_grid_idx] + offset + }); + for (let interval of startpoints_within_bin) { + active_intervals.set(interval.idx, [ + interval.start, + interval.end, + interval.factor + ]); + active_endpoints.insert(interval.end, interval.idx); + } + var contrib = 0; + for (let [pdf_idx, bounds_and_ratio] of active_intervals.entries()) { + let overlap_start = Math.max( + output_grid[out_grid_idx] - offset, + bounds_and_ratio[0] + ); + let overlap_end = Math.min( + output_grid[out_grid_idx] + offset, + bounds_and_ratio[1] + ); + let interval_size = bounds_and_ratio[1] - bounds_and_ratio[0]; + let contrib_frac = + interval_size === 0 + ? 0 + : (overlap_end - overlap_start) * bounds_and_ratio[2]; + let t = contrib_frac * pdf_vals[pdf_idx[0]][pdf_idx[1]]; + contrib += t; + } + final_pdf_vals.push(contrib); + let endpoints_within_bin = active_endpoints.betweenBounds({ + $gte: output_grid[out_grid_idx] - offset, + $lt: output_grid[out_grid_idx] + offset + }); + for (let interval_idx of endpoints_within_bin) { + active_intervals.delete(interval_idx); + } + } + return final_pdf_vals; +} + +function get_pdf_from_user_input(user_input_string) { + // Entrypoint. Pass user input strings to this function, + // get the corresponding pdf values and input points back. + // If the pdf requires monte carlo (it contains a between-distr function) + // we first determing which distr to have deterministic + // and whih to sample from. This is decided based on which + // choice gives the least variance. + let parsed = parse.parse_initial_string(user_input_string); + let mm_args = parse.separate_mm_args(parsed.mm_args_string); + const is_mm = mm_args.distrs.length > 0; + let tree = new bst.AVLTree(); + let possible_start_pts = []; + let possible_end_pts = []; + let all_vals = []; + let weights = is_mm ? math.compile(mm_args.weights).evaluate()._data : [1]; + let weights_sum = weights.reduce((a, b) => a + b); + weights = weights.map(x => x / weights_sum); + let n_iters = is_mm ? mm_args.distrs.length : 1; + for (let i = 0; i < n_iters; ++i) { + let distr_string = is_mm ? mm_args.distrs[i] : parsed.outer_string; + var [deterministic_pdf, mc_distrs] = choose_pdf_func(distr_string); + var grid_transform = get_grid_transform(distr_string); + var [start_pt, end_pt] = update_transformed_divider_points_bst( + grid_transform, + deterministic_pdf, + mc_distrs, + i, + NUM_MC_SAMPLES, + tree + ); + possible_start_pts.push(start_pt); + possible_end_pts.push(end_pt); + all_vals.push(deterministic_pdf.pdf_vals.map(x => x * weights[i])); + } + start_pt = Math.min(...possible_start_pts); + end_pt = Math.max(...possible_end_pts); + let output_grid = evenly_spaced_grid(start_pt, end_pt, OUTPUT_GRID_NUMEL); + let final_pdf_vals = get_final_pdf(all_vals, tree, output_grid); + return [final_pdf_vals, output_grid]; +} + +function variance(vals) { + var vari = 0; + for (let i = 0; i < vals[0].length; ++i) { + let mean = 0; + let this_vari = 0; + for (let val of vals) { + mean += val[i] / vals.length; + } + for (let val of vals) { + this_vari += (val[i] - mean) ** 2; + } + vari += this_vari; + } + return vari; +} + +function pluck_from_array(array, idx) { + return [array[idx], array.slice(0, idx).concat(array.slice(idx + 1))]; +} + +function choose_pdf_func(distr_string) { + // If distr_string requires MC, try all possible + // choices for the deterministic distribution, + // and pick the one with the least variance. + var variances = []; + let transform_func = get_grid_transform(distr_string); + let substrings = parse.get_distr_substrings(distr_string); + var pdfs = get_distributions(substrings); + if (pdfs.length === 1) { + return [pdfs[0], []]; + } + var start_pt = 0; + var end_pt = 0; + for (let i = 0; i < pdfs.length; ++i) { + var outputs = []; + for (let j = 0; j < 20; ++j) { + let tree = new bst.AVLTree(); + let [deterministic_pdf, mc_distrs] = pluck_from_array(pdfs, i); + let [this_start_pt, this_end_pt] = update_transformed_divider_points_bst( + transform_func, + deterministic_pdf, + mc_distrs, + 0, + 10, + tree + ); + [start_pt, end_pt] = + j === 0 ? [this_start_pt, this_end_pt] : [start_pt, end_pt]; + var output_grid = evenly_spaced_grid(start_pt, end_pt, 100); + let final_pdf_vals = get_final_pdf( + [deterministic_pdf.pdf_vals], + tree, + output_grid + ); + outputs.push(final_pdf_vals); + } + variances.push(variance(outputs)); + } + let best_variance = Math.min(...variances); + let best_idx = variances + .map((val, idx) => [val, idx]) + .filter(x => x[0] === best_variance)[0][1]; + let mc_distrs = pdfs.slice(0, best_idx).concat(pdfs.slice(best_idx + 1)); + return [pdfs[best_idx], mc_distrs]; +} + +function get_grid_transform(distr_string) { + let substrings = parse.get_distr_substrings(distr_string); + let arg_strings = []; + for (let i = 0; i < substrings.length; ++i) { + distr_string = distr_string.replace(substrings[i], "x_" + i.toString()); + arg_strings.push("x_" + i.toString()); + } + let compiled = math.compile(distr_string); + function grid_transform(x) { + let kv_pairs = arg_strings.map((val, idx) => [val, x[idx]]); + let args_obj = Object.fromEntries(new Map(kv_pairs)); + return compiled.evaluate(args_obj); + } + return grid_transform; +} + +exports.get_pdf_from_user_input = get_pdf_from_user_input; diff --git a/src/components/editor/parse.js b/src/components/editor/parse.js new file mode 100644 index 00000000..e81818c9 --- /dev/null +++ b/src/components/editor/parse.js @@ -0,0 +1,119 @@ +// Functions for parsing/processing user input strings are here +const _math = require("mathjs"); +const math = _math.create(_math.all); + +const DISTR_REGEXS = [ + /beta\(/g, + /(log)?normal\(/g, + /multimodal\(/g, + /mm\(/g, + /uniform\(/g +]; + +function parse_initial_string(user_input_string) { + let outer_output_string = ""; + let mm_args_string = ""; + let idx = 0; + while (idx < user_input_string.length) { + if ( + user_input_string.substring(idx - 11, idx) === "multimodal(" || + user_input_string.substring(idx - 3, idx) === "mm(" + ) { + let num_open_brackets = 1; + while (num_open_brackets > 0 && idx < user_input_string.length) { + mm_args_string += user_input_string[idx]; + idx += 1; + if (user_input_string[idx] === ")") { + num_open_brackets -= 1; + } else if (user_input_string[idx] === "(") { + num_open_brackets += 1; + } + } + outer_output_string += ")"; + idx += 1; + } else { + outer_output_string += user_input_string[idx]; + idx += 1; + } + } + return { + outer_string: outer_output_string, + mm_args_string: mm_args_string + }; +} + +function separate_mm_args(mm_args_string) { + if (mm_args_string.endsWith(",")) { + mm_args_string = mm_args_string.slice(0, -1); + } + let args_array = []; + let num_open_brackets = 0; + let arg_substring = ""; + for (let char of mm_args_string) { + if (num_open_brackets === 0 && char === ",") { + args_array.push(arg_substring.trim()); + arg_substring = ""; + } else { + if (char === ")" || char === "]") { + num_open_brackets -= 1; + } else if (char === "(" || char === "[") { + num_open_brackets += 1; + } + arg_substring += char; + } + } + return { + distrs: args_array, + weights: arg_substring.trim() + }; +} + +function get_distr_substrings(distr_string) { + let substrings = []; + for (let regex of DISTR_REGEXS) { + let matches = distr_string.matchAll(regex); + for (let match of matches) { + let idx = match.index + match[0].length; + let num_open_brackets = 1; + let distr_substring = ""; + while (num_open_brackets !== 0 && idx < distr_string.length) { + distr_substring += distr_string[idx]; + if (distr_string[idx] === "(") { + num_open_brackets += 1; + } else if (distr_string[idx] === ")") { + num_open_brackets -= 1; + } + idx += 1; + } + substrings.push((match[0] + distr_substring).trim()); + } + } + + return substrings; +} + +function get_distr_name_and_args(substr) { + let distr_name = ""; + let args_str = ""; + let args_flag = false; + for (let char of substr) { + if (!args_flag && char !== "(") { + distr_name += char; + } + if (args_flag && char !== ")") { + args_str += char; + } + if (char === "(") { + args_str += "["; + args_flag = true; + } + } + args_str += "]"; + let args = math.compile(args_str).evaluate()._data; + return [distr_name, args]; +} + +exports.get_distr_name_and_args = get_distr_name_and_args; +exports.get_distr_substrings = get_distr_substrings; +exports.separate_mm_args = separate_mm_args; +exports.parse_initial_string = parse_initial_string; diff --git a/yarn.lock b/yarn.lock index 59e4f2b4..244be22d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2333,6 +2333,13 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== +binary-search-tree@0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/binary-search-tree/-/binary-search-tree-0.2.6.tgz#c6d29194e286827fcffe079010e6bf77def10ce3" + integrity sha1-xtKRlOKGgn/P/geQEOa/d97xDOM= + dependencies: + underscore "~1.4.4" + bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -2889,7 +2896,7 @@ commander@2, commander@^2.11.0, commander@^2.18.0, commander@^2.19.0, commander@ resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -complex.js@2.0.11: +complex.js@2.0.11, complex.js@^2.0.11: version "2.0.11" resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.11.tgz#09a873fbf15ffd8c18c9c2201ccef425c32b8bf1" integrity sha512-6IArJLApNtdg1P1dFtn3dnyzoZBEF0MwMnrfF1exSBRpZYoy4yieMkpZhQDC0uwctw48vii0CFVyHfpgZ/DfGw== @@ -3632,7 +3639,7 @@ decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= -decimal.js@10.2.0: +decimal.js@10.2.0, decimal.js@^10.2.0: version "10.2.0" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.0.tgz#39466113a9e036111d02f82489b5fd6b0b5ed231" integrity sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw== @@ -4043,7 +4050,7 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-latex@1.2.0: +escape-latex@1.2.0, escape-latex@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/escape-latex/-/escape-latex-1.2.0.tgz#07c03818cf7dac250cce517f4fda1b001ef2bca1" integrity sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw== @@ -4438,7 +4445,7 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -fraction.js@4.0.12: +fraction.js@4.0.12, fraction.js@^4.0.12: version "4.0.12" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.12.tgz#0526d47c65a5fb4854df78bc77f7bec708d7b8c3" integrity sha512-8Z1K0VTG4hzYY7kA/1sj4/r1/RWLBD3xwReT/RCrUCbzPszjNQCCsy3ktkU/eaEqX3MYa4pY37a52eiBlPMlhA== @@ -5410,7 +5417,7 @@ iterall@^1.2.1, iterall@^1.2.2: resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== -javascript-natural-sort@0.7.1: +javascript-natural-sort@0.7.1, javascript-natural-sort@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59" integrity sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k= @@ -6320,6 +6327,11 @@ jstat@1.9.0: resolved "https://registry.yarnpkg.com/jstat/-/jstat-1.9.0.tgz#96a625f5697566f6ba3b15832fb371f9451b8614" integrity sha512-xSsSJ3qY4rS+u8+dAwRcJ0LQGxNdibdW6rSalNPZDbLYkW1C7b0/j79IxXtQjrweqMNI3asN7FCIPceNSIJr2g== +jstat@1.9.2: + version "1.9.2" + resolved "https://registry.yarnpkg.com/jstat/-/jstat-1.9.2.tgz#cd2d24df200fd3488861dc7868be01ff65a238cc" + integrity sha512-nc3uAadgrWWvJz6RyXUFN0lvTWEXYxMVIrm6ZVoOh4YPLvukLKYpqMofKIE2ReWkL7gFw6hEo6VWZjotYW2Bsw== + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -6559,6 +6571,20 @@ mathjs@5.10.3: tiny-emitter "2.1.0" typed-function "1.1.0" +mathjs@6.6.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-6.6.0.tgz#0d71c7cc6b50bd112e160b55703a395caf4db4b1" + integrity sha512-gYYc1+irFbwuwYUx6O8G2YauvbD1+tPBbq829PaxAiRWpPzPEE8pvwGgvdMuk6c3pqhm6Do/mN26vLiQP46H5A== + dependencies: + complex.js "^2.0.11" + decimal.js "^10.2.0" + escape-latex "^1.2.0" + fraction.js "^4.0.12" + javascript-natural-sort "^0.7.1" + seed-random "^2.2.0" + tiny-emitter "^2.1.0" + typed-function "^1.1.1" + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -9235,7 +9261,7 @@ screenfull@^5.0.0: resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.0.2.tgz#b9acdcf1ec676a948674df5cd0ff66b902b0bed7" integrity sha512-cCF2b+L/mnEiORLN5xSAz6H3t18i2oHh9BA8+CQlAh5DRw2+NFAGQJOSYbcGw8B2k04g/lVvFcfZ83b3ysH5UQ== -seed-random@2.2.0: +seed-random@2.2.0, seed-random@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/seed-random/-/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54" integrity sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ= @@ -9989,7 +10015,7 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= -tiny-emitter@2.1.0: +tiny-emitter@2.1.0, tiny-emitter@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== @@ -10176,6 +10202,11 @@ typed-function@1.1.0: resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-1.1.0.tgz#ea149706e0fb42aca1791c053a6d94ccd6c4fdcb" integrity sha512-TuQzwiT4DDg19beHam3E66oRXhyqlyfgjHB/5fcvsRXbfmWPJfto9B4a0TBdTrQAPGlGmXh/k7iUI+WsObgORA== +typed-function@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-1.1.1.tgz#a1316187ec3628c9e219b91ca96918660a10138e" + integrity sha512-RbN7MaTQBZLJYzDENHPA0nUmWT0Ex80KHItprrgbTPufYhIlTePvCXZxyQK7wgn19FW5bnuaBIKcBb5mRWjB1Q== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -10208,6 +10239,11 @@ uncss@^0.17.2: postcss-selector-parser "6.0.2" request "^2.88.0" +underscore@~1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604" + integrity sha1-YaajIBBiKvoHljvzJSA88SI51gQ= + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" From c99a7c508574b924447523e77da70aed90c9f806 Mon Sep 17 00:00:00 2001 From: Roman Galochkin Date: Thu, 27 Feb 2020 15:09:04 +0300 Subject: [PATCH 2/4] Works, the first step --- src/App.re | 7 + src/components/DistBuilder2.re | 149 ++++++++++++++++++ .../DistributionPlot/DistributionPlot.re | 2 +- src/components/editor/distribution.js | 8 + src/components/editor/index.js | 31 ---- src/components/editor/main.js | 2 + 6 files changed, 167 insertions(+), 32 deletions(-) create mode 100644 src/components/DistBuilder2.re delete mode 100644 src/components/editor/index.js diff --git a/src/App.re b/src/App.re index 95c76f31..058f25fa 100644 --- a/src/App.re +++ b/src/App.re @@ -1,6 +1,7 @@ type route = | Model(string) | DistBuilder + | DistBuilder2 | Home | NotFound; @@ -8,6 +9,7 @@ let routeToPath = route => switch (route) { | Model(modelId) => "/m/" ++ modelId | DistBuilder => "/dist-builder" + | DistBuilder2 => "/dist-builder2" | Home => "/" | _ => "/" }; @@ -76,6 +78,9 @@ module Menu = { {"Dist Builder" |> E.ste} + + {"Dist Builder 2" |> E.ste} + ; }; }; @@ -88,6 +93,7 @@ let make = () => { switch (url.path) { | ["m", modelId] => Model(modelId) | ["dist-builder"] => DistBuilder + | ["dist-builder2"] => DistBuilder2 | [] => Home | _ => NotFound }; @@ -101,6 +107,7 @@ let make = () => { | None =>
{"Page is not found" |> E.ste}
} | DistBuilder => + | DistBuilder2 => | Home =>
{"Welcome" |> E.ste}
| _ =>
{"Page is not found" |> E.ste}
}} diff --git a/src/components/DistBuilder2.re b/src/components/DistBuilder2.re new file mode 100644 index 00000000..56986523 --- /dev/null +++ b/src/components/DistBuilder2.re @@ -0,0 +1,149 @@ +open BsReform; +open Antd.Grid; + +type shape = (array(float), array(float)); + +[@bs.module "./editor/main.js"] +external getPdfFromUserInput: string => shape = "get_pdf_from_user_input"; + +module FormConfig = [%lenses type state = {guesstimatorString: string}]; + +module Form = ReForm.Make(FormConfig); + +let schema = Form.Validation.Schema([||]); + +module FieldString = { + [@react.component] + let make = (~field, ~label) => { + + E.ste}> + validate()} + /> + + } + />; + }; +}; + +module Styles = { + open Css; + let rows = + style([ + selector( + ">.ant-col:first-child", + [paddingLeft(em(0.25)), paddingRight(em(0.125))], + ), + selector( + ">.ant-col:last-child", + [paddingLeft(em(0.125)), paddingRight(em(0.25))], + ), + selector( + ">.ant-col:not(:first-child):not(:last-child)", + [paddingLeft(em(0.125)), paddingRight(em(0.125))], + ), + ]); + let parent = + style([ + selector(".ant-input-number", [width(`percent(100.))]), + selector(".anticon", [verticalAlign(`zero)]), + ]); + let form = style([backgroundColor(hex("eee")), padding(em(1.))]); + let dist = style([padding(em(1.))]); + let spacer = style([marginTop(em(1.))]); + let groupA = + style([ + selector( + ".ant-input-number-input", + [backgroundColor(hex("fff7db"))], + ), + ]); + let groupB = + style([ + selector( + ".ant-input-number-input", + [backgroundColor(hex("eaf4ff"))], + ), + ]); +}; + +module DemoDist = { + [@react.component] + let make = (~guesstimatorString: string) => { + let (ys, xs) = getPdfFromUserInput("normal(1, 1) / normal(10, 1)"); + let continuous: DistTypes.xyShape = {xs, ys}; + E.ste}> +
+ + ; + }; +}; + +[@react.component] +let make = () => { + let (reloader, setRealoader) = React.useState(() => 1); + let reform = + Form.use( + ~validationStrategy=OnDemand, + ~schema, + ~onSubmit=({state}) => {None}, + ~initialState={guesstimatorString: "normal(1, 1) / normal(10, 1)"}, + (), + ); + + let onSubmit = e => { + e->ReactEvent.Synthetic.preventDefault; + reform.submit(); + }; + + let demoDist = + React.useMemo1( + () => { + + }, + [|reform.state.values.guesstimatorString|], + ); + + let onRealod = _ => { + setRealoader(_ => reloader + 1); + }; + +
+
+ demoDist +
+ E.ste} + extra={ + + }> + + + + + + + + + {"Update Distribution" |> E.ste} + + + + +
+
; +}; diff --git a/src/components/charts/DistributionPlot/DistributionPlot.re b/src/components/charts/DistributionPlot/DistributionPlot.re index 7de5506e..ea4064ed 100644 --- a/src/components/charts/DistributionPlot/DistributionPlot.re +++ b/src/components/charts/DistributionPlot/DistributionPlot.re @@ -114,7 +114,7 @@ let make = ~minX=?, ~yMaxDiscreteDomainFactor=?, ~yMaxContinuousDomainFactor=?, - ~onHover: float => unit, + ~onHover: float => unit=_ => (), ~continuous=?, ~scale=?, ~showDistributionLines=false, diff --git a/src/components/editor/distribution.js b/src/components/editor/distribution.js index 02ef8c5c..c16c1fd5 100644 --- a/src/components/editor/distribution.js +++ b/src/components/editor/distribution.js @@ -82,9 +82,11 @@ class BaseDistributionBinned { let args_str = args.toString() + ")"; let substr = this.name + ".pdf(x, " + args_str; let compiled = math.compile(substr); + function pdf_func(x) { return compiled.evaluate({ x: x }); } + let mc_compiled = math.compile(this.name + ".sample(" + args_str); let kv_pairs = this.param_names.map((val, idx) => [val, args[idx]]); let params = Object.fromEntries(new Map(kv_pairs)); @@ -97,12 +99,14 @@ class NormalDistributionBinned extends BaseDistributionBinned { this.name = "normal"; this.param_names = ["mean", "std"]; } + get_bounds() { return [ this.params.mean - 4 * this.params.std, this.params.mean + 4 * this.params.std ]; } + bin() { return this._adabin(this.params.std); } @@ -114,9 +118,11 @@ class UniformDistributionBinned extends BaseDistributionBinned { this.param_names = ["start_point", "end_point"]; this.num_bins = 200; } + get_bounds() { return [this.params.start_point, this.params.end_point]; } + bin() { let divider_pts = evenly_spaced_grid( this.params.start_point, @@ -152,6 +158,7 @@ class LogNormalDistributionBinned extends BaseDistributionBinned { } return largest_buffer[n - 1]; } + get_bounds() { let samples = Array(this.n_bounds_samples) .fill(0) @@ -161,6 +168,7 @@ class LogNormalDistributionBinned extends BaseDistributionBinned { this._nth_largest(samples, this.n_largest_bound_sample) ]; } + bin() { return this._adabin(); } diff --git a/src/components/editor/index.js b/src/components/editor/index.js deleted file mode 100644 index 64de1ba4..00000000 --- a/src/components/editor/index.js +++ /dev/null @@ -1,31 +0,0 @@ -import "./styles.css"; -const embed = require("vega-embed").embed; -const get_pdf_from_user_input = require("./main.js").get_pdf_from_user_input; - -let [y, x] = get_pdf_from_user_input("normal(1, 1) / normal(10, 1)"); - -let pdf = x.map((val, idx) => ({ x: val, pdf: y[idx] })); - -let spec = { - data: { - values: pdf - }, - mark: { type: "area", line: true }, - encoding: { - x: { field: "x", type: "quantitative" }, - y: { - field: "pdf", - type: "quantitative", - scale: { domain: [0, 3 * Math.max(...y)] } - } - }, - width: 500 -}; - -embed("#viz", spec); - -console.log(y.reduce((a, b) => a + b)); - -document.getElementById("app").innerHTML = ` -
-`; diff --git a/src/components/editor/main.js b/src/components/editor/main.js index 3ff9b355..c44b314b 100644 --- a/src/components/editor/main.js +++ b/src/components/editor/main.js @@ -267,11 +267,13 @@ function get_grid_transform(distr_string) { arg_strings.push("x_" + i.toString()); } let compiled = math.compile(distr_string); + function grid_transform(x) { let kv_pairs = arg_strings.map((val, idx) => [val, x[idx]]); let args_obj = Object.fromEntries(new Map(kv_pairs)); return compiled.evaluate(args_obj); } + return grid_transform; } From 0931914ae4fa8503f2c7b284ba1c6eec37111fea Mon Sep 17 00:00:00 2001 From: Roman Galochkin Date: Thu, 27 Feb 2020 15:12:19 +0300 Subject: [PATCH 3/4] Fixes height --- src/components/DistBuilder2.re | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DistBuilder2.re b/src/components/DistBuilder2.re index 56986523..06325721 100644 --- a/src/components/DistBuilder2.re +++ b/src/components/DistBuilder2.re @@ -78,7 +78,7 @@ module DemoDist = { let continuous: DistTypes.xyShape = {xs, ys}; E.ste}>
- + ; }; }; From 306ddb70c02889a85594e64389f1d8a9d9058970 Mon Sep 17 00:00:00 2001 From: Roman Galochkin Date: Thu, 27 Feb 2020 15:16:48 +0300 Subject: [PATCH 4/4] Removes obsolete parts --- src/components/DistBuilder2.re | 73 +++-------------------------- src/components/editor/DistEditor.re | 3 ++ 2 files changed, 9 insertions(+), 67 deletions(-) create mode 100644 src/components/editor/DistEditor.re diff --git a/src/components/DistBuilder2.re b/src/components/DistBuilder2.re index 06325721..d6cb3eb0 100644 --- a/src/components/DistBuilder2.re +++ b/src/components/DistBuilder2.re @@ -1,11 +1,6 @@ open BsReform; open Antd.Grid; -type shape = (array(float), array(float)); - -[@bs.module "./editor/main.js"] -external getPdfFromUserInput: string => shape = "get_pdf_from_user_input"; - module FormConfig = [%lenses type state = {guesstimatorString: string}]; module Form = ReForm.Make(FormConfig); @@ -32,49 +27,15 @@ module FieldString = { module Styles = { open Css; - let rows = - style([ - selector( - ">.ant-col:first-child", - [paddingLeft(em(0.25)), paddingRight(em(0.125))], - ), - selector( - ">.ant-col:last-child", - [paddingLeft(em(0.125)), paddingRight(em(0.25))], - ), - selector( - ">.ant-col:not(:first-child):not(:last-child)", - [paddingLeft(em(0.125)), paddingRight(em(0.125))], - ), - ]); - let parent = - style([ - selector(".ant-input-number", [width(`percent(100.))]), - selector(".anticon", [verticalAlign(`zero)]), - ]); - let form = style([backgroundColor(hex("eee")), padding(em(1.))]); let dist = style([padding(em(1.))]); let spacer = style([marginTop(em(1.))]); - let groupA = - style([ - selector( - ".ant-input-number-input", - [backgroundColor(hex("fff7db"))], - ), - ]); - let groupB = - style([ - selector( - ".ant-input-number-input", - [backgroundColor(hex("eaf4ff"))], - ), - ]); }; module DemoDist = { [@react.component] let make = (~guesstimatorString: string) => { - let (ys, xs) = getPdfFromUserInput("normal(1, 1) / normal(10, 1)"); + let (ys, xs) = + DistEditor.getPdfFromUserInput("normal(1, 1) / normal(10, 1)"); let continuous: DistTypes.xyShape = {xs, ys}; E.ste}>
@@ -85,7 +46,6 @@ module DemoDist = { [@react.component] let make = () => { - let (reloader, setRealoader) = React.useState(() => 1); let reform = Form.use( ~validationStrategy=OnDemand, @@ -95,11 +55,6 @@ let make = () => { (), ); - let onSubmit = e => { - e->ReactEvent.Synthetic.preventDefault; - reform.submit(); - }; - let demoDist = React.useMemo1( () => { @@ -110,26 +65,14 @@ let make = () => { [|reform.state.values.guesstimatorString|], ); - let onRealod = _ => { - setRealoader(_ => reloader + 1); - }; - -
+
demoDist
- E.ste} - extra={ - - }> + E.ste}> - - + + { /> - - {"Update Distribution" |> E.ste} - diff --git a/src/components/editor/DistEditor.re b/src/components/editor/DistEditor.re new file mode 100644 index 00000000..ea315d11 --- /dev/null +++ b/src/components/editor/DistEditor.re @@ -0,0 +1,3 @@ +[@bs.module "./main.js"] +external getPdfFromUserInput: string => (array(float), array(float)) = + "get_pdf_from_user_input";