diff --git a/packages/squiggle-lang/__tests__/Distributions/Invariants/Means_test.res b/packages/squiggle-lang/__tests__/Distributions/Invariants/Means_test.res index 51f9d1c1..bcc461bc 100644 --- a/packages/squiggle-lang/__tests__/Distributions/Invariants/Means_test.res +++ b/packages/squiggle-lang/__tests__/Distributions/Invariants/Means_test.res @@ -1,15 +1,64 @@ /* This is the most basic file in our invariants family of tests. -See document in https://github.com/quantified-uncertainty/squiggle/pull/238 for details +Validate that the addition of means equals the mean of the addition, similar for subtraction and multiplication. -Note: digits parameter should be higher than -4. +Details in https://develop--squiggle-documentation.netlify.app/docs/internal/invariants/ + +Note: epsilon of 1e3 means the invariants are, in general, not being satisfied. */ open Jest open Expect open TestHelpers +module Internals = { + let epsilon = 5e1 + + let mean = GenericDist_Types.Constructors.UsingDists.mean + + let expectImpossiblePath: string => assertion = algebraicOp => + `${algebraicOp} has`->expect->toEqual("failed") + + let distributions = list{ + normalMake(4e0, 1e0), + betaMake(2e0, 4e0), + exponentialMake(1.234e0), + uniformMake(7e0, 1e1), + // cauchyMake(1e0, 1e0), + lognormalMake(2e0, 1e0), + triangularMake(1e0, 1e1, 5e1), + Ok(floatMake(1e1)), + } + let pairsOfDifferentDistributions = E.L.combinations2(distributions) + + let runMean: DistributionTypes.genericDist => float = dist => { + dist->mean->run->toFloat->E.O2.toExn("Shouldn't see this because we trust testcase input") + } + + let testOperationMean = ( + distOp: ( + DistributionTypes.genericDist, + DistributionTypes.genericDist, + ) => result, + description: string, + floatOp: (float, float) => float, + dist1': SymbolicDistTypes.symbolicDist, + dist2': SymbolicDistTypes.symbolicDist, + ~epsilon: float, + ) => { + let dist1 = dist1'->DistributionTypes.Symbolic + let dist2 = dist2'->DistributionTypes.Symbolic + let received = + distOp(dist1, dist2)->E.R2.fmap(mean)->E.R2.fmap(run)->E.R2.fmap(toFloat)->E.R.toExn + let expected = floatOp(runMean(dist1), runMean(dist2)) + switch received { + | None => expectImpossiblePath(description) + | Some(x) => expectErrorToBeBounded(x, expected, ~epsilon) + } + } +} + let { algebraicAdd, algebraicMultiply, @@ -26,115 +75,82 @@ let algebraicSubtract = algebraicSubtract(~env) let algebraicLogarithm = algebraicLogarithm(~env) let algebraicPower = algebraicPower(~env) -describe("Mean", () => { - let digits = -4 +let {testOperationMean, distributions, pairsOfDifferentDistributions, epsilon} = module(Internals) - let mean = GenericDist_Types.Constructors.UsingDists.mean +describe("Means are invariant", () => { + describe("for addition", () => { + let testAdditionMean = testOperationMean(algebraicAdd, "algebraicAdd", \"+.", ~epsilon) - let runMean: result => float = distR => { - distR - ->E.R2.fmap(mean) - ->E.R2.fmap(run) - ->E.R2.fmap(toFloat) - ->E.R.toExn - ->E.O2.toExn("Shouldn't see this because we trust testcase input") - } - - let impossiblePath: string => assertion = algebraicOp => - `${algebraicOp} has`->expect->toEqual("failed") - - let distributions = list{ - normalMake(0.0, 1e0), - betaMake(2e0, 4e0), - exponentialMake(1.234e0), - uniformMake(7e0, 1e1), - // cauchyMake(1e0, 1e0), - lognormalMake(1e0, 1e0), - triangularMake(1e0, 1e1, 5e1), - Ok(floatMake(1e1)), - } - let combinations = E.L.combinations2(distributions) - let zipDistsDists = E.L.zip(distributions, distributions) - - let testOperationMean = ( - distOp: (DistributionTypes.genericDist, DistributionTypes.genericDist) => result, - description: string, - floatOp: (float, float) => float, - dist1': result, - dist2': result - ) => { - let dist1 = dist1'->E.R2.fmap(x=>DistributionTypes.Symbolic(x))->E.R2.fmap2(s=>DistributionTypes.Other(s)) - let dist2 = dist2'->E.R2.fmap(x=>DistributionTypes.Symbolic(x))->E.R2.fmap2(s=>DistributionTypes.Other(s)) - let received = - E.R.liftJoin2(distOp, dist1, dist2) - ->E.R2.fmap(mean) - ->E.R2.fmap(run) - ->E.R2.fmap(toFloat) - let expected = floatOp(runMean(dist1), runMean(dist2)) - switch received { - | Error(err) => impossiblePath(description) - | Ok(x) => - switch x { - | None => impossiblePath(description) - | Some(x) => x->expect->toBeSoCloseTo(expected, ~digits) - } - } - } - - describe("addition", () => { - let testAdditionMean = testOperationMean(algebraicAdd, "algebraicAdd", \"+.") - - testAll("homogeneous addition", zipDistsDists, dists => { - let (dist1, dist2) = dists - testAdditionMean(dist1, dist2) + testAll("with two of the same distribution", distributions, dist => { + E.R.liftM2(testAdditionMean, dist, dist)->E.R.toExn }) - testAll("heterogeneous addition (1)", combinations, dists => { + testAll("with two different distributions", pairsOfDifferentDistributions, dists => { let (dist1, dist2) = dists - testAdditionMean(dist1, dist2) + E.R.liftM2(testAdditionMean, dist1, dist2)->E.R.toExn }) - testAll("heterogeneous addition (commuted of 1 (or; 2))", combinations, dists => { - let (dist1, dist2) = dists - testAdditionMean(dist2, dist1) - }) + testAll( + "with two different distributions in swapped order", + pairsOfDifferentDistributions, + dists => { + let (dist1, dist2) = dists + E.R.liftM2(testAdditionMean, dist2, dist1)->E.R.toExn + }, + ) }) - describe("subtraction", () => { - let testSubtractionMean = testOperationMean(algebraicSubtract, "algebraicSubtract", \"-.") + describe("for subtraction", () => { + let testSubtractionMean = testOperationMean( + algebraicSubtract, + "algebraicSubtract", + \"-.", + ~epsilon, + ) - testAll("homogeneous subtraction", zipDistsDists, dists => { - let (dist1, dist2) = dists - testSubtractionMean(dist1, dist2) + testAll("with two of the same distribution", distributions, dist => { + E.R.liftM2(testSubtractionMean, dist, dist)->E.R.toExn }) - testAll("heterogeneous subtraction (1)", combinations, dists => { + testAll("with two different distributions", pairsOfDifferentDistributions, dists => { let (dist1, dist2) = dists - testSubtractionMean(dist1, dist2) + E.R.liftM2(testSubtractionMean, dist1, dist2)->E.R.toExn }) - testAll("heterogeneous subtraction (commuted of 1 (or; 2))", combinations, dists => { - let (dist1, dist2) = dists - testSubtractionMean(dist2, dist1) - }) + testAll( + "with two different distributions in swapped order", + pairsOfDifferentDistributions, + dists => { + let (dist1, dist2) = dists + E.R.liftM2(testSubtractionMean, dist2, dist1)->E.R.toExn + }, + ) }) - describe("multiplication", () => { - let testMultiplicationMean = testOperationMean(algebraicMultiply, "algebraicMultiply", \"*.") + describe("for multiplication", () => { + let testMultiplicationMean = testOperationMean( + algebraicMultiply, + "algebraicMultiply", + \"*.", + ~epsilon, + ) - testAll("homogeneous subtraction", zipDistsDists, dists => { - let (dist1, dist2) = dists - testMultiplicationMean(dist1, dist2) + testAll("with two of the same distribution", distributions, dist => { + E.R.liftM2(testMultiplicationMean, dist, dist)->E.R.toExn }) - testAll("heterogeneoous subtraction (1)", combinations, dists => { + testAll("with two different distributions", pairsOfDifferentDistributions, dists => { let (dist1, dist2) = dists - testMultiplicationMean(dist1, dist2) + E.R.liftM2(testMultiplicationMean, dist1, dist2)->E.R.toExn }) - testAll("heterogeneoous subtraction (commuted of 1 (or; 2))", combinations, dists => { - let (dist1, dist2) = dists - testMultiplicationMean(dist2, dist1) - }) + testAll( + "with two different distributions in swapped order", + pairsOfDifferentDistributions, + dists => { + let (dist1, dist2) = dists + E.R.liftM2(testMultiplicationMean, dist2, dist1)->E.R.toExn + }, + ) }) }) diff --git a/packages/squiggle-lang/__tests__/TestHelpers.res b/packages/squiggle-lang/__tests__/TestHelpers.res index 4967f75c..5147857b 100644 --- a/packages/squiggle-lang/__tests__/TestHelpers.res +++ b/packages/squiggle-lang/__tests__/TestHelpers.res @@ -1,6 +1,25 @@ open Jest open Expect +/* +This encodes the expression for percent error +The test says "the percent error of received against expected is bounded by epsilon" + +However, the semantics are degraded by catching some numerical instability: +when expected is too small, the return of this function might blow up to infinity. +So we capture that by taking the max of abs(expected) against a 1. + +A sanity check of this function would be welcome, in general it is a better way of approaching +squiggle-lang tests than toBeSoCloseTo. +*/ +let expectErrorToBeBounded = (received, expected, ~epsilon) => { + let distance = Js.Math.abs_float(received -. expected) + let expectedAbs = Js.Math.abs_float(expected) + let normalizingDenom = Js.Math.max_float(expectedAbs, 1e0) + let error = distance /. normalizingDenom + error->expect->toBeLessThan(epsilon) +} + let makeTest = (~only=false, str, item1, item2) => only ? Only.test(str, () => expect(item1)->toEqual(item2)) diff --git a/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_GenericDistribution.res b/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_GenericDistribution.res index 918c4b70..b0ce84e3 100644 --- a/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_GenericDistribution.res +++ b/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_GenericDistribution.res @@ -117,7 +117,8 @@ module Helpers = { | Error(err) => GenDistError(ArgumentError(err)) } } - | Some(EvDistribution(b)) => switch parseDistributionArray(args) { + | Some(EvDistribution(b)) => + switch parseDistributionArray(args) { | Ok(distributions) => mixtureWithDefaultWeights(distributions) | Error(err) => GenDistError(ArgumentError(err)) } diff --git a/packages/squiggle-lang/src/rescript/Utility/E.res b/packages/squiggle-lang/src/rescript/Utility/E.res index cb921c39..24e7947c 100644 --- a/packages/squiggle-lang/src/rescript/Utility/E.res +++ b/packages/squiggle-lang/src/rescript/Utility/E.res @@ -215,8 +215,8 @@ module R2 = { | Ok(r) => Ok(r) | Error(e) => map(e) } - - let fmap2 = (xR, f) => + + let fmap2 = (xR, f) => switch xR { | Ok(x) => x->Ok | Error(x) => x->f->Error