Merge pull request #38 from QURIresearch/rescript-refactor

Rescript refactor
This commit is contained in:
Ozzie Gooen 2022-03-03 14:57:02 -05:00 committed by GitHub
commit 20ef47d170
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
189 changed files with 135541 additions and 81629 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

19
.github/workflows/lang-jest.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: Squiggle Lang Jest Tests
on: [push]
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: packages/squiggle-lang
steps:
- uses: actions/checkout@v2
- name: Install Packages
run: yarn
- name: Build rescript
run: yarn run build
- name: Run tests
run: yarn test

19
.gitignore vendored
View File

@ -1,16 +1,5 @@
.DS_Store
.merlin
.bsb.lock
npm-debug.log
/node_modules/
.cache
.cache/*
dist
lib/*
*.cache
build
node_modules
yarn-error.log
*.bs.js
# Local Netlify folder
.netlify
.idea
.cache
.merlin
.parcel-cache

View File

@ -2,18 +2,31 @@
This is an experiment DSL/language for making probabilistic estimates.
## DistPlus
We have a custom library called DistPlus to handle distributions with additional metadata. This helps handle mixed distributions (continuous + discrete), a cache for a cdf, possible unit types (specific times are supported), and limited domains.
This monorepo has several packages that can be used for various purposes. All
the packages can be found in `packages`.
## Running
`@squiggle/lang` in `packages/squiggle-lang` contains the core language, particularly
an interface to parse squiggle expressions and return descriptions of distributions
or results.
Currently it only has a few very simple models.
`@squiggle/components` in `packages/components` contains React components that
can be passed squiggle strings as props, and return a presentation of the result
of the calculation.
```
yarn
yarn run start
yarn run parcel
```
`@squiggle/playground` in `packages/playground` contains a website for a playground
for squiggle. This website is hosted at `playground.squiggle-language.com`
`@squiggle/website` in `packages/website` The main descriptive website for squiggle,
it is hosted at `squiggle-language.com`.
The playground depends on the components library which then depends on the language.
This means that if you wish to work on the components library, you will need
to package the language, and for the playground to work, you will need to package
the components library and the playground.
Scripts are available for you in the root directory to do important activities,
such as:
`yarn build:lang`. Builds and packages the language
`yarn storybook:components`. Hosts the component storybook
## Expected future setup
![setup](https://raw.githubusercontent.com/foretold-app/widedomain/master/Screen%20Shot%202020-06-30%20at%208.27.32%20AM.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 KiB

View File

@ -1,13 +0,0 @@
open Jest;
open Expect;
describe("Bandwidth", () => {
test("nrd0()", () => {
let data = [|1., 4., 3., 2.|];
expect(Bandwidth.nrd0(data)) |> toEqual(0.7625801874014622);
});
test("nrd()", () => {
let data = [|1., 4., 3., 2.|];
expect(Bandwidth.nrd(data)) |> toEqual(0.8981499984950554);
});
});

View File

@ -1,415 +0,0 @@
open Jest;
open Expect;
let shape: DistTypes.xyShape = {xs: [|1., 4., 8.|], ys: [|8., 9., 2.|]};
// let makeTest = (~only=false, str, item1, item2) =>
// only
// ? Only.test(str, () =>
// expect(item1) |> toEqual(item2)
// )
// : test(str, () =>
// expect(item1) |> toEqual(item2)
// );
// let makeTestCloseEquality = (~only=false, str, item1, item2, ~digits) =>
// only
// ? Only.test(str, () =>
// expect(item1) |> toBeSoCloseTo(item2, ~digits)
// )
// : test(str, () =>
// expect(item1) |> toBeSoCloseTo(item2, ~digits)
// );
// describe("Shape", () => {
// describe("Continuous", () => {
// open Continuous;
// let continuous = make(`Linear, shape, None);
// makeTest("minX", T.minX(continuous), 1.0);
// makeTest("maxX", T.maxX(continuous), 8.0);
// makeTest(
// "mapY",
// T.mapY(r => r *. 2.0, continuous) |> getShape |> (r => r.ys),
// [|16., 18.0, 4.0|],
// );
// describe("xToY", () => {
// describe("when Linear", () => {
// makeTest(
// "at 4.0",
// T.xToY(4., continuous),
// {continuous: 9.0, discrete: 0.0},
// );
// // Note: This below is weird to me, I'm not sure if it's what we want really.
// makeTest(
// "at 0.0",
// T.xToY(0., continuous),
// {continuous: 8.0, discrete: 0.0},
// );
// makeTest(
// "at 5.0",
// T.xToY(5., continuous),
// {continuous: 7.25, discrete: 0.0},
// );
// makeTest(
// "at 10.0",
// T.xToY(10., continuous),
// {continuous: 2.0, discrete: 0.0},
// );
// });
// describe("when Stepwise", () => {
// let continuous = make(`Stepwise, shape, None);
// makeTest(
// "at 4.0",
// T.xToY(4., continuous),
// {continuous: 9.0, discrete: 0.0},
// );
// makeTest(
// "at 0.0",
// T.xToY(0., continuous),
// {continuous: 0.0, discrete: 0.0},
// );
// makeTest(
// "at 5.0",
// T.xToY(5., continuous),
// {continuous: 9.0, discrete: 0.0},
// );
// makeTest(
// "at 10.0",
// T.xToY(10., continuous),
// {continuous: 2.0, discrete: 0.0},
// );
// });
// });
// makeTest(
// "integral",
// T.Integral.get(~cache=None, continuous) |> getShape,
// {xs: [|1.0, 4.0, 8.0|], ys: [|0.0, 25.5, 47.5|]},
// );
// makeTest(
// "toLinear",
// {
// let continuous =
// make(`Stepwise, {xs: [|1., 4., 8.|], ys: [|0.1, 5., 1.0|]}, None);
// continuous |> toLinear |> E.O.fmap(getShape);
// },
// Some({
// xs: [|1.00007, 1.00007, 4.0, 4.00007, 8.0, 8.00007|],
// ys: [|0.0, 0.1, 0.1, 5.0, 5.0, 1.0|],
// }),
// );
// makeTest(
// "toLinear",
// {
// let continuous = make(`Stepwise, {xs: [|0.0|], ys: [|0.3|]}, None);
// continuous |> toLinear |> E.O.fmap(getShape);
// },
// Some({xs: [|0.0|], ys: [|0.3|]}),
// );
// makeTest(
// "integralXToY",
// T.Integral.xToY(~cache=None, 0.0, continuous),
// 0.0,
// );
// makeTest(
// "integralXToY",
// T.Integral.xToY(~cache=None, 2.0, continuous),
// 8.5,
// );
// makeTest(
// "integralXToY",
// T.Integral.xToY(~cache=None, 100.0, continuous),
// 47.5,
// );
// makeTest(
// "integralEndY",
// continuous
// |> T.normalize //scaleToIntegralSum(~intendedSum=1.0)
// |> T.Integral.sum(~cache=None),
// 1.0,
// );
// });
// describe("Discrete", () => {
// open Discrete;
// let shape: DistTypes.xyShape = {
// xs: [|1., 4., 8.|],
// ys: [|0.3, 0.5, 0.2|],
// };
// let discrete = make(shape, None);
// makeTest("minX", T.minX(discrete), 1.0);
// makeTest("maxX", T.maxX(discrete), 8.0);
// makeTest(
// "mapY",
// T.mapY(r => r *. 2.0, discrete) |> (r => getShape(r).ys),
// [|0.6, 1.0, 0.4|],
// );
// makeTest(
// "xToY at 4.0",
// T.xToY(4., discrete),
// {discrete: 0.5, continuous: 0.0},
// );
// makeTest(
// "xToY at 0.0",
// T.xToY(0., discrete),
// {discrete: 0.0, continuous: 0.0},
// );
// makeTest(
// "xToY at 5.0",
// T.xToY(5., discrete),
// {discrete: 0.0, continuous: 0.0},
// );
// makeTest(
// "scaleBy",
// scaleBy(~scale=4.0, discrete),
// make({xs: [|1., 4., 8.|], ys: [|1.2, 2.0, 0.8|]}, None),
// );
// makeTest(
// "normalize, then scale by 4.0",
// discrete
// |> T.normalize
// |> scaleBy(~scale=4.0),
// make({xs: [|1., 4., 8.|], ys: [|1.2, 2.0, 0.8|]}, None),
// );
// makeTest(
// "scaleToIntegralSum: back and forth",
// discrete
// |> T.normalize
// |> scaleBy(~scale=4.0)
// |> T.normalize,
// discrete,
// );
// makeTest(
// "integral",
// T.Integral.get(~cache=None, discrete),
// Continuous.make(
// `Stepwise,
// {xs: [|1., 4., 8.|], ys: [|0.3, 0.8, 1.0|]},
// None
// ),
// );
// makeTest(
// "integral with 1 element",
// T.Integral.get(~cache=None, Discrete.make({xs: [|0.0|], ys: [|1.0|]}, None)),
// Continuous.make(`Stepwise, {xs: [|0.0|], ys: [|1.0|]}, None),
// );
// makeTest(
// "integralXToY",
// T.Integral.xToY(~cache=None, 6.0, discrete),
// 0.9,
// );
// makeTest("integralEndY", T.Integral.sum(~cache=None, discrete), 1.0);
// makeTest("mean", T.mean(discrete), 3.9);
// makeTestCloseEquality(
// "variance",
// T.variance(discrete),
// 5.89,
// ~digits=7,
// );
// });
// describe("Mixed", () => {
// open Distributions.Mixed;
// let discreteShape: DistTypes.xyShape = {
// xs: [|1., 4., 8.|],
// ys: [|0.3, 0.5, 0.2|],
// };
// let discrete = Discrete.make(discreteShape, None);
// let continuous =
// Continuous.make(
// `Linear,
// {xs: [|3., 7., 14.|], ys: [|0.058, 0.082, 0.124|]},
// None
// )
// |> Continuous.T.normalize; //scaleToIntegralSum(~intendedSum=1.0);
// let mixed = Mixed.make(
// ~continuous,
// ~discrete,
// );
// makeTest("minX", T.minX(mixed), 1.0);
// makeTest("maxX", T.maxX(mixed), 14.0);
// makeTest(
// "mapY",
// T.mapY(r => r *. 2.0, mixed),
// Mixed.make(
// ~continuous=
// Continuous.make(
// `Linear,
// {
// xs: [|3., 7., 14.|],
// ys: [|
// 0.11588411588411589,
// 0.16383616383616384,
// 0.24775224775224775,
// |],
// },
// None
// ),
// ~discrete=Discrete.make({xs: [|1., 4., 8.|], ys: [|0.6, 1.0, 0.4|]}, None)
// ),
// );
// makeTest(
// "xToY at 4.0",
// T.xToY(4., mixed),
// {discrete: 0.25, continuous: 0.03196803196803197},
// );
// makeTest(
// "xToY at 0.0",
// T.xToY(0., mixed),
// {discrete: 0.0, continuous: 0.028971028971028972},
// );
// makeTest(
// "xToY at 5.0",
// T.xToY(7., mixed),
// {discrete: 0.0, continuous: 0.04095904095904096},
// );
// makeTest("integralEndY", T.Integral.sum(~cache=None, mixed), 1.0);
// makeTest(
// "scaleBy",
// Mixed.scaleBy(~scale=2.0, mixed),
// Mixed.make(
// ~continuous=
// Continuous.make(
// `Linear,
// {
// xs: [|3., 7., 14.|],
// ys: [|
// 0.11588411588411589,
// 0.16383616383616384,
// 0.24775224775224775,
// |],
// },
// None
// ),
// ~discrete=Discrete.make({xs: [|1., 4., 8.|], ys: [|0.6, 1.0, 0.4|]}, None),
// ),
// );
// makeTest(
// "integral",
// T.Integral.get(~cache=None, mixed),
// Continuous.make(
// `Linear,
// {
// xs: [|1.00007, 1.00007, 3., 4., 4.00007, 7., 8., 8.00007, 14.|],
// ys: [|
// 0.0,
// 0.0,
// 0.15,
// 0.18496503496503497,
// 0.4349674825174825,
// 0.5398601398601399,
// 0.5913086913086913,
// 0.6913122927072927,
// 1.0,
// |],
// },
// None,
// ),
// );
// });
// describe("Distplus", () => {
// open DistPlus;
// let discreteShape: DistTypes.xyShape = {
// xs: [|1., 4., 8.|],
// ys: [|0.3, 0.5, 0.2|],
// };
// let discrete = Discrete.make(discreteShape, None);
// let continuous =
// Continuous.make(
// `Linear,
// {xs: [|3., 7., 14.|], ys: [|0.058, 0.082, 0.124|]},
// None
// )
// |> Continuous.T.normalize; //scaleToIntegralSum(~intendedSum=1.0);
// let mixed =
// Mixed.make(
// ~continuous,
// ~discrete,
// );
// let distPlus =
// DistPlus.make(
// ~shape=Mixed(mixed),
// ~squiggleString=None,
// (),
// );
// makeTest("minX", T.minX(distPlus), 1.0);
// makeTest("maxX", T.maxX(distPlus), 14.0);
// makeTest(
// "xToY at 4.0",
// T.xToY(4., distPlus),
// {discrete: 0.25, continuous: 0.03196803196803197},
// );
// makeTest(
// "xToY at 0.0",
// T.xToY(0., distPlus),
// {discrete: 0.0, continuous: 0.028971028971028972},
// );
// makeTest(
// "xToY at 5.0",
// T.xToY(7., distPlus),
// {discrete: 0.0, continuous: 0.04095904095904096},
// );
// makeTest("integralEndY", T.Integral.sum(~cache=None, distPlus), 1.0);
// makeTest(
// "integral",
// T.Integral.get(~cache=None, distPlus) |> T.toContinuous,
// Some(
// Continuous.make(
// `Linear,
// {
// xs: [|1.00007, 1.00007, 3., 4., 4.00007, 7., 8., 8.00007, 14.|],
// ys: [|
// 0.0,
// 0.0,
// 0.15,
// 0.18496503496503497,
// 0.4349674825174825,
// 0.5398601398601399,
// 0.5913086913086913,
// 0.6913122927072927,
// 1.0,
// |],
// },
// None,
// ),
// ),
// );
// });
// describe("Shape", () => {
// let mean = 10.0;
// let stdev = 4.0;
// let variance = stdev ** 2.0;
// let numSamples = 10000;
// open Distributions.Shape;
// let normal: SymbolicTypes.symbolicDist = `Normal({mean, stdev});
// let normalShape = ExpressionTree.toShape(numSamples, `SymbolicDist(normal));
// let lognormal = SymbolicDist.Lognormal.fromMeanAndStdev(mean, stdev);
// let lognormalShape = ExpressionTree.toShape(numSamples, `SymbolicDist(lognormal));
// makeTestCloseEquality(
// "Mean of a normal",
// T.mean(normalShape),
// mean,
// ~digits=2,
// );
// makeTestCloseEquality(
// "Variance of a normal",
// T.variance(normalShape),
// variance,
// ~digits=1,
// );
// makeTestCloseEquality(
// "Mean of a lognormal",
// T.mean(lognormalShape),
// mean,
// ~digits=2,
// );
// makeTestCloseEquality(
// "Variance of a lognormal",
// T.variance(lognormalShape),
// variance,
// ~digits=0,
// );
// });
// });

View File

@ -1,57 +0,0 @@
open Jest;
open Expect;
let makeTest = (~only=false, str, item1, item2) =>
only
? Only.test(str, () =>
expect(item1) |> toEqual(item2)
)
: test(str, () =>
expect(item1) |> toEqual(item2)
);
let evalParams: ExpressionTypes.ExpressionTree.evaluationParams = {
samplingInputs: {
sampleCount: 1000,
outputXYPoints: 10000,
kernelWidth: None,
shapeLength: 1000,
},
environment:
[|
("K", `SymbolicDist(`Float(1000.0))),
("M", `SymbolicDist(`Float(1000000.0))),
("B", `SymbolicDist(`Float(1000000000.0))),
("T", `SymbolicDist(`Float(1000000000000.0))),
|]
->Belt.Map.String.fromArray,
evaluateNode: ExpressionTreeEvaluator.toLeaf,
};
let shape1: DistTypes.xyShape = {xs: [|1., 4., 8.|], ys: [|0.2, 0.4, 0.8|]};
describe("XYShapes", () => {
describe("logScorePoint", () => {
makeTest(
"When identical",
{
let foo =
HardcodedFunctions.(
makeRenderedDistFloat("scaleMultiply", (dist, float) =>
verticalScaling(`Multiply, dist, float)
)
);
TypeSystem.Function.T.run(
evalParams,
[|
`SymbolicDist(`Float(100.0)),
`SymbolicDist(`Float(1.0)),
|],
foo,
);
},
Error("Sad"),
)
})
});

View File

@ -1,24 +0,0 @@
open Jest;
open Expect;
let makeTest = (~only=false, str, item1, item2) =>
only
? Only.test(str, () =>
expect(item1) |> toEqual(item2)
)
: test(str, () =>
expect(item1) |> toEqual(item2)
);
describe("Lodash", () => {
describe("Lodash", () => {
makeTest("min", Lodash.min([|1, 3, 4|]), 1);
makeTest("max", Lodash.max([|1, 3, 4|]), 4);
makeTest("uniq", Lodash.uniq([|1, 3, 4, 4|]), [|1, 3, 4|]);
makeTest(
"countBy",
Lodash.countBy([|1, 3, 4, 4|], r => r),
Js.Dict.fromArray([|("1", 1), ("3", 1), ("4", 2)|]),
);
})
});

View File

@ -1,51 +0,0 @@
open Jest;
open Expect;
let makeTest = (~only=false, str, item1, item2) =>
only
? Only.test(str, () =>
expect(item1) |> toEqual(item2)
)
: test(str, () =>
expect(item1) |> toEqual(item2)
);
describe("Lodash", () => {
describe("Lodash", () => {
makeTest(
"split",
SamplesToShape.Internals.T.splitContinuousAndDiscrete([|1.432, 1.33455, 2.0|]),
([|1.432, 1.33455, 2.0|], E.FloatFloatMap.empty()),
);
makeTest(
"split",
SamplesToShape.Internals.T.splitContinuousAndDiscrete([|
1.432,
1.33455,
2.0,
2.0,
2.0,
2.0,
|])
|> (((c, disc)) => (c, disc |> E.FloatFloatMap.toArray)),
([|1.432, 1.33455|], [|(2.0, 4.0)|]),
);
let makeDuplicatedArray = count => {
let arr = Belt.Array.range(1, count) |> E.A.fmap(float_of_int);
let sorted = arr |> Belt.SortArray.stableSortBy(_, compare);
E.A.concatMany([|sorted, sorted, sorted, sorted|])
|> Belt.SortArray.stableSortBy(_, compare);
};
let (_, discrete) =
SamplesToShape.Internals.T.splitContinuousAndDiscrete(makeDuplicatedArray(10));
let toArr = discrete |> E.FloatFloatMap.toArray;
makeTest("splitMedium", toArr |> Belt.Array.length, 10);
let (c, discrete) =
SamplesToShape.Internals.T.splitContinuousAndDiscrete(makeDuplicatedArray(500));
let toArr = discrete |> E.FloatFloatMap.toArray;
makeTest("splitMedium", toArr |> Belt.Array.length, 500);
})
});

View File

@ -1,63 +0,0 @@
open Jest;
open Expect;
let makeTest = (~only=false, str, item1, item2) =>
only
? Only.test(str, () =>
expect(item1) |> toEqual(item2)
)
: test(str, () =>
expect(item1) |> toEqual(item2)
);
let shape1: DistTypes.xyShape = {xs: [|1., 4., 8.|], ys: [|0.2, 0.4, 0.8|]};
let shape2: DistTypes.xyShape = {
xs: [|1., 5., 10.|],
ys: [|0.2, 0.5, 0.8|],
};
let shape3: DistTypes.xyShape = {
xs: [|1., 20., 50.|],
ys: [|0.2, 0.5, 0.8|],
};
describe("XYShapes", () => {
describe("logScorePoint", () => {
makeTest(
"When identical",
XYShape.logScorePoint(30, shape1, shape1),
Some(0.0),
);
makeTest(
"When similar",
XYShape.logScorePoint(30, shape1, shape2),
Some(1.658971191043856),
);
makeTest(
"When very different",
XYShape.logScorePoint(30, shape1, shape3),
Some(210.3721280423322),
);
});
// describe("transverse", () => {
// makeTest(
// "When very different",
// XYShape.Transversal._transverse(
// (aCurrent, aLast) => aCurrent +. aLast,
// [|1.0, 2.0, 3.0, 4.0|],
// ),
// [|1.0, 3.0, 6.0, 10.0|],
// )
// });
describe("integrateWithTriangles", () => {
makeTest(
"integrates correctly",
XYShape.Range.integrateWithTriangles(shape1),
Some({
xs: [|1., 4., 8.|],
ys: [|0.0, 0.9000000000000001, 3.3000000000000007|],
}),
)
});
});

View File

@ -1,77 +1,21 @@
{
"name": "estiband",
"version": "0.1.0",
"homepage": "https://foretold-app.github.io/estiband/",
"private": true,
"name": "squiggle",
"scripts": {
"build": "bsb -make-world",
"build:style": "tailwind build src/styles/index.css -o src/styles/tailwind.css",
"start": "bsb -make-world -w -ws _ ",
"clean": "bsb -clean-world",
"parcel": "parcel ./src/index.html --public-url / --no-autoinstall -- watch",
"parcel-build": "parcel build ./src/index.html --no-source-maps --no-autoinstall",
"showcase": "PORT=12345 parcel showcase/index.html",
"server": "moduleserve ./ --port 8000",
"predeploy": "parcel build ./src/index.html --no-source-maps --no-autoinstall",
"deploy": "gh-pages -d dist",
"test": "jest",
"test:ci": "yarn jest",
"watch:test": "jest --watchAll",
"watch:s": "yarn jest -- Converter_test --watch"
"build:lang": "cd packages/squiggle-lang && yarn && yarn build && yarn package",
"storybook:components": "cd packages/components && yarn && yarn storybook",
"build-storybook:components": "cd packages/components && yarn && yarn build-storybook",
"build:components": "cd packages/components && yarn && yarn package",
"build:playground": "cd packages/playground && yarn && yarn parcel-build",
"ci:lang": "yarn workspace @squiggle/lang ci",
"ci:components": "yarn ci:lang && yarn workspace @squiggle/components ci",
"ci:playground": "yarn ci:components && yarn workspace @squiggle/playground ci"
},
"keywords": [
"BuckleScript",
"ReasonReact",
"reason-react"
"workspaces": [
"packages/*"
],
"author": "",
"license": "MIT",
"dependencies": {
"@foretold/components": "0.0.6",
"@glennsl/bs-json": "^5.0.2",
"ace-builds": "^1.4.12",
"antd": "3.17.0",
"autoprefixer": "9.7.4",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"binary-search-tree": "0.2.6",
"bs-ant-design-alt": "2.0.0-alpha.33",
"bs-css": "11.0.0",
"bs-moment": "0.4.5",
"bs-reform": "9.7.1",
"bsb-js": "1.1.7",
"d3": "5.15.0",
"gh-pages": "2.2.0",
"jest": "^25.5.1",
"jstat": "1.9.2",
"lenses-ppx": "5.1.0",
"less": "3.10.3",
"lodash": "4.17.15",
"mathjs": "5.10.3",
"moduleserve": "0.9.1",
"moment": "2.24.0",
"pdfast": "^0.2.0",
"postcss-cli": "7.1.0",
"rationale": "0.2.0",
"react": "^16.10.0",
"react-ace": "^9.2.0",
"react-dom": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0",
"react-use": "^13.27.0",
"react-vega": "^7.4.1",
"reason-react": ">=0.7.0",
"reschema": "1.3.0",
"tailwindcss": "1.2.0",
"vega": "*",
"vega-embed": "6.6.0",
"vega-lite": "*"
"resolutions": {
"@types/react": "17.0.39"
},
"devDependencies": {
"@glennsl/bs-jest": "^0.5.1",
"bs-platform": "7.3.2",
"parcel-bundler": "1.12.4",
"parcel-plugin-bundle-visualiser": "^1.2.0",
"parcel-plugin-less-js-enabled": "1.0.2"
},
"alias": {
"react": "./node_modules/react",
"react-dom": "./node_modules/react-dom"
}
"packageManager": "yarn@1.22.17"
}

25
packages/components/.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
storybook-static
dist

View File

@ -0,0 +1,35 @@
//const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = {
/* webpackFinal: async (config) => {
config.resolve.plugins = [
...(config.resolve.plugins || []),
new TsconfigPathsPlugin({
extensions: config.resolve.extensions,
}),
];
return config;
},*/
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/preset-create-react-app"
],
"framework": "@storybook/react",
"core": {
"builder": "webpack5"
},
typescript: {
check: false,
checkOptions: {},
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
},
},
}

View File

@ -0,0 +1,9 @@
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
}

View File

