Merge pull request #257 from quantified-uncertainty/issue250
Improvements to `Means_test.res` (and `expectErrorToBeBounded`)
This commit is contained in:
commit
02bf4c6a8a
|
@ -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://deploy-preview-251--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 =>
|
|
||||||
`${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("heterogeneous addition (1)", combinations, dists => {
|
testAll("with two different distributions", pairsOfDifferentDistributions, dists => {
|
||||||
let (dist1, dist2) = 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 => {
|
testAll(
|
||||||
let (dist1, dist2) = dists
|
"with two different distributions in swapped order",
|
||||||
testAdditionMean(dist2, dist1)
|
pairsOfDifferentDistributions,
|
||||||
})
|
dists => {
|
||||||
|
let (dist1, dist2) = dists
|
||||||
|
E.R.liftM2(testAdditionMean, dist2, dist1)->E.R.toExn
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("subtraction", () => {
|
describe("for subtraction", () => {
|
||||||
let testSubtractionMean = testOperationMean(algebraicSubtract, "algebraicSubtract", \"-.")
|
let testSubtractionMean = testOperationMean(
|
||||||
|
algebraicSubtract,
|
||||||
|
"algebraicSubtract",
|
||||||
|
\"-.",
|
||||||
|
~epsilon,
|
||||||
|
)
|
||||||
|
|
||||||
testAll("homogeneous subtraction", zipDistsDists, dists => {
|
testAll("with two of the same distribution", distributions, dist => {
|
||||||
let (dist1, dist2) = dists
|
E.R.liftM2(testSubtractionMean, dist, dist)->E.R.toExn
|
||||||
testSubtractionMean(dist1, dist2)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
testAll("heterogeneous subtraction (1)", combinations, dists => {
|
testAll("with two different distributions", pairsOfDifferentDistributions, dists => {
|
||||||
let (dist1, dist2) = 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 => {
|
testAll(
|
||||||
let (dist1, dist2) = dists
|
"with two different distributions in swapped order",
|
||||||
testSubtractionMean(dist2, dist1)
|
pairsOfDifferentDistributions,
|
||||||
})
|
dists => {
|
||||||
|
let (dist1, dist2) = dists
|
||||||
|
E.R.liftM2(testSubtractionMean, dist2, dist1)->E.R.toExn
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("multiplication", () => {
|
describe("for multiplication", () => {
|
||||||
let testMultiplicationMean = testOperationMean(algebraicMultiply, "algebraicMultiply", \"*.")
|
let testMultiplicationMean = testOperationMean(
|
||||||
|
algebraicMultiply,
|
||||||
|
"algebraicMultiply",
|
||||||
|
\"*.",
|
||||||
|
~epsilon,
|
||||||
|
)
|
||||||
|
|
||||||
testAll("homogeneous subtraction", zipDistsDists, dists => {
|
testAll("with two of the same distribution", distributions, dist => {
|
||||||
let (dist1, dist2) = dists
|
E.R.liftM2(testMultiplicationMean, dist, dist)->E.R.toExn
|
||||||
testMultiplicationMean(dist1, dist2)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
testAll("heterogeneoous subtraction (1)", combinations, dists => {
|
testAll("with two different distributions", pairsOfDifferentDistributions, dists => {
|
||||||
let (dist1, dist2) = 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 => {
|
testAll(
|
||||||
let (dist1, dist2) = dists
|
"with two different distributions in swapped order",
|
||||||
testMultiplicationMean(dist2, dist1)
|
pairsOfDifferentDistributions,
|
||||||
})
|
dists => {
|
||||||
|
let (dist1, dist2) = dists
|
||||||
|
E.R.liftM2(testMultiplicationMean, dist2, dist1)->E.R.toExn
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user