diff --git a/__tests__/Parser__test.re b/__tests__/Parser__test.re index 79ee6ac2..935ca668 100644 --- a/__tests__/Parser__test.re +++ b/__tests__/Parser__test.re @@ -1,21 +1,12 @@ 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); + let parsed1 = MathJsParser.fromString("mm(normal(0,1), normal(10,1))"); + Js.log(parsed1 |> E.R.fmap(Jstat.toString)); + Js.log(parsed1 |> E.R.fmap(Jstat.toShape(20))); expect(1.0) |> toEqual(1.0); }) }) diff --git a/src/App.re b/src/App.re index e2526c25..236879d6 100644 --- a/src/App.re +++ b/src/App.re @@ -2,6 +2,7 @@ type route = | Model(string) | DistBuilder | DistBuilder2 + | DistBuilder3 | Home | NotFound; @@ -10,6 +11,7 @@ let routeToPath = route => | Model(modelId) => "/m/" ++ modelId | DistBuilder => "/dist-builder" | DistBuilder2 => "/dist-builder2" + | DistBuilder3 => "/dist-builder3" | Home => "/" | _ => "/" }; @@ -81,6 +83,9 @@ module Menu = { {"Dist Builder 2" |> E.ste} + + {"Dist Builder 3" |> E.ste} + ; }; }; @@ -94,6 +99,7 @@ let make = () => { | ["m", modelId] => Model(modelId) | ["dist-builder"] => DistBuilder | ["dist-builder2"] => DistBuilder2 + | ["dist-builder3"] => DistBuilder3 | [] => Home | _ => NotFound }; @@ -108,8 +114,9 @@ let make = () => { } | DistBuilder => | DistBuilder2 => + | DistBuilder3 => | Home =>
{"Welcome" |> E.ste}
| _ =>
{"Page is not found" |> E.ste}
}} ; -}; +}; \ No newline at end of file diff --git a/src/components/DistBuilder3.re b/src/components/DistBuilder3.re new file mode 100644 index 00000000..a3f36c1e --- /dev/null +++ b/src/components/DistBuilder3.re @@ -0,0 +1,114 @@ +open BsReform; +open Antd.Grid; + +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 dist = style([padding(em(1.))]); + let spacer = style([marginTop(em(1.))]); +}; + +module DemoDist = { + [@react.component] + let make = (~guesstimatorString: string) => { + let parsed1 = MathJsParser.fromString(guesstimatorString); + let shape = + switch (parsed1) { + | Ok(r) => Some(Jstat.toShape(10000, r)) + | _ => None + }; + + let str = + switch (parsed1) { + | Ok(r) => Jstat.toString(r) + | Error(e) => e + }; + + let inside = + shape + |> E.O.fmap(shape => { + let distPlus = + Distributions.DistPlus.make( + ~shape=Continuous(Distributions.Continuous.fromShape(shape)), + ~domain=Complete, + ~unit=UnspecifiedDistribution, + ~guesstimatorString=None, + (), + ) + |> Distributions.DistPlus.T.scaleToIntegralSum(~intendedSum=1.0); + ; + }) + |> E.O.default(ReasonReact.null); + E.ste}> +
+ inside + {str |> ReasonReact.string} + ; + }; +}; + +[@react.component] +let make = () => { + let reform = + Form.use( + ~validationStrategy=OnDemand, + ~schema, + ~onSubmit=({state}) => {None}, + ~initialState={guesstimatorString: "lognormal(6.1, 3)"}, + (), + ); + + let demoDist = + React.useMemo1( + () => { + + }, + [|reform.state.values.guesstimatorString|], + ); + +
+
+ demoDist +
+ E.ste}> + + + + + + + + + + +
+
; +}; \ No newline at end of file diff --git a/src/components/charts/DistPlusPlot.re b/src/components/charts/DistPlusPlot.re index bf041b89..32886ddb 100644 --- a/src/components/charts/DistPlusPlot.re +++ b/src/components/charts/DistPlusPlot.re @@ -216,20 +216,31 @@ module DistPlusChart = { |> E.O.fmap(Distributions.Continuous.getShape); let range = T.xTotalRange(distPlus); - // We subtract a bit from the range to make sure that it fits. Maybe this should be done in d3 instead. - let minX = - switch ( - distPlus |> Distributions.DistPlus.T.Integral.yToX(~cache=None, 0.01), - range, - ) { - | (min, Some(range)) => Some(min -. range *. 0.001) - | _ => None - }; + // // We subtract a bit from the range to make sure that it fits. Maybe this should be done in d3 instead. + // let minX = + // switch ( + // distPlus + // |> Distributions.DistPlus.T.Integral.yToX(~cache=None, 0.0001), + // range, + // ) { + // | (min, Some(range)) => Some(min -. range *. 0.001) + // | _ => None + // }; + + let minX = { + distPlus |> Distributions.DistPlus.T.Integral.yToX(~cache=None, 0.00001); + }; let maxX = { distPlus |> Distributions.DistPlus.T.Integral.yToX(~cache=None, 0.99); }; + Js.log3( + distPlus |> Distributions.DistPlus.T.Integral.yToX(~cache=None, 0.0001), + minX, + distPlus |> Distributions.DistPlus.T.Integral.yToX(~cache=None, 0.98), + ); + let timeScale = distPlus.unit |> DistTypes.DistributionUnit.toJson; let toDiscreteProbabilityMass = distPlus |> Distributions.DistPlus.T.toDiscreteProbabilityMass; diff --git a/src/symbolic/Jstat.re b/src/symbolic/Jstat.re index 654c2bfc..3a415ae5 100644 --- a/src/symbolic/Jstat.re +++ b/src/symbolic/Jstat.re @@ -102,15 +102,15 @@ module Mixed = { let min = dist => switch (dist) { - | `Normal(n) => Normal.inv(0.01, n) - | `Lognormal(n) => Lognormal.inv(0.01, n) + | `Normal(n) => Normal.inv(0.0001, n) + | `Lognormal(n) => Lognormal.inv(0.0001, n) | `Uniform({low}) => low }; let max = dist => switch (dist) { - | `Normal(n) => Normal.inv(0.99, n) - | `Lognormal(n) => Lognormal.inv(0.99, n) + | `Normal(n) => Normal.inv(0.9999, n) + | `Lognormal(n) => Lognormal.inv(0.9999, n) | `Uniform({high}) => high }; @@ -121,7 +121,7 @@ module Mixed = { switch (xSelection) { | `Linear => Functions.range(min(dist), max(dist), sampleCount) | `ByWeight => - Functions.range(0.001, 0.999, sampleCount) + Functions.range(0.00001, 0.99999, sampleCount) |> E.A.fmap(x => inv(x, dist)) }; let ys = xs |> E.A.fmap(r => pdf(r, dist)); @@ -151,7 +151,7 @@ module PointwiseAddDistributionsWeighted = { 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; + dists |> E.A.fmap(d => d |> fst |> Mixed.max) |> Functions.max; let toShape = (dists: t, sampleCount: int) => { let xs = Functions.range(min(dists), max(dists), sampleCount); @@ -180,4 +180,10 @@ let toString = (r: bigDist) => | `Dist(d) => Mixed.toString(d) | `PointwiseCombination(d) => PointwiseAddDistributionsWeighted.toString(d) - ); \ No newline at end of file + ); + +let toShape = n => + fun + | `Dist(d) => Mixed.toShape(~xSelection=`ByWeight, d, n) + | `PointwiseCombination(d) => + PointwiseAddDistributionsWeighted.toShape(d, n); \ No newline at end of file diff --git a/src/symbolic/MathJsParser.re b/src/symbolic/MathJsParser.re index 7270ba5b..2081db79 100644 --- a/src/symbolic/MathJsParser.re +++ b/src/symbolic/MathJsParser.re @@ -1,14 +1,16 @@ open Jstat; type arg = + | Symbol(string) | Value(float) | Fn(fn) + | Array(array(arg)) and fn = { name: string, args: array(arg), }; -let rec parseMathjs = (j: Js.Json.t) => { +let rec parseMathjs = (j: Js.Json.t) => Json.Decode.( switch (field("mathjs", string, j)) { | "FunctionNode" => @@ -19,19 +21,56 @@ let rec parseMathjs = (j: Js.Json.t) => { args: args |> E.A.O.concatSomes, }), ); - | "ConstantNode" => Some(Value(field("value", float, j))) - | _ => None + | "OperatorNode" => + let args = j |> field("args", array(parseMathjs)); + Some( + Fn({ + name: j |> field("fn", string), + args: args |> E.A.O.concatSomes, + }), + ); + | "ConstantNode" => Some(Value(field("value", Json.Decode.float, j))) + | "ArrayNode" => + let items = field("items", array(parseMathjs), j); + Some(Array(items |> E.A.O.concatSomes)); + | "SymbolNode" => Some(Symbol(field("name", string, j))) + | n => + Js.log2("Couldn't parse mathjs node", 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 logHigh = math.log(high); +// let logLow = math.log(low); + +// let mean = (math.mean(logHigh, logLow)).toFixed(3); +// let stdev = ((logHigh-logLow) / (2*1.645)).toFixed(3); + +let normal: array(arg) => result(bigDist, string) = + fun + | [|Value(mean), Value(stdev)|] => Ok(`Dist(`Normal({mean, stdev}))) + | _ => Error("Wrong number of variables in normal distribution"); + +let lognormal: array(arg) => result(bigDist, string) = + fun + | [|Value(mu), Value(sigma)|] => Ok(`Dist(`Lognormal({mu, sigma}))) + | _ => Error("Wrong number of variables in lognormal distribution"); + +let to_: array(arg) => result(bigDist, string) = + fun + | [|Value(low), Value(high)|] => { + let logLow = Js.Math.log(low); + let logHigh = Js.Math.log(high); + let mu = Functions.mean([|logLow, logHigh|]); + let sigma = (logHigh -. logLow) /. (2.0 *. 1.645); + Ok(`Dist(`Lognormal({mu, sigma}))); + } + | _ => Error("Wrong number of variables in lognormal distribution"); + +let uniform: array(arg) => result(bigDist, string) = + fun + | [|Value(low), Value(high)|] => Ok(`Dist(`Uniform({low, high}))) + | _ => Error("Wrong number of variables in lognormal distribution"); let rec toValue = (r): result(bigDist, string) => r @@ -39,14 +78,16 @@ let rec toValue = (r): result(bigDist, string) => fun | Value(_) => Error("Top level can't be value") | Fn({name: "normal", args}) => normal(args) + | Fn({name: "lognormal", args}) => lognormal(args) + | Fn({name: "uniform", args}) => uniform(args) + | Fn({name: "to", args}) => to_(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})) + | Ok(`Dist(n)) => Some(n) | _ => None, ) |> E.A.O.concatSomes; @@ -55,4 +96,15 @@ let rec toValue = (r): result(bigDist, string) => Ok(`PointwiseCombination(inputs)); } | Fn({name}) => Error(name ++ ": name not found") - ); \ No newline at end of file + | Array(_) => Error("Array not valid as top level") + | Symbol(_) => Error("Symbol not valid as top level") + ); + +let fromString = str => + Mathjs.parseMath(str) + |> E.R.bind(_, r => + switch (parseMathjs(r)) { + | Some(r) => toValue(r) + | None => Error("Second parse failed") + } + ); \ No newline at end of file diff --git a/src/symbolic/Mathjs.re b/src/symbolic/Mathjs.re index 87a7cb01..1b0c7bc7 100644 --- a/src/symbolic/Mathjs.re +++ b/src/symbolic/Mathjs.re @@ -1,2 +1,10 @@ [@bs.module "./MathjsWrapper.js"] -external parseMath: string => Js.Json.t = "parseMath"; \ No newline at end of file +external parseMathExt: string => Js.Json.t = "parseMath"; + +let parseMath = (str: string): result(Js.Json.t, string) => + switch (parseMathExt(str)) { + | exception (Js.Exn.Error(err)) => + Error(Js.Exn.message(err) |> E.O.default("MathJS Parse Error")) + | exception _ => Error("MathJS Parse Error") + | j => Ok(j) + }; \ No newline at end of file