@ -0,0 +1,6 @@
# Squiggle Components
This package contains all the components for squiggle. These can be used either
as a library or hosted as a [storybook](https://storybook.js.org/).
To run the storybook, run `yarn` then `yarn storybook`.

57037
packages/components/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,79 @@
{
"name": "@squiggle/components",
"version": "0.1.0",
"private": true,
"dependencies": {
"@squiggle/lang": "0.1.9",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.4.0",
"@types/lodash": "^4.14.178",
"@types/node": "^17.0.16",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"cross-env": "^7.0.3",
"lodash": "^4.17.21",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "5.0.0",
"react-vega": "^7.4.4",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.5.5",
"vega": "^5.21.0",
"vega-embed": "^6.20.6",
"vega-lite": "^5.2.0",
"web-vitals": "^2.1.4",
"webpack-cli": "^4.9.2"
},
"scripts": {
"storybook": "cross-env REACT_APP_FAST_REFRESH=false && start-storybook -p 6006 -s public",
"build-storybook": "build-storybook -s public",
"package": "tsc",
"ci": "yarn package"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"overrides": [
{
"files": [
"**/*.stories.*"
],
"rules": {
"import/no-anonymous-default-export": "off"
}
}
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@storybook/addon-actions": "^6.4.18",
"@storybook/addon-essentials": "^6.4.18",
"@storybook/addon-links": "^6.4.18",
"@storybook/builder-webpack5": "^6.4.18",
"@storybook/manager-webpack5": "^6.4.18",
"@storybook/node-logger": "^6.4.18",
"@storybook/preset-create-react-app": "^4.0.0",
"@storybook/react": "^6.4.18",
"webpack": "^5.68.0"
},
"resolutions": {
"@types/react": "17.0.39"
},
"main": "dist/index.js",
"types": "dist/index.d.ts"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Squiggle components"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<title>Squiggle Components</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 107 KiB

View File

@ -0,0 +1,5 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
name = "squiggle-components";
buildInputs = with pkgs; [ nodePackages.yarn nodejs ];
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,355 @@
import * as React from 'react';
import * as _ from 'lodash';
import type { Spec } from 'vega';
import { run } from '@squiggle/lang';
import type { DistPlus, SamplingInputs } from '@squiggle/lang';
import { createClassFromSpec } from 'react-vega';
import * as chartSpecification from './spec-distributions.json'
import * as percentilesSpec from './spec-pertentiles.json'
let SquiggleVegaChart = createClassFromSpec({'spec': chartSpecification as Spec});
let SquigglePercentilesChart = createClassFromSpec({'spec': percentilesSpec as Spec});
export interface SquiggleChartProps {
/** The input string for squiggle */
squiggleString : string,
/** If the output requires monte carlo sampling, the amount of samples */
sampleCount? : number,
/** The amount of points returned to draw the distribution */
outputXYPoints? : number,
kernelWidth? : number,
pointDistLength? : number,
/** If the result is a function, where the function starts */
diagramStart? : number,
/** If the result is a function, where the function ends */
diagramStop? : number,
/** If the result is a function, how many points along the function it samples */
diagramCount? : number
}
export const SquiggleChart : React.FC<SquiggleChartProps> = props => {
let samplingInputs : SamplingInputs = {
sampleCount : props.sampleCount,
outputXYPoints : props.outputXYPoints,
kernelWidth : props.kernelWidth,
pointDistLength : props.pointDistLength
}
let result = run(props.squiggleString, samplingInputs);
console.log(result)
if (result.tag === "Ok") {
let chartResults = result.value.map(chartResult => {
console.log(chartResult)
if(chartResult["NAME"] === "Float"){
return <MakeNumberShower precision={3} number={chartResult["VAL"]} />;
}
else if(chartResult["NAME"] === "DistPlus"){
let shape = chartResult.VAL.pointSetDist;
if(shape.tag === "Continuous"){
let xyShape = shape.value.xyShape;
let totalY = xyShape.ys.reduce((a, b) => a + b);
let total = 0;
let cdf = xyShape.ys.map(y => {
total += y;
return total / totalY;
})
let values = _.zip(cdf, xyShape.xs, xyShape.ys).map(([c, x, y ]) => ({cdf: (c * 100).toFixed(2) + "%", x: x, y: y}));
return (
<SquiggleVegaChart
data={{"con": values}}
/>
);
}
else if(shape.tag === "Discrete"){
let xyShape = shape.value.xyShape;
let totalY = xyShape.ys.reduce((a, b) => a + b);
let total = 0;
let cdf = xyShape.ys.map(y => {
total += y;
return total / totalY;
})
let values = _.zip(cdf, xyShape.xs, xyShape.ys).map(([c, x,y]) => ({cdf: (c * 100).toFixed(2) + "%", x: x, y: y}));
return (
<SquiggleVegaChart
data={{"dis": values}}
/>
);
}
else if(shape.tag === "Mixed"){
let discreteShape = shape.value.discrete.xyShape;
let totalDiscrete = discreteShape.ys.reduce((a, b) => a + b);
let discretePoints = _.zip(discreteShape.xs, discreteShape.ys);
let continuousShape = shape.value.continuous.xyShape;
let continuousPoints = _.zip(continuousShape.xs, continuousShape.ys);
interface labeledPoint {
x: number,
y: number,
type: "discrete" | "continuous"
};
let markedDisPoints : labeledPoint[] = discretePoints.map(([x,y]) => ({x: x, y: y, type: "discrete"}))
let markedConPoints : labeledPoint[] = continuousPoints.map(([x,y]) => ({x: x, y: y, type: "continuous"}))
let sortedPoints = _.sortBy(markedDisPoints.concat(markedConPoints), 'x')
let totalContinuous = 1 - totalDiscrete;
let totalY = continuousShape.ys.reduce((a:number, b:number) => a + b);
let total = 0;
let cdf = sortedPoints.map((point: labeledPoint) => {
if(point.type == "discrete") {
total += point.y;
return total;
}
else if (point.type == "continuous") {
total += point.y / totalY * totalContinuous;
return total;
}
});
interface cdfLabeledPoint {
cdf: string,
x: number,
y: number,
type: "discrete" | "continuous"
}
let cdfLabeledPoint : cdfLabeledPoint[] = _.zipWith(cdf, sortedPoints, (c: number, point: labeledPoint) => ({...point, cdf: (c * 100).toFixed(2) + "%"}))
let continuousValues = cdfLabeledPoint.filter(x => x.type == "continuous")
let discreteValues = cdfLabeledPoint.filter(x => x.type == "discrete")
return (
<SquiggleVegaChart
data={{"con": continuousValues, "dis": discreteValues}}
/>
);
}
}
else if(chartResult.NAME === "Function"){
// We are looking at a function. In this case, we draw a Percentiles chart
let start = props.diagramStart ? props.diagramStart : 0
let stop = props.diagramStop ? props.diagramStop : 10
let count = props.diagramCount ? props.diagramCount : 0.1
let step = (stop - start)/ count
let data = _.range(start, stop, step).map(x => {
if(chartResult.NAME=="Function"){
let result = chartResult.VAL(x);
if(result.tag == "Ok"){
let percentileArray = [
0.01,
0.05,
0.1,
0.2,
0.3,
0.4,
0.5,
0.6,
0.7,
0.8,
0.9,
0.95,
0.99
]
let percentiles = getPercentiles(percentileArray, result.value);
return {
"x": x,
"p1": percentiles[0],
"p5": percentiles[1],
"p10": percentiles[2],
"p20": percentiles[3],
"p30": percentiles[4],
"p40": percentiles[5],
"p50": percentiles[6],
"p60": percentiles[7],
"p70": percentiles[8],
"p80": percentiles[9],
"p90": percentiles[10],
"p95": percentiles[11],
"p99": percentiles[12]
}
}
}
return 0;
})
return <SquigglePercentilesChart data={{"facet": data}} />
}
})
return <>{chartResults}</>;
}
else if(result.tag == "Error") {
// At this point, we came across an error. What was our error?
return (<p>{"Error parsing Squiggle: " + result.value}</p>)
}
return (<p>{"Invalid Response"}</p>)
};
function getPercentiles(percentiles:number[], t : DistPlus) {
if(t.pointSetDist.tag == "Discrete") {
let total = 0;
let maxX = _.max(t.pointSetDist.value.xyShape.xs)
let bounds = percentiles.map(_ => maxX);
_.zipWith(t.pointSetDist.value.xyShape.xs,t.pointSetDist.value.xyShape.ys, (x,y) => {
total += y
percentiles.forEach((v, i) => {
if(total > v && bounds[i] == maxX){
bounds[i] = x
}
})
});
return bounds;
}
else if(t.pointSetDist.tag == "Continuous"){
let total = 0;
let maxX = _.max(t.pointSetDist.value.xyShape.xs)
let totalY = _.sum(t.pointSetDist.value.xyShape.ys)
let bounds = percentiles.map(_ => maxX);
_.zipWith(t.pointSetDist.value.xyShape.xs,t.pointSetDist.value.xyShape.ys, (x,y) => {
total += y / totalY;
percentiles.forEach((v, i) => {
if(total > v && bounds[i] == maxX){
bounds[i] = x
}
})
});
return bounds;
}
else if(t.pointSetDist.tag == "Mixed"){
let discreteShape = t.pointSetDist.value.discrete.xyShape;
let totalDiscrete = discreteShape.ys.reduce((a, b) => a + b);
let discretePoints = _.zip(discreteShape.xs, discreteShape.ys);
let continuousShape = t.pointSetDist.value.continuous.xyShape;
let continuousPoints = _.zip(continuousShape.xs, continuousShape.ys);
interface labeledPoint {
x: number,
y: number,
type: "discrete" | "continuous"
};
let markedDisPoints : labeledPoint[] = discretePoints.map(([x,y]) => ({x: x, y: y, type: "discrete"}))
let markedConPoints : labeledPoint[] = continuousPoints.map(([x,y]) => ({x: x, y: y, type: "continuous"}))
let sortedPoints = _.sortBy(markedDisPoints.concat(markedConPoints), 'x')
let totalContinuous = 1 - totalDiscrete;
let totalY = continuousShape.ys.reduce((a:number, b:number) => a + b);
let total = 0;
let maxX = _.max(sortedPoints.map(x => x.x));
let bounds = percentiles.map(_ => maxX);
sortedPoints.map((point: labeledPoint) => {
if(point.type == "discrete") {
total += point.y;
}
else if (point.type == "continuous") {
total += point.y / totalY * totalContinuous;
}
percentiles.forEach((v,i) => {
if(total > v && bounds[i] == maxX){
bounds[i] = total;
}
})
return total;
});
return bounds;
}
}
function MakeNumberShower(props: {number: number, precision :number}){
let numberWithPresentation = numberShow(props.number, props.precision);
return (
<span>
{numberWithPresentation.value}
{numberWithPresentation.symbol}
{numberWithPresentation.power ?
<span>
{'\u00b710'}
<span style={{fontSize: "0.6em", verticalAlign: "super"}}>
{numberWithPresentation.power}
</span>
</span>
: <></>}
</span>
);
}
const orderOfMagnitudeNum = (n:number) => {
return Math.pow(10, n);
};
// 105 -> 3
const orderOfMagnitude = (n:number) => {
return Math.floor(Math.log(n) / Math.LN10 + 0.000000001);
};
function withXSigFigs(number:number, sigFigs:number) {
const withPrecision = number.toPrecision(sigFigs);
const formatted = Number(withPrecision);
return `${formatted}`;
}
class NumberShower {
number: number
precision: number
constructor(number:number, precision = 2) {
this.number = number;
this.precision = precision;
}
convert() {
const number = Math.abs(this.number);
const response = this.evaluate(number);
if (this.number < 0) {
response.value = '-' + response.value;
}
return response
}
metricSystem(number: number, order: number) {
const newNumber = number / orderOfMagnitudeNum(order);
const precision = this.precision;
return `${withXSigFigs(newNumber, precision)}`;
}
evaluate(number: number) {
if (number === 0) {
return { value: this.metricSystem(0, 0) }
}
const order = orderOfMagnitude(number);
if (order < -2) {
return { value: this.metricSystem(number, order), power: order };
} else if (order < 4) {
return { value: this.metricSystem(number, 0) };
} else if (order < 6) {
return { value: this.metricSystem(number, 3), symbol: 'K' };
} else if (order < 9) {
return { value: this.metricSystem(number, 6), symbol: 'M' };
} else if (order < 12) {
return { value: this.metricSystem(number, 9), symbol: 'B' };
} else if (order < 15) {
return { value: this.metricSystem(number, 12), symbol: 'T' };
} else {
return { value: this.metricSystem(number, order), power: order };
}
}
}
export function numberShow(number: number, precision = 2) {
const ns = new NumberShower(number, precision);
return ns.convert();
}

View File

@ -0,0 +1 @@
export { SquiggleChart } from './SquiggleChart';

View File

@ -0,0 +1,122 @@
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"description": "A basic area chart example.",
"width": 500,
"height": 200,
"padding": 5,
"data": [{"name": "con"}, {"name": "dis"}],
"signals": [
{
"name": "mousex",
"description": "x position of mouse",
"update": "0",
"on": [{"events": "mousemove", "update": "1-x()/width"}]
},
{
"name": "xscale",
"description": "The transform of the x scale",
"value": 1.0,
"bind": {
"input": "range",
"min": 0.1,
"max": 1
}
},
{
"name": "yscale",
"description": "The transform of the y scale",
"value": 1.0,
"bind": {
"input": "range",
"min": 0.1,
"max": 1
}
}
],
"scales": [{
"name": "xscale",
"type": "pow",
"exponent": {"signal": "xscale"},
"range": "width",
"zero": false,
"nice": false,
"domain": {
"fields": [
{ "data": "con", "field": "x"},
{ "data": "dis", "field": "x"}
]
}
}, {
"name": "yscale",
"type": "pow",
"exponent": {"signal": "yscale"},
"range": "height",
"nice": true,
"zero": true,
"domain": {
"fields": [
{ "data": "con", "field": "y"},
{ "data": "dis", "field": "y"}
]
}
}
],
"axes": [
{"orient": "bottom", "scale": "xscale", "tickCount": 20},
{"orient": "left", "scale": "yscale"}
],
"marks": [
{
"type": "area",
"from": {"data": "con"},
"encode": {
"enter": {
"tooltip": {"signal": "datum.cdf"}
},
"update": {
"x": {"scale": "xscale", "field": "x"},
"y": {"scale": "yscale", "field": "y"},
"y2": {"scale": "yscale", "value": 0},
"fill": {
"signal": "{gradient: 'linear', x1: 1, y1: 1, x2: 0, y2: 1, stops: [ {offset: 0.0, color: 'steelblue'}, {offset: clamp(mousex, 0, 1), color: 'steelblue'}, {offset: clamp(mousex, 0, 1), color: 'blue'}, {offset: 1.0, color: 'blue'} ] }"
},
"interpolate": {"value": "monotone"},
"fillOpacity": {"value": 1}
}
}
},
{
"type": "rect",
"from": {"data": "dis"},
"encode": {
"enter": {
"y2": {"scale": "yscale", "value": 0},
"width": {"value": 1}
},
"update": {
"x": {"scale": "xscale", "field": "x"},
"y": {"scale": "yscale", "field": "y"}
}
}
},
{
"type": "symbol",
"from": {"data": "dis"},
"encode": {
"enter": {
"shape": {"value": "circle"},
"width": {"value": 5},
"tooltip": {"signal": "datum.y"}
},
"update": {
"x": {"scale": "xscale", "field": "x"},
"y": {"scale": "yscale", "field": "y"}
}
}
}
]
}

View File

@ -0,0 +1,9 @@
import { Meta } from '@storybook/addon-docs';
<Meta title="Squiggle/Introduction" />
This is the component library for Squiggle. All of these components are react
components, and can be used in any application that you see fit.
Currently, the only component that is provided is the SquiggleChart component.
This component allows you to render the result of a squiggle expression.

View File

@ -0,0 +1 @@
{"version":3,"file":"SquiggleChart.stories.js","sourceRoot":"","sources":["SquiggleChart.stories.tsx"],"names":[],"mappings":";;;AAAA,6BAA8B;AAC9B,iDAA+C;AAG/C,qBAAe;IACb,KAAK,EAAE,uBAAuB;IAC9B,SAAS,EAAE,6BAAa;CACzB,CAAA;AAED,IAAM,QAAQ,GAAG,UAAC,EAAgB;QAAf,cAAc,oBAAA;IAAM,OAAA,oBAAC,6BAAa,IAAC,cAAc,EAAE,cAAc,GAAI;AAAjD,CAAiD,CAAA;AAE3E,QAAA,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AACxC,eAAO,CAAC,IAAI,GAAG;IACb,cAAc,EAAE,cAAc;CAC/B,CAAC"}

View File

@ -0,0 +1,82 @@
import { SquiggleChart } from '../SquiggleChart'
import { Canvas, Meta, Story, Props } from '@storybook/addon-docs';
<Meta title="Squiggle/SquiggleChart" component={ SquiggleChart } />
export const Template = SquiggleChart
# Squiggle Chart
Squiggle chart evaluates squiggle expressions, and then returns a graph representing
the result of a squiggle expression.
A squiggle expression can have three different types of returns. A distribution,
a constant, and a function.
A distribution means that the result forms a probability distribution. This
could be continuous, discrete or mixed.
## Distributions
An example of a normal distribution is:
<Canvas>
<Story
name="Normal"
args={{
squiggleString: "normal(5,2)"
}}>
{Template.bind({})}
</Story>
</Canvas>
An example of a Discrete distribution is:
<Canvas>
<Story
name="Discrete"
args={{
squiggleString: "mm(0, 1, [0.5, 0.5])"
}}>
{Template.bind({})}
</Story>
</Canvas>
An example of a Mixed distribution is:
<Canvas>
<Story
name="Mixed"
args={{
squiggleString: "mm(0, 5 to 10, [0.5, 0.5])"
}}>
{Template.bind({})}
</Story>
</Canvas>
## Constants
A constant is a simple number as a result. This has special formatting rules
to allow large and small numbers being printed cleanly.
<Canvas>
<Story
name="Constant"
args={{
squiggleString: "500000 * 5000000"
}}>
{Template.bind({})}
</Story>
</Canvas>
## Functions
Finally, a function can be returned, and this shows how the distribution changes
over the axis between x = 0 and 10.
<Canvas>
<Story
name="Function"
args={{
squiggleString: "f(x) = normal(x,x)\nf"
}}>
{Template.bind({})}
</Story>
</Canvas>
<Props of={SquiggleChart} />

View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "commonjs",
"jsx": "react",
"noImplicitAny": false,
"esModuleInterop": true,
"removeComments": true,
"preserveConstEnums": true,
"resolveJsonModule": true,
"outDir": "./dist",
"declarationDir": "./dist",
"declaration": true,
"sourceMap": true
},
"target": "ES6",
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.spec.ts"]
}

16
packages/playground/.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
.DS_Store
.merlin
.bsb.lock
npm-debug.log
/node_modules/
.cache
.cache/*
dist
lib/*
*.cache
build
yarn-error.log
*.bs.js
# Local Netlify folder
.netlify
.idea

View File

@ -0,0 +1,19 @@
# Squiggle Playground
This repository contains the squiggle playground, a small web interface
for playing around with squiggle concepts.
It depends on `@squiggle/components` and `@squiggle/lang` so both of them will
need to be packaged for this to work. This can be done from the root directory
with
```
yarn build:lang
yarn build:components
```
Then, starting the playground can be done with:
```
yarn parcel
```

View File

@ -0,0 +1,54 @@
{
"name": "@squiggle/playground",
"version": "0.1.0",
"homepage": "https://foretold-app.github.io/estiband/",
"scripts": {
"parcel": "parcel ./src/index.html",
"parcel-build": "parcel build ./src/index.html --no-source-maps --no-autoinstall --no-scope-hoist",
"deploy": "gh-pages -d dist",
"ci": "yarn parcel-build"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"@emotion/react": "^11.8.1",
"@squiggle/lang": "^0.1.9",
"ace-builds": "^1.4.12",
"antd": "^4.18.5",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"binary-search-tree": "0.2.6",
"css-loader": "^6.6.0",
"gh-pages": "2.2.0",
"jstat": "1.9.2",
"lenses-ppx": "5.1.0",
"less": "3.10.3",
"lodash": "4.17.15",
"mathjs": "5.10.3",
"moduleserve": "0.9.1",
"moment": "2.24.0",
"pdfast": "^0.2.0",
"rationale": "0.2.0",
"react": "17.0.2",
"react-ace": "^9.2.0",
"react-dom": "^17.0.2",
"react-use": "^17.3.2",
"react-vega": "^7.4.4",
"vega": "*",
"vega-embed": "6.6.0",
"vega-lite": "*"
},
"devDependencies": {
"@emotion/babel-plugin": "^11.7.2",
"@parcel/core": "^2.3.2",
"@types/react": "^17.0.39",
"autoprefixer": "^10.4.2",
"docsify": "^4.12.2",
"jest": "^27.5.1",
"parcel": "^2.3.2",
"postcss": "^8.4.7",
"postcss-cli": "^9.1.0",
"tailwindcss": "^3.0.23",
"typescript": "^4.6.2"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -0,0 +1,5 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
name = "squiggle";
buildInputs = with pkgs; [ yarn yarn2nix nodePackages.npm ];
}

View File

@ -0,0 +1,9 @@
import React from 'react'
import { render } from "react-dom"
import DistBuilder from "./components/DistBuilder"
var root = document.querySelector("#app")
if (!(root == null)) {
render(<DistBuilder />, root)
}

View File

@ -0,0 +1,34 @@
import React, {FC} from "react";
import AceEditor from "react-ace";
import "ace-builds/src-noconflict/mode-golang";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/ext-language_tools";
import "ace-builds/src-noconflict/keybinding-vim";
interface CodeEditorProps {
value : string,
onChange : (value: string) => void
}
export let CodeEditor : FC<CodeEditorProps> = (props) =>
<AceEditor
value={props.value}
mode="golang"
height="400px"
width="100%"
theme="github"
showGutter={false}
highlightActiveLine={false}
showPrintMargin={false}
onChange={props.onChange}
name="UNIQUE_ID_OF_DIV"
editorProps={{
$blockScrolling: true,
}}
setOptions={{
enableBasicAutocompletion: false,
enableLiveAutocompletion: true,
enableSnippets: true,
}}
/>

View File

@ -0,0 +1,171 @@
import { FC, useState } from "react"
import { SquiggleChart } from "@squiggle/components"
import { CodeEditor } from "./CodeEditor"
import { Form, Input, Card, Row, Col } from "antd"
import { css } from '@emotion/react'
interface FieldFloatProps {
label : string,
className? : string,
value : number,
onChange : (value: number) => void,
}
function FieldFloat(Props: FieldFloatProps) {
let [contents, setContents] = useState(Props.value + "");
return <Form.Item label={Props.label}>
<Input
value={contents}
className={Props.className ? Props.className : ""}
onChange={(e) => setContents(e.target.value)}
onBlur={(_) => {
let result = parseFloat(contents);
if(result != NaN) {
Props.onChange(result)
}
}}
/>
</Form.Item>
}
let rows = css`
>.antCol:firstChild {
paddingLeft: 0.25em;
paddingRight: 0.125em;
}
>.antCol:lastChild {
paddingLeft: 0.125em;
paddingRight: 0.25em;
}
>.antCol:not(:lastChild):not(:lastChild) {
paddingLeft: 0.125em;
paddingRight: 0.125em;
}
`
let parent = css`
.antImportNumber {
width: 100%;
}
.anticon {
verticalAlign: "zero";
}
`
var form = css`
backgroundColor: #eee;
padding: 1em;
`
var dist = css`
padding: 1em;
`
var spacer = css`
marginTop: 1em;
`
var groupA = css`
.antInputNumberInputs {
backgroundColor: #fff7db;
}
`
var groupB = css`
.antInputNumberInput {
backgroundColor: #eaf4ff;
}
`
var Styles = {
rows: rows,
parent: parent,
form: form,
dist: dist,
spacer: spacer,
groupA: groupA,
groupB: groupB
};
let DistBuilder : FC<{}> = (_: {}) => {
let [squiggleString, setSquiggleString] = useState("mm(normal(5,2), normal(10,2))")
let [sampleCount, setSampleCount] = useState(1000)
let [outputXYPoints, setOutputXYPoints] = useState(1000)
let [pointDistLength, setPointDistLength] = useState(undefined)
let [kernelWidth, setKernelWidth] = useState(undefined)
let [diagramStart, setDiagramStart] = useState(0)
let [diagramStop, setDiagramStop] = useState(10)
let [diagramCount, setDiagramCount] = useState(20)
var demoDist =
<SquiggleChart
squiggleString={squiggleString}
sampleCount={sampleCount}
outputXYPoints={outputXYPoints}
diagramStart={diagramStart}
diagramStop={diagramStop}
diagramCount={diagramCount}
pointDistLength={pointDistLength}
/>
return (
<div className="grid grid-cols-2 gap-4">
<div>
<Card
title="Distribution Form">
<Form>
<Row css={Styles.rows}>
<Col span={24}>
<CodeEditor value={squiggleString} onChange={setSquiggleString} /> </Col>
</Row>
<Row css={Styles.rows}>
<Col span={12}>
<FieldFloat
value={sampleCount}
label="Sample Count"
onChange={setSampleCount}
/> </Col>
<Col span={12}>
<FieldFloat
value={outputXYPoints}
onChange={setOutputXYPoints}
label="Output XY-points" />
</Col>
<Col span={12}>
<FieldFloat
value={pointDistLength}
onChange={setPointDistLength}
label="Downsample To"
/>
</Col>
<Col span={12}>
<FieldFloat
value={kernelWidth}
onChange={setKernelWidth}
label="Kernel Width"
/> </Col>
<Col span={12}>
<FieldFloat
value={diagramStart}
onChange={setDiagramStart}
label="Diagram Start"
/>
</Col>
<Col span={12}>
<FieldFloat
value={diagramStop}
onChange={setDiagramStop}
label="Diagram Stop"
/> </Col>
<Col span={12}>
<FieldFloat
value={diagramCount}
onChange={setDiagramCount}
label="Diagram Count"
/>
</Col>
</Row>
</Form>
</Card>
</div>
{demoDist}
</div>
)
}
export default DistBuilder

View File

@ -5,8 +5,9 @@
<meta charset="UTF-8">
<title>Squiggle Language</title>
<link href="https://fonts.googleapis.com/css?family=Lato:300,400,700,900" rel="stylesheet">
<link href="./styles/antd.css" rel="stylesheet">
<link href="./styles/index.css" rel="stylesheet">
<script src="./Index.re" defer></script>
<script type="module" src="./Index.tsx" defer></script>
</head>
<body>

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,9 @@
module.exports = {
content: [
"./src/components/*.tsx"
],
theme: {
extend: {},
},
plugins: [],
}

View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"module": "commonjs",
"jsx": "react-jsx",
"jsxImportSource": "@emotion/react",
"noImplicitAny": false,
"removeComments": true,
"preserveConstEnums": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"outDir": "./dist",
"declarationDir": "./dist",
"declaration": true,
"sourceMap": true
},
"target": "ES6",
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.spec.ts"]
}

11654
packages/playground/yarn.nix Normal file

File diff suppressed because it is too large Load Diff

18
packages/squiggle-lang/.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
.DS_Store
.merlin
.bsb.lock
npm-debug.log
/node_modules/
.cache
.cache/*
lib/*
*.cache
build
yarn-error.log
*.bs.js
# Local Netlify folder
.netlify
.idea
*.gen.ts
*.gen.js
dist

View File

@ -0,0 +1,64 @@
# Squiggle Language
Squiggle is a language for representing probability distributions, as well as
functions that return probability distributions. Its original intended use is
for improving epistemics around EA decisions.
This package, @squiggle/lang, contains the core language of squiggle. The main
feature revolves around evaluating squiggle expressions. Currently the package
only exports a single function, named "run", which from a squiggle string returns
an object representing the result of the evaluation.
If using this package for tests or as a dependency, typescript typings are available
and recommended to be used.
## Building this package
This package doesn't have any dependencies on any other packages within the monorepo,
so if you wish you can generally ignore lerna or yarn workspaces when dealing
with this package in particular.
First, as per any node package, you will need to install dependencies, we recommend
using [yarn](https://classic.yarnpkg.com/en/).
```bash
yarn
```
This package is mainly written in [ReScript](https://rescript-lang.org/). But has
a typescript interface.
ReScript has an interesting philosophy of not providing much in the way of effective
build tools. Every ReScript file is compiled into .bs.js and .gen.ts files with the same name
and same location, and then you can use these files in other js files to
create your program. To generate these files to build the package, you run
`yarn build`.
```bash
yarn build
```
.gen.ts files are created by [genType](https://rescript-lang.org/docs/gentype/latest/getting-started),
which creates typescript typings for needed parts of the codebase so that they
can be easily used in typescript. These .gen.ts files reference the .bs.js files
generated by rescript.
You can also go `yarn start` for the purposes of watching for file changes and
rebuilding every time there is one.
Finally, `yarn test` runs the current test suite over the language.
You may notice sometimes, that there are errors about the `rationale` package.
If you ever get these errors, `yarn build` should fix this issue. These errors
occur because `yarn build` also needs to create build files that are in `node_modules`.
So if you replace `node_modules` you may need to rebuild to get those files back.
## Distributing this package or using this package from other monorepo packages
If you would like to distribute this package, run `yarn package` to compile all the js
and typescript into the `dist` directory. This `dist` directory code is what's
referenced by other packages in the monorepo.
## Using this package
The return type of this packages only experted function `run` is currently quite
complicated, as it has to return either a number, or a distribution, or even
a representation of a function of distributions. Currently the export is simply
the generated type that rescript creates, and can be quite confusing. We therefore
highly recommend the use of typescript when creating tests or using this package.

View File

@ -0,0 +1,13 @@
open Jest
open Expect
describe("Bandwidth", () => {
test("nrd0()", () => {
let data = [1., 4., 3., 2.]
expect(Bandwidth.nrd0(data)) -> toEqual(0.7625801874014622)
})
test("nrd()", () => {
let data = [1., 4., 3., 2.]
expect(Bandwidth.nrd(data)) -> toEqual(0.8981499984950554)
})
})

View File

@ -1,71 +1,56 @@
open Jest;
open Expect;
open Jest
open Expect
let makeTest = (~only=false, str, item1, item2) =>
only
? Only.test(str, () =>
expect(item1) |> toEqual(item2)
)
: test(str, () =>
expect(item1) |> toEqual(item2)
);
? Only.test(str, () => expect(item1) -> toEqual(item2))
: test(str, () => expect(item1) -> toEqual(item2))
describe("DistTypes", () => {
describe("PointSetTypes", () =>
describe("Domain", () => {
let makeComplete = (yPoint, expectation) =>
makeTest(
"With input: " ++ Js.Float.toString(yPoint),
DistTypes.Domain.yPointToSubYPoint(Complete, yPoint),
PointSetTypes.Domain.yPointToSubYPoint(Complete, yPoint),
expectation,
);
let makeSingle =
(
direction: [ | `left | `right],
excludingProbabilityMass,
yPoint,
expectation,
) =>
)
let makeSingle = (direction: [#left | #right], excludingProbabilityMass, yPoint, expectation) =>
makeTest(
"Excluding: "
++ Js.Float.toString(excludingProbabilityMass)
++ " and yPoint: "
++ Js.Float.toString(yPoint),
DistTypes.Domain.yPointToSubYPoint(
direction == `left
? LeftLimited({xPoint: 3.0, excludingProbabilityMass})
: RightLimited({xPoint: 3.0, excludingProbabilityMass}),
"Excluding: " ++
(Js.Float.toString(excludingProbabilityMass) ++
(" and yPoint: " ++ Js.Float.toString(yPoint))),
PointSetTypes.Domain.yPointToSubYPoint(
direction == #left
? LeftLimited({xPoint: 3.0, excludingProbabilityMass: excludingProbabilityMass})
: RightLimited({xPoint: 3.0, excludingProbabilityMass: excludingProbabilityMass}),
yPoint,
),
expectation,
);
)
let makeDouble = (domain, yPoint, expectation) =>
makeTest(
"Excluding: limits",
DistTypes.Domain.yPointToSubYPoint(domain, yPoint),
expectation,
);
makeTest("Excluding: limits", PointSetTypes.Domain.yPointToSubYPoint(domain, yPoint), expectation)
describe("With Complete Domain", () => {
makeComplete(0.0, Some(0.0));
makeComplete(0.6, Some(0.6));
makeComplete(1.0, Some(1.0));
});
makeComplete(0.0, Some(0.0))
makeComplete(0.6, Some(0.6))
makeComplete(1.0, Some(1.0))
})
describe("With Left Limit", () => {
makeSingle(`left, 0.5, 1.0, Some(1.0));
makeSingle(`left, 0.5, 0.75, Some(0.5));
makeSingle(`left, 0.8, 0.9, Some(0.5));
makeSingle(`left, 0.5, 0.4, None);
makeSingle(`left, 0.5, 0.5, Some(0.0));
});
makeSingle(#left, 0.5, 1.0, Some(1.0))
makeSingle(#left, 0.5, 0.75, Some(0.5))
makeSingle(#left, 0.8, 0.9, Some(0.5))
makeSingle(#left, 0.5, 0.4, None)
makeSingle(#left, 0.5, 0.5, Some(0.0))
})
describe("With Right Limit", () => {
makeSingle(`right, 0.5, 1.0, None);
makeSingle(`right, 0.5, 0.25, Some(0.5));
makeSingle(`right, 0.8, 0.5, None);
makeSingle(`right, 0.2, 0.2, Some(0.25));
makeSingle(`right, 0.5, 0.5, Some(1.0));
makeSingle(`right, 0.5, 0.0, Some(0.0));
makeSingle(`right, 0.5, 0.5, Some(1.0));
});
makeSingle(#right, 0.5, 1.0, None)
makeSingle(#right, 0.5, 0.25, Some(0.5))
makeSingle(#right, 0.8, 0.5, None)
makeSingle(#right, 0.2, 0.2, Some(0.25))
makeSingle(#right, 0.5, 0.5, Some(1.0))
makeSingle(#right, 0.5, 0.0, Some(0.0))
makeSingle(#right, 0.5, 0.5, Some(1.0))
})
describe("With Left and Right Limit", () => {
makeDouble(
LeftAndRightLimited(
@ -74,7 +59,7 @@ describe("DistTypes", () => {
),
0.5,
Some(0.5),
);
)
makeDouble(
LeftAndRightLimited(
{excludingProbabilityMass: 0.1, xPoint: 3.0},
@ -82,7 +67,7 @@ describe("DistTypes", () => {
),
0.2,
Some(0.125),
);
)
makeDouble(
LeftAndRightLimited(
{excludingProbabilityMass: 0.1, xPoint: 3.0},
@ -90,7 +75,7 @@ describe("DistTypes", () => {
),
0.1,
Some(0.0),
);
)
makeDouble(
LeftAndRightLimited(
{excludingProbabilityMass: 0.1, xPoint: 3.0},
@ -98,7 +83,7 @@ describe("DistTypes", () => {
),
0.05,
None,
);
});
)
})
});
})
)

View File

@ -0,0 +1,15 @@
import { run } from '../src/js/index';
describe("A simple result", () => {
test("mean(normal(5,2))", () => {
expect(run("mean(normal(5,2))")).toEqual({ tag: 'Ok', value: [ { NAME: 'Float', VAL: 5 } ] });
});
test("10+10", () => {
let foo = run("normal(5,2)");
expect(1).toEqual(1);
});
test("log(1) = 0", () => {
let foo = run("log(1)");
expect(foo).toEqual({ tag: 'Ok', value: [ { NAME: 'Float', VAL: 0} ]});
})
});

View File

@ -0,0 +1,20 @@
open Jest
open Expect
let makeTest = (~only=false, str, item1, item2) =>
only
? Only.test(str, () => expect(item1) -> toEqual(item2))
: test(str, () => expect(item1) -> toEqual(item2))
describe("Lodash", () =>
describe("Lodash", () => {
makeTest("min", Lodash.min([1, 3, 4]), 1)
makeTest("max", Lodash.max([1, 3, 4]), 4)
makeTest("uniq", Lodash.uniq([1, 3, 4, 4]), [1, 3, 4])
makeTest(
"countBy",
Lodash.countBy([1, 3, 4, 4], r => r),
Js.Dict.fromArray([("1", 1), ("3", 1), ("4", 2)]),
)
})
)

View File

@ -0,0 +1,47 @@
open Jest
open Expect
let makeTest = (~only=false, str, item1, item2) =>
only
? Only.test(str, () => expect(item1) -> toEqual(item2))
: test(str, () => expect(item1) -> toEqual(item2))
describe("Lodash", () =>
describe("Lodash", () => {
makeTest(
"split",
SampleSet.Internals.T.splitContinuousAndDiscrete([1.432, 1.33455, 2.0]),
([1.432, 1.33455, 2.0], E.FloatFloatMap.empty()),
)
makeTest(
"split",
SampleSet.Internals.T.splitContinuousAndDiscrete([
1.432,
1.33455,
2.0,
2.0,
2.0,
2.0,
]) |> (((c, disc)) => (c, disc |> E.FloatFloatMap.toArray)),
([1.432, 1.33455], [(2.0, 4.0)]),
)
let makeDuplicatedArray = count => {
let arr = Belt.Array.range(1, count) |> E.A.fmap(float_of_int)
let sorted = arr |> Belt.SortArray.stableSortBy(_, compare)
E.A.concatMany([sorted, sorted, sorted, sorted]) |> Belt.SortArray.stableSortBy(_, compare)
}
let (_, discrete) = SampleSet.Internals.T.splitContinuousAndDiscrete(
makeDuplicatedArray(10),
)
let toArr = discrete |> E.FloatFloatMap.toArray
makeTest("splitMedium", toArr |> Belt.Array.length, 10)
let (_c, discrete) = SampleSet.Internals.T.splitContinuousAndDiscrete(
makeDuplicatedArray(500),
)
let toArr = discrete |> E.FloatFloatMap.toArray
makeTest("splitMedium", toArr |> Belt.Array.length, 500)
})
)

View File

@ -0,0 +1,51 @@
open Jest
open Expect
let makeTest = (~only=false, str, item1, item2) =>
only
? Only.test(str, () => expect(item1) -> toEqual(item2))
: test(str, () => expect(item1) -> toEqual(item2))
let pointSetDist1: PointSetTypes.xyShape = {xs: [1., 4., 8.], ys: [0.2, 0.4, 0.8]}
let pointSetDist2: PointSetTypes.xyShape = {
xs: [1., 5., 10.],
ys: [0.2, 0.5, 0.8],
}
let pointSetDist3: PointSetTypes.xyShape = {
xs: [1., 20., 50.],
ys: [0.2, 0.5, 0.8],
}
describe("XYShapes", () => {
describe("logScorePoint", () => {
makeTest("When identical", XYShape.logScorePoint(30, pointSetDist1, pointSetDist1), Some(0.0))
makeTest("When similar", XYShape.logScorePoint(30, pointSetDist1, pointSetDist2), Some(1.658971191043856))
makeTest(
"When very different",
XYShape.logScorePoint(30, pointSetDist1, pointSetDist3),
Some(210.3721280423322),
)
})
// describe("transverse", () => {
// makeTest(
// "When very different",
// XYShape.Transversal._transverse(
// (aCurrent, aLast) => aCurrent +. aLast,
// [|1.0, 2.0, 3.0, 4.0|],
// ),
// [|1.0, 3.0, 6.0, 10.0|],
// )
// });
describe("integrateWithTriangles", () =>
makeTest(
"integrates correctly",
XYShape.Range.integrateWithTriangles(pointSetDist1),
Some({
xs: [1., 4., 8.],
ys: [0.0, 0.9000000000000001, 3.3000000000000007],
}),
)
)
})

View File

@ -1,18 +1,11 @@
{
"name": "probExample",
"reason": {
"react-jsx": 3
},
"name": "@squiggle/lang",
"reason": {},
"sources": [
{
"dir": "src",
"subdirs": true
},
{
"dir": "showcase",
"type": "dev",
"subdirs": true
},
{
"dir": "__tests__",
"type": "dev",
@ -33,19 +26,23 @@
"suffix": ".bs.js",
"namespace": true,
"bs-dependencies": [
"@glennsl/bs-jest",
"@glennsl/rescript-jest",
"@glennsl/bs-json",
"@foretold/components",
"bs-ant-design-alt",
"reason-react",
"bs-reform",
"bs-css",
"rationale",
"bs-moment",
"reschema"
"rationale"
],
"gentypeconfig": {
"language": "typescript",
"generatedFileExtension": ".gen.ts",
"module": "commonjs",
"shims": {},
"debug": {
"all": false,
"basic": false
}
},
"refmt": 3,
"ppx-flags": [
"lenses-ppx/ppx"
]
"warnings": {
"number": "+A-42-48-9-30-4-102"
},
"ppx-flags": []
}

1368
packages/squiggle-lang/dist/index.js vendored Normal file

File diff suppressed because one or more lines are too long

393
packages/squiggle-lang/dist/report.html vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};

View File

@ -0,0 +1,47 @@
{
"name": "@squiggle/lang",
"version": "0.1.9",
"homepage": "https://foretold-app.github.io/estiband/",
"scripts": {
"build": "rescript build -with-deps",
"parcel": "parcel build ./src/js/index.js --no-source-maps --no-autoinstall",
"start": "rescript build -w -with-deps",
"clean": "rescript clean",
"test": "jest",
"test:ci": "yarn jest ./__tests__/Lodash__test.re",
"watch:test": "jest --watchAll",
"watch:s": "yarn jest -- Converter_test --watch",
"package": "tsc",
"ci": "yarn build && yarn package"
},
"keywords": [
"Rescript"
],
"author": "Quantified Uncertainty Research Institute",
"license": "MIT",
"dependencies": {
"@glennsl/bs-json": "^5.0.2",
"@rescriptbr/reform": "^11.0.1",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"lodash": "4.17.15",
"mathjs": "5.10.3",
"pdfast": "^0.2.0",
"rationale": "0.2.0"
},
"devDependencies": {
"@glennsl/rescript-jest": "^0.9.0",
"@types/jest": "^27.4.0",
"docsify": "^4.12.2",
"gentype": "^4.3.0",
"jest": "^27.5.1",
"jstat": "1.9.2",
"moduleserve": "0.9.1",
"parcel": "^2.2.1",
"parcel-bundler": "1.12.4",
"rescript": "^9.1.4",
"ts-jest": "^27.1.3",
"typescript": "^4.5.5"
},
"main": "./dist/js/index.js",
"types": "./dist/js/index.d.ts"
}

View File

@ -0,0 +1,5 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
name = "squiggle";
buildInputs = with pkgs; [ yarn yarn2nix nodePackages.npm ];
}

View File

@ -0,0 +1,15 @@
import {runAll} from '../rescript/ProgramEvaluator.gen';
import type { Inputs_SamplingInputs_t as SamplingInputs } from '../rescript/ProgramEvaluator.gen';
export type { SamplingInputs }
export type {t as DistPlus} from '../rescript/pointSetDist/DistPlus.gen';
export let defaultSamplingInputs : SamplingInputs = {
sampleCount : 10000,
outputXYPoints : 10000,
pointDistLength : 1000
}
export function run(squiggleString : string, samplingInputs? : SamplingInputs) {
let si : SamplingInputs = samplingInputs ? samplingInputs : defaultSamplingInputs
return runAll(squiggleString, si)
}

View File

@ -0,0 +1,205 @@
// TODO: This setup is more confusing than it should be, there's more work to do in cleanup here.
module Inputs = {
module SamplingInputs = {
type t = {
sampleCount: option<int>,
outputXYPoints: option<int>,
kernelWidth: option<float>,
pointDistLength: option<int>,
}
}
let defaultRecommendedLength = 100
let defaultShouldDownsample = true
type inputs = {
squiggleString: string,
samplingInputs: SamplingInputs.t,
environment: ASTTypes.environment,
}
let empty: SamplingInputs.t = {
sampleCount: None,
outputXYPoints: None,
kernelWidth: None,
pointDistLength: None,
}
let make = (
~samplingInputs=empty,
~squiggleString,
~environment=ASTTypes.Environment.empty,
(),
): inputs => {
samplingInputs: samplingInputs,
squiggleString: squiggleString,
environment: environment,
}
}
type exportType = [
| #DistPlus(DistPlus.t)
| #Float(float)
| #Function((float) => Belt.Result.t<DistPlus.t,string>)
]
module Internals = {
let addVariable = (
{samplingInputs, squiggleString, environment}: Inputs.inputs,
str,
node,
): Inputs.inputs => {
samplingInputs: samplingInputs,
squiggleString: squiggleString,
environment: ASTTypes.Environment.update(environment, str, _ => Some(
node,
)),
}
type outputs = {
graph: ASTTypes.node,
pointSetDist: PointSetTypes.pointSetDist,
}
let makeOutputs = (graph, shape): outputs => {graph: graph, pointSetDist: shape}
let makeInputs = (inputs: Inputs.inputs): SamplingInputs.samplingInputs => {
sampleCount: inputs.samplingInputs.sampleCount |> E.O.default(10000),
outputXYPoints: inputs.samplingInputs.outputXYPoints |> E.O.default(10000),
kernelWidth: inputs.samplingInputs.kernelWidth,
pointSetDistLength: inputs.samplingInputs.pointDistLength |> E.O.default(10000),
}
let runNode = (inputs, node) =>
AST.toLeaf(makeInputs(inputs), inputs.environment, node)
let runProgram = (inputs: Inputs.inputs, p: ASTTypes.program) => {
let ins = ref(inputs)
p
|> E.A.fmap(x =>
switch x {
| #Assignment(name, node) =>
ins := addVariable(ins.contents, name, node)
None
| #Expression(node) =>
Some(runNode(ins.contents, node) |> E.R.fmap(r => (ins.contents.environment, r)))
}
)
|> E.A.O.concatSomes
|> E.A.R.firstErrorOrOpen
}
let inputsToLeaf = (inputs: Inputs.inputs) =>
Parser.fromString(inputs.squiggleString) |> E.R.bind(_, g => runProgram(inputs, g))
let outputToDistPlus = (inputs: Inputs.inputs, pointSetDist: PointSetTypes.pointSetDist) =>
DistPlus.make(~pointSetDist, ~squiggleString=Some(inputs.squiggleString), ())
}
let renderIfNeeded = (inputs: Inputs.inputs, node: ASTTypes.node): result<
ASTTypes.node,
string,
> =>
node |> (
x =>
switch x {
| #Normalize(_) as n
| #SymbolicDist(_) as n =>
#Render(n)
|> Internals.runNode(inputs)
|> (
x =>
switch x {
| Ok(#RenderedDist(_)) as r => r
| Error(r) => Error(r)
| _ => Error("Didn't render, but intended to")
}
)
| n => Ok(n)
}
)
let rec returnDist = (functionInfo : (array<string>, ASTTypes.node),
inputs : Inputs.inputs,
env : ASTTypes.environment) => {
(input : float) => {
let foo: Inputs.inputs = {...inputs, environment: env};
evaluateFunction(
foo,
functionInfo,
[#SymbolicDist(#Float(input))],
) |> E.R.bind(_, a =>
switch a {
| #DistPlus(d) => Ok(DistPlus.T.normalize(d))
| n =>
Js.log2("Error here", n)
Error("wrong type")
}
)
}
}
// TODO: Consider using ExpressionTypes.ExpressionTree.getFloat or similar in this function
and coersionToExportedTypes = (
inputs,
env: ASTTypes.environment,
node: ASTTypes.node,
): result<exportType, string> =>
node
|> renderIfNeeded(inputs)
|> E.R.bind(_, x =>
switch x {
| #RenderedDist(Discrete({xyShape: {xs: [x], ys: [1.0]}})) => Ok(#Float(x))
| #SymbolicDist(#Float(x)) => Ok(#Float(x))
| #RenderedDist(n) => Ok(#DistPlus(Internals.outputToDistPlus(inputs, n)))
| #Function(n) => Ok(#Function(returnDist(n, inputs, env)))
| n => Error("Didn't output a rendered distribution. Format:" ++ AST.toString(n))
}
)
and evaluateFunction = (
inputs: Inputs.inputs,
fn: (array<string>, ASTTypes.node),
fnInputs,
) => {
let output = AST.runFunction(
Internals.makeInputs(inputs),
inputs.environment,
fnInputs,
fn,
)
output |> E.R.bind(_, coersionToExportedTypes(inputs, inputs.environment))
}
let rec mapM = (f, xs) =>
switch xs {
| [] => Ok([])
| arr =>
switch f(arr[0]) {
| Error(err) => Error(err)
| Ok(val) =>
switch mapM(f, Belt.Array.sliceToEnd(arr, 1)) {
| Error(err) => Error(err)
| Ok(restList) => Ok(Belt.Array.concat([val], restList))
}
}
}
let evaluateProgram = (inputs: Inputs.inputs) =>
inputs
|> Internals.inputsToLeaf
|> E.R.bind(_, xs => mapM(((a, b)) => coersionToExportedTypes(inputs, a, b), xs))
@genType
let runAll = (squiggleString: string, samplingInputs: Inputs.SamplingInputs.t) => {
let inputs = Inputs.make(
~samplingInputs,
~squiggleString,
~environment=[]->Belt.Map.String.fromArray,
(),
)
let response1 = evaluateProgram(inputs);
response1
}

View File

@ -0,0 +1,24 @@
open ASTTypes
let toString = ASTTypes.Node.toString
let envs = (samplingInputs, environment) => {
samplingInputs: samplingInputs,
environment: environment,
evaluateNode: ASTEvaluator.toLeaf,
}
let toLeaf = (samplingInputs, environment, node: node) =>
ASTEvaluator.toLeaf(envs(samplingInputs, environment), node)
let toPointSetDist = (samplingInputs, environment, node: node) =>
switch toLeaf(samplingInputs, environment, node) {
| Ok(#RenderedDist(pointSetDist)) => Ok(pointSetDist)
| Ok(_) => Error("Rendering failed.")
| Error(e) => Error(e)
}
let runFunction = (samplingInputs, environment, inputs, fn: ASTTypes.Function.t) => {
let params = envs(samplingInputs, environment)
ASTTypes.Function.run(params, inputs, fn)
}

View File

@ -0,0 +1,257 @@
open ASTTypes
type tResult = node => result<node, string>
/* Given two random variables A and B, this returns the distribution
of a new variable that is the result of the operation on A and B.
For instance, normal(0, 1) + normal(1, 1) -> normal(1, 2).
In general, this is implemented via convolution. */
module AlgebraicCombination = {
let tryAnalyticalSimplification = (operation, t1: node, t2: node) =>
switch (operation, t1, t2) {
| (operation, #SymbolicDist(d1), #SymbolicDist(d2)) =>
switch SymbolicDist.T.tryAnalyticalSimplification(d1, d2, operation) {
| #AnalyticalSolution(symbolicDist) => Ok(#SymbolicDist(symbolicDist))
| #Error(er) => Error(er)
| #NoSolution => Ok(#AlgebraicCombination(operation, t1, t2))
}
| _ => Ok(#AlgebraicCombination(operation, t1, t2))
}
let combinationByRendering = (evaluationParams, algebraicOp, t1: node, t2: node): result<
node,
string,
> =>
E.R.merge(
Node.ensureIsRenderedAndGetShape(evaluationParams, t1),
Node.ensureIsRenderedAndGetShape(evaluationParams, t2),
) |> E.R.fmap(((a, b)) => #RenderedDist(PointSetDist.combineAlgebraically(algebraicOp, a, b)))
let nodeScore: node => int = x =>
switch x {
| #SymbolicDist(#Float(_)) => 1
| #SymbolicDist(_) => 1000
| #RenderedDist(Discrete(m)) => m.xyShape |> XYShape.T.length
| #RenderedDist(Mixed(_)) => 1000
| #RenderedDist(Continuous(_)) => 1000
| _ => 1000
}
let choose = (t1: node, t2: node) =>
nodeScore(t1) * nodeScore(t2) > 10000 ? #Sampling : #Analytical
let combine = (evaluationParams, algebraicOp, t1: node, t2: node): result<node, string> =>
E.R.merge(
ASTTypes.SamplingDistribution.renderIfIsNotSamplingDistribution(evaluationParams, t1),
ASTTypes.SamplingDistribution.renderIfIsNotSamplingDistribution(evaluationParams, t2),
) |> E.R.bind(_, ((a, b)) =>
switch choose(a, b) {
| #Sampling =>
ASTTypes.SamplingDistribution.combineShapesUsingSampling(
evaluationParams,
algebraicOp,
a,
b,
)
| #Analytical => combinationByRendering(evaluationParams, algebraicOp, a, b)
}
)
let operationToLeaf = (
evaluationParams: evaluationParams,
algebraicOp: Operation.algebraicOperation,
t1: node,
t2: node,
): result<node, string> =>
algebraicOp
|> tryAnalyticalSimplification(_, t1, t2)
|> E.R.bind(_, x =>
switch x {
| #SymbolicDist(_) as t => Ok(t)
| _ => combine(evaluationParams, algebraicOp, t1, t2)
}
)
}
module PointwiseCombination = {
//TODO: This is crude and slow. It forces everything to be pointSetDist, even though much
//of the process could happen on symbolic distributions without a conversion to be a pointSetDist.
let pointwiseAdd = (evaluationParams: evaluationParams, t1: node, t2: node) =>
switch (Node.render(evaluationParams, t1), Node.render(evaluationParams, t2)) {
| (Ok(#RenderedDist(rs1)), Ok(#RenderedDist(rs2))) =>
Ok(
#RenderedDist(
PointSetDist.combinePointwise(
~integralSumCachesFn=(a, b) => Some(a +. b),
~integralCachesFn=(a, b) => Some(
Continuous.combinePointwise(~distributionType=#CDF, \"+.", a, b),
),
\"+.",
rs1,
rs2,
),
),
)
| (Error(e1), _) => Error(e1)
| (_, Error(e2)) => Error(e2)
| _ => Error("Pointwise combination: rendering failed.")
}
let pointwiseCombine = (fn, evaluationParams: evaluationParams, t1: node, t2: node) =>
switch // TODO: construct a function that we can easily sample from, to construct
// a RenderedDist. Use the xMin and xMax of the rendered pointSetDists to tell the sampling function where to look.
// TODO: This should work for symbolic distributions too!
(Node.render(evaluationParams, t1), Node.render(evaluationParams, t2)) {
| (Ok(#RenderedDist(rs1)), Ok(#RenderedDist(rs2))) =>
Ok(#RenderedDist(PointSetDist.combinePointwise(fn, rs1, rs2)))
| (Error(e1), _) => Error(e1)
| (_, Error(e2)) => Error(e2)
| _ => Error("Pointwise combination: rendering failed.")
}
let operationToLeaf = (
evaluationParams: evaluationParams,
pointwiseOp: Operation.pointwiseOperation,
t1: node,
t2: node,
) =>
switch pointwiseOp {
| #Add => pointwiseAdd(evaluationParams, t1, t2)
| #Multiply => pointwiseCombine(\"*.", evaluationParams, t1, t2)
| #Exponentiate => pointwiseCombine(\"**", evaluationParams, t1, t2)
}
}
module Truncate = {
type simplificationResult = [
| #Solution(ASTTypes.node)
| #Error(string)
| #NoSolution
]
let trySimplification = (leftCutoff, rightCutoff, t): simplificationResult =>
switch (leftCutoff, rightCutoff, t) {
| (None, None, t) => #Solution(t)
| (Some(lc), Some(rc), _) if lc > rc =>
#Error("Left truncation bound must be smaller than right truncation bound.")
| (lc, rc, #SymbolicDist(#Uniform(u))) =>
#Solution(#SymbolicDist(#Uniform(SymbolicDist.Uniform.truncate(lc, rc, u))))
| _ => #NoSolution
}
let truncateAsShape = (evaluationParams: evaluationParams, leftCutoff, rightCutoff, t) =>
switch // TODO: use named args for xMin/xMax in renderToShape; if we're lucky we can at least get the tail
// of a distribution we otherwise wouldn't get at all
Node.ensureIsRendered(evaluationParams, t) {
| Ok(#RenderedDist(rs)) =>
Ok(#RenderedDist(PointSetDist.T.truncate(leftCutoff, rightCutoff, rs)))
| Error(e) => Error(e)
| _ => Error("Could not truncate distribution.")
}
let operationToLeaf = (
evaluationParams,
leftCutoff: option<float>,
rightCutoff: option<float>,
t: node,
): result<node, string> =>
t
|> trySimplification(leftCutoff, rightCutoff)
|> (
x =>
switch x {
| #Solution(t) => Ok(t)
| #Error(e) => Error(e)
| #NoSolution => truncateAsShape(evaluationParams, leftCutoff, rightCutoff, t)
}
)
}
module Normalize = {
let rec operationToLeaf = (evaluationParams, t: node): result<node, string> =>
switch t {
| #RenderedDist(s) => Ok(#RenderedDist(PointSetDist.T.normalize(s)))
| #SymbolicDist(_) => Ok(t)
| _ => ASTTypes.Node.evaluateAndRetry(evaluationParams, operationToLeaf, t)
}
}
module FunctionCall = {
let _runHardcodedFunction = (name, evaluationParams, args) =>
TypeSystem.Function.Ts.findByNameAndRun(HardcodedFunctions.all, name, evaluationParams, args)
let _runLocalFunction = (name, evaluationParams: evaluationParams, args) =>
Environment.getFunction(evaluationParams.environment, name) |> E.R.bind(_, ((argNames, fn)) =>
ASTTypes.Function.run(evaluationParams, args, (argNames, fn))
)
let _runWithEvaluatedInputs = (
evaluationParams: ASTTypes.evaluationParams,
name,
args: array<ASTTypes.node>,
) =>
_runHardcodedFunction(name, evaluationParams, args) |> E.O.default(
_runLocalFunction(name, evaluationParams, args),
)
// TODO: This forces things to be floats
let run = (evaluationParams, name, args) =>
args
|> E.A.fmap(a => evaluationParams.evaluateNode(evaluationParams, a))
|> E.A.R.firstErrorOrOpen
|> E.R.bind(_, _runWithEvaluatedInputs(evaluationParams, name))
}
module Render = {
let rec operationToLeaf = (evaluationParams: evaluationParams, t: node): result<node, string> =>
switch t {
| #Function(_) => Error("Cannot render a function")
| #SymbolicDist(d) =>
Ok(
#RenderedDist(
SymbolicDist.T.toPointSetDist(evaluationParams.samplingInputs.pointSetDistLength, d),
),
)
| #RenderedDist(_) as t => Ok(t) // already a rendered pointSetDist, we're done here
| _ => ASTTypes.Node.evaluateAndRetry(evaluationParams, operationToLeaf, t)
}
}
/* This function recursively goes through the nodes of the parse tree,
replacing each Operation node and its subtree with a Data node.
Whenever possible, the replacement produces a new Symbolic Data node,
but most often it will produce a RenderedDist.
This function is used mainly to turn a parse tree into a single RenderedDist
that can then be displayed to the user. */
let rec toLeaf = (evaluationParams: ASTTypes.evaluationParams, node: node): result<node, string> =>
switch node {
// Leaf nodes just stay leaf nodes
| #SymbolicDist(_)
| #Function(_)
| #RenderedDist(_) =>
Ok(node)
| #Array(args) =>
args |> E.A.fmap(toLeaf(evaluationParams)) |> E.A.R.firstErrorOrOpen |> E.R.fmap(r => #Array(r))
// Operations nevaluationParamsd to be turned into leaves
| #AlgebraicCombination(algebraicOp, t1, t2) =>
AlgebraicCombination.operationToLeaf(evaluationParams, algebraicOp, t1, t2)
| #PointwiseCombination(pointwiseOp, t1, t2) =>
PointwiseCombination.operationToLeaf(evaluationParams, pointwiseOp, t1, t2)
| #Truncate(leftCutoff, rightCutoff, t) =>
Truncate.operationToLeaf(evaluationParams, leftCutoff, rightCutoff, t)
| #Normalize(t) => Normalize.operationToLeaf(evaluationParams, t)
| #Render(t) => Render.operationToLeaf(evaluationParams, t)
| #Hash(t) =>
t
|> E.A.fmap(((name: string, node: node)) =>
toLeaf(evaluationParams, node) |> E.R.fmap(r => (name, r))
)
|> E.A.R.firstErrorOrOpen
|> E.R.fmap(r => #Hash(r))
| #Symbol(r) =>
ASTTypes.Environment.get(evaluationParams.environment, r)
|> E.O.toResult("Undeclared variable " ++ r)
|> E.R.bind(_, toLeaf(evaluationParams))
| #FunctionCall(name, args) =>
FunctionCall.run(evaluationParams, name, args) |> E.R.bind(_, toLeaf(evaluationParams))
}

View File

@ -0,0 +1,232 @@
type rec hash = array<(string, node)>
and node = [
| #SymbolicDist(SymbolicDistTypes.symbolicDist)
| #RenderedDist(PointSetTypes.pointSetDist)
| #Symbol(string)
| #Hash(hash)
| #Array(array<node>)
| #Function(array<string>, node)
| #AlgebraicCombination(Operation.algebraicOperation, node, node)
| #PointwiseCombination(Operation.pointwiseOperation, node, node)
| #Normalize(node)
| #Render(node)
| #Truncate(option<float>, option<float>, node)
| #FunctionCall(string, array<node>)
]
type statement = [
| #Assignment(string, node)
| #Expression(node)
]
type program = array<statement>
type environment = Belt.Map.String.t<node>
type rec evaluationParams = {
samplingInputs: SamplingInputs.samplingInputs,
environment: environment,
evaluateNode: (evaluationParams, node) => Belt.Result.t<node, string>,
}
module Environment = {
type t = environment
module MS = Belt.Map.String
let fromArray = MS.fromArray
let empty: t = []->fromArray
let mergeKeepSecond = (a: t, b: t) =>
MS.merge(a, b, (_, a, b) =>
switch (a, b) {
| (_, Some(b)) => Some(b)
| (Some(a), _) => Some(a)
| _ => None
}
)
let update = (t, str, fn) => MS.update(t, str, fn)
let get = (t: t, str) => MS.get(t, str)
let getFunction = (t: t, str) =>
switch get(t, str) {
| Some(#Function(argNames, fn)) => Ok((argNames, fn))
| _ => Error("Function " ++ (str ++ " not found"))
}
}
module Node = {
let getFloat = (node: node) =>
node |> (
x =>
switch x {
| #RenderedDist(Discrete({xyShape: {xs: [x], ys: [1.0]}})) => Some(x)
| #SymbolicDist(#Float(x)) => Some(x)
| _ => None
}
)
let evaluate = (evaluationParams: evaluationParams) =>
evaluationParams.evaluateNode(evaluationParams)
let evaluateAndRetry = (evaluationParams, fn, node) =>
node |> evaluationParams.evaluateNode(evaluationParams) |> E.R.bind(_, fn(evaluationParams))
let rec toString: node => string = x =>
switch x {
| #SymbolicDist(d) => SymbolicDist.T.toString(d)
| #RenderedDist(_) => "[renderedShape]"
| #AlgebraicCombination(op, t1, t2) =>
Operation.Algebraic.format(op, toString(t1), toString(t2))
| #PointwiseCombination(op, t1, t2) =>
Operation.Pointwise.format(op, toString(t1), toString(t2))
| #Normalize(t) => "normalize(k" ++ (toString(t) ++ ")")
| #Truncate(lc, rc, t) => Operation.Truncate.toString(lc, rc, toString(t))
| #Render(t) => toString(t)
| #Symbol(t) => "Symbol: " ++ t
| #FunctionCall(name, args) =>
"[Function call: (" ++
(name ++
((args |> E.A.fmap(toString) |> Js.String.concatMany(_, ",")) ++ ")]"))
| #Function(args, internal) =>
"[Function: (" ++ ((args |> Js.String.concatMany(_, ",")) ++ (toString(internal) ++ ")]"))
| #Array(a) => "[" ++ ((a |> E.A.fmap(toString) |> Js.String.concatMany(_, ",")) ++ "]")
| #Hash(h) =>
"{" ++
((h
|> E.A.fmap(((name, value)) => name ++ (":" ++ toString(value)))
|> Js.String.concatMany(_, ",")) ++
"}")
}
let render = (evaluationParams: evaluationParams, r) => #Render(r) |> evaluate(evaluationParams)
let ensureIsRendered = (params, t) =>
switch t {
| #RenderedDist(_) => Ok(t)
| _ =>
switch render(params, t) {
| Ok(#RenderedDist(r)) => Ok(#RenderedDist(r))
| Ok(_) => Error("Did not render as requested")
| Error(e) => Error(e)
}
}
let ensureIsRenderedAndGetShape = (params, t) =>
switch ensureIsRendered(params, t) {
| Ok(#RenderedDist(r)) => Ok(r)
| Ok(_) => Error("Did not render as requested")
| Error(e) => Error(e)
}
let toPointSetDist = (item: node) =>
switch item {
| #RenderedDist(r) => Some(r)
| _ => None
}
let _toFloat = (t: PointSetTypes.pointSetDist) =>
switch t {
| Discrete({xyShape: {xs: [x], ys: [1.0]}}) => Some(#SymbolicDist(#Float(x)))
| _ => None
}
let toFloat = (item: node): result<node, string> =>
item |> toPointSetDist |> E.O.bind(_, _toFloat) |> E.O.toResult("Not valid shape")
}
module Function = {
type t = (array<string>, node)
let fromNode: node => option<t> = node =>
switch node {
| #Function(r) => Some(r)
| _ => None
}
let argumentNames = ((a, _): t) => a
let internals = ((_, b): t) => b
let run = (evaluationParams: evaluationParams, args: array<node>, t: t) =>
if E.A.length(args) == E.A.length(argumentNames(t)) {
let newEnvironment = Belt.Array.zip(argumentNames(t), args) |> Environment.fromArray
let newEvaluationParams: evaluationParams = {
samplingInputs: evaluationParams.samplingInputs,
environment: Environment.mergeKeepSecond(evaluationParams.environment, newEnvironment),
evaluateNode: evaluationParams.evaluateNode,
}
evaluationParams.evaluateNode(newEvaluationParams, internals(t))
} else {
Error("Wrong number of variables")
}
}
module SamplingDistribution = {
type t = [
| #SymbolicDist(SymbolicDistTypes.symbolicDist)
| #RenderedDist(PointSetTypes.pointSetDist)
]
let isSamplingDistribution: node => bool = x =>
switch x {
| #SymbolicDist(_) => true
| #RenderedDist(_) => true
| _ => false
}
let fromNode: node => result<t, string> = x =>
switch x {
| #SymbolicDist(n) => Ok(#SymbolicDist(n))
| #RenderedDist(n) => Ok(#RenderedDist(n))
| _ => Error("Not valid type")
}
let renderIfIsNotSamplingDistribution = (params, t): result<node, string> =>
!isSamplingDistribution(t)
? switch Node.render(params, t) {
| Ok(r) => Ok(r)
| Error(e) => Error(e)
}
: Ok(t)
let map = (~renderedDistFn, ~symbolicDistFn, node: node) =>
node |> (
x =>
switch x {
| #RenderedDist(r) => Some(renderedDistFn(r))
| #SymbolicDist(s) => Some(symbolicDistFn(s))
| _ => None
}
)
let sampleN = n =>
map(~renderedDistFn=PointSetDist.sampleNRendered(n), ~symbolicDistFn=SymbolicDist.T.sampleN(n))
let getCombinationSamples = (n, algebraicOp, t1: node, t2: node) =>
switch (sampleN(n, t1), sampleN(n, t2)) {
| (Some(a), Some(b)) =>
Some(
Belt.Array.zip(a, b) |> E.A.fmap(((a, b)) => Operation.Algebraic.toFn(algebraicOp, a, b)),
)
| _ => None
}
let combineShapesUsingSampling = (
evaluationParams: evaluationParams,
algebraicOp,
t1: node,
t2: node,
) => {
let i1 = renderIfIsNotSamplingDistribution(evaluationParams, t1)
let i2 = renderIfIsNotSamplingDistribution(evaluationParams, t2)
E.R.merge(i1, i2) |> E.R.bind(_, ((a, b)) => {
let samples = getCombinationSamples(
evaluationParams.samplingInputs.sampleCount,
algebraicOp,
a,
b,
)
let pointSetDist =
samples
|> E.O.fmap(r =>
SampleSet.toPointSetDist(~samplingInputs=evaluationParams.samplingInputs, ~samples=r, ())
)
|> E.O.bind(_, r => r.pointSetDist)
|> E.O.toResult("No response")
pointSetDist |> E.R.fmap(r => #Normalize(#RenderedDist(r)))
})
}
}

View File

@ -0,0 +1,232 @@
open TypeSystem
let wrongInputsError = (r: array<typedValue>) => {
let inputs = r |> E.A.fmap(TypedValue.toString) |> Js.String.concatMany(_, ",")
Js.log3("Inputs were", inputs, r)
Error("Wrong inputs. The inputs were:" ++ inputs)
}
let to_: (float, float) => result<node, string> = (low, high) =>
switch (low, high) {
| (low, high) if low <= 0.0 && low < high =>
Ok(#SymbolicDist(SymbolicDist.Normal.from90PercentCI(low, high)))
| (low, high) if low < high =>
Ok(#SymbolicDist(SymbolicDist.Lognormal.from90PercentCI(low, high)))
| (_, _) => Error("Low value must be less than high value.")
}
let makeSymbolicFromTwoFloats = (name, fn) =>
Function.T.make(
~name,
~outputType=#SamplingDistribution,
~inputTypes=[#Float, #Float],
~run=x =>
switch x {
| [#Float(a), #Float(b)] => Ok(#SymbolicDist(fn(a, b)))
| e => wrongInputsError(e)
},
(),
)
let makeSymbolicFromOneFloat = (name, fn) =>
Function.T.make(
~name,
~outputType=#SamplingDistribution,
~inputTypes=[#Float],
~run=x =>
switch x {
| [#Float(a)] => Ok(#SymbolicDist(fn(a)))
| e => wrongInputsError(e)
},
(),
)
let makeDistFloat = (name, fn) =>
Function.T.make(
~name,
~outputType=#SamplingDistribution,
~inputTypes=[#SamplingDistribution, #Float],
~run=x =>
switch x {
| [#SamplingDist(a), #Float(b)] => fn(a, b)
| [#RenderedDist(a), #Float(b)] => fn(#RenderedDist(a), b)
| e => wrongInputsError(e)
},
(),
)
let makeRenderedDistFloat = (name, fn) =>
Function.T.make(
~name,
~outputType=#RenderedDistribution,
~inputTypes=[#RenderedDistribution, #Float],
~shouldCoerceTypes=true,
~run=x =>
switch x {
| [#RenderedDist(a), #Float(b)] => fn(a, b)
| e => wrongInputsError(e)
},
(),
)
let makeDist = (name, fn) =>
Function.T.make(
~name,
~outputType=#SamplingDistribution,
~inputTypes=[#SamplingDistribution],
~run=x =>
switch x {
| [#SamplingDist(a)] => fn(a)
| [#RenderedDist(a)] => fn(#RenderedDist(a))
| e => wrongInputsError(e)
},
(),
)
let floatFromDist = (
distToFloatOp: Operation.distToFloatOperation,
t: TypeSystem.samplingDist,
): result<node, string> =>
switch t {
| #SymbolicDist(s) =>
SymbolicDist.T.operate(distToFloatOp, s) |> E.R.bind(_, v => Ok(#SymbolicDist(#Float(v))))
| #RenderedDist(rs) => PointSetDist.operate(distToFloatOp, rs) |> (v => Ok(#SymbolicDist(#Float(v))))
}
let verticalScaling = (scaleOp, rs, scaleBy) => {
// scaleBy has to be a single float, otherwise we'll return an error.
let fn = (secondary, main) => Operation.Scale.toFn(scaleOp, main, secondary)
let integralSumCacheFn = Operation.Scale.toIntegralSumCacheFn(scaleOp)
let integralCacheFn = Operation.Scale.toIntegralCacheFn(scaleOp)
Ok(
#RenderedDist(
PointSetDist.T.mapY(
~integralSumCacheFn=integralSumCacheFn(scaleBy),
~integralCacheFn=integralCacheFn(scaleBy),
~fn=fn(scaleBy),
rs,
),
),
)
}
module Multimodal = {
let getByNameResult = Hash.getByNameResult
let _paramsToDistsAndWeights = (r: array<typedValue>) =>
switch r {
| [#Hash(r)] =>
let dists =
getByNameResult(r, "dists")
->E.R.bind(TypeSystem.TypedValue.toArray)
->E.R.bind(r => r |> E.A.fmap(TypeSystem.TypedValue.toDist) |> E.A.R.firstErrorOrOpen)
let weights =
getByNameResult(r, "weights")
->E.R.bind(TypeSystem.TypedValue.toArray)
->E.R.bind(r => r |> E.A.fmap(TypeSystem.TypedValue.toFloat) |> E.A.R.firstErrorOrOpen)
E.R.merge(dists, weights) |> E.R.fmap(((a, b)) =>
E.A.zipMaxLength(a, b) |> E.A.fmap(((a, b)) => (a |> E.O.toExn(""), b |> E.O.default(1.0)))
)
| _ => Error("Needs items")
}
let _runner: array<typedValue> => result<node, string> = r => {
let paramsToDistsAndWeights =
_paramsToDistsAndWeights(r) |> E.R.fmap(
E.A.fmap(((dist, weight)) =>
#FunctionCall("scaleMultiply", [dist, #SymbolicDist(#Float(weight))])
),
)
let pointwiseSum: result<node, string> =
paramsToDistsAndWeights->E.R.bind(E.R.errorIfCondition(E.A.isEmpty, "Needs one input"))
|> E.R.fmap(r =>
r
|> Js.Array.sliceFrom(1)
|> E.A.fold_left((acc, x) => #PointwiseCombination(#Add, acc, x), E.A.unsafe_get(r, 0))
)
pointwiseSum
}
let _function = Function.T.make(
~name="multimodal",
~outputType=#SamplingDistribution,
~inputTypes=[#Hash([("dists", #Array(#SamplingDistribution)), ("weights", #Array(#Float))])],
~run=_runner,
(),
)
}
let all = [
makeSymbolicFromTwoFloats("normal", SymbolicDist.Normal.make),
makeSymbolicFromTwoFloats("uniform", SymbolicDist.Uniform.make),
makeSymbolicFromTwoFloats("beta", SymbolicDist.Beta.make),
makeSymbolicFromTwoFloats("lognormal", SymbolicDist.Lognormal.make),
makeSymbolicFromTwoFloats("lognormalFromMeanAndStdDev", SymbolicDist.Lognormal.fromMeanAndStdev),
makeSymbolicFromOneFloat("exponential", SymbolicDist.Exponential.make),
Function.T.make(
~name="to",
~outputType=#SamplingDistribution,
~inputTypes=[#Float, #Float],
~run=x =>
switch x {
| [#Float(a), #Float(b)] => to_(a, b)
| e => wrongInputsError(e)
},
(),
),
Function.T.make(
~name="triangular",
~outputType=#SamplingDistribution,
~inputTypes=[#Float, #Float, #Float],
~run=x =>
switch x {
| [#Float(a), #Float(b), #Float(c)] =>
SymbolicDist.Triangular.make(a, b, c) |> E.R.fmap(r => #SymbolicDist(r))
| e => wrongInputsError(e)
},
(),
),
Function.T.make(
~name="log",
~outputType=#Float,
~inputTypes=[#Float],
~run=x =>
switch x {
| [#Float(a)] => Ok(#SymbolicDist(#Float(Js.Math.log(a))))
| e => wrongInputsError(e)
},
(),
),
makeDistFloat("pdf", (dist, float) => floatFromDist(#Pdf(float), dist)),
makeDistFloat("inv", (dist, float) => floatFromDist(#Inv(float), dist)),
makeDistFloat("cdf", (dist, float) => floatFromDist(#Cdf(float), dist)),
makeDist("mean", dist => floatFromDist(#Mean, dist)),
makeDist("sample", dist => floatFromDist(#Sample, dist)),
Function.T.make(
~name="render",
~outputType=#RenderedDistribution,
~inputTypes=[#RenderedDistribution],
~run=x =>
switch x {
| [#RenderedDist(c)] => Ok(#RenderedDist(c))
| e => wrongInputsError(e)
},
(),
),
Function.T.make(
~name="normalize",
~outputType=#SamplingDistribution,
~inputTypes=[#SamplingDistribution],
~run=x =>
switch x {
| [#SamplingDist(#SymbolicDist(c))] => Ok(#SymbolicDist(c))
| [#SamplingDist(#RenderedDist(c))] => Ok(#RenderedDist(PointSetDist.T.normalize(c)))
| e => wrongInputsError(e)
},
(),
),
makeRenderedDistFloat("scaleExp", (dist, float) => verticalScaling(#Exponentiate, dist, float)),
makeRenderedDistFloat("scaleMultiply", (dist, float) => verticalScaling(#Multiply, dist, float)),
makeRenderedDistFloat("scaleLog", (dist, float) => verticalScaling(#Log, dist, float)),
Multimodal._function,
]

View File

@ -0,0 +1,204 @@
type node = ASTTypes.node
let getFloat = ASTTypes.Node.getFloat
type samplingDist = [
| #SymbolicDist(SymbolicDistTypes.symbolicDist)
| #RenderedDist(PointSetTypes.pointSetDist)
]
type rec hashType = array<(string, _type)>
and _type = [
| #Float
| #SamplingDistribution
| #RenderedDistribution
| #Array(_type)
| #Hash(hashType)
]
type rec hashTypedValue = array<(string, typedValue)>
and typedValue = [
| #Float(float)
| #RenderedDist(PointSetTypes.pointSetDist)
| #SamplingDist(samplingDist)
| #Array(array<typedValue>)
| #Hash(hashTypedValue)
]
type _function = {
name: string,
inputTypes: array<_type>,
outputType: _type,
run: array<typedValue> => result<node, string>,
shouldCoerceTypes: bool,
}
type functions = array<_function>
type inputNodes = array<node>
module TypedValue = {
let rec toString: typedValue => string = x =>
switch x {
| #SamplingDist(_) => "[sampling dist]"
| #RenderedDist(_) => "[rendered PointSetDist]"
| #Float(f) => "Float: " ++ Js.Float.toString(f)
| #Array(a) => "[" ++ ((a |> E.A.fmap(toString) |> Js.String.concatMany(_, ",")) ++ "]")
| #Hash(v) =>
"{" ++
((v
|> E.A.fmap(((name, value)) => name ++ (":" ++ toString(value)))
|> Js.String.concatMany(_, ",")) ++
"}")
}
let rec fromNode = (node: node): result<typedValue, string> =>
switch node {
| #SymbolicDist(#Float(r)) => Ok(#Float(r))
| #SymbolicDist(s) => Ok(#SamplingDist(#SymbolicDist(s)))
| #RenderedDist(s) => Ok(#RenderedDist(s))
| #Array(r) => r |> E.A.fmap(fromNode) |> E.A.R.firstErrorOrOpen |> E.R.fmap(r => #Array(r))
| #Hash(hash) =>
hash
|> E.A.fmap(((name, t)) => fromNode(t) |> E.R.fmap(r => (name, r)))
|> E.A.R.firstErrorOrOpen
|> E.R.fmap(r => #Hash(r))
| e => Error("Wrong type: " ++ ASTTypes.Node.toString(e))
}
// todo: Arrays and hashes
let rec fromNodeWithTypeCoercion = (evaluationParams, _type: _type, node) =>
switch (_type, node) {
| (#Float, _) =>
switch getFloat(node) {
| Some(a) => Ok(#Float(a))
| _ => Error("Type Error: Expected float.")
}
| (#SamplingDistribution, _) =>
ASTTypes.SamplingDistribution.renderIfIsNotSamplingDistribution(
evaluationParams,
node,
) |> E.R.bind(_, fromNode)
| (#RenderedDistribution, _) =>
ASTTypes.Node.render(evaluationParams, node) |> E.R.bind(_, fromNode)
| (#Array(_type), #Array(b)) =>
b
|> E.A.fmap(fromNodeWithTypeCoercion(evaluationParams, _type))
|> E.A.R.firstErrorOrOpen
|> E.R.fmap(r => #Array(r))
| (#Hash(named), #Hash(r)) =>
let keyValues =
named |> E.A.fmap(((name, intendedType)) => (
name,
intendedType,
Hash.getByName(r, name),
))
let typedHash =
keyValues
|> E.A.fmap(((name, intendedType, optionNode)) =>
switch optionNode {
| Some(node) =>
fromNodeWithTypeCoercion(evaluationParams, intendedType, node) |> E.R.fmap(node => (
name,
node,
))
| None => Error("Hash parameter not present in hash.")
}
)
|> E.A.R.firstErrorOrOpen
|> E.R.fmap(r => #Hash(r))
typedHash
| _ => Error("fromNodeWithTypeCoercion error, sorry.")
}
let toFloat: typedValue => result<float, string> = x =>
switch x {
| #Float(x) => Ok(x)
| _ => Error("Not a float")
}
let toArray: typedValue => result<array<'a>, string> = x =>
switch x {
| #Array(x) => Ok(x)
| _ => Error("Not an array")
}
let toNamed: typedValue => result<hashTypedValue, string> = x =>
switch x {
| #Hash(x) => Ok(x)
| _ => Error("Not a named item")
}
let toDist: typedValue => result<node, string> = x =>
switch x {
| #SamplingDist(#SymbolicDist(c)) => Ok(#SymbolicDist(c))
| #SamplingDist(#RenderedDist(c)) => Ok(#RenderedDist(c))
| #RenderedDist(c) => Ok(#RenderedDist(c))
| #Float(x) => Ok(#SymbolicDist(#Float(x)))
| x => Error("Cannot be converted into a distribution: " ++ toString(x))
}
}
module Function = {
type t = _function
type ts = functions
module T = {
let make = (~name, ~inputTypes, ~outputType, ~run, ~shouldCoerceTypes=true, _): t => {
name: name,
inputTypes: inputTypes,
outputType: outputType,
run: run,
shouldCoerceTypes: shouldCoerceTypes,
}
let _inputLengthCheck = (inputNodes: inputNodes, t: t) => {
let expectedLength = E.A.length(t.inputTypes)
let actualLength = E.A.length(inputNodes)
expectedLength == actualLength
? Ok(inputNodes)
: Error(
"Wrong number of inputs. Expected" ++
((expectedLength |> E.I.toString) ++
(". Got:" ++ (actualLength |> E.I.toString))),
)
}
let _coerceInputNodes = (evaluationParams, inputTypes, shouldCoerce, inputNodes) =>
Belt.Array.zip(inputTypes, inputNodes)
|> E.A.fmap(((def, input)) =>
shouldCoerce
? TypedValue.fromNodeWithTypeCoercion(evaluationParams, def, input)
: TypedValue.fromNode(input)
)
|> E.A.R.firstErrorOrOpen
let inputsToTypedValues = (
evaluationParams: ASTTypes.evaluationParams,
inputNodes: inputNodes,
t: t,
) =>
_inputLengthCheck(inputNodes, t)->E.R.bind(
_coerceInputNodes(evaluationParams, t.inputTypes, t.shouldCoerceTypes),
)
let run = (
evaluationParams: ASTTypes.evaluationParams,
inputNodes: inputNodes,
t: t,
) =>
inputsToTypedValues(evaluationParams, inputNodes, t)->E.R.bind(t.run)
|> (
x =>
switch x {
| Ok(i) => Ok(i)
| Error(r) => Error("Function " ++ (t.name ++ (" error: " ++ r)))
}
)
}
module Ts = {
let findByName = (ts: ts, n: string) => ts |> Belt.Array.getBy(_, ({name}) => name == n)
let findByNameAndRun = (ts: ts, n: string, evaluationParams, inputTypes) =>
findByName(ts, n) |> E.O.fmap(T.run(evaluationParams, inputTypes))
}
}

View File

@ -0,0 +1,304 @@
module MathJsonToMathJsAdt = {
type rec arg =
| Symbol(string)
| Value(float)
| Fn(fn)
| Array(array<arg>)
| Blocks(array<arg>)
| Object(Js.Dict.t<arg>)
| Assignment(arg, arg)
| FunctionAssignment(fnAssignment)
and fn = {
name: string,
args: array<arg>,
}
and fnAssignment = {
name: string,
args: array<string>,
expression: arg,
}
let rec run = (j: Js.Json.t) => {
open Json.Decode
switch field("mathjs", string, j) {
| "FunctionNode" =>
let args = j |> field("args", array(run))
let name = j |> optional(field("fn", field("name", string)))
name |> E.O.fmap(name => Fn({name: name, args: args |> E.A.O.concatSomes}))
| "OperatorNode" =>
let args = j |> field("args", array(run))
Some(
Fn({
name: j |> field("fn", string),
args: args |> E.A.O.concatSomes,
}),
)
| "ConstantNode" => optional(field("value", Json.Decode.float), j) |> E.O.fmap(r => Value(r))
| "ParenthesisNode" => j |> field("content", run)
| "ObjectNode" =>
let properties = j |> field("properties", dict(run))
Js.Dict.entries(properties)
|> E.A.fmap(((key, value)) => value |> E.O.fmap(v => (key, v)))
|> E.A.O.concatSomes
|> Js.Dict.fromArray
|> (r => Some(Object(r)))
| "ArrayNode" =>
let items = field("items", array(run), j)
Some(Array(items |> E.A.O.concatSomes))
| "SymbolNode" => Some(Symbol(field("name", string, j)))
| "AssignmentNode" =>
let object_ = j |> field("object", run)
let value_ = j |> field("value", run)
switch (object_, value_) {
| (Some(o), Some(v)) => Some(Assignment(o, v))
| _ => None
}
| "BlockNode" =>
let block = r => r |> field("node", run)
let args = j |> field("blocks", array(block)) |> E.A.O.concatSomes
Some(Blocks(args))
| "FunctionAssignmentNode" =>
let name = j |> field("name", string)
let args = j |> field("params", array(field("name", string)))
let expression = j |> field("expr", run)
expression |> E.O.fmap(expression => FunctionAssignment({
name: name,
args: args,
expression: expression,
}))
| n =>
Js.log3("Couldn't parse mathjs node", j, n)
None
}
}
}
module MathAdtToDistDst = {
open MathJsonToMathJsAdt
let handleSymbol = sym => Ok(#Symbol(sym))
// TODO: This only works on the top level, which needs to be refactored. Also, I think functions don't need to be done like this anymore.
module MathAdtCleaner = {
let transformWithSymbol = (f: float, s: string) =>
switch s {
| "K" => Some(f *. 1000.)
| "M" => Some(f *. 1000000.)
| "B" => Some(f *. 1000000000.)
| "T" => Some(f *. 1000000000000.)
| _ => None
}
let rec run = x =>
switch x {
| Fn({name: "multiply", args: [Value(f), Symbol(s)]}) as doNothing =>
transformWithSymbol(f, s) |> E.O.fmap(r => Value(r)) |> E.O.default(doNothing)
| Fn({name: "unaryMinus", args: [Value(f)]}) => Value(-1.0 *. f)
| Fn({name, args}) => Fn({name: name, args: args |> E.A.fmap(run)})
| Array(args) => Array(args |> E.A.fmap(run))
| Symbol(s) => Symbol(s)
| Value(v) => Value(v)
| Blocks(args) => Blocks(args |> E.A.fmap(run))
| Assignment(a, b) => Assignment(a, run(b))
| FunctionAssignment(a) => FunctionAssignment(a)
| Object(v) =>
Object(
v
|> Js.Dict.entries
|> E.A.fmap(((key, value)) => (key, run(value)))
|> Js.Dict.fromArray,
)
}
}
let lognormal = (args, parseArgs, nodeParser) =>
switch args {
| [Object(o)] =>
let g = s =>
Js.Dict.get(o, s) |> E.O.toResult("Variable was empty") |> E.R.bind(_, nodeParser)
switch (g("mean"), g("stdev"), g("mu"), g("sigma")) {
| (Ok(mean), Ok(stdev), _, _) =>
Ok(#FunctionCall("lognormalFromMeanAndStdDev", [mean, stdev]))
| (_, _, Ok(mu), Ok(sigma)) => Ok(#FunctionCall("lognormal", [mu, sigma]))
| _ => Error("Lognormal distribution needs either mean and stdev or mu and sigma")
}
| _ =>
parseArgs() |> E.R.fmap((args: array<ASTTypes.node>) =>
#FunctionCall("lognormal", args)
)
}
// Error("Dotwise exponentiation needs two operands")
let operationParser = (
name: string,
args: result<array<ASTTypes.node>, string>,
): result<ASTTypes.node, string> => {
let toOkAlgebraic = r => Ok(#AlgebraicCombination(r))
let toOkPointwise = r => Ok(#PointwiseCombination(r))
let toOkTruncate = r => Ok(#Truncate(r))
args |> E.R.bind(_, args =>
switch (name, args) {
| ("add", [l, r]) => toOkAlgebraic((#Add, l, r))
| ("add", _) => Error("Addition needs two operands")
| ("unaryMinus", [l]) => toOkAlgebraic((#Multiply, #SymbolicDist(#Float(-1.0)), l))
| ("subtract", [l, r]) => toOkAlgebraic((#Subtract, l, r))
| ("subtract", _) => Error("Subtraction needs two operands")
| ("multiply", [l, r]) => toOkAlgebraic((#Multiply, l, r))
| ("multiply", _) => Error("Multiplication needs two operands")
| ("pow", [l, r]) => toOkAlgebraic((#Exponentiate, l, r))
| ("pow", _) => Error("Exponentiation needs two operands")
| ("dotMultiply", [l, r]) => toOkPointwise((#Multiply, l, r))
| ("dotMultiply", _) => Error("Dotwise multiplication needs two operands")
| ("dotPow", [l, r]) => toOkPointwise((#Exponentiate, l, r))
| ("dotPow", _) => Error("Dotwise exponentiation needs two operands")
| ("rightLogShift", [l, r]) => toOkPointwise((#Add, l, r))
| ("rightLogShift", _) => Error("Dotwise addition needs two operands")
| ("divide", [l, r]) => toOkAlgebraic((#Divide, l, r))
| ("divide", _) => Error("Division needs two operands")
| ("leftTruncate", [d, #SymbolicDist(#Float(lc))]) => toOkTruncate((Some(lc), None, d))
| ("leftTruncate", _) =>
Error("leftTruncate needs two arguments: the expression and the cutoff")
| ("rightTruncate", [d, #SymbolicDist(#Float(rc))]) => toOkTruncate((None, Some(rc), d))
| ("rightTruncate", _) =>
Error("rightTruncate needs two arguments: the expression and the cutoff")
| ("truncate", [d, #SymbolicDist(#Float(lc)), #SymbolicDist(#Float(rc))]) =>
toOkTruncate((Some(lc), Some(rc), d))
| ("truncate", _) => Error("truncate needs three arguments: the expression and both cutoffs")
| _ => Error("This type not currently supported")
}
)
}
let functionParser = (
nodeParser: MathJsonToMathJsAdt.arg => Belt.Result.t<
ASTTypes.node,
string,
>,
name: string,
args: array<MathJsonToMathJsAdt.arg>,
): result<ASTTypes.node, string> => {
let parseArray = ags => ags |> E.A.fmap(nodeParser) |> E.A.R.firstErrorOrOpen
let parseArgs = () => parseArray(args)
switch name {
| "lognormal" => lognormal(args, parseArgs, nodeParser)
| "multimodal"
| "add"
| "subtract"
| "multiply"
| "unaryMinus"
| "dotMultiply"
| "dotPow"
| "rightLogShift"
| "divide"
| "pow"
| "leftTruncate"
| "rightTruncate"
| "truncate" =>
operationParser(name, parseArgs())
| "mm" =>
let weights =
args
|> E.A.last
|> E.O.bind(_, x =>
switch x {
| Array(values) => Some(parseArray(values))
| _ => None
}
)
let possibleDists = E.O.isSome(weights)
? Belt.Array.slice(args, ~offset=0, ~len=E.A.length(args) - 1)
: args
let dists = parseArray(possibleDists)
switch (weights, dists) {
| (Some(Error(r)), _) => Error(r)
| (_, Error(r)) => Error(r)
| (None, Ok(dists)) =>
let hash: ASTTypes.node = #FunctionCall(
"multimodal",
[#Hash([("dists", #Array(dists)), ("weights", #Array([]))])],
)
Ok(hash)
| (Some(Ok(weights)), Ok(dists)) =>
let hash: ASTTypes.node = #FunctionCall(
"multimodal",
[#Hash([("dists", #Array(dists)), ("weights", #Array(weights))])],
)
Ok(hash)
}
| name =>
parseArgs() |> E.R.fmap((args: array<ASTTypes.node>) =>
#FunctionCall(name, args)
)
}
}
let rec nodeParser: MathJsonToMathJsAdt.arg => result<
ASTTypes.node,
string,
> = x =>
switch x {
| Value(f) => Ok(#SymbolicDist(#Float(f)))
| Symbol(sym) => Ok(#Symbol(sym))
| Fn({name, args}) => functionParser(nodeParser, name, args)
| _ => Error("This type not currently supported")
}
// | FunctionAssignment({name, args, expression}) => {
// let evaluatedExpression = run(expression);
// `Function(_ => Ok(evaluatedExpression));
// }
let rec topLevel = (r): result<ASTTypes.program, string> =>
switch r {
| FunctionAssignment({name, args, expression}) =>
switch nodeParser(expression) {
| Ok(r) => Ok([#Assignment(name, #Function(args, r))])
| Error(r) => Error(r)
}
| Value(_) as r => nodeParser(r) |> E.R.fmap(r => [#Expression(r)])
| Fn(_) as r => nodeParser(r) |> E.R.fmap(r => [#Expression(r)])
| Array(_) => Error("Array not valid as top level")
| Symbol(s) => handleSymbol(s) |> E.R.fmap(r => [#Expression(r)])
| Object(_) => Error("Object not valid as top level")
| Assignment(name, value) =>
switch name {
| Symbol(symbol) => nodeParser(value) |> E.R.fmap(r => [#Assignment(symbol, r)])
| _ => Error("Symbol not a string")
}
| Blocks(blocks) =>
blocks |> E.A.fmap(b => topLevel(b)) |> E.A.R.firstErrorOrOpen |> E.R.fmap(E.A.concatMany)
}
let run = (r): result<ASTTypes.program, string> =>
r |> MathAdtCleaner.run |> topLevel
}
/* The MathJs parser doesn't support '.+' syntax, but we want it because it
would make sense with '.*'. Our workaround is to change this to >>>, which is
logShift in mathJS. We don't expect to use logShift anytime soon, so this tradeoff
seems fine.
*/
let pointwiseToRightLogShift = Js.String.replaceByRe(%re("/\.\+/g"), ">>>")
let fromString2 = str => {
/* We feed the user-typed string into Mathjs.parseMath,
which returns a JSON with (hopefully) a single-element array.
This array element is the top-level node of a nested-object tree
representing the functions/arguments/values/etc. in the string.
The function MathJsonToMathJsAdt then recursively unpacks this JSON into a typed data structure we can use.
Inside of this function, MathAdtToDistDst is called whenever a distribution function is encountered.
*/
let mathJsToJson = str |> pointwiseToRightLogShift |> Mathjs.parseMath
let mathJsParse = E.R.bind(mathJsToJson, r =>
switch MathJsonToMathJsAdt.run(r) {
| Some(r) => Ok(r)
| None => Error("MathJsParse Error")
}
)
let value = E.R.bind(mathJsParse, MathAdtToDistDst.run)
Js.log2(mathJsParse, value)
value
}
let fromString = str => fromString2(str)

View File

@ -0,0 +1,264 @@
type pointMassesWithMoments = {
n: int,
masses: array<float>,
means: array<float>,
variances: array<float>,
}
/* This function takes a continuous distribution and efficiently approximates it as
point masses that have variances associated with them.
We estimate the means and variances from overlapping triangular distributions which we imagine are making up the
XYShape.
We can then use the algebra of random variables to "convolve" the point masses and their variances,
and finally reconstruct a new distribution from them, e.g. using a Fast Gauss Transform or Raykar et al. (2007). */
let toDiscretePointMassesFromTriangulars = (
~inverse=false,
s: XYShape.T.t,
): pointMassesWithMoments => {
// TODO: what if there is only one point in the distribution?
let n = s |> XYShape.T.length
// first, double up the leftmost and rightmost points:
let {xs, ys}: XYShape.T.t = s
Js.Array.unshift(xs[0], xs) |> ignore
Js.Array.unshift(ys[0], ys) |> ignore
Js.Array.push(xs[n - 1], xs) |> ignore
Js.Array.push(ys[n - 1], ys) |> ignore
let n = E.A.length(xs)
// squares and neighbourly products of the xs
let xsSq: array<float> = Belt.Array.makeUninitializedUnsafe(n)
let xsProdN1: array<float> = Belt.Array.makeUninitializedUnsafe(n - 1)
let xsProdN2: array<float> = Belt.Array.makeUninitializedUnsafe(n - 2)
for i in 0 to n - 1 {
Belt.Array.set(xsSq, i, xs[i] *. xs[i]) |> ignore
()
}
for i in 0 to n - 2 {
Belt.Array.set(xsProdN1, i, xs[i] *. xs[i + 1]) |> ignore
()
}
for i in 0 to n - 3 {
Belt.Array.set(xsProdN2, i, xs[i] *. xs[i + 2]) |> ignore
()
}
// means and variances
let masses: array<float> = Belt.Array.makeUninitializedUnsafe(n - 2) // doesn't include the fake first and last points
let means: array<float> = Belt.Array.makeUninitializedUnsafe(n - 2)
let variances: array<float> = Belt.Array.makeUninitializedUnsafe(n - 2)
if inverse {
for i in 1 to n - 2 {
Belt.Array.set(masses, i - 1, (xs[i + 1] -. xs[i - 1]) *. ys[i] /. 2.) |> ignore
// this only works when the whole triange is either on the left or on the right of zero
let a = xs[i - 1]
let c = xs[i]
let b = xs[i + 1]
// These are the moments of the reciprocal of a triangular distribution, as symbolically integrated by Mathematica.
// They're probably pretty close to invMean ~ 1/mean = 3/(a+b+c) and invVar. But I haven't worked out
// the worst case error, so for now let's use these monster equations
let inverseMean =
2. *. (a *. log(a /. c) /. (a -. c) +. b *. log(c /. b) /. (b -. c)) /. (a -. b)
let inverseVar =
2. *. (log(c /. a) /. (a -. c) +. b *. log(b /. c) /. (b -. c)) /. (a -. b) -.
inverseMean ** 2.
Belt.Array.set(means, i - 1, inverseMean) |> ignore
Belt.Array.set(variances, i - 1, inverseVar) |> ignore
()
}
{n: n - 2, masses: masses, means: means, variances: variances}
} else {
for i in 1 to n - 2 {
// area of triangle = width * height / 2
Belt.Array.set(masses, i - 1, (xs[i + 1] -. xs[i - 1]) *. ys[i] /. 2.) |> ignore
// means of triangle = (a + b + c) / 3
Belt.Array.set(means, i - 1, (xs[i - 1] +. xs[i] +. xs[i + 1]) /. 3.) |> ignore
// variance of triangle = (a^2 + b^2 + c^2 - ab - ac - bc) / 18
Belt.Array.set(
variances,
i - 1,
(xsSq[i - 1] +.
xsSq[i] +.
xsSq[i + 1] -.
xsProdN1[i - 1] -.
xsProdN1[i] -.
xsProdN2[i - 1]) /. 18.,
) |> ignore
()
}
{n: n - 2, masses: masses, means: means, variances: variances}
}
}
let combineShapesContinuousContinuous = (
op: Operation.algebraicOperation,
s1: PointSetTypes.xyShape,
s2: PointSetTypes.xyShape,
): PointSetTypes.xyShape => {
// if we add the two distributions, we should probably use normal filters.
// if we multiply the two distributions, we should probably use lognormal filters.
let t1m = toDiscretePointMassesFromTriangulars(s1)
let t2m = switch op {
| #Divide => toDiscretePointMassesFromTriangulars(~inverse=true, s2)
| _ => toDiscretePointMassesFromTriangulars(~inverse=false, s2)
}
let combineMeansFn = switch op {
| #Add => (m1, m2) => m1 +. m2
| #Subtract => (m1, m2) => m1 -. m2
| #Multiply => (m1, m2) => m1 *. m2
| #Divide => (m1, mInv2) => m1 *. mInv2
| #Exponentiate => (m1, mInv2) => m1 ** mInv2
} // note: here, mInv2 = mean(1 / t2) ~= 1 / mean(t2)
// TODO: I don't know what the variances are for exponentatiation
// converts the variances and means of the two inputs into the variance of the output
let combineVariancesFn = switch op {
| #Add => (v1, v2, _, _) => v1 +. v2
| #Subtract => (v1, v2, _, _) => v1 +. v2
| #Multiply => (v1, v2, m1, m2) => v1 *. v2 +. v1 *. m2 ** 2. +. v2 *. m1 ** 2.
| #Exponentiate => (v1, v2, m1, m2) => v1 *. v2 +. v1 *. m2 ** 2. +. v2 *. m1 ** 2.
| #Divide => (v1, vInv2, m1, mInv2) => v1 *. vInv2 +. v1 *. mInv2 ** 2. +. vInv2 *. m1 ** 2.
}
// TODO: If operating on two positive-domain distributions, we should take that into account
let outputMinX: ref<float> = ref(infinity)
let outputMaxX: ref<float> = ref(neg_infinity)
let masses: array<float> = Belt.Array.makeUninitializedUnsafe(t1m.n * t2m.n)
let means: array<float> = Belt.Array.makeUninitializedUnsafe(t1m.n * t2m.n)
let variances: array<float> = Belt.Array.makeUninitializedUnsafe(t1m.n * t2m.n)
// then convolve the two sets of pointMassesWithMoments
for i in 0 to t1m.n - 1 {
for j in 0 to t2m.n - 1 {
let k = i * t2m.n + j
Belt.Array.set(masses, k, t1m.masses[i] *. t2m.masses[j]) |> ignore
let mean = combineMeansFn(t1m.means[i], t2m.means[j])
let variance = combineVariancesFn(
t1m.variances[i],
t2m.variances[j],
t1m.means[i],
t2m.means[j],
)
Belt.Array.set(means, k, mean) |> ignore
Belt.Array.set(variances, k, variance) |> ignore
// update bounds
let minX = mean -. 2. *. sqrt(variance) *. 1.644854
let maxX = mean +. 2. *. sqrt(variance) *. 1.644854
if minX < outputMinX.contents {
outputMinX := minX
}
if maxX > outputMaxX.contents {
outputMaxX := maxX
}
}
}
// we now want to create a set of target points. For now, let's just evenly distribute 200 points between
// between the outputMinX and outputMaxX
let nOut = 300
let outputXs: array<float> = E.A.Floats.range(outputMinX.contents, outputMaxX.contents, nOut)
let outputYs: array<float> = Belt.Array.make(nOut, 0.0)
// now, for each of the outputYs, accumulate from a Gaussian kernel over each input point.
for j in 0 to E.A.length(masses) - 1 {
if (
// go through all of the result points
variances[j] > 0. && masses[j] > 0.
) {
for i in 0 to E.A.length(outputXs) - 1 {
// go through all of the target points
let dx = outputXs[i] -. means[j]
let contribution =
masses[j] *.
exp(-.(dx ** 2.) /. (2. *. variances[j])) /.
sqrt(2. *. 3.14159276 *. variances[j])
Belt.Array.set(outputYs, i, outputYs[i] +. contribution) |> ignore
}
}
}
{xs: outputXs, ys: outputYs}
}
let toDiscretePointMassesFromDiscrete = (s: PointSetTypes.xyShape): pointMassesWithMoments => {
let {xs, ys}: XYShape.T.t = s
let n = E.A.length(xs)
let masses: array<float> = Belt.Array.makeBy(n, i => ys[i])
let means: array<float> = Belt.Array.makeBy(n, i => xs[i])
let variances: array<float> = Belt.Array.makeBy(n, _ => 0.0)
{n: n, masses: masses, means: means, variances: variances}
}
let combineShapesContinuousDiscrete = (
op: Operation.algebraicOperation,
continuousShape: PointSetTypes.xyShape,
discreteShape: PointSetTypes.xyShape,
): PointSetTypes.xyShape => {
let t1n = continuousShape |> XYShape.T.length
let t2n = discreteShape |> XYShape.T.length
// each x pair is added/subtracted
let fn = Operation.Algebraic.toFn(op)
let outXYShapes: array<array<(float, float)>> = Belt.Array.makeUninitializedUnsafe(t2n)
switch op {
| #Add
| #Subtract =>
for j in 0 to t2n - 1 {
// creates a new continuous shape for each one of the discrete points, and collects them in outXYShapes.
let dxyShape: array<(float, float)> = Belt.Array.makeUninitializedUnsafe(t1n)
for i in 0 to t1n - 1 {
Belt.Array.set(
dxyShape,
i,
(
fn(continuousShape.xs[i], discreteShape.xs[j]),
continuousShape.ys[i] *. discreteShape.ys[j],
),
) |> ignore
()
}
Belt.Array.set(outXYShapes, j, dxyShape) |> ignore
()
}
| #Multiply
| #Exponentiate
| #Divide =>
for j in 0 to t2n - 1 {
// creates a new continuous shape for each one of the discrete points, and collects them in outXYShapes.
let dxyShape: array<(float, float)> = Belt.Array.makeUninitializedUnsafe(t1n)
for i in 0 to t1n - 1 {
Belt.Array.set(
dxyShape,
i,
(
fn(continuousShape.xs[i], discreteShape.xs[j]),
continuousShape.ys[i] *. discreteShape.ys[j] /. discreteShape.xs[j],
),
) |> ignore
()
}
Belt.Array.set(outXYShapes, j, dxyShape) |> ignore
()
}
}
outXYShapes
|> E.A.fmap(XYShape.T.fromZippedArray)
|> E.A.fold_left(
XYShape.PointwiseCombination.combine(
\"+.",
XYShape.XtoY.continuousInterpolator(#Linear, #UseZero),
),
XYShape.T.empty,
)
}

View File

@ -0,0 +1,264 @@
open Distributions
type t = PointSetTypes.continuousShape
let getShape = (t: t) => t.xyShape
let interpolation = (t: t) => t.interpolation
let make = (~interpolation=#Linear, ~integralSumCache=None, ~integralCache=None, xyShape): t => {
xyShape: xyShape,
interpolation: interpolation,
integralSumCache: integralSumCache,
integralCache: integralCache,
}
let shapeMap = (fn, {xyShape, interpolation, integralSumCache, integralCache}: t): t => {
xyShape: fn(xyShape),
interpolation: interpolation,
integralSumCache: integralSumCache,
integralCache: integralCache,
}
let lastY = (t: t) => t |> getShape |> XYShape.T.lastY
let oShapeMap = (fn, {xyShape, interpolation, integralSumCache, integralCache}: t): option<
PointSetTypes.continuousShape,
> => fn(xyShape) |> E.O.fmap(make(~interpolation, ~integralSumCache, ~integralCache))
let emptyIntegral: PointSetTypes.continuousShape = {
xyShape: {
xs: [neg_infinity],
ys: [0.0],
},
interpolation: #Linear,
integralSumCache: Some(0.0),
integralCache: None,
}
let empty: PointSetTypes.continuousShape = {
xyShape: XYShape.T.empty,
interpolation: #Linear,
integralSumCache: Some(0.0),
integralCache: Some(emptyIntegral),
}
let stepwiseToLinear = (t: t): t =>
make(
~integralSumCache=t.integralSumCache,
~integralCache=t.integralCache,
XYShape.Range.stepwiseToLinear(t.xyShape),
)
// Note: This results in a distribution with as many points as the sum of those in t1 and t2.
let combinePointwise = (
~integralSumCachesFn=(_, _) => None,
~integralCachesFn: (t, t) => option<t>=(_, _) => None,
~distributionType: PointSetTypes.distributionType=#PDF,
fn: (float, float) => float,
t1: PointSetTypes.continuousShape,
t2: PointSetTypes.continuousShape,
): PointSetTypes.continuousShape => {
// If we're adding the distributions, and we know the total of each, then we
// can just sum them up. Otherwise, all bets are off.
let combinedIntegralSum = Common.combineIntegralSums(
integralSumCachesFn,
t1.integralSumCache,
t2.integralSumCache,
)
// TODO: does it ever make sense to pointwise combine the integrals here?
// It could be done for pointwise additions, but is that ever needed?
// If combining stepwise and linear, we must convert the stepwise to linear first,
// i.e. add a point at the bottom of each step
let (t1, t2) = switch (t1.interpolation, t2.interpolation) {
| (#Linear, #Linear) => (t1, t2)
| (#Stepwise, #Stepwise) => (t1, t2)
| (#Linear, #Stepwise) => (t1, stepwiseToLinear(t2))
| (#Stepwise, #Linear) => (stepwiseToLinear(t1), t2)
}
let extrapolation = switch distributionType {
| #PDF => #UseZero
| #CDF => #UseOutermostPoints
}
let interpolator = XYShape.XtoY.continuousInterpolator(t1.interpolation, extrapolation)
make(
~integralSumCache=combinedIntegralSum,
XYShape.PointwiseCombination.combine(fn, interpolator, t1.xyShape, t2.xyShape),
)
}
let toLinear = (t: t): option<t> =>
switch t {
| {interpolation: #Stepwise, xyShape, integralSumCache, integralCache} =>
xyShape |> XYShape.Range.stepsToContinuous |> E.O.fmap(make(~integralSumCache, ~integralCache))
| {interpolation: #Linear} => Some(t)
}
let shapeFn = (fn, t: t) => t |> getShape |> fn
let updateIntegralSumCache = (integralSumCache, t: t): t => {
...t,
integralSumCache: integralSumCache,
}
let updateIntegralCache = (integralCache, t: t): t => {...t, integralCache: integralCache}
let reduce = (
~integralSumCachesFn: (float, float) => option<float>=(_, _) => None,
~integralCachesFn: (t, t) => option<t>=(_, _) => None,
fn,
continuousShapes,
) =>
continuousShapes |> E.A.fold_left(
combinePointwise(~integralSumCachesFn, ~integralCachesFn, fn),
empty,
)
let mapY = (~integralSumCacheFn=_ => None, ~integralCacheFn=_ => None, ~fn, t: t) =>
make(
~interpolation=t.interpolation,
~integralSumCache=t.integralSumCache |> E.O.bind(_, integralSumCacheFn),
~integralCache=t.integralCache |> E.O.bind(_, integralCacheFn),
t |> getShape |> XYShape.T.mapY(fn),
)
let rec scaleBy = (~scale=1.0, t: t): t => {
let scaledIntegralSumCache = E.O.bind(t.integralSumCache, v => Some(scale *. v))
let scaledIntegralCache = E.O.bind(t.integralCache, v => Some(scaleBy(~scale, v)))
t
|> mapY(~fn=(r: float) => r *. scale)
|> updateIntegralSumCache(scaledIntegralSumCache)
|> updateIntegralCache(scaledIntegralCache)
}
module T = Dist({
type t = PointSetTypes.continuousShape
type integral = PointSetTypes.continuousShape
let minX = shapeFn(XYShape.T.minX)
let maxX = shapeFn(XYShape.T.maxX)
let mapY = mapY
let updateIntegralCache = updateIntegralCache
let toDiscreteProbabilityMassFraction = _ => 0.0
let toPointSetDist = (t: t): PointSetTypes.pointSetDist => Continuous(t)
let xToY = (f, {interpolation, xyShape}: t) =>
switch interpolation {
| #Stepwise => xyShape |> XYShape.XtoY.stepwiseIncremental(f) |> E.O.default(0.0)
| #Linear => xyShape |> XYShape.XtoY.linear(f)
} |> PointSetTypes.MixedPoint.makeContinuous
let truncate = (leftCutoff: option<float>, rightCutoff: option<float>, t: t) => {
let lc = E.O.default(neg_infinity, leftCutoff)
let rc = E.O.default(infinity, rightCutoff)
let truncatedZippedPairs =
t |> getShape |> XYShape.T.zip |> XYShape.Zipped.filterByX(x => x >= lc && x <= rc)
let leftNewPoint = leftCutoff |> E.O.dimap(lc => [(lc -. epsilon_float, 0.)], _ => [])
let rightNewPoint = rightCutoff |> E.O.dimap(rc => [(rc +. epsilon_float, 0.)], _ => [])
let truncatedZippedPairsWithNewPoints = E.A.concatMany([
leftNewPoint,
truncatedZippedPairs,
rightNewPoint,
])
let truncatedShape = XYShape.T.fromZippedArray(truncatedZippedPairsWithNewPoints)
make(truncatedShape)
}
// TODO: This should work with stepwise plots.
let integral = t =>
switch (getShape(t) |> XYShape.T.isEmpty, t.integralCache) {
| (true, _) => emptyIntegral
| (false, Some(cache)) => cache
| (false, None) =>
t
|> getShape
|> XYShape.Range.integrateWithTriangles
|> E.O.toExt("This should not have happened")
|> make
}
let downsample = (length, t): t =>
t |> shapeMap(XYShape.XsConversion.proportionByProbabilityMass(length, integral(t).xyShape))
let integralEndY = (t: t) => t.integralSumCache |> E.O.default(t |> integral |> lastY)
let integralXtoY = (f, t: t) => t |> integral |> shapeFn(XYShape.XtoY.linear(f))
let integralYtoX = (f, t: t) => t |> integral |> shapeFn(XYShape.YtoX.linear(f))
let toContinuous = t => Some(t)
let toDiscrete = _ => None
let normalize = (t: t): t =>
t
|> updateIntegralCache(Some(integral(t)))
|> scaleBy(~scale=1. /. integralEndY(t))
|> updateIntegralSumCache(Some(1.0))
let mean = (t: t) => {
let indefiniteIntegralStepwise = (p, h1) => h1 *. p ** 2.0 /. 2.0
let indefiniteIntegralLinear = (p, a, b) => a *. p ** 2.0 /. 2.0 +. b *. p ** 3.0 /. 3.0
XYShape.Analysis.integrateContinuousShape(
~indefiniteIntegralStepwise,
~indefiniteIntegralLinear,
t,
)
}
let variance = (t: t): float =>
XYShape.Analysis.getVarianceDangerously(
t,
mean,
XYShape.Analysis.getMeanOfSquaresContinuousShape,
)
})
/* This simply creates multiple copies of the continuous distribution, scaled and shifted according to
each discrete data point, and then adds them all together. */
let combineAlgebraicallyWithDiscrete = (
op: Operation.algebraicOperation,
t1: t,
t2: PointSetTypes.discreteShape,
) => {
let t1s = t1 |> getShape
let t2s = t2.xyShape // TODO would like to use Discrete.getShape here, but current file structure doesn't allow for that
if XYShape.T.isEmpty(t1s) || XYShape.T.isEmpty(t2s) {
empty
} else {
let continuousAsLinear = switch t1.interpolation {
| #Linear => t1
| #Stepwise => stepwiseToLinear(t1)
}
let combinedShape = AlgebraicShapeCombination.combineShapesContinuousDiscrete(
op,
continuousAsLinear |> getShape,
t2s,
)
let combinedIntegralSum = switch op {
| #Multiply
| #Divide =>
Common.combineIntegralSums((a, b) => Some(a *. b), t1.integralSumCache, t2.integralSumCache)
| _ => None
}
// TODO: It could make sense to automatically transform the integrals here (shift or scale)
make(~interpolation=t1.interpolation, ~integralSumCache=combinedIntegralSum, combinedShape)
}
}
let combineAlgebraically = (op: Operation.algebraicOperation, t1: t, t2: t) => {
let s1 = t1 |> getShape
let s2 = t2 |> getShape
let t1n = s1 |> XYShape.T.length
let t2n = s2 |> XYShape.T.length
if t1n == 0 || t2n == 0 {
empty
} else {
let combinedShape = AlgebraicShapeCombination.combineShapesContinuousContinuous(op, s1, s2)
let combinedIntegralSum = Common.combineIntegralSums(
(a, b) => Some(a *. b),
t1.integralSumCache,
t2.integralSumCache,
)
// return a new Continuous distribution
make(~integralSumCache=combinedIntegralSum, combinedShape)
}
}

View File

@ -0,0 +1,216 @@
open Distributions
type t = PointSetTypes.discreteShape
let make = (~integralSumCache=None, ~integralCache=None, xyShape): t => {
xyShape: xyShape,
integralSumCache: integralSumCache,
integralCache: integralCache,
}
let shapeMap = (fn, {xyShape, integralSumCache, integralCache}: t): t => {
xyShape: fn(xyShape),
integralSumCache: integralSumCache,
integralCache: integralCache,
}
let getShape = (t: t) => t.xyShape
let oShapeMap = (fn, {xyShape, integralSumCache, integralCache}: t): option<t> =>
fn(xyShape) |> E.O.fmap(make(~integralSumCache, ~integralCache))
let emptyIntegral: PointSetTypes.continuousShape = {
xyShape: {xs: [neg_infinity], ys: [0.0]},
interpolation: #Stepwise,
integralSumCache: Some(0.0),
integralCache: None,
}
let empty: PointSetTypes.discreteShape = {
xyShape: XYShape.T.empty,
integralSumCache: Some(0.0),
integralCache: Some(emptyIntegral),
}
let shapeFn = (fn, t: t) => t |> getShape |> fn
let lastY = (t: t) => t |> getShape |> XYShape.T.lastY
let combinePointwise = (
~integralSumCachesFn=(_, _) => None,
~integralCachesFn: (
PointSetTypes.continuousShape,
PointSetTypes.continuousShape,
) => option<PointSetTypes.continuousShape>=(_, _) => None,
fn,
t1: PointSetTypes.discreteShape,
t2: PointSetTypes.discreteShape,
): PointSetTypes.discreteShape => {
let combinedIntegralSum = Common.combineIntegralSums(
integralSumCachesFn,
t1.integralSumCache,
t2.integralSumCache,
)
// TODO: does it ever make sense to pointwise combine the integrals here?
// It could be done for pointwise additions, but is that ever needed?
make(
~integralSumCache=combinedIntegralSum,
XYShape.PointwiseCombination.combine(
\"+.",
XYShape.XtoY.discreteInterpolator,
t1.xyShape,
t2.xyShape,
),
)
}
let reduce = (
~integralSumCachesFn=(_, _) => None,
~integralCachesFn=(_, _) => None,
fn,
discreteShapes,
): PointSetTypes.discreteShape =>
discreteShapes |> E.A.fold_left(
combinePointwise(~integralSumCachesFn, ~integralCachesFn, fn),
empty,
)
let updateIntegralSumCache = (integralSumCache, t: t): t => {
...t,
integralSumCache: integralSumCache,
}
let updateIntegralCache = (integralCache, t: t): t => {
...t,
integralCache: integralCache,
}
/* This multiples all of the data points together and creates a new discrete distribution from the results.
Data points at the same xs get added together. It may be a good idea to downsample t1 and t2 before and/or the result after. */
let combineAlgebraically = (op: Operation.algebraicOperation, t1: t, t2: t): t => {
let t1s = t1 |> getShape
let t2s = t2 |> getShape
let t1n = t1s |> XYShape.T.length
let t2n = t2s |> XYShape.T.length
let combinedIntegralSum = Common.combineIntegralSums(
(s1, s2) => Some(s1 *. s2),
t1.integralSumCache,
t2.integralSumCache,
)
let fn = Operation.Algebraic.toFn(op)
let xToYMap = E.FloatFloatMap.empty()
for i in 0 to t1n - 1 {
for j in 0 to t2n - 1 {
let x = fn(t1s.xs[i], t2s.xs[j])
let cv = xToYMap |> E.FloatFloatMap.get(x) |> E.O.default(0.)
let my = t1s.ys[i] *. t2s.ys[j]
let _ = Belt.MutableMap.set(xToYMap, x, cv +. my)
}
}
let rxys = xToYMap |> E.FloatFloatMap.toArray |> XYShape.Zipped.sortByX
let combinedShape = XYShape.T.fromZippedArray(rxys)
make(~integralSumCache=combinedIntegralSum, combinedShape)
}
let mapY = (~integralSumCacheFn=_ => None, ~integralCacheFn=_ => None, ~fn, t: t) =>
make(
~integralSumCache=t.integralSumCache |> E.O.bind(_, integralSumCacheFn),
~integralCache=t.integralCache |> E.O.bind(_, integralCacheFn),
t |> getShape |> XYShape.T.mapY(fn),
)
let scaleBy = (~scale=1.0, t: t): t => {
let scaledIntegralSumCache = t.integralSumCache |> E.O.fmap(\"*."(scale))
let scaledIntegralCache = t.integralCache |> E.O.fmap(Continuous.scaleBy(~scale))
t
|> mapY(~fn=(r: float) => r *. scale)
|> updateIntegralSumCache(scaledIntegralSumCache)
|> updateIntegralCache(scaledIntegralCache)
}
module T = Dist({
type t = PointSetTypes.discreteShape
type integral = PointSetTypes.continuousShape
let integral = t =>
switch (getShape(t) |> XYShape.T.isEmpty, t.integralCache) {
| (true, _) => emptyIntegral
| (false, Some(c)) => c
| (false, None) =>
let ts = getShape(t)
// The first xy of this integral should always be the zero, to ensure nice plotting
let firstX = ts |> XYShape.T.minX
let prependedZeroPoint: XYShape.T.t = {xs: [firstX -. epsilon_float], ys: [0.]}
let integralShape =
ts |> XYShape.T.concat(prependedZeroPoint) |> XYShape.T.accumulateYs(\"+.")
Continuous.make(~interpolation=#Stepwise, integralShape)
}
let integralEndY = (t: t) => t.integralSumCache |> E.O.default(t |> integral |> Continuous.lastY)
let minX = shapeFn(XYShape.T.minX)
let maxX = shapeFn(XYShape.T.maxX)
let toDiscreteProbabilityMassFraction = _ => 1.0
let mapY = mapY
let updateIntegralCache = updateIntegralCache
let toPointSetDist = (t: t): PointSetTypes.pointSetDist => Discrete(t)
let toContinuous = _ => None
let toDiscrete = t => Some(t)
let normalize = (t: t): t =>
t |> scaleBy(~scale=1. /. integralEndY(t)) |> updateIntegralSumCache(Some(1.0))
let downsample = (i, t: t): t => {
// It's not clear how to downsample a set of discrete points in a meaningful way.
// The best we can do is to clip off the smallest values.
let currentLength = t |> getShape |> XYShape.T.length
if i < currentLength && (i >= 1 && currentLength > 1) {
t
|> getShape
|> XYShape.T.zip
|> XYShape.Zipped.sortByY
|> Belt.Array.reverse
|> Belt.Array.slice(_, ~offset=0, ~len=i)
|> XYShape.Zipped.sortByX
|> XYShape.T.fromZippedArray
|> make
} else {
t
}
}
let truncate = (leftCutoff: option<float>, rightCutoff: option<float>, t: t): t =>
t
|> getShape
|> XYShape.T.zip
|> XYShape.Zipped.filterByX(x =>
x >= E.O.default(neg_infinity, leftCutoff) && x <= E.O.default(infinity, rightCutoff)
)
|> XYShape.T.fromZippedArray
|> make
let xToY = (f, t) =>
t
|> getShape
|> XYShape.XtoY.stepwiseIfAtX(f)
|> E.O.default(0.0)
|> PointSetTypes.MixedPoint.makeDiscrete
let integralXtoY = (f, t) => t |> integral |> Continuous.getShape |> XYShape.XtoY.linear(f)
let integralYtoX = (f, t) => t |> integral |> Continuous.getShape |> XYShape.YtoX.linear(f)
let mean = (t: t): float => {
let s = getShape(t)
E.A.reducei(s.xs, 0.0, (acc, x, i) => acc +. x *. s.ys[i])
}
let variance = (t: t): float => {
let getMeanOfSquares = t => t |> shapeMap(XYShape.Analysis.squareXYShape) |> mean
XYShape.Analysis.getVarianceDangerously(t, mean, getMeanOfSquares)
}
})

View File

@ -1,40 +1,41 @@
open DistTypes;
open PointSetTypes;
type t = DistTypes.distPlus;
@genType
type t = PointSetTypes.distPlus;
let shapeIntegral = shape => Shape.T.Integral.get(shape);
let pointSetDistIntegral = pointSetDist => PointSetDist.T.Integral.get(pointSetDist);
let make =
(
~shape,
~pointSetDist,
~squiggleString,
~domain=Complete,
~unit=UnspecifiedDistribution,
(),
)
: t => {
let integral = shapeIntegral(shape);
{shape, domain, integralCache: integral, unit, squiggleString};
let integral = pointSetDistIntegral(pointSetDist);
{pointSetDist, domain, integralCache: integral, unit, squiggleString};
};
let update =
(
~shape=?,
~pointSetDist=?,
~integralCache=?,
~domain=?,
~unit=?,
~squiggleString=?,
t: t,
) => {
shape: E.O.default(t.shape, shape),
pointSetDist: E.O.default(t.pointSetDist, pointSetDist),
integralCache: E.O.default(t.integralCache, integralCache),
domain: E.O.default(t.domain, domain),
unit: E.O.default(t.unit, unit),
squiggleString: E.O.default(t.squiggleString, squiggleString),
};
let updateShape = (shape, t) => {
let integralCache = shapeIntegral(shape);
update(~shape, ~integralCache, t);
let updateShape = (pointSetDist, t) => {
let integralCache = pointSetDistIntegral(pointSetDist);
update(~pointSetDist, ~integralCache, t);
};
let domainIncludedProbabilityMass = (t: t) =>
@ -43,87 +44,87 @@ let domainIncludedProbabilityMass = (t: t) =>
let domainIncludedProbabilityMassAdjustment = (t: t, f) =>
f *. Domain.includedProbabilityMass(t.domain);
let toShape = ({shape, _}: t) => shape;
let toPointSetDist = ({pointSetDist, _}: t) => pointSetDist;
let shapeFn = (fn, {shape}: t) => fn(shape);
let pointSetDistFn = (fn, {pointSetDist}: t) => fn(pointSetDist);
module T =
Distributions.Dist({
type t = DistTypes.distPlus;
type integral = DistTypes.distPlus;
let toShape = toShape;
let toContinuous = shapeFn(Shape.T.toContinuous);
let toDiscrete = shapeFn(Shape.T.toDiscrete);
type t = PointSetTypes.distPlus;
type integral = PointSetTypes.distPlus;
let toPointSetDist = toPointSetDist;
let toContinuous = pointSetDistFn(PointSetDist.T.toContinuous);
let toDiscrete = pointSetDistFn(PointSetDist.T.toDiscrete);
let normalize = (t: t): t => {
let normalizedShape = t |> toShape |> Shape.T.normalize;
let normalizedShape = t |> toPointSetDist |> PointSetDist.T.normalize;
t |> updateShape(normalizedShape);
};
let truncate = (leftCutoff, rightCutoff, t: t): t => {
let truncatedShape =
t
|> toShape
|> Shape.T.truncate(leftCutoff, rightCutoff);
|> toPointSetDist
|> PointSetDist.T.truncate(leftCutoff, rightCutoff);
t |> updateShape(truncatedShape);
};
let xToY = (f, t: t) =>
t
|> toShape
|> Shape.T.xToY(f)
|> toPointSetDist
|> PointSetDist.T.xToY(f)
|> MixedPoint.fmap(domainIncludedProbabilityMassAdjustment(t));
let minX = shapeFn(Shape.T.minX);
let maxX = shapeFn(Shape.T.maxX);
let minX = pointSetDistFn(PointSetDist.T.minX);
let maxX = pointSetDistFn(PointSetDist.T.maxX);
let toDiscreteProbabilityMassFraction =
shapeFn(Shape.T.toDiscreteProbabilityMassFraction);
pointSetDistFn(PointSetDist.T.toDiscreteProbabilityMassFraction);
// This bit is kind of awkward, could probably use rethinking.
let integral = (t: t) =>
updateShape(Continuous(t.integralCache), t);
let updateIntegralCache = (integralCache: option(DistTypes.continuousShape), t) =>
let updateIntegralCache = (integralCache: option<PointSetTypes.continuousShape>, t) =>
update(~integralCache=E.O.default(t.integralCache, integralCache), t);
let downsample = (i, t): t =>
updateShape(t |> toShape |> Shape.T.downsample(i), t);
updateShape(t |> toPointSetDist |> PointSetDist.T.downsample(i), t);
// todo: adjust for limit, maybe?
let mapY =
(
~integralSumCacheFn=previousIntegralSum => None,
~integralCacheFn=previousIntegralCache => None,
~fn,
{shape, _} as t: t,
{pointSetDist, _} as t: t,
)
: t =>
Shape.T.mapY(~integralSumCacheFn, ~fn, shape)
PointSetDist.T.mapY(~integralSumCacheFn, ~fn, pointSetDist)
|> updateShape(_, t);
// get the total of everything
let integralEndY = (t: t) => {
Shape.T.Integral.sum(
toShape(t),
PointSetDist.T.Integral.sum(
toPointSetDist(t),
);
};
// TODO: Fix this below, obviously. Adjust for limits
let integralXtoY = (f, t: t) => {
Shape.T.Integral.xToY(
PointSetDist.T.Integral.xToY(
f,
toShape(t),
toPointSetDist(t),
)
|> domainIncludedProbabilityMassAdjustment(t);
};
// TODO: This part is broken when there is a limit, if this is supposed to be taken into account.
let integralYtoX = (f, t: t) => {
Shape.T.Integral.yToX(f, toShape(t));
PointSetDist.T.Integral.yToX(f, toPointSetDist(t));
};
let mean = (t: t) => {
Shape.T.mean(t.shape);
PointSetDist.T.mean(t.pointSetDist);
};
let variance = (t: t) => Shape.T.variance(t.shape);
let variance = (t: t) => PointSetDist.T.variance(t.pointSetDist);
});

View File

@ -0,0 +1,89 @@
module type dist = {
type t
type integral
let minX: t => float
let maxX: t => float
let mapY: (
~integralSumCacheFn: float => option<float>=?,
~integralCacheFn: PointSetTypes.continuousShape => option<PointSetTypes.continuousShape>=?,
~fn: float => float,
t,
) => t
let xToY: (float, t) => PointSetTypes.mixedPoint
let toPointSetDist: t => PointSetTypes.pointSetDist
let toContinuous: t => option<PointSetTypes.continuousShape>
let toDiscrete: t => option<PointSetTypes.discreteShape>
let normalize: t => t
let toDiscreteProbabilityMassFraction: t => float
let downsample: (int, t) => t
let truncate: (option<float>, option<float>, t) => t
let updateIntegralCache: (option<PointSetTypes.continuousShape>, t) => t
let integral: t => integral
let integralEndY: t => float
let integralXtoY: (float, t) => float
let integralYtoX: (float, t) => float
let mean: t => float
let variance: t => float
}
module Dist = (T: dist) => {
type t = T.t
type integral = T.integral
let minX = T.minX
let maxX = T.maxX
let integral = T.integral
let xTotalRange = (t: t) => maxX(t) -. minX(t)
let mapY = T.mapY
let xToY = T.xToY
let downsample = T.downsample
let toPointSetDist = T.toPointSetDist
let toDiscreteProbabilityMassFraction = T.toDiscreteProbabilityMassFraction
let toContinuous = T.toContinuous
let toDiscrete = T.toDiscrete
let normalize = T.normalize
let truncate = T.truncate
let mean = T.mean
let variance = T.variance
let updateIntegralCache = T.updateIntegralCache
module Integral = {
type t = T.integral
let get = T.integral
let xToY = T.integralXtoY
let yToX = T.integralYtoX
let sum = T.integralEndY
}
}
module Common = {
let combineIntegralSums = (
combineFn: (float, float) => option<float>,
t1IntegralSumCache: option<float>,
t2IntegralSumCache: option<float>,
) =>
switch (t1IntegralSumCache, t2IntegralSumCache) {
| (None, _)
| (_, None) =>
None
| (Some(s1), Some(s2)) => combineFn(s1, s2)
}
let combineIntegrals = (
combineFn: (
PointSetTypes.continuousShape,
PointSetTypes.continuousShape,
) => option<PointSetTypes.continuousShape>,
t1IntegralCache: option<PointSetTypes.continuousShape>,
t2IntegralCache: option<PointSetTypes.continuousShape>,
) =>
switch (t1IntegralCache, t2IntegralCache) {
| (None, _)
| (_, None) =>
None
| (Some(s1), Some(s2)) => combineFn(s1, s2)
}
}

View File

@ -0,0 +1,304 @@
open Distributions
type t = PointSetTypes.mixedShape
let make = (~integralSumCache=None, ~integralCache=None, ~continuous, ~discrete): t => {
continuous: continuous,
discrete: discrete,
integralSumCache: integralSumCache,
integralCache: integralCache,
}
let totalLength = (t: t): int => {
let continuousLength = t.continuous |> Continuous.getShape |> XYShape.T.length
let discreteLength = t.discrete |> Discrete.getShape |> XYShape.T.length
continuousLength + discreteLength
}
let scaleBy = (~scale=1.0, t: t): t => {
let scaledDiscrete = Discrete.scaleBy(~scale, t.discrete)
let scaledContinuous = Continuous.scaleBy(~scale, t.continuous)
let scaledIntegralCache = E.O.bind(t.integralCache, v => Some(Continuous.scaleBy(~scale, v)))
let scaledIntegralSumCache = E.O.bind(t.integralSumCache, s => Some(s *. scale))
make(
~discrete=scaledDiscrete,
~continuous=scaledContinuous,
~integralSumCache=scaledIntegralSumCache,
~integralCache=scaledIntegralCache,
)
}
let toContinuous = ({continuous}: t) => Some(continuous)
let toDiscrete = ({discrete}: t) => Some(discrete)
let updateIntegralCache = (integralCache, t: t): t => {
...t,
integralCache: integralCache,
}
module T = Dist({
type t = PointSetTypes.mixedShape
type integral = PointSetTypes.continuousShape
let minX = ({continuous, discrete}: t) =>
min(Continuous.T.minX(continuous), Discrete.T.minX(discrete))
let maxX = ({continuous, discrete}: t) =>
max(Continuous.T.maxX(continuous), Discrete.T.maxX(discrete))
let toPointSetDist = (t: t): PointSetTypes.pointSetDist => Mixed(t)
let updateIntegralCache = updateIntegralCache
let toContinuous = toContinuous
let toDiscrete = toDiscrete
let truncate = (
leftCutoff: option<float>,
rightCutoff: option<float>,
{discrete, continuous}: t,
) => {
let truncatedContinuous = Continuous.T.truncate(leftCutoff, rightCutoff, continuous)
let truncatedDiscrete = Discrete.T.truncate(leftCutoff, rightCutoff, discrete)
make(
~integralSumCache=None,
~integralCache=None,
~discrete=truncatedDiscrete,
~continuous=truncatedContinuous,
)
}
let normalize = (t: t): t => {
let continuousIntegral = Continuous.T.Integral.get(t.continuous)
let discreteIntegral = Discrete.T.Integral.get(t.discrete)
let continuous = t.continuous |> Continuous.updateIntegralCache(Some(continuousIntegral))
let discrete = t.discrete |> Discrete.updateIntegralCache(Some(discreteIntegral))
let continuousIntegralSum = Continuous.T.Integral.sum(continuous)
let discreteIntegralSum = Discrete.T.Integral.sum(discrete)
let totalIntegralSum = continuousIntegralSum +. discreteIntegralSum
let newContinuousSum = continuousIntegralSum /. totalIntegralSum
let newDiscreteSum = discreteIntegralSum /. totalIntegralSum
let normalizedContinuous =
continuous
|> Continuous.scaleBy(~scale=newContinuousSum /. continuousIntegralSum)
|> Continuous.updateIntegralSumCache(Some(newContinuousSum))
let normalizedDiscrete =
discrete
|> Discrete.scaleBy(~scale=newDiscreteSum /. discreteIntegralSum)
|> Discrete.updateIntegralSumCache(Some(newDiscreteSum))
make(
~integralSumCache=Some(1.0),
~integralCache=None,
~continuous=normalizedContinuous,
~discrete=normalizedDiscrete,
)
}
let xToY = (x, t: t) => {
// This evaluates the mixedShape at x, interpolating if necessary.
// Note that we normalize entire mixedShape first.
let {continuous, discrete}: t = normalize(t)
let c = Continuous.T.xToY(x, continuous)
let d = Discrete.T.xToY(x, discrete)
PointSetTypes.MixedPoint.add(c, d) // "add" here just combines the two values into a single MixedPoint.
}
let toDiscreteProbabilityMassFraction = ({discrete, continuous}: t) => {
let discreteIntegralSum = Discrete.T.Integral.sum(discrete)
let continuousIntegralSum = Continuous.T.Integral.sum(continuous)
let totalIntegralSum = discreteIntegralSum +. continuousIntegralSum
discreteIntegralSum /. totalIntegralSum
}
let downsample = (count, t: t): t => {
// We will need to distribute the new xs fairly between the discrete and continuous shapes.
// The easiest way to do this is to simply go by the previous probability masses.
let discreteIntegralSum = Discrete.T.Integral.sum(t.discrete)
let continuousIntegralSum = Continuous.T.Integral.sum(t.continuous)
let totalIntegralSum = discreteIntegralSum +. continuousIntegralSum
// TODO: figure out what to do when the totalIntegralSum is zero.
let downsampledDiscrete = Discrete.T.downsample(
int_of_float(float_of_int(count) *. (discreteIntegralSum /. totalIntegralSum)),
t.discrete,
)
let downsampledContinuous = Continuous.T.downsample(
int_of_float(float_of_int(count) *. (continuousIntegralSum /. totalIntegralSum)),
t.continuous,
)
{...t, discrete: downsampledDiscrete, continuous: downsampledContinuous}
}
let integral = (t: t) =>
switch t.integralCache {
| Some(cache) => cache
| None =>
// note: if the underlying shapes aren't normalized, then these integrals won't be either -- but that's the way it should be.
let continuousIntegral = Continuous.T.Integral.get(t.continuous)
let discreteIntegral = Continuous.stepwiseToLinear(Discrete.T.Integral.get(t.discrete))
Continuous.make(
XYShape.PointwiseCombination.combine(
\"+.",
XYShape.XtoY.continuousInterpolator(#Linear, #UseOutermostPoints),
Continuous.getShape(continuousIntegral),
Continuous.getShape(discreteIntegral),
),
)
}
let integralEndY = (t: t) => t |> integral |> Continuous.lastY
let integralXtoY = (f, t) => t |> integral |> Continuous.getShape |> XYShape.XtoY.linear(f)
let integralYtoX = (f, t) => t |> integral |> Continuous.getShape |> XYShape.YtoX.linear(f)
// This pipes all ys (continuous and discrete) through fn.
// If mapY is a linear operation, we might be able to update the integralSumCaches as well;
// if not, they'll be set to None.
let mapY = (
~integralSumCacheFn=previousIntegralSum => None,
~integralCacheFn=previousIntegral => None,
~fn,
t: t,
): t => {
let yMappedDiscrete: PointSetTypes.discreteShape =
t.discrete
|> Discrete.T.mapY(~fn)
|> Discrete.updateIntegralSumCache(E.O.bind(t.discrete.integralSumCache, integralSumCacheFn))
|> Discrete.updateIntegralCache(E.O.bind(t.discrete.integralCache, integralCacheFn))
let yMappedContinuous: PointSetTypes.continuousShape =
t.continuous
|> Continuous.T.mapY(~fn)
|> Continuous.updateIntegralSumCache(
E.O.bind(t.continuous.integralSumCache, integralSumCacheFn),
)
|> Continuous.updateIntegralCache(E.O.bind(t.continuous.integralCache, integralCacheFn))
{
discrete: yMappedDiscrete,
continuous: yMappedContinuous,
integralSumCache: E.O.bind(t.integralSumCache, integralSumCacheFn),
integralCache: E.O.bind(t.integralCache, integralCacheFn),
}
}
let mean = ({discrete, continuous}: t): float => {
let discreteMean = Discrete.T.mean(discrete)
let continuousMean = Continuous.T.mean(continuous)
// the combined mean is the weighted sum of the two:
let discreteIntegralSum = Discrete.T.Integral.sum(discrete)
let continuousIntegralSum = Continuous.T.Integral.sum(continuous)
let totalIntegralSum = discreteIntegralSum +. continuousIntegralSum
(discreteMean *. discreteIntegralSum +. continuousMean *. continuousIntegralSum) /.
totalIntegralSum
}
let variance = ({discrete, continuous} as t: t): float => {
// the combined mean is the weighted sum of the two:
let discreteIntegralSum = Discrete.T.Integral.sum(discrete)
let continuousIntegralSum = Continuous.T.Integral.sum(continuous)
let totalIntegralSum = discreteIntegralSum +. continuousIntegralSum
let getMeanOfSquares = ({discrete, continuous}: t) => {
let discreteMean =
discrete |> Discrete.shapeMap(XYShape.Analysis.squareXYShape) |> Discrete.T.mean
let continuousMean = continuous |> XYShape.Analysis.getMeanOfSquaresContinuousShape
(discreteMean *. discreteIntegralSum +. continuousMean *. continuousIntegralSum) /.
totalIntegralSum
}
switch discreteIntegralSum /. totalIntegralSum {
| 1.0 => Discrete.T.variance(discrete)
| 0.0 => Continuous.T.variance(continuous)
| _ => XYShape.Analysis.getVarianceDangerously(t, mean, getMeanOfSquares)
}
}
})
let combineAlgebraically = (op: Operation.algebraicOperation, t1: t, t2: t): t => {
// Discrete convolution can cause a huge increase in the number of samples,
// so we'll first downsample.
// An alternative (to be explored in the future) may be to first perform the full convolution and then to downsample the result;
// to use non-uniform fast Fourier transforms (for addition only), add web workers or gpu.js, etc. ...
// we have to figure out where to downsample, and how to effectively
//let downsampleIfTooLarge = (t: t) => {
// let sqtl = sqrt(float_of_int(totalLength(t)));
// sqtl > 10 ? T.downsample(int_of_float(sqtl), t) : t;
//};
// continuous (*) continuous => continuous, but also
// discrete (*) continuous => continuous (and vice versa). We have to take care of all combos and then combine them:
let ccConvResult = Continuous.combineAlgebraically(op, t1.continuous, t2.continuous)
let dcConvResult = Continuous.combineAlgebraicallyWithDiscrete(op, t2.continuous, t1.discrete)
let cdConvResult = Continuous.combineAlgebraicallyWithDiscrete(op, t1.continuous, t2.discrete)
let continuousConvResult = Continuous.reduce(\"+.", [ccConvResult, dcConvResult, cdConvResult])
// ... finally, discrete (*) discrete => discrete, obviously:
let discreteConvResult = Discrete.combineAlgebraically(op, t1.discrete, t2.discrete)
let combinedIntegralSum = Common.combineIntegralSums(
(a, b) => Some(a *. b),
t1.integralSumCache,
t2.integralSumCache,
)
{
discrete: discreteConvResult,
continuous: continuousConvResult,
integralSumCache: combinedIntegralSum,
integralCache: None,
}
}
let combinePointwise = (
~integralSumCachesFn=(_, _) => None,
~integralCachesFn=(_, _) => None,
fn,
t1: t,
t2: t,
): t => {
let reducedDiscrete =
[t1, t2]
|> E.A.fmap(toDiscrete)
|> E.A.O.concatSomes
|> Discrete.reduce(~integralSumCachesFn, ~integralCachesFn, fn)
let reducedContinuous =
[t1, t2]
|> E.A.fmap(toContinuous)
|> E.A.O.concatSomes
|> Continuous.reduce(~integralSumCachesFn, ~integralCachesFn, fn)
let combinedIntegralSum = Common.combineIntegralSums(
integralSumCachesFn,
t1.integralSumCache,
t2.integralSumCache,
)
let combinedIntegral = Common.combineIntegrals(
integralCachesFn,
t1.integralCache,
t2.integralCache,
)
make(
~integralSumCache=combinedIntegralSum,
~integralCache=combinedIntegral,
~discrete=reducedDiscrete,
~continuous=reducedContinuous,
)
}

View File

@ -0,0 +1,29 @@
type assumption =
| ADDS_TO_1
| ADDS_TO_CORRECT_PROBABILITY
type assumptions = {
continuous: assumption,
discrete: assumption,
discreteProbabilityMass: option<float>,
}
let buildSimple = (
~continuous: option<PointSetTypes.continuousShape>,
~discrete: option<PointSetTypes.discreteShape>,
): option<PointSetTypes.pointSetDist> => {
let continuous =
continuous |> E.O.default(Continuous.make(~integralSumCache=Some(0.0), {xs: [], ys: []}))
let discrete =
discrete |> E.O.default(Discrete.make(~integralSumCache=Some(0.0), {xs: [], ys: []}))
let cLength = continuous |> Continuous.getShape |> XYShape.T.xs |> E.A.length
let dLength = discrete |> Discrete.getShape |> XYShape.T.xs |> E.A.length
switch (cLength, dLength) {
| (0 | 1, 0) => None
| (0 | 1, _) => Some(Discrete(discrete))
| (_, 0) => Some(Continuous(continuous))
| (_, _) =>
let mixedDist = Mixed.make(~integralSumCache=None, ~integralCache=None, ~continuous, ~discrete)
Some(Mixed(mixedDist))
}
}

View File

@ -0,0 +1,203 @@
open Distributions
type t = PointSetTypes.pointSetDist
let mapToAll = ((fn1, fn2, fn3), t: t) =>
switch t {
| Mixed(m) => fn1(m)
| Discrete(m) => fn2(m)
| Continuous(m) => fn3(m)
}
let fmap = ((fn1, fn2, fn3), t: t): t =>
switch t {
| Mixed(m) => Mixed(fn1(m))
| Discrete(m) => Discrete(fn2(m))
| Continuous(m) => Continuous(fn3(m))
}
let toMixed = mapToAll((
m => m,
d =>
Mixed.make(
~integralSumCache=d.integralSumCache,
~integralCache=d.integralCache,
~discrete=d,
~continuous=Continuous.empty,
),
c =>
Mixed.make(
~integralSumCache=c.integralSumCache,
~integralCache=c.integralCache,
~discrete=Discrete.empty,
~continuous=c,
),
))
let combineAlgebraically = (op: Operation.algebraicOperation, t1: t, t2: t): t =>
switch (t1, t2) {
| (Continuous(m1), Continuous(m2)) =>
Continuous.combineAlgebraically(op, m1, m2) |> Continuous.T.toPointSetDist
| (Continuous(m1), Discrete(m2))
| (Discrete(m2), Continuous(m1)) =>
Continuous.combineAlgebraicallyWithDiscrete(op, m1, m2) |> Continuous.T.toPointSetDist
| (Discrete(m1), Discrete(m2)) => Discrete.combineAlgebraically(op, m1, m2) |> Discrete.T.toPointSetDist
| (m1, m2) => Mixed.combineAlgebraically(op, toMixed(m1), toMixed(m2)) |> Mixed.T.toPointSetDist
}
let combinePointwise = (
~integralSumCachesFn: (float, float) => option<float>=(_, _) => None,
~integralCachesFn: (
PointSetTypes.continuousShape,
PointSetTypes.continuousShape,
) => option<PointSetTypes.continuousShape>=(_, _) => None,
fn,
t1: t,
t2: t,
) =>
switch (t1, t2) {
| (Continuous(m1), Continuous(m2)) =>
PointSetTypes.Continuous(
Continuous.combinePointwise(~integralSumCachesFn, ~integralCachesFn, fn, m1, m2),
)
| (Discrete(m1), Discrete(m2)) =>
PointSetTypes.Discrete(
Discrete.combinePointwise(~integralSumCachesFn, ~integralCachesFn, fn, m1, m2),
)
| (m1, m2) =>
PointSetTypes.Mixed(
Mixed.combinePointwise(~integralSumCachesFn, ~integralCachesFn, fn, toMixed(m1), toMixed(m2)),
)
}
module T = Dist({
type t = PointSetTypes.pointSetDist
type integral = PointSetTypes.continuousShape
let xToY = (f: float) => mapToAll((Mixed.T.xToY(f), Discrete.T.xToY(f), Continuous.T.xToY(f)))
let toPointSetDist = (t: t) => t
let downsample = (i, t) =>
fmap((Mixed.T.downsample(i), Discrete.T.downsample(i), Continuous.T.downsample(i)), t)
let truncate = (leftCutoff, rightCutoff, t): t =>
fmap(
(
Mixed.T.truncate(leftCutoff, rightCutoff),
Discrete.T.truncate(leftCutoff, rightCutoff),
Continuous.T.truncate(leftCutoff, rightCutoff),
),
t,
)
let normalize = fmap((Mixed.T.normalize, Discrete.T.normalize, Continuous.T.normalize))
let updateIntegralCache = (integralCache, t: t): t =>
fmap(
(
Mixed.T.updateIntegralCache(integralCache),
Discrete.T.updateIntegralCache(integralCache),
Continuous.T.updateIntegralCache(integralCache),
),
t,
)
let toContinuous = mapToAll((
Mixed.T.toContinuous,
Discrete.T.toContinuous,
Continuous.T.toContinuous,
))
let toDiscrete = mapToAll((Mixed.T.toDiscrete, Discrete.T.toDiscrete, Continuous.T.toDiscrete))
let toDiscreteProbabilityMassFraction = mapToAll((
Mixed.T.toDiscreteProbabilityMassFraction,
Discrete.T.toDiscreteProbabilityMassFraction,
Continuous.T.toDiscreteProbabilityMassFraction,
))
let minX = mapToAll((Mixed.T.minX, Discrete.T.minX, Continuous.T.minX))
let integral = mapToAll((
Mixed.T.Integral.get,
Discrete.T.Integral.get,
Continuous.T.Integral.get,
))
let integralEndY = mapToAll((
Mixed.T.Integral.sum,
Discrete.T.Integral.sum,
Continuous.T.Integral.sum,
))
let integralXtoY = f =>
mapToAll((Mixed.T.Integral.xToY(f), Discrete.T.Integral.xToY(f), Continuous.T.Integral.xToY(f)))
let integralYtoX = f =>
mapToAll((Mixed.T.Integral.yToX(f), Discrete.T.Integral.yToX(f), Continuous.T.Integral.yToX(f)))
let maxX = mapToAll((Mixed.T.maxX, Discrete.T.maxX, Continuous.T.maxX))
let mapY = (
~integralSumCacheFn=previousIntegralSum => None,
~integralCacheFn=previousIntegral => None,
~fn,
) =>
fmap((
Mixed.T.mapY(~integralSumCacheFn, ~integralCacheFn, ~fn),
Discrete.T.mapY(~integralSumCacheFn, ~integralCacheFn, ~fn),
Continuous.T.mapY(~integralSumCacheFn, ~integralCacheFn, ~fn),
))
let mean = (t: t): float =>
switch t {
| Mixed(m) => Mixed.T.mean(m)
| Discrete(m) => Discrete.T.mean(m)
| Continuous(m) => Continuous.T.mean(m)
}
let variance = (t: t): float =>
switch t {
| Mixed(m) => Mixed.T.variance(m)
| Discrete(m) => Discrete.T.variance(m)
| Continuous(m) => Continuous.T.variance(m)
}
})
let pdf = (f: float, t: t) => {
let mixedPoint: PointSetTypes.mixedPoint = T.xToY(f, t)
mixedPoint.continuous +. mixedPoint.discrete
}
let inv = T.Integral.yToX
let cdf = T.Integral.xToY
let doN = (n, fn) => {
let items = Belt.Array.make(n, 0.0)
for x in 0 to n - 1 {
let _ = Belt.Array.set(items, x, fn())
}
items
}
let sample = (t: t): float => {
let randomItem = Random.float(1.)
let bar = t |> T.Integral.yToX(randomItem)
bar
}
let isFloat = (t: t) =>
switch t {
| Discrete({xyShape: {xs: [_], ys: [1.0]}}) => true
| _ => false
}
let sampleNRendered = (n, dist) => {
let integralCache = T.Integral.get(dist)
let distWithUpdatedIntegralCache = T.updateIntegralCache(Some(integralCache), dist)
doN(n, () => sample(distWithUpdatedIntegralCache))
}
let operate = (distToFloatOp: Operation.distToFloatOperation, s): float =>
switch distToFloatOp {
| #Pdf(f) => pdf(f, s)
| #Cdf(f) => pdf(f, s)
| #Inv(f) => inv(f, s)
| #Sample => sample(s)
| #Mean => T.mean(s)
}

View File

@ -0,0 +1,154 @@
type domainLimit = {
xPoint: float,
excludingProbabilityMass: float,
}
type domain =
| Complete
| LeftLimited(domainLimit)
| RightLimited(domainLimit)
| LeftAndRightLimited(domainLimit, domainLimit)
type distributionType = [
| #PDF
| #CDF
]
type xyShape = {
xs: array<float>,
ys: array<float>,
}
type interpolationStrategy = [
| #Stepwise
| #Linear
]
type extrapolationStrategy = [
| #UseZero
| #UseOutermostPoints
]
type interpolator = (xyShape, int, float) => float
type rec continuousShape = {
xyShape: xyShape,
interpolation: interpolationStrategy,
integralSumCache: option<float>,
integralCache: option<continuousShape>,
}
type discreteShape = {
xyShape: xyShape,
integralSumCache: option<float>,
integralCache: option<continuousShape>,
}
type mixedShape = {
continuous: continuousShape,
discrete: discreteShape,
integralSumCache: option<float>,
integralCache: option<continuousShape>,
}
type pointSetDistMonad<'a, 'b, 'c> =
| Mixed('a)
| Discrete('b)
| Continuous('c)
@genType
type pointSetDist = pointSetDistMonad<mixedShape, discreteShape, continuousShape>
module ShapeMonad = {
let fmap = (t: pointSetDistMonad<'a, 'b, 'c>, (fn1, fn2, fn3)): pointSetDistMonad<'d, 'e, 'f> =>
switch t {
| Mixed(m) => Mixed(fn1(m))
| Discrete(m) => Discrete(fn2(m))
| Continuous(m) => Continuous(fn3(m))
}
}
type generationSource =
| SquiggleString(string)
| Shape(pointSetDist)
type distributionUnit =
| UnspecifiedDistribution
@genType
type distPlus = {
pointSetDist: pointSetDist,
domain: domain,
integralCache: continuousShape,
unit: distributionUnit,
squiggleString: option<string>,
}
module DistributionUnit = {
let toJson = (distributionUnit: distributionUnit) =>
switch distributionUnit {
| _ => Js.Null.fromOption(None)
}
}
module Domain = {
let excludedProbabilityMass = (t: domain) =>
switch t {
| Complete => 0.0
| LeftLimited({excludingProbabilityMass}) => excludingProbabilityMass
| RightLimited({excludingProbabilityMass}) => excludingProbabilityMass
| LeftAndRightLimited({excludingProbabilityMass: l}, {excludingProbabilityMass: r}) => l +. r
}
let includedProbabilityMass = (t: domain) => 1.0 -. excludedProbabilityMass(t)
let initialProbabilityMass = (t: domain) =>
switch t {
| Complete
| RightLimited(_) => 0.0
| LeftLimited({excludingProbabilityMass}) => excludingProbabilityMass
| LeftAndRightLimited({excludingProbabilityMass}, _) => excludingProbabilityMass
}
let normalizeProbabilityMass = (t: domain) => 1. /. excludedProbabilityMass(t)
let yPointToSubYPoint = (t: domain, yPoint) =>
switch t {
| Complete => Some(yPoint)
| LeftLimited({excludingProbabilityMass}) if yPoint < excludingProbabilityMass => None
| LeftLimited({excludingProbabilityMass}) if yPoint >= excludingProbabilityMass =>
Some((yPoint -. excludingProbabilityMass) /. includedProbabilityMass(t))
| RightLimited({excludingProbabilityMass}) if yPoint > 1. -. excludingProbabilityMass => None
| RightLimited({excludingProbabilityMass}) if yPoint <= 1. -. excludingProbabilityMass =>
Some(yPoint /. includedProbabilityMass(t))
| LeftAndRightLimited({excludingProbabilityMass: l}, _) if yPoint < l => None
| LeftAndRightLimited(_, {excludingProbabilityMass: r}) if yPoint > 1.0 -. r => None
| LeftAndRightLimited({excludingProbabilityMass: l}, _) =>
Some((yPoint -. l) /. includedProbabilityMass(t))
| _ => None
}
}
type mixedPoint = {
continuous: float,
discrete: float,
}
module MixedPoint = {
type t = mixedPoint
let toContinuousValue = (t: t) => t.continuous
let toDiscreteValue = (t: t) => t.discrete
let makeContinuous = (continuous: float): t => {continuous: continuous, discrete: 0.0}
let makeDiscrete = (discrete: float): t => {continuous: 0.0, discrete: discrete}
let fmap = (fn: float => float, t: t) => {
continuous: fn(t.continuous),
discrete: fn(t.discrete),
}
let combine2 = (fn, c: t, d: t): t => {
continuous: fn(c.continuous, d.continuous),
discrete: fn(c.discrete, d.discrete),
}
let add = combine2((a, b) => a +. b)
}

View File

@ -0,0 +1,440 @@
open PointSetTypes
let interpolate = (xMin: float, xMax: float, yMin: float, yMax: float, xIntended: float): float => {
let minProportion = (xMax -. xIntended) /. (xMax -. xMin)
let maxProportion = (xIntended -. xMin) /. (xMax -. xMin)
yMin *. minProportion +. yMax *. maxProportion
}
// TODO: Make sure that shapes cannot be empty.
let extImp = E.O.toExt("Tried to perform an operation on an empty XYShape.")
module T = {
type t = xyShape
let toXyShape = (t: t): xyShape => t
type ts = array<xyShape>
let xs = (t: t) => t.xs
let ys = (t: t) => t.ys
let length = (t: t) => E.A.length(t.xs)
let empty = {xs: [], ys: []}
let isEmpty = (t: t) => length(t) == 0
let minX = (t: t) => t |> xs |> E.A.Sorted.min |> extImp
let maxX = (t: t) => t |> xs |> E.A.Sorted.max |> extImp
let firstY = (t: t) => t |> ys |> E.A.first |> extImp
let lastY = (t: t) => t |> ys |> E.A.last |> extImp
let xTotalRange = (t: t) => maxX(t) -. minX(t)
let mapX = (fn, t: t): t => {xs: E.A.fmap(fn, t.xs), ys: t.ys}
let mapY = (fn, t: t): t => {xs: t.xs, ys: E.A.fmap(fn, t.ys)}
let zip = ({xs, ys}: t) => Belt.Array.zip(xs, ys)
let fromArray = ((xs, ys)): t => {xs: xs, ys: ys}
let fromArrays = (xs, ys): t => {xs: xs, ys: ys}
let accumulateYs = (fn, p: t) => fromArray((p.xs, E.A.accumulate(fn, p.ys)))
let concat = (t1: t, t2: t) => {
let cxs = Array.concat(list{t1.xs, t2.xs})
let cys = Array.concat(list{t1.ys, t2.ys})
{xs: cxs, ys: cys}
}
let fromZippedArray = (pairs: array<(float, float)>): t => pairs |> Belt.Array.unzip |> fromArray
let equallyDividedXs = (t: t, newLength) => E.A.Floats.range(minX(t), maxX(t), newLength)
let toJs = (t: t) => {"xs": t.xs, "ys": t.ys}
}
module Ts = {
type t = T.ts
let minX = (t: t) => t |> E.A.fmap(T.minX) |> E.A.min |> extImp
let maxX = (t: t) => t |> E.A.fmap(T.maxX) |> E.A.max |> extImp
let equallyDividedXs = (t: t, newLength) => E.A.Floats.range(minX(t), maxX(t), newLength)
let allXs = (t: t) => t |> E.A.fmap(T.xs) |> E.A.Sorted.concatMany
}
module Pairs = {
let x = fst
let y = snd
let first = (t: T.t) => (T.minX(t), T.firstY(t))
let last = (t: T.t) => (T.maxX(t), T.lastY(t))
let getBy = (t: T.t, fn) => t |> T.zip |> E.A.getBy(_, fn)
let firstAtOrBeforeXValue = (xValue, t: T.t) => {
let zipped = T.zip(t)
let firstIndex = zipped |> Belt.Array.getIndexBy(_, ((x, _)) => x > xValue)
let previousIndex = switch firstIndex {
| None => Some(Array.length(zipped) - 1)
| Some(0) => None
| Some(n) => Some(n - 1)
}
previousIndex |> Belt.Option.flatMap(_, Belt.Array.get(zipped))
}
}
module YtoX = {
let linear = (y: float, t: T.t): float => {
let firstHigherIndex = E.A.Sorted.binarySearchFirstElementGreaterIndex(T.ys(t), y)
let foundX = switch firstHigherIndex {
| #overMax => T.maxX(t)
| #underMin => T.minX(t)
| #firstHigher(firstHigherIndex) =>
let lowerOrEqualIndex = firstHigherIndex - 1 < 0 ? 0 : firstHigherIndex - 1
let (_xs, _ys) = (T.xs(t), T.ys(t))
let needsInterpolation = _ys[lowerOrEqualIndex] != y
if needsInterpolation {
interpolate(
_ys[lowerOrEqualIndex],
_ys[firstHigherIndex],
_xs[lowerOrEqualIndex],
_xs[firstHigherIndex],
y,
)
} else {
_xs[lowerOrEqualIndex]
}
}
foundX
}
}
module XtoY = {
let stepwiseIncremental = (f, t: T.t) => Pairs.firstAtOrBeforeXValue(f, t) |> E.O.fmap(Pairs.y)
let stepwiseIfAtX = (f: float, t: T.t) =>
Pairs.getBy(t, ((x: float, _)) => x == f) |> E.O.fmap(Pairs.y)
let linear = (x: float, t: T.t): float => {
let firstHigherIndex = E.A.Sorted.binarySearchFirstElementGreaterIndex(T.xs(t), x)
let n = switch firstHigherIndex {
| #overMax => T.lastY(t)
| #underMin => T.firstY(t)
| #firstHigher(firstHigherIndex) =>
let lowerOrEqualIndex = firstHigherIndex - 1 < 0 ? 0 : firstHigherIndex - 1
let (_xs, _ys) = (T.xs(t), T.ys(t))
let needsInterpolation = _xs[lowerOrEqualIndex] != x
if needsInterpolation {
interpolate(
_xs[lowerOrEqualIndex],
_xs[firstHigherIndex],
_ys[lowerOrEqualIndex],
_ys[firstHigherIndex],
x,
)
} else {
_ys[lowerOrEqualIndex]
}
}
n
}
/* Returns a between-points-interpolating function that can be used with PointwiseCombination.combine.
Interpolation can either be stepwise (using the value on the left) or linear. Extrapolation can be `UseZero or `UseOutermostPoints. */
let continuousInterpolator = (
interpolation: PointSetTypes.interpolationStrategy,
extrapolation: PointSetTypes.extrapolationStrategy,
): interpolator =>
switch (interpolation, extrapolation) {
| (#Linear, #UseZero) =>
(t: T.t, leftIndex: int, x: float) =>
if leftIndex < 0 {
0.0
} else if leftIndex >= T.length(t) - 1 {
0.0
} else {
let x1 = t.xs[leftIndex]
let x2 = t.xs[leftIndex + 1]
let y1 = t.ys[leftIndex]
let y2 = t.ys[leftIndex + 1]
let fraction = (x -. x1) /. (x2 -. x1)
y1 *. (1. -. fraction) +. y2 *. fraction
}
| (#Linear, #UseOutermostPoints) =>
(t: T.t, leftIndex: int, x: float) =>
if leftIndex < 0 {
t.ys[0]
} else if leftIndex >= T.length(t) - 1 {
t.ys[T.length(t) - 1]
} else {
let x1 = t.xs[leftIndex]
let x2 = t.xs[leftIndex + 1]
let y1 = t.ys[leftIndex]
let y2 = t.ys[leftIndex + 1]
let fraction = (x -. x1) /. (x2 -. x1)
y1 *. (1. -. fraction) +. y2 *. fraction
}
| (#Stepwise, #UseZero) =>
(t: T.t, leftIndex: int, _x: float) =>
if leftIndex < 0 {
0.0
} else if leftIndex >= T.length(t) - 1 {
0.0
} else {
t.ys[leftIndex]
}
| (#Stepwise, #UseOutermostPoints) =>
(t: T.t, leftIndex: int, _x: float) =>
if leftIndex < 0 {
t.ys[0]
} else if leftIndex >= T.length(t) - 1 {
t.ys[T.length(t) - 1]
} else {
t.ys[leftIndex]
}
}
/* Returns a between-points-interpolating function that can be used with PointwiseCombination.combine.
For discrete distributions, the probability density between points is zero, so we just return zero here. */
let discreteInterpolator: interpolator = (t: T.t, leftIndex: int, x: float) => 0.0
}
module XsConversion = {
let _replaceWithXs = (newXs: array<float>, t: T.t): T.t => {
let newYs = Belt.Array.map(newXs, XtoY.linear(_, t))
{xs: newXs, ys: newYs}
}
let equallyDivideXByMass = (newLength: int, integral: T.t) =>
E.A.Floats.range(0.0, 1.0, newLength) |> E.A.fmap(YtoX.linear(_, integral))
let proportionEquallyOverX = (newLength: int, t: T.t): T.t =>
T.equallyDividedXs(t, newLength) |> _replaceWithXs(_, t)
let proportionByProbabilityMass = (newLength: int, integral: T.t, t: T.t): T.t =>
integral |> equallyDivideXByMass(newLength) |> _replaceWithXs(_, t) // creates a new set of xs at evenly spaced percentiles // linearly interpolates new ys for the new xs
}
module Zipped = {
type zipped = array<(float, float)>
let compareYs = ((_, y1), (_, y2)) => y1 > y2 ? 1 : 0
let compareXs = ((x1, _), (x2, _)) => x1 > x2 ? 1 : 0
let sortByY = (t: zipped) => t |> E.A.stableSortBy(_, compareYs)
let sortByX = (t: zipped) => t |> E.A.stableSortBy(_, compareXs)
let filterByX = (testFn: float => bool, t: zipped) => t |> E.A.filter(((x, _)) => testFn(x))
}
module PointwiseCombination = {
// t1Interpolator and t2Interpolator are functions from XYShape.XtoY, e.g. linearBetweenPointsExtrapolateFlat.
let combine = %raw(` // : (float => float => float, T.t, T.t, bool) => T.t
// This function combines two xyShapes by looping through both of them simultaneously.
// It always moves on to the next smallest x, whether that's in the first or second input's xs,
// and interpolates the value on the other side, thus accumulating xs and ys.
// This is written in raw JS because this can still be a bottleneck, and using refs for the i and j indices is quite painful.
function(fn, interpolator, t1, t2) {
let t1n = t1.xs.length;
let t2n = t2.xs.length;
let outX = [];
let outY = [];
let i = -1;
let j = -1;
while (i <= t1n - 1 && j <= t2n - 1) {
let x, ya, yb;
if (j == t2n - 1 && i < t1n - 1 ||
t1.xs[i+1] < t2.xs[j+1]) { // if a has to catch up to b, or if b is already done
i++;
x = t1.xs[i];
ya = t1.ys[i];
yb = interpolator(t2, j, x);
} else if (i == t1n - 1 && j < t2n - 1 ||
t1.xs[i+1] > t2.xs[j+1]) { // if b has to catch up to a, or if a is already done
j++;
x = t2.xs[j];
yb = t2.ys[j];
ya = interpolator(t1, i, x);
} else if (i < t1n - 1 && j < t2n && t1.xs[i+1] === t2.xs[j+1]) { // if they happen to be equal, move both ahead
i++;
j++;
x = t1.xs[i];
ya = t1.ys[i];
yb = t2.ys[j];
} else if (i === t1n - 1 && j === t2n - 1) {
// finished!
i = t1n;
j = t2n;
continue;
} else {
console.log("Error!", i, j);
}
outX.push(x);
outY.push(fn(ya, yb));
}
return {xs: outX, ys: outY};
}
`)
let combineEvenXs = (~fn, ~xToYSelection, sampleCount, t1: T.t, t2: T.t) =>
switch (E.A.length(t1.xs), E.A.length(t2.xs)) {
| (0, 0) => T.empty
| (0, _) => t2
| (_, 0) => t1
| (_, _) =>
let allXs = Ts.equallyDividedXs([t1, t2], sampleCount)
let allYs = allXs |> E.A.fmap(x => fn(xToYSelection(x, t1), xToYSelection(x, t2)))
T.fromArrays(allXs, allYs)
}
// TODO: I'd bet this is pretty slow. Maybe it would be faster to intersperse Xs and Ys separately.
let intersperse = (t1: T.t, t2: T.t) => E.A.intersperse(T.zip(t1), T.zip(t2)) |> T.fromZippedArray
}
// I'm really not sure this part is actually what we want at this point.
module Range = {
// ((lastX, lastY), (nextX, nextY))
type zippedRange = ((float, float), (float, float))
let toT = T.fromZippedArray
let nextX = ((_, (nextX, _)): zippedRange) => nextX
let rangePointAssumingSteps = (((_, lastY), (nextX, _)): zippedRange) => (nextX, lastY)
let rangeAreaAssumingTriangles = (((lastX, lastY), (nextX, nextY)): zippedRange) =>
(nextX -. lastX) *. (lastY +. nextY) /. 2.
//Todo: figure out how to without making new array.
let rangeAreaAssumingTrapezoids = (((lastX, lastY), (nextX, nextY)): zippedRange) =>
(nextX -. lastX) *. (Js.Math.min_float(lastY, nextY) +. (lastY +. nextY) /. 2.)
let delta_y_over_delta_x = (((lastX, lastY), (nextX, nextY)): zippedRange) =>
(nextY -. lastY) /. (nextX -. lastX)
let mapYsBasedOnRanges = (fn, t) =>
Belt.Array.zip(t.xs, t.ys)
|> E.A.toRanges
|> E.R.toOption
|> E.O.fmap(r => r |> Belt.Array.map(_, r => (nextX(r), fn(r))))
// This code is messy, in part because I'm trying to make things easy on garbage collection here.
// It's using triangles instead of trapezoids right now.
let integrateWithTriangles = ({xs, ys}) => {
let length = E.A.length(xs)
let cumulativeY = Belt.Array.make(length, 0.0)
for x in 0 to E.A.length(xs) - 2 {
let _ = Belt.Array.set(
cumulativeY,
x + 1,
(xs[x + 1] -. xs[x]) *. ((ys[x] +. ys[x + 1]) /. 2.) +. cumulativeY[x], // dx // (1/2) * (avgY)
)
}
Some({xs: xs, ys: cumulativeY})
}
let derivative = mapYsBasedOnRanges(delta_y_over_delta_x)
let stepwiseToLinear = ({xs, ys}: T.t): T.t => {
// adds points at the bottom of each step.
let length = E.A.length(xs)
let newXs: array<float> = Belt.Array.makeUninitializedUnsafe(2 * length)
let newYs: array<float> = Belt.Array.makeUninitializedUnsafe(2 * length)
Belt.Array.set(newXs, 0, xs[0] -. epsilon_float) |> ignore
Belt.Array.set(newYs, 0, 0.) |> ignore
Belt.Array.set(newXs, 1, xs[0]) |> ignore
Belt.Array.set(newYs, 1, ys[0]) |> ignore
for i in 1 to E.A.length(xs) - 1 {
Belt.Array.set(newXs, i * 2, xs[i] -. epsilon_float) |> ignore
Belt.Array.set(newYs, i * 2, ys[i - 1]) |> ignore
Belt.Array.set(newXs, i * 2 + 1, xs[i]) |> ignore
Belt.Array.set(newYs, i * 2 + 1, ys[i]) |> ignore
()
}
{xs: newXs, ys: newYs}
}
// TODO: I think this isn't needed by any functions anymore.
let stepsToContinuous = t => {
// TODO: It would be nicer if this the diff didn't change the first element, and also maybe if there were a more elegant way of doing this.
let diff = T.xTotalRange(t) |> (r => r *. 0.00001)
let items = switch E.A.toRanges(Belt.Array.zip(t.xs, t.ys)) {
| Ok(items) =>
Some(
items
|> Belt.Array.map(_, rangePointAssumingSteps)
|> T.fromZippedArray
|> PointwiseCombination.intersperse(t |> T.mapX(e => e +. diff)),
)
| _ => Some(t)
}
let first = items |> E.O.fmap(T.zip) |> E.O.bind(_, E.A.get(_, 0))
switch (items, first) {
| (Some(items), Some((0.0, _))) => Some(items)
| (Some(items), Some((firstX, _))) =>
let all = E.A.append([(firstX, 0.0)], items |> T.zip)
all |> T.fromZippedArray |> E.O.some
| _ => None
}
}
}
let pointLogScore = (prediction, answer) =>
switch answer {
| 0. => 0.0
| answer => answer *. Js.Math.log2(Js.Math.abs_float(prediction /. answer))
}
let logScorePoint = (sampleCount, t1, t2) =>
PointwiseCombination.combineEvenXs(
~fn=pointLogScore,
~xToYSelection=XtoY.linear,
sampleCount,
t1,
t2,
)
|> Range.integrateWithTriangles
|> E.O.fmap(T.accumulateYs(\"+."))
|> E.O.fmap(Pairs.last)
|> E.O.fmap(Pairs.y)
module Analysis = {
let integrateContinuousShape = (
~indefiniteIntegralStepwise=(p, h1) => h1 *. p,
~indefiniteIntegralLinear=(p, a, b) => a *. p +. b *. p ** 2.0 /. 2.0,
t: PointSetTypes.continuousShape,
): float => {
let xs = t.xyShape.xs
let ys = t.xyShape.ys
E.A.reducei(xs, 0.0, (acc, _x, i) => {
let areaUnderIntegral = // TODO Take this switch statement out of the loop body
switch (t.interpolation, i) {
| (_, 0) => 0.0
| (#Stepwise, _) =>
indefiniteIntegralStepwise(xs[i], ys[i - 1]) -.
indefiniteIntegralStepwise(xs[i - 1], ys[i - 1])
| (#Linear, _) =>
let x1 = xs[i - 1]
let x2 = xs[i]
if x1 == x2 {
0.0
} else {
let h1 = ys[i - 1]
let h2 = ys[i]
let b = (h1 -. h2) /. (x1 -. x2)
let a = h1 -. b *. x1
indefiniteIntegralLinear(x2, a, b) -. indefiniteIntegralLinear(x1, a, b)
}
}
acc +. areaUnderIntegral
})
}
let getMeanOfSquaresContinuousShape = (t: PointSetTypes.continuousShape) => {
let indefiniteIntegralLinear = (p, a, b) => a *. p ** 3.0 /. 3.0 +. b *. p ** 4.0 /. 4.0
let indefiniteIntegralStepwise = (p, h1) => h1 *. p ** 3.0 /. 3.0
integrateContinuousShape(~indefiniteIntegralStepwise, ~indefiniteIntegralLinear, t)
}
let getVarianceDangerously = (t: 't, mean: 't => float, getMeanOfSquares: 't => float): float => {
let meanSquared = mean(t) ** 2.0
let meanOfSquares = getMeanOfSquares(t)
meanOfSquares -. meanSquared
}
let squareXYShape = T.mapX(x => x ** 2.0)
}

View File

@ -0,0 +1,27 @@
//The math here was taken from https://github.com/jasondavies/science.js/blob/master/src/stats/bandwidth.js
let len = x => E.A.length(x) |> float_of_int
let iqr = x => Jstat.percentile(x, 0.75, true) -. Jstat.percentile(x, 0.25, true)
// Silverman, B. W. (1986) Density Estimation. London: Chapman and Hall.
let nrd0 = x => {
let hi = Js_math.sqrt(Jstat.variance(x))
let lo = Js_math.minMany_float([hi, iqr(x) /. 1.34])
let e = Js_math.abs_float(x[1])
let lo' = switch (lo, hi, e) {
| (lo, _, _) if !Js.Float.isNaN(lo) => lo
| (_, hi, _) if !Js.Float.isNaN(hi) => hi
| (_, _, e) if !Js.Float.isNaN(e) => e
| _ => 1.0
}
0.9 *. lo' *. Js.Math.pow_float(~base=len(x), ~exp=-0.2)
}
// Scott, D. W. (1992) Multivariate Density Estimation: Theory, Practice, and Visualization. Wiley.
let nrd = x => {
let h = iqr(x) /. 1.34
1.06 *.
Js.Math.min_float(Js.Math.sqrt(Jstat.variance(x)), h) *.
Js.Math.pow_float(~base=len(x), ~exp=-1.0 /. 5.0)
}

View File

@ -15,7 +15,6 @@ const samplesToContinuousPdf = (
return {xs: pdf.map(r => r.x), ys: pdf.map(r => r.y)};
};
module.exports = {
samplesToContinuousPdf,
};

View File

@ -0,0 +1,141 @@
module Internals = {
module Types = {
type samplingStats = {
sampleCount: int,
outputXYPoints: int,
bandwidthXSuggested: float,
bandwidthUnitSuggested: float,
bandwidthXImplemented: float,
bandwidthUnitImplemented: float,
}
type outputs = {
continuousParseParams: option<samplingStats>,
pointSetDist: option<PointSetTypes.pointSetDist>,
}
}
module JS = {
@deriving(abstract)
type distJs = {
xs: array<float>,
ys: array<float>,
}
let jsToDist = (d: distJs): PointSetTypes.xyShape => {
xs: xsGet(d),
ys: ysGet(d),
}
@module("./KdeLibrary.js")
external samplesToContinuousPdf: (array<float>, int, int) => distJs = "samplesToContinuousPdf"
}
module KDE = {
let normalSampling = (samples, outputXYPoints, kernelWidth) =>
samples |> JS.samplesToContinuousPdf(_, outputXYPoints, kernelWidth) |> JS.jsToDist
}
module T = {
type t = array<float>
let splitContinuousAndDiscrete = (sortedArray: t) => {
let continuous = []
let discrete = E.FloatFloatMap.empty()
Belt.Array.forEachWithIndex(sortedArray, (index, element) => {
let maxIndex = (sortedArray |> Array.length) - 1
let possiblySimilarElements = switch index {
| 0 => [index + 1]
| n if n == maxIndex => [index - 1]
| _ => [index - 1, index + 1]
} |> Belt.Array.map(_, r => sortedArray[r])
let hasSimilarElement = Belt.Array.some(possiblySimilarElements, r => r == element)
hasSimilarElement
? E.FloatFloatMap.increment(element, discrete)
: {
let _ = Js.Array.push(element, continuous)
}
()
})
(continuous, discrete)
}
let xWidthToUnitWidth = (samples, outputXYPoints, xWidth) => {
let xyPointRange = E.A.Sorted.range(samples) |> E.O.default(0.0)
let xyPointWidth = xyPointRange /. float_of_int(outputXYPoints)
xWidth /. xyPointWidth
}
let formatUnitWidth = w => Jstat.max([w, 1.0]) |> int_of_float
let suggestedUnitWidth = (samples, outputXYPoints) => {
let suggestedXWidth = Bandwidth.nrd0(samples)
xWidthToUnitWidth(samples, outputXYPoints, suggestedXWidth)
}
let kde = (~samples, ~outputXYPoints, width) =>
KDE.normalSampling(samples, outputXYPoints, width)
}
}
let toPointSetDist = (
~samples: Internals.T.t,
~samplingInputs: SamplingInputs.samplingInputs,
(),
) => {
Array.fast_sort(compare, samples)
let (continuousPart, discretePart) = E.A.Sorted.Floats.split(samples)
let length = samples |> E.A.length |> float_of_int
let discrete: PointSetTypes.discreteShape =
discretePart
|> E.FloatFloatMap.fmap(r => r /. length)
|> E.FloatFloatMap.toArray
|> XYShape.T.fromZippedArray
|> Discrete.make
let pdf =
continuousPart |> E.A.length > 5
? {
let _suggestedXWidth = Bandwidth.nrd0(continuousPart)
// todo: This does some recalculating from the last step.
let _suggestedUnitWidth = Internals.T.suggestedUnitWidth(
continuousPart,
samplingInputs.outputXYPoints,
)
let usedWidth = samplingInputs.kernelWidth |> E.O.default(_suggestedXWidth)
let usedUnitWidth = Internals.T.xWidthToUnitWidth(
samples,
samplingInputs.outputXYPoints,
usedWidth,
)
let samplingStats: Internals.Types.samplingStats = {
sampleCount: samplingInputs.sampleCount,
outputXYPoints: samplingInputs.outputXYPoints,
bandwidthXSuggested: _suggestedXWidth,
bandwidthUnitSuggested: _suggestedUnitWidth,
bandwidthXImplemented: usedWidth,
bandwidthUnitImplemented: usedUnitWidth,
}
continuousPart
|> Internals.T.kde(
~samples=_,
~outputXYPoints=samplingInputs.outputXYPoints,
Internals.T.formatUnitWidth(usedUnitWidth),
)
|> Continuous.make
|> (r => Some((r, samplingStats)))
}
: None
let pointSetDist = MixedShapeBuilder.buildSimple(
~continuous=pdf |> E.O.fmap(fst),
~discrete=Some(discrete),
)
let samplesParse: Internals.Types.outputs = {
continuousParseParams: pdf |> E.O.fmap(snd),
pointSetDist: pointSetDist,
}
samplesParse
}

View File

@ -0,0 +1,328 @@
open SymbolicDistTypes
module Normal = {
type t = normal
let make = (mean, stdev): symbolicDist => #Normal({mean: mean, stdev: stdev})
let pdf = (x, t: t) => Jstat.Normal.pdf(x, t.mean, t.stdev)
let cdf = (x, t: t) => Jstat.Normal.cdf(x, t.mean, t.stdev)
let from90PercentCI = (low, high) => {
let mean = E.A.Floats.mean([low, high])
let stdev = (high -. low) /. (2. *. 1.644854)
#Normal({mean: mean, stdev: stdev})
}
let inv = (p, t: t) => Jstat.Normal.inv(p, t.mean, t.stdev)
let sample = (t: t) => Jstat.Normal.sample(t.mean, t.stdev)
let mean = (t: t) => Ok(Jstat.Normal.mean(t.mean, t.stdev))
let toString = ({mean, stdev}: t) => j`Normal($mean,$stdev)`
let add = (n1: t, n2: t) => {
let mean = n1.mean +. n2.mean
let stdev = sqrt(n1.stdev ** 2. +. n2.stdev ** 2.)
#Normal({mean: mean, stdev: stdev})
}
let subtract = (n1: t, n2: t) => {
let mean = n1.mean -. n2.mean
let stdev = sqrt(n1.stdev ** 2. +. n2.stdev ** 2.)
#Normal({mean: mean, stdev: stdev})
}
// TODO: is this useful here at all? would need the integral as well ...
let pointwiseProduct = (n1: t, n2: t) => {
let mean =
(n1.mean *. n2.stdev ** 2. +. n2.mean *. n1.stdev ** 2.) /. (n1.stdev ** 2. +. n2.stdev ** 2.)
let stdev = 1. /. (1. /. n1.stdev ** 2. +. 1. /. n2.stdev ** 2.)
#Normal({mean: mean, stdev: stdev})
}
let operate = (operation: Operation.Algebraic.t, n1: t, n2: t) =>
switch operation {
| #Add => Some(add(n1, n2))
| #Subtract => Some(subtract(n1, n2))
| _ => None
}
}
module Exponential = {
type t = exponential
let make = (rate: float): symbolicDist =>
#Exponential({
rate: rate,
})
let pdf = (x, t: t) => Jstat.Exponential.pdf(x, t.rate)
let cdf = (x, t: t) => Jstat.Exponential.cdf(x, t.rate)
let inv = (p, t: t) => Jstat.Exponential.inv(p, t.rate)
let sample = (t: t) => Jstat.Exponential.sample(t.rate)
let mean = (t: t) => Ok(Jstat.Exponential.mean(t.rate))
let toString = ({rate}: t) => j`Exponential($rate)`
}
module Cauchy = {
type t = cauchy
let make = (local, scale): symbolicDist => #Cauchy({local: local, scale: scale})
let pdf = (x, t: t) => Jstat.Cauchy.pdf(x, t.local, t.scale)
let cdf = (x, t: t) => Jstat.Cauchy.cdf(x, t.local, t.scale)
let inv = (p, t: t) => Jstat.Cauchy.inv(p, t.local, t.scale)
let sample = (t: t) => Jstat.Cauchy.sample(t.local, t.scale)
let mean = (_: t) => Error("Cauchy distributions have no mean value.")
let toString = ({local, scale}: t) => j`Cauchy($local, $scale)`
}
module Triangular = {
type t = triangular
let make = (low, medium, high): result<symbolicDist, string> =>
low < medium && medium < high
? Ok(#Triangular({low: low, medium: medium, high: high}))
: Error("Triangular values must be increasing order")
let pdf = (x, t: t) => Jstat.Triangular.pdf(x, t.low, t.high, t.medium)
let cdf = (x, t: t) => Jstat.Triangular.cdf(x, t.low, t.high, t.medium)
let inv = (p, t: t) => Jstat.Triangular.inv(p, t.low, t.high, t.medium)
let sample = (t: t) => Jstat.Triangular.sample(t.low, t.high, t.medium)
let mean = (t: t) => Ok(Jstat.Triangular.mean(t.low, t.high, t.medium))
let toString = ({low, medium, high}: t) => j`Triangular($low, $medium, $high)`
}
module Beta = {
type t = beta
let make = (alpha, beta) => #Beta({alpha: alpha, beta: beta})
let pdf = (x, t: t) => Jstat.Beta.pdf(x, t.alpha, t.beta)
let cdf = (x, t: t) => Jstat.Beta.cdf(x, t.alpha, t.beta)
let inv = (p, t: t) => Jstat.Beta.inv(p, t.alpha, t.beta)
let sample = (t: t) => Jstat.Beta.sample(t.alpha, t.beta)
let mean = (t: t) => Ok(Jstat.Beta.mean(t.alpha, t.beta))
let toString = ({alpha, beta}: t) => j`Beta($alpha,$beta)`
}
module Lognormal = {
type t = lognormal
let make = (mu, sigma) => #Lognormal({mu: mu, sigma: sigma})
let pdf = (x, t: t) => Jstat.Lognormal.pdf(x, t.mu, t.sigma)
let cdf = (x, t: t) => Jstat.Lognormal.cdf(x, t.mu, t.sigma)
let inv = (p, t: t) => Jstat.Lognormal.inv(p, t.mu, t.sigma)
let mean = (t: t) => Ok(Jstat.Lognormal.mean(t.mu, t.sigma))
let sample = (t: t) => Jstat.Lognormal.sample(t.mu, t.sigma)
let toString = ({mu, sigma}: t) => j`Lognormal($mu,$sigma)`
let from90PercentCI = (low, high) => {
let logLow = Js.Math.log(low)
let logHigh = Js.Math.log(high)
let mu = E.A.Floats.mean([logLow, logHigh])
let sigma = (logHigh -. logLow) /. (2.0 *. 1.645)
#Lognormal({mu: mu, sigma: sigma})
}
let fromMeanAndStdev = (mean, stdev) => {
let variance = Js.Math.pow_float(~base=stdev, ~exp=2.0)
let meanSquared = Js.Math.pow_float(~base=mean, ~exp=2.0)
let mu = Js.Math.log(mean) -. 0.5 *. Js.Math.log(variance /. meanSquared +. 1.0)
let sigma = Js.Math.pow_float(~base=Js.Math.log(variance /. meanSquared +. 1.0), ~exp=0.5)
#Lognormal({mu: mu, sigma: sigma})
}
let multiply = (l1, l2) => {
let mu = l1.mu +. l2.mu
let sigma = l1.sigma +. l2.sigma
#Lognormal({mu: mu, sigma: sigma})
}
let divide = (l1, l2) => {
let mu = l1.mu -. l2.mu
let sigma = l1.sigma +. l2.sigma
#Lognormal({mu: mu, sigma: sigma})
}
let operate = (operation: Operation.Algebraic.t, n1: t, n2: t) =>
switch operation {
| #Multiply => Some(multiply(n1, n2))
| #Divide => Some(divide(n1, n2))
| _ => None
}
}
module Uniform = {
type t = uniform
let make = (low, high) => #Uniform({low: low, high: high})
let pdf = (x, t: t) => Jstat.Uniform.pdf(x, t.low, t.high)
let cdf = (x, t: t) => Jstat.Uniform.cdf(x, t.low, t.high)
let inv = (p, t: t) => Jstat.Uniform.inv(p, t.low, t.high)
let sample = (t: t) => Jstat.Uniform.sample(t.low, t.high)
let mean = (t: t) => Ok(Jstat.Uniform.mean(t.low, t.high))
let toString = ({low, high}: t) => j`Uniform($low,$high)`
let truncate = (low, high, t: t): t => {
let newLow = max(E.O.default(neg_infinity, low), t.low)
let newHigh = min(E.O.default(infinity, high), t.high)
{low: newLow, high: newHigh}
}
}
module Float = {
type t = float
let make = t => #Float(t)
let pdf = (x, t: t) => x == t ? 1.0 : 0.0
let cdf = (x, t: t) => x >= t ? 1.0 : 0.0
let inv = (p, t: t) => p < t ? 0.0 : 1.0
let mean = (t: t) => Ok(t)
let sample = (t: t) => t
let toString = Js.Float.toString
}
module T = {
let minCdfValue = 0.0001
let maxCdfValue = 0.9999
let pdf = (x, dist) =>
switch dist {
| #Normal(n) => Normal.pdf(x, n)
| #Triangular(n) => Triangular.pdf(x, n)
| #Exponential(n) => Exponential.pdf(x, n)
| #Cauchy(n) => Cauchy.pdf(x, n)
| #Lognormal(n) => Lognormal.pdf(x, n)
| #Uniform(n) => Uniform.pdf(x, n)
| #Beta(n) => Beta.pdf(x, n)
| #Float(n) => Float.pdf(x, n)
}
let cdf = (x, dist) =>
switch dist {
| #Normal(n) => Normal.cdf(x, n)
| #Triangular(n) => Triangular.cdf(x, n)
| #Exponential(n) => Exponential.cdf(x, n)
| #Cauchy(n) => Cauchy.cdf(x, n)
| #Lognormal(n) => Lognormal.cdf(x, n)
| #Uniform(n) => Uniform.cdf(x, n)
| #Beta(n) => Beta.cdf(x, n)
| #Float(n) => Float.cdf(x, n)
}
let inv = (x, dist) =>
switch dist {
| #Normal(n) => Normal.inv(x, n)
| #Triangular(n) => Triangular.inv(x, n)
| #Exponential(n) => Exponential.inv(x, n)
| #Cauchy(n) => Cauchy.inv(x, n)
| #Lognormal(n) => Lognormal.inv(x, n)
| #Uniform(n) => Uniform.inv(x, n)
| #Beta(n) => Beta.inv(x, n)
| #Float(n) => Float.inv(x, n)
}
let sample: symbolicDist => float = x =>
switch x {
| #Normal(n) => Normal.sample(n)
| #Triangular(n) => Triangular.sample(n)
| #Exponential(n) => Exponential.sample(n)
| #Cauchy(n) => Cauchy.sample(n)
| #Lognormal(n) => Lognormal.sample(n)
| #Uniform(n) => Uniform.sample(n)
| #Beta(n) => Beta.sample(n)
| #Float(n) => Float.sample(n)
}
let doN = (n, fn) => {
let items = Belt.Array.make(n, 0.0)
for x in 0 to n - 1 {
let _ = Belt.Array.set(items, x, fn())
}
items
}
let sampleN = (n, dist) => doN(n, () => sample(dist))
let toString: symbolicDist => string = x =>
switch x {
| #Triangular(n) => Triangular.toString(n)
| #Exponential(n) => Exponential.toString(n)
| #Cauchy(n) => Cauchy.toString(n)
| #Normal(n) => Normal.toString(n)
| #Lognormal(n) => Lognormal.toString(n)
| #Uniform(n) => Uniform.toString(n)
| #Beta(n) => Beta.toString(n)
| #Float(n) => Float.toString(n)
}
let min: symbolicDist => float = x =>
switch x {
| #Triangular({low}) => low
| #Exponential(n) => Exponential.inv(minCdfValue, n)
| #Cauchy(n) => Cauchy.inv(minCdfValue, n)
| #Normal(n) => Normal.inv(minCdfValue, n)
| #Lognormal(n) => Lognormal.inv(minCdfValue, n)
| #Uniform({low}) => low
| #Beta(n) => Beta.inv(minCdfValue, n)
| #Float(n) => n
}
let max: symbolicDist => float = x =>
switch x {
| #Triangular(n) => n.high
| #Exponential(n) => Exponential.inv(maxCdfValue, n)
| #Cauchy(n) => Cauchy.inv(maxCdfValue, n)
| #Normal(n) => Normal.inv(maxCdfValue, n)
| #Lognormal(n) => Lognormal.inv(maxCdfValue, n)
| #Beta(n) => Beta.inv(maxCdfValue, n)
| #Uniform({high}) => high
| #Float(n) => n
}
let mean: symbolicDist => result<float, string> = x =>
switch x {
| #Triangular(n) => Triangular.mean(n)
| #Exponential(n) => Exponential.mean(n)
| #Cauchy(n) => Cauchy.mean(n)
| #Normal(n) => Normal.mean(n)
| #Lognormal(n) => Lognormal.mean(n)
| #Beta(n) => Beta.mean(n)
| #Uniform(n) => Uniform.mean(n)
| #Float(n) => Float.mean(n)
}
let operate = (distToFloatOp: Operation.distToFloatOperation, s) =>
switch distToFloatOp {
| #Cdf(f) => Ok(cdf(f, s))
| #Pdf(f) => Ok(pdf(f, s))
| #Inv(f) => Ok(inv(f, s))
| #Sample => Ok(sample(s))
| #Mean => mean(s)
}
let interpolateXs = (~xSelection: [#Linear | #ByWeight]=#Linear, dist: symbolicDist, n) =>
switch (xSelection, dist) {
| (#Linear, _) => E.A.Floats.range(min(dist), max(dist), n)
| (#ByWeight, #Uniform(n)) =>
// In `ByWeight mode, uniform distributions get special treatment because we need two x's
// on either side for proper rendering (just left and right of the discontinuities).
let dx = 0.00001 *. (n.high -. n.low)
[n.low -. dx, n.low +. dx, n.high -. dx, n.high +. dx]
| (#ByWeight, _) =>
let ys = E.A.Floats.range(minCdfValue, maxCdfValue, n)
ys |> E.A.fmap(y => inv(y, dist))
}
/* Calling e.g. "Normal.operate" returns an optional that wraps a result.
If the optional is None, there is no valid analytic solution. If it Some, it
can still return an error if there is a serious problem,
like in the case of a divide by 0.
*/
let tryAnalyticalSimplification = (
d1: symbolicDist,
d2: symbolicDist,
op: Operation.algebraicOperation,
): analyticalSimplificationResult =>
switch (d1, d2) {
| (#Float(v1), #Float(v2)) =>
switch Operation.Algebraic.applyFn(op, v1, v2) {
| Ok(r) => #AnalyticalSolution(#Float(r))
| Error(n) => #Error(n)
}
| (#Normal(v1), #Normal(v2)) =>
Normal.operate(op, v1, v2) |> E.O.dimap(r => #AnalyticalSolution(r), () => #NoSolution)
| (#Lognormal(v1), #Lognormal(v2)) =>
Lognormal.operate(op, v1, v2) |> E.O.dimap(r => #AnalyticalSolution(r), () => #NoSolution)
| _ => #NoSolution
}
let toPointSetDist = (sampleCount, d: symbolicDist): PointSetTypes.pointSetDist =>
switch d {
| #Float(v) => Discrete(Discrete.make(~integralSumCache=Some(1.0), {xs: [v], ys: [1.0]}))
| _ =>
let xs = interpolateXs(~xSelection=#ByWeight, d, sampleCount)
let ys = xs |> E.A.fmap(x => pdf(x, d))
Continuous(Continuous.make(~integralSumCache=Some(1.0), {xs: xs, ys: ys}))
}
}

View File

@ -0,0 +1,49 @@
type normal = {
mean: float,
stdev: float,
}
type lognormal = {
mu: float,
sigma: float,
}
type uniform = {
low: float,
high: float,
}
type beta = {
alpha: float,
beta: float,
}
type exponential = {rate: float}
type cauchy = {
local: float,
scale: float,
}
type triangular = {
low: float,
medium: float,
high: float,
}
type symbolicDist = [
| #Normal(normal)
| #Beta(beta)
| #Lognormal(lognormal)
| #Uniform(uniform)
| #Exponential(exponential)
| #Cauchy(cauchy)
| #Triangular(triangular)
| #Float(float)
]
type analyticalSimplificationResult = [
| #AnalyticalSolution(symbolicDist)
| #Error(string)
| #NoSolution
]

View File

@ -0,0 +1,443 @@
open Rationale.Function.Infix
module FloatFloatMap = {
module Id = Belt.Id.MakeComparable({
type t = float
let cmp: (float, float) => int = Pervasives.compare
})
type t = Belt.MutableMap.t<Id.t, float, Id.identity>
let fromArray = (ar: array<(float, float)>) => Belt.MutableMap.fromArray(ar, ~id=module(Id))
let toArray = (t: t) => Belt.MutableMap.toArray(t)
let empty = () => Belt.MutableMap.make(~id=module(Id))
let increment = (el, t: t) =>
Belt.MutableMap.update(t, el, x =>
switch x {
| Some(n) => Some(n +. 1.0)
| None => Some(1.0)
}
)
let get = (el, t: t) => Belt.MutableMap.get(t, el)
let fmap = (fn, t: t) => Belt.MutableMap.map(t, fn)
}
module Int = {
let max = (i1: int, i2: int) => i1 > i2 ? i1 : i2
}
/* Utils */
module U = {
let isEqual = (a, b) => a == b
let toA = a => [a]
let id = e => e
}
module O = {
let dimap = (sFn, rFn, e) =>
switch e {
| Some(r) => sFn(r)
| None => rFn()
}
()
let fmap = Rationale.Option.fmap
let bind = Rationale.Option.bind
let default = Rationale.Option.default
let isSome = Rationale.Option.isSome
let isNone = Rationale.Option.isNone
let toExn = Rationale.Option.toExn
let some = Rationale.Option.some
let firstSome = Rationale.Option.firstSome
let toExt = Rationale.Option.toExn
let flatApply = (fn, b) => Rationale.Option.apply(fn, Some(b)) |> Rationale.Option.flatten
let toBool = opt =>
switch opt {
| Some(_) => true
| _ => false
}
let ffmap = (fn, r) =>
switch r {
| Some(sm) => fn(sm)
| _ => None
}
let toString = opt =>
switch opt {
| Some(s) => s
| _ => ""
}
let toResult = (error, e) =>
switch e {
| Some(r) => Belt.Result.Ok(r)
| None => Error(error)
}
let compare = (compare, f1: option<float>, f2: option<float>) =>
switch (f1, f2) {
| (Some(f1), Some(f2)) => Some(compare(f1, f2) ? f1 : f2)
| (Some(f1), None) => Some(f1)
| (None, Some(f2)) => Some(f2)
| (None, None) => None
}
let min = compare(\"<")
let max = compare(\">")
}
/* Functions */
module F = {
let apply = (a, e) => a |> e
let flatten2Callbacks = (fn1, fn2, fnlast) =>
fn1(response1 => fn2(response2 => fnlast(response1, response2)))
let flatten3Callbacks = (fn1, fn2, fn3, fnlast) =>
fn1(response1 => fn2(response2 => fn3(response3 => fnlast(response1, response2, response3))))
let flatten4Callbacks = (fn1, fn2, fn3, fn4, fnlast) =>
fn1(response1 =>
fn2(response2 =>
fn3(response3 => fn4(response4 => fnlast(response1, response2, response3, response4)))
)
)
}
module Bool = {
type t = bool
let toString = (t: t) => t ? "TRUE" : "FALSE"
let fromString = str => str == "TRUE" ? true : false
module O = {
let toBool = opt =>
switch opt {
| Some(true) => true
| _ => false
}
}
}
module Float = {
let with2DigitsPrecision = Js.Float.toPrecisionWithPrecision(_, ~digits=2)
let with3DigitsPrecision = Js.Float.toPrecisionWithPrecision(_, ~digits=3)
let toFixed = Js.Float.toFixed
let toString = Js.Float.toString
}
module I = {
let increment = n => n + 1
let decrement = n => n - 1
let toString = Js.Int.toString
}
/* R for Result */
module R = {
let result = Rationale.Result.result
let id = e => e |> result(U.id, U.id)
let fmap = Rationale.Result.fmap
let bind = Rationale.Result.bind
let toExn = Belt.Result.getExn
let default = (default, res: Belt.Result.t<'a, 'b>) =>
switch res {
| Ok(r) => r
| Error(_) => default
}
let merge = (a, b) =>
switch (a, b) {
| (Error(e), _) => Error(e)
| (_, Error(e)) => Error(e)
| (Ok(a), Ok(b)) => Ok((a, b))
}
let toOption = (e: Belt.Result.t<'a, 'b>) =>
switch e {
| Ok(r) => Some(r)
| Error(_) => None
}
let errorIfCondition = (errorCondition, errorMessage, r) =>
errorCondition(r) ? Error(errorMessage) : Ok(r)
}
let safe_fn_of_string = (fn, s: string): option<'a> =>
try Some(fn(s)) catch {
| _ => None
}
module S = {
let safe_float = float_of_string->safe_fn_of_string
let safe_int = int_of_string->safe_fn_of_string
let default = (defaultStr, str) => str == "" ? defaultStr : str
}
module J = {
let toString = \"||>"(Js.Json.decodeString, O.default(""))
let fromString = Js.Json.string
let fromNumber = Js.Json.number
module O = {
let fromString = (str: string) =>
switch str {
| "" => None
| _ => Some(Js.Json.string(str))
}
let toString = (str: option<'a>) =>
switch str {
| Some(str) => Some(str |> \"||>"(Js.Json.decodeString, O.default("")))
| _ => None
}
}
}
module JsDate = {
let fromString = Js.Date.fromString
let now = Js.Date.now
let make = Js.Date.make
let valueOf = Js.Date.valueOf
}
/* List */
module L = {
let fmap = List.map
let get = Belt.List.get
let toArray = Array.of_list
let fmapi = List.mapi
let concat = List.concat
let drop = Rationale.RList.drop
let remove = Rationale.RList.remove
let find = List.find
let filter = List.filter
let for_all = List.for_all
let exists = List.exists
let sort = List.sort
let length = List.length
let filter_opt = Rationale.RList.filter_opt
let uniqBy = Rationale.RList.uniqBy
let join = Rationale.RList.join
let head = Rationale.RList.head
let uniq = Rationale.RList.uniq
let flatten = List.flatten
let last = Rationale.RList.last
let append = List.append
let getBy = Belt.List.getBy
let dropLast = Rationale.RList.dropLast
let contains = Rationale.RList.contains
let without = Rationale.RList.without
let update = Rationale.RList.update
let iter = List.iter
let findIndex = Rationale.RList.findIndex
}
/* A for Array */
module A = {
let fmap = Array.map
let fmapi = Array.mapi
let to_list = Array.to_list
let of_list = Array.of_list
let length = Array.length
let append = Array.append
// let empty = [||];
let unsafe_get = Array.unsafe_get
let get = Belt.Array.get
let getBy = Belt.Array.getBy
let last = a => get(a, length(a) - 1)
let first = get(_, 0)
let hasBy = (r, fn) => Belt.Array.getBy(r, fn) |> O.isSome
let fold_left = Array.fold_left
let fold_right = Array.fold_right
let concatMany = Belt.Array.concatMany
let keepMap = Belt.Array.keepMap
let init = Array.init
let reduce = Belt.Array.reduce
let reducei = Belt.Array.reduceWithIndex
let isEmpty = r => length(r) < 1
let min = a => get(a, 0) |> O.fmap(first => Belt.Array.reduce(a, first, (i, j) => i < j ? i : j))
let max = a => get(a, 0) |> O.fmap(first => Belt.Array.reduce(a, first, (i, j) => i > j ? i : j))
let stableSortBy = Belt.SortArray.stableSortBy
let toRanges = (a: array<'a>) =>
switch a |> Belt.Array.length {
| 0
| 1 =>
Belt.Result.Error("Must be at least 2 elements")
| n =>
Belt.Array.makeBy(n - 1, r => r)
|> Belt.Array.map(_, index => (
Belt.Array.getUnsafe(a, index),
Belt.Array.getUnsafe(a, index + 1),
))
|> Rationale.Result.return
}
// This zips while taking the longest elements of each array.
let zipMaxLength = (array1, array2) => {
let maxLength = Int.max(length(array1), length(array2))
let result = maxLength |> Belt.Array.makeUninitializedUnsafe
for i in 0 to maxLength - 1 {
Belt.Array.set(result, i, (get(array1, i), get(array2, i))) |> ignore
}
result
}
let asList = (f: list<'a> => list<'a>, r: array<'a>) => r |> to_list |> f |> of_list
/* TODO: Is there a better way of doing this? */
let uniq = r => asList(L.uniq, r)
//intersperse([1,2,3], [10,11,12]) => [1,10,2,11,3,12]
let intersperse = (a: array<'a>, b: array<'a>) => {
let items: ref<array<'a>> = ref([])
Belt.Array.forEachWithIndex(a, (i, item) =>
switch Belt.Array.get(b, i) {
| Some(r) => items := append(items.contents, [item, r])
| None => items := append(items.contents, [item])
}
)
items.contents
}
// This is like map, but
//accumulate((a,b) => a + b, [1,2,3]) => [1, 3, 5]
let accumulate = (fn: ('a, 'a) => 'a, items: array<'a>) => {
let length = items |> length
let empty = Belt.Array.make(length, items |> unsafe_get(_, 0))
Belt.Array.forEachWithIndex(items, (index, element) => {
let item = switch index {
| 0 => element
| index => fn(element, unsafe_get(empty, index - 1))
}
let _ = Belt.Array.set(empty, index, item)
})
empty
}
// @todo: Is -1 still the indicator that this is false (as is true with
// @todo: js findIndex)? Wasn't sure.
let findIndex = (e, i) =>
Js.Array.findIndex(e, i) |> (
r =>
switch r {
| -1 => None
| r => Some(r)
}
)
let filter = (o, e) => Js.Array.filter(o, e)
module O = {
let concatSomes = (optionals: array<option<'a>>): array<'a> =>
optionals
|> Js.Array.filter(Rationale.Option.isSome)
|> Js.Array.map(Rationale.Option.toExn("Warning: This should not have happened"))
let defaultEmpty = (o: option<array<'a>>): array<'a> =>
switch o {
| Some(o) => o
| None => []
}
}
module R = {
let firstErrorOrOpen = (results: array<Belt.Result.t<'a, 'b>>): Belt.Result.t<
array<'a>,
'b,
> => {
let bringErrorUp = switch results |> Belt.Array.getBy(_, Belt.Result.isError) {
| Some(Belt.Result.Error(err)) => Belt.Result.Error(err)
| Some(Belt.Result.Ok(_)) => Belt.Result.Ok(results)
| None => Belt.Result.Ok(results)
}
let forceOpen = (r: array<Belt.Result.t<'a, 'b>>): array<'a> =>
r |> Belt.Array.map(_, r => Belt.Result.getExn(r))
bringErrorUp |> Belt.Result.map(_, forceOpen)
}
}
module Sorted = {
let min = first
let max = last
let range = (~min=min, ~max=max, a) =>
switch (min(a), max(a)) {
| (Some(min), Some(max)) => Some(max -. min)
| _ => None
}
let binarySearchFirstElementGreaterIndex = (ar: array<'a>, el: 'a) => {
let el = Belt.SortArray.binarySearchBy(ar, el, compare)
let el = el < 0 ? el * -1 - 1 : el
switch el {
| e if e >= length(ar) => #overMax
| e if e == 0 => #underMin
| e => #firstHigher(e)
}
}
let concat = (t1: array<'a>, t2: array<'a>) => {
let ts = Belt.Array.concat(t1, t2)
ts |> Array.fast_sort(compare)
ts
}
let concatMany = (t1: array<array<'a>>) => {
let ts = Belt.Array.concatMany(t1)
ts |> Array.fast_sort(compare)
ts
}
module Floats = {
let makeIncrementalUp = (a, b) =>
Array.make(b - a + 1, a) |> Array.mapi((i, c) => c + i) |> Belt.Array.map(_, float_of_int)
let makeIncrementalDown = (a, b) =>
Array.make(a - b + 1, a) |> Array.mapi((i, c) => c - i) |> Belt.Array.map(_, float_of_int)
let split = (sortedArray: array<float>) => {
let continuous = []
let discrete = FloatFloatMap.empty()
Belt.Array.forEachWithIndex(sortedArray, (index, element) => {
let maxIndex = (sortedArray |> Array.length) - 1
let possiblySimilarElements = switch index {
| 0 => [index + 1]
| n if n == maxIndex => [index - 1]
| _ => [index - 1, index + 1]
} |> Belt.Array.map(_, r => sortedArray[r])
let hasSimilarElement = Belt.Array.some(possiblySimilarElements, r => r == element)
hasSimilarElement
? FloatFloatMap.increment(element, discrete)
: {
let _ = Js.Array.push(element, continuous)
}
()
})
(continuous, discrete)
}
}
}
module Floats = {
let sum = Belt.Array.reduce(_, 0., (i, j) => i +. j)
let mean = a => sum(a) /. (Array.length(a) |> float_of_int)
let random = Js.Math.random_int
exception RangeError(string)
let range = (min: float, max: float, n: int): array<float> =>
switch n {
| 0 => []
| 1 => [min]
| 2 => [min, max]
| _ if min == max => Belt.Array.make(n, min)
| _ if n < 0 => raise(RangeError("n must be greater than 0"))
| _ if min > max => raise(RangeError("Min value is less then max value"))
| _ =>
let diff = (max -. min) /. Belt.Float.fromInt(n - 1)
Belt.Array.makeBy(n, i => min +. Belt.Float.fromInt(i) *. diff)
}
}
}
module JsArray = {
let concatSomes = (optionals: Js.Array.t<option<'a>>): Js.Array.t<'a> =>
optionals
|> Js.Array.filter(Rationale.Option.isSome)
|> Js.Array.map(Rationale.Option.toExn("Warning: This should not have happened"))
let filter = Js.Array.filter
}

View File

@ -0,0 +1,8 @@
type t<'a> = array<(string, 'a)>
let getByName = (t: t<'a>, name) => E.A.getBy(t, ((n, _)) => n == name) |> E.O.fmap(((_, r)) => r)
let getByNameResult = (t: t<'a>, name) =>
getByName(t, name) |> E.O.toResult(name ++ " expected and not found")
let getByNames = (hash: t<'a>, names: array<string>) =>
names |> E.A.fmap(name => (name, getByName(hash, name)))

View File

@ -0,0 +1,100 @@
// Todo: Another way of doing this is with [@bs.scope "normal"], which may be more elegant
module Normal = {
@module("jstat") @scope("normal") external pdf: (float, float, float) => float = "pdf"
@module("jstat") @scope("normal") external cdf: (float, float, float) => float = "cdf"
@module("jstat") @scope("normal") external inv: (float, float, float) => float = "inv"
@module("jstat") @scope("normal") external sample: (float, float) => float = "sample"
@module("jstat") @scope("normal") external mean: (float, float) => float = "mean"
}
module Lognormal = {
@module("jstat") @scope("lognormal") external pdf: (float, float, float) => float = "pdf"
@module("jstat") @scope("lognormal") external cdf: (float, float, float) => float = "cdf"
@module("jstat") @scope("lognormal") external inv: (float, float, float) => float = "inv"
@module("jstat") @scope("lognormal") external sample: (float, float) => float = "sample"
@module("jstat") @scope("lognormal") external mean: (float, float) => float = "mean"
}
module Uniform = {
@module("jstat") @scope("uniform") external pdf: (float, float, float) => float = "pdf"
@module("jstat") @scope("uniform") external cdf: (float, float, float) => float = "cdf"
@module("jstat") @scope("uniform") external inv: (float, float, float) => float = "inv"
@module("jstat") @scope("uniform") external sample: (float, float) => float = "sample"
@module("jstat") @scope("uniform") external mean: (float, float) => float = "mean"
}
type beta
module Beta = {
@module("jstat") @scope("uniform") external pdf: (float, float, float) => float = "pdf"
@module("jstat") @scope("uniform") external cdf: (float, float, float) => float = "cdf"
@module("jstat") @scope("uniform") external inv: (float, float, float) => float = "inv"
@module("jstat") @scope("uniform") external sample: (float, float) => float = "sample"
@module("jstat") @scope("uniform") external mean: (float, float) => float = "mean"
}
module Exponential = {
@module("jstat") @scope("uniform") external pdf: (float, float) => float = "pdf"
@module("jstat") @scope("uniform") external cdf: (float, float) => float = "cdf"
@module("jstat") @scope("uniform") external inv: (float, float) => float = "inv"
@module("jstat") @scope("uniform") external sample: (float) => float = "sample"
@module("jstat") @scope("uniform") external mean: (float) => float = "mean"
}
module Cauchy = {
@module("jstat") @scope("uniform") external pdf: (float, float, float) => float = "pdf"
@module("jstat") @scope("uniform") external cdf: (float, float, float) => float = "cdf"
@module("jstat") @scope("uniform") external inv: (float, float, float) => float = "inv"
@module("jstat") @scope("uniform") external sample: (float, float) => float = "sample"
@module("jstat") @scope("uniform") external mean: (float, float) => float = "mean"
}
module Triangular = {
@module("jstat") @scope("uniform") external pdf: (float, float, float, float) => float = "pdf"
@module("jstat") @scope("uniform") external cdf: (float, float, float, float) => float = "cdf"
@module("jstat") @scope("uniform") external inv: (float, float, float, float) => float = "inv"
@module("jstat") @scope("uniform") external sample: (float, float, float) => float = "sample"
@module("jstat") @scope("uniform") external mean: (float, float, float) => float = "mean"
}
module Pareto = {
@module("jstat") @scope("uniform") external pdf: (float, float, float) => float = "pdf"
@module("jstat") @scope("uniform") external cdf: (float, float, float) => float = "cdf"
@module("jstat") @scope("uniform") external inv: (float, float, float) => float = "inv"
}
module Poisson = {
@module("jstat") @scope("uniform") external pdf: (float, float) => float = "pdf"
@module("jstat") @scope("uniform") external cdf: (float, float) => float = "cdf"
@module("jstat") @scope("uniform") external sample: (float) => float = "sample"
@module("jstat") @scope("uniform") external mean: (float) => float = "mean"
}
module Weibull = {
@module("jstat") @scope("uniform") external pdf: (float, float, float) => float = "pdf"
@module("jstat") @scope("uniform") external cdf: (float, float,float ) => float = "cdf"
@module("jstat") @scope("uniform") external sample: (float,float) => float = "sample"
@module("jstat") @scope("uniform") external mean: (float,float) => float = "mean"
}
module Binomial = {
@module("jstat") @scope("uniform") external pdf: (float, float, float) => float = "pdf"
@module("jstat") @scope("uniform") external cdf: (float, float,float ) => float = "cdf"
}
@module("jstat") external sum: array<float> => float = "sum"
@module("jstat") external product: array<float> => float = "product"
@module("jstat") external min: array<float> => float = "min"
@module("jstat") external max: array<float> => float = "max"
@module("jstat") external mean: array<float> => float = "mean"
@module("jstat") external geomean: array<float> => float = "geomean"
@module("jstat") external mode: array<float> => float = "mode"
@module("jstat") external variance: array<float> => float = "variance"
@module("jstat") external deviation: array<float> => float = "deviation"
@module("jstat") external stdev: array<float> => float = "stdev"
@module("jstat")
external quartiles: array<float> => array<float> = "quartiles"
@module("jstat")
external quantiles: (array<float>, array<float>) => array<float> = "quantiles"
@module("jstat")
external percentile: (array<float>, float, bool) => float = "percentile"

View File

@ -0,0 +1,5 @@
@module("lodash") external min: array<'a> => 'a = "min"
@module("lodash") external max: array<'a> => 'a = "max"
@module("lodash") external uniq: array<'a> => array<'a> = "uniq"
@module("lodash")
external countBy: (array<'a>, 'a => 'b) => Js.Dict.t<int> = "countBy"

View File

@ -0,0 +1,9 @@
@module("./MathjsWrapper.js")
external parseMathExt: string => Js.Json.t = "parseMath"
let parseMath = (str: string): result<Js.Json.t, string> =>
switch parseMathExt(str) {
| exception Js.Exn.Error(err) => Error(Js.Exn.message(err) |> E.O.default("MathJS Parse Error"))
| exception _ => Error("MathJS Parse Error")
| j => Ok(j)
}

View File

@ -0,0 +1,112 @@
// This file has no dependencies. It's used outside of the interpreter, but the interpreter depends on it.
type algebraicOperation = [
| #Add
| #Multiply
| #Subtract
| #Divide
| #Exponentiate
]
type pointwiseOperation = [#Add | #Multiply | #Exponentiate]
type scaleOperation = [#Multiply | #Exponentiate | #Log]
type distToFloatOperation = [
| #Pdf(float)
| #Cdf(float)
| #Inv(float)
| #Mean
| #Sample
]
module Algebraic = {
type t = algebraicOperation
let toFn: (t, float, float) => float = x =>
switch x {
| #Add => \"+."
| #Subtract => \"-."
| #Multiply => \"*."
| #Exponentiate => \"**"
| #Divide => \"/."
}
let applyFn = (t, f1, f2) =>
switch (t, f1, f2) {
| (#Divide, _, 0.) => Error("Cannot divide $v1 by zero.")
| _ => Ok(toFn(t, f1, f2))
}
let toString = x =>
switch x {
| #Add => "+"
| #Subtract => "-"
| #Multiply => "*"
| #Exponentiate => "**"
| #Divide => "/"
}
let format = (a, b, c) => b ++ (" " ++ (toString(a) ++ (" " ++ c)))
}
module Pointwise = {
type t = pointwiseOperation
let toString = x =>
switch x {
| #Add => "+"
| #Exponentiate => "^"
| #Multiply => "*"
}
let format = (a, b, c) => b ++ (" " ++ (toString(a) ++ (" " ++ c)))
}
module DistToFloat = {
type t = distToFloatOperation
let format = (operation, value) =>
switch operation {
| #Cdf(f) => j`cdf(x=$f,$value)`
| #Pdf(f) => j`pdf(x=$f,$value)`
| #Inv(f) => j`inv(x=$f,$value)`
| #Sample => "sample($value)"
| #Mean => "mean($value)"
}
}
// Note that different logarithms don't really do anything.
module Scale = {
type t = scaleOperation
let toFn = x =>
switch x {
| #Multiply => \"*."
| #Exponentiate => \"**"
| #Log => (a, b) => log(a) /. log(b)
}
let format = (operation: t, value, scaleBy) =>
switch operation {
| #Multiply => j`verticalMultiply($value, $scaleBy) `
| #Exponentiate => j`verticalExponentiate($value, $scaleBy) `
| #Log => j`verticalLog($value, $scaleBy) `
}
let toIntegralSumCacheFn = x =>
switch x {
| #Multiply => (a, b) => Some(a *. b)
| #Exponentiate => (_, _) => None
| #Log => (_, _) => None
}
let toIntegralCacheFn = x =>
switch x {
| #Multiply => (_, _) => None // TODO: this could probably just be multiplied out (using Continuous.scaleBy)
| #Exponentiate => (_, _) => None
| #Log => (_, _) => None
}
}
module Truncate = {
let toString = (left: option<float>, right: option<float>, nodeToString) => {
let left = left |> E.O.dimap(Js.Float.toString, () => "-inf")
let right = right |> E.O.dimap(Js.Float.toString, () => "inf")
j`truncate($nodeToString, $left, $right)`
}
}

View File

@ -0,0 +1,21 @@
type samplingInputs = {
sampleCount: int,
outputXYPoints: int,
kernelWidth: option<float>,
pointSetDistLength: int,
}
module SamplingInputs = {
type t = {
sampleCount: option<int>,
outputXYPoints: option<int>,
kernelWidth: option<float>,
pointSetDistLength: option<int>,
}
let withDefaults = (t: t): samplingInputs => {
sampleCount: t.sampleCount |> E.O.default(10000),
outputXYPoints: t.outputXYPoints |> E.O.default(10000),
kernelWidth: t.kernelWidth,
pointSetDistLength: t.pointSetDistLength |> E.O.default(10000),
}
}

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"allowJs": true,
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true,
"outDir": "./dist",
"declarationDir": "./dist",
"declaration": true
},
"target": "ES6",
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.spec.ts"]
}

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More