diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 000d73ec..9c9c86c3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,3 +9,5 @@ updates: directory: "/" # Location of package manifests schedule: interval: "daily" + commit-message: + prefix: "⬆️" diff --git a/packages/squiggle-lang/__tests__/JS__Test.ts b/packages/squiggle-lang/__tests__/TS/JS_test.ts similarity index 98% rename from packages/squiggle-lang/__tests__/JS__Test.ts rename to packages/squiggle-lang/__tests__/TS/JS_test.ts index 72f68ba4..8e6db265 100644 --- a/packages/squiggle-lang/__tests__/JS__Test.ts +++ b/packages/squiggle-lang/__tests__/TS/JS_test.ts @@ -4,7 +4,7 @@ import { resultMap, squiggleExpression, errorValueToString, -} from "../src/js/index"; +} from "../../src/js/index"; let testRun = (x: string): squiggleExpression => { let result = run(x, { sampleCount: 100, xyPointLength: 100 }); diff --git a/packages/squiggle-lang/__tests__/TS/Jstat_test.ts b/packages/squiggle-lang/__tests__/TS/Jstat_test.ts new file mode 100644 index 00000000..d4522ec2 --- /dev/null +++ b/packages/squiggle-lang/__tests__/TS/Jstat_test.ts @@ -0,0 +1,58 @@ +import { + run, + Distribution, + squiggleExpression, + errorValueToString, + errorValue, + result, +} from "../../src/js/index"; +import * as fc from "fast-check"; + +let testRun = (x: string): any => { + //result => { + return run(x, { sampleCount: 1000, xyPointLength: 100 }); +}; + +let failDefault = () => expect("codepath should never").toBe("be reached"); + +describe("Jstat: cumulative density function", () => { + test("of a normal distribution at 3 stdevs to the right of the mean is within epsilon of 1", () => { + fc.assert( + fc.property(fc.float(), fc.float({ min: 1e-7 }), (mean, stdev) => { + let squiggleString = `cdf(normal(${mean}, ${stdev}), ${ + mean + 3 * stdev + })`; + let squiggleResult = testRun(squiggleString); + let epsilon = 5e-3; + switch (squiggleResult.tag) { + case "Error": + expect(errorValueToString(squiggleResult.value)).toEqual( + "" + ); + case "Ok": + expect(squiggleResult.value.value).toBeGreaterThan(1 - epsilon); + } + }) + ); + }); + + test("of a normal distribution at 3 stdevs to the left of the mean is within epsilon of 0", () => { + fc.assert( + fc.property(fc.float(), fc.float({ min: 1e-7 }), (mean, stdev) => { + let squiggleString = `cdf(normal(${mean}, ${stdev}), ${ + mean - 3 * stdev + })`; + let squiggleResult = testRun(squiggleString); + let epsilon = 5e-3; + switch (squiggleResult.tag) { + case "Error": + expect(errorValueToString(squiggleResult.value)).toEqual( + "" + ); + case "Ok": + expect(squiggleResult.value.value).toBeLessThan(epsilon); + } + }) + ); + }); +}); diff --git a/packages/squiggle-lang/__tests__/TS/Parser_test.ts b/packages/squiggle-lang/__tests__/TS/Parser_test.ts new file mode 100644 index 00000000..d6579c49 --- /dev/null +++ b/packages/squiggle-lang/__tests__/TS/Parser_test.ts @@ -0,0 +1,58 @@ +import { + run, + squiggleExpression, + errorValue, + result, +} from "../../src/js/index"; +import * as fc from "fast-check"; + +let testRun = (x: string): result => { + return run(x, { sampleCount: 1000, xyPointLength: 100 }); +}; + +describe("Squiggle is whitespace insensitive", () => { + test("when assigning a distribution to a name and calling that name", () => { + /* + * intersperse varying amounts of whitespace in a squiggle string + */ + let squiggleString = ( + a: string, + b: string, + c: string, + d: string, + e: string, + f: string, + g: string, + h: string + ): string => { + return `theDist${a}=${b}beta(${c}4${d},${e}5e1)${f};${g}theDist${h}`; + }; + let squiggleOutput = testRun( + squiggleString("", "", "", "", "", "", "", "") + ); + /* + * Add "\n" to this when multiline is introduced. + */ + let whitespaceGen = () => { + return fc.constantFrom("", " ", "\t", " ", " ", " ", " "); + }; + + fc.assert( + fc.property( + whitespaceGen(), + whitespaceGen(), + whitespaceGen(), + whitespaceGen(), + whitespaceGen(), + whitespaceGen(), + whitespaceGen(), + whitespaceGen(), + (a, b, c, d, e, f, g, h) => { + expect(testRun(squiggleString(a, b, c, d, e, f, g, h))).toEqual( + squiggleOutput + ); + } + ) + ); + }); +}); diff --git a/packages/squiggle-lang/__tests__/TS/SampleSet_test.ts b/packages/squiggle-lang/__tests__/TS/SampleSet_test.ts new file mode 100644 index 00000000..e21d760e --- /dev/null +++ b/packages/squiggle-lang/__tests__/TS/SampleSet_test.ts @@ -0,0 +1,238 @@ +import { + run, + Distribution, + squiggleExpression, + errorValueToString, + errorValue, + result, +} from "../../src/js/index"; +import * as fc from "fast-check"; + +let testRun = (x: string): result => { + return run(x, { sampleCount: 1000, xyPointLength: 100 }); +}; + +let failDefault = () => expect("codepath should never").toBe("be reached"); + +// Beware: float64Array makes it appear in an infinite loop. +let arrayGen = () => + fc.float32Array({ + minLength: 10, + maxLength: 10000, + noDefaultInfinity: true, + noNaN: true, + }); + +describe("SampleSet: cdf", () => { + let n = 10000 + test("at the highest number in the distribution is within epsilon of 1", () => { + fc.assert( + fc.property(arrayGen(), (xs) => { + let ys = Array.from(xs); + let max = Math.max(...ys); + // Should compute with squiglge strings once interpreter has `sample` + let dist = new Distribution( + { tag: "SampleSet", value: ys }, + { sampleCount: n, xyPointLength: 100 } + ); + let cdfValue = dist.cdf(max).value + let min = Math.min(...ys) + let epsilon = 5e-3; + if (max - min < epsilon) { + expect(cdfValue).toBeLessThan(1 - epsilon) + } else { + expect(dist.cdf(max).value).toBeGreaterThan(1 - epsilon); + } + }) + ); + }); + + test("at the lowest number in the distribution is within epsilon of 0", () => { + fc.assert( + fc.property(arrayGen(), (xs) => { + let ys = Array.from(xs); + let min = Math.min(...ys); + // Should compute with squiggle strings once interpreter has `sample` + let dist = new Distribution( + { tag: "SampleSet", value: ys }, + { sampleCount: n, xyPointLength: 100 } + ); + let cdfValue = dist.cdf(min).value + let max = Math.max(...ys); + let epsilon = 5e-3; + if (max - min < epsilon) { + expect(cdfValue).toBeGreaterThan(epsilon) + } else { + expect(cdfValue).toBeLessThan(epsilon); + } + }) + ); + }); + + test("is <= 1 everywhere with equality when x is higher than the max", () => { + fc.assert( + fc.property(arrayGen(), fc.float(), (xs, x) => { + let ys = Array.from(xs) + let dist = new Distribution( + { tag: "SampleSet", value: ys }, + { sampleCount: n, xyPointLength: 100 } + ); + let cdfValue = dist.cdf(x).value + let epsilon = 1e-1 + if (x > Math.max(...ys)) { // The really good way to do this is to have epsilon be a function of the percentage by which x > max(ys) + expect(cdfValue).toBeGreaterThan(1 - epsilon - epsilon ** 2) + } else { + expect(cdfValue).toBeLessThan(1); + } + }) + ); + }); + + test("is >= 0 everywhere with equality when x is lower than the min", () => { + fc.assert( + fc.property(arrayGen(), fc.float(), (xs, x) => { + let ys = Array.from(xs) + let dist = new Distribution( + { tag: "SampleSet", value: ys }, + { sampleCount: n, xyPointLength: 100 } + ); + let cdfValue = dist.cdf(x).value + if (x < Math.min(...ys)) { + expect(cdfValue).toEqual(0) + } else { + expect(cdfValue).toBeGreaterThan(0); + } + }) + ); + }); +}); + +describe("SampleSet: pdf of extremes is lower than pdf of mean.", () => { + let n = 1000 + + test("a sampleset distribution's pdf assigns less weight to the max than to the mean", () => { + fc.assert( + fc.property(arrayGen(), (xs) => { + let ys = Array.from(xs); + let max = Math.max(...ys); + let mean = ys.reduce((a, b) => a + b, 0.0) / ys.length; + // Should be from squiggleString once interpreter exposes sampleset + let dist = new Distribution( + { tag: "SampleSet", value: ys }, + { sampleCount: n, xyPointLength: 100 } + ); + let pdfMean = dist.pdf(mean); + let pdfMax = dist.pdf(max); + switch (pdfMax.tag) { + case "Ok": + let min = Math.min(...ys) + switch (pdfMean.tag) { + case "Ok": + if (max == min) { + expect(pdfMax.value).toBeLessThanOrEqual(pdfMean.value); + } else { + expect(pdfMax.value).toBeLessThan(pdfMean.value); + } + case "Error": + if (max == min) { + expect(pdfMean.value).toEqual(1); + } else { + expect(pdfMean.value).toEqual("error message"); + } + default: + failDefault(); + } + case "Error": + switch (pdfMean.tag) { + case "Ok": + expect(pdfMax.value).toEqual("error message"); + case "Error": + expect(pdfMax.value).toEqual(pdfMean.value); + default: + failDefault(); + } + default: + failDefault(); + } + }) + ); + }); +}); + +// describe("SampleSet: mean is mean", () => { +// test("mean(samples(xs)) sampling twice as widely as the input", () => { +// fc.assert( +// fc.property( +// fc.float64Array({ minLength: 10, maxLength: 100000 }), +// (xs) => { +// let ys = Array.from(xs); +// let n = ys.length; +// let dist = new Distribution( +// { tag: "SampleSet", value: ys }, +// { sampleCount: 2 * n, xyPointLength: 4 * n } +// ); +// +// expect(dist.mean().value).toBeCloseTo( +// ys.reduce((a, b) => a + b, 0.0) / n +// ); +// } +// ) +// ); +// }); +// +// test("mean(samples(xs)) sampling half as widely as the input", () => { +// fc.assert( +// fc.property( +// fc.float64Array({ minLength: 10, maxLength: 100000 }), +// (xs) => { +// let ys = Array.from(xs); +// let n = ys.length; +// let dist = new Distribution( +// { tag: "SampleSet", value: ys }, +// { sampleCount: Math.floor(5 / 2), xyPointLength: 4 * n } +// ); +// +// expect(dist.mean().value).toBeCloseTo( +// ys.reduce((a, b) => a + b, 0.0) / n +// ); +// } +// ) +// ); +// }); +// }); + +// describe("Mean of mixture is weighted average of means", () => { +// test("mx(beta(a,b), lognormal(m,s), [x,y])", () => { +// fc.assert( +// fc.property( +// fc.float({ min: 1e-1 }), // alpha +// fc.float({ min: 1 }), // beta +// fc.float(), // mu +// fc.float({ min: 1e-1 }), // sigma +// fc.float({ min: 1e-7 }), +// fc.float({ min: 1e-7 }), +// (a, b, m, s, x, y) => { +// let squiggleString = `mean(mx(beta(${a},${b}), lognormal(${m},${s}), [${x}, ${y}]))`; +// let res = testRun(squiggleString); +// switch (res.tag) { +// case "Error": +// expect(errorValueToString(res.value)).toEqual( +// "" +// ); +// case "Ok": +// let betaWeight = x / (x + y); +// let lognormalWeight = y / (x + y); +// let betaMean = 1 / (1 + b / a); +// let lognormalMean = m + s ** 2 / 2; +// expect(res.value).toEqual({ +// tag: "number", +// value: betaWeight * betaMean + lognormalWeight * lognormalMean, +// }); +// default: +// expect("mean returned").toBe(`something other than a number`); +// } +// } +// ) +// ); +// }); +// }); diff --git a/packages/squiggle-lang/__tests__/TS/Scalars_test.ts b/packages/squiggle-lang/__tests__/TS/Scalars_test.ts new file mode 100644 index 00000000..401ac32a --- /dev/null +++ b/packages/squiggle-lang/__tests__/TS/Scalars_test.ts @@ -0,0 +1,62 @@ +import { + run, + Distribution, + resultMap, + squiggleExpression, + errorValueToString, + errorValue, + result, +} from "../../src/js/index"; +import * as fc from "fast-check"; + +let testRun = (x: string): result => { + return run(x, { sampleCount: 100, xyPointLength: 100 }); +}; + +describe("Scalar manipulation is well-modeled by javascript math", () => { + test("in the case of logarithms (with assignment)", () => { + fc.assert( + fc.property(fc.float(), (x) => { + let squiggleString = `x = log(${x}); x`; + let squiggleResult = testRun(squiggleString); + if (x == 0) { + expect(squiggleResult.value).toEqual({ + tag: "number", + value: -Infinity, + }); + } else if (x < 0) { + expect(squiggleResult.value).toEqual({ + tag: "RETodo", + value: + "somemessage (confused why a test case hasn't pointed out to me that this message is bogus)", + }); + } else { + expect(squiggleResult.value).toEqual({ + tag: "number", + value: Math.log(x), + }); + } + }) + ); + }); + + test("in the case of addition (with assignment)", () => { + fc.assert( + fc.property(fc.float(), fc.float(), fc.float(), (x, y, z) => { + let squiggleString = `x = ${x}; y = ${y}; z = ${z}; x + y + z`; + let squiggleResult = testRun(squiggleString); + switch (squiggleResult.tag) { + case "Error": + expect(errorValueToString(squiggleResult.value)).toEqual( + "some message (hopefully a test case points it out to me)" + ); + case "Ok": + expect(squiggleResult.value).toEqual({ + tag: "number", + value: x + y + z, + }); + } + }) + ); + }); +}); diff --git a/packages/squiggle-lang/__tests__/TS/Symbolic_test.ts b/packages/squiggle-lang/__tests__/TS/Symbolic_test.ts new file mode 100644 index 00000000..f97adbec --- /dev/null +++ b/packages/squiggle-lang/__tests__/TS/Symbolic_test.ts @@ -0,0 +1,44 @@ +import { + run, + squiggleExpression, + errorValueToString, + errorValue, + result, +} from "../../src/js/index"; +import * as fc from "fast-check"; + +let testRun = (x: string): result => { + return run(x, { sampleCount: 100, xyPointLength: 100 }); +}; + +describe("Symbolic mean", () => { + let triangularInputError = { + tag: "Error", + value: { + tag: "RETodo", + value: "Triangular values must be increasing order.", + }, + }; + test("mean(triangular(x,y,z))", () => { + fc.assert( + fc.property(fc.float(), fc.float(), fc.float(), (x, y, z) => { + let res = testRun(`mean(triangular(${x},${y},${z}))`); + if (!(x < y && y < z)) { + expect(res).toEqual(triangularInputError); + } else { + switch (res.tag) { + case "Error": + expect(errorValueToString(res.value)).toEqual( + "" + ); + case "Ok": + expect(res.value).toEqual({ + tag: "number", + value: (x + y + z) / 3, + }); + } + } + }) + ); + }); +}); diff --git a/packages/squiggle-lang/package.json b/packages/squiggle-lang/package.json index fa551fae..efec7810 100644 --- a/packages/squiggle-lang/package.json +++ b/packages/squiggle-lang/package.json @@ -44,6 +44,7 @@ "moduleserve": "0.9.1", "ts-jest": "^27.1.4", "ts-loader": "^9.2.8", + "fast-check": "2.24.0", "typescript": "^4.6.3", "webpack": "^5.72.0", "webpack-cli": "^4.9.2" diff --git a/yarn.lock b/yarn.lock index 1bd0a6cc..8f6746b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8417,6 +8417,13 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" +fast-check@2.24.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-2.24.0.tgz#39f85586862108a4de6394c5196ebcf8b76b6c8b" + integrity sha512-iNXbN90lbabaCUfnW5jyXYPwMJLFYl09eJDkXA9ZoidFlBK63gNRvcKxv+8D1OJ1kIYjwBef4bO/K3qesUeWLQ== + dependencies: + pure-rand "^5.0.1" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3, fast-deep-equal@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -13573,6 +13580,11 @@ pure-color@^1.2.0: resolved "https://registry.yarnpkg.com/pure-color/-/pure-color-1.3.0.tgz#1fe064fb0ac851f0de61320a8bf796836422f33e" integrity sha1-H+Bk+wrIUfDeYTIKi/eWg2Qi8z4= +pure-rand@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-5.0.1.tgz#97a287b4b4960b2a3448c0932bf28f2405cac51d" + integrity sha512-ksWccjmXOHU2gJBnH0cK1lSYdvSZ0zLoCMSz/nTGh6hDvCSgcRxDyIcOBD6KNxFz3xhMPm/T267Tbe2JRymKEQ== + q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"