Merge pull request #38 from QURIresearch/rescript-refactor
Rescript refactor
19
.github/workflows/lang-jest.yml
vendored
Normal 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
|
@ -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
|
||||
|
|
35
README.md
|
@ -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)
|
||||
|
|
Before Width: | Height: | Size: 347 KiB |
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
// );
|
||||
// });
|
||||
// });
|
|
@ -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"),
|
||||
)
|
||||
})
|
||||
});
|
|
@ -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)|]),
|
||||
);
|
||||
})
|
||||
});
|
|
@ -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);
|
||||
})
|
||||
});
|
|
@ -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|],
|
||||
}),
|
||||
)
|
||||
});
|
||||
});
|
86
package.json
|
@ -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
|
@ -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
|
35
packages/components/.storybook/main.js
Normal 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),
|
||||
},
|
||||
},
|
||||
}
|
9
packages/components/.storybook/preview.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
}
|
6
packages/components/README.md
Normal 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
79
packages/components/package.json
Normal 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"
|
||||
}
|
BIN
packages/components/public/favicon.ico
Normal file
After Width: | Height: | Size: 13 KiB |
19
packages/components/public/index.html
Normal 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>
|
BIN
packages/components/public/logo16.png
Normal file
After Width: | Height: | Size: 327 B |
BIN
packages/components/public/logo192.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
packages/components/public/logo32.png
Normal file
After Width: | Height: | Size: 697 B |
BIN
packages/components/public/logo42.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
packages/components/public/logo512.png
Normal file
After Width: | Height: | Size: 31 KiB |
25
packages/components/public/manifest.json
Normal 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"
|
||||
}
|
3
packages/components/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
676
packages/components/public/squiggle.svg
Normal file
After Width: | Height: | Size: 107 KiB |
5
packages/components/shell.nix
Normal file
|
@ -0,0 +1,5 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
pkgs.mkShell {
|
||||
name = "squiggle-components";
|
||||
buildInputs = with pkgs; [ nodePackages.yarn nodejs ];
|
||||
}
|
1
packages/components/src/SquiggleChart.js.map
Normal file
355
packages/components/src/SquiggleChart.tsx
Normal 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();
|
||||
}
|
1
packages/components/src/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { SquiggleChart } from './SquiggleChart';
|
122
packages/components/src/spec-distributions.json
Normal 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"}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
9
packages/components/src/stories/Introduction.stories.mdx
Normal 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.
|
|
@ -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"}
|
82
packages/components/src/stories/SquiggleChart.stories.mdx
Normal 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} />
|
18
packages/components/tsconfig.json
Normal 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
|
@ -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
|
19
packages/playground/README.md
Normal 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
|
||||
```
|
54
packages/playground/package.json
Normal 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"
|
||||
}
|
||||
}
|
6
packages/playground/postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
5
packages/playground/shell.nix
Normal file
|
@ -0,0 +1,5 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
pkgs.mkShell {
|
||||
name = "squiggle";
|
||||
buildInputs = with pkgs; [ yarn yarn2nix nodePackages.npm ];
|
||||
}
|
9
packages/playground/src/Index.tsx
Normal 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)
|
||||
}
|
34
packages/playground/src/components/CodeEditor.tsx
Normal 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,
|
||||
}}
|
||||
/>
|
171
packages/playground/src/components/DistBuilder.tsx
Normal 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
|
|
@ -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>
|
21397
packages/playground/src/styles/antd.css
Normal file
|
@ -1,5 +1,3 @@
|
|||
@tailwind base;
|
||||
|
||||
@tailwind components;
|
||||
|
||||
@tailwind utilities;
|
9
packages/playground/tailwind.config.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
content: [
|
||||
"./src/components/*.tsx"
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
19
packages/playground/tsconfig.json
Normal 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
18
packages/squiggle-lang/.gitignore
vendored
Normal 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
|
64
packages/squiggle-lang/README.md
Normal 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.
|
13
packages/squiggle-lang/__tests__/Bandwidth__Test.res
Normal 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)
|
||||
})
|
||||
})
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
});
|
||||
})
|
||||
)
|
15
packages/squiggle-lang/__tests__/JS__Test.ts
Normal 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} ]});
|
||||
})
|
||||
});
|
20
packages/squiggle-lang/__tests__/Lodash__test.res
Normal 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)]),
|
||||
)
|
||||
})
|
||||
)
|
47
packages/squiggle-lang/__tests__/Samples__test.res
Normal 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)
|
||||
})
|
||||
)
|
51
packages/squiggle-lang/__tests__/XYShape__Test.res
Normal 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],
|
||||
}),
|
||||
)
|
||||
)
|
||||
})
|
|
@ -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
393
packages/squiggle-lang/dist/report.html
vendored
Normal file
5
packages/squiggle-lang/jest.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
};
|
47
packages/squiggle-lang/package.json
Normal 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"
|
||||
}
|
5
packages/squiggle-lang/shell.nix
Normal file
|
@ -0,0 +1,5 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
pkgs.mkShell {
|
||||
name = "squiggle";
|
||||
buildInputs = with pkgs; [ yarn yarn2nix nodePackages.npm ];
|
||||
}
|
15
packages/squiggle-lang/src/js/index.ts
Normal 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)
|
||||
}
|
205
packages/squiggle-lang/src/rescript/ProgramEvaluator.res
Normal 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
|
||||
}
|
24
packages/squiggle-lang/src/rescript/interpreter/AST.res
Normal 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)
|
||||
}
|
257
packages/squiggle-lang/src/rescript/interpreter/ASTEvaluator.res
Normal 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))
|
||||
}
|
232
packages/squiggle-lang/src/rescript/interpreter/ASTTypes.res
Normal 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)))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
]
|
|
@ -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))
|
||||
}
|
||||
}
|
304
packages/squiggle-lang/src/rescript/parser/Parser.res
Normal 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)
|
|
@ -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,
|
||||
)
|
||||
}
|
264
packages/squiggle-lang/src/rescript/pointSetDist/Continuous.res
Normal 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)
|
||||
}
|
||||
}
|
216
packages/squiggle-lang/src/rescript/pointSetDist/Discrete.res
Normal 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)
|
||||
}
|
||||
})
|
|
@ -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);
|
||||
});
|
|
@ -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)
|
||||
}
|
||||
}
|
304
packages/squiggle-lang/src/rescript/pointSetDist/Mixed.res
Normal 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,
|
||||
)
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
440
packages/squiggle-lang/src/rescript/pointSetDist/XYShape.res
Normal 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)
|
||||
}
|
27
packages/squiggle-lang/src/rescript/sampleSet/Bandwidth.res
Normal 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)
|
||||
}
|
|
@ -15,7 +15,6 @@ const samplesToContinuousPdf = (
|
|||
return {xs: pdf.map(r => r.x), ys: pdf.map(r => r.y)};
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
samplesToContinuousPdf,
|
||||
};
|
141
packages/squiggle-lang/src/rescript/sampleSet/SampleSet.res
Normal 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
|
||||
}
|
|
@ -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}))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
]
|
443
packages/squiggle-lang/src/rescript/utility/E.res
Normal 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
|
||||
}
|
8
packages/squiggle-lang/src/rescript/utility/Hash.res
Normal 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)))
|
100
packages/squiggle-lang/src/rescript/utility/Jstat.res
Normal 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"
|
5
packages/squiggle-lang/src/rescript/utility/Lodash.res
Normal 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"
|
9
packages/squiggle-lang/src/rescript/utility/Mathjs.res
Normal 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)
|
||||
}
|
112
packages/squiggle-lang/src/rescript/utility/Operation.res
Normal 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)`
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
16
packages/squiggle-lang/tsconfig.json
Normal 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"]
|
||||
}
|