From 41eca03618c24a64466d77c10b0433f98253b1c5 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Thu, 2 Jul 2020 18:12:03 +0100 Subject: [PATCH] Set up new expressionTree directory --- __tests__/Distributions__Test.re | 4 +- src/components/DistBuilder3.re | 4 +- src/components/Drawer.re | 2 +- .../distribution/AlgebraicCombinations.re | 2 +- src/distPlus/distribution/Distributions.re | 16 +- src/distPlus/expressionTree/ExpressionTree.re | 22 + .../expressionTree/ExpressionTreeEvaluator.re | 294 +++++++++++++ .../expressionTree/ExpressionTypes.re | 24 ++ .../MathJsParser.re | 24 +- .../{symbolic => expressionTree}/Mathjs.re | 0 src/distPlus/expressionTree/Operation.re | 93 +++++ src/distPlus/renderers/RenderTypes.re | 2 +- src/distPlus/renderers/ShapeRenderer.re | 2 +- src/distPlus/symbolic/MathjsWrapper.js | 8 - src/distPlus/symbolic/SymbolicDist.re | 10 +- src/distPlus/symbolic/SymbolicTypes.re | 78 +--- src/distPlus/symbolic/TreeNode.re | 387 ------------------ 17 files changed, 467 insertions(+), 505 deletions(-) create mode 100644 src/distPlus/expressionTree/ExpressionTree.re create mode 100644 src/distPlus/expressionTree/ExpressionTreeEvaluator.re create mode 100644 src/distPlus/expressionTree/ExpressionTypes.re rename src/distPlus/{symbolic => expressionTree}/MathJsParser.re (91%) rename src/distPlus/{symbolic => expressionTree}/Mathjs.re (100%) create mode 100644 src/distPlus/expressionTree/Operation.re delete mode 100644 src/distPlus/symbolic/MathjsWrapper.js delete mode 100644 src/distPlus/symbolic/TreeNode.re diff --git a/__tests__/Distributions__Test.re b/__tests__/Distributions__Test.re index 4e16bb80..341ef8a4 100644 --- a/__tests__/Distributions__Test.re +++ b/__tests__/Distributions__Test.re @@ -383,9 +383,9 @@ describe("Shape", () => { let numSamples = 10000; open Distributions.Shape; let normal: SymbolicTypes.symbolicDist = `Normal({mean, stdev}); - let normalShape = TreeNode.toShape(numSamples, `Leaf(`SymbolicDist(normal))); + let normalShape = ExpressionTree.toShape(numSamples, `Leaf(`SymbolicDist(normal))); let lognormal = SymbolicDist.Lognormal.fromMeanAndStdev(mean, stdev); - let lognormalShape = TreeNode.toShape(numSamples, `Leaf(`SymbolicDist(lognormal))); + let lognormalShape = ExpressionTree.toShape(numSamples, `Leaf(`SymbolicDist(lognormal))); makeTestCloseEquality( "Mean of a normal", diff --git a/src/components/DistBuilder3.re b/src/components/DistBuilder3.re index 124aad0f..c0a5aac3 100644 --- a/src/components/DistBuilder3.re +++ b/src/components/DistBuilder3.re @@ -37,13 +37,13 @@ module DemoDist = { let parsed1 = MathJsParser.fromString(guesstimatorString); let shape = switch (parsed1) { - | Ok(r) => Some(TreeNode.toShape(10000, r)) + | Ok(r) => Some(ExpressionTree.toShape(10000, r)) | _ => None }; let str = switch (parsed1) { - | Ok(r) => TreeNode.toString(r) + | Ok(r) => ExpressionTree.toString(r) | Error(e) => e }; diff --git a/src/components/Drawer.re b/src/components/Drawer.re index f39cd3ad..8a0f2cfa 100644 --- a/src/components/Drawer.re +++ b/src/components/Drawer.re @@ -389,7 +389,7 @@ module Draw = { let numSamples = 3000; let normal: SymbolicTypes.symbolicDist = `Normal({mean, stdev}); - let normalShape = TreeNode.toShape(numSamples, `Leaf(`SymbolicDist(normal))); + let normalShape = ExpressionTree.toShape(numSamples, `Leaf(`SymbolicDist(normal))); let xyShape: Types.xyShape = switch (normalShape) { | Mixed(_) => {xs: [||], ys: [||]} diff --git a/src/distPlus/distribution/AlgebraicCombinations.re b/src/distPlus/distribution/AlgebraicCombinations.re index 17e1a1c0..538a5117 100644 --- a/src/distPlus/distribution/AlgebraicCombinations.re +++ b/src/distPlus/distribution/AlgebraicCombinations.re @@ -110,7 +110,7 @@ let toDiscretePointMassesFromTriangulars = }; let combineShapesContinuousContinuous = - (op: SymbolicTypes.algebraicOperation, s1: DistTypes.xyShape, s2: DistTypes.xyShape) + (op: ExpressionTypes.algebraicOperation, s1: DistTypes.xyShape, s2: DistTypes.xyShape) : DistTypes.xyShape => { let t1n = s1 |> XYShape.T.length; let t2n = s2 |> XYShape.T.length; diff --git a/src/distPlus/distribution/Distributions.re b/src/distPlus/distribution/Distributions.re index 2e4856b9..24053418 100644 --- a/src/distPlus/distribution/Distributions.re +++ b/src/distPlus/distribution/Distributions.re @@ -282,7 +282,7 @@ module Continuous = { let combineAlgebraicallyWithDiscrete = ( ~downsample=false, - op: SymbolicTypes.algebraicOperation, + op: ExpressionTypes.algebraicOperation, t1: t, t2: DistTypes.discreteShape, ) => { @@ -291,7 +291,7 @@ module Continuous = { let t1n = t1s |> XYShape.T.length; let t2n = t2s |> XYShape.T.length; - let fn = SymbolicTypes.Algebraic.toFn(op); + let fn = Operation.Algebraic.toFn(op); let outXYShapes: array(array((float, float))) = Belt.Array.makeUninitializedUnsafe(t2n); @@ -333,7 +333,7 @@ module Continuous = { }; let combineAlgebraically = - (~downsample=false, op: SymbolicTypes.algebraicOperation, t1: t, t2: t) => { + (~downsample=false, op: ExpressionTypes.algebraicOperation, t1: t, t2: t) => { let s1 = t1 |> getShape; let s2 = t2 |> getShape; let t1n = s1 |> XYShape.T.length; @@ -413,7 +413,7 @@ module Discrete = { /* This multiples all of the data points together and creates a new discrete distribution from the results. Data points at the same xs get added together. It may be a good idea to downsample t1 and t2 before and/or the result after. */ let combineAlgebraically = - (op: SymbolicTypes.algebraicOperation, t1: t, t2: t) => { + (op: ExpressionTypes.algebraicOperation, t1: t, t2: t) => { let t1s = t1 |> getShape; let t2s = t2 |> getShape; let t1n = t1s |> XYShape.T.length; @@ -426,7 +426,7 @@ module Discrete = { t2.knownIntegralSum, ); - let fn = SymbolicTypes.Algebraic.toFn(op); + let fn = Operation.Algebraic.toFn(op); let xToYMap = E.FloatFloatMap.empty(); for (i in 0 to t1n - 1) { @@ -840,7 +840,7 @@ module Mixed = { }); let combineAlgebraically = - (~downsample=false, op: SymbolicTypes.algebraicOperation, t1: t, t2: t) + (~downsample=false, op: ExpressionTypes.algebraicOperation, t1: t, t2: t) : t => { // Discrete convolution can cause a huge increase in the number of samples, // so we'll first downsample. @@ -914,7 +914,7 @@ module Shape = { )); let combineAlgebraically = - (op: SymbolicTypes.algebraicOperation, t1: t, t2: t): t => { + (op: ExpressionTypes.algebraicOperation, t1: t, t2: t): t => { switch (t1, t2) { | (Continuous(m1), Continuous(m2)) => DistTypes.Continuous( @@ -1096,7 +1096,7 @@ module Shape = { }; }); - let operate = (distToFloatOp: SymbolicTypes.distToFloatOperation, s) => + let operate = (distToFloatOp: ExpressionTypes.distToFloatOperation, s) => switch (distToFloatOp) { | `Pdf(f) => pdf(f, s) | `Inv(f) => inv(f, s) diff --git a/src/distPlus/expressionTree/ExpressionTree.re b/src/distPlus/expressionTree/ExpressionTree.re new file mode 100644 index 00000000..2ceb783b --- /dev/null +++ b/src/distPlus/expressionTree/ExpressionTree.re @@ -0,0 +1,22 @@ +open ExpressionTypes.ExpressionTree; + +let toShape = (sampleCount: int, node: node) => { + let renderResult = + ExpressionTreeEvaluator.toLeaf(`Operation(`Render(node)), sampleCount); + + switch (renderResult) { + | Ok(`Leaf(`RenderedDist(rs))) => + let continuous = Distributions.Shape.T.toContinuous(rs); + let discrete = Distributions.Shape.T.toDiscrete(rs); + let shape = MixedShapeBuilder.buildSimple(~continuous, ~discrete); + shape |> E.O.toExt("Could not build final shape."); + | Ok(_) => E.O.toExn("Rendering failed.", None) + | Error(message) => E.O.toExn("No shape found, error: " ++ message, None) + }; +}; + +let rec toString = + fun + | `Leaf(`SymbolicDist(d)) => SymbolicDist.T.toString(d) + | `Leaf(`RenderedDist(_)) => "[shape]" + | `Operation(op) => Operation.T.toString(toString, op); diff --git a/src/distPlus/expressionTree/ExpressionTreeEvaluator.re b/src/distPlus/expressionTree/ExpressionTreeEvaluator.re new file mode 100644 index 00000000..348f91ef --- /dev/null +++ b/src/distPlus/expressionTree/ExpressionTreeEvaluator.re @@ -0,0 +1,294 @@ +/* This module represents a tree node. */ +open ExpressionTypes; +open ExpressionTypes.ExpressionTree; + +type t = node; +type tResult = node => result(node, string); + +/* Given two random variables A and B, this returns the distribution + of a new variable that is the result of the operation on A and B. + For instance, normal(0, 1) + normal(1, 1) -> normal(1, 2). + In general, this is implemented via convolution. */ +module AlgebraicCombination = { + let toTreeNode = (op, t1, t2) => + `Operation(`AlgebraicCombination((op, t1, t2))); + let tryAnalyticalSolution = + fun + | `Operation( + `AlgebraicCombination( + operation, + `Leaf(`SymbolicDist(d1)), + `Leaf(`SymbolicDist(d2)), + ), + ) as t => + switch (SymbolicDist.T.attemptAnalyticalOperation(d1, d2, operation)) { + | `AnalyticalSolution(symbolicDist) => + Ok(`Leaf(`SymbolicDist(symbolicDist))) + | `Error(er) => Error(er) + | `NoSolution => Ok(t) + } + | t => Ok(t); + + // todo: I don't like the name evaluateNumerically that much, if this renders and does it algebraically. It's tricky. + let evaluateNumerically = (algebraicOp, operationToLeaf, t1, t2) => { + // force rendering into shapes + let renderShape = r => operationToLeaf(`Render(r)); + switch (renderShape(t1), renderShape(t2)) { + | (Ok(`Leaf(`RenderedDist(s1))), Ok(`Leaf(`RenderedDist(s2)))) => + Ok( + `Leaf( + `RenderedDist( + Distributions.Shape.combineAlgebraically(algebraicOp, s1, s2), + ), + ), + ) + | (Error(e1), _) => Error(e1) + | (_, Error(e2)) => Error(e2) + | _ => Error("Could not render shapes.") + }; + }; + + let toLeaf = + ( + operationToLeaf, + algebraicOp: ExpressionTypes.algebraicOperation, + t1: t, + t2: t, + ) + : result(node, string) => + toTreeNode(algebraicOp, t1, t2) + |> tryAnalyticalSolution + |> E.R.bind( + _, + fun + | `Leaf(d) => Ok(`Leaf(d)) // the analytical simplifaction worked, nice! + | `Operation(_) => + // if not, run the convolution + evaluateNumerically(algebraicOp, operationToLeaf, t1, t2), + ); +}; + +module VerticalScaling = { + let toLeaf = (operationToLeaf, scaleOp, t, scaleBy) => { + // scaleBy has to be a single float, otherwise we'll return an error. + let fn = Operation.Scale.toFn(scaleOp); + let knownIntegralSumFn = Operation.Scale.toKnownIntegralSumFn(scaleOp); + let renderedShape = operationToLeaf(`Render(t)); + + switch (renderedShape, scaleBy) { + | (Ok(`Leaf(`RenderedDist(rs))), `Leaf(`SymbolicDist(`Float(sm)))) => + Ok( + `Leaf( + `RenderedDist( + Distributions.Shape.T.mapY( + ~knownIntegralSumFn=knownIntegralSumFn(sm), + fn(sm), + rs, + ), + ), + ), + ) + | (Error(e1), _) => Error(e1) + | (_, _) => Error("Can only scale by float values.") + }; + }; +}; + +module PointwiseCombination = { + let pointwiseAdd = (operationToLeaf, t1, t2) => { + let renderedShape1 = operationToLeaf(`Render(t1)); + let renderedShape2 = operationToLeaf(`Render(t2)); + + switch (renderedShape1, renderedShape2) { + | (Ok(`Leaf(`RenderedDist(rs1))), Ok(`Leaf(`RenderedDist(rs2)))) => + Ok( + `Leaf( + `RenderedDist( + Distributions.Shape.combinePointwise( + ~knownIntegralSumsFn=(a, b) => Some(a +. b), + (+.), + rs1, + rs2, + ), + ), + ), + ) + | (Error(e1), _) => Error(e1) + | (_, Error(e2)) => Error(e2) + | _ => Error("Could not perform pointwise addition.") + }; + }; + + let pointwiseMultiply = (operationToLeaf, t1, t2) => { + // TODO: construct a function that we can easily sample from, to construct + // a RenderedDist. Use the xMin and xMax of the rendered shapes to tell the sampling function where to look. + Error( + "Pointwise multiplication not yet supported.", + ); + }; + + let toLeaf = (operationToLeaf, pointwiseOp, t1, t2) => { + switch (pointwiseOp) { + | `Add => pointwiseAdd(operationToLeaf, t1, t2) + | `Multiply => pointwiseMultiply(operationToLeaf, t1, t2) + }; + }; +}; + +module Truncate = { + module Simplify = { + let tryTruncatingNothing: tResult = + fun + | `Operation(`Truncate(None, None, `Leaf(d))) => Ok(`Leaf(d)) + | t => Ok(t); + + let tryTruncatingUniform: tResult = + fun + | `Operation(`Truncate(lc, rc, `Leaf(`SymbolicDist(`Uniform(u))))) => { + // just create a new Uniform distribution + let newLow = max(E.O.default(neg_infinity, lc), u.low); + let newHigh = min(E.O.default(infinity, rc), u.high); + Ok(`Leaf(`SymbolicDist(`Uniform({low: newLow, high: newHigh})))); + } + | t => Ok(t); + + let attempt = (leftCutoff, rightCutoff, t): result(node, string) => { + let originalTreeNode = + `Operation(`Truncate((leftCutoff, rightCutoff, t))); + + originalTreeNode + |> tryTruncatingNothing + |> E.R.bind(_, tryTruncatingUniform); + }; + }; + + let evaluateNumerically = (leftCutoff, rightCutoff, operationToLeaf, t) => { + // TODO: use named args in renderToShape; if we're lucky we can at least get the tail + // of a distribution we otherwise wouldn't get at all + let renderedShape = operationToLeaf(`Render(t)); + + switch (renderedShape) { + | Ok(`Leaf(`RenderedDist(rs))) => + let truncatedShape = + rs |> Distributions.Shape.T.truncate(leftCutoff, rightCutoff); + Ok(`Leaf(`RenderedDist(rs))); + | Error(e1) => Error(e1) + | _ => Error("Could not truncate distribution.") + }; + }; + + let toLeaf = + ( + operationToLeaf, + leftCutoff: option(float), + rightCutoff: option(float), + t: node, + ) + : result(node, string) => { + t + |> Simplify.attempt(leftCutoff, rightCutoff) + |> E.R.bind( + _, + fun + | `Leaf(d) => Ok(`Leaf(d)) // the analytical simplifaction worked, nice! + | `Operation(_) => + evaluateNumerically(leftCutoff, rightCutoff, operationToLeaf, t), + ); // if not, run the convolution + }; +}; + +module Normalize = { + let rec toLeaf = (operationToLeaf, t: node): result(node, string) => { + switch (t) { + | `Leaf(`RenderedDist(s)) => + Ok(`Leaf(`RenderedDist(Distributions.Shape.T.normalize(s)))) + | `Leaf(`SymbolicDist(_)) => Ok(t) + | `Operation(op) => + operationToLeaf(op) |> E.R.bind(_, toLeaf(operationToLeaf)) + }; + }; +}; + +module FloatFromDist = { + let symbolicToLeaf = (distToFloatOp: distToFloatOperation, s) => { + SymbolicDist.T.operate(distToFloatOp, s) + |> E.R.bind(_, v => Ok(`Leaf(`SymbolicDist(`Float(v))))); + }; + let renderedToLeaf = + (distToFloatOp: distToFloatOperation, rs: DistTypes.shape) + : result(node, string) => { + Distributions.Shape.operate(distToFloatOp, rs) + |> (v => Ok(`Leaf(`SymbolicDist(`Float(v))))); + }; + let rec toLeaf = + (operationToLeaf, distToFloatOp: distToFloatOperation, t: node) + : result(node, string) => { + switch (t) { + | `Leaf(`SymbolicDist(s)) => symbolicToLeaf(distToFloatOp, s) // we want to evaluate the distToFloatOp on the symbolic dist + | `Leaf(`RenderedDist(rs)) => renderedToLeaf(distToFloatOp, rs) + | `Operation(op) => + E.R.bind(operationToLeaf(op), toLeaf(operationToLeaf, distToFloatOp)) + }; + }; +}; + +module Render = { + let rec toLeaf = + ( + operationToLeaf: operation => result(t, string), + sampleCount: int, + t: node, + ) + : result(t, string) => { + switch (t) { + | `Leaf(`SymbolicDist(d)) => + Ok(`Leaf(`RenderedDist(SymbolicDist.T.toShape(sampleCount, d)))) + | `Leaf(`RenderedDist(_)) as t => Ok(t) // already a rendered shape, we're done here + | `Operation(op) => + E.R.bind(operationToLeaf(op), toLeaf(operationToLeaf, sampleCount)) + }; + }; +}; + +let rec operationToLeaf = + (sampleCount: int, op: operation): result(t, string) => { + // the functions that convert the Operation nodes to Leaf nodes need to + // have a way to call this function on their children, if their children are themselves Operation nodes. + switch (op) { + | `AlgebraicCombination(algebraicOp, t1, t2) => + AlgebraicCombination.toLeaf( + operationToLeaf(sampleCount), + algebraicOp, + t1, + t2 // we want to give it the option to render or simply leave it as is + ) + | `PointwiseCombination(pointwiseOp, t1, t2) => + PointwiseCombination.toLeaf( + operationToLeaf(sampleCount), + pointwiseOp, + t1, + t2, + ) + | `VerticalScaling(scaleOp, t, scaleBy) => + VerticalScaling.toLeaf(operationToLeaf(sampleCount), scaleOp, t, scaleBy) + | `Truncate(leftCutoff, rightCutoff, t) => + Truncate.toLeaf(operationToLeaf(sampleCount), leftCutoff, rightCutoff, t) + | `FloatFromDist(distToFloatOp, t) => + FloatFromDist.toLeaf(operationToLeaf(sampleCount), distToFloatOp, t) + | `Normalize(t) => Normalize.toLeaf(operationToLeaf(sampleCount), t) + | `Render(t) => Render.toLeaf(operationToLeaf(sampleCount), sampleCount, t) + }; +}; + +/* This function recursively goes through the nodes of the parse tree, + replacing each Operation node and its subtree with a Data node. + Whenever possible, the replacement produces a new Symbolic Data node, + but most often it will produce a RenderedDist. + This function is used mainly to turn a parse tree into a single RenderedDist + that can then be displayed to the user. */ +let toLeaf = (node: t, sampleCount: int): result(t, string) => { + switch (node) { + | `Leaf(d) => Ok(`Leaf(d)) + | `Operation(op) => operationToLeaf(sampleCount, op) + }; +}; diff --git a/src/distPlus/expressionTree/ExpressionTypes.re b/src/distPlus/expressionTree/ExpressionTypes.re new file mode 100644 index 00000000..730a228b --- /dev/null +++ b/src/distPlus/expressionTree/ExpressionTypes.re @@ -0,0 +1,24 @@ +type algebraicOperation = [ | `Add | `Multiply | `Subtract | `Divide]; +type pointwiseOperation = [ | `Add | `Multiply]; +type scaleOperation = [ | `Multiply | `Exponentiate | `Log]; +type distToFloatOperation = [ | `Pdf(float) | `Inv(float) | `Mean | `Sample]; + +type abstractOperation('a) = [ + | `AlgebraicCombination(algebraicOperation, 'a, 'a) + | `PointwiseCombination(pointwiseOperation, 'a, 'a) + | `VerticalScaling(scaleOperation, 'a, 'a) + | `Render('a) + | `Truncate(option(float), option(float), 'a) + | `Normalize('a) + | `FloatFromDist(distToFloatOperation, 'a) +]; + +module ExpressionTree = { + type leaf = [ + | `SymbolicDist(SymbolicTypes.symbolicDist) + | `RenderedDist(DistTypes.shape) + ]; + + type node = [ | `Leaf(leaf) | `Operation(operation)] + and operation = abstractOperation(node); +}; diff --git a/src/distPlus/symbolic/MathJsParser.re b/src/distPlus/expressionTree/MathJsParser.re similarity index 91% rename from src/distPlus/symbolic/MathJsParser.re rename to src/distPlus/expressionTree/MathJsParser.re index 3874fda4..92227736 100644 --- a/src/distPlus/symbolic/MathJsParser.re +++ b/src/distPlus/expressionTree/MathJsParser.re @@ -86,13 +86,13 @@ module MathAdtToDistDst = { ); }; - let normal: array(arg) => result(TreeNode.treeNode, string) = + let normal: array(arg) => result(ExpressionTypes.ExpressionTree.node, string) = fun | [|Value(mean), Value(stdev)|] => Ok(`Leaf(`SymbolicDist(`Normal({mean, stdev})))) | _ => Error("Wrong number of variables in normal distribution"); - let lognormal: array(arg) => result(TreeNode.treeNode, string) = + let lognormal: array(arg) => result(ExpressionTypes.ExpressionTree.node, string) = fun | [|Value(mu), Value(sigma)|] => Ok(`Leaf(`SymbolicDist(`Lognormal({mu, sigma})))) @@ -114,7 +114,7 @@ module MathAdtToDistDst = { } | _ => Error("Wrong number of variables in lognormal distribution"); - let to_: array(arg) => result(TreeNode.treeNode, string) = + let to_: array(arg) => result(ExpressionTypes.ExpressionTree.node, string) = fun | [|Value(low), Value(high)|] when low <= 0.0 && low < high => { Ok( @@ -134,31 +134,31 @@ module MathAdtToDistDst = { Error("Low value must be less than high value.") | _ => Error("Wrong number of variables in lognormal distribution"); - let uniform: array(arg) => result(TreeNode.treeNode, string) = + let uniform: array(arg) => result(ExpressionTypes.ExpressionTree.node, string) = fun | [|Value(low), Value(high)|] => Ok(`Leaf(`SymbolicDist(`Uniform({low, high})))) | _ => Error("Wrong number of variables in lognormal distribution"); - let beta: array(arg) => result(TreeNode.treeNode, string) = + let beta: array(arg) => result(ExpressionTypes.ExpressionTree.node, string) = fun | [|Value(alpha), Value(beta)|] => Ok(`Leaf(`SymbolicDist(`Beta({alpha, beta})))) | _ => Error("Wrong number of variables in lognormal distribution"); - let exponential: array(arg) => result(TreeNode.treeNode, string) = + let exponential: array(arg) => result(ExpressionTypes.ExpressionTree.node, string) = fun | [|Value(rate)|] => Ok(`Leaf(`SymbolicDist(`Exponential({rate: rate})))) | _ => Error("Wrong number of variables in Exponential distribution"); - let cauchy: array(arg) => result(TreeNode.treeNode, string) = + let cauchy: array(arg) => result(ExpressionTypes.ExpressionTree.node, string) = fun | [|Value(local), Value(scale)|] => Ok(`Leaf(`SymbolicDist(`Cauchy({local, scale})))) | _ => Error("Wrong number of variables in cauchy distribution"); - let triangular: array(arg) => result(TreeNode.treeNode, string) = + let triangular: array(arg) => result(ExpressionTypes.ExpressionTree.node, string) = fun | [|Value(low), Value(medium), Value(high)|] => Ok(`Leaf(`SymbolicDist(`Triangular({low, medium, high})))) @@ -166,7 +166,7 @@ module MathAdtToDistDst = { let multiModal = ( - args: array(result(TreeNode.treeNode, string)), + args: array(result(ExpressionTypes.ExpressionTree.node, string)), weights: option(array(float)), ) => { let weights = weights |> E.O.default([||]); @@ -215,7 +215,7 @@ module MathAdtToDistDst = { }; }; - let arrayParser = (args: array(arg)): result(TreeNode.treeNode, string) => { + let arrayParser = (args: array(arg)): result(ExpressionTypes.ExpressionTree.node, string) => { let samples = args |> E.A.fmap( @@ -241,7 +241,7 @@ module MathAdtToDistDst = { }; let operationParser = - (name: string, args: array(result(TreeNode.treeNode, string))) => { + (name: string, args: array(result(ExpressionTypes.ExpressionTree.node, string))) => { let toOkAlgebraic = r => Ok(`Operation(`AlgebraicCombination(r))); let toOkTrunctate = r => Ok(`Operation(`Truncate(r))); switch (name, args) { @@ -347,7 +347,7 @@ module MathAdtToDistDst = { | Symbol(_) => Error("Symbol not valid as top level") | Object(_) => Error("Object not valid as top level"); - let run = (r): result(TreeNode.treeNode, string) => + let run = (r): result(ExpressionTypes.ExpressionTree.node, string) => r |> MathAdtCleaner.run |> topLevel; }; diff --git a/src/distPlus/symbolic/Mathjs.re b/src/distPlus/expressionTree/Mathjs.re similarity index 100% rename from src/distPlus/symbolic/Mathjs.re rename to src/distPlus/expressionTree/Mathjs.re diff --git a/src/distPlus/expressionTree/Operation.re b/src/distPlus/expressionTree/Operation.re new file mode 100644 index 00000000..112ca17e --- /dev/null +++ b/src/distPlus/expressionTree/Operation.re @@ -0,0 +1,93 @@ +open ExpressionTypes; + +module Algebraic = { + type t = algebraicOperation; + let toFn: (t, float, float) => float = + fun + | `Add => (+.) + | `Subtract => (-.) + | `Multiply => ( *. ) + | `Divide => (/.); + + let applyFn = (t, f1, f2) => { + switch (t, f1, f2) { + | (`Divide, _, 0.) => Error("Cannot divide $v1 by zero.") + | _ => Ok(toFn(t, f1, f2)) + }; + }; + + let toString = + fun + | `Add => "+" + | `Subtract => "-" + | `Multiply => "*" + | `Divide => "/"; + + let format = (a, b, c) => b ++ " " ++ toString(a) ++ " " ++ c; +}; + +module Pointwise = { + type t = pointwiseOperation; + let toString = + fun + | `Add => "+" + | `Multiply => "*"; + + let format = (a, b, c) => b ++ " " ++ toString(a) ++ " " ++ c; +}; + +module DistToFloat = { + type t = distToFloatOperation; + + let format = (operation, value) => + switch (operation) { + | `Pdf(f) => {j|pdf(x=$f,$value)|j} + | `Inv(f) => {j|inv(x=$f,$value)|j} + | `Sample => "sample($value)" + | `Mean => "mean($value)" + }; +}; + +module Scale = { + type t = scaleOperation; + let toFn = + fun + | `Multiply => ( *. ) + | `Exponentiate => ( ** ) + | `Log => ((a, b) => log(a) /. log(b)); + + let format = (operation: t, value, scaleBy) => + switch (operation) { + | `Multiply => {j|scaleMultiply($value, $scaleBy) |j} + | `Exponentiate => {j|ScaleExponentiate($value, $scaleBy) |j} + | `Log => {j|ScaleLog($value, $scaleBy) |j} + }; + + let toKnownIntegralSumFn = + fun + | `Multiply => ((a, b) => Some(a *. b)) + | `Exponentiate => ((_, _) => None) + | `Log => ((_, _) => None); +}; + +module T = { + let truncateToString = + (left: option(float), right: option(float), nodeToString) => { + let left = left |> E.O.dimap(Js.Float.toString, () => "-inf"); + let right = right |> E.O.dimap(Js.Float.toString, () => "inf"); + {j|truncate($nodeToString, $left, $right)|j}; + }; + let toString = nodeToString => + fun + | `AlgebraicCombination(op, t1, t2) => + Algebraic.format(op, nodeToString(t1), nodeToString(t2)) + | `PointwiseCombination(op, t1, t2) => + Pointwise.format(op, nodeToString(t1), nodeToString(t2)) + | `VerticalScaling(scaleOp, t, scaleBy) => + Scale.format(scaleOp, nodeToString(t), nodeToString(scaleBy)) + | `Normalize(t) => "normalize(" ++ nodeToString(t) ++ ")" + | `FloatFromDist(floatFromDistOp, t) => + DistToFloat.format(floatFromDistOp, nodeToString(t)) + | `Truncate(lc, rc, t) => truncateToString(lc, rc, nodeToString(t)) + | `Render(t) => nodeToString(t); +}; diff --git a/src/distPlus/renderers/RenderTypes.re b/src/distPlus/renderers/RenderTypes.re index e091ecad..9b37503f 100644 --- a/src/distPlus/renderers/RenderTypes.re +++ b/src/distPlus/renderers/RenderTypes.re @@ -43,7 +43,7 @@ module ShapeRenderer = { module Symbolic = { type inputs = {length: int}; type outputs = { - graph: TreeNode.treeNode, + graph: ExpressionTypes.ExpressionTree.node, shape: DistTypes.shape, }; let make = (graph, shape) => {graph, shape}; diff --git a/src/distPlus/renderers/ShapeRenderer.re b/src/distPlus/renderers/ShapeRenderer.re index 8542ba4a..b439240b 100644 --- a/src/distPlus/renderers/ShapeRenderer.re +++ b/src/distPlus/renderers/ShapeRenderer.re @@ -21,7 +21,7 @@ let runSymbolic = (guesstimatorString, length) => { |> E.R.fmap(g => RenderTypes.ShapeRenderer.Symbolic.make( g, - TreeNode.toShape(length, g), + ExpressionTree.toShape(length, g), ) ); }; diff --git a/src/distPlus/symbolic/MathjsWrapper.js b/src/distPlus/symbolic/MathjsWrapper.js deleted file mode 100644 index 01fd4994..00000000 --- a/src/distPlus/symbolic/MathjsWrapper.js +++ /dev/null @@ -1,8 +0,0 @@ - -const math = require("mathjs"); - -function parseMath(f){ return JSON.parse(JSON.stringify(math.parse(f))) }; - -module.exports = { - parseMath, -}; diff --git a/src/distPlus/symbolic/SymbolicDist.re b/src/distPlus/symbolic/SymbolicDist.re index 18142e74..94e513d6 100644 --- a/src/distPlus/symbolic/SymbolicDist.re +++ b/src/distPlus/symbolic/SymbolicDist.re @@ -76,7 +76,7 @@ module Normal = { `Normal({mean, stdev}); }; - let operate = (operation: SymbolicTypes.Algebraic.t, n1: t, n2: t) => + let operate = (operation: Operation.Algebraic.t, n1: t, n2: t) => switch (operation) { | `Add => Some(add(n1, n2)) | `Subtract => Some(subtract(n1, n2)) @@ -130,7 +130,7 @@ module Lognormal = { let sigma = l1.sigma +. l2.sigma; `Lognormal({mu, sigma}); }; - let operate = (operation: SymbolicTypes.Algebraic.t, n1: t, n2: t) => + let operate = (operation: Operation.Algebraic.t, n1: t, n2: t) => switch (operation) { | `Multiply => Some(multiply(n1, n2)) | `Divide => Some(divide(n1, n2)) @@ -246,7 +246,7 @@ module T = { | `Uniform(n) => Uniform.mean(n) | `Float(n) => Float.mean(n); - let operate = (distToFloatOp: distToFloatOperation, s) => + let operate = (distToFloatOp: ExpressionTypes.distToFloatOperation, s) => switch (distToFloatOp) { | `Pdf(f) => Ok(pdf(f, s)) | `Inv(f) => Ok(inv(f, s)) @@ -283,12 +283,12 @@ module T = { ( d1: symbolicDist, d2: symbolicDist, - op: SymbolicTypes.algebraicOperation, + op: ExpressionTypes.algebraicOperation, ) : analyticalSolutionAttempt => switch (d1, d2) { | (`Float(v1), `Float(v2)) => - switch (SymbolicTypes.Algebraic.applyFn(op, v1, v2)) { + switch (Operation.Algebraic.applyFn(op, v1, v2)) { | Ok(r) => `AnalyticalSolution(`Float(r)) | Error(n) => `Error(n) } diff --git a/src/distPlus/symbolic/SymbolicTypes.re b/src/distPlus/symbolic/SymbolicTypes.re index 2dbb95db..b372a00f 100644 --- a/src/distPlus/symbolic/SymbolicTypes.re +++ b/src/distPlus/symbolic/SymbolicTypes.re @@ -46,80 +46,4 @@ type symbolicDist = [ | `Triangular(triangular) | `ContinuousShape(continuousShape) | `Float(float) // Dirac delta at x. Practically useful only in the context of multimodals. -]; - -// todo: These operations are really applicable for all dists -type algebraicOperation = [ | `Add | `Multiply | `Subtract | `Divide]; -type pointwiseOperation = [ | `Add | `Multiply]; -type scaleOperation = [ | `Multiply | `Exponentiate | `Log]; -type distToFloatOperation = [ | `Pdf(float) | `Inv(float) | `Mean | `Sample]; - -module Algebraic = { - type t = algebraicOperation; - let toFn: (t, float, float) => float = - fun - | `Add => (+.) - | `Subtract => (-.) - | `Multiply => ( *. ) - | `Divide => (/.); - - let applyFn = (t, f1, f2) => { - switch (t, f1, f2) { - | (`Divide, _, 0.) => Error("Cannot divide $v1 by zero.") - | _ => Ok(toFn(t, f1, f2)) - }; - }; - - let toString = - fun - | `Add => "+" - | `Subtract => "-" - | `Multiply => "*" - | `Divide => "/"; - - let format = (a, b, c) => b ++ " " ++ toString(a) ++ " " ++ c; -}; - -module Pointwise = { - type t = pointwiseOperation; - let toString = - fun - | `Add => "+" - | `Multiply => "*"; - - let format = (a, b, c) => b ++ " " ++ toString(a) ++ " " ++ c; -}; - -module DistToFloat = { - type t = distToFloatOperation; - - let format = (operation, value) => - switch (operation) { - | `Pdf(f) => {j|pdf(x=$f,$value)|j} - | `Inv(f) => {j|inv(x=$f,$value)|j} - | `Sample => "sample($value)" - | `Mean => "mean($value)" - }; -}; - -module Scale = { - type t = scaleOperation; - let toFn = - fun - | `Multiply => ( *. ) - | `Exponentiate => ( ** ) - | `Log => ((a, b) => log(a) /. log(b)); - - let format = (operation:t, value, scaleBy) => - switch (operation) { - | `Multiply => {j|scaleMultiply($value, $scaleBy) |j} - | `Exponentiate => {j|ScaleExponentiate($value, $scaleBy) |j} - | `Log => {j|ScaleLog($value, $scaleBy) |j} - }; - - let toKnownIntegralSumFn = - fun - | `Multiply => ((a, b) => Some(a *. b)) - | `Exponentiate => ((_, _) => None) - | `Log => ((_, _) => None); -}; +]; \ No newline at end of file diff --git a/src/distPlus/symbolic/TreeNode.re b/src/distPlus/symbolic/TreeNode.re deleted file mode 100644 index b6f83801..00000000 --- a/src/distPlus/symbolic/TreeNode.re +++ /dev/null @@ -1,387 +0,0 @@ -/* This module represents a tree node. */ -open SymbolicTypes; - -type leaf = [ - | `SymbolicDist(SymbolicTypes.symbolicDist) - | `RenderedDist(DistTypes.shape) -]; - -/* TreeNodes are either Data (i.e. symbolic or rendered distributions) or Operations. Operations always refer to two child nodes.*/ -type treeNode = [ | `Leaf(leaf) | `Operation(operation)] -and operation = [ - | `AlgebraicCombination(algebraicOperation, treeNode, treeNode) - | `PointwiseCombination(pointwiseOperation, treeNode, treeNode) - | `VerticalScaling(scaleOperation, treeNode, treeNode) - | `Render(treeNode) - | `Truncate(option(float), option(float), treeNode) - | `Normalize(treeNode) - | `FloatFromDist(distToFloatOperation, treeNode) -]; - -module Operation = { - type t = operation; - let truncateToString = - (left: option(float), right: option(float), nodeToString) => { - let left = left |> E.O.dimap(Js.Float.toString, () => "-inf"); - let right = right |> E.O.dimap(Js.Float.toString, () => "inf"); - {j|truncate($nodeToString, $left, $right)|j}; - }; - - let toString = nodeToString => - fun - | `AlgebraicCombination(op, t1, t2) => - SymbolicTypes.Algebraic.format(op, nodeToString(t1), nodeToString(t2)) - | `PointwiseCombination(op, t1, t2) => - SymbolicTypes.Pointwise.format(op, nodeToString(t1), nodeToString(t2)) - | `VerticalScaling(scaleOp, t, scaleBy) => - SymbolicTypes.Scale.format( - scaleOp, - nodeToString(t), - nodeToString(scaleBy), - ) - | `Normalize(t) => "normalize(" ++ nodeToString(t) ++ ")" - | `FloatFromDist(floatFromDistOp, t) => - SymbolicTypes.DistToFloat.format(floatFromDistOp, nodeToString(t)) - | `Truncate(lc, rc, t) => truncateToString(lc, rc, nodeToString(t)) - | `Render(t) => nodeToString(t); -}; - -module TreeNode = { - type t = treeNode; - type tResult = treeNode => result(treeNode, string); - - let rec toString = - fun - | `Leaf(`SymbolicDist(d)) => SymbolicDist.T.toString(d) - | `Leaf(`RenderedDist(_)) => "[shape]" - | `Operation(op) => Operation.toString(toString, op); - - /* The following modules encapsulate everything we can do with - * different kinds of operations. */ - - /* Given two random variables A and B, this returns the distribution - of a new variable that is the result of the operation on A and B. - For instance, normal(0, 1) + normal(1, 1) -> normal(1, 2). - In general, this is implemented via convolution. */ - module AlgebraicCombination = { - let toTreeNode = (op, t1, t2) => - `Operation(`AlgebraicCombination((op, t1, t2))); - let tryAnalyticalSolution = - fun - | `Operation( - `AlgebraicCombination( - operation, - `Leaf(`SymbolicDist(d1)), - `Leaf(`SymbolicDist(d2)), - ), - ) as t => - switch (SymbolicDist.T.attemptAnalyticalOperation(d1, d2, operation)) { - | `AnalyticalSolution(symbolicDist) => - Ok(`Leaf(`SymbolicDist(symbolicDist))) - | `Error(er) => Error(er) - | `NoSolution => Ok(t) - } - | t => Ok(t); - - // todo: I don't like the name evaluateNumerically that much, if this renders and does it algebraically. It's tricky. - let evaluateNumerically = (algebraicOp, operationToLeaf, t1, t2) => { - // force rendering into shapes - let renderShape = r => operationToLeaf(`Render(r)); - switch (renderShape(t1), renderShape(t2)) { - | (Ok(`Leaf(`RenderedDist(s1))), Ok(`Leaf(`RenderedDist(s2)))) => - Ok( - `Leaf( - `RenderedDist( - Distributions.Shape.combineAlgebraically(algebraicOp, s1, s2), - ), - ), - ) - | (Error(e1), _) => Error(e1) - | (_, Error(e2)) => Error(e2) - | _ => Error("Could not render shapes.") - }; - }; - - let toLeaf = - ( - operationToLeaf, - algebraicOp: SymbolicTypes.algebraicOperation, - t1: t, - t2: t, - ) - : result(treeNode, string) => - toTreeNode(algebraicOp, t1, t2) - |> tryAnalyticalSolution - |> E.R.bind( - _, - fun - | `Leaf(d) => Ok(`Leaf(d)) // the analytical simplifaction worked, nice! - | `Operation(_) => - // if not, run the convolution - evaluateNumerically(algebraicOp, operationToLeaf, t1, t2), - ); - }; - - module VerticalScaling = { - let toLeaf = (operationToLeaf, scaleOp, t, scaleBy) => { - // scaleBy has to be a single float, otherwise we'll return an error. - let fn = SymbolicTypes.Scale.toFn(scaleOp); - let knownIntegralSumFn = - SymbolicTypes.Scale.toKnownIntegralSumFn(scaleOp); - let renderedShape = operationToLeaf(`Render(t)); - - switch (renderedShape, scaleBy) { - | (Ok(`Leaf(`RenderedDist(rs))), `Leaf(`SymbolicDist(`Float(sm)))) => - Ok( - `Leaf( - `RenderedDist( - Distributions.Shape.T.mapY( - ~knownIntegralSumFn=knownIntegralSumFn(sm), - fn(sm), - rs, - ), - ), - ), - ) - | (Error(e1), _) => Error(e1) - | (_, _) => Error("Can only scale by float values.") - }; - }; - }; - - module PointwiseCombination = { - let pointwiseAdd = (operationToLeaf, t1, t2) => { - let renderedShape1 = operationToLeaf(`Render(t1)); - let renderedShape2 = operationToLeaf(`Render(t2)); - - switch (renderedShape1, renderedShape2) { - | (Ok(`Leaf(`RenderedDist(rs1))), Ok(`Leaf(`RenderedDist(rs2)))) => - Ok( - `Leaf( - `RenderedDist( - Distributions.Shape.combinePointwise( - ~knownIntegralSumsFn=(a, b) => Some(a +. b), - (+.), - rs1, - rs2, - ), - ), - ), - ) - | (Error(e1), _) => Error(e1) - | (_, Error(e2)) => Error(e2) - | _ => Error("Could not perform pointwise addition.") - }; - }; - - let pointwiseMultiply = (operationToLeaf, t1, t2) => { - // TODO: construct a function that we can easily sample from, to construct - // a RenderedDist. Use the xMin and xMax of the rendered shapes to tell the sampling function where to look. - Error( - "Pointwise multiplication not yet supported.", - ); - }; - - let toLeaf = (operationToLeaf, pointwiseOp, t1, t2) => { - switch (pointwiseOp) { - | `Add => pointwiseAdd(operationToLeaf, t1, t2) - | `Multiply => pointwiseMultiply(operationToLeaf, t1, t2) - }; - }; - }; - - module Truncate = { - module Simplify = { - let tryTruncatingNothing: tResult = - fun - | `Operation(`Truncate(None, None, `Leaf(d))) => Ok(`Leaf(d)) - | t => Ok(t); - - let tryTruncatingUniform: tResult = - fun - | `Operation(`Truncate(lc, rc, `Leaf(`SymbolicDist(`Uniform(u))))) => { - // just create a new Uniform distribution - let newLow = max(E.O.default(neg_infinity, lc), u.low); - let newHigh = min(E.O.default(infinity, rc), u.high); - Ok( - `Leaf(`SymbolicDist(`Uniform({low: newLow, high: newHigh}))), - ); - } - | t => Ok(t); - - let attempt = (leftCutoff, rightCutoff, t): result(treeNode, string) => { - let originalTreeNode = - `Operation(`Truncate((leftCutoff, rightCutoff, t))); - - originalTreeNode - |> tryTruncatingNothing - |> E.R.bind(_, tryTruncatingUniform); - }; - }; - - let evaluateNumerically = (leftCutoff, rightCutoff, operationToLeaf, t) => { - // TODO: use named args in renderToShape; if we're lucky we can at least get the tail - // of a distribution we otherwise wouldn't get at all - let renderedShape = operationToLeaf(`Render(t)); - - switch (renderedShape) { - | Ok(`Leaf(`RenderedDist(rs))) => - let truncatedShape = - rs |> Distributions.Shape.T.truncate(leftCutoff, rightCutoff); - Ok(`Leaf(`RenderedDist(rs))); - | Error(e1) => Error(e1) - | _ => Error("Could not truncate distribution.") - }; - }; - - let toLeaf = - ( - operationToLeaf, - leftCutoff: option(float), - rightCutoff: option(float), - t: treeNode, - ) - : result(treeNode, string) => { - t - |> Simplify.attempt(leftCutoff, rightCutoff) - |> E.R.bind( - _, - fun - | `Leaf(d) => Ok(`Leaf(d)) // the analytical simplifaction worked, nice! - | `Operation(_) => - evaluateNumerically(leftCutoff, rightCutoff, operationToLeaf, t), - ); // if not, run the convolution - }; - }; - - module Normalize = { - let rec toLeaf = (operationToLeaf, t: treeNode): result(treeNode, string) => { - switch (t) { - | `Leaf(`RenderedDist(s)) => - Ok(`Leaf(`RenderedDist(Distributions.Shape.T.normalize(s)))) - | `Leaf(`SymbolicDist(_)) => Ok(t) - | `Operation(op) => - operationToLeaf(op) |> E.R.bind(_, toLeaf(operationToLeaf)) - }; - }; - }; - - module FloatFromDist = { - let symbolicToLeaf = (distToFloatOp: distToFloatOperation, s) => { - SymbolicDist.T.operate(distToFloatOp, s) - |> E.R.bind(_, v => Ok(`Leaf(`SymbolicDist(`Float(v))))); - }; - let renderedToLeaf = - (distToFloatOp: distToFloatOperation, rs: DistTypes.shape) - : result(treeNode, string) => { - Distributions.Shape.operate(distToFloatOp, rs) - |> (v => Ok(`Leaf(`SymbolicDist(`Float(v))))); - }; - let rec toLeaf = - ( - operationToLeaf, - distToFloatOp: distToFloatOperation, - t: treeNode, - ) - : result(treeNode, string) => { - switch (t) { - | `Leaf(`SymbolicDist(s)) => symbolicToLeaf(distToFloatOp, s) // we want to evaluate the distToFloatOp on the symbolic dist - | `Leaf(`RenderedDist(rs)) => renderedToLeaf(distToFloatOp, rs) - | `Operation(op) => - E.R.bind( - operationToLeaf(op), - toLeaf(operationToLeaf, distToFloatOp), - ) - }; - }; - }; - - module Render = { - let rec toLeaf = - ( - operationToLeaf: operation => result(t, string), - sampleCount: int, - t: treeNode, - ) - : result(t, string) => { - switch (t) { - | `Leaf(`SymbolicDist(d)) => - Ok(`Leaf(`RenderedDist(SymbolicDist.T.toShape(sampleCount, d)))) - | `Leaf(`RenderedDist(_)) as t => Ok(t) // already a rendered shape, we're done here - | `Operation(op) => - E.R.bind(operationToLeaf(op), toLeaf(operationToLeaf, sampleCount)) - }; - }; - }; - - let rec operationToLeaf = - (sampleCount: int, op: operation): result(t, string) => { - // the functions that convert the Operation nodes to Leaf nodes need to - // have a way to call this function on their children, if their children are themselves Operation nodes. - switch (op) { - | `AlgebraicCombination(algebraicOp, t1, t2) => - AlgebraicCombination.toLeaf( - operationToLeaf(sampleCount), - algebraicOp, - t1, - t2 // we want to give it the option to render or simply leave it as is - ) - | `PointwiseCombination(pointwiseOp, t1, t2) => - PointwiseCombination.toLeaf( - operationToLeaf(sampleCount), - pointwiseOp, - t1, - t2, - ) - | `VerticalScaling(scaleOp, t, scaleBy) => - VerticalScaling.toLeaf( - operationToLeaf(sampleCount), - scaleOp, - t, - scaleBy, - ) - | `Truncate(leftCutoff, rightCutoff, t) => - Truncate.toLeaf( - operationToLeaf(sampleCount), - leftCutoff, - rightCutoff, - t, - ) - | `FloatFromDist(distToFloatOp, t) => - FloatFromDist.toLeaf(operationToLeaf(sampleCount), distToFloatOp, t) - | `Normalize(t) => Normalize.toLeaf(operationToLeaf(sampleCount), t) - | `Render(t) => - Render.toLeaf(operationToLeaf(sampleCount), sampleCount, t) - }; - }; - - /* This function recursively goes through the nodes of the parse tree, - replacing each Operation node and its subtree with a Data node. - Whenever possible, the replacement produces a new Symbolic Data node, - but most often it will produce a RenderedDist. - This function is used mainly to turn a parse tree into a single RenderedDist - that can then be displayed to the user. */ - let toLeaf = (treeNode: t, sampleCount: int): result(t, string) => { - switch (treeNode) { - | `Leaf(d) => Ok(`Leaf(d)) - | `Operation(op) => operationToLeaf(sampleCount, op) - }; - }; -}; - -let toShape = (sampleCount: int, treeNode: treeNode) => { - let renderResult = - TreeNode.toLeaf(`Operation(`Render(treeNode)), sampleCount); - - switch (renderResult) { - | Ok(`Leaf(`RenderedDist(rs))) => - let continuous = Distributions.Shape.T.toContinuous(rs); - let discrete = Distributions.Shape.T.toDiscrete(rs); - let shape = MixedShapeBuilder.buildSimple(~continuous, ~discrete); - shape |> E.O.toExt("Could not build final shape."); - | Ok(_) => E.O.toExn("Rendering failed.", None) - | Error(message) => E.O.toExn("No shape found, error: " ++ message, None) - }; -}; - -let toString = (treeNode: treeNode) => TreeNode.toString(treeNode);