From dc1ec1bb8651a93bfe23c2f1d8b245792bb4790a Mon Sep 17 00:00:00 2001 From: Sebastian Kosch Date: Fri, 26 Jun 2020 21:29:21 -0700 Subject: [PATCH] It compiles! --- __tests__/Distributions__Test.re | 87 ++-- src/components/DistBuilder.re | 30 +- src/components/DistBuilder3.re | 4 +- src/distPlus/distribution/Distributions.re | 547 +++++++++++++-------- src/distPlus/renderers/DistPlusRenderer.re | 16 +- src/distPlus/renderers/RenderTypes.re | 8 +- src/distPlus/symbolic/MathJsParser.re | 38 +- src/distPlus/symbolic/TreeNode.re | 485 +++++++++++------- src/interface/FormBuilder.re | 4 +- 9 files changed, 751 insertions(+), 468 deletions(-) diff --git a/__tests__/Distributions__Test.re b/__tests__/Distributions__Test.re index 20c7ce34..c02430fe 100644 --- a/__tests__/Distributions__Test.re +++ b/__tests__/Distributions__Test.re @@ -24,7 +24,7 @@ let makeTestCloseEquality = (~only=false, str, item1, item2, ~digits) => describe("Shape", () => { describe("Continuous", () => { open Distributions.Continuous; - let continuous = make(`Linear, shape); + let continuous = make(`Linear, shape, None); makeTest("minX", T.minX(continuous), 1.0); makeTest("maxX", T.maxX(continuous), 8.0); makeTest( @@ -57,7 +57,7 @@ describe("Shape", () => { ); }); describe("when Stepwise", () => { - let continuous = make(`Stepwise, shape); + let continuous = make(`Stepwise, shape, None); makeTest( "at 4.0", T.xToY(4., continuous), @@ -89,7 +89,7 @@ describe("Shape", () => { "toLinear", { let continuous = - make(`Stepwise, {xs: [|1., 4., 8.|], ys: [|0.1, 5., 1.0|]}); + make(`Stepwise, {xs: [|1., 4., 8.|], ys: [|0.1, 5., 1.0|]}, None); continuous |> toLinear |> E.O.fmap(getShape); }, Some({ @@ -100,7 +100,7 @@ describe("Shape", () => { makeTest( "toLinear", { - let continuous = make(`Stepwise, {xs: [|0.0|], ys: [|0.3|]}); + let continuous = make(`Stepwise, {xs: [|0.0|], ys: [|0.3|]}, None); continuous |> toLinear |> E.O.fmap(getShape); }, Some({xs: [|0.0|], ys: [|0.3|]}), @@ -123,7 +123,7 @@ describe("Shape", () => { makeTest( "integralEndY", continuous - |> T.scaleToIntegralSum(~intendedSum=1.0) + |> T.normalize //scaleToIntegralSum(~intendedSum=1.0) |> T.Integral.sum(~cache=None), 1.0, ); @@ -135,12 +135,12 @@ describe("Shape", () => { xs: [|1., 4., 8.|], ys: [|0.3, 0.5, 0.2|], }; - let discrete = shape; + let discrete = make(shape, None); makeTest("minX", T.minX(discrete), 1.0); makeTest("maxX", T.maxX(discrete), 8.0); makeTest( "mapY", - T.mapY(r => r *. 2.0, discrete) |> (r => r.ys), + T.mapY(r => r *. 2.0, discrete) |> (r => getShape(r).ys), [|0.6, 1.0, 0.4|], ); makeTest( @@ -160,19 +160,22 @@ describe("Shape", () => { ); makeTest( "scaleBy", - T.scaleBy(~scale=4.0, discrete), - {xs: [|1., 4., 8.|], ys: [|1.2, 2.0, 0.8|]}, + scaleBy(~scale=4.0, discrete), + make({xs: [|1., 4., 8.|], ys: [|1.2, 2.0, 0.8|]}, None), ); makeTest( - "scaleToIntegralSum", - T.scaleToIntegralSum(~intendedSum=4.0, discrete), - {xs: [|1., 4., 8.|], ys: [|1.2, 2.0, 0.8|]}, + "normalize, then scale by 4.0", + discrete + |> T.normalize + |> scaleBy(~scale=4.0), + make({xs: [|1., 4., 8.|], ys: [|1.2, 2.0, 0.8|]}, None), ); makeTest( "scaleToIntegralSum: back and forth", discrete - |> T.scaleToIntegralSum(~intendedSum=4.0) - |> T.scaleToIntegralSum(~intendedSum=1.0), + |> T.normalize + |> scaleBy(~scale=4.0) + |> T.normalize, discrete, ); makeTest( @@ -181,12 +184,13 @@ describe("Shape", () => { Distributions.Continuous.make( `Stepwise, {xs: [|1., 4., 8.|], ys: [|0.3, 0.8, 1.0|]}, + None ), ); makeTest( "integral with 1 element", - T.Integral.get(~cache=None, {xs: [|0.0|], ys: [|1.0|]}), - Distributions.Continuous.make(`Stepwise, {xs: [|0.0|], ys: [|1.0|]}), + T.Integral.get(~cache=None, Distributions.Discrete.make({xs: [|0.0|], ys: [|1.0|]}, None)), + Distributions.Continuous.make(`Stepwise, {xs: [|0.0|], ys: [|1.0|]}, None), ); makeTest( "integralXToY", @@ -205,27 +209,22 @@ describe("Shape", () => { describe("Mixed", () => { open Distributions.Mixed; - let discrete: DistTypes.xyShape = { + let discreteShape: DistTypes.xyShape = { xs: [|1., 4., 8.|], ys: [|0.3, 0.5, 0.2|], }; + let discrete = Distributions.Discrete.make(discreteShape, None); let continuous = Distributions.Continuous.make( `Linear, {xs: [|3., 7., 14.|], ys: [|0.058, 0.082, 0.124|]}, + None ) - |> Distributions.Continuous.T.scaleToIntegralSum(~intendedSum=1.0); - let mixed = - MixedShapeBuilder.build( + |> Distributions.Continuous.T.normalize; //scaleToIntegralSum(~intendedSum=1.0); + let mixed = Distributions.Mixed.make( ~continuous, ~discrete, - ~assumptions={ - continuous: ADDS_TO_CORRECT_PROBABILITY, - discrete: ADDS_TO_CORRECT_PROBABILITY, - discreteProbabilityMass: Some(0.5), - }, - ) - |> E.O.toExn(""); + ); makeTest("minX", T.minX(mixed), 1.0); makeTest("maxX", T.maxX(mixed), 14.0); makeTest( @@ -243,9 +242,9 @@ describe("Shape", () => { 0.24775224775224775, |], }, + None ), - ~discrete={xs: [|1., 4., 8.|], ys: [|0.6, 1.0, 0.4|]}, - ~discreteProbabilityMassFraction=0.5, + ~discrete=Distributions.Discrete.make({xs: [|1., 4., 8.|], ys: [|0.6, 1.0, 0.4|]}, None) ), ); makeTest( @@ -266,7 +265,7 @@ describe("Shape", () => { makeTest("integralEndY", T.Integral.sum(~cache=None, mixed), 1.0); makeTest( "scaleBy", - T.scaleBy(~scale=2.0, mixed), + Distributions.Mixed.scaleBy(~scale=2.0, mixed), Distributions.Mixed.make( ~continuous= Distributions.Continuous.make( @@ -279,9 +278,9 @@ describe("Shape", () => { 0.24775224775224775, |], }, + None ), - ~discrete={xs: [|1., 4., 8.|], ys: [|0.6, 1.0, 0.4|]}, - ~discreteProbabilityMassFraction=0.5, + ~discrete=Distributions.Discrete.make({xs: [|1., 4., 8.|], ys: [|0.6, 1.0, 0.4|]}, None), ), ); makeTest( @@ -302,34 +301,31 @@ describe("Shape", () => { 0.6913122927072927, 1.0, |], - }, + }, + None, ), ); }); describe("Distplus", () => { open Distributions.DistPlus; - let discrete: DistTypes.xyShape = { + let discreteShape: DistTypes.xyShape = { xs: [|1., 4., 8.|], ys: [|0.3, 0.5, 0.2|], }; + let discrete = Distributions.Discrete.make(discreteShape, None); let continuous = Distributions.Continuous.make( `Linear, {xs: [|3., 7., 14.|], ys: [|0.058, 0.082, 0.124|]}, + None ) - |> Distributions.Continuous.T.scaleToIntegralSum(~intendedSum=1.0); + |> Distributions.Continuous.T.normalize; //scaleToIntegralSum(~intendedSum=1.0); let mixed = - MixedShapeBuilder.build( + Distributions.Mixed.make( ~continuous, ~discrete, - ~assumptions={ - continuous: ADDS_TO_CORRECT_PROBABILITY, - discrete: ADDS_TO_CORRECT_PROBABILITY, - discreteProbabilityMass: Some(0.5), - }, - ) - |> E.O.toExn(""); + ); let distPlus = Distributions.DistPlus.make( ~shape=Mixed(mixed), @@ -374,6 +370,7 @@ describe("Shape", () => { 1.0, |], }, + None, ), ), ); @@ -386,9 +383,9 @@ describe("Shape", () => { let numSamples = 10000; open Distributions.Shape; let normal: SymbolicDist.dist = `Normal({mean, stdev}); - let normalShape = TreeNode.toShape(numSamples, normal); + let normalShape = TreeNode.toShape(numSamples, `DistData(`Symbolic(normal))); let lognormal = SymbolicDist.Lognormal.fromMeanAndStdev(mean, stdev); - let lognormalShape = TreeNode.toShape(numSamples, lognormal); + let lognormalShape = TreeNode.toShape(numSamples, `DistData(`Symbolic(lognormal))); makeTestCloseEquality( "Mean of a normal", diff --git a/src/components/DistBuilder.re b/src/components/DistBuilder.re index 7f57f9c1..93856fc9 100644 --- a/src/components/DistBuilder.re +++ b/src/components/DistBuilder.re @@ -17,7 +17,7 @@ module FormConfig = [%lenses // sampleCount: string, outputXYPoints: string, - truncateTo: string, + downsampleTo: string, kernelWidth: string, } ]; @@ -25,7 +25,7 @@ module FormConfig = [%lenses type options = { sampleCount: int, outputXYPoints: int, - truncateTo: option(int), + downsampleTo: option(int), kernelWidth: option(float), }; @@ -115,7 +115,7 @@ type inputs = { samplingInputs: RenderTypes.ShapeRenderer.Sampling.inputs, guesstimatorString: string, length: int, - shouldTruncateSampledDistribution: int, + shouldDownsampleSampledDistribution: int, }; module DemoDist = { @@ -141,8 +141,8 @@ module DemoDist = { kernelWidth: options.kernelWidth, }, ~distPlusIngredients, - ~shouldTruncate=options.truncateTo |> E.O.isSome, - ~recommendedLength=options.truncateTo |> E.O.default(10000), + ~shouldDownsample=options.downsampleTo |> E.O.isSome, + ~recommendedLength=options.downsampleTo |> E.O.default(10000), (), ); let response = DistPlusRenderer.run(inputs); @@ -182,7 +182,7 @@ let make = () => { unit: "days", sampleCount: "30000", outputXYPoints: "10000", - truncateTo: "1000", + downsampleTo: "1000", kernelWidth: "5", }, (), @@ -210,7 +210,7 @@ let make = () => { let sampleCount = reform.state.values.sampleCount |> Js.Float.fromString; let outputXYPoints = reform.state.values.outputXYPoints |> Js.Float.fromString; - let truncateTo = reform.state.values.truncateTo |> Js.Float.fromString; + let downsampleTo = reform.state.values.downsampleTo |> Js.Float.fromString; let kernelWidth = reform.state.values.kernelWidth |> Js.Float.fromString; let domain = @@ -252,20 +252,20 @@ let make = () => { }; let options = - switch (sampleCount, outputXYPoints, truncateTo) { + switch (sampleCount, outputXYPoints, downsampleTo) { | (_, _, _) when !Js.Float.isNaN(sampleCount) && !Js.Float.isNaN(outputXYPoints) - && !Js.Float.isNaN(truncateTo) + && !Js.Float.isNaN(downsampleTo) && sampleCount > 10. && outputXYPoints > 10. => Some({ sampleCount: sampleCount |> int_of_float, outputXYPoints: outputXYPoints |> int_of_float, - truncateTo: - int_of_float(truncateTo) > 0 - ? Some(int_of_float(truncateTo)) : None, + downsampleTo: + int_of_float(downsampleTo) > 0 + ? Some(int_of_float(downsampleTo)) : None, kernelWidth: kernelWidth == 0.0 ? None : Some(kernelWidth), }) | _ => None @@ -287,7 +287,7 @@ let make = () => { reform.state.values.unit, reform.state.values.sampleCount, reform.state.values.outputXYPoints, - reform.state.values.truncateTo, + reform.state.values.downsampleTo, reform.state.values.kernelWidth, reloader |> string_of_int, |], @@ -481,7 +481,7 @@ let make = () => { /> - + @@ -496,4 +496,4 @@ let make = () => {
; -}; \ No newline at end of file +}; diff --git a/src/components/DistBuilder3.re b/src/components/DistBuilder3.re index 86bb1d2a..124aad0f 100644 --- a/src/components/DistBuilder3.re +++ b/src/components/DistBuilder3.re @@ -43,7 +43,7 @@ module DemoDist = { let str = switch (parsed1) { - | Ok(r) => SymbolicDist.toString(r) + | Ok(r) => TreeNode.toString(r) | Error(e) => e }; @@ -58,7 +58,7 @@ module DemoDist = { ~guesstimatorString=None, (), ) - |> Distributions.DistPlus.T.scaleToIntegralSum(~intendedSum=1.0); + |> Distributions.DistPlus.T.normalize; ; }) |> E.O.default(ReasonReact.null); diff --git a/src/distPlus/distribution/Distributions.re b/src/distPlus/distribution/Distributions.re index f5807cd8..9497957d 100644 --- a/src/distPlus/distribution/Distributions.re +++ b/src/distPlus/distribution/Distributions.re @@ -3,7 +3,8 @@ module type dist = { type integral; let minX: t => float; let maxX: t => float; - let mapY: (~knownIntegralSumFn: float => option(float)=?, float => float, t) => t; + let mapY: + (~knownIntegralSumFn: float => option(float)=?, float => float, t) => t; let xToY: (float, t) => DistTypes.mixedPoint; let toShape: t => DistTypes.shape; let toContinuous: t => option(DistTypes.continuousShape); @@ -13,6 +14,7 @@ module type dist = { let normalizedToDiscrete: t => option(DistTypes.discreteShape); let toDiscreteProbabilityMassFraction: t => float; let downsample: (~cache: option(integral)=?, int, t) => t; + let truncate: (option(float), option(float), t) => t; let integral: (~cache: option(integral), t) => integral; let integralEndY: (~cache: option(integral), t) => float; @@ -38,6 +40,7 @@ module Dist = (T: dist) => { let toContinuous = T.toContinuous; let toDiscrete = T.toDiscrete; let normalize = T.normalize; + let truncate = T.truncate; let normalizedToContinuous = T.normalizedToContinuous; let normalizedToDiscrete = T.normalizedToDiscrete; let mean = T.mean; @@ -52,7 +55,22 @@ module Dist = (T: dist) => { }; }; -module Continuous { +module Common = { + let combineIntegralSums = + ( + combineFn: (float, float) => option(float), + t1KnownIntegralSum: option(float), + t2KnownIntegralSum: option(float), + ) => { + switch (t1KnownIntegralSum, t2KnownIntegralSum) { + | (None, _) + | (_, None) => None + | (Some(s1), Some(s2)) => combineFn(s1, s2) + }; + }; +}; + +module Continuous = { type t = DistTypes.continuousShape; let getShape = (t: t) => t.xyShape; let interpolation = (t: t) => t.interpolation; @@ -78,17 +96,21 @@ module Continuous { knownIntegralSum: Some(0.0), }; let combine = - (fn, t1: DistTypes.continuousShape, t2: DistTypes.continuousShape) + ( + ~knownIntegralSumsFn, + fn, + t1: DistTypes.continuousShape, + t2: DistTypes.continuousShape, + ) : DistTypes.continuousShape => { - // If we're adding the distributions, and we know the total of each, then we // can just sum them up. Otherwise, all bets are off. let combinedIntegralSum = - switch (fn, t1.knownIntegralSum, t2.knownIntegralSum) { - | (_, None, _) - | (_, _, None) => None - | ((+.), Some(s1), Some(s2)) => Some(s1 +. s2) - }; + Common.combineIntegralSums( + knownIntegralSumsFn, + t1.knownIntegralSum, + t2.knownIntegralSum, + ); make( `Linear, @@ -102,7 +124,6 @@ module Continuous { combinedIntegralSum, ); }; - let reduce = (fn, items) => items |> E.A.fold_left(combine(fn), empty); let toLinear = (t: t): option(t) => { switch (t) { @@ -114,7 +135,19 @@ module Continuous { }; }; let shapeFn = (fn, t: t) => t |> getShape |> fn; - let updateKnownIntegralSum = (knownIntegralSum, t: t): t => ({...t, knownIntegralSum}); + let updateKnownIntegralSum = (knownIntegralSum, t: t): t => { + ...t, + knownIntegralSum, + }; + + let reduce = + ( + ~knownIntegralSumsFn: (float, float) => option(float)=(_, _) => None, + fn, + continuousShapes, + ) => + continuousShapes + |> E.A.fold_left(combine(~knownIntegralSumsFn, fn), empty); // Contracts every point in the continuous xyShape into a single dirac-Delta-like point, // using the centerpoints between adjacent xs and the area under each trapezoid. @@ -128,11 +161,18 @@ module Continuous { Belt.Array.set( pointMassesY, x, - (xs[x + 1] -. xs[x]) *. ((ys[x] +. ys[x + 1]) /. 2.)); // = dx * (1/2) * (avgY) + (xs[x + 1] -. xs[x]) *. ((ys[x] +. ys[x + 1]) /. 2.), + ); // = dx * (1/2) * (avgY) (); }; - {xyShape: {xs: xs, ys: pointMassesY}, knownIntegralSum: t.knownIntegralSum}; + { + xyShape: { + xs, + ys: pointMassesY, + }, + knownIntegralSum: t.knownIntegralSum, + }; }; /* Performs a discrete convolution between two continuous distributions A and B. @@ -153,18 +193,25 @@ module Continuous { let t1n = t1s |> XYShape.T.length; let t2n = t2s |> XYShape.T.length; - let outXYShapes: array(array((float, float))) = Belt.Array.makeUninitializedUnsafe(t1n); + let outXYShapes: array(array((float, float))) = + Belt.Array.makeUninitializedUnsafe(t1n); for (i in 0 to t1n - 1) { // create a new distribution - let dxyShape: array((float, float)) = Belt.Array.makeUninitializedUnsafe(t2n); + let dxyShape: array((float, float)) = + Belt.Array.makeUninitializedUnsafe(t2n); for (j in 0 to t2n - 1) { - let _ = Belt.Array.set(dxyShape, j, (fn(t1s.xs[i], t2s.xs[j]), t1s.ys[i] *. t2s.ys[j])); + let _ = + Belt.Array.set( + dxyShape, + j, + (fn(t1s.xs[i], t2s.xs[j]), t1s.ys[i] *. t2s.ys[j]), + ); (); - } + }; let _ = Belt.Array.set(outXYShapes, i, dxyShape); (); - } + }; let combinedIntegralSum = switch (t1.knownIntegralSum, t2.knownIntegralSum) { @@ -175,9 +222,9 @@ module Continuous { outXYShapes |> E.A.fmap(s => { - let xyShape = XYShape.T.fromZippedArray(s); - make(`Linear, xyShape, None); - }) + let xyShape = XYShape.T.fromZippedArray(s); + make(`Linear, xyShape, None); + }) |> reduce((+.)) |> updateKnownIntegralSum(combinedIntegralSum); }; @@ -185,35 +232,22 @@ module Continuous { let convolve = (fn, t1: t, t2: t) => convolveWithDiscrete(fn, t1, toDiscretePointMasses(t2)); - let mapY = (~knownIntegralSumFn=(previousKnownIntegralSum => None), fn, t: t) => { + let mapY = (~knownIntegralSumFn=previousKnownIntegralSum => None, fn, t: t) => { let u = E.O.bind(_, knownIntegralSumFn); let yMapFn = shapeMap(XYShape.T.mapY(fn)); t |> yMapFn |> updateKnownIntegralSum(u(t.knownIntegralSum)); }; - let scaleBy = (~scale=1.0, ~knownIntegralSum=None, t: t): t => - t |> mapY((r: float) => r *. scale) |> updateKnownIntegralSum(knownIntegralSum); - - let truncate = (leftCutoff: option(float), rightCutoff: option(float), t: t) => { - let truncatedZippedPairs = - t - |> getShape - |> XYShape.T.zip - |> XYShape.Zipped.filterByX(x => x >= E.O.default(neg_infinity, leftCutoff) || x <= E.O.default(infinity, rightCutoff)); - - let eps = (t |> getShape |> XYShape.T.xTotalRange) *. 0.0001; - - let leftNewPoint = leftCutoff |> E.O.dimap(lc => [| (lc -. eps, 0.) |], _ => [||]); - let rightNewPoint = rightCutoff |> E.O.dimap(rc => [| (rc +. eps, 0.) |], _ => [||]); - - let truncatedZippedPairsWithNewPoints = - E.A.concatMany([| leftNewPoint, truncatedZippedPairs, rightNewPoint |]); - let truncatedShape = XYShape.T.fromZippedArray(truncatedZippedPairsWithNewPoints); - - make(`Linear, truncatedShape, None); + let scaleBy = (~scale=1.0, t: t): t => { + t + |> mapY((r: float) => r *. scale) + |> updateKnownIntegralSum( + E.O.bind(t.knownIntegralSum, v => Some(scale *. v)), + ); }; + module T = Dist({ type t = DistTypes.continuousShape; @@ -236,12 +270,31 @@ module Continuous { |> DistTypes.MixedPoint.makeContinuous; }; - // let combineWithFn = (t1: t, t2: t, fn: (float, float) => float) => { - // switch(t1, t2){ - // | ({interpolation: `Stepwise}, {interpolation: `Stepwise}) => 3.0 - // | ({interpolation: `Linear}, {interpolation: `Linear}) => 3.0 - // } - // }; + let truncate = + (leftCutoff: option(float), rightCutoff: option(float), t: t) => { + let truncatedZippedPairs = + t + |> getShape + |> XYShape.T.zip + |> XYShape.Zipped.filterByX(x => + x >= E.O.default(neg_infinity, leftCutoff) + || x <= E.O.default(infinity, rightCutoff) + ); + + let eps = (t |> getShape |> XYShape.T.xTotalRange) *. 0.0001; + + let leftNewPoint = + leftCutoff |> E.O.dimap(lc => [|(lc -. eps, 0.)|], _ => [||]); + let rightNewPoint = + rightCutoff |> E.O.dimap(rc => [|(rc +. eps, 0.)|], _ => [||]); + + let truncatedZippedPairsWithNewPoints = + E.A.concatMany([|leftNewPoint, truncatedZippedPairs, rightNewPoint|]); + let truncatedShape = + XYShape.T.fromZippedArray(truncatedZippedPairsWithNewPoints); + + make(`Linear, truncatedShape, None); + }; // TODO: This should work with stepwise plots. let integral = (~cache, t) => @@ -272,9 +325,9 @@ module Continuous { let toDiscrete = _ => None; let normalize = (t: t): t => { - let continuousIntegralSum = integralEndY(~cache=None, t); - - scaleBy(~scale=(1. /. continuousIntegralSum), ~knownIntegralSum=Some(1.0), t); + t + |> scaleBy(~scale=1. /. integralEndY(~cache=None, t)) + |> updateKnownIntegralSum(Some(1.0)); }; let normalizedToContinuous = t => Some(t); // TODO: this should be normalized @@ -316,40 +369,41 @@ module Discrete = { let lastY = (t: t) => t |> getShape |> XYShape.T.lastY; - let combineIntegralSums = (combineFn: ((float, float) => option(float)), t1KnownIntegralSum: option(float), t2KnownIntegralSum: option(float)) => { - switch (t1KnownIntegralSum, t2KnownIntegralSum) { - | (None, _) - | (_, None) => None - | (Some(s1), Some(s2)) => combineFn(s1, s2) - }; - }; - - let combine = (combineIntegralSumsFn, fn, t1: DistTypes.discreteShape, t2: DistTypes.discreteShape) + let combine = + ( + ~knownIntegralSumsFn, + fn, + t1: DistTypes.discreteShape, + t2: DistTypes.discreteShape, + ) : DistTypes.discreteShape => { - - let combinedIntegralSum = combineIntegralSums(combineIntegralSumsFn, t1.knownIntegralSum, t2.knownIntegralSum); + let combinedIntegralSum = + Common.combineIntegralSums( + knownIntegralSumsFn, + t1.knownIntegralSum, + t2.knownIntegralSum, + ); make( XYShape.Combine.combine( ~xsSelection=ALL_XS, ~xToYSelection=XYShape.XtoY.stepwiseIfAtX, - ~fn, // stepwiseIfAtX returns option(float), so this fn needs to handle None, which is what the _default0 wrapper is for + ~fn=((a, b) => fn(E.O.default(0.0, a), E.O.default(0.0, b))), // stepwiseIfAtX returns option(float), so this fn needs to handle None t1.xyShape, t2.xyShape, ), combinedIntegralSum, ); }; - let _default0 = (fn, a, b) => - fn(E.O.default(0.0, a), E.O.default(0.0, b)); - let reduce = (fn, items) => - items |> E.A.fold_left(combine((_, _) => None, _default0(fn)), empty); - // a special version of reduce that adds the results (which should be the most common case by far), - // and conveniently also adds the knownIntegralSums. - let reduceAdd = (fn, items) => - items |> E.A.fold_left(combine((s1, s2) => Some(s1 +. s2), _default0((+.))), empty); - let updateKnownIntegralSum = (knownIntegralSum, t: t): t => ({...t, knownIntegralSum}); + let reduce = (~knownIntegralSumsFn=(_, _) => None, fn, discreteShapes): DistTypes.discreteShape => + discreteShapes + |> E.A.fold_left(combine(~knownIntegralSumsFn, fn), empty); + + let updateKnownIntegralSum = (knownIntegralSum, t: t): t => { + ...t, + knownIntegralSum, + }; let convolve = (fn, t1: t, t2: t) => { let t1s = t1 |> getShape; @@ -357,7 +411,12 @@ module Discrete = { let t1n = t1s |> XYShape.T.length; let t2n = t2s |> XYShape.T.length; - let combinedIntegralSum = combineIntegralSums((s1, s2) => Some(s1 *. s2), t1.knownIntegralSum, t2.knownIntegralSum); + let combinedIntegralSum = + Common.combineIntegralSums( + (s1, s2) => Some(s1 *. s2), + t1.knownIntegralSum, + t2.knownIntegralSum, + ); let xToYMap = E.FloatFloatMap.empty(); @@ -368,8 +427,8 @@ module Discrete = { let my = t1s.ys[i] *. t2s.ys[j]; let _ = Belt.MutableMap.set(xToYMap, x, cv +. my); (); - } - } + }; + }; let rxys = xToYMap |> E.FloatFloatMap.toArray |> XYShape.Zipped.sortByX; @@ -378,25 +437,19 @@ module Discrete = { make(convolvedShape, combinedIntegralSum); }; - let mapY = (~knownIntegralSumFn=(previousKnownIntegralSum => None), fn, t: t) => { + let mapY = (~knownIntegralSumFn=previousKnownIntegralSum => None, fn, t: t) => { let u = E.O.bind(_, knownIntegralSumFn); let yMapFn = shapeMap(XYShape.T.mapY(fn)); t |> yMapFn |> updateKnownIntegralSum(u(t.knownIntegralSum)); }; - let scaleBy = (~scale=1.0, ~knownIntegralSum=None, t: t): t => - t |> mapY((r: float) => r *. scale) |> updateKnownIntegralSum(knownIntegralSum); - - let truncate = (leftCutoff: option(float), rightCutoff: option(float), t: t) => { - let truncatedShape = - t - |> getShape - |> XYShape.T.zip - |> XYShape.Zipped.filterByX(x => x >= E.O.default(neg_infinity, leftCutoff) || x <= E.O.default(infinity, rightCutoff)) - |> XYShape.T.fromZippedArray; - - make(truncatedShape, None); + let scaleBy = (~scale=1.0, t: t): t => { + t + |> mapY((r: float) => r *. scale) + |> updateKnownIntegralSum( + E.O.bind(t.knownIntegralSum, v => Some(scale *. v)), + ); }; module T = @@ -414,7 +467,8 @@ module Discrete = { ) }; let integralEndY = (~cache, t: t) => - t.knownIntegralSum |> E.O.default(t |> integral(~cache) |> Continuous.lastY); + t.knownIntegralSum + |> E.O.default(t |> integral(~cache) |> Continuous.lastY); let minX = shapeFn(XYShape.T.minX); let maxX = shapeFn(XYShape.T.maxX); let toDiscreteProbabilityMassFraction = _ => 1.0; @@ -424,9 +478,9 @@ module Discrete = { let toDiscrete = t => Some(t); let normalize = (t: t): t => { - let discreteIntegralSum = integralEndY(~cache=None, t); - - scaleBy(~scale=(1. /. discreteIntegralSum), ~knownIntegralSum=Some(1.0), t); + t + |> scaleBy(~scale=1. /. integralEndY(~cache=None, t)) + |> updateKnownIntegralSum(Some(1.0)); }; let normalizedToContinuous = _ => None; @@ -448,6 +502,21 @@ module Discrete = { make(clippedShape, None); // if someone needs the sum, they'll have to recompute it }; + let truncate = + (leftCutoff: option(float), rightCutoff: option(float), t: t): t => { + let truncatedShape = + t + |> getShape + |> XYShape.T.zip + |> XYShape.Zipped.filterByX(x => + x >= E.O.default(neg_infinity, leftCutoff) + || x <= E.O.default(infinity, rightCutoff) + ) + |> XYShape.T.fromZippedArray; + + make(truncatedShape, None); + }; + let xToY = (f, t) => t |> getShape @@ -477,53 +546,43 @@ module Discrete = { XYShape.Analysis.getVarianceDangerously(t, mean, getMeanOfSquares); }; }); - }; -// TODO: I think this shouldn't assume continuous/discrete are normalized to 1.0, and thus should not need the discreteProbabilityMassFraction being separate. module Mixed = { type t = DistTypes.mixedShape; - let make = (~continuous, ~discrete): t => { - continuous, - discrete, - }; + let make = (~continuous, ~discrete): t => {continuous, discrete}; let totalLength = (t: t): int => { - let continuousLength = t.continuous |> Continuous.getShape |> XYShape.T.length; + let continuousLength = + t.continuous |> Continuous.getShape |> XYShape.T.length; let discreteLength = t.discrete |> Discrete.getShape |> XYShape.T.length; continuousLength + discreteLength; }; - // TODO: Put into scaling module - //let normalizeMixedPoint = (t, f) => f *. discreteProbabilityMassFraction;*/ + let scaleBy = (~scale=1.0, {discrete, continuous}: t): t => { + let scaledDiscrete = Discrete.scaleBy(~scale, discrete); + let scaledContinuous = Continuous.scaleBy(~scale, continuous); + make(~discrete=scaledDiscrete, ~continuous=scaledContinuous); + }; - //TODO: Warning: This currently computes the integral, which is expensive. - /*let scaleContinuousFn = - ({discreteProbabilityMassFraction}: DistTypes.mixedShape, f) => - f *. (1.0 -. discreteProbabilityMassFraction); */ + let toContinuous = ({continuous}: t) => Some(continuous); + let toDiscrete = ({discrete}: t) => Some(discrete); - //TODO: Warning: This currently computes the integral, which is expensive. + let combine = (~knownIntegralSumsFn, fn, t1: t, t2: t) => { + let reducedDiscrete = + [|t1, t2|] + |> E.A.fmap(toDiscrete) + |> E.A.O.concatSomes + |> Discrete.reduce(~knownIntegralSumsFn, fn); - // Normalizes to 1.0. - /*let scaleContinuous = ({discreteProbabilityMassFraction}: t, continuous) => - // get only the continuous, and scale it to the respective - continuous - |> Continuous.T.scaleToIntegralSum( - ~intendedSum=1.0 -. discreteProbabilityMassFraction, - ); + let reducedContinuous = + [|t1, t2|] + |> E.A.fmap(toContinuous) + |> E.A.O.concatSomes + |> Continuous.reduce(~knownIntegralSumsFn, fn); - let scaleDiscrete = ({discreteProbabilityMassFraction}: t, disrete) => - disrete - |> Discrete.T.scaleToIntegralSum( - ~intendedSum=discreteProbabilityMassFraction, - );*/ - - let truncate = (leftCutoff: option(float), rightCutoff: option(float), {discrete, continuous}: t) => { - let truncatedDiscrete = Discrete.truncate(leftCutoff, rightCutoff, discrete); - let truncatedContinuous = Continuous.truncate(leftCutoff, rightCutoff, continuous); - - make(~discrete=truncatedDiscrete, ~continuous=truncatedContinuous); + make(~discrete=reducedDiscrete, ~continuous=reducedContinuous); }; module T = @@ -536,19 +595,40 @@ module Mixed = { let maxX = ({continuous, discrete}: t) => max(Continuous.T.maxX(continuous), Discrete.T.maxX(discrete)); let toShape = (t: t): DistTypes.shape => Mixed(t); - let toContinuous = ({continuous}: t) => Some(continuous); - let toDiscrete = ({discrete}: t) => Some(discrete); + + let toContinuous = toContinuous; + let toDiscrete = toDiscrete; + + let truncate = + ( + leftCutoff: option(float), + rightCutoff: option(float), + {discrete, continuous}: t, + ) => { + let truncatedContinuous = Continuous.T.truncate(leftCutoff, rightCutoff, continuous); + let truncatedDiscrete = Discrete.T.truncate(leftCutoff, rightCutoff, discrete); + + make(~discrete=truncatedDiscrete, ~continuous=truncatedContinuous); + }; let normalize = (t: t): t => { - let continuousIntegralSum = Continuous.T.Integral.sum(~cache=None, t.continuous); - let discreteIntegralSum = Discrete.T.Integral.sum(~cache=None, t.discrete); + let continuousIntegralSum = + Continuous.T.Integral.sum(~cache=None, t.continuous); + let discreteIntegralSum = + Discrete.T.Integral.sum(~cache=None, t.discrete); let totalIntegralSum = continuousIntegralSum +. discreteIntegralSum; let newContinuousSum = continuousIntegralSum /. totalIntegralSum; let newDiscreteSum = discreteIntegralSum /. totalIntegralSum; - let normalizedContinuous = Continuous.scaleBy(~scale=(1. /. newContinuousSum), ~knownIntegralSum=Some(newContinuousSum), t.continuous); - let normalizedDiscrete = Discrete.scaleBy(~scale=(1. /. newDiscreteSum), ~knownIntegralSum=Some(newDiscreteSum), t.discrete); + let normalizedContinuous = + t.continuous + |> Continuous.scaleBy(~scale=1. /. newContinuousSum) + |> Continuous.updateKnownIntegralSum(Some(newContinuousSum)); + let normalizedDiscrete = + t.discrete + |> Discrete.scaleBy(~scale=1. /. newDiscreteSum) + |> Discrete.updateKnownIntegralSum(Some(newDiscreteSum)); make(~continuous=normalizedContinuous, ~discrete=normalizedDiscrete); }; @@ -563,8 +643,10 @@ module Mixed = { }; let toDiscreteProbabilityMassFraction = ({discrete, continuous}: t) => { - let discreteIntegralSum = Discrete.T.Integral.sum(~cache=None, discrete); - let continuousIntegralSum = Continuous.T.Integral.sum(~cache=None, continuous); + let discreteIntegralSum = + Discrete.T.Integral.sum(~cache=None, discrete); + let continuousIntegralSum = + Continuous.T.Integral.sum(~cache=None, continuous); let totalIntegralSum = discreteIntegralSum +. continuousIntegralSum; discreteIntegralSum /. totalIntegralSum; @@ -575,20 +657,25 @@ module Mixed = { // The easiest way to do this is to simply go by the previous probability masses. // The cache really isn't helpful here, because we would need two separate caches - let discreteIntegralSum = Discrete.T.Integral.sum(~cache=None, discrete); - let continuousIntegralSum = Continuous.T.Integral.sum(~cache=None, continuous); + let discreteIntegralSum = + Discrete.T.Integral.sum(~cache=None, discrete); + let continuousIntegralSum = + Continuous.T.Integral.sum(~cache=None, continuous); let totalIntegralSum = discreteIntegralSum +. continuousIntegralSum; let downsampledDiscrete = Discrete.T.downsample( - int_of_float(float_of_int(count) *. (discreteIntegralSum /. totalIntegralSum)), + int_of_float( + float_of_int(count) *. (discreteIntegralSum /. totalIntegralSum), + ), discrete, ); let downsampledContinuous = Continuous.T.downsample( int_of_float( - float_of_int(count) *. (continuousIntegralSum /. totalIntegralSum), + float_of_int(count) + *. (continuousIntegralSum /. totalIntegralSum), ), continuous, ); @@ -596,23 +683,20 @@ module Mixed = { {discrete: downsampledDiscrete, continuous: downsampledContinuous}; }; - let normalizedToContinuous = (t: t) => - Some(normalize(t).continuous); + let normalizedToContinuous = (t: t) => Some(normalize(t).continuous); - let normalizedToDiscrete = ({discrete} as t: t) => - Some(normalize(t).discrete); + let normalizedToDiscrete = ({discrete} as t: t) => + Some(normalize(t).discrete); - let integral = - ( - ~cache, - {continuous, discrete}: t, - ) => { + let integral = (~cache, {continuous, discrete}: t) => { switch (cache) { | Some(cache) => cache - | None => { + | None => // note: if the underlying shapes aren't normalized, then these integrals won't be either! - let continuousIntegral = Continuous.T.Integral.get(~cache=None, continuous); - let discreteIntegral = Discrete.T.Integral.get(~cache=None, discrete); + let continuousIntegral = + Continuous.T.Integral.get(~cache=None, continuous); + let discreteIntegral = + Discrete.T.Integral.get(~cache=None, discrete); Continuous.make( `Linear, @@ -623,7 +707,6 @@ module Mixed = { ), None, ); - } }; }; @@ -648,14 +731,26 @@ module Mixed = { // This pipes all ys (continuous and discrete) through fn. // If mapY is a linear operation, we might be able to update the knownIntegralSums as well; // if not, they'll be set to None. - let mapY = (~knownIntegralSumFn=(previousIntegralSum => None), fn, {discrete, continuous}: t): t => { + let mapY = + ( + ~knownIntegralSumFn=previousIntegralSum => None, + fn, + {discrete, continuous}: t, + ) + : t => { let u = E.O.bind(_, knownIntegralSumFn); let yMappedDiscrete = - discrete |> Discrete.T.mapY(fn) |> Discrete.updateKnownIntegralSum(u(discrete.knownIntegralSum)); + discrete + |> Discrete.T.mapY(fn) + |> Discrete.updateKnownIntegralSum(u(discrete.knownIntegralSum)); let yMappedContinuous = - continuous |> Continuous.T.mapY(fn) |> Continuous.updateKnownIntegralSum(u(continuous.knownIntegralSum)); + continuous + |> Continuous.T.mapY(fn) + |> Continuous.updateKnownIntegralSum( + u(continuous.knownIntegralSum), + ); { discrete: yMappedDiscrete, @@ -668,34 +763,55 @@ module Mixed = { let continuousMean = Continuous.T.mean(continuous); // the combined mean is the weighted sum of the two: - let discreteIntegralSum = Discrete.T.Integral.sum(~cache=None, discrete); - let continuousIntegralSum = Continuous.T.Integral.sum(~cache=None, continuous); + let discreteIntegralSum = + Discrete.T.Integral.sum(~cache=None, discrete); + let continuousIntegralSum = + Continuous.T.Integral.sum(~cache=None, continuous); let totalIntegralSum = discreteIntegralSum +. continuousIntegralSum; - (discreteMean *. discreteIntegralSum +. continuousMean *. continuousIntegralSum) /. totalIntegralSum; + ( + discreteMean + *. discreteIntegralSum + +. continuousMean + *. continuousIntegralSum + ) + /. totalIntegralSum; }; let variance = ({discrete, continuous} as t: t): float => { // the combined mean is the weighted sum of the two: - let discreteIntegralSum = Discrete.T.Integral.sum(~cache=None, discrete); - let continuousIntegralSum = Continuous.T.Integral.sum(~cache=None, continuous); + let discreteIntegralSum = + Discrete.T.Integral.sum(~cache=None, discrete); + let continuousIntegralSum = + Continuous.T.Integral.sum(~cache=None, continuous); let totalIntegralSum = discreteIntegralSum +. continuousIntegralSum; let getMeanOfSquares = ({discrete, continuous} as t: t) => { - let discreteMean = discrete |> Discrete.shapeMap(XYShape.Analysis.squareXYShape) |> Discrete.T.mean; - let continuousMean = continuous |> XYShape.Analysis.getMeanOfSquaresContinuousShape; - (discreteMean *. discreteIntegralSum +. continuousMean *. continuousIntegralSum) /. totalIntegralSum + let discreteMean = + discrete + |> Discrete.shapeMap(XYShape.Analysis.squareXYShape) + |> Discrete.T.mean; + let continuousMean = + continuous |> XYShape.Analysis.getMeanOfSquaresContinuousShape; + ( + discreteMean + *. discreteIntegralSum + +. continuousMean + *. continuousIntegralSum + ) + /. totalIntegralSum; }; switch (discreteIntegralSum /. totalIntegralSum) { | 1.0 => Discrete.T.variance(discrete) | 0.0 => Continuous.T.variance(continuous) - | _ => XYShape.Analysis.getVarianceDangerously(t, mean, getMeanOfSquares) + | _ => + XYShape.Analysis.getVarianceDangerously(t, mean, getMeanOfSquares) }; }; }); - let convolve = (fn: ((float, float) => float), t1: t, t2: t): t => { + let convolve = (fn: (float, float) => float, t1: t, t2: t): t => { // Discrete convolution can cause a huge increase in the number of samples, // so we'll first downsample. @@ -713,16 +829,21 @@ module Mixed = { // continuous (*) continuous => continuous, but also // discrete (*) continuous => continuous (and vice versa). We have to take care of all combos and then combine them: - let ccConvResult = Continuous.convolve(fn, t1d.continuous, t2d.continuous); - let dcConvResult = Continuous.convolveWithDiscrete(fn, t2d.continuous, t1d.discrete); - let cdConvResult = Continuous.convolveWithDiscrete(fn, t1d.continuous, t2d.discrete); - let continuousConvResult = Continuous.reduce((+.), [|ccConvResult, dcConvResult, cdConvResult|]); + let ccConvResult = + Continuous.convolve(fn, t1d.continuous, t2d.continuous); + let dcConvResult = + Continuous.convolveWithDiscrete(fn, t2d.continuous, t1d.discrete); + let cdConvResult = + Continuous.convolveWithDiscrete(fn, t1d.continuous, t2d.discrete); + let continuousConvResult = + Continuous.reduce((+.), [|ccConvResult, dcConvResult, cdConvResult|]); // ... finally, discrete (*) discrete => discrete, obviously: - let discreteConvResult = Discrete.convolve(fn, t1d.discrete, t2d.discrete); + let discreteConvResult = + Discrete.convolve(fn, t1d.discrete, t2d.discrete); {discrete: discreteConvResult, continuous: continuousConvResult}; - } + }; }; module Shape = { @@ -741,43 +862,31 @@ module Shape = { | Continuous(m) => Continuous(fn3(m)) }; - let toMixed = mapToAll(( - m => m, - d => Mixed.make(~discrete=d, ~continuous=Continuous.empty), - c => Mixed.make(~discrete=Discrete.empty, ~continuous=c), - )); + let toMixed = + mapToAll(( + m => m, + d => Mixed.make(~discrete=d, ~continuous=Continuous.empty), + c => Mixed.make(~discrete=Discrete.empty, ~continuous=c), + )); let convolve = (fn, t1: t, t2: t): t => { Mixed(Mixed.convolve(fn, toMixed(t1), toMixed(t2))); }; - let downsample = (~cache=None, i, t) => - fmap(( - Mixed.T.downsample(i), - Discrete.T.downsample(i), - Continuous.T.downsample(i), - ), t); - - let normalize = - fmap(( - Mixed.T.normalize, - Discrete.T.normalize, - Continuous.T.normalize, - )); - - let truncate (leftCutoff, rightCutoff, t): t = - fmap(( - Mixed.truncate(leftCutoff, rightCutoff), - Discrete.truncate(leftCutoff, rightCutoff), - Continuous.truncate(leftCutoff, rightCutoff), - ), t); + let combine = (~knownIntegralSumsFn=(_, _) => None, fn, t1: t, t2: t) => + switch ((t1, t2)) { + | (Continuous(m1), Continuous(m2)) => DistTypes.Continuous(Continuous.combine(~knownIntegralSumsFn, fn, m1, m2)) + | (Discrete(m1), Discrete(m2)) => DistTypes.Discrete(Discrete.combine(~knownIntegralSumsFn, fn, m1, m2)) + | (m1, m2) => { + DistTypes.Mixed(Mixed.combine(~knownIntegralSumsFn, fn, toMixed(m1), toMixed(m2))) + } + }; module T = Dist({ type t = DistTypes.shape; type integral = DistTypes.continuousShape; - let xToY = (f: float) => mapToAll(( Mixed.T.xToY(f), @@ -789,9 +898,31 @@ module Shape = { let toContinuous = t => None; let toDiscrete = t => None; - let downsample = (~cache=None, i, t) => t; - let toDiscreteProbabilityMassFraction = t => 0.0; - let normalize = t => t; + + + let downsample = (~cache=None, i, t) => + fmap( + ( + Mixed.T.downsample(i), + Discrete.T.downsample(i), + Continuous.T.downsample(i), + ), + t, + ); + + let truncate = (leftCutoff, rightCutoff, t): t => + fmap( + ( + Mixed.T.truncate(leftCutoff, rightCutoff), + Discrete.T.truncate(leftCutoff, rightCutoff), + Continuous.T.truncate(leftCutoff, rightCutoff), + ), + t, + ); + + let toDiscreteProbabilityMassFraction = t => 0.0; + let normalize = + fmap((Mixed.T.normalize, Discrete.T.normalize, Continuous.T.normalize)); let toContinuous = mapToAll(( Mixed.T.toContinuous, @@ -853,7 +984,7 @@ module Shape = { )); }; let maxX = mapToAll((Mixed.T.maxX, Discrete.T.maxX, Continuous.T.maxX)); - let mapY = (~knownIntegralSumFn=(previousIntegralSum => None), fn) => + let mapY = (~knownIntegralSumFn=previousIntegralSum => None, fn) => fmap(( Mixed.T.mapY(~knownIntegralSumFn, fn), Discrete.T.mapY(~knownIntegralSumFn, fn), @@ -935,14 +1066,18 @@ module DistPlus = { let toDiscrete = shapeFn(Shape.T.toDiscrete); let normalize = (t: t): t => { - let normalizedShape = - t |> toShape |> Shape.T.normalize; - - t |> updateShape(normalizedShape); + let normalizedShape = t |> toShape |> Shape.T.normalize; + t |> updateShape(normalizedShape); // TODO: also adjust for domainIncludedProbabilityMass here. }; + let truncate = (leftCutoff, rightCutoff, t: t): t => { + let truncatedShape = t |> toShape |> Shape.T.truncate(leftCutoff, rightCutoff); + + t |> updateShape(truncatedShape); + }; + // TODO: replace this with let normalizedToContinuous = (t: t) => { t @@ -980,7 +1115,13 @@ module DistPlus = { let downsample = (~cache=None, i, t): t => updateShape(t |> toShape |> Shape.T.downsample(i), t); // todo: adjust for limit, maybe? - let mapY = (~knownIntegralSumFn=(previousIntegralSum => None), fn, {shape, _} as t: t): t => + let mapY = + ( + ~knownIntegralSumFn=previousIntegralSum => None, + fn, + {shape, _} as t: t, + ) + : t => Shape.T.mapY(~knownIntegralSumFn, fn, shape) |> updateShape(_, t); let integralEndY = (~cache as _, t: t) => diff --git a/src/distPlus/renderers/DistPlusRenderer.re b/src/distPlus/renderers/DistPlusRenderer.re index c2bf8360..e141d83c 100644 --- a/src/distPlus/renderers/DistPlusRenderer.re +++ b/src/distPlus/renderers/DistPlusRenderer.re @@ -1,13 +1,13 @@ -let truncateIfShould = +let downsampleIfShould = ( - {recommendedLength, shouldTruncate}: RenderTypes.DistPlusRenderer.inputs, + {recommendedLength, shouldDownsample}: RenderTypes.DistPlusRenderer.inputs, outputs: RenderTypes.ShapeRenderer.Combined.outputs, dist, ) => { - let willTruncate = - shouldTruncate + let willDownsample = + shouldDownsample && RenderTypes.ShapeRenderer.Combined.methodUsed(outputs) == `Sampling; - willTruncate ? dist |> Distributions.DistPlus.T.truncate(recommendedLength) : dist; + willDownsample ? dist |> Distributions.DistPlus.T.downsample(recommendedLength) : dist; }; let run = @@ -21,7 +21,7 @@ let run = ~guesstimatorString=Some(inputs.distPlusIngredients.guesstimatorString), (), ) - |> Distributions.DistPlus.T.scaleToIntegralSum(~intendedSum=1.0); + |> Distributions.DistPlus.T.normalize; let outputs = ShapeRenderer.run({ samplingInputs: inputs.samplingInputs, @@ -32,6 +32,6 @@ let run = }); let shape = outputs |> RenderTypes.ShapeRenderer.Combined.getShape; let dist = - shape |> E.O.fmap(toDist) |> E.O.fmap(truncateIfShould(inputs, outputs)); + shape |> E.O.fmap(toDist) |> E.O.fmap(downsampleIfShould(inputs, outputs)); RenderTypes.DistPlusRenderer.Outputs.make(outputs, dist); -}; \ No newline at end of file +}; diff --git a/src/distPlus/renderers/RenderTypes.re b/src/distPlus/renderers/RenderTypes.re index c94ca69a..e091ecad 100644 --- a/src/distPlus/renderers/RenderTypes.re +++ b/src/distPlus/renderers/RenderTypes.re @@ -75,7 +75,7 @@ module ShapeRenderer = { module DistPlusRenderer = { let defaultRecommendedLength = 10000; - let defaultShouldTruncate = true; + let defaultShouldDownsample = true; type ingredients = { guesstimatorString: string, domain: DistTypes.domain, @@ -85,7 +85,7 @@ module DistPlusRenderer = { distPlusIngredients: ingredients, samplingInputs: ShapeRenderer.Sampling.inputs, recommendedLength: int, - shouldTruncate: bool, + shouldDownsample: bool, }; module Ingredients = { let make = @@ -105,7 +105,7 @@ module DistPlusRenderer = { ( ~samplingInputs=ShapeRenderer.Sampling.Inputs.empty, ~recommendedLength=defaultRecommendedLength, - ~shouldTruncate=defaultShouldTruncate, + ~shouldDownsample=defaultShouldDownsample, ~distPlusIngredients, (), ) @@ -113,7 +113,7 @@ module DistPlusRenderer = { distPlusIngredients, samplingInputs, recommendedLength, - shouldTruncate, + shouldDownsample, }; type outputs = { shapeRenderOutputs: ShapeRenderer.Combined.outputs, diff --git a/src/distPlus/symbolic/MathJsParser.re b/src/distPlus/symbolic/MathJsParser.re index 5353aba0..d80cf004 100644 --- a/src/distPlus/symbolic/MathJsParser.re +++ b/src/distPlus/symbolic/MathJsParser.re @@ -154,16 +154,17 @@ module MathAdtToDistDst = { weights: option(array(float)), ) => { let weights = weights |> E.O.default([||]); - let dists = + + /*let dists: = args |> E.A.fmap( fun | Ok(a) => a | Error(e) => Error(e) - ); + );*/ - let firstWithError = dists |> Belt.Array.getBy(_, Belt.Result.isError); - let withoutErrors = dists |> E.A.fmap(E.R.toOption) |> E.A.O.concatSomes; + let firstWithError = args |> Belt.Array.getBy(_, Belt.Result.isError); + let withoutErrors = args |> E.A.fmap(E.R.toOption) |> E.A.O.concatSomes; switch (firstWithError) { | Some(Error(e)) => Error(e) @@ -174,16 +175,16 @@ module MathAdtToDistDst = { |> E.A.fmapi((index, t) => { let w = weights |> E.A.get(_, index) |> E.O.default(1.0); - `Operation(`ScaleBy(`Multiply, t, `DistData(`Symbolic(`Float(w))))) + `Operation(`ScaleOperation(`Multiply, t, `DistData(`Symbolic(`Float(w))))) }); let pointwiseSum = components |> Js.Array.sliceFrom(1) |> E.A.fold_left((acc, x) => { - `PointwiseSum(acc, x) + `Operation(`PointwiseOperation(`Add, acc, x)) }, E.A.unsafe_get(components, 0)) - Ok(`Normalize(pointwiseSum)) + Ok(`Operation(`Normalize(pointwiseSum))) } }; }; @@ -254,21 +255,21 @@ module MathAdtToDistDst = { args |> E.A.fmap(functionParser) |> (fun - | [|Ok(l), Ok(r)|] => Ok(`Combination(l, r, `AddOperation)) + | [|Ok(l), Ok(r)|] => Ok(`Operation(`StandardOperation(`Add, l, r))) | _ => Error("Addition needs two operands")) } | Fn({name: "subtract", args}) => { args |> E.A.fmap(functionParser) |> (fun - | [|Ok(l), Ok(r)|] => Ok(`Combination(l, r, `SubtractOperation)) + | [|Ok(l), Ok(r)|] => Ok(`Operation(`StandardOperation(`Subtract, l, r))) | _ => Error("Subtraction needs two operands")) } | Fn({name: "multiply", args}) => { args |> E.A.fmap(functionParser) |> (fun - | [|Ok(l), Ok(r)|] => Ok(`Combination(l, r, `MultiplyOperation)) + | [|Ok(l), Ok(r)|] => Ok(`Operation(`StandardOperation(`Multiply, l, r))) | _ => Error("Multiplication needs two operands")) } | Fn({name: "divide", args}) => { @@ -276,28 +277,37 @@ module MathAdtToDistDst = { |> E.A.fmap(functionParser) |> (fun | [|Ok(l), Ok(`DistData(`Symbolic(`Float(0.0))))|] => Error("Division by zero") - | [|Ok(l), Ok(r)|] => Ok(`Combination(l, r, `DivideOperation)) + | [|Ok(l), Ok(r)|] => Ok(`Operation(`StandardOperation(`Divide, l, r))) | _ => Error("Division needs two operands")) } | Fn({name: "pow", args}) => { args |> E.A.fmap(functionParser) |> (fun - | [|Ok(l), Ok(r)|] => Ok(`Combination(l, r, `ExponentiateOperation)) + | [|Ok(l), Ok(r)|] => Ok(`Operation(`StandardOperation(`Exponentiate, l, r))) + | _ => Error("Division needs two operands") | _ => Error("Exponentiations needs two operands")) } | Fn({name: "leftTruncate", args}) => { args |> E.A.fmap(functionParser) |> (fun - | [|Ok(l), Ok(`DistData(`Symbolic(`Float(r))))|] => Ok(`LeftTruncate(l, r)) + | [|Ok(d), Ok(`DistData(`Symbolic(`Float(lc))))|] => Ok(`Operation(`Truncate(Some(lc), None, d))) | _ => Error("leftTruncate needs two arguments: the expression and the cutoff")) } | Fn({name: "rightTruncate", args}) => { args |> E.A.fmap(functionParser) |> (fun - | [|Ok(l), Ok(`DistData(`Symbolic(`Float(r))))|] => Ok(`RightTruncate(l, r)) + | [|Ok(d), Ok(`DistData(`Symbolic(`Float(rc))))|] => Ok(`Operation(`Truncate(None, Some(rc), d))) + | _ => Error("rightTruncate needs two arguments: the expression and the cutoff")) + } + | Fn({name: "truncate", args}) => { + args + |> E.A.fmap(functionParser) + |> (fun + | [|Ok(d), Ok(`DistData(`Symbolic(`Float(lc)))), Ok(`DistData(`Symbolic(`Float(rc))))|] => Ok(`Operation(`Truncate(Some(lc), Some(rc), d))) + // TODO: allow on-the-fly evaluations of FloatFromDists to be used as cutoff arguments here. | _ => Error("rightTruncate needs two arguments: the expression and the cutoff")) } | Fn({name}) => Error(name ++ ": function not supported") diff --git a/src/distPlus/symbolic/TreeNode.re b/src/distPlus/symbolic/TreeNode.re index 5f1aece7..4aa645fb 100644 --- a/src/distPlus/symbolic/TreeNode.re +++ b/src/distPlus/symbolic/TreeNode.re @@ -1,69 +1,60 @@ /* This module represents a tree node. */ -/* TreeNodes are either Data (i.e. symbolic or rendered distributions) or Operations. */ -type treeNode = [ - | `DistData(distData) - | `Operation(operation) -] and distData = [ +type distData = [ | `Symbolic(SymbolicDist.dist) | `RenderedShape(DistTypes.shape) -] and operation = [ - // binary operations - | `StandardOperation(standardOperation, treeNode, treeNode) - | `PointwiseOperation(pointwiseOperation, treeNode, treeNode) - | `ScaleOperation(scaleOperation, treeNode, scaleBy) - // unary operations - | `Render(treeNode) // always evaluates to `DistData(`RenderedShape(...)) - | `Truncate(leftCutoff, rightCutoff, treeNode) - | `Normalize(treeNode) - // direct evaluations of dists (e.g. cdf, sample) - | `FloatFromDist(distToFloatOperation, treeNode) -] and standardOperation = [ +]; + +type standardOperation = [ | `Add | `Multiply | `Subtract | `Divide | `Exponentiate -] and pointwiseOperation = [ - | `Add - | `Multiply -] and scaleOperation = [ - | `Multiply - | `Log +]; +type pointwiseOperation = [ | `Add | `Multiply]; +type scaleOperation = [ | `Multiply | `Exponentiate | `Log]; +type distToFloatOperation = [ | `Pdf(float) | `Inv(float) | `Mean | `Sample]; + +/* TreeNodes are either Data (i.e. symbolic or rendered distributions) or Operations. */ +type treeNode = [ + | `DistData(distData) // a leaf node that describes a distribution + | `Operation(operation) // an operation on two child nodes ] -and scaleBy = treeNode and leftCutoff = option(float) and rightCutoff = option(float) -and distToFloatOperation = [ - | `Pdf(float) - | `Cdf(float) - | `Inv(float) - | `Sample +and operation = [ + | // binary operations + `StandardOperation( + standardOperation, + treeNode, + treeNode, + ) + // unary operations + | `PointwiseOperation(pointwiseOperation, treeNode, treeNode) // always evaluates to `DistData(`RenderedShape(...)) + | `ScaleOperation(scaleOperation, treeNode, treeNode) // always evaluates to `DistData(`RenderedShape(...)) + | `Render(treeNode) // always evaluates to `DistData(`RenderedShape(...)) + | `Truncate // always evaluates to `DistData(`RenderedShape(...)) +( + option(float), + option(float), + treeNode, + ) // leftCutoff and rightCutoff + | `Normalize // always evaluates to `DistData(`RenderedShape(...)) + // leftCutoff and rightCutoff +( + treeNode, + ) + | `FloatFromDist // always evaluates to `DistData(`RenderedShape(...)) + // leftCutoff and rightCutoff +( + distToFloatOperation, + treeNode, + ) ]; module TreeNode = { type t = treeNode; type simplifier = treeNode => result(treeNode, string); - type renderParams = { - operationToDistData: (int, operation) => result(t, string), - sampleCount: int, - } - - let rec renderToShape = (renderParams, t: t): result(DistTypes.shape, string) => { - switch (t) { - | `DistData(`RenderedShape(s)) => Ok(s) // already a rendered shape, we're done here - | `DistData(`Symbolic(d)) => - switch (d) { - | `Float(v) => - Ok(Discrete(Distributions.Discrete.make({xs: [|v|], ys: [|1.0|]}, Some(1.0)))); - | _ => - let xs = SymbolicDist.GenericDistFunctions.interpolateXs(~xSelection=`ByWeight, d, renderParams.sampleCount); - let ys = xs |> E.A.fmap(x => SymbolicDist.GenericDistFunctions.pdf(x, d)); - Ok(Continuous(Distributions.Continuous.make(`Linear, {xs, ys}, Some(1.0)))); - } - | `Operation(op) => E.R.bind(renderParams.operationToDistData(renderParams.sampleCount, op), renderToShape(renderParams)) - }; - }; - /* The following modules encapsulate everything we can do with * different kinds of operations. */ @@ -154,207 +145,328 @@ module TreeNode = { }; }; - let evaluateNumerically = (standardOp, renderParams, t1, t2) => { + let evaluateNumerically = (standardOp, operationToDistData, t1, t2) => { let func = funcFromOp(standardOp); - // TODO: downsample the two shapes - let renderedShape1 = t1 |> renderToShape(renderParams); - let renderedShape2 = t2 |> renderToShape(renderParams); + // force rendering into shapes + let renderedShape1 = operationToDistData(`Render(t1)); + let renderedShape2 = operationToDistData(`Render(t2)); - // This will most likely require a mixed - - switch ((renderedShape1, renderedShape2)) { - | (Error(e1), _) => Error(e1) - | (_, Error(e2)) => Error(e2) - | (Ok(s1), Ok(s2)) => Ok(`DistData(`RenderedShape(Distributions.Shape.convolve(func, s1, s2)))) + switch (renderedShape1, renderedShape2) { + | ( + Ok(`DistData(`RenderedShape(s1))), + Ok(`DistData(`RenderedShape(s2))), + ) => + Ok( + `DistData( + `RenderedShape(Distributions.Shape.convolve(func, s1, s2)), + ), + ) + | (Error(e1), _) => Error(e1) + | (_, Error(e2)) => Error(e2) + | _ => Error("Could not render shapes.") }; }; let evaluateToDistData = - (standardOp: standardOperation, renderParams, t1: t, t2: t): result(treeNode, string) => + (standardOp: standardOperation, operationToDistData, t1: t, t2: t) + : result(treeNode, string) => standardOp |> Simplify.attempt(_, t1, t2) |> E.R.bind( _, fun | `DistData(d) => Ok(`DistData(d)) // the analytical simplifaction worked, nice! - | `Operation(_) => // if not, run the convolution - evaluateNumerically(standardOp, renderParams, t1, t2), + | `Operation(_) => + // if not, run the convolution + evaluateNumerically(standardOp, operationToDistData, t1, t2), ); }; module ScaleOperation = { - let rec mean = (renderParams, t: t): result(float, string) => { - switch (t) { - | `DistData(`RenderedShape(s)) => Ok(Distributions.Shape.T.mean(s)) - | `DistData(`Symbolic(s)) => SymbolicDist.GenericDistFunctions.mean(s) - // evaluating the operation returns result(treeNode(distData)). We then want to make sure - | `Operation(op) => E.R.bind(renderParams.operationToDistData(renderParams.sampleCount, op), mean(renderParams)) - } - }; - let fnFromOp = fun - | `Multiply => (*.) - | `Log => ((a, b) => ( log(a) /. log(b) )); + | `Multiply => ( *. ) + | `Exponentiate => ( ** ) + | `Log => ((a, b) => log(a) /. log(b)); let knownIntegralSumFnFromOp = fun - | `Multiply => (a, b) => Some(a *. b) + | `Multiply => ((a, b) => Some(a *. b)) + | `Exponentiate => ((_, _) => None) | `Log => ((_, _) => None); - let evaluateToDistData = (scaleOp, renderParams, t, scaleBy) => { + let evaluateToDistData = (scaleOp, operationToDistData, t, scaleBy) => { + // scaleBy has to be a single float, otherwise we'll return an error. let fn = fnFromOp(scaleOp); let knownIntegralSumFn = knownIntegralSumFnFromOp(scaleOp); - let renderedShape = t |> renderToShape(renderParams); - let scaleByMeanValue = mean(renderParams, scaleBy); - switch ((renderedShape, scaleByMeanValue)) { + let renderedShape = operationToDistData(`Render(t)); + + switch (renderedShape, scaleBy) { | (Error(e1), _) => Error(e1) - | (_, Error(e2)) => Error(e2) - | (Ok(rs), Ok(sm)) => - Ok(`DistData(`RenderedShape(Distributions.Shape.T.mapY(~knownIntegralSumFn=knownIntegralSumFn(sm), fn(sm), rs)))) - } + | ( + Ok(`DistData(`RenderedShape(rs))), + `DistData(`Symbolic(`Float(sm))), + ) => + Ok( + `DistData( + `RenderedShape( + Distributions.Shape.T.mapY( + ~knownIntegralSumFn=knownIntegralSumFn(sm), + fn(sm), + rs, + ), + ), + ), + ) + | (_, _) => Error("Can only scale by float values.") + }; }; }; module PointwiseOperation = { - let funcFromOp: (pointwiseOperation => ((float, float) => float)) = - fun - | `Add => (+.) - | `Multiply => ( *. ); + let pointwiseAdd = (operationToDistData, t1, t2) => { + let renderedShape1 = operationToDistData(`Render(t1)); + let renderedShape2 = operationToDistData(`Render(t2)); - let evaluateToDistData = (pointwiseOp, renderParams, t1, t2) => { - let func = funcFromOp(pointwiseOp); - let renderedShape1 = t1 |> renderToShape(renderParams); - let renderedShape2 = t2 |> renderToShape(renderParams); + switch ((renderedShape1, renderedShape2)) { + | (Error(e1), _) => Error(e1) + | (_, Error(e2)) => Error(e2) + | (Ok(`DistData(`RenderedShape(rs1))), Ok(`DistData(`RenderedShape(rs2)))) => Ok(`DistData(`RenderedShape(Distributions.Shape.combine(~knownIntegralSumsFn=(a, b) => Some(a +. b), (+.), rs1, rs2)))) + | _ => Error("Could not perform pointwise addition.") + }; + }; - // TODO: figure out integral, diff between pointwiseAdd and pointwiseProduct and other stuff - // Distributions.Shape.reduce(func, renderedShape1, renderedShape2); + let pointwiseMultiply = (operationToDistData, t1, t2) => { + // TODO: construct a function that we can easily sample from, to construct + // a RenderedShape. Use the xMin and xMax of the rendered shapes to tell the sampling function where to look. + Error("Pointwise multiplication not yet supported."); + }; - Error("Pointwise operations currently not supported.") + let evaluateToDistData = (pointwiseOp, operationToDistData, t1, t2) => { + switch (pointwiseOp) { + | `Add => pointwiseAdd(operationToDistData, t1, t2) + | `Multiply => pointwiseMultiply(operationToDistData, t1, t2) + } }; }; module Truncate = { module Simplify = { - let tryTruncatingNothing: simplifier = fun - | `Operation(`Truncate(None, None, `DistData(d))) => Ok(`DistData(d)) - | t => Ok(t); + let tryTruncatingNothing: simplifier = + fun + | `Operation(`Truncate(None, None, `DistData(d))) => + Ok(`DistData(d)) + | t => Ok(t); - let tryTruncatingUniform: simplifier = fun - | `Operation(`Truncate(lc, rc, `DistData(`Symbolic(`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(`DistData(`Symbolic(`Uniform({low: newLow, high: newHigh})))); - } - | t => Ok(t); + let tryTruncatingUniform: simplifier = + fun + | `Operation(`Truncate(lc, rc, `DistData(`Symbolic(`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( + `DistData(`Symbolic(`Uniform({low: newLow, high: newHigh}))), + ); + } + | t => Ok(t); let attempt = (leftCutoff, rightCutoff, t): result(treeNode, string) => { - let originalTreeNode = `Operation(`Truncate(leftCutoff, rightCutoff, t)); + let originalTreeNode = + `Operation(`Truncate((leftCutoff, rightCutoff, t))); - originalTreeNode - |> tryTruncatingNothing - |> E.R.bind(_, tryTruncatingUniform); + originalTreeNode + |> tryTruncatingNothing + |> E.R.bind(_, tryTruncatingUniform); }; }; - let evaluateNumerically = (leftCutoff, rightCutoff, renderParams, t) => { + let evaluateNumerically = + (leftCutoff, rightCutoff, operationToDistData, 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 = t |> renderToShape(renderParams); + let renderedShape = operationToDistData(`Render(t)); - E.R.bind(renderedShape, rs => { - let truncatedShape = rs |> Distributions.Shape.truncate(leftCutoff, rightCutoff); + switch (renderedShape) { + | Ok(`DistData(`RenderedShape(rs))) => + let truncatedShape = + rs |> Distributions.Shape.T.truncate(leftCutoff, rightCutoff); Ok(`DistData(`RenderedShape(rs))); - }); + | Error(e1) => Error(e1) + | _ => Error("Could not truncate distribution.") + }; }; - let evaluateToDistData = (leftCutoff: option(float), rightCutoff: option(float), renderParams, t: treeNode): result(treeNode, string) => { + let evaluateToDistData = + ( + leftCutoff: option(float), + rightCutoff: option(float), + operationToDistData, + t: treeNode, + ) + : result(treeNode, string) => { t |> Simplify.attempt(leftCutoff, rightCutoff) |> E.R.bind( _, fun | `DistData(d) => Ok(`DistData(d)) // the analytical simplifaction worked, nice! - | `Operation(_) => evaluateNumerically(leftCutoff, rightCutoff, renderParams, t), + | `Operation(_) => + evaluateNumerically( + leftCutoff, + rightCutoff, + operationToDistData, + t, + ), ); // if not, run the convolution - }; + }; }; module Normalize = { - let rec evaluateToDistData = (renderParams, t: treeNode): result(treeNode, string) => { + let rec evaluateToDistData = + (operationToDistData, t: treeNode): result(treeNode, string) => { switch (t) { | `DistData(`Symbolic(_)) => Ok(t) - | `DistData(`RenderedShape(s)) => { - let normalized = Distributions.Shape.normalize(s); + | `DistData(`RenderedShape(s)) => + let normalized = Distributions.Shape.T.normalize(s); Ok(`DistData(`RenderedShape(normalized))); - } - | `Operation(op) => E.R.bind(renderParams.operationToDistData(renderParams.sampleCount, op), evaluateToDistData(renderParams)) - } - } + | `Operation(op) => + E.R.bind( + operationToDistData(op), + evaluateToDistData(operationToDistData), + ) + }; + }; }; module FloatFromDist = { let evaluateFromSymbolic = (distToFloatOp: distToFloatOperation, s) => { - let value = switch (distToFloatOp) { - | `Pdf(f) => SymbolicDist.GenericDistFunctions.pdf(f, s) - | `Cdf(f) => 0.0 - | `Inv(f) => SymbolicDist.GenericDistFunctions.inv(f, s) - | `Sample => SymbolicDist.GenericDistFunctions.sample(s) - } - Ok(`DistData(`Symbolic(`Float(value)))); + let value = + switch (distToFloatOp) { + | `Pdf(f) => Ok(SymbolicDist.GenericDistFunctions.pdf(f, s)) + | `Inv(f) => Ok(SymbolicDist.GenericDistFunctions.inv(f, s)) + | `Sample => Ok(SymbolicDist.GenericDistFunctions.sample(s)) + | `Mean => SymbolicDist.GenericDistFunctions.mean(s) + }; + E.R.bind(value, v => Ok(`DistData(`Symbolic(`Float(v))))); }; - let evaluateFromRenderedShape = (distToFloatOp: distToFloatOperation, rs: DistTypes.shape): result(treeNode, string) => { - // evaluate the pdf, cdf, get sample, etc. from the renderedShape rs - // Should be a float like Ok(`DistData(`Symbolic(Float(0.0)))); - Error("Float from dist is not yet implemented."); + let evaluateFromRenderedShape = + (distToFloatOp: distToFloatOperation, rs: DistTypes.shape) + : result(treeNode, string) => { + Ok(`DistData(`Symbolic(`Float(Distributions.Shape.T.mean(rs))))); }; - let rec evaluateToDistData = (distToFloatOp: distToFloatOperation, renderParams, t: treeNode): result(treeNode, string) => { + let rec evaluateToDistData = + ( + distToFloatOp: distToFloatOperation, + operationToDistData, + t: treeNode, + ) + : result(treeNode, string) => { switch (t) { | `DistData(`Symbolic(s)) => evaluateFromSymbolic(distToFloatOp, s) // we want to evaluate the distToFloatOp on the symbolic dist - | `DistData(`RenderedShape(rs)) => evaluateFromRenderedShape(distToFloatOp, rs) - | `Operation(op) => E.R.bind(renderParams.operationToDistData(renderParams.sampleCount, op), evaluateToDistData(distToFloatOp, renderParams)) - } - } + | `DistData(`RenderedShape(rs)) => + evaluateFromRenderedShape(distToFloatOp, rs) + | `Operation(op) => + E.R.bind( + operationToDistData(op), + evaluateToDistData(distToFloatOp, operationToDistData), + ) + }; + }; }; module Render = { - let evaluateToRenderedShape = (renderParams, t: treeNode): result(t, string) => { - E.R.bind(renderToShape(renderParams, t), rs => Ok(`DistData(`RenderedShape(rs)))); - } + let rec evaluateToRenderedShape = + (operationToDistData: operation => result(t, string), sampleCount: int, t: treeNode) + : result(t, string) => { + switch (t) { + | `DistData(`RenderedShape(s)) => Ok(`DistData(`RenderedShape(s))) // already a rendered shape, we're done here + | `DistData(`Symbolic(d)) => + switch (d) { + | `Float(v) => + Ok( + `DistData( + `RenderedShape( + Discrete( + Distributions.Discrete.make( + {xs: [|v|], ys: [|1.0|]}, + Some(1.0), + ), + ), + ), + ), + ) + | _ => + let xs = + SymbolicDist.GenericDistFunctions.interpolateXs( + ~xSelection=`ByWeight, + d, + sampleCount, + ); + let ys = + xs |> E.A.fmap(x => SymbolicDist.GenericDistFunctions.pdf(x, d)); + Ok( + `DistData( + `RenderedShape( + Continuous( + Distributions.Continuous.make( + `Linear, + {xs, ys}, + Some(1.0), + ), + ), + ), + ), + ); + } + | `Operation(op) => + E.R.bind( + operationToDistData(op), + evaluateToRenderedShape(operationToDistData, sampleCount), + ) + }; + }; }; let rec operationToDistData = - (sampleCount: int, op: operation): result(t, string) => { - + (sampleCount: int, op: operation): result(t, string) => { // the functions that convert the Operation nodes to DistData nodes need to // have a way to call this function on their children, if their children are themselves Operation nodes. - - let renderParams: renderParams = { - operationToDistData: operationToDistData, - sampleCount: sampleCount, - }; - switch (op) { | `StandardOperation(standardOp, t1, t2) => StandardOperation.evaluateToDistData( - standardOp, renderParams, t1, t2 // we want to give it the option to render or simply leave it as is + standardOp, + operationToDistData(sampleCount), + t1, + t2 // we want to give it the option to render or simply leave it as is ) | `PointwiseOperation(pointwiseOp, t1, t2) => PointwiseOperation.evaluateToDistData( pointwiseOp, - renderParams, + operationToDistData(sampleCount), t1, t2, ) | `ScaleOperation(scaleOp, t, scaleBy) => - ScaleOperation.evaluateToDistData(scaleOp, renderParams, t, scaleBy) - | `Truncate(leftCutoff, rightCutoff, t) => Truncate.evaluateToDistData(leftCutoff, rightCutoff, renderParams, t) - | `FloatFromDist(distToFloatOp, t) => FloatFromDist.evaluateToDistData(distToFloatOp, renderParams, t) - | `Normalize(t) => Normalize.evaluateToDistData(renderParams, t) - | `Render(t) => Render.evaluateToRenderedShape(renderParams, t) + ScaleOperation.evaluateToDistData( + scaleOp, + operationToDistData(sampleCount), + t, + scaleBy, + ) + | `Truncate(leftCutoff, rightCutoff, t) => + Truncate.evaluateToDistData( + leftCutoff, + rightCutoff, + operationToDistData(sampleCount), + t, + ) + | `FloatFromDist(distToFloatOp, t) => + FloatFromDist.evaluateToDistData(distToFloatOp, operationToDistData(sampleCount), t) + | `Normalize(t) => Normalize.evaluateToDistData(operationToDistData(sampleCount), t) + | `Render(t) => + Render.evaluateToRenderedShape(operationToDistData(sampleCount), sampleCount, t) }; }; @@ -372,7 +484,8 @@ module TreeNode = { }; let rec toString = (t: t): string => { - let stringFromStandardOperation = fun + let stringFromStandardOperation = + fun | `Add => " + " | `Subtract => " - " | `Multiply => " * " @@ -384,31 +497,53 @@ module TreeNode = { | `Add => " .+ " | `Multiply => " .* "; + let stringFromFloatFromDistOperation = + fun + | `Pdf(f) => "pdf(x=$f, " + | `Inv(f) => "inv(c=$f, " + | `Sample => "sample(" + | `Mean => "mean("; + + switch (t) { - | `DistData(`Symbolic(d)) => SymbolicDist.GenericDistFunctions.toString(d) + | `DistData(`Symbolic(d)) => + SymbolicDist.GenericDistFunctions.toString(d) | `DistData(`RenderedShape(s)) => "[shape]" - | `Operation(`StandardOperation(op, t1, t2)) => toString(t1) ++ stringFromStandardOperation(op) ++ toString(t2) - | `Operation(`PointwiseOperation(op, t1, t2)) => toString(t1) ++ stringFromPointwiseOperation(op) ++ toString(t2) - | `Operation(`ScaleOperation(_scaleOp, t, scaleBy)) => toString(t) ++ " @ " ++ toString(scaleBy) + | `Operation(`StandardOperation(op, t1, t2)) => + toString(t1) ++ stringFromStandardOperation(op) ++ toString(t2) + | `Operation(`PointwiseOperation(op, t1, t2)) => + toString(t1) ++ stringFromPointwiseOperation(op) ++ toString(t2) + | `Operation(`ScaleOperation(_scaleOp, t, scaleBy)) => + toString(t) ++ " @ " ++ toString(scaleBy) | `Operation(`Normalize(t)) => "normalize(" ++ toString(t) ++ ")" - | `Operation(`Truncate(lc, rc, t)) => "truncate(" ++ toString(t) ++ ", " ++ E.O.dimap(string_of_float, () => "-inf", lc) ++ ", " ++ E.O.dimap(string_of_float, () => "inf", rc) ++ ")" + | `Operation(`FloatFromDist(floatFromDistOp, t)) => stringFromFloatFromDistOperation(floatFromDistOp) ++ toString(t) ++ ")" + | `Operation(`Truncate(lc, rc, t)) => + "truncate(" + ++ toString(t) + ++ ", " + ++ E.O.dimap(Js.Float.toString, () => "-inf", lc) + ++ ", " + ++ E.O.dimap(Js.Float.toString, () => "inf", rc) + ++ ")" | `Operation(`Render(t)) => toString(t) - } + }; }; }; let toShape = (sampleCount: int, treeNode: treeNode) => { - let renderResult = TreeNode.toDistData(`Operation(`Render(treeNode)), sampleCount); - + let renderResult = + TreeNode.toDistData(`Operation(`Render(treeNode)), sampleCount); switch (renderResult) { - | Ok(`DistData(`RenderedShape(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(""); - } + | Ok(`DistData(`RenderedShape(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(""); | Ok(_) => E.O.toExn("Rendering failed.", None) - | Error(message) => E.O.toExn("No shape found!", None) - } + | Error(message) => E.O.toExn("No shape found, error: " ++ message, None) + }; }; + +let toString = (treeNode: treeNode) => + TreeNode.toString(treeNode); diff --git a/src/interface/FormBuilder.re b/src/interface/FormBuilder.re index 55c4f071..7556a82f 100644 --- a/src/interface/FormBuilder.re +++ b/src/interface/FormBuilder.re @@ -22,7 +22,7 @@ let propValue = (t: Prop.Value.t) => { RenderTypes.DistPlusRenderer.make( ~distPlusIngredients=r, ~recommendedLength=10000, - ~shouldTruncate=true, + ~shouldDownsample=true, (), ) |> DistPlusRenderer.run @@ -105,4 +105,4 @@ module ModelForm = { ; }; -}; \ No newline at end of file +};