From f662ccd6c6348e6b00578bff72be44de4790ce22 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Mon, 23 Mar 2020 21:31:06 +0000 Subject: [PATCH] First full-through with symbolic parsing --- __tests__/Jstat__test.re | 11 ++ __tests__/Parser__test.re | 22 ++++ __tests__/math__test.js | 5 + __tests__/mathtest.js | 64 +++++++++ bsconfig.json | 20 +-- package.json | 3 +- src/components/editor/distribution.js | 4 +- src/components/editor/main.js | 2 + src/distributions/XYShape.re | 7 - src/symbolic/Jstat.re | 183 ++++++++++++++++++++++++++ src/symbolic/MathJsParser.re | 58 ++++++++ src/symbolic/Mathjs.re | 2 + src/symbolic/MathjsWrapper.js | 8 ++ yarn.lock | 14 +- 14 files changed, 376 insertions(+), 27 deletions(-) create mode 100644 __tests__/Jstat__test.re create mode 100644 __tests__/Parser__test.re create mode 100644 __tests__/math__test.js create mode 100644 __tests__/mathtest.js create mode 100644 src/symbolic/Jstat.re create mode 100644 src/symbolic/MathJsParser.re create mode 100644 src/symbolic/Mathjs.re create mode 100644 src/symbolic/MathjsWrapper.js diff --git a/__tests__/Jstat__test.re b/__tests__/Jstat__test.re new file mode 100644 index 00000000..3d48ef35 --- /dev/null +++ b/__tests__/Jstat__test.re @@ -0,0 +1,11 @@ +open Jest; +open Expect; + +describe("Shape", () => { + describe("Continuous", () => { + test("", () => { + Js.log(Jstat.Jstat.normal); + expect(Jstat.Jstat.normal##pdf(3.0, 3.0, 3.0)) |> toEqual(1.0); + }) + }) +}); \ No newline at end of file diff --git a/__tests__/Parser__test.re b/__tests__/Parser__test.re new file mode 100644 index 00000000..79ee6ac2 --- /dev/null +++ b/__tests__/Parser__test.re @@ -0,0 +1,22 @@ +open Jest; +open Expect; + +let json = Mathjs.parseMath("mm(normal(5,2), normal(10))"); + +describe("Shape", () => { + describe("Parser", () => { + test("", () => { + let parsed1 = MathJsParser.parseMathjs(json); + let parsed2 = + ( + switch (parsed1 |> E.O.fmap(MathJsParser.toValue)) { + | Some(Ok(r)) => Some(r) + | _ => None + } + ) + |> E.O.fmap(Jstat.toString); + Js.log2("YOYOYYO", parsed2); + expect(1.0) |> toEqual(1.0); + }) + }) +}); \ No newline at end of file diff --git a/__tests__/math__test.js b/__tests__/math__test.js new file mode 100644 index 00000000..ec937914 --- /dev/null +++ b/__tests__/math__test.js @@ -0,0 +1,5 @@ +const foo = require('./mathtest') + +test('sdf', () => { + expect(1).toBe(2) +}) \ No newline at end of file diff --git a/__tests__/mathtest.js b/__tests__/mathtest.js new file mode 100644 index 00000000..239d402e --- /dev/null +++ b/__tests__/mathtest.js @@ -0,0 +1,64 @@ +// This example demonstrates importing a custom data type, +// and extending an existing function (add) with support for this data type. + +const { create, factory, all } = require('mathjs'); +const math = create(all) + +// factory function which defines a new data type FunctionalDistribution +const createFunctionalDistribution = factory('FunctionalDistribution', ['typed'], ({ typed }) => { + // create a new data type + function FunctionalDistribution (value) { + this.value = value + } + FunctionalDistribution.prototype.isFunctionalDistribution = true + FunctionalDistribution.prototype.toString = function () { + return 'FunctionalDistribution:' + this.value + } + + // define a new data type with typed-function + typed.addType({ + name: 'FunctionalDistribution', + test: function (x) { + // test whether x is of type FunctionalDistribution + return x && x.isFunctionalDistribution === true + } + }) + + return FunctionalDistribution +}) + +// function add which can add the FunctionalDistribution data type +// When imported in math.js, the existing function `add` with support for +// FunctionalDistribution, because both implementations are typed-functions and do not +// have conflicting signatures. +const createAddFunctionalDistribution = factory('add', ['typed', 'FunctionalDistribution'], ({ typed, FunctionalDistribution }) => { + return typed('add', { + 'FunctionalDistribution, FunctionalDistribution': function (a, b) { + return new FunctionalDistribution(a.value + b.value) + } + }) +}) + +const createSubtractFunctionalDistribution = factory('subtract', ['typed', 'FunctionalDistribution'], ({ typed, FunctionalDistribution }) => { + return typed('subtract', { + 'FunctionalDistribution, FunctionalDistribution': function (a, b) { + return new FunctionalDistribution(a.value - b.value) + } + }) +}) + +// import the new data type and function +math.import([ + createFunctionalDistribution, + createAddFunctionalDistribution, + createSubtractFunctionalDistribution +]) + +// use the new type +const ans = math.chain(new math.FunctionalDistribution(2)).subtract(new math.FunctionalDistribution(3)) +// ans = FunctionalDistribution(5) + +console.log(ans.toString()) +// outputs 'FunctionalDistribution:5' + +module.exports = ans diff --git a/bsconfig.json b/bsconfig.json index 8ff71201..7661e312 100644 --- a/bsconfig.json +++ b/bsconfig.json @@ -3,7 +3,8 @@ "reason": { "react-jsx": 3 }, - "sources": [{ + "sources": [ + { "dir": "src", "subdirs": true }, @@ -19,14 +20,17 @@ } ], "bsc-flags": ["-bs-super-errors", "-bs-no-version-header"], - "package-specs": [{ - "module": "commonjs", - "in-source": true - }], + "package-specs": [ + { + "module": "commonjs", + "in-source": true + } + ], "suffix": ".bs.js", "namespace": true, "bs-dependencies": [ "@glennsl/bs-jest", + "@glennsl/bs-json", "@foretold/components", "bs-ant-design-alt", "reason-react", @@ -37,7 +41,5 @@ "reschema" ], "refmt": 3, - "ppx-flags": [ - "lenses-ppx/ppx" - ] -} \ No newline at end of file + "ppx-flags": ["lenses-ppx/ppx"] +} diff --git a/package.json b/package.json index b14a95ae..aab31893 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@foretold/components": "0.0.3", "@foretold/guesstimator": "1.0.10", "@glennsl/bs-jest": "^0.5.0", + "@glennsl/bs-json": "^5.0.2", "antd": "3.17.0", "autoprefixer": "9.7.4", "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", @@ -65,4 +66,4 @@ "react": "./node_modules/react", "react-dom": "./node_modules/react-dom" } -} \ No newline at end of file +} diff --git a/src/components/editor/distribution.js b/src/components/editor/distribution.js index 4e212fa3..00543338 100644 --- a/src/components/editor/distribution.js +++ b/src/components/editor/distribution.js @@ -25,7 +25,7 @@ class BaseDistributionBinned { this.max_bin_size = 0.005; this.min_bin_size = 0; this.increment = 0.0001; - this.desired_delta = 0.0001; + this.desired_delta = 0.001; this.start_bin_size = 0.0001; [this.params, this.pdf_func, this.sample] = this.get_params_and_pdf_func( @@ -44,6 +44,8 @@ class BaseDistributionBinned { throw new Error("NotImplementedError"); } + + //Adaptive binning. Specify a desired change in density to get adjusted bin sizes. /** * @returns {(number[]|[*])[]} * @private diff --git a/src/components/editor/main.js b/src/components/editor/main.js index bc85200c..f88a3a8e 100644 --- a/src/components/editor/main.js +++ b/src/components/editor/main.js @@ -8,6 +8,7 @@ const math = _math.create(_math.all); const NUM_MC_SAMPLES = 3000; const OUTPUT_GRID_NUMEL = 3000; + /** * The main algorithmic work is done by functions in this module. * It also contains the main function, taking the user's string @@ -290,6 +291,7 @@ function pluck_from_array(array, idx) { * If distr_string requires MC, try all possible * choices for the deterministic distribution, * and pick the one with the least variance. + * It's much better to sample from a normal than a lognormal. * * @param distr_string * @returns {(*|*[])[]|*[]} diff --git a/src/distributions/XYShape.re b/src/distributions/XYShape.re index 40ebd46f..7f7e073e 100644 --- a/src/distributions/XYShape.re +++ b/src/distributions/XYShape.re @@ -200,13 +200,6 @@ module T = { | (true, true) => (-1) }; - // todo: This is broken :( - let combine = (t1: t, t2: t) => { - let array = Belt.Array.concat(zip(t1), zip(t2)); - Array.fast_sort(comparePoints, array); - array |> Belt.Array.unzip |> fromArray; - }; - // TODO: I'd bet this is pretty slow let intersperce = (t1: t, t2: t) => { let items: ref(array((float, float))) = ref([||]); diff --git a/src/symbolic/Jstat.re b/src/symbolic/Jstat.re new file mode 100644 index 00000000..654c2bfc --- /dev/null +++ b/src/symbolic/Jstat.re @@ -0,0 +1,183 @@ +// Todo: Another way of doing this is with [@bs.scope "normal"], which may be more elegant +module Jstat = { + type normal = { + . + [@bs.meth] "pdf": (float, float, float) => float, + [@bs.meth] "cdf": (float, float, float) => float, + [@bs.meth] "inv": (float, float, float) => float, + [@bs.meth] "sample": (float, float) => float, + }; + type lognormal = { + . + [@bs.meth] "pdf": (float, float, float) => float, + [@bs.meth] "cdf": (float, float, float) => float, + [@bs.meth] "inv": (float, float, float) => float, + [@bs.meth] "sample": (float, float) => float, + }; + type uniform = { + . + [@bs.meth] "pdf": (float, float, float) => float, + [@bs.meth] "cdf": (float, float, float) => float, + [@bs.meth] "inv": (float, float, float) => float, + [@bs.meth] "sample": (float, float) => float, + }; + [@bs.module "jStat"] external normal: normal = "normal"; + [@bs.module "jStat"] external lognormal: lognormal = "lognormal"; + [@bs.module "jStat"] external uniform: uniform = "uniform"; +}; + +type normal = { + mean: float, + stdev: float, +}; + +type lognormal = { + mu: float, + sigma: float, +}; + +type uniform = { + low: float, + high: float, +}; + +module Normal = { + type t = normal; + let pdf = (x, t: t) => Jstat.normal##pdf(x, t.mean, t.stdev); + let inv = (p, t: t) => Jstat.normal##inv(p, t.mean, t.stdev); + let sample = (t: t) => Jstat.normal##sample(t.mean, t.stdev); + let toString = ({mean, stdev}: t) => {j|Normal($mean,$stdev)|j}; +}; + +module Lognormal = { + type t = lognormal; + let pdf = (x, t: t) => Jstat.lognormal##pdf(x, t.mu, t.sigma); + let inv = (p, t: t) => Jstat.lognormal##inv(p, t.mu, t.sigma); + let sample = (t: t) => Jstat.lognormal##sample(t.mu, t.sigma); + let toString = ({mu, sigma}: t) => {j|Lognormal($mu,$sigma)|j}; +}; + +module Uniform = { + type t = uniform; + let pdf = (x, t: t) => Jstat.uniform##pdf(x, t.low, t.high); + let inv = (p, t: t) => Jstat.uniform##inv(p, t.low, t.high); + let sample = (t: t) => Jstat.uniform##sample(t.low, t.high); + let toString = ({low, high}: t) => {j|Uniform($low,$high)|j}; +}; + +type dist = [ + | `Normal(normal) + | `Lognormal(lognormal) + | `Uniform(uniform) +]; + +module Mixed = { + let pdf = (x, dist) => + switch (dist) { + | `Normal(n) => Normal.pdf(x, n) + | `Lognormal(n) => Lognormal.pdf(x, n) + | `Uniform(n) => Uniform.pdf(x, n) + }; + + let inv = (x, dist) => + switch (dist) { + | `Normal(n) => Normal.inv(x, n) + | `Lognormal(n) => Lognormal.inv(x, n) + | `Uniform(n) => Uniform.inv(x, n) + }; + + let sample = dist => + switch (dist) { + | `Normal(n) => Normal.sample(n) + | `Lognormal(n) => Lognormal.sample(n) + | `Uniform(n) => Uniform.sample(n) + }; + + let toString = dist => + switch (dist) { + | `Normal(n) => Normal.toString(n) + | `Lognormal(n) => Lognormal.toString(n) + | `Uniform(n) => Uniform.toString(n) + }; + + let min = dist => + switch (dist) { + | `Normal(n) => Normal.inv(0.01, n) + | `Lognormal(n) => Lognormal.inv(0.01, n) + | `Uniform({low}) => low + }; + + let max = dist => + switch (dist) { + | `Normal(n) => Normal.inv(0.99, n) + | `Lognormal(n) => Lognormal.inv(0.99, n) + | `Uniform({high}) => high + }; + + // will space linear + let toShape = + (~xSelection: [ | `Linear | `ByWeight]=`Linear, dist: dist, sampleCount) => { + let xs = + switch (xSelection) { + | `Linear => Functions.range(min(dist), max(dist), sampleCount) + | `ByWeight => + Functions.range(0.001, 0.999, sampleCount) + |> E.A.fmap(x => inv(x, dist)) + }; + let ys = xs |> E.A.fmap(r => pdf(r, dist)); + XYShape.T.fromArrays(xs, ys); + }; +}; + +// module PointwiseCombination = { +// type math = Multiply | Add | Exponent | Power; +// let fn = fun +// | Multiply => 3.0 +// | Add => 4.0 +// } + +module PointwiseAddDistributionsWeighted = { + type t = array((dist, float)); + + let normalizeWeights = (dists: t) => { + let total = dists |> E.A.fmap(snd) |> Functions.sum; + dists |> E.A.fmap(((a, b)) => (a, b /. total)); + }; + + let pdf = (dists: t, x: float) => + dists |> E.A.fmap(((e, w)) => Mixed.pdf(x, e) *. w) |> Functions.sum; + + let min = (dists: t) => + dists |> E.A.fmap(d => d |> fst |> Mixed.min) |> Functions.min; + + let max = (dists: t) => + dists |> E.A.fmap(d => d |> fst |> Mixed.min) |> Functions.min; + + let toShape = (dists: t, sampleCount: int) => { + let xs = Functions.range(min(dists), max(dists), sampleCount); + let ys = xs |> E.A.fmap(pdf(dists)); + XYShape.T.fromArrays(xs, ys); + }; + + let toString = (dists: t) => { + let distString = + dists + |> E.A.fmap(d => Mixed.toString(fst(d))) + |> Js.Array.joinWith(","); + {j|pointwideAdded($distString)|j}; + }; +}; + +type bigDist = [ + | `Dist(dist) + | `PointwiseCombination(PointwiseAddDistributionsWeighted.t) +]; + +let toString = (r: bigDist) => + r + |> ( + fun + | `Dist(d) => Mixed.toString(d) + | `PointwiseCombination(d) => + PointwiseAddDistributionsWeighted.toString(d) + ); \ No newline at end of file diff --git a/src/symbolic/MathJsParser.re b/src/symbolic/MathJsParser.re new file mode 100644 index 00000000..7270ba5b --- /dev/null +++ b/src/symbolic/MathJsParser.re @@ -0,0 +1,58 @@ +open Jstat; + +type arg = + | Value(float) + | Fn(fn) +and fn = { + name: string, + args: array(arg), +}; + +let rec parseMathjs = (j: Js.Json.t) => { + Json.Decode.( + switch (field("mathjs", string, j)) { + | "FunctionNode" => + let args = j |> field("args", array(parseMathjs)); + Some( + Fn({ + name: j |> field("fn", field("name", string)), + args: args |> E.A.O.concatSomes, + }), + ); + | "ConstantNode" => Some(Value(field("value", float, j))) + | _ => None + } + ); +}; + +let normal = (r): result(bigDist, string) => + r + |> ( + fun + | [|Value(mean), Value(stdev)|] => Ok(`Dist(`Normal({mean, stdev}))) + | _ => Error("Wrong number of variables in normal distribution") + ); + +let rec toValue = (r): result(bigDist, string) => + r + |> ( + fun + | Value(_) => Error("Top level can't be value") + | Fn({name: "normal", args}) => normal(args) + | Fn({name: "mm", args}) => { + let dists: array(dist) = + args + |> E.A.fmap(toValue) + |> E.A.fmap( + fun + | Ok(`Dist(`Normal({mean, stdev}))) => + Some(`Normal({mean, stdev})) + | _ => None, + ) + |> E.A.O.concatSomes; + + let inputs = dists |> E.A.fmap(r => (r, 1.0)); + Ok(`PointwiseCombination(inputs)); + } + | Fn({name}) => Error(name ++ ": name not found") + ); \ No newline at end of file diff --git a/src/symbolic/Mathjs.re b/src/symbolic/Mathjs.re new file mode 100644 index 00000000..87a7cb01 --- /dev/null +++ b/src/symbolic/Mathjs.re @@ -0,0 +1,2 @@ +[@bs.module "./MathjsWrapper.js"] +external parseMath: string => Js.Json.t = "parseMath"; \ No newline at end of file diff --git a/src/symbolic/MathjsWrapper.js b/src/symbolic/MathjsWrapper.js new file mode 100644 index 00000000..01fd4994 --- /dev/null +++ b/src/symbolic/MathjsWrapper.js @@ -0,0 +1,8 @@ + +const math = require("mathjs"); + +function parseMath(f){ return JSON.parse(JSON.stringify(math.parse(f))) }; + +module.exports = { + parseMath, +}; diff --git a/yarn.lock b/yarn.lock index 8f07e92a..3f52b481 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1017,15 +1017,6 @@ lodash "4.17.15" pdfast "0.2.0" -"@foretold/cdf@1.0.15": - version "1.0.15" - resolved "https://registry.yarnpkg.com/@foretold/cdf/-/cdf-1.0.15.tgz#69ce4755158693e3d325e7be10d0aa9cdb465730" - integrity sha512-I7GhFQd4HaFd+tGD1IJ0W8xvFp2YiJdcFiXSCq9vYQZWWy+Npi4QOYsMoDJyoUTvOlVba4ARa/pDKPD2hn+uuQ== - dependencies: - lodash "4.17.15" - parcel "1.12.3" - pdfast "0.2.0" - "@foretold/components@0.0.3": version "0.0.3" resolved "https://registry.yarnpkg.com/@foretold/components/-/components-0.0.3.tgz#a195912647499735f64cb2b74f722eee4b2da13f" @@ -1081,6 +1072,11 @@ dependencies: jest "^25.1.0" +"@glennsl/bs-json@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@glennsl/bs-json/-/bs-json-5.0.2.tgz#cfb85d94d370ec6dc17849e0ddb1a51eee08cfcc" + integrity sha512-vVlHJNrhmwvhyea14YiV4L5pDLjqw1edE3GzvMxlbPPQZVhzgO3sTWrUxCpQd2gV+CkMfk4FHBYunx9nWtBoDg== + "@iarna/toml@^2.2.0": version "2.2.3" resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.3.tgz#f060bf6eaafae4d56a7dac618980838b0696e2ab"