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.
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<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 {
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<DistributionTypes.genericDist, DistributionTypes.error> => 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<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
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
},
)
})
})

View File

@ -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))

View File

@ -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))
}

View File

@ -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