diff --git a/README.md b/README.md index 3ded43c6..13aa5ef7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Squiggle +![Packages check](https://github.com/QURIresearch/squiggle/actions/workflows/ci.yaml/badge.svg) This is an experiment DSL/language for making probabilistic estimates. The full story can be found [here](https://www.lesswrong.com/s/rDe8QE5NvXcZYzgZ3). diff --git a/packages/squiggle-lang/README.md b/packages/squiggle-lang/README.md index a7d0d5d0..87bc76fe 100644 --- a/packages/squiggle-lang/README.md +++ b/packages/squiggle-lang/README.md @@ -13,6 +13,9 @@ Other: yarn start # listens to files and recompiles at every mutation yarn test yarn test:watch # keeps an active session and runs all tests at every mutation + +# where o := open in osx and o := xdg-open in linux, +yarn coverage; o _coverage/index.html # produces coverage report and opens it in browser ``` # TODO: clean up this README.md diff --git a/packages/squiggle-lang/__tests__/Bandwidth__Test.res b/packages/squiggle-lang/__tests__/Bandwidth_test.res similarity index 53% rename from packages/squiggle-lang/__tests__/Bandwidth__Test.res rename to packages/squiggle-lang/__tests__/Bandwidth_test.res index d37e5bb8..4e5c630a 100644 --- a/packages/squiggle-lang/__tests__/Bandwidth__Test.res +++ b/packages/squiggle-lang/__tests__/Bandwidth_test.res @@ -4,10 +4,10 @@ open Expect describe("Bandwidth", () => { test("nrd0()", () => { let data = [1., 4., 3., 2.] - expect(Bandwidth.nrd0(data)) -> toEqual(0.7625801874014622) + expect(SampleSetDist_Bandwidth.nrd0(data)) -> toEqual(0.7625801874014622) }) test("nrd()", () => { let data = [1., 4., 3., 2.] - expect(Bandwidth.nrd(data)) -> toEqual(0.8981499984950554) + expect(SampleSetDist_Bandwidth.nrd(data)) -> toEqual(0.8981499984950554) }) }) diff --git a/packages/squiggle-lang/__tests__/Distributions/DistributionOperation_test.res b/packages/squiggle-lang/__tests__/Distributions/DistributionOperation_test.res new file mode 100644 index 00000000..34a8dd6e --- /dev/null +++ b/packages/squiggle-lang/__tests__/Distributions/DistributionOperation_test.res @@ -0,0 +1,103 @@ +open Jest +open Expect + +let env: DistributionOperation.env = { + sampleCount: 100, + xyPointLength: 100, +} + +let { + normalDist5, + normalDist10, + normalDist20, + normalDist, + uniformDist, + betaDist, + lognormalDist, + cauchyDist, + triangularDist, + exponentialDist, +} = module(GenericDist_Fixtures) +let mkNormal = (mean, stdev) => GenericDist_Types.Symbolic(#Normal({mean: mean, stdev: stdev})) + +let {toFloat, toDist, toString, toError} = module(DistributionOperation.Output) +let {run} = module(DistributionOperation) +let {fmap} = module(DistributionOperation.Output) +let run = run(~env) +let outputMap = fmap(~env) +let toExt: option<'a> => 'a = E.O.toExt( + "Should be impossible to reach (This error is in test file)", +) + +describe("sparkline", () => { + let runTest = ( + name: string, + dist: GenericDist_Types.genericDist, + expected: DistributionOperation.outputType, + ) => { + test(name, () => { + let result = DistributionOperation.run(~env, FromDist(ToString(ToSparkline(20)), dist)) + expect(result)->toEqual(expected) + }) + } + + runTest( + "normal", + normalDist, + String(`▁▁▁▁▁▂▄▆▇██▇▆▄▂▁▁▁▁▁`), + ) + + runTest( + "uniform", + uniformDist, + String(`████████████████████`), + ) + + runTest("beta", betaDist, String(`▁▄▇████▇▆▅▄▃▃▂▁▁▁▁▁▁`)) + + runTest( + "lognormal", + lognormalDist, + String(`▁█▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁`), + ) + + runTest( + "cauchy", + cauchyDist, + String(`▁▁▁▁▁▁▁▁▁██▁▁▁▁▁▁▁▁▁`), + ) + + runTest( + "triangular", + triangularDist, + String(`▁▁▂▃▄▅▆▇████▇▆▅▄▃▂▁▁`), + ) + + runTest( + "exponential", + exponentialDist, + String(`█▅▄▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁`), + ) +}) + +describe("toPointSet", () => { + test("on symbolic normal distribution", () => { + let result = + run(FromDist(ToDist(ToPointSet), normalDist5)) + ->outputMap(FromDist(ToFloat(#Mean))) + ->toFloat + ->toExt + expect(result)->toBeSoCloseTo(5.0, ~digits=0) + }) + + test("on sample set", () => { + let result = + run(FromDist(ToDist(ToPointSet), normalDist5)) + ->outputMap(FromDist(ToDist(ToSampleSet(1000)))) + ->outputMap(FromDist(ToDist(ToPointSet))) + ->outputMap(FromDist(ToFloat(#Mean))) + ->toFloat + ->toExt + expect(result)->toBeSoCloseTo(5.0, ~digits=-1) + }) +}) diff --git a/packages/squiggle-lang/__tests__/Distributions/GenericDist_Fixtures.res b/packages/squiggle-lang/__tests__/Distributions/GenericDist_Fixtures.res new file mode 100644 index 00000000..05f0981b --- /dev/null +++ b/packages/squiggle-lang/__tests__/Distributions/GenericDist_Fixtures.res @@ -0,0 +1,11 @@ +let normalDist5: GenericDist_Types.genericDist = Symbolic(#Normal({mean: 5.0, stdev: 2.0})) +let normalDist10: GenericDist_Types.genericDist = Symbolic(#Normal({mean: 10.0, stdev: 2.0})) +let normalDist20: GenericDist_Types.genericDist = Symbolic(#Normal({mean: 20.0, stdev: 2.0})) +let normalDist: GenericDist_Types.genericDist = normalDist5 + +let betaDist: GenericDist_Types.genericDist = Symbolic(#Beta({alpha: 2.0, beta: 5.0})) +let lognormalDist: GenericDist_Types.genericDist = Symbolic(#Lognormal({mu: 0.0, sigma: 1.0})) +let cauchyDist: GenericDist_Types.genericDist = Symbolic(#Cauchy({local: 1.0, scale: 1.0})) +let triangularDist: GenericDist_Types.genericDist = Symbolic(#Triangular({low: 1.0, medium: 2.0, high: 3.0})) +let exponentialDist: GenericDist_Types.genericDist = Symbolic(#Exponential({rate: 2.0})) +let uniformDist: GenericDist_Types.genericDist = Symbolic(#Uniform({low: 9.0, high: 10.0})) diff --git a/packages/squiggle-lang/__tests__/Distributions/Mixture_test.res b/packages/squiggle-lang/__tests__/Distributions/Mixture_test.res new file mode 100644 index 00000000..2ab1de08 --- /dev/null +++ b/packages/squiggle-lang/__tests__/Distributions/Mixture_test.res @@ -0,0 +1,70 @@ +open Jest +open Expect +open TestHelpers + +// TODO: use Normal.make (etc.), but preferably after the new validation dispatch is in. +let mkNormal = (mean, stdev) => GenericDist_Types.Symbolic(#Normal({mean: mean, stdev: stdev})) +let mkBeta = (alpha, beta) => GenericDist_Types.Symbolic(#Beta({alpha: alpha, beta: beta})) +let mkExponential = rate => GenericDist_Types.Symbolic(#Exponential({rate: rate})) +let mkUniform = (low, high) => GenericDist_Types.Symbolic(#Uniform({low: low, high: high})) +let mkCauchy = (local, scale) => GenericDist_Types.Symbolic(#Cauchy({local: local, scale: scale})) +let mkLognormal = (mu, sigma) => GenericDist_Types.Symbolic(#Lognormal({mu: mu, sigma: sigma})) + +describe("mixture", () => { + testAll("fair mean of two normal distributions", list{(0.0, 1e2), (-1e1, -1e-4), (-1e1, 1e2), (-1e1, 1e1)}, tup => { // should be property + let (mean1, mean2) = tup + let meanValue = { + run(Mixture([(mkNormal(mean1, 9e-1), 0.5), (mkNormal(mean2, 9e-1), 0.5)])) + -> outputMap(FromDist(ToFloat(#Mean))) + } + meanValue -> unpackFloat -> expect -> toBeSoCloseTo((mean1 +. mean2) /. 2.0, ~digits=-1) + }) + testAll( + "weighted mean of a beta and an exponential", + // This would not survive property testing, it was easy for me to find cases that NaN'd out. + list{((128.0, 1.0), 2.0), ((2e-1, 64.0), 16.0), ((1e0, 1e0), 64.0)}, + tup => { + let ((alpha, beta), rate) = tup + let betaWeight = 0.25 + let exponentialWeight = 0.75 + let meanValue = { + run(Mixture( + [ + (mkBeta(alpha, beta), betaWeight), + (mkExponential(rate), exponentialWeight) + ] + )) -> outputMap(FromDist(ToFloat(#Mean))) + } + let betaMean = 1.0 /. (1.0 +. beta /. alpha) + let exponentialMean = 1.0 /. rate + meanValue + -> unpackFloat + -> expect + -> toBeSoCloseTo( + betaWeight *. betaMean +. exponentialWeight *. exponentialMean, + ~digits=-1 + ) + } + ) + testAll( + "weighted mean of lognormal and uniform", + // Would not survive property tests: very easy to find cases that NaN out. + list{((-1e2,1e1), (2e0,1e0)), ((-1e-16,1e-16), (1e-8,1e0)), ((0.0,1e0), (1e0,1e-2))}, + tup => { + let ((low, high), (mu, sigma)) = tup + let uniformWeight = 0.6 + let lognormalWeight = 0.4 + let meanValue = { + run(Mixture([(mkUniform(low, high), uniformWeight), (mkLognormal(mu, sigma), lognormalWeight)])) + -> outputMap(FromDist(ToFloat(#Mean))) + } + let uniformMean = (low +. high) /. 2.0 + let lognormalMean = mu +. sigma ** 2.0 /. 2.0 + meanValue + -> unpackFloat + -> expect + -> toBeSoCloseTo(uniformWeight *. uniformMean +. lognormalWeight *. lognormalMean, ~digits=-1) + } + ) +}) + diff --git a/packages/squiggle-lang/__tests__/Distributions/SampleSetDist_test.res b/packages/squiggle-lang/__tests__/Distributions/SampleSetDist_test.res new file mode 100644 index 00000000..5a48dd80 --- /dev/null +++ b/packages/squiggle-lang/__tests__/Distributions/SampleSetDist_test.res @@ -0,0 +1,41 @@ +open Jest +open TestHelpers + +describe("Continuous and discrete splits", () => { + makeTest( + "splits (1)", + SampleSetDist_ToPointSet.Internals.T.splitContinuousAndDiscrete([1.432, 1.33455, 2.0]), + ([1.432, 1.33455, 2.0], E.FloatFloatMap.empty()), + ) + makeTest( + "splits (2)", + SampleSetDist_ToPointSet.Internals.T.splitContinuousAndDiscrete([ + 1.432, + 1.33455, + 2.0, + 2.0, + 2.0, + 2.0, + ]) |> (((c, disc)) => (c, disc |> E.FloatFloatMap.toArray)), + ([1.432, 1.33455], [(2.0, 4.0)]), + ) + + let makeDuplicatedArray = count => { + let arr = Belt.Array.range(1, count) |> E.A.fmap(float_of_int) + let sorted = arr |> Belt.SortArray.stableSortBy(_, compare) + E.A.concatMany([sorted, sorted, sorted, sorted]) |> Belt.SortArray.stableSortBy(_, compare) + } + + let (_, discrete1) = SampleSetDist_ToPointSet.Internals.T.splitContinuousAndDiscrete( + makeDuplicatedArray(10), + ) + let toArr1 = discrete1 |> E.FloatFloatMap.toArray + makeTest("splitMedium at count=10", toArr1 |> Belt.Array.length, 10) + + let (_c, discrete2) = SampleSetDist_ToPointSet.Internals.T.splitContinuousAndDiscrete( + makeDuplicatedArray(500), + ) + let toArr2 = discrete2 |> E.FloatFloatMap.toArray + makeTest("splitMedium at count=500", toArr2 |> Belt.Array.length, 500) +}) + diff --git a/packages/squiggle-lang/__tests__/Distributions/Symbolic_test.res b/packages/squiggle-lang/__tests__/Distributions/Symbolic_test.res new file mode 100644 index 00000000..9a37a63a --- /dev/null +++ b/packages/squiggle-lang/__tests__/Distributions/Symbolic_test.res @@ -0,0 +1,161 @@ +open Jest +open Expect +open TestHelpers + +// TODO: use Normal.make (but preferably after teh new validation dispatch is in) +let mkNormal = (mean, stdev) => GenericDist_Types.Symbolic(#Normal({mean: mean, stdev: stdev})) + +describe("(Symbolic) normalize", () => { + testAll("has no impact on normal distributions", list{-1e8, -1e-2, 0.0, 1e-4, 1e16}, mean => { + let normalValue = mkNormal(mean, 2.0) + let normalizedValue = run(FromDist(ToDist(Normalize), normalValue)) + normalizedValue + -> unpackDist + -> expect + -> toEqual(normalValue) + }) +}) + +describe("(Symbolic) mean", () => { + testAll("of normal distributions", list{-1e8, -16.0, -1e-2, 0.0, 1e-4, 32.0, 1e16}, mean => { + run(FromDist(ToFloat(#Mean), mkNormal(mean, 4.0))) + -> unpackFloat + -> expect + -> toBeCloseTo(mean) + }) + + Skip.test("of normal(0, -1) (it NaNs out)", () => { + run(FromDist(ToFloat(#Mean), mkNormal(1e1, -1e0))) + -> unpackFloat + -> expect + -> ExpectJs.toBeFalsy + }) + + test("of normal(0, 1e-8) (it doesn't freak out at tiny stdev)", () => { + run(FromDist(ToFloat(#Mean), mkNormal(0.0, 1e-8))) + -> unpackFloat + -> expect + -> toBeCloseTo(0.0) + }) + + testAll("of exponential distributions", list{1e-7, 2.0, 10.0, 100.0}, rate => { + let meanValue = run(FromDist(ToFloat(#Mean), GenericDist_Types.Symbolic(#Exponential({rate: rate})))) + meanValue -> unpackFloat -> expect -> toBeCloseTo(1.0 /. rate) // https://en.wikipedia.org/wiki/Exponential_distribution#Mean,_variance,_moments,_and_median + }) + + test("of a cauchy distribution", () => { + let meanValue = run(FromDist(ToFloat(#Mean), GenericDist_Types.Symbolic(#Cauchy({local: 1.0, scale: 1.0})))) + meanValue + -> unpackFloat + -> expect + -> toBeCloseTo(2.01868297874546) + //-> toBe(GenDistError(Other("Cauchy distributions may have no mean value."))) + }) + + testAll("of triangular distributions", list{(1.0,2.0,3.0), (-1e7,-1e-7,1e-7), (-1e-7,1e0,1e7), (-1e-16,0.0,1e-16)}, tup => { + let (low, medium, high) = tup + let meanValue = run(FromDist( + ToFloat(#Mean), + GenericDist_Types.Symbolic(#Triangular({low: low, medium: medium, high: high})) + )) + meanValue + -> unpackFloat + -> expect + -> toBeCloseTo((low +. medium +. high) /. 3.0) // https://www.statology.org/triangular-distribution/ + }) + + // TODO: nonpositive inputs are SUPPOSED to crash. + testAll("of beta distributions", list{(1e-4, 6.4e1), (1.28e2, 1e0), (1e-16, 1e-16), (1e16, 1e16), (-1e4, 1e1), (1e1, -1e4)}, tup => { + let (alpha, beta) = tup + let meanValue = run(FromDist( + ToFloat(#Mean), + GenericDist_Types.Symbolic(#Beta({alpha: alpha, beta: beta})) + )) + meanValue + -> unpackFloat + -> expect + -> toBeCloseTo(1.0 /. (1.0 +. (beta /. alpha))) // https://en.wikipedia.org/wiki/Beta_distribution#Mean + }) + + // TODO: When we have our theory of validators we won't want this to be NaN but to be an error. + test("of beta(0, 0)", () => { + let meanValue = run(FromDist( + ToFloat(#Mean), + GenericDist_Types.Symbolic(#Beta({alpha: 0.0, beta: 0.0})) + )) + meanValue + -> unpackFloat + -> expect + -> ExpectJs.toBeFalsy + }) + + testAll("of lognormal distributions", list{(2.0, 4.0), (1e-7, 1e-2), (-1e6, 10.0), (1e3, -1e2), (-1e8, -1e4), (1e2, 1e-5)}, tup => { + let (mu, sigma) = tup + let meanValue = run(FromDist( + ToFloat(#Mean), + GenericDist_Types.Symbolic(#Lognormal({mu: mu, sigma: sigma})) + )) + meanValue + -> unpackFloat + -> expect + -> toBeCloseTo(Js.Math.exp(mu +. sigma ** 2.0 /. 2.0 )) // https://brilliant.org/wiki/log-normal-distribution/ + }) + + testAll("of uniform distributions", list{(1e-5, 12.345), (-1e4, 1e4), (-1e16, -1e2), (5.3e3, 9e9)}, tup => { + let (low, high) = tup + let meanValue = run(FromDist( + ToFloat(#Mean), + GenericDist_Types.Symbolic(#Uniform({low: low, high: high})) + )) + meanValue + -> unpackFloat + -> expect + -> toBeCloseTo((low +. high) /. 2.0) // https://en.wikipedia.org/wiki/Continuous_uniform_distribution#Moments + }) + + test("of a float", () => { + let meanValue = run(FromDist( + ToFloat(#Mean), + GenericDist_Types.Symbolic(#Float(7.7)) + )) + meanValue -> unpackFloat -> expect -> toBeCloseTo(7.7) + }) +}) + +describe("Normal distribution with sparklines", () => { + + let parameterWiseAdditionPdf = (n1: SymbolicDistTypes.normal, n2: SymbolicDistTypes.normal) => { + let normalDistAtSumMeanConstr = SymbolicDist.Normal.add(n1, n2) + let normalDistAtSumMean: SymbolicDistTypes.normal = switch normalDistAtSumMeanConstr { + | #Normal(params) => params + } + x => SymbolicDist.Normal.pdf(x, normalDistAtSumMean) + } + + let normalDistAtMean5: SymbolicDistTypes.normal = {mean: 5.0, stdev: 2.0} + let normalDistAtMean10: SymbolicDistTypes.normal = {mean: 10.0, stdev: 2.0} + let range20Float = E.A.Floats.range(0.0, 20.0, 20) // [0.0,1.0,2.0,3.0,4.0,...19.0,] + + test("mean=5 pdf", () => { + let pdfNormalDistAtMean5 = x => SymbolicDist.Normal.pdf(x, normalDistAtMean5) + let sparklineMean5 = fnImage(pdfNormalDistAtMean5, range20Float) + Sparklines.create(sparklineMean5, ()) + -> expect + -> toEqual(`▁▂▃▆██▇▅▂▁▁▁▁▁▁▁▁▁▁▁`) + }) + + test("parameter-wise addition of two normal distributions", () => { + let sparklineMean15 = normalDistAtMean5 -> parameterWiseAdditionPdf(normalDistAtMean10) -> fnImage(range20Float) + Sparklines.create(sparklineMean15, ()) + -> expect + -> toEqual(`▁▁▁▁▁▁▁▁▁▂▃▄▆███▇▅▄▂`) + }) + + test("mean=10 cdf", () => { + let cdfNormalDistAtMean10 = x => SymbolicDist.Normal.cdf(x, normalDistAtMean10) + let sparklineMean10 = fnImage(cdfNormalDistAtMean10, range20Float) + Sparklines.create(sparklineMean10, ()) + -> expect + -> toEqual(`▁▁▁▁▁▁▁▁▂▄▅▇████████`) + }) +}) diff --git a/packages/squiggle-lang/__tests__/GenericDist/GenericOperation__Test.res b/packages/squiggle-lang/__tests__/GenericDist/GenericOperation__Test.res deleted file mode 100644 index dc456865..00000000 --- a/packages/squiggle-lang/__tests__/GenericDist/GenericOperation__Test.res +++ /dev/null @@ -1,76 +0,0 @@ -open Jest -open Expect - -let env: DistributionOperation.env = { - sampleCount: 100, - xyPointLength: 100, -} - -let normalDist5: GenericDist_Types.genericDist = Symbolic(#Normal({mean: 5.0, stdev: 2.0})) -let normalDist10: GenericDist_Types.genericDist = Symbolic(#Normal({mean: 10.0, stdev: 2.0})) -let normalDist20: GenericDist_Types.genericDist = Symbolic(#Normal({mean: 20.0, stdev: 2.0})) -let uniformDist: GenericDist_Types.genericDist = Symbolic(#Uniform({low: 9.0, high: 10.0})) - -let {toFloat, toDist, toString, toError} = module(DistributionOperation.Output) -let {run} = module(DistributionOperation) -let {fmap} = module(DistributionOperation.Output) -let run = run(~env) -let outputMap = fmap(~env) -let toExt: option<'a> => 'a = E.O.toExt( - "Should be impossible to reach (This error is in test file)", -) - -describe("normalize", () => { - test("has no impact on normal dist", () => { - let result = run(FromDist(ToDist(Normalize), normalDist5)) - expect(result)->toEqual(Dist(normalDist5)) - }) -}) - -describe("mean", () => { - test("for a normal distribution", () => { - let result = DistributionOperation.run(~env, FromDist(ToFloat(#Mean), normalDist5)) - expect(result)->toEqual(Float(5.0)) - }) -}) - -describe("mixture", () => { - test("on two normal distributions", () => { - let result = - run(Mixture([(normalDist10, 0.5), (normalDist20, 0.5)])) - ->outputMap(FromDist(ToFloat(#Mean))) - ->toFloat - ->toExt - expect(result)->toBeCloseTo(15.28) - }) -}) - -describe("toPointSet", () => { - test("on symbolic normal distribution", () => { - let result = - run(FromDist(ToDist(ToPointSet), normalDist5)) - ->outputMap(FromDist(ToFloat(#Mean))) - ->toFloat - ->toExt - expect(result)->toBeCloseTo(5.09) - }) - - test("on sample set distribution with under 4 points", () => { - let result = - run(FromDist(ToDist(ToPointSet), SampleSet([0.0, 1.0, 2.0, 3.0])))->outputMap( - FromDist(ToFloat(#Mean)), - ) - expect(result)->toEqual(GenDistError(Other("Converting sampleSet to pointSet failed"))) - }) - - Skip.test("on sample set", () => { - let result = - run(FromDist(ToDist(ToPointSet), normalDist5)) - ->outputMap(FromDist(ToDist(ToSampleSet(1000)))) - ->outputMap(FromDist(ToDist(ToPointSet))) - ->outputMap(FromDist(ToFloat(#Mean))) - ->toFloat - ->toExt - expect(result)->toBeCloseTo(5.09) - }) -}) diff --git a/packages/squiggle-lang/__tests__/JS__Test.ts b/packages/squiggle-lang/__tests__/JS__Test.ts index aded69c1..8e5961a3 100644 --- a/packages/squiggle-lang/__tests__/JS__Test.ts +++ b/packages/squiggle-lang/__tests__/JS__Test.ts @@ -1,34 +1,87 @@ -import { run } from '../src/js/index'; +import { run, GenericDist, resultMap, makeSampleSetDist } from "../src/js/index"; let testRun = (x: string) => { - let result = run(x) - if(result.tag == 'Ok'){ - return { tag: 'Ok', value: result.value.exports } + let result = run(x); + if (result.tag == "Ok") { + return { tag: "Ok", value: result.value.exports }; + } else { + return result; } - else { - return result - } -} +}; describe("Simple calculations and results", () => { - test("mean(normal(5,2))", () => { - expect(testRun("mean(normal(5,2))")).toEqual({ tag: 'Ok', value: [ { NAME: 'Float', VAL: 5 } ] }) - }) - test("10+10", () => { - let foo = testRun("10 + 10") - expect(foo).toEqual({ tag: 'Ok', value: [ { NAME: 'Float', VAL: 20 } ] }) - }) -}) + test("mean(normal(5,2))", () => { + expect(testRun("mean(normal(5,2))")).toEqual({ + tag: "Ok", + value: [{ NAME: "Float", VAL: 5 }], + }); + }); + test("10+10", () => { + let foo = testRun("10 + 10"); + expect(foo).toEqual({ tag: "Ok", value: [{ NAME: "Float", VAL: 20 }] }); + }); +}); describe("Log function", () => { - test("log(1) = 0", () => { - let foo = testRun("log(1)") - expect(foo).toEqual({ tag: 'Ok', value: [ { NAME: 'Float', VAL: 0} ]}) - }) -}) + test("log(1) = 0", () => { + let foo = testRun("log(1)"); + expect(foo).toEqual({ tag: "Ok", value: [{ NAME: "Float", VAL: 0 }] }); + }); +}); describe("Multimodal too many weights error", () => { - test("mm(0,0,[0,0,0])", () => { - let foo = testRun("mm(0,0,[0,0,0])") - expect(foo).toEqual({ "tag": "Error", "value": "Function multimodal error: Too many weights provided" }) - }) + test("mm(0,0,[0,0,0])", () => { + let foo = testRun("mm(0,0,[0,0,0])"); + expect(foo).toEqual({ + tag: "Error", + value: "Function multimodal error: Too many weights provided", + }); + }); +}); + +describe("GenericDist", () => { + + //It's important that sampleCount is less than 9. If it's more, than that will create randomness + //Also, note, the value should be created using makeSampleSetDist() later on. + let env = { sampleCount: 8, xyPointLength: 100 }; + let dist = new GenericDist( + { tag: "SampleSet", value: [3, 4, 5, 6, 6, 7, 10, 15, 30] }, + env + ); + let dist2 = new GenericDist( + { tag: "SampleSet", value: [20, 22, 24, 29, 30, 35, 38, 44, 52] }, + env + ); + + test("mean", () => { + expect(dist.mean().value).toBeCloseTo(3.737); + }); + test("pdf", () => { + expect(dist.pdf(5.0).value).toBeCloseTo(0.0431); + }); + test("cdf", () => { + expect(dist.cdf(5.0).value).toBeCloseTo(0.155); + }); + test("inv", () => { + expect(dist.inv(0.5).value).toBeCloseTo(9.458); + }); + test("toPointSet", () => { + expect( + resultMap(dist.toPointSet(), (r: GenericDist) => r.toString()).value.value + ).toBe("Point Set Distribution"); + }); + test("toSparkline", () => { + expect(dist.toSparkline(20).value).toBe("▁▁▃▅███▆▄▃▂▁▁▂▂▃▂▁▁▁"); + }); + test("algebraicAdd", () => { + expect( + resultMap(dist.algebraicAdd(dist2), (r: GenericDist) => r.toSparkline(20)) + .value.value + ).toBe("▁▁▂▄▆████▇▆▄▄▃▃▃▂▁▁▁"); + }); + test("pointwiseAdd", () => { + expect( + resultMap(dist.pointwiseAdd(dist2), (r: GenericDist) => r.toSparkline(20)) + .value.value + ).toBe("▁▂▅██▅▅▅▆▇█▆▅▃▃▂▂▁▁▁"); + }); }); diff --git a/packages/squiggle-lang/__tests__/Lodash__test.res b/packages/squiggle-lang/__tests__/Lodash_test.res similarity index 100% rename from packages/squiggle-lang/__tests__/Lodash__test.res rename to packages/squiggle-lang/__tests__/Lodash_test.res diff --git a/packages/squiggle-lang/__tests__/Reducer/Reducer_TestHelpers.res b/packages/squiggle-lang/__tests__/Reducer/Reducer_TestHelpers.res index 7029e2ff..65f689cc 100644 --- a/packages/squiggle-lang/__tests__/Reducer/Reducer_TestHelpers.res +++ b/packages/squiggle-lang/__tests__/Reducer/Reducer_TestHelpers.res @@ -9,6 +9,3 @@ let expectParseToBe = (expr: string, answer: string) => let expectEvalToBe = (expr: string, answer: string) => Reducer.eval(expr)->ExpressionValue.toStringResult->expect->toBe(answer) - -// Current configuration does not ignore this file so we have to have a test -test("test helpers", () => expect(1)->toBe(1)) diff --git a/packages/squiggle-lang/__tests__/ReducerInterface/ReducerInterface_Distribution_test.res b/packages/squiggle-lang/__tests__/ReducerInterface/ReducerInterface_Distribution_test.res index 85d80919..096ac360 100644 --- a/packages/squiggle-lang/__tests__/ReducerInterface/ReducerInterface_Distribution_test.res +++ b/packages/squiggle-lang/__tests__/ReducerInterface/ReducerInterface_Distribution_test.res @@ -30,6 +30,9 @@ describe("eval on distribution functions", () => { testEval("mean(normal(5,2))", "Ok(5)") testEval("mean(lognormal(1,2))", "Ok(20.085536923187668)") }) + describe("toString", () => { + testEval("toString(normal(5,2))", "Ok('Normal(5,2)')") + }) describe("normalize", () => { testEval("normalize(normal(5,2))", "Ok(Normal(5,2))") }) diff --git a/packages/squiggle-lang/__tests__/Samples__test.res b/packages/squiggle-lang/__tests__/Samples__test.res deleted file mode 100644 index 01c248f0..00000000 --- a/packages/squiggle-lang/__tests__/Samples__test.res +++ /dev/null @@ -1,47 +0,0 @@ -open Jest -open Expect - -let makeTest = (~only=false, str, item1, item2) => - only - ? Only.test(str, () => expect(item1) -> toEqual(item2)) - : test(str, () => expect(item1) -> toEqual(item2)) - -describe("Lodash", () => - describe("Lodash", () => { - makeTest( - "split", - SampleSet.Internals.T.splitContinuousAndDiscrete([1.432, 1.33455, 2.0]), - ([1.432, 1.33455, 2.0], E.FloatFloatMap.empty()), - ) - makeTest( - "split", - SampleSet.Internals.T.splitContinuousAndDiscrete([ - 1.432, - 1.33455, - 2.0, - 2.0, - 2.0, - 2.0, - ]) |> (((c, disc)) => (c, disc |> E.FloatFloatMap.toArray)), - ([1.432, 1.33455], [(2.0, 4.0)]), - ) - - let makeDuplicatedArray = count => { - let arr = Belt.Array.range(1, count) |> E.A.fmap(float_of_int) - let sorted = arr |> Belt.SortArray.stableSortBy(_, compare) - E.A.concatMany([sorted, sorted, sorted, sorted]) |> Belt.SortArray.stableSortBy(_, compare) - } - - let (_, discrete) = SampleSet.Internals.T.splitContinuousAndDiscrete( - makeDuplicatedArray(10), - ) - let toArr = discrete |> E.FloatFloatMap.toArray - makeTest("splitMedium", toArr |> Belt.Array.length, 10) - - let (_c, discrete) = SampleSet.Internals.T.splitContinuousAndDiscrete( - makeDuplicatedArray(500), - ) - let toArr = discrete |> E.FloatFloatMap.toArray - makeTest("splitMedium", toArr |> Belt.Array.length, 500) - }) -) diff --git a/packages/squiggle-lang/__tests__/Symbolic_test.res b/packages/squiggle-lang/__tests__/Symbolic_test.res deleted file mode 100644 index 8ec3be22..00000000 --- a/packages/squiggle-lang/__tests__/Symbolic_test.res +++ /dev/null @@ -1,33 +0,0 @@ -open Jest -open Expect -open Js.Array -open SymbolicDist - -let makeTest = (~only=false, str, item1, item2) => - only - ? Only.test(str, () => expect(item1) -> toEqual(item2)) - : test(str, () => expect(item1) -> toEqual(item2)) - -let pdfImage = (thePdf, inps) => map(thePdf, inps) - -let parameterWiseAdditionHelper = (n1: SymbolicDistTypes.normal, n2: SymbolicDistTypes.normal) => { - let normalDistAtSumMeanConstr = Normal.add(n1, n2) - let normalDistAtSumMean: SymbolicDistTypes.normal = switch normalDistAtSumMeanConstr { - | #Normal(params) => params - } - x => Normal.pdf(x, normalDistAtSumMean) -} - -describe("Normal distribution with sparklines", () => { - - let normalDistAtMean5: SymbolicDistTypes.normal = {mean: 5.0, stdev: 2.0} - let normalDistAtMean10: SymbolicDistTypes.normal = {mean: 10.0, stdev: 2.0} - let range20Float = E.A.rangeFloat(0, 20) // [0.0,1.0,2.0,3.0,4.0,...19.0,] - - let pdfNormalDistAtMean5 = x => Normal.pdf(x, normalDistAtMean5) - let sparklineMean5 = pdfImage(pdfNormalDistAtMean5, range20Float) - makeTest("mean=5", Sparklines.create(sparklineMean5, ()), `▁▂▃▅███▅▃▂▁▁▁▁▁▁▁▁▁▁▁`) - - let sparklineMean15 = normalDistAtMean5 -> parameterWiseAdditionHelper(normalDistAtMean10) -> pdfImage(range20Float) - makeTest("parameter-wise addition of two normal distributions", Sparklines.create(sparklineMean15, ()), `▁▁▁▁▁▁▁▁▁▁▂▃▅▇███▇▅▃▂`) -}) diff --git a/packages/squiggle-lang/__tests__/TestHelpers.res b/packages/squiggle-lang/__tests__/TestHelpers.res new file mode 100644 index 00000000..a61f57d0 --- /dev/null +++ b/packages/squiggle-lang/__tests__/TestHelpers.res @@ -0,0 +1,26 @@ +open Jest +open Expect + +let makeTest = (~only=false, str, item1, item2) => + only + ? Only.test(str, () => expect(item1) -> toEqual(item2)) + : test(str, () => expect(item1) -> toEqual(item2)) + + +let {toFloat, toDist, toString, toError, fmap} = module(DistributionOperation.Output) + +let fnImage = (theFn, inps) => Js.Array.map(theFn, inps) + +let env: DistributionOperation.env = { + sampleCount: 100, + xyPointLength: 100, +} + +let run = DistributionOperation.run(~env) +let outputMap = fmap(~env) +let unreachableInTestFileMessage = "Should be impossible to reach (This error is in test file)" +let toExtFloat: option => float = E.O.toExt(unreachableInTestFileMessage) +let toExtDist: option => GenericDist_Types.genericDist = E.O.toExt(unreachableInTestFileMessage) +// let toExt: option<'a> => 'a = E.O.toExt(unreachableInTestFileMessage) +let unpackFloat = x => x -> toFloat -> toExtFloat +let unpackDist = y => y -> toDist -> toExtDist diff --git a/packages/squiggle-lang/__tests__/XYShape__Test.res b/packages/squiggle-lang/__tests__/XYShape_test.res similarity index 100% rename from packages/squiggle-lang/__tests__/XYShape__Test.res rename to packages/squiggle-lang/__tests__/XYShape_test.res diff --git a/packages/squiggle-lang/bsconfig.json b/packages/squiggle-lang/bsconfig.json index 4f38e124..958a3af8 100644 --- a/packages/squiggle-lang/bsconfig.json +++ b/packages/squiggle-lang/bsconfig.json @@ -33,7 +33,7 @@ "gentypeconfig": { "language": "typescript", "module": "commonjs", - "shims": {}, + "shims": {"Js": "Js"}, "debug": { "all": false, "basic": false @@ -47,7 +47,7 @@ [ "../../node_modules/bisect_ppx/ppx", "--exclude-files", - ".*_Test\\.res$$" + ".*_test\\.res$$" ] ] } diff --git a/packages/squiggle-lang/jest.config.js b/packages/squiggle-lang/jest.config.js index 09bf05a8..3347de7c 100644 --- a/packages/squiggle-lang/jest.config.js +++ b/packages/squiggle-lang/jest.config.js @@ -1,8 +1,13 @@ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', + preset: "ts-jest", + testEnvironment: "node", setupFilesAfterEnv: [ - "/../../node_modules/bisect_ppx/src/runtime/js/jest.bs.js" + "/../../node_modules/bisect_ppx/src/runtime/js/jest.bs.js", + ], + testPathIgnorePatterns: [ + ".*Fixtures.bs.js", + "/node_modules/", + ".*Helpers.bs.js", ], }; diff --git a/packages/squiggle-lang/package.json b/packages/squiggle-lang/package.json index 459c3d2b..03983cd2 100644 --- a/packages/squiggle-lang/package.json +++ b/packages/squiggle-lang/package.json @@ -10,7 +10,7 @@ "test:reducer": "jest --testPathPattern '.*__tests__/Reducer.*'", "test": "jest", "test:watch": "jest --watchAll", - "coverage": "rm -f *.coverage; yarn clean; BISECT_ENABLE=yes yarn build; yarn test; ./node_modules/.bin/bisect-ppx-report html", + "coverage": "rm -f *.coverage; yarn clean; BISECT_ENABLE=yes yarn build; yarn test; bisect-ppx-report html", "all": "yarn build && yarn bundle && yarn test" }, "keywords": [ diff --git a/packages/squiggle-lang/src/js/index.ts b/packages/squiggle-lang/src/js/index.ts index 7856ef6f..4892c2f3 100644 --- a/packages/squiggle-lang/src/js/index.ts +++ b/packages/squiggle-lang/src/js/index.ts @@ -1,17 +1,228 @@ -import {runAll} from '../rescript/ProgramEvaluator.gen'; -import type { Inputs_SamplingInputs_t as SamplingInputs, exportEnv, exportType, exportDistribution} from '../rescript/ProgramEvaluator.gen'; -export type { SamplingInputs, exportEnv, exportDistribution } -export type {t as DistPlus} from '../rescript/OldInterpreter/DistPlus.gen'; +import { runAll } from "../rescript/ProgramEvaluator.gen"; +import type { + Inputs_SamplingInputs_t as SamplingInputs, + exportEnv, + exportType, + exportDistribution, +} from "../rescript/ProgramEvaluator.gen"; +export type { SamplingInputs, exportEnv, exportDistribution }; +export type { t as DistPlus } from "../rescript/OldInterpreter/DistPlus.gen"; +import { + genericDist, + env, + resultDist, + resultFloat, + resultString, +} from "../rescript/TypescriptInterface.gen"; +export {makeSampleSetDist} from "../rescript/TypescriptInterface.gen"; +import { + Constructors_mean, + Constructors_sample, + Constructors_pdf, + Constructors_cdf, + Constructors_inv, + Constructors_normalize, + Constructors_toPointSet, + Constructors_toSampleSet, + Constructors_truncate, + Constructors_inspect, + Constructors_toString, + Constructors_toSparkline, + Constructors_algebraicAdd, + Constructors_algebraicMultiply, + Constructors_algebraicDivide, + Constructors_algebraicSubtract, + Constructors_algebraicLogarithm, + Constructors_algebraicPower, + Constructors_pointwiseAdd, + Constructors_pointwiseMultiply, + Constructors_pointwiseDivide, + Constructors_pointwiseSubtract, + Constructors_pointwiseLogarithm, + Constructors_pointwisePower, +} from "../rescript/Distributions/DistributionOperation/DistributionOperation.gen"; -export let defaultSamplingInputs : SamplingInputs = { - sampleCount : 10000, - outputXYPoints : 10000, - pointDistLength : 1000 +export let defaultSamplingInputs: SamplingInputs = { + sampleCount: 10000, + outputXYPoints: 10000, + pointDistLength: 1000, +}; + +export function run( + squiggleString: string, + samplingInputs?: SamplingInputs, + environment?: exportEnv +): { tag: "Ok"; value: exportType } | { tag: "Error"; value: string } { + let si: SamplingInputs = samplingInputs + ? samplingInputs + : defaultSamplingInputs; + let env: exportEnv = environment ? environment : []; + return runAll(squiggleString, si, env); } -export function run(squiggleString : string, samplingInputs? : SamplingInputs, environment?: exportEnv) : { tag: "Ok"; value: exportType } - | { tag: "Error"; value: string } { - let si : SamplingInputs = samplingInputs ? samplingInputs : defaultSamplingInputs - let env : exportEnv = environment ? environment : [] - return runAll(squiggleString, si, env) +//This is clearly not fully typed. I think later we should use a functional library to +// provide a better Either type and corresponding functions. +type result = + | { + tag: "Ok"; + value: any; + } + | { + tag: "Error"; + value: any; + }; + +export function resultMap(r: result, mapFn: any): result { + if (r.tag === "Ok") { + return { tag: "Ok", value: mapFn(r.value) }; + } else { + return r; + } +} + +export function resultExn(r: result): any { + r.value +} + +export class GenericDist { + t: genericDist; + env: env; + + constructor(t: genericDist, env: env) { + this.t = t; + this.env = env; + return this; + } + + mapResultDist(r: resultDist) { + return resultMap(r, (v: genericDist) => new GenericDist(v, this.env)); + } + + mean() { + return Constructors_mean({ env: this.env }, this.t); + } + + sample(): resultFloat { + return Constructors_sample({ env: this.env }, this.t); + } + + pdf(n: number): resultFloat { + return Constructors_pdf({ env: this.env }, this.t, n); + } + + cdf(n: number): resultFloat { + return Constructors_cdf({ env: this.env }, this.t, n); + } + + inv(n: number): resultFloat { + return Constructors_inv({ env: this.env }, this.t, n); + } + + normalize() { + return this.mapResultDist( + Constructors_normalize({ env: this.env }, this.t) + ); + } + + toPointSet() { + return this.mapResultDist( + Constructors_toPointSet({ env: this.env }, this.t) + ); + } + + toSampleSet(n: number) { + return this.mapResultDist( + Constructors_toSampleSet({ env: this.env }, this.t, n) + ); + } + + truncate(left: number, right: number) { + return this.mapResultDist( + Constructors_truncate({ env: this.env }, this.t, left, right) + ); + } + + inspect() { + return this.mapResultDist(Constructors_inspect({ env: this.env }, this.t)); + } + + toString(): resultString { + return Constructors_toString({ env: this.env }, this.t); + } + + toSparkline(n: number): resultString { + return Constructors_toSparkline({ env: this.env }, this.t, n); + } + + algebraicAdd(d2: GenericDist) { + return this.mapResultDist( + Constructors_algebraicAdd({ env: this.env }, this.t, d2.t) + ); + } + + algebraicMultiply(d2: GenericDist) { + return this.mapResultDist( + Constructors_algebraicMultiply({ env: this.env }, this.t, d2.t) + ); + } + + algebraicDivide(d2: GenericDist) { + return this.mapResultDist( + Constructors_algebraicDivide({ env: this.env }, this.t, d2.t) + ); + } + + algebraicSubtract(d2: GenericDist) { + return this.mapResultDist( + Constructors_algebraicSubtract({ env: this.env }, this.t, d2.t) + ); + } + + algebraicLogarithm(d2: GenericDist) { + return this.mapResultDist( + Constructors_algebraicLogarithm({ env: this.env }, this.t, d2.t) + ); + } + + algebraicPower(d2: GenericDist) { + return this.mapResultDist( + Constructors_algebraicPower({ env: this.env }, this.t, d2.t) + ); + } + + pointwiseAdd(d2: GenericDist) { + return this.mapResultDist( + Constructors_pointwiseAdd({ env: this.env }, this.t, d2.t) + ); + } + + pointwiseMultiply(d2: GenericDist) { + return this.mapResultDist( + Constructors_pointwiseMultiply({ env: this.env }, this.t, d2.t) + ); + } + + pointwiseDivide(d2: GenericDist) { + return this.mapResultDist( + Constructors_pointwiseDivide({ env: this.env }, this.t, d2.t) + ); + } + + pointwiseSubtract(d2: GenericDist) { + return this.mapResultDist( + Constructors_pointwiseSubtract({ env: this.env }, this.t, d2.t) + ); + } + + pointwiseLogarithm(d2: GenericDist) { + return this.mapResultDist( + Constructors_pointwiseLogarithm({ env: this.env }, this.t, d2.t) + ); + } + + pointwisePower(d2: GenericDist) { + return this.mapResultDist( + Constructors_pointwisePower({ env: this.env }, this.t, d2.t) + ); + } } diff --git a/packages/squiggle-lang/src/rescript/Distributions/DistributionOperation/DistributionOperation.res b/packages/squiggle-lang/src/rescript/Distributions/DistributionOperation/DistributionOperation.res index 351389ae..71776f61 100644 --- a/packages/squiggle-lang/src/rescript/Distributions/DistributionOperation/DistributionOperation.res +++ b/packages/squiggle-lang/src/rescript/Distributions/DistributionOperation/DistributionOperation.res @@ -10,10 +10,10 @@ type env = { } type outputType = - | Dist(GenericDist_Types.genericDist) + | Dist(genericDist) | Float(float) | String(string) - | GenDistError(GenericDist_Types.error) + | GenDistError(error) /* We're going to add another function to this module later, so first define a @@ -113,7 +113,11 @@ let rec run = (~env, functionCallInfo: functionCallInfo): outputType => { GenericDist.toFloatOperation(dist, ~toPointSetFn, ~distToFloatOperation) ->E.R2.fmap(r => Float(r)) ->OutputLocal.fromResult - | ToString => dist->GenericDist.toString->String + | ToString(ToString) => dist->GenericDist.toString->String + | ToString(ToSparkline(bucketCount)) => + GenericDist.toSparkline(dist, ~sampleCount, ~bucketCount, ()) + ->E.R2.fmap(r => String(r)) + ->OutputLocal.fromResult | ToDist(Inspect) => { Js.log2("Console log requested: ", dist) Dist(dist) @@ -124,10 +128,13 @@ let rec run = (~env, functionCallInfo: functionCallInfo): outputType => { ->E.R2.fmap(r => Dist(r)) ->OutputLocal.fromResult | ToDist(ToSampleSet(n)) => - dist->GenericDist.sampleN(n)->E.R2.fmap(r => Dist(SampleSet(r)))->OutputLocal.fromResult + dist + ->GenericDist.toSampleSetDist(n) + ->E.R2.fmap(r => Dist(SampleSet(r))) + ->OutputLocal.fromResult | ToDist(ToPointSet) => dist - ->GenericDist.toPointSet(~xyPointLength, ~sampleCount) + ->GenericDist.toPointSet(~xyPointLength, ~sampleCount, ()) ->E.R2.fmap(r => Dist(PointSet(r))) ->OutputLocal.fromResult | ToDistCombination(Algebraic, _, #Float(_)) => GenDistError(NotYetImplemented) @@ -181,3 +188,43 @@ module Output = { newFnCall->E.R2.fmap(run(~env))->OutputLocal.fromResult } } + +// See comment above GenericDist_Types.Constructors to explain the purpose of this module. +// I tried having another internal module called UsingDists, similar to how its done in +// GenericDist_Types.Constructors. However, this broke GenType for me, so beware. +module Constructors = { + module C = GenericDist_Types.Constructors.UsingDists + open OutputLocal + let mean = (~env, dist) => C.mean(dist)->run(~env)->toFloatR + let sample = (~env, dist) => C.sample(dist)->run(~env)->toFloatR + let cdf = (~env, dist, f) => C.cdf(dist, f)->run(~env)->toFloatR + let inv = (~env, dist, f) => C.inv(dist, f)->run(~env)->toFloatR + let pdf = (~env, dist, f) => C.pdf(dist, f)->run(~env)->toFloatR + let normalize = (~env, dist) => C.normalize(dist)->run(~env)->toDistR + let toPointSet = (~env, dist) => C.toPointSet(dist)->run(~env)->toDistR + let toSampleSet = (~env, dist, n) => C.toSampleSet(dist, n)->run(~env)->toDistR + let truncate = (~env, dist, leftCutoff, rightCutoff) => + C.truncate(dist, leftCutoff, rightCutoff)->run(~env)->toDistR + let inspect = (~env, dist) => C.inspect(dist)->run(~env)->toDistR + let toString = (~env, dist) => C.toString(dist)->run(~env)->toStringR + let toSparkline = (~env, dist, bucketCount) => + C.toSparkline(dist, bucketCount)->run(~env)->toStringR + let algebraicAdd = (~env, dist1, dist2) => C.algebraicAdd(dist1, dist2)->run(~env)->toDistR + let algebraicMultiply = (~env, dist1, dist2) => + C.algebraicMultiply(dist1, dist2)->run(~env)->toDistR + let algebraicDivide = (~env, dist1, dist2) => C.algebraicDivide(dist1, dist2)->run(~env)->toDistR + let algebraicSubtract = (~env, dist1, dist2) => + C.algebraicSubtract(dist1, dist2)->run(~env)->toDistR + let algebraicLogarithm = (~env, dist1, dist2) => + C.algebraicLogarithm(dist1, dist2)->run(~env)->toDistR + let algebraicPower = (~env, dist1, dist2) => C.algebraicPower(dist1, dist2)->run(~env)->toDistR + let pointwiseAdd = (~env, dist1, dist2) => C.pointwiseAdd(dist1, dist2)->run(~env)->toDistR + let pointwiseMultiply = (~env, dist1, dist2) => + C.pointwiseMultiply(dist1, dist2)->run(~env)->toDistR + let pointwiseDivide = (~env, dist1, dist2) => C.pointwiseDivide(dist1, dist2)->run(~env)->toDistR + let pointwiseSubtract = (~env, dist1, dist2) => + C.pointwiseSubtract(dist1, dist2)->run(~env)->toDistR + let pointwiseLogarithm = (~env, dist1, dist2) => + C.pointwiseLogarithm(dist1, dist2)->run(~env)->toDistR + let pointwisePower = (~env, dist1, dist2) => C.pointwisePower(dist1, dist2)->run(~env)->toDistR +} diff --git a/packages/squiggle-lang/src/rescript/Distributions/DistributionOperation/DistributionOperation.resi b/packages/squiggle-lang/src/rescript/Distributions/DistributionOperation/DistributionOperation.resi index 3c3e132a..bfe45013 100644 --- a/packages/squiggle-lang/src/rescript/Distributions/DistributionOperation/DistributionOperation.resi +++ b/packages/squiggle-lang/src/rescript/Distributions/DistributionOperation/DistributionOperation.resi @@ -1,19 +1,24 @@ +@genType type env = { sampleCount: int, xyPointLength: int, } +open GenericDist_Types + +@genType type outputType = - | Dist(GenericDist_Types.genericDist) + | Dist(genericDist) | Float(float) | String(string) - | GenDistError(GenericDist_Types.error) + | GenDistError(error) +@genType let run: (~env: env, GenericDist_Types.Operation.genericFunctionCallInfo) => outputType let runFromDist: ( ~env: env, ~functionCallInfo: GenericDist_Types.Operation.fromDist, - GenericDist_Types.genericDist, + genericDist, ) => outputType let runFromFloat: ( ~env: env, @@ -23,12 +28,68 @@ let runFromFloat: ( module Output: { type t = outputType - let toDist: t => option - let toDistR: t => result + let toDist: t => option + let toDistR: t => result let toFloat: t => option - let toFloatR: t => result + let toFloatR: t => result let toString: t => option - let toStringR: t => result - let toError: t => option + let toStringR: t => result + let toError: t => option let fmap: (~env: env, t, GenericDist_Types.Operation.singleParamaterFunction) => t } + +module Constructors: { + @genType + let mean: (~env: env, genericDist) => result + @genType + let sample: (~env: env, genericDist) => result + @genType + let cdf: (~env: env, genericDist, float) => result + @genType + let inv: (~env: env, genericDist, float) => result + @genType + let pdf: (~env: env, genericDist, float) => result + @genType + let normalize: (~env: env, genericDist) => result + @genType + let toPointSet: (~env: env, genericDist) => result + @genType + let toSampleSet: (~env: env, genericDist, int) => result + @genType + let truncate: ( + ~env: env, + genericDist, + option, + option, + ) => result + @genType + let inspect: (~env: env, genericDist) => result + @genType + let toString: (~env: env, genericDist) => result + @genType + let toSparkline: (~env: env, genericDist, int) => result + @genType + let algebraicAdd: (~env: env, genericDist, genericDist) => result + @genType + let algebraicMultiply: (~env: env, genericDist, genericDist) => result + @genType + let algebraicDivide: (~env: env, genericDist, genericDist) => result + @genType + let algebraicSubtract: (~env: env, genericDist, genericDist) => result + @genType + let algebraicLogarithm: (~env: env, genericDist, genericDist) => result + @genType + let algebraicPower: (~env: env, genericDist, genericDist) => result + @genType + let pointwiseAdd: (~env: env, genericDist, genericDist) => result + @genType + let pointwiseMultiply: (~env: env, genericDist, genericDist) => result + @genType + let pointwiseDivide: (~env: env, genericDist, genericDist) => result + @genType + let pointwiseSubtract: (~env: env, genericDist, genericDist) => result + @genType + let pointwiseLogarithm: (~env: env, genericDist, genericDist) => result + @genType + let pointwisePower: (~env: env, genericDist, genericDist) => result +} diff --git a/packages/squiggle-lang/src/rescript/Distributions/DistributionTypes.res b/packages/squiggle-lang/src/rescript/Distributions/DistributionTypes.res index a3e249d3..cab58839 100644 --- a/packages/squiggle-lang/src/rescript/Distributions/DistributionTypes.res +++ b/packages/squiggle-lang/src/rescript/Distributions/DistributionTypes.res @@ -19,7 +19,7 @@ module Operation = { | #Multiply | #Subtract | #Divide - | #Exponentiate + | #Power | #Logarithm ] @@ -28,7 +28,7 @@ module Operation = { | #Add => \"+." | #Multiply => \"*." | #Subtract => \"-." - | #Exponentiate => \"**" + | #Power => \"**" | #Divide => \"/." | #Logarithm => (a, b) => log(a) /. log(b) } diff --git a/packages/squiggle-lang/src/rescript/Distributions/GenericDist/GenericDist.res b/packages/squiggle-lang/src/rescript/Distributions/GenericDist/GenericDist.res index f71d9b93..b14ea27f 100644 --- a/packages/squiggle-lang/src/rescript/Distributions/GenericDist/GenericDist.res +++ b/packages/squiggle-lang/src/rescript/Distributions/GenericDist/GenericDist.res @@ -2,17 +2,20 @@ type t = GenericDist_Types.genericDist type error = GenericDist_Types.error type toPointSetFn = t => result -type toSampleSetFn = t => result, error> +type toSampleSetFn = t => result type scaleMultiplyFn = (t, float) => result type pointwiseAddFn = (t, t) => result let sampleN = (t: t, n) => switch t { - | PointSet(r) => Ok(PointSetDist.sampleNRendered(n, r)) - | Symbolic(r) => Ok(SymbolicDist.T.sampleN(n, r)) - | SampleSet(_) => Error(GenericDist_Types.NotYetImplemented) + | PointSet(r) => PointSetDist.sampleNRendered(n, r) + | Symbolic(r) => SymbolicDist.T.sampleN(n, r) + | SampleSet(r) => SampleSetDist.sampleN(r, n) } +let toSampleSetDist = (t: t, n) => + SampleSetDist.make(sampleN(t, n))->GenericDist_Types.Error.resultStringToResultError + let fromFloat = (f: float): t => Symbolic(SymbolicDist.Float.make(f)) let toString = (t: t) => @@ -49,32 +52,45 @@ let toFloatOperation = ( } } -//Todo: If it's a pointSet, but the xyPointLenght is different from what it has, it should change. +//Todo: If it's a pointSet, but the xyPointLength is different from what it has, it should change. // This is tricky because the case of discrete distributions. // Also, change the outputXYPoints/pointSetDistLength details -let toPointSet = (~xyPointLength, ~sampleCount, t): result => { +let toPointSet = ( + t, + ~xyPointLength, + ~sampleCount, + ~xSelection: GenericDist_Types.Operation.pointsetXSelection=#ByWeight, + unit, +): result => { switch (t: t) { | PointSet(pointSet) => Ok(pointSet) - | Symbolic(r) => Ok(SymbolicDist.T.toPointSetDist(xyPointLength, r)) - | SampleSet(r) => { - let response = SampleSet.toPointSetDist( - ~samples=r, - ~samplingInputs={ - sampleCount: sampleCount, - outputXYPoints: xyPointLength, - pointSetDistLength: xyPointLength, - kernelWidth: None, - }, - (), - ).pointSetDist - switch response { - | Some(r) => Ok(r) - | None => Error(Other("Converting sampleSet to pointSet failed")) - } - } + | Symbolic(r) => Ok(SymbolicDist.T.toPointSetDist(~xSelection, xyPointLength, r)) + | SampleSet(r) => + SampleSetDist.toPointSetDist( + ~samples=r, + ~samplingInputs={ + sampleCount: sampleCount, + outputXYPoints: xyPointLength, + pointSetDistLength: xyPointLength, + kernelWidth: None, + }, + )->GenericDist_Types.Error.resultStringToResultError } } +/* + PointSetDist.toSparkline calls "downsampleEquallyOverX", which downsamples it to n=bucketCount. + It first needs a pointSetDist, so we convert to a pointSetDist. In this process we want the + xyPointLength to be a bit longer than the eventual toSparkline downsampling. I chose 3 + fairly arbitrarily. + */ +let toSparkline = (t: t, ~sampleCount: int, ~bucketCount: int=20, unit): result => + t + ->toPointSet(~xSelection=#Linear, ~xyPointLength=bucketCount * 3, ~sampleCount, ()) + ->E.R.bind(r => + r->PointSetDist.toSparkline(bucketCount)->GenericDist_Types.Error.resultStringToResultError + ) + module Truncate = { let trySymbolicSimplification = (leftCutoff, rightCutoff, t: t): option => switch (leftCutoff, rightCutoff, t) { @@ -147,10 +163,12 @@ module AlgebraicCombination = { t1: t, t2: t, ) => { - let arithmeticOperation = Operation.Algebraic.toFn(arithmeticOperation) - E.R.merge(toSampleSet(t1), toSampleSet(t2))->E.R2.fmap(((a, b)) => { - Belt.Array.zip(a, b)->E.A2.fmap(((a, b)) => arithmeticOperation(a, b)) + let fn = Operation.Algebraic.toFn(arithmeticOperation) + E.R.merge(toSampleSet(t1), toSampleSet(t2)) + ->E.R.bind(((t1, t2)) => { + SampleSetDist.map2(~fn, ~t1, ~t2)->GenericDist_Types.Error.resultStringToResultError }) + ->E.R2.fmap(r => GenericDist_Types.SampleSet(r)) } //I'm (Ozzie) really just guessing here, very little idea what's best @@ -181,13 +199,7 @@ module AlgebraicCombination = { | Some(Error(e)) => Error(Other(e)) | None => switch chooseConvolutionOrMonteCarlo(t1, t2) { - | #CalculateWithMonteCarlo => - runMonteCarlo( - toSampleSetFn, - arithmeticOperation, - t1, - t2, - )->E.R2.fmap(r => GenericDist_Types.SampleSet(r)) + | #CalculateWithMonteCarlo => runMonteCarlo(toSampleSetFn, arithmeticOperation, t1, t2) | #CalculateWithConvolution => runConvolution( toPointSetFn, @@ -228,7 +240,7 @@ let pointwiseCombinationFloat = ( ): result => { let m = switch arithmeticOperation { | #Add | #Subtract => Error(GenericDist_Types.DistributionVerticalShiftIsInvalid) - | (#Multiply | #Divide | #Exponentiate | #Logarithm) as arithmeticOperation => + | (#Multiply | #Divide | #Power | #Logarithm) as arithmeticOperation => toPointSetFn(t)->E.R2.fmap(t => { //TODO: Move to PointSet codebase let fn = (secondary, main) => Operation.Scale.toFn(arithmeticOperation, main, secondary) @@ -253,7 +265,7 @@ let mixture = ( ~pointwiseAddFn: pointwiseAddFn, ) => { if E.A.length(values) == 0 { - Error(GenericDist_Types.Other("mixture must have at least 1 element")) + Error(GenericDist_Types.Other("Mixture error: mixture must have at least 1 element")) } else { let totalWeight = values->E.A2.fmap(E.Tuple2.second)->E.A.Floats.sum let properlyWeightedValues = diff --git a/packages/squiggle-lang/src/rescript/Distributions/GenericDist/GenericDist.resi b/packages/squiggle-lang/src/rescript/Distributions/GenericDist/GenericDist.resi index 46db83a7..4565ec14 100644 --- a/packages/squiggle-lang/src/rescript/Distributions/GenericDist/GenericDist.resi +++ b/packages/squiggle-lang/src/rescript/Distributions/GenericDist/GenericDist.resi @@ -1,11 +1,13 @@ type t = GenericDist_Types.genericDist type error = GenericDist_Types.error type toPointSetFn = t => result -type toSampleSetFn = t => result, error> +type toSampleSetFn = t => result type scaleMultiplyFn = (t, float) => result type pointwiseAddFn = (t, t) => result -let sampleN: (t, int) => result, error> +let sampleN: (t, int) => array + +let toSampleSetDist: (t, int) => Belt.Result.t let fromFloat: float => t @@ -20,17 +22,20 @@ let toFloatOperation: ( ) => result let toPointSet: ( + t, ~xyPointLength: int, ~sampleCount: int, - t, + ~xSelection: GenericDist_Types.Operation.pointsetXSelection=?, + unit, ) => result +let toSparkline: (t, ~sampleCount: int, ~bucketCount: int=?, unit) => result let truncate: ( t, ~toPointSetFn: toPointSetFn, ~leftCutoff: option=?, ~rightCutoff: option=?, - unit + unit, ) => result let algebraicCombination: ( @@ -59,4 +64,4 @@ let mixture: ( array<(t, float)>, ~scaleMultiplyFn: scaleMultiplyFn, ~pointwiseAddFn: pointwiseAddFn, -) => result \ No newline at end of file +) => result diff --git a/packages/squiggle-lang/src/rescript/Distributions/GenericDist/GenericDist_Types.res b/packages/squiggle-lang/src/rescript/Distributions/GenericDist/GenericDist_Types.res index 43ce5d74..96e7d3f8 100644 --- a/packages/squiggle-lang/src/rescript/Distributions/GenericDist/GenericDist_Types.res +++ b/packages/squiggle-lang/src/rescript/Distributions/GenericDist/GenericDist_Types.res @@ -1,14 +1,24 @@ type genericDist = | PointSet(PointSetTypes.pointSetDist) - | SampleSet(array) + | SampleSet(SampleSetDist.t) | Symbolic(SymbolicDistTypes.symbolicDist) +@genType type error = | NotYetImplemented | Unreachable | DistributionVerticalShiftIsInvalid | Other(string) +module Error = { + type t = error + + let fromString = (s: string): t => Other(s) + + let resultStringToResultError: result<'a, string> => result<'a, error> = n => + n->E.R2.errMap(r => r->fromString->Error) +} + module Operation = { type direction = | Algebraic @@ -19,7 +29,7 @@ module Operation = { | #Multiply | #Subtract | #Divide - | #Exponentiate + | #Power | #Logarithm ] @@ -28,7 +38,7 @@ module Operation = { | #Add => \"+." | #Multiply => \"*." | #Subtract => \"-." - | #Exponentiate => \"**" + | #Power => \"**" | #Divide => \"/." | #Logarithm => (a, b) => log(a) /. log(b) } @@ -41,6 +51,8 @@ module Operation = { | #Sample ] + type pointsetXSelection = [#Linear | #ByWeight] + type toDist = | Normalize | ToPointSet @@ -50,16 +62,21 @@ module Operation = { type toFloatArray = Sample(int) + type toString = + | ToString + | ToSparkline(int) + type fromDist = | ToFloat(toFloat) | ToDist(toDist) | ToDistCombination(direction, arithmeticOperation, [#Dist(genericDist) | #Float(float)]) - | ToString + | ToString(toString) type singleParamaterFunction = | FromDist(fromDist) | FromFloat(fromDist) + @genType type genericFunctionCallInfo = | FromDist(fromDist, genericDist) | FromFloat(fromDist, float) @@ -77,7 +94,8 @@ module Operation = { | ToDist(ToSampleSet(r)) => `toSampleSet(${E.I.toString(r)})` | ToDist(Truncate(_, _)) => `truncate` | ToDist(Inspect) => `inspect` - | ToString => `toString` + | ToString(ToString) => `toString` + | ToString(ToSparkline(n)) => `toSparkline(${E.I.toString(n)})` | ToDistCombination(Algebraic, _, _) => `algebraic` | ToDistCombination(Pointwise, _, _) => `pointwise` } @@ -88,3 +106,79 @@ module Operation = { | Mixture(_) => `mixture` } } + +/* +It can be a pain to write out the genericFunctionCallInfo. The constructors help with this. +This code only covers some of genericFunctionCallInfo: many arguments could be called with either a +float or a distribution. The "UsingDists" module assumes that everything is a distribution. +This is a tradeoff of some generality in order to get a bit more simplicity. +I could see having a longer interface in the future, but it could be messy. +Like, algebraicAddDistFloat vs. algebraicAddDistDist +*/ +module Constructors = { + type t = Operation.genericFunctionCallInfo + + module UsingDists = { + @genType + let mean = (dist): t => FromDist(ToFloat(#Mean), dist) + let sample = (dist): t => FromDist(ToFloat(#Sample), dist) + let cdf = (dist, x): t => FromDist(ToFloat(#Cdf(x)), dist) + let inv = (dist, x): t => FromDist(ToFloat(#Inv(x)), dist) + let pdf = (dist, x): t => FromDist(ToFloat(#Pdf(x)), dist) + let normalize = (dist): t => FromDist(ToDist(Normalize), dist) + let toPointSet = (dist): t => FromDist(ToDist(ToPointSet), dist) + let toSampleSet = (dist, r): t => FromDist(ToDist(ToSampleSet(r)), dist) + let truncate = (dist, left, right): t => FromDist(ToDist(Truncate(left, right)), dist) + let inspect = (dist): t => FromDist(ToDist(Inspect), dist) + let toString = (dist): t => FromDist(ToString(ToString), dist) + let toSparkline = (dist, n): t => FromDist(ToString(ToSparkline(n)), dist) + let algebraicAdd = (dist1, dist2: genericDist): t => FromDist( + ToDistCombination(Algebraic, #Add, #Dist(dist2)), + dist1, + ) + let algebraicMultiply = (dist1, dist2): t => FromDist( + ToDistCombination(Algebraic, #Multiply, #Dist(dist2)), + dist1, + ) + let algebraicDivide = (dist1, dist2): t => FromDist( + ToDistCombination(Algebraic, #Divide, #Dist(dist2)), + dist1, + ) + let algebraicSubtract = (dist1, dist2): t => FromDist( + ToDistCombination(Algebraic, #Subtract, #Dist(dist2)), + dist1, + ) + let algebraicLogarithm = (dist1, dist2): t => FromDist( + ToDistCombination(Algebraic, #Logarithm, #Dist(dist2)), + dist1, + ) + let algebraicPower = (dist1, dist2): t => FromDist( + ToDistCombination(Algebraic, #Power, #Dist(dist2)), + dist1, + ) + let pointwiseAdd = (dist1, dist2): t => FromDist( + ToDistCombination(Pointwise, #Add, #Dist(dist2)), + dist1, + ) + let pointwiseMultiply = (dist1, dist2): t => FromDist( + ToDistCombination(Pointwise, #Multiply, #Dist(dist2)), + dist1, + ) + let pointwiseDivide = (dist1, dist2): t => FromDist( + ToDistCombination(Pointwise, #Divide, #Dist(dist2)), + dist1, + ) + let pointwiseSubtract = (dist1, dist2): t => FromDist( + ToDistCombination(Pointwise, #Subtract, #Dist(dist2)), + dist1, + ) + let pointwiseLogarithm = (dist1, dist2): t => FromDist( + ToDistCombination(Pointwise, #Logarithm, #Dist(dist2)), + dist1, + ) + let pointwisePower = (dist1, dist2): t => FromDist( + ToDistCombination(Pointwise, #Power, #Dist(dist2)), + dist1, + ) + } +} diff --git a/packages/squiggle-lang/src/rescript/Distributions/PointSetDist/AlgebraicShapeCombination.res b/packages/squiggle-lang/src/rescript/Distributions/PointSetDist/AlgebraicShapeCombination.res index c0d85e60..70440878 100644 --- a/packages/squiggle-lang/src/rescript/Distributions/PointSetDist/AlgebraicShapeCombination.res +++ b/packages/squiggle-lang/src/rescript/Distributions/PointSetDist/AlgebraicShapeCombination.res @@ -114,7 +114,7 @@ let combineShapesContinuousContinuous = ( | #Subtract => (m1, m2) => m1 -. m2 | #Multiply => (m1, m2) => m1 *. m2 | #Divide => (m1, mInv2) => m1 *. mInv2 - | #Exponentiate => (m1, mInv2) => m1 ** mInv2 + | #Power => (m1, mInv2) => m1 ** mInv2 | #Logarithm => (m1, m2) => log(m1) /. log(m2) } // note: here, mInv2 = mean(1 / t2) ~= 1 / mean(t2) @@ -124,7 +124,7 @@ let combineShapesContinuousContinuous = ( | #Add => (v1, v2, _, _) => v1 +. v2 | #Subtract => (v1, v2, _, _) => v1 +. v2 | #Multiply => (v1, v2, m1, m2) => v1 *. v2 +. v1 *. m2 ** 2. +. v2 *. m1 ** 2. - | #Exponentiate => (v1, v2, m1, m2) => v1 *. v2 +. v1 *. m2 ** 2. +. v2 *. m1 ** 2. + | #Power => (v1, v2, m1, m2) => v1 *. v2 +. v1 *. m2 ** 2. +. v2 *. m1 ** 2. | #Logarithm => (v1, v2, m1, m2) => v1 *. v2 +. v1 *. m2 ** 2. +. v2 *. m1 ** 2. | #Divide => (v1, vInv2, m1, mInv2) => v1 *. vInv2 +. v1 *. mInv2 ** 2. +. vInv2 *. m1 ** 2. } @@ -233,7 +233,7 @@ let combineShapesContinuousDiscrete = ( () } | #Multiply - | #Exponentiate + | #Power | #Logarithm | #Divide => for j in 0 to t2n - 1 { diff --git a/packages/squiggle-lang/src/rescript/Distributions/PointSetDist/Continuous.res b/packages/squiggle-lang/src/rescript/Distributions/PointSetDist/Continuous.res index f01457b7..aa27fb62 100644 --- a/packages/squiggle-lang/src/rescript/Distributions/PointSetDist/Continuous.res +++ b/packages/squiggle-lang/src/rescript/Distributions/PointSetDist/Continuous.res @@ -249,6 +249,9 @@ module T = Dist({ ) }) +let downsampleEquallyOverX = (length, t): t => + t |> shapeMap(XYShape.XsConversion.proportionEquallyOverX(length)) + /* This simply creates multiple copies of the continuous distribution, scaled and shifted according to each discrete data point, and then adds them all together. */ let combineAlgebraicallyWithDiscrete = ( diff --git a/packages/squiggle-lang/src/rescript/Distributions/PointSetDist/PointSetDist.res b/packages/squiggle-lang/src/rescript/Distributions/PointSetDist/PointSetDist.res index 834b244f..f3f3c20c 100644 --- a/packages/squiggle-lang/src/rescript/Distributions/PointSetDist/PointSetDist.res +++ b/packages/squiggle-lang/src/rescript/Distributions/PointSetDist/PointSetDist.res @@ -191,7 +191,6 @@ let isFloat = (t: t) => let sampleNRendered = (n, dist) => { let integralCache = T.Integral.get(dist) let distWithUpdatedIntegralCache = T.updateIntegralCache(Some(integralCache), dist) - doN(n, () => sample(distWithUpdatedIntegralCache)) } @@ -203,3 +202,9 @@ let operate = (distToFloatOp: Operation.distToFloatOperation, s): float => | #Sample => sample(s) | #Mean => T.mean(s) } + +let toSparkline = (t: t, bucketCount) => + T.toContinuous(t) + ->E.O2.fmap(Continuous.downsampleEquallyOverX(bucketCount)) + ->E.O2.toResult("toContinous Error: Could not convert into continuous distribution") + ->E.R2.fmap(r => Continuous.getShape(r).ys->Sparklines.create()) \ No newline at end of file diff --git a/packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/SampleSetDist.res b/packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/SampleSetDist.res new file mode 100644 index 00000000..fcd4055d --- /dev/null +++ b/packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/SampleSetDist.res @@ -0,0 +1,68 @@ +/* +This is used as a smart constructor. The only way to create a SampleSetDist.t is to call +this constructor. +https://stackoverflow.com/questions/66909578/how-to-make-a-type-constructor-private-in-rescript-except-in-current-module +*/ +module T: { + //This really should be hidden (remove the array). The reason it isn't is to act as an escape hatch in JS__Test.ts. + //When we get a good functional library in TS, we could refactor that out. + @genType + type t = array + let make: array => result + let get: t => array +} = { + type t = array + let make = (a: array) => + if E.A.length(a) > 5 { + Ok(a) + } else { + Error("too small") + } + let get = (a: t) => a +} + +include T + +let length = (t: t) => get(t)->E.A.length + +/* +TODO: Refactor to get a more precise estimate. Also, this code is just fairly messy, could use +some refactoring. +*/ +let toPointSetDist = (~samples: t, ~samplingInputs: SamplingInputs.samplingInputs): result< + PointSetTypes.pointSetDist, + string, +> => + SampleSetDist_ToPointSet.toPointSetDist( + ~samples=get(samples), + ~samplingInputs, + (), + ).pointSetDist->E.O2.toResult("Failed to convert to PointSetDist") + +//Randomly get one sample from the distribution +let sample = (t: t): float => { + let i = E.Int.random(~min=0, ~max=E.A.length(get(t)) - 1) + E.A.unsafe_get(get(t), i) +} + +/* +If asked for a length of samples shorter or equal the length of the distribution, +return this first n samples of this distribution. +Else, return n random samples of the distribution. +The former helps in cases where multiple distributions are correlated. +However, if n > length(t), then there's no clear right answer, so we just randomly +sample everything. +*/ +let sampleN = (t: t, n) => { + if n <= E.A.length(get(t)) { + E.A.slice(get(t), ~offset=0, ~len=n) + } else { + Belt.Array.makeBy(n, _ => sample(t)) + } +} + +//TODO: Figure out what to do if distributions are different lengths. ``zip`` is kind of inelegant for this. +let map2 = (~fn: (float, float) => float, ~t1: t, ~t2: t) => { + let samples = Belt.Array.zip(get(t1), get(t2))->E.A2.fmap(((a, b)) => fn(a, b)) + make(samples) +} diff --git a/packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/Bandwidth.res b/packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/SampleSetDist_Bandwidth.res similarity index 94% rename from packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/Bandwidth.res rename to packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/SampleSetDist_Bandwidth.res index 6650b862..aef659d1 100644 --- a/packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/Bandwidth.res +++ b/packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/SampleSetDist_Bandwidth.res @@ -1,4 +1,4 @@ -//The math here was taken from https://github.com/jasondavies/science.js/blob/master/src/stats/bandwidth.js +//The math here was taken from https://github.com/jasondavies/science.js/blob/master/src/stats/SampleSetDist_Bandwidth.js let len = x => E.A.length(x) |> float_of_int diff --git a/packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/SampleSet.res b/packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/SampleSetDist_ToPointSet.res similarity index 95% rename from packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/SampleSet.res rename to packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/SampleSetDist_ToPointSet.res index 746f13d7..59b4fa46 100644 --- a/packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/SampleSet.res +++ b/packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/SampleSetDist_ToPointSet.res @@ -1,5 +1,3 @@ -// TODO: Refactor to raise correct error when not enough samples - module Internals = { module Types = { type samplingStats = { @@ -57,6 +55,7 @@ module Internals = { : { let _ = Js.Array.push(element, continuous) } + () }) (continuous, discrete) @@ -71,7 +70,7 @@ module Internals = { let formatUnitWidth = w => Jstat.max([w, 1.0]) |> int_of_float let suggestedUnitWidth = (samples, outputXYPoints) => { - let suggestedXWidth = Bandwidth.nrd0(samples) + let suggestedXWidth = SampleSetDist_Bandwidth.nrd0(samples) xWidthToUnitWidth(samples, outputXYPoints, suggestedXWidth) } @@ -98,7 +97,7 @@ let toPointSetDist = ( let pdf = continuousPart |> E.A.length > 5 ? { - let _suggestedXWidth = Bandwidth.nrd0(continuousPart) + let _suggestedXWidth = SampleSetDist_Bandwidth.nrd0(continuousPart) // todo: This does some recalculating from the last step. let _suggestedUnitWidth = Internals.T.suggestedUnitWidth( continuousPart, @@ -140,4 +139,4 @@ let toPointSetDist = ( } samplesParse -} \ No newline at end of file +} diff --git a/packages/squiggle-lang/src/rescript/Distributions/SymbolicDist/SymbolicDist.res b/packages/squiggle-lang/src/rescript/Distributions/SymbolicDist/SymbolicDist.res index cd4132b3..f2c1e886 100644 --- a/packages/squiggle-lang/src/rescript/Distributions/SymbolicDist/SymbolicDist.res +++ b/packages/squiggle-lang/src/rescript/Distributions/SymbolicDist/SymbolicDist.res @@ -55,7 +55,7 @@ module Exponential = { rate: rate, }), ) - : Error("Exponential distributions mean must be larger than 0") + : Error("Exponential distributions rate must be larger than 0.") let pdf = (x, t: t) => Jstat.Exponential.pdf(x, t.rate) let cdf = (x, t: t) => Jstat.Exponential.cdf(x, t.rate) let inv = (p, t: t) => Jstat.Exponential.inv(p, t.rate) @@ -71,7 +71,7 @@ module Cauchy = { let cdf = (x, t: t) => Jstat.Cauchy.cdf(x, t.local, t.scale) let inv = (p, t: t) => Jstat.Cauchy.inv(p, t.local, t.scale) let sample = (t: t) => Jstat.Cauchy.sample(t.local, t.scale) - let mean = (_: t) => Error("Cauchy distributions have no mean value.") + let mean = (_: t) => Error("Cauchy distributions may have no mean value.") let toString = ({local, scale}: t) => j`Cauchy($local, $scale)` } @@ -80,8 +80,8 @@ module Triangular = { let make = (low, medium, high): result => low < medium && medium < high ? Ok(#Triangular({low: low, medium: medium, high: high})) - : Error("Triangular values must be increasing order") - let pdf = (x, t: t) => Jstat.Triangular.pdf(x, t.low, t.high, t.medium) + : Error("Triangular values must be increasing order.") + let pdf = (x, t: t) => Jstat.Triangular.pdf(x, t.low, t.high, t.medium) // not obvious in jstat docs that high comes before medium? let cdf = (x, t: t) => Jstat.Triangular.cdf(x, t.low, t.high, t.medium) let inv = (p, t: t) => Jstat.Triangular.inv(p, t.low, t.high, t.medium) let sample = (t: t) => Jstat.Triangular.sample(t.low, t.high, t.medium) @@ -346,11 +346,11 @@ module T = { | _ => #NoSolution } - let toPointSetDist = (sampleCount, d: symbolicDist): PointSetTypes.pointSetDist => + let toPointSetDist = (~xSelection=#ByWeight, sampleCount, d: symbolicDist): PointSetTypes.pointSetDist => switch d { | #Float(v) => Discrete(Discrete.make(~integralSumCache=Some(1.0), {xs: [v], ys: [1.0]})) | _ => - let xs = interpolateXs(~xSelection=#ByWeight, d, sampleCount) + let xs = interpolateXs(~xSelection, d, sampleCount) let ys = xs |> E.A.fmap(x => pdf(x, d)) Continuous(Continuous.make(~integralSumCache=Some(1.0), {xs: xs, ys: ys})) } diff --git a/packages/squiggle-lang/src/rescript/OldInterpreter/ASTEvaluator.res b/packages/squiggle-lang/src/rescript/OldInterpreter/ASTEvaluator.res index 44c5565e..cd4c241d 100644 --- a/packages/squiggle-lang/src/rescript/OldInterpreter/ASTEvaluator.res +++ b/packages/squiggle-lang/src/rescript/OldInterpreter/ASTEvaluator.res @@ -118,7 +118,7 @@ module PointwiseCombination = { switch pointwiseOp { | #Add => pointwiseAdd(evaluationParams, t1, t2) | #Multiply => pointwiseCombine(\"*.", evaluationParams, t1, t2) - | #Exponentiate => pointwiseCombine(\"**", evaluationParams, t1, t2) + | #Power => pointwiseCombine(\"**", evaluationParams, t1, t2) } } diff --git a/packages/squiggle-lang/src/rescript/OldInterpreter/ASTTypes.res b/packages/squiggle-lang/src/rescript/OldInterpreter/ASTTypes.res index 31217374..17477f8f 100644 --- a/packages/squiggle-lang/src/rescript/OldInterpreter/ASTTypes.res +++ b/packages/squiggle-lang/src/rescript/OldInterpreter/ASTTypes.res @@ -218,15 +218,14 @@ module SamplingDistribution = { algebraicOp, a, b, - ) + ) |> E.O.toResult("Could not get samples") - let pointSetDist = - samples - |> E.O.fmap(r => - SampleSet.toPointSetDist(~samplingInputs=evaluationParams.samplingInputs, ~samples=r, ()) - ) - |> E.O.bind(_, r => r.pointSetDist) - |> E.O.toResult("No response") + let sampleSetDist = samples -> E.R.bind(SampleSetDist.make) + + let pointSetDist = + sampleSetDist + -> E.R.bind(r => + SampleSetDist.toPointSetDist(~samplingInputs=evaluationParams.samplingInputs, ~samples=r)); pointSetDist |> E.R.fmap(r => #Normalize(#RenderedDist(r))) }) } diff --git a/packages/squiggle-lang/src/rescript/OldInterpreter/typeSystem/HardcodedFunctions.res b/packages/squiggle-lang/src/rescript/OldInterpreter/typeSystem/HardcodedFunctions.res index cf8fe470..8302b532 100644 --- a/packages/squiggle-lang/src/rescript/OldInterpreter/typeSystem/HardcodedFunctions.res +++ b/packages/squiggle-lang/src/rescript/OldInterpreter/typeSystem/HardcodedFunctions.res @@ -227,7 +227,7 @@ let all = [ }, (), ), - makeRenderedDistFloat("scaleExp", (dist, float) => verticalScaling(#Exponentiate, dist, float)), + makeRenderedDistFloat("scaleExp", (dist, float) => verticalScaling(#Power, dist, float)), makeRenderedDistFloat("scaleMultiply", (dist, float) => verticalScaling(#Multiply, dist, float)), makeRenderedDistFloat("scaleLog", (dist, float) => verticalScaling(#Logarithm, dist, float)), Multimodal._function, diff --git a/packages/squiggle-lang/src/rescript/OldParser/Parser.res b/packages/squiggle-lang/src/rescript/OldParser/Parser.res index 0a837823..6f5fd9ef 100644 --- a/packages/squiggle-lang/src/rescript/OldParser/Parser.res +++ b/packages/squiggle-lang/src/rescript/OldParser/Parser.res @@ -144,11 +144,11 @@ module MathAdtToDistDst = { | ("subtract", _) => Error("Subtraction needs two operands") | ("multiply", [l, r]) => toOkAlgebraic((#Multiply, l, r)) | ("multiply", _) => Error("Multiplication needs two operands") - | ("pow", [l, r]) => toOkAlgebraic((#Exponentiate, l, r)) + | ("pow", [l, r]) => toOkAlgebraic((#Power, l, r)) | ("pow", _) => Error("Exponentiation needs two operands") | ("dotMultiply", [l, r]) => toOkPointwise((#Multiply, l, r)) | ("dotMultiply", _) => Error("Dotwise multiplication needs two operands") - | ("dotPow", [l, r]) => toOkPointwise((#Exponentiate, l, r)) + | ("dotPow", [l, r]) => toOkPointwise((#Power, l, r)) | ("dotPow", _) => Error("Dotwise exponentiation needs two operands") | ("rightLogShift", [l, r]) => toOkPointwise((#Add, l, r)) | ("rightLogShift", _) => Error("Dotwise addition needs two operands") diff --git a/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_GenericDistribution.res b/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_GenericDistribution.res index 84e0697f..ea65b963 100644 --- a/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_GenericDistribution.res +++ b/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_GenericDistribution.res @@ -18,8 +18,8 @@ module Helpers = { | "divide" => #Divide | "log" => #Logarithm | "dotDivide" => #Divide - | "pow" => #Exponentiate - | "dotPow" => #Exponentiate + | "pow" => #Power + | "dotPow" => #Power | "multiply" => #Multiply | "dotMultiply" => #Multiply | "dotLog" => #Logarithm @@ -45,6 +45,13 @@ module Helpers = { FromDist(GenericDist_Types.Operation.ToFloat(fnCall), dist)->runGenericOperation->Some } + let toStringFn = ( + fnCall: GenericDist_Types.Operation.toString, + dist: GenericDist_Types.genericDist, + ) => { + FromDist(GenericDist_Types.Operation.ToString(fnCall), dist)->runGenericOperation->Some + } + let toDistFn = (fnCall: GenericDist_Types.Operation.toDist, dist) => { FromDist(GenericDist_Types.Operation.ToDist(fnCall), dist)->runGenericOperation->Some } @@ -119,6 +126,9 @@ let dispatchToGenericOutput = (call: ExpressionValue.functionCall): option< ->SymbolicConstructors.symbolicResultToOutput | ("sample", [EvDistribution(dist)]) => Helpers.toFloatFn(#Sample, dist) | ("mean", [EvDistribution(dist)]) => Helpers.toFloatFn(#Mean, dist) + | ("toString", [EvDistribution(dist)]) => Helpers.toStringFn(ToString, dist) + | ("toSparkline", [EvDistribution(dist)]) => Helpers.toStringFn(ToSparkline(20), dist) + | ("toSparkline", [EvDistribution(dist), EvNumber(n)]) => Helpers.toStringFn(ToSparkline(Belt.Float.toInt(n)), dist) | ("exp", [EvDistribution(a)]) => // https://mathjs.org/docs/reference/functions/exp.html Helpers.twoDiststoDistFn(Algebraic, "pow", GenericDist.fromFloat(Math.e), a)->Some diff --git a/packages/squiggle-lang/src/rescript/TypescriptInterface.res b/packages/squiggle-lang/src/rescript/TypescriptInterface.res new file mode 100644 index 00000000..6fe6f3d4 --- /dev/null +++ b/packages/squiggle-lang/src/rescript/TypescriptInterface.res @@ -0,0 +1,27 @@ +/* +This is meant as a file to contain @genType declarations as needed for Typescript. +I would ultimately want to have all @genType declarations here, vs. other files, but +@genType doesn't play as nicely with renaming Modules and functions as +would be preferable. + +The below few seem to work fine. In the future there's definitely more work to do here. +*/ + +@genType +type env = DistributionOperation.env + +@genType +type genericDist = GenericDist_Types.genericDist + +@genType +type error = GenericDist_Types.error + +@genType +type resultDist = result +@genType +type resultFloat = result +@genType +type resultString = result + +@genType +let makeSampleSetDist = SampleSetDist.make \ No newline at end of file diff --git a/packages/squiggle-lang/src/rescript/Utility/E.res b/packages/squiggle-lang/src/rescript/Utility/E.res index e36b6124..16d87896 100644 --- a/packages/squiggle-lang/src/rescript/Utility/E.res +++ b/packages/squiggle-lang/src/rescript/Utility/E.res @@ -24,6 +24,7 @@ module FloatFloatMap = { module Int = { let max = (i1: int, i2: int) => i1 > i2 ? i1 : i2 + let random = (~min, ~max) => Js.Math.random_int(min, max) } /* Utils */ module U = { @@ -101,6 +102,7 @@ module O2 = { let default = (a, b) => O.default(b, a) let toExn = (a, b) => O.toExn(b, a) let fmap = (a, b) => O.fmap(b, a) + let toResult = (a, b) => O.toResult(b, a) } /* Functions */ @@ -178,6 +180,13 @@ module R = { module R2 = { let fmap = (a,b) => R.fmap(b,a) + let bind = (a, b) => R.bind(b, a) + + //Converts result type to change error type only + let errMap = (a, map) => switch(a){ + | Ok(r) => Ok(r) + | Error(e) => map(e) + } } let safe_fn_of_string = (fn, s: string): option<'a> => @@ -269,6 +278,7 @@ module A = { let fold_right = Array.fold_right let concatMany = Belt.Array.concatMany let keepMap = Belt.Array.keepMap + let slice = Belt.Array.slice let init = Array.init let reduce = Belt.Array.reduce let reducei = Belt.Array.reduceWithIndex @@ -289,8 +299,7 @@ module A = { )) |> Rationale.Result.return } - let rangeFloat = (~step=1, start, stop) => - Belt.Array.rangeBy(start, stop, ~step) |> fmap(Belt.Int.toFloat) + // This zips while taking the longest elements of each array. let zipMaxLength = (array1, array2) => { @@ -442,6 +451,12 @@ module A = { let mean = a => sum(a) /. (Array.length(a) |> float_of_int) let random = Js.Math.random_int + // Gives an array with all the differences between values + // diff([1,5,3,7]) = [4,-2,4] + let diff = (arr: array): array => + Belt.Array.zipBy(arr, Belt.Array.sliceToEnd(arr, 1), (left, right) => right -. left) + + exception RangeError(string) let range = (min: float, max: float, n: int): array => switch n { diff --git a/packages/squiggle-lang/src/rescript/Utility/Operation.res b/packages/squiggle-lang/src/rescript/Utility/Operation.res index 55e0b42f..6fb3b24b 100644 --- a/packages/squiggle-lang/src/rescript/Utility/Operation.res +++ b/packages/squiggle-lang/src/rescript/Utility/Operation.res @@ -6,12 +6,12 @@ type algebraicOperation = [ | #Multiply | #Subtract | #Divide - | #Exponentiate + | #Power | #Logarithm ] @genType -type pointwiseOperation = [#Add | #Multiply | #Exponentiate] -type scaleOperation = [#Multiply | #Exponentiate | #Logarithm | #Divide] +type pointwiseOperation = [#Add | #Multiply | #Power] +type scaleOperation = [#Multiply | #Power | #Logarithm | #Divide] type distToFloatOperation = [ | #Pdf(float) | #Cdf(float) @@ -27,7 +27,7 @@ module Algebraic = { | #Add => \"+." | #Subtract => \"-." | #Multiply => \"*." - | #Exponentiate => \"**" + | #Power => \"**" | #Divide => \"/." | #Logarithm => (a, b) => log(a) /. log(b) } @@ -43,7 +43,7 @@ module Algebraic = { | #Add => "+" | #Subtract => "-" | #Multiply => "*" - | #Exponentiate => "**" + | #Power => "**" | #Divide => "/" | #Logarithm => "log" } @@ -56,7 +56,7 @@ module Pointwise = { let toString = x => switch x { | #Add => "+" - | #Exponentiate => "^" + | #Power => "**" | #Multiply => "*" } @@ -83,7 +83,7 @@ module Scale = { switch x { | #Multiply => \"*." | #Divide => \"/." - | #Exponentiate => \"**" + | #Power => \"**" | #Logarithm => (a, b) => log(a) /. log(b) } @@ -91,7 +91,7 @@ module Scale = { switch operation { | #Multiply => j`verticalMultiply($value, $scaleBy) ` | #Divide => j`verticalDivide($value, $scaleBy) ` - | #Exponentiate => j`verticalExponentiate($value, $scaleBy) ` + | #Power => j`verticalPower($value, $scaleBy) ` | #Logarithm => j`verticalLog($value, $scaleBy) ` } @@ -99,7 +99,7 @@ module Scale = { switch x { | #Multiply => (a, b) => Some(a *. b) | #Divide => (a, b) => Some(a /. b) - | #Exponentiate => (_, _) => None + | #Power => (_, _) => None | #Logarithm => (_, _) => None } @@ -107,7 +107,7 @@ module Scale = { switch x { | #Multiply => (_, _) => None // TODO: this could probably just be multiplied out (using Continuous.scaleBy) | #Divide => (_, _) => None - | #Exponentiate => (_, _) => None + | #Power => (_, _) => None | #Logarithm => (_, _) => None } }