Merge branch 'develop' into reducer-dev

This commit is contained in:
Umur Ozkul 2022-04-14 17:04:54 +02:00
commit 1fa1867470
4 changed files with 128 additions and 92 deletions

View File

@ -1,15 +1,64 @@
/* /*
This is the most basic file in our invariants family of tests. 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 Jest
open Expect open Expect
open TestHelpers 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<DistributionTypes.genericDist, DistributionTypes.error>,
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 { let {
algebraicAdd, algebraicAdd,
algebraicMultiply, algebraicMultiply,
@ -26,115 +75,82 @@ let algebraicSubtract = algebraicSubtract(~env)
let algebraicLogarithm = algebraicLogarithm(~env) let algebraicLogarithm = algebraicLogarithm(~env)
let algebraicPower = algebraicPower(~env) let algebraicPower = algebraicPower(~env)
describe("Mean", () => { let {testOperationMean, distributions, pairsOfDifferentDistributions, epsilon} = module(Internals)
let digits = -4
let mean = GenericDist_Types.Constructors.UsingDists.mean describe("Means are invariant", () => {
describe("for addition", () => {
let testAdditionMean = testOperationMean(algebraicAdd, "algebraicAdd", \"+.", ~epsilon)
let runMean: result<DistributionTypes.genericDist, DistributionTypes.error> => float = distR => { testAll("with two of the same distribution", distributions, dist => {
distR E.R.liftM2(testAdditionMean, dist, dist)->E.R.toExn
->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 => testAll("with two different distributions", pairsOfDifferentDistributions, dists => {
`${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<DistributionTypes.genericDist, DistributionTypes.error>,
description: string,
floatOp: (float, float) => float,
dist1': result<SymbolicDistTypes.symbolicDist, string>,
dist2': result<SymbolicDistTypes.symbolicDist, string>
) => {
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 let (dist1, dist2) = dists
testAdditionMean(dist1, dist2) E.R.liftM2(testAdditionMean, dist1, dist2)->E.R.toExn
}) })
testAll("heterogeneous addition (1)", combinations, dists => { testAll(
"with two different distributions in swapped order",
pairsOfDifferentDistributions,
dists => {
let (dist1, dist2) = dists let (dist1, dist2) = dists
testAdditionMean(dist1, dist2) E.R.liftM2(testAdditionMean, dist2, dist1)->E.R.toExn
},
)
}) })
testAll("heterogeneous addition (commuted of 1 (or; 2))", combinations, dists => { describe("for subtraction", () => {
let testSubtractionMean = testOperationMean(
algebraicSubtract,
"algebraicSubtract",
\"-.",
~epsilon,
)
testAll("with two of the same distribution", distributions, dist => {
E.R.liftM2(testSubtractionMean, dist, dist)->E.R.toExn
})
testAll("with two different distributions", pairsOfDifferentDistributions, dists => {
let (dist1, dist2) = dists let (dist1, dist2) = dists
testAdditionMean(dist2, dist1) E.R.liftM2(testSubtractionMean, dist1, dist2)->E.R.toExn
})
}) })
describe("subtraction", () => { testAll(
let testSubtractionMean = testOperationMean(algebraicSubtract, "algebraicSubtract", \"-.") "with two different distributions in swapped order",
pairsOfDifferentDistributions,
testAll("homogeneous subtraction", zipDistsDists, dists => { dists => {
let (dist1, dist2) = dists let (dist1, dist2) = dists
testSubtractionMean(dist1, dist2) E.R.liftM2(testSubtractionMean, dist2, dist1)->E.R.toExn
},
)
}) })
testAll("heterogeneous subtraction (1)", combinations, dists => { describe("for multiplication", () => {
let testMultiplicationMean = testOperationMean(
algebraicMultiply,
"algebraicMultiply",
\"*.",
~epsilon,
)
testAll("with two of the same distribution", distributions, dist => {
E.R.liftM2(testMultiplicationMean, dist, dist)->E.R.toExn
})
testAll("with two different distributions", pairsOfDifferentDistributions, dists => {
let (dist1, dist2) = dists let (dist1, dist2) = dists
testSubtractionMean(dist1, dist2) E.R.liftM2(testMultiplicationMean, dist1, dist2)->E.R.toExn
}) })
testAll("heterogeneous subtraction (commuted of 1 (or; 2))", combinations, dists => { testAll(
"with two different distributions in swapped order",
pairsOfDifferentDistributions,
dists => {
let (dist1, dist2) = dists let (dist1, dist2) = dists
testSubtractionMean(dist2, dist1) E.R.liftM2(testMultiplicationMean, dist2, dist1)->E.R.toExn
}) },
}) )
describe("multiplication", () => {
let testMultiplicationMean = testOperationMean(algebraicMultiply, "algebraicMultiply", \"*.")
testAll("homogeneous subtraction", zipDistsDists, dists => {
let (dist1, dist2) = dists
testMultiplicationMean(dist1, dist2)
})
testAll("heterogeneoous subtraction (1)", combinations, dists => {
let (dist1, dist2) = dists
testMultiplicationMean(dist1, dist2)
})
testAll("heterogeneoous subtraction (commuted of 1 (or; 2))", combinations, dists => {
let (dist1, dist2) = dists
testMultiplicationMean(dist2, dist1)
})
}) })
}) })

View File

@ -1,6 +1,25 @@
open Jest open Jest
open Expect 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) => let makeTest = (~only=false, str, item1, item2) =>
only only
? Only.test(str, () => expect(item1)->toEqual(item2)) ? Only.test(str, () => expect(item1)->toEqual(item2))

View File

@ -117,7 +117,8 @@ module Helpers = {
| Error(err) => GenDistError(ArgumentError(err)) | Error(err) => GenDistError(ArgumentError(err))
} }
} }
| Some(EvDistribution(b)) => switch parseDistributionArray(args) { | Some(EvDistribution(b)) =>
switch parseDistributionArray(args) {
| Ok(distributions) => mixtureWithDefaultWeights(distributions) | Ok(distributions) => mixtureWithDefaultWeights(distributions)
| Error(err) => GenDistError(ArgumentError(err)) | Error(err) => GenDistError(ArgumentError(err))
} }