Merge pull request #70 from foretold-app/adding-victory

Adding a simple chart for functions
This commit is contained in:
Ozzie Gooen 2020-11-12 12:53:36 -08:00 committed by GitHub
commit 1421c6a9b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 2486 additions and 1866 deletions

View File

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

View File

@ -42,9 +42,7 @@
"bs-css",
"rationale",
"bs-moment",
"reschema",
"bs-webapi",
"bs-fetch"
"reschema"
],
"refmt": 3,
"ppx-flags": [

View File

@ -28,16 +28,15 @@
"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-fetch": "^0.5.2",
"bs-moment": "0.4.5",
"bs-reform": "9.7.1",
"bs-webapi": "^0.15.9",
"bsb-js": "1.1.7",
"d3": "5.15.0",
"gh-pages": "2.2.0",
@ -52,12 +51,17 @@
"pdfast": "^0.2.0",
"postcss-cli": "7.1.0",
"rationale": "0.2.0",
"react": "^16.8.0",
"react-dom": "^16.8.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"
"tailwindcss": "1.2.0",
"vega": "*",
"vega-embed": "6.6.0",
"vega-lite": "*"
},
"devDependencies": {
"@glennsl/bs-jest": "^0.5.1",

View File

@ -1 +1 @@
let entries = EntryTypes.[Continuous2.entry,ExpressionTreeExamples.entry];
let entries = EntryTypes.[ExpressionTreeExamples.entry];

View File

@ -5,84 +5,84 @@
// floor(3 to 4)
// uniform(0,1) > 0.3 ? lognormal(6.652, -0.41): 0
let timeDist ={
let ingredients = DistPlusRenderer.Inputs.Ingredients.make(
~guesstimatorString="(floor(10 to 15))",
~domain=RightLimited({xPoint: 50.0, excludingProbabilityMass: 0.3}),
~unit=
DistTypes.TimeDistribution({zero: MomentRe.momentNow(), unit: `years}),
());
let inputs = DistPlusRenderer.Inputs.make(~distPlusIngredients=ingredients,())
inputs |> DistPlusRenderer.run
}
// let timeDist ={
// let ingredients = DistPlusRenderer.Inputs.Ingredients.make(
// ~guesstimatorString="(floor(10 to 15))",
// ~domain=RightLimited({xPoint: 50.0, excludingProbabilityMass: 0.3}),
// ~unit=
// DistTypes.TimeDistribution({zero: MomentRe.momentNow(), unit: `years}),
// ());
// let inputs = DistPlusRenderer.Inputs.make(~distPlusIngredients=ingredients,())
// inputs |> DistPlusRenderer.run
// }
let setup = dist =>
DistPlusRenderer.Inputs.make(~distPlusIngredients=dist,())
|> DistPlusRenderer.run
|> E.R.fmap(distPlus => <DistPlusPlot distPlus />)
|> E.R.toOption
|> E.O.toExn("")
// let setup = dist =>
// DistPlusRenderer.Inputs.make(~distPlusIngredients=dist,())
// |> DistPlusRenderer.run
// |> E.R.fmap(distPlus => <DistPlusPlot distPlus />)
// |> E.R.toOption
// |> E.O.toExn("")
let simpleExample = (name, guesstimatorString) =>
<>
<h3 className="text-gray-600 text-lg font-bold">
{name |> ReasonReact.string}
</h3>
{setup(DistPlusRenderer.Inputs.Ingredients.make(~guesstimatorString, ()))}
</>;
// let simpleExample = (name, guesstimatorString) =>
// <>
// <h3 className="text-gray-600 text-lg font-bold">
// {name |> ReasonReact.string}
// </h3>
// {setup(DistPlusRenderer.Inputs.Ingredients.make(~guesstimatorString, ()))}
// </>;
let timeExample = (name, guesstimatorString) =>
<>
<h3 className="text-gray-600 text-lg font-bold">
{name |> ReasonReact.string}
</h3>
{setup(
DistPlusRenderer.Inputs.Ingredients.make(
~guesstimatorString,
~unit=TimeDistribution({zero: MomentRe.momentNow(), unit: `years}),
(),
),
)}
</>;
// let timeExample = (name, guesstimatorString) =>
// <>
// <h3 className="text-gray-600 text-lg font-bold">
// {name |> ReasonReact.string}
// </h3>
// {setup(
// DistPlusRenderer.Inputs.Ingredients.make(
// ~guesstimatorString,
// ~unit=TimeDistribution({zero: MomentRe.momentNow(), unit: `years}),
// (),
// ),
// )}
// </>;
let distributions = () =>
<div>
<div>
<h2 className="text-gray-800 text-xl font-bold">
{"Initial Section" |> ReasonReact.string}
</h2>
{simpleExample("Continuous", "5 to 20")}
{simpleExample("Continuous, wide range", "1 to 1000000")}
{simpleExample("Continuous, tiny values", "0.000000001 to 0.00000001")}
{simpleExample(
"Continuous large values",
"50000000000000 to 200000000000000000",
)}
{simpleExample("Discrete", "floor(10 to 20)")}
{simpleExample(
"Discrete and below 0, normal(10,30)",
"floor(normal(10,30))",
)}
{simpleExample("Discrete, wide range", "floor(10 to 200000)")}
{simpleExample("Mixed", "mm(5 to 20, floor(20 to 30), [.5,.5])")}
{simpleExample("Mixed, Early-Discrete Point", "mm(1, 5 to 20, [.5,.5])")}
{simpleExample(
"Mixed, Two-Discrete Points",
"mm(0,10, 5 to 20, [.5,.5,.5])",
)}
<h2 className="text-gray-800 text-xl font-bold">
{"Over Time" |> ReasonReact.string}
</h2>
{timeExample("Continuous", "5 to 20")}
{timeExample("Continuous Over Long Period", "500 to 200000")}
{timeExample("Continuous Over Short Period", "0.0001 to 0.001")}
{timeExample(
"Continuous Over Very Long Period",
"500 to 20000000000000",
)}
{timeExample("Discrete", "floor(5 to 20)")}
{timeExample("Mixed", "mm(5 to 20, floor(5 to 20), [.5,.5])")}
</div>
</div>;
// let distributions = () =>
// <div>
// <div>
// <h2 className="text-gray-800 text-xl font-bold">
// {"Initial Section" |> ReasonReact.string}
// </h2>
// {simpleExample("Continuous", "5 to 20")}
// {simpleExample("Continuous, wide range", "1 to 1000000")}
// {simpleExample("Continuous, tiny values", "0.000000001 to 0.00000001")}
// {simpleExample(
// "Continuous large values",
// "50000000000000 to 200000000000000000",
// )}
// {simpleExample("Discrete", "floor(10 to 20)")}
// {simpleExample(
// "Discrete and below 0, normal(10,30)",
// "floor(normal(10,30))",
// )}
// {simpleExample("Discrete, wide range", "floor(10 to 200000)")}
// {simpleExample("Mixed", "mm(5 to 20, floor(20 to 30), [.5,.5])")}
// {simpleExample("Mixed, Early-Discrete Point", "mm(1, 5 to 20, [.5,.5])")}
// {simpleExample(
// "Mixed, Two-Discrete Points",
// "mm(0,10, 5 to 20, [.5,.5,.5])",
// )}
// <h2 className="text-gray-800 text-xl font-bold">
// {"Over Time" |> ReasonReact.string}
// </h2>
// {timeExample("Continuous", "5 to 20")}
// {timeExample("Continuous Over Long Period", "500 to 200000")}
// {timeExample("Continuous Over Short Period", "0.0001 to 0.001")}
// {timeExample(
// "Continuous Over Very Long Period",
// "500 to 20000000000000",
// )}
// {timeExample("Discrete", "floor(5 to 20)")}
// {timeExample("Mixed", "mm(5 to 20, floor(5 to 20), [.5,.5])")}
// </div>
// </div>;
let entry = EntryTypes.(entry(~title="Mixed Distributions", ~render=distributions));
// let entry = EntryTypes.(entry(~title="Mixed Distributions", ~render=distributions));

View File

@ -1,18 +1,18 @@
let setup = dist =>
DistPlusRenderer.Inputs.make(~distPlusIngredients=dist, ())
|> DistPlusRenderer.run
|> E.R.fmap(distPlus => <DistPlusPlot distPlus />)
|> E.R.toOption
|> E.O.toExn("")
// let setup = dist =>
// DistPlusRenderer.Inputs.make(~distPlusIngredients=dist, ())
// |> DistPlusRenderer.run
// |> E.R.fmap(distPlus => <DistPlusPlot distPlus />)
// |> E.R.toOption
// |> E.O.toExn("")
let simpleExample = (guesstimatorString, ~problem="", ()) =>
<>
<p> {guesstimatorString |> ReasonReact.string} </p>
<p> {problem |> (e => "problem: " ++ e) |> ReasonReact.string} </p>
{setup(
DistPlusRenderer.Inputs.Ingredients.make(~guesstimatorString, ()),
)}
</>;
// let simpleExample = (guesstimatorString, ~problem="", ()) =>
// <>
// <p> {guesstimatorString |> ReasonReact.string} </p>
// <p> {problem |> (e => "problem: " ++ e) |> ReasonReact.string} </p>
// {setup(
// DistPlusRenderer.Inputs.Ingredients.make(~guesstimatorString, ()),
// )}
// </>;
let distributions = () =>
<div>
@ -20,51 +20,51 @@ let distributions = () =>
<h2 className="text-gray-800 text-xl font-bold">
{"Initial Section" |> ReasonReact.string}
</h2>
{simpleExample(
"normal(-1, 1) + normal(5, 2)",
~problem="Tails look too flat",
(),
)}
{simpleExample(
"mm(normal(4,2), normal(10,1))",
~problem="Tails look too flat",
(),
)}
{simpleExample(
"normal(-1, 1) * normal(5, 2)",
~problem="This looks really weird",
(),
)}
{simpleExample(
"normal(1,2) * normal(2,2) * normal(3,1)",
~problem="Seems like important parts are cut off",
(),
)}
{simpleExample(
"mm(uniform(0, 1) , normal(3,2))",
~problem="Uniform distribution seems to break multimodal",
(),
)}
{simpleExample(
"truncate(mm(1 to 10, 10 to 30), 10, 20)",
~problem="Truncate seems to have no effect",
(),
)}
{simpleExample(
"normal(5,2)*(10^3)",
~problem="Multiplied items should be evaluated.",
(),
)}
{simpleExample(
"normal(5,10*3)",
~problem="At least simple operations in the distributions should be evaluated.",
(),
)}
{simpleExample(
"normal(5,10)^3",
~problem="Exponentiation not yet supported",
(),
)}
// {simpleExample(
// "normal(-1, 1) + normal(5, 2)",
// ~problem="Tails look too flat",
// (),
// )}
// {simpleExample(
// "mm(normal(4,2), normal(10,1))",
// ~problem="Tails look too flat",
// (),
// )}
// {simpleExample(
// "normal(-1, 1) * normal(5, 2)",
// ~problem="This looks really weird",
// (),
// )}
// {simpleExample(
// "normal(1,2) * normal(2,2) * normal(3,1)",
// ~problem="Seems like important parts are cut off",
// (),
// )}
// {simpleExample(
// "mm(uniform(0, 1) , normal(3,2))",
// ~problem="Uniform distribution seems to break multimodal",
// (),
// )}
// {simpleExample(
// "truncate(mm(1 to 10, 10 to 30), 10, 20)",
// ~problem="Truncate seems to have no effect",
// (),
// )}
// {simpleExample(
// "normal(5,2)*(10^3)",
// ~problem="Multiplied items should be evaluated.",
// (),
// )}
// {simpleExample(
// "normal(5,10*3)",
// ~problem="At least simple operations in the distributions should be evaluated.",
// (),
// )}
// {simpleExample(
// "normal(5,10)^3",
// ~problem="Exponentiation not yet supported",
// (),
// )}
</div>
</div>;

View File

@ -1,7 +1,6 @@
type route =
| Model(string)
| DistBuilder
| Drawer
| Home
| NotFound;
@ -9,7 +8,6 @@ let routeToPath = route =>
switch (route) {
| Model(modelId) => "/m/" ++ modelId
| DistBuilder => "/dist-builder"
| Drawer => "/drawer"
| Home => "/"
| _ => "/"
};
@ -71,14 +69,13 @@ module Menu = {
<Item href={routeToPath(DistBuilder)} key="dist-builder">
{"Dist Builder" |> R.ste}
</Item>
<Item href={routeToPath(Drawer)} key="drawer">
{"Drawer" |> R.ste}
</Item>
</div>;
};
};
let fixedLength = r =>
<div className="w-full max-w-screen-xl mx-auto px-6"> r </div>;
[@react.component]
let make = () => {
let url = ReasonReactRouter.useUrl();
@ -87,23 +84,24 @@ let make = () => {
switch (url.path) {
| ["m", modelId] => Model(modelId)
| ["dist-builder"] => DistBuilder
| ["drawer"] => Drawer
| [] => Home
| _ => NotFound
};
<div className="w-full max-w-screen-xl mx-auto px-6">
<>
<Menu />
{switch (routing) {
| Model(id) =>
switch (Models.getById(id)) {
| Some(model) => <FormBuilder.ModelForm model key=id />
| None => <div> {"Page is not found" |> R.ste} </div>
}
(
switch (Models.getById(id)) {
| Some(model) => <FormBuilder.ModelForm model key=id />
| None => <div> {"Page is not found" |> R.ste} </div>
}
)
|> fixedLength
| DistBuilder => <DistBuilder />
| Drawer => <Drawer />
| Home => <Home />
| _ => <div> {"Page is not found" |> R.ste} </div>
| _ => fixedLength({"Page is not found" |> R.ste})
}}
</div>;
};
</>;
};

View File

@ -0,0 +1,42 @@
import React 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";
function onChange(newValue) {
console.log("change", newValue);
}
export class CodeEditor extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<AceEditor
value={this.props.value}
mode="golang"
height="400px"
width="100%"
keyboardHandler="vim"
theme="github"
showGutter={false}
highlightActiveLine={false}
showPrintMargin={false}
onChange={this.props.onChange}
name="UNIQUE_ID_OF_DIV"
editorProps={{
$blockScrolling: true,
}}
setOptions={{
enableBasicAutocompletion: false,
enableLiveAutocompletion: true,
enableSnippets: true,
}}
/>
);
}
}

View File

@ -0,0 +1,10 @@
[@bs.module "./CodeEditor.js"]
external codeEditor: ReasonReact.reactClass = "CodeEditor";
[@react.component]
let make = (~value="", ~onChange=(_:string) => (), ~children=ReasonReact.null) =>
ReasonReact.wrapJsForReason(~reactClass=codeEditor, ~props={
"value": value,
"onChange": onChange
}, children)
|> ReasonReact.element;

View File

@ -19,6 +19,9 @@ module FormConfig = [%lenses
outputXYPoints: string,
downsampleTo: string,
kernelWidth: string,
diagramStart: string,
diagramStop: string,
diagramCount: string,
}
];
@ -27,6 +30,9 @@ type options = {
outputXYPoints: int,
downsampleTo: option(int),
kernelWidth: option(float),
diagramStart: float,
diagramStop: float,
diagramCount: int,
};
module Form = ReForm.Make(FormConfig);
@ -36,18 +42,14 @@ let schema = Form.Validation.Schema([||]);
module FieldText = {
[@react.component]
let make = (~field, ~label) => {
<Form.Field
field
render={({handleChange, error, value, validate}) =>
<Antd.Form.Item label={label |> R.ste}>
<Antd.Input.TextArea
value
onChange={BsReform.Helpers.handleChange(handleChange)}
onBlur={_ => validate()}
/>
</Antd.Form.Item>
}
/>;
<>
<Form.Field
field
render={({handleChange, error, value, validate}) =>
<CodeEditor value onChange={r => handleChange(r)} />
}
/>
</>;
};
};
module FieldString = {
@ -132,7 +134,6 @@ module DemoDist = {
[@react.component]
let make = (~guesstimatorString, ~domain, ~unit, ~options) => {
<Antd.Card title={"Distribution" |> R.ste}>
<div className=Styles.spacer />
<div>
{switch (domain, unit, options) {
| (Some(domain), Some(unit), Some(options)) =>
@ -149,22 +150,57 @@ module DemoDist = {
sampleCount: Some(options.sampleCount),
outputXYPoints: Some(options.outputXYPoints),
kernelWidth: options.kernelWidth,
shapeLength: Some(options.downsampleTo |> E.O.default(1000))
shapeLength:
Some(options.downsampleTo |> E.O.default(1000)),
},
~distPlusIngredients,
~environment=
[|("p", `SymbolicDist(`Float(1.0)))|]
[|
("K", `SymbolicDist(`Float(1000.0))),
("M", `SymbolicDist(`Float(1000000.0))),
("B", `SymbolicDist(`Float(1000000000.0))),
("T", `SymbolicDist(`Float(1000000000000.0))),
|]
->Belt.Map.String.fromArray,
(),
);
let response1 = DistPlusRenderer.run(inputs1);
let response1 = DistPlusRenderer.run2(inputs1);
switch (response1) {
| (Ok(distPlus1)) =>
<>
<DistPlusPlot distPlus={DistPlus.T.normalize(distPlus1)} />
</>
| (Error(r)) => r |> R.ste
| Ok(`DistPlus(distPlus1)) =>
<DistPlusPlot distPlus={DistPlus.T.normalize(distPlus1)} />
| Ok(`Float(f)) =>
<ForetoldComponents.NumberShower number=f precision=3 />
| Ok(`Function((f, a), env)) =>
// Problem: When it gets the function, it doesn't save state about previous commands
let foo: DistPlusRenderer.Inputs.inputs = {
distPlusIngredients: inputs1.distPlusIngredients,
samplingInputs: inputs1.samplingInputs,
environment: env,
};
let results =
E.A.Floats.range(options.diagramStart, options.diagramStop, options.diagramCount)
|> E.A.fmap(r =>
DistPlusRenderer.runFunction(
foo,
(f, a),
[|`SymbolicDist(`Float(r))|],
)
|> E.R.bind(_, a =>
switch (a) {
| `DistPlus(d) => Ok((r, DistPlus.T.normalize(d)))
| n =>
Js.log2("Error here", n);
Error("wrong type");
}
)
)
|> E.A.R.firstErrorOrOpen;
switch (results) {
| Ok(dists) => <PercentilesChart dists />
| Error(r) => r |> R.ste
};
| Error(r) => r |> R.ste
};
| _ =>
"Nothing to show. Try to change the distribution description."
@ -175,9 +211,21 @@ module DemoDist = {
};
};
// guesstimatorString: "
// us_economy_2018 = (10.5 to 10.6)T
// growth_rate = 1.08 to 1.2
// us_economy(t) = us_economy_2018 * (growth_rate^t)
// us_population_2019 = 320M to 330M
// us_population_growth_rate = 1.01 to 1.02
// us_population(t) = us_population_2019 * (us_population_growth_rate^t)
// gdp_per_person(t) = us_economy(t)/us_population(t)
// gdp_per_person
// ",
[@react.component]
let make = () => {
let (reloader, setRealoader) = React.useState(() => 1);
let (reloader, setReloader) = React.useState(() => 1);
let reform =
Form.use(
~validationStrategy=OnDemand,
@ -185,7 +233,7 @@ let make = () => {
~onSubmit=({state}) => {None},
~initialState={
//guesstimatorString: "mm(normal(-10, 2), uniform(18, 25), lognormal({mean: 10, stdev: 8}), triangular(31,40,50))",
guesstimatorString: "mm(1, 2, 3, normal(2, 1))", // , triangular(30, 40, 60)
guesstimatorString: "mm(3)",
domainType: "Complete",
xPoint: "50.0",
xPoint2: "60.0",
@ -194,10 +242,13 @@ let make = () => {
unitType: "UnspecifiedDistribution",
zero: MomentRe.momentNow(),
unit: "days",
sampleCount: "30000",
sampleCount: "1000",
outputXYPoints: "1000",
downsampleTo: "",
kernelWidth: "",
diagramStart: "0",
diagramStop: "10",
diagramCount: "20",
},
(),
);
@ -226,6 +277,9 @@ let make = () => {
reform.state.values.outputXYPoints |> Js.Float.fromString;
let downsampleTo = reform.state.values.downsampleTo |> Js.Float.fromString;
let kernelWidth = reform.state.values.kernelWidth |> Js.Float.fromString;
let diagramStart = reform.state.values.diagramStart |> Js.Float.fromString;
let diagramStop = reform.state.values.diagramStop |> Js.Float.fromString;
let diagramCount = reform.state.values.diagramCount |> Js.Float.fromString;
let domain =
switch (domainType) {
@ -281,6 +335,9 @@ let make = () => {
int_of_float(downsampleTo) > 0
? Some(int_of_float(downsampleTo)) : None,
kernelWidth: kernelWidth == 0.0 ? None : Some(kernelWidth),
diagramStart: diagramStart,
diagramStop: diagramStop,
diagramCount: diagramCount |> int_of_float,
})
| _ => None
};
@ -303,214 +360,86 @@ let make = () => {
reform.state.values.outputXYPoints,
reform.state.values.downsampleTo,
reform.state.values.kernelWidth,
reform.state.values.diagramStart,
reform.state.values.diagramStop,
reform.state.values.diagramCount,
reloader |> string_of_int,
|],
);
let onRealod = _ => {
setRealoader(_ => reloader + 1);
let onReload = _ => {
setReloader(_ => reloader + 1);
};
<div className=Styles.parent>
<div className=Styles.spacer />
demoDist
<div className=Styles.spacer />
<Antd.Card
title={"Distribution Form" |> R.ste}
extra={
<Antd.Button
icon=Antd.IconName.reload
shape=`circle
onClick=onRealod
/>
}>
<Form.Provider value=reform>
<Antd.Form onSubmit>
<Row _type=`flex className=Styles.rows>
<Col span=24>
<FieldText
field=FormConfig.GuesstimatorString
label="Guesstimator String"
/>
</Col>
</Row>
<Row _type=`flex className=Styles.rows>
<Col span=4>
<Form.Field
field=FormConfig.DomainType
render={({handleChange, value}) =>
<Antd.Form.Item label={"Domain Type" |> R.ste}>
<Antd.Select value onChange={e => e |> handleChange}>
<Antd.Select.Option value="Complete">
{"Complete" |> R.ste}
</Antd.Select.Option>
<Antd.Select.Option value="LeftLimited">
{"Left Limited" |> R.ste}
</Antd.Select.Option>
<Antd.Select.Option value="RightLimited">
{"Right Limited" |> R.ste}
</Antd.Select.Option>
<Antd.Select.Option value="LeftAndRightLimited">
{"Left And Right Limited" |> R.ste}
</Antd.Select.Option>
</Antd.Select>
</Antd.Form.Item>
}
/>
</Col>
{<>
<Col span=4>
<FieldFloat
field=FormConfig.XPoint
label="Left X-point"
className=Styles.groupA
/>
</Col>
<Col span=4>
<FieldFloat
field=FormConfig.ExcludingProbabilityMass
label="Left Excluding Probability Mass"
className=Styles.groupA
/>
</Col>
</>
|> R.showIf(
E.L.contains(
reform.state.values.domainType,
["LeftLimited", "LeftAndRightLimited"],
),
)}
{<>
<Col span=4>
<FieldFloat
field=FormConfig.XPoint2
label="Right X-point"
className=Styles.groupB
/>
</Col>
<Col span=4>
<FieldFloat
field=FormConfig.ExcludingProbabilityMass2
label="Right Excluding Probability Mass"
className=Styles.groupB
/>
</Col>
</>
|> R.showIf(
E.L.contains(
reform.state.values.domainType,
["RightLimited", "LeftAndRightLimited"],
),
)}
</Row>
<Row _type=`flex className=Styles.rows>
<Col span=4>
<Form.Field
field=FormConfig.UnitType
render={({handleChange, value}) =>
<Antd.Form.Item label={"Unit Type" |> R.ste}>
<Antd.Select value onChange={e => e |> handleChange}>
<Antd.Select.Option value="UnspecifiedDistribution">
{"Unspecified Distribution" |> R.ste}
</Antd.Select.Option>
<Antd.Select.Option value="TimeDistribution">
{"Time Distribution" |> R.ste}
</Antd.Select.Option>
</Antd.Select>
</Antd.Form.Item>
}
/>
</Col>
{<>
<Col span=4>
<Form.Field
field=FormConfig.Zero
render={({handleChange, value}) =>
<Antd.Form.Item label={"Zero Point" |> R.ste}>
<Antd_DatePicker
value
onChange={e => {
e |> handleChange;
_ => ();
}}
/>
</Antd.Form.Item>
}
/>
</Col>
<Col span=4>
<Form.Field
field=FormConfig.Unit
render={({handleChange, value}) =>
<Antd.Form.Item label={"Unit" |> R.ste}>
<Antd.Select value onChange={e => e |> handleChange}>
<Antd.Select.Option value="days">
{"Days" |> R.ste}
</Antd.Select.Option>
<Antd.Select.Option value="hours">
{"Hours" |> R.ste}
</Antd.Select.Option>
<Antd.Select.Option value="milliseconds">
{"Milliseconds" |> R.ste}
</Antd.Select.Option>
<Antd.Select.Option value="minutes">
{"Minutes" |> R.ste}
</Antd.Select.Option>
<Antd.Select.Option value="months">
{"Months" |> R.ste}
</Antd.Select.Option>
<Antd.Select.Option value="quarters">
{"Quarters" |> R.ste}
</Antd.Select.Option>
<Antd.Select.Option value="seconds">
{"Seconds" |> R.ste}
</Antd.Select.Option>
<Antd.Select.Option value="weeks">
{"Weeks" |> R.ste}
</Antd.Select.Option>
<Antd.Select.Option value="years">
{"Years" |> R.ste}
</Antd.Select.Option>
</Antd.Select>
</Antd.Form.Item>
}
/>
</Col>
</>
|> R.showIf(
E.L.contains(
reform.state.values.unitType,
["TimeDistribution"],
),
)}
</Row>
<Row _type=`flex className=Styles.rows>
<Col span=4>
<FieldFloat field=FormConfig.SampleCount label="Sample Count" />
</Col>
<Col span=4>
<FieldFloat
field=FormConfig.OutputXYPoints
label="Output XY-points"
/>
</Col>
<Col span=4>
<FieldFloat
field=FormConfig.DownsampleTo
label="Downsample To"
/>
</Col>
<Col span=4>
<FieldFloat field=FormConfig.KernelWidth label="Kernel Width" />
</Col>
</Row>
<div className="grid grid-cols-2 gap-4">
<div>
<Antd.Card
title={"Distribution Form" |> R.ste}
extra={
<Antd.Button
_type=`primary icon=Antd.IconName.reload onClick=onRealod>
{"Update Distribution" |> R.ste}
</Antd.Button>
</Antd.Form>
</Form.Provider>
</Antd.Card>
<div className=Styles.spacer />
icon=Antd.IconName.reload
shape=`circle
onClick=onReload
/>
}>
<Form.Provider value=reform>
<Antd.Form onSubmit>
<Row _type=`flex className=Styles.rows>
<Col span=24>
<FieldText
field=FormConfig.GuesstimatorString
label="Program"
/>
</Col>
</Row>
<Row _type=`flex className=Styles.rows>
<Col span=12>
<FieldFloat
field=FormConfig.SampleCount
label="Sample Count"
/>
</Col>
<Col span=12>
<FieldFloat
field=FormConfig.OutputXYPoints
label="Output XY-points"
/>
</Col>
<Col span=12>
<FieldFloat
field=FormConfig.DownsampleTo
label="Downsample To"
/>
</Col>
<Col span=12>
<FieldFloat
field=FormConfig.KernelWidth
label="Kernel Width"
/>
</Col>
<Col span=12>
<FieldFloat
field=FormConfig.DiagramStart
label="Diagram Start"
/>
</Col>
<Col span=12>
<FieldFloat
field=FormConfig.DiagramStop
label="Diagram Stop"
/>
</Col>
<Col span=12>
<FieldFloat
field=FormConfig.DiagramCount
label="Diagram Count"
/>
</Col>
</Row>
</Antd.Form>
</Form.Provider>
</Antd.Card>
</div>
<div> demoDist </div>
</div>;
};

View File

@ -1,992 +0,0 @@
module Types = {
type rectangle = {
// Ref: https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
left: int,
top: int,
right: int,
bottom: int,
x: int,
y: int,
width: int,
height: int,
};
type webapi = Webapi.Canvas.Canvas2d.t;
type xyShape = DistTypes.xyShape; /* {
xs: array(float),
ys: array(float),
};*/
type continuousShape = DistTypes.continuousShape; /*{
xyShape,
interpolation: [ | `Stepwise | `Linear],
};*/
type canvasPoint = {
w: float,
h: float,
};
type canvasShape = {
ws: array(float),
hs: array(float),
xValues: array(float),
};
type foretoldFormElements = {
measurableId: string,
token: string,
comment: string,
};
type distributionLimits = {
lower: float,
upper: float,
};
type canvasState = {
canvasShape: option(canvasShape),
lastMousePosition: option(canvasPoint),
isMouseDown: bool,
readyToRender: bool,
hasJustBeenSentToForetold: bool,
limitsHaveJustBeenUpdated: bool,
foretoldFormElements,
distributionLimits,
};
};
module CanvasContext = {
type t = Types.webapi;
/* Externals */
[@bs.send]
external getBoundingClientRect: Dom.element => Types.rectangle =
"getBoundingClientRect";
[@bs.send] external setLineDash: (t, array(int)) => unit = "setLineDash";
/* Webapi functions */
// Ref: https://github.com/reasonml-community/bs-webapi-incubator/blob/master/src/Webapi/Webapi__Canvas/Webapi__Canvas__Canvas2d.re
let getContext2d: Dom.element => t = Webapi.Canvas.CanvasElement.getContext2d;
module Canvas2d = Webapi.Canvas.Canvas2d;
let clearRect = Canvas2d.clearRect;
let setFillStyle = Canvas2d.setFillStyle;
let fillRect = Canvas2d.fillRect;
let beginPath = Canvas2d.beginPath;
let closePath = Canvas2d.closePath;
let setStrokeStyle = Canvas2d.setStrokeStyle;
let lineWidth = Canvas2d.lineWidth;
let moveTo = Canvas2d.moveTo;
let lineTo = Canvas2d.lineTo;
let stroke = Canvas2d.stroke;
let font = Canvas2d.font;
let textAlign = Canvas2d.textAlign;
let strokeText = Canvas2d.strokeText;
let fillText = Canvas2d.fillText;
/* Padding */
let paddingRatioX = 0.9;
let paddingRatioY = 0.9;
let paddingFactorX = (rectangleWidth: int) =>
(1. -. paddingRatioX) *. float_of_int(rectangleWidth) /. 2.0;
let paddingFactorY = (rectangleHeight: int) =>
(1. -. paddingRatioY) *. float_of_int(rectangleHeight) /. 2.0;
let translatePointToInside = (canvasElement: Dom.element) => {
let rectangle: Types.rectangle = getBoundingClientRect(canvasElement);
let translate = (p: Types.canvasPoint): Types.canvasPoint => {
let w = p.w -. float_of_int(rectangle.x);
let h = p.h -. float_of_int(rectangle.y);
{w, h};
};
translate;
};
};
module Convert = {
/*
- In this module, the fundamental unit for the canvas shape is the distance vector from the (0,0) point at the upper leftmost corner of the screen.
- For some drawing functions, this is instead from the (0,0) point at the upper leftmost corner of the canvas element. This is irrelevant in this module.
- The fundamental unit for a probability distribution is an x coordinate and its corresponding y probability density
*/
let xyShapeToCanvasShape =
(~xyShape: Types.xyShape, ~canvasElement: Dom.element) => {
let xs = xyShape.xs;
let ys = xyShape.ys;
let rectangle: Types.rectangle =
CanvasContext.getBoundingClientRect(canvasElement);
let lengthX = E.A.length(xs);
let minX = xs[0];
let maxX = xs[lengthX - 1];
let ratioXs =
float_of_int(rectangle.width)
*. CanvasContext.paddingRatioX
/. (maxX -. minX);
let ws =
E.A.fmap(
x =>
(x -. minX)
*. ratioXs
+. float_of_int(rectangle.left)
+. (1. -. CanvasContext.paddingRatioX)
*. float_of_int(rectangle.width)
/. 2.0,
xs,
);
let minY = 0.;
let maxY = E.A.reduce(ys, 0., (x, y) => x > y ? x : y);
let ratioYs =
float_of_int(rectangle.height)
*. CanvasContext.paddingRatioY
/. (maxY -. minY);
let hs =
E.A.fmap(
y =>
float_of_int(rectangle.bottom)
-. y
*. ratioYs
-. CanvasContext.paddingFactorY(rectangle.height),
ys,
);
let canvasShape: Types.canvasShape = {ws, hs, xValues: xs};
canvasShape;
};
let canvasShapeToContinuousShape =
(~canvasShape: Types.canvasShape, ~canvasElement: Dom.element)
: Types.continuousShape => {
let xs = canvasShape.xValues;
let hs = canvasShape.hs;
let rectangle: Types.rectangle =
CanvasContext.getBoundingClientRect(canvasElement);
let bottom = float_of_int(rectangle.bottom);
let paddingFactorY = CanvasContext.paddingFactorX(rectangle.height);
let windowScrollY: float = [%raw "window.scrollY"];
let y0Line = bottom +. windowScrollY -. paddingFactorY;
let ys = E.A.fmap(h => y0Line -. h, hs);
let xyShape: Types.xyShape = {xs, ys};
let continuousShape: Types.continuousShape = {
xyShape,
interpolation: `Linear,
integralSumCache: None,
integralCache: None,
};
let integral = XYShape.Analysis.integrateContinuousShape(continuousShape);
let ys = E.A.fmap(y => y /. integral, ys);
let continuousShape: Types.continuousShape = {
xyShape: {
xs,
ys,
},
interpolation: `Linear,
integralSumCache: Some(1.0),
integralCache: None,
};
continuousShape;
};
/* Misc helper functions */
let log2 = x => log(x) /. log(2.0);
let findClosestInOrderedArrayDangerously = (x: float, xs: array(float)) => {
let l = Array.length(xs);
let a = ref(0);
let b = ref(l - 1);
let numSteps = int_of_float(log2(float_of_int(l))) + 1;
for (_ in 0 to numSteps) {
let c = (a^ + b^) / 2;
xs[c] > x ? b := c : a := c;
};
b^;
};
let getPoint = (canvasShape: Types.canvasShape, n: int): Types.canvasPoint => {
let point: Types.canvasPoint = {
w: canvasShape.ws[n],
h: canvasShape.hs[n],
};
point;
};
};
module Draw = {
let line =
(
canvasElement: Dom.element,
~point0: Types.canvasPoint,
~point1: Types.canvasPoint,
)
: unit => {
let translator = CanvasContext.translatePointToInside(canvasElement);
let point0 = translator(point0);
let point1 = translator(point1);
let context = CanvasContext.getContext2d(canvasElement);
CanvasContext.beginPath(context);
CanvasContext.moveTo(context, ~x=point0.w, ~y=point0.h);
CanvasContext.lineTo(context, ~x=point1.w, ~y=point1.h);
CanvasContext.stroke(context);
};
let canvasPlot =
(canvasElement: Dom.element, canvasShape: Types.canvasShape) => {
let context = CanvasContext.getContext2d(canvasElement);
let rectangle: Types.rectangle =
CanvasContext.getBoundingClientRect(canvasElement);
/* Some useful reference points */
let paddingFactorX = CanvasContext.paddingFactorX(rectangle.width);
let paddingFactorY = CanvasContext.paddingFactorX(rectangle.height);
let p00: Types.canvasPoint = {
w: float_of_int(rectangle.left) +. paddingFactorX,
h: float_of_int(rectangle.bottom) -. paddingFactorY,
};
let p01: Types.canvasPoint = {
w: float_of_int(rectangle.left) +. paddingFactorX,
h: float_of_int(rectangle.top) +. paddingFactorY,
};
let p10: Types.canvasPoint = {
w: float_of_int(rectangle.right) -. paddingFactorX,
h: float_of_int(rectangle.bottom) -. paddingFactorY,
};
let p11: Types.canvasPoint = {
w: float_of_int(rectangle.right) -. paddingFactorX,
h: float_of_int(rectangle.top) +. paddingFactorY,
};
/* Clear the canvas with new white sheet */
CanvasContext.clearRect(
context,
~x=0.,
~y=0.,
~w=float_of_int(rectangle.width),
~h=float_of_int(rectangle.height),
);
/* Draw a line between every two adjacent points */
let length = Array.length(canvasShape.ws);
let windowScrollY: float = [%raw "window.scrollY"];
CanvasContext.setStrokeStyle(context, String, "#5680cc");
CanvasContext.lineWidth(context, 4.);
for (i in 1 to length - 1) {
let point0 = Convert.getPoint(canvasShape, i - 1);
let point1 = Convert.getPoint(canvasShape, i);
let point0 = {...point0, h: point0.h -. windowScrollY};
let point1 = {...point1, h: point1.h -. windowScrollY};
line(canvasElement, ~point0, ~point1);
};
/* Draws the expected value line */
// Removed on the grounds that it didn't play nice with changes in limits.
/*
let continuousShape =
Convert.canvasShapeToContinuousShape(~canvasShape, ~canvasElement);
let mean = Continuous.T.mean(continuousShape);
let variance = Continuous.T.variance(continuousShape);
let meanLocation =
Convert.findClosestInOrderedArrayDangerously(mean, canvasShape.xValues);
let meanLocationCanvasX = canvasShape.ws[meanLocation];
let meanLocationCanvasY = canvasShape.hs[meanLocation];
CanvasContext.beginPath(context);
CanvasContext.setStrokeStyle(context, String, "#5680cc");
CanvasContext.setLineDash(context, [|5, 10|]);
line(
canvasElement,
~point0={w: meanLocationCanvasX, h: p00.h},
~point1={
w: meanLocationCanvasX,
h: meanLocationCanvasY -. windowScrollY,
},
);
CanvasContext.stroke(context);
CanvasContext.setLineDash(context, [||]);
*/
/* draws lines parallel to x axis + factors to help w/ precise drawing. */
CanvasContext.beginPath(context);
CanvasContext.setStrokeStyle(context, String, "#CCC");
CanvasContext.lineWidth(context, 2.);
CanvasContext.font(context, "18px Roboto");
CanvasContext.textAlign(context, "center");
let numLines = 8;
let height =
float_of_int(rectangle.height)
*. CanvasContext.paddingRatioX
/. float_of_int(numLines);
for (i in 0 to numLines - 1) {
let pLeft = {...p00, h: p00.h -. height *. float_of_int(i)};
let pRight = {...p10, h: p10.h -. height *. float_of_int(i)};
line(canvasElement, ~point0=pLeft, ~point1=pRight);
CanvasContext.fillText(
string_of_int(i) ++ "x",
~x=pLeft.w -. float_of_int(rectangle.left) +. 15.0,
~y=pLeft.h -. float_of_int(rectangle.top) -. 5.0,
context,
);
};
/* Draw a frame around the drawable area */
CanvasContext.lineWidth(context, 2.0);
CanvasContext.setStrokeStyle(context, String, "#000");
line(canvasElement, ~point0=p00, ~point1=p01);
line(canvasElement, ~point0=p01, ~point1=p11);
line(canvasElement, ~point0=p11, ~point1=p10);
line(canvasElement, ~point0=p10, ~point1=p00);
/* draw units along the x axis */
CanvasContext.font(context, "16px Roboto");
CanvasContext.lineWidth(context, 2.0);
let numIntervals = 10;
let width = float_of_int(rectangle.width);
let height = float_of_int(rectangle.height);
let xMin = canvasShape.xValues[0];
let xMax = canvasShape.xValues[length - 1];
let xSpan = (xMax -. xMin) /. float_of_int(numIntervals - 1);
for (i in 0 to numIntervals - 1) {
let x =
float_of_int(rectangle.left)
+. width
*. float_of_int(i)
/. float_of_int(numIntervals);
let dashValue = xMin +. xSpan *. float_of_int(i);
CanvasContext.fillText(
Js.Float.toFixedWithPrecision(dashValue, ~digits=2),
~x,
~y=height,
context,
);
line(
canvasElement,
~point0={
w: x +. CanvasContext.paddingFactorX(rectangle.width),
h: p00.h,
},
~point1={
w: x +. CanvasContext.paddingFactorX(rectangle.width),
h: p00.h +. 10.0,
},
);
};
};
let initialDistribution = (canvasElement: Dom.element, setState) => {
let mean = 100.0;
let stdev = 15.0;
let numSamples = 3000;
let normal: SymbolicTypes.symbolicDist = `Normal({mean, stdev});
let normalShape =
ExpressionTree.toShape(
{sampleCount: 10000, outputXYPoints: 10000, kernelWidth: None, shapeLength:numSamples},
ExpressionTypes.ExpressionTree.Environment.empty,
`SymbolicDist(normal),
) |> E.R.toExn;
let xyShape: Types.xyShape =
switch (normalShape) {
| Mixed(_) => {xs: [||], ys: [||]}
| Discrete(_) => {xs: [||], ys: [||]}
| Continuous(m) => Continuous.getShape(m)
};
/* // To use a lognormal instead:
let lognormal = SymbolicTypes.Lognormal.fromMeanAndStdev(mean, stdev);
let lognormalShape =
SymbolicTypes.GenericSimple.toShape(lognormal, numSamples);
let lognormalXYShape: Types.xyShape =
switch (lognormalShape) {
| Mixed(_) => {xs: [||], ys: [||]}
| Discrete(_) => {xs: [||], ys: [||]}
| Continuous(m) => Continuous.getShape(m)
};
*/
let canvasShape = Convert.xyShapeToCanvasShape(~xyShape, ~canvasElement);
/* let continuousShapeBack =
Convert.canvasShapeToContinuousShape(~canvasShape, ~canvasElement);
*/
let windowScrollY: float = [%raw "window.scrollY"];
let canvasShape = {
...canvasShape,
hs: E.A.fmap(h => h +. windowScrollY, canvasShape.hs),
};
setState((state: Types.canvasState) => {
{...state, canvasShape: Some(canvasShape)}
});
canvasPlot(canvasElement, canvasShape);
};
};
module ForetoldAPI = {
let predict = (~measurableId, ~token, ~xs, ~ys, ~comment) => {
let payload = Js.Dict.empty();
let xsString = Js.Array.toString(xs);
let ysString = Js.Array.toString(ys);
let query = {j|mutation {
measurementCreate(input: {
value: {
floatCdf: {
xs: [$xsString]
ys: [$ysString]
}
}
valueText: "Drawn by hand."
description: "$comment"
competitorType: COMPETITIVE
measurableId: "$measurableId"
}) {
id
}
}|j};
Js.Dict.set(payload, "query", Js.Json.string(query));
Js.Promise.(
Fetch.fetchWithInit(
"https://api.foretold.io/graphql?token=" ++ token,
Fetch.RequestInit.make(
~method_=Post,
~body=
Fetch.BodyInit.make(
Js.Json.stringify(Js.Json.object_(payload)),
),
~headers=
Fetch.HeadersInit.make({
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/json",
"Accept": "application/json",
"Connection": "keep-alive",
"DNT": "1",
"Origin": "https://api.foretold.io",
}),
(),
),
)
|> then_(Fetch.Response.json)
);
};
};
module State = {
type t = Types.canvasState;
let initialState: t = {
canvasShape: None,
lastMousePosition: None,
isMouseDown: false,
readyToRender: false,
hasJustBeenSentToForetold: false,
limitsHaveJustBeenUpdated: false,
foretoldFormElements: {
measurableId: "",
token: "",
comment: "",
},
distributionLimits: {
lower: 0.0,
upper: 1000.0,
},
};
let updateMousePosition = (~point: Types.canvasPoint, ~setState) => {
setState((state: t) => {...state, lastMousePosition: Some(point)});
};
let onMouseMovement =
(
~event: ReactEvent.Mouse.t,
~potentialCanvas: option(Dom.element),
~state: t,
~setState,
) => {
/* Helper functions and objects*/
let x = ReactEvent.Mouse.clientX(event);
let y = ReactEvent.Mouse.clientY(event);
let windowScrollY: float = [%raw "window.scrollY"];
let point1: Types.canvasPoint = {
w: float_of_int(x),
h: float_of_int(y) +. windowScrollY,
};
let pointIsInBetween =
(a: Types.canvasPoint, b: Types.canvasPoint, c: Types.canvasPoint)
: bool => {
let x0 = a.w;
let x1 = b.w;
let x2 = c.w;
x0 < x2 && x2 < x1 || x1 < x2 && x2 < x0;
};
/* If all conditions are met, update the distribution */
let updateDistWithMouseMovement =
(
~point0: Types.canvasPoint,
~point1: Types.canvasPoint,
~canvasShape: Types.canvasShape,
) => {
/*
The mouse moves across the screen, and we get a series of (x,y) positions.
We know where the mouse last was
we update everything between the last (x,y) position and the new (x,y), using linear interpolation
Note that we only want to update & iterate over the parts of the canvas which are changed by the mouse movement
(otherwise, things might be too slow)
*/
let slope = (point1.h -. point0.h) /. (point1.w -. point0.w);
let pos0 =
Convert.findClosestInOrderedArrayDangerously(
point0.w,
canvasShape.ws,
);
let pos1 =
Convert.findClosestInOrderedArrayDangerously(
point1.w,
canvasShape.ws,
);
// Mouse is moving to the right
for (i in pos0 to pos1) {
let pointN = Convert.getPoint(canvasShape, i);
switch (pointIsInBetween(point0, point1, pointN)) {
| false => ()
| true =>
canvasShape.hs[i] = point0.h +. slope *. (pointN.w -. point0.w)
};
};
// Mouse is moving to the left
for (i in pos0 downto pos1) {
let pointN = Convert.getPoint(canvasShape, i);
switch (pointIsInBetween(point0, point1, pointN)) {
| false => ()
| true =>
canvasShape.hs[i] = point0.h +. slope *. (pointN.w -. point0.w)
};
};
canvasShape;
};
/* Check that the mouse movement was within the paddding box. */
let validateYCoordinates =
(~point: Types.canvasPoint, ~rectangle: Types.rectangle) => {
switch (
/*
- If we also validate the xs, this produces a jaded user experience around the edges.
- Instead, we will also update the first and last points in the updateDistWithMouseMovement, with the findClosestInOrderedArrayDangerously function, even when the x is outside the padding zone
- When we send the distribution to foretold, we'll get rid of the first and last points.
*/
/*
point.w >= float_of_int(rectangle.left)
+. CanvasContext.paddingFactorX(rectangle.width),
point.w <= float_of_int(rectangle.right)
-. CanvasContext.paddingFactorX(rectangle.width),
*/
point.h
-. windowScrollY >= float_of_int(rectangle.top)
+. CanvasContext.paddingFactorY(rectangle.height),
point.h
-. windowScrollY <= float_of_int(rectangle.bottom)
-. CanvasContext.paddingFactorY(rectangle.height),
) {
| (true, true) => true
| _ => false
};
};
let decideWithCanvas = (~canvasElement, ~canvasShape, ~point0) => {
let rectangle = CanvasContext.getBoundingClientRect(canvasElement);
switch (
validateYCoordinates(~point=point0, ~rectangle),
validateYCoordinates(~point=point1, ~rectangle),
) {
| (true, true) =>
let newCanvasShape =
updateDistWithMouseMovement(~point0, ~point1, ~canvasShape);
setState((state: t) => {
{
...state,
lastMousePosition: Some(point1),
canvasShape: Some(newCanvasShape),
readyToRender: false,
}
});
state.readyToRender
? Draw.canvasPlot(canvasElement, newCanvasShape) : ();
| (false, true) => updateMousePosition(~point=point1, ~setState)
| (_, false) => ()
};
};
switch (
potentialCanvas,
state.canvasShape,
state.isMouseDown,
state.lastMousePosition,
) {
| (Some(canvasElement), Some(canvasShape), true, Some(point0)) =>
decideWithCanvas(~canvasElement, ~canvasShape, ~point0)
| (Some(canvasElement), _, true, None) =>
let rectangle = CanvasContext.getBoundingClientRect(canvasElement);
validateYCoordinates(~point=point1, ~rectangle)
? updateMousePosition(~point=point1, ~setState) : ();
| _ => ()
};
};
let onMouseClick = (~setState, ~state) => {
setState((state: t) => {
{...state, isMouseDown: !state.isMouseDown, lastMousePosition: None}
});
};
let onSubmitForetoldForm =
(
~state: Types.canvasState,
~potentialCanvasElement: option(Dom.element),
~setState,
) => {
let potentialCanvasShape = state.canvasShape;
switch (potentialCanvasShape, potentialCanvasElement) {
| (None, _) => ()
| (_, None) => ()
| (Some(canvasShape), Some(canvasElement)) =>
let pdf =
Convert.canvasShapeToContinuousShape(~canvasShape, ~canvasElement);
/* create a cdf from a pdf */
let _pdf = Continuous.T.normalize(pdf);
let cdf = Continuous.T.integral(_pdf);
let xs = [||];
let ys = [||];
for (i in 1 to 999) {
/*
- see comment in validateYCoordinates as to why this starts at 1.
- foretold accepts distributions with up to 1000 points.
*/
let j = i * 3;
Js.Array.push(cdf.xyShape.xs[j], xs);
Js.Array.push(cdf.xyShape.ys[j], ys);
();
};
ForetoldAPI.predict(
~measurableId=state.foretoldFormElements.measurableId,
~token=state.foretoldFormElements.token,
~comment=state.foretoldFormElements.comment,
~xs,
~ys,
);
setState((state: t) => {...state, hasJustBeenSentToForetold: true});
Js.Global.setTimeout(
() => {
setState((state: t) =>
{...state, hasJustBeenSentToForetold: false}
)
},
5000,
);
();
};
();
};
let onSubmitLimitsForm =
(
~state: Types.canvasState,
~potentialCanvasElement: option(Dom.element),
~setState,
) => {
let potentialCanvasShape = state.canvasShape;
switch (potentialCanvasShape, potentialCanvasElement) {
| (None, _) => ()
| (_, None) => ()
| (Some(canvasShape), Some(canvasElement)) =>
let xValues = canvasShape.xValues;
let length = Array.length(xValues);
let xMin = xValues[0];
let xMax = xValues[length - 1];
let lower = state.distributionLimits.lower;
let upper = state.distributionLimits.upper;
let slope = (upper -. lower) /. (xMax -. xMin);
let delta = lower -. slope *. xMin;
let xValues = E.A.fmap(x => delta +. x *. slope, xValues);
let newCanvasShape = {...canvasShape, xValues};
setState((state: t) =>
{
...state,
canvasShape: Some(newCanvasShape),
limitsHaveJustBeenUpdated: true,
}
);
Draw.canvasPlot(canvasElement, newCanvasShape);
Js.Global.setTimeout(
() => {
setState((state: t) =>
{...state, limitsHaveJustBeenUpdated: false}
)
},
5000,
);
();
};
();
};
};
module Styles = {
open Css;
let dist = style([padding(em(1.))]);
let spacer = style([marginTop(em(1.))]);
};
[@react.component]
let make = () => {
let canvasRef: React.Ref.t(option(Dom.element)) = React.useRef(None); // should morally live inside the state, but this is tricky.
let (state, setState) = React.useState(() => State.initialState);
/* Draw the initial distribution */
React.useEffect0(() => {
let potentialCanvas = React.Ref.current(canvasRef);
(
switch (potentialCanvas) {
| Some(canvasElement) =>
Draw.initialDistribution(canvasElement, setState)
| None => ()
}
)
|> ignore;
None;
});
/* Render the current distribution every 30ms, while the mouse is moving and changing it */
React.useEffect0(() => {
let runningInterval =
Js.Global.setInterval(
() => {
setState((state: Types.canvasState) => {
{...state, readyToRender: true}
})
},
30,
);
Some(() => Js.Global.clearInterval(runningInterval));
});
<Antd.Card title={"Distribution Drawer" |> R.ste}>
<div className=Styles.spacer />
<p> {"Click to begin drawing, click to stop drawing" |> R.ste} </p>
<canvas
width="1000"
height="700"
ref={ReactDOMRe.Ref.callbackDomRef(elem =>
React.Ref.setCurrent(canvasRef, Js.Nullable.toOption(elem))
)}
onMouseMove={event =>
State.onMouseMovement(
~event,
~potentialCanvas=React.Ref.current(canvasRef),
~state,
~setState,
)
}
onClick={_e => State.onMouseClick(~setState, ~state)}
/>
<div className=Styles.spacer />
<div className=Styles.spacer />
<br />
<br />
<br />
<Antd.Card title={"Update upper and lower limits" |> R.ste}>
<form
id="update-limits"
onSubmit={(e: ReactEvent.Form.t): unit => {
ReactEvent.Form.preventDefault(e);
/* code to run on submit */
State.onSubmitLimitsForm(
~state,
~potentialCanvasElement=React.Ref.current(canvasRef),
~setState,
);
();
}}>
<div>
<label> {"Lower: " |> R.ste} </label>
<input
type_="number"
id="lowerlimit"
name="lowerlimit"
value={Js.Float.toString(state.distributionLimits.lower)}
placeholder="a number. f.ex., 0"
required=true
step=0.001
onChange={event => {
let value = ReactEvent.Form.target(event)##value;
setState((state: Types.canvasState) => {
{
...state,
distributionLimits: {
...state.distributionLimits,
lower: value,
},
}
});
}}
/>
</div>
<br />
<div>
<label> {"Upper: " |> R.ste} </label>
<input
type_="number"
id="upperlimit"
name="upperlimit"
value={Js.Float.toString(state.distributionLimits.upper)}
placeholder="a number. f.ex., 100"
required=true
step=0.001
onChange={event => {
let value = ReactEvent.Form.target(event)##value;
setState((state: Types.canvasState) => {
{
...state,
distributionLimits: {
...state.distributionLimits,
upper: value,
},
}
});
}}
/>
</div>
<br />
<button type_="submit" id="updatelimits">
{"Update limits" |> R.ste}
</button>
<br />
<p hidden={!state.limitsHaveJustBeenUpdated}>
{"Updated!" |> R.ste}
</p>
</form>
</Antd.Card>
<br />
<br />
<br />
<Antd.Card title={"Send to foretold" |> R.ste}>
<form
id="send-to-foretold"
onSubmit={(e: ReactEvent.Form.t): unit => {
ReactEvent.Form.preventDefault(e);
/* code to run on submit */
State.onSubmitForetoldForm(
~state,
~potentialCanvasElement=React.Ref.current(canvasRef),
~setState,
);
();
}}>
<div>
<label> {"MeasurableId: " |> R.ste} </label>
<input
type_="text"
id="measurableId"
name="measurableId"
value={state.foretoldFormElements.measurableId}
placeholder="The last bit in the url, after the m"
required=true
onChange={event => {
let value = ReactEvent.Form.target(event)##value;
setState((state: Types.canvasState) => {
{
...state,
foretoldFormElements: {
...state.foretoldFormElements,
measurableId: value,
},
}
});
}}
/>
</div>
<br />
<div>
<label> {"Foretold bot token: " |> R.ste} </label>
<input
type_="text"
id="foretoldToken"
name="foretoldToken"
value={state.foretoldFormElements.token}
placeholder="Profile -> Bots -> (New Bot) -> Token"
required=true
onChange={event => {
let value = ReactEvent.Form.target(event)##value;
setState((state: Types.canvasState) => {
{
...state,
foretoldFormElements: {
...state.foretoldFormElements,
token: value,
},
}
});
}}
/>
</div>
<br />
<textarea
id="comment"
name="comment"
rows=20
cols=70
placeholder="Explain a little bit what this distribution is about"
onChange={event => {
let value = ReactEvent.Form.target(event)##value;
setState((state: Types.canvasState) => {
{
...state,
foretoldFormElements: {
...state.foretoldFormElements,
comment: value,
},
}
});
}}
/>
<br />
<button type_="submit" id="sendToForetoldButton">
{"Send to foretold" |> R.ste}
</button>
<br />
<p hidden={!state.hasJustBeenSentToForetold}> {"Sent!" |> R.ste} </p>
</form>
</Antd.Card>
</Antd.Card>;
};

View File

@ -259,7 +259,6 @@ module DistPlusChart = {
module IntegralChart = {
[@react.component]
let make = (~distPlus: DistTypes.distPlus, ~config: chartConfig, ~onHover) => {
open DistPlus;
let integral = distPlus.integralCache;
let continuous =
integral

View File

@ -104,8 +104,9 @@ let reducer = (state: state, action: action) =>
let init = {
showStats: false,
showParams: false,
showPercentiles: true,
showPercentiles: false,
distributions: [
{yLog: false, xLog: false, isCumulative: false, height: 1},
{yLog: false, xLog: false, isCumulative: false, height: 4},
{yLog: false, xLog: false, isCumulative: true, height: 1},
],
};

View File

@ -0,0 +1,10 @@
import * as _ from "lodash";
import { createClassFromSpec } from "react-vega";
import spec from "./spec-percentiles";
const PercentilesChart = createClassFromSpec({
spec,
style: "width: 100%",
});
export { PercentilesChart };

View File

@ -0,0 +1,41 @@
[@bs.module "./PercentilesChart.js"]
external percentilesChart: ReasonReact.reactClass = "PercentilesChart";
module Internal = {
[@react.component]
let make = (~data, ~signalListeners, ~children=ReasonReact.null) =>
ReasonReact.wrapJsForReason(
~reactClass=percentilesChart,
~props={"data": data, "signalListeners": signalListeners},
children,
)
|> ReasonReact.element;
};
[@react.component]
let make =
(~dists: array((float, DistTypes.distPlus)), ~children=ReasonReact.null) => {
let data =
dists
|> E.A.fmap(((x, r)) => {
{
"x": x,
"p1": r |> DistPlus.T.Integral.yToX(0.01),
"p5": r |> DistPlus.T.Integral.yToX(0.05),
"p10": r |> DistPlus.T.Integral.yToX(0.1),
"p20": r |> DistPlus.T.Integral.yToX(0.2),
"p30": r |> DistPlus.T.Integral.yToX(0.3),
"p40": r |> DistPlus.T.Integral.yToX(0.4),
"p50": r |> DistPlus.T.Integral.yToX(0.5),
"p60": r |> DistPlus.T.Integral.yToX(0.6),
"p70": r |> DistPlus.T.Integral.yToX(0.7),
"p80": r |> DistPlus.T.Integral.yToX(0.8),
"p90": r |> DistPlus.T.Integral.yToX(0.9),
"p95": r |> DistPlus.T.Integral.yToX(0.95),
"p99": r |> DistPlus.T.Integral.yToX(0.99),
}
});
Js.log3("Data", dists, data);
let da = {"facet": data};
<Internal data=da signalListeners={}/>;
};

View File

@ -0,0 +1,208 @@
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"width": 500,
"height": 400,
"padding": 5,
"data": [
{
"name": "facet",
"values": [],
"format": { "type": "json", "parse": { "timestamp": "date" } }
},
{
"name": "table",
"source": "facet",
"transform": [
{
"type": "aggregate",
"groupby": ["x"],
"ops": [
"mean",
"mean",
"mean",
"mean",
"mean",
"mean",
"mean",
"mean",
"mean",
"mean",
"mean",
"mean",
"mean"
],
"fields": [
"p1",
"p5",
"p10",
"p20",
"p30",
"p40",
"p50",
"p60",
"p70",
"p80",
"p90",
"p95",
"p99"
],
"as": [
"p1",
"p5",
"p10",
"p20",
"p30",
"p40",
"p50",
"p60",
"p70",
"p80",
"p90",
"p95",
"p99"
]
}
]
}
],
"scales": [
{
"name": "xscale",
"type": "linear",
"nice": true,
"domain": { "data": "facet", "field": "x" },
"range": "width"
},
{
"name": "yscale",
"type": "linear",
"range": "height",
"nice": true,
"zero": true,
"domain": { "data": "facet", "field": "p99" }
}
],
"axes": [
{
"orient": "bottom",
"scale": "xscale",
"grid": false,
"tickSize": 2,
"encode": {
"grid": { "enter": { "stroke": { "value": "#ccc" } } },
"ticks": { "enter": { "stroke": { "value": "#ccc" } } }
}
},
{
"orient": "left",
"scale": "yscale",
"grid": false,
"domain": false,
"tickSize": 2,
"encode": {
"grid": { "enter": { "stroke": { "value": "#ccc" } } },
"ticks": { "enter": { "stroke": { "value": "#ccc" } } }
}
}
],
"marks": [
{
"type": "area",
"from": { "data": "table" },
"encode": {
"enter": { "fill": { "value": "#4C78A8" } },
"update": {
"interpolate": { "value": "monotone" },
"x": { "scale": "xscale", "field": "x" },
"y": { "scale": "yscale", "field": "p1" },
"y2": { "scale": "yscale", "field": "p99" },
"opacity": { "value": 0.05 }
}
}
},
{
"type": "area",
"from": { "data": "table" },
"encode": {
"enter": { "fill": { "value": "#4C78A8" } },
"update": {
"interpolate": { "value": "monotone" },
"x": { "scale": "xscale", "field": "x" },
"y": { "scale": "yscale", "field": "p5" },
"y2": { "scale": "yscale", "field": "p95" },
"opacity": { "value": 0.1 }
}
}
},
{
"type": "area",
"from": { "data": "table" },
"encode": {
"enter": { "fill": { "value": "#4C78A8" } },
"update": {
"interpolate": { "value": "monotone" },
"x": { "scale": "xscale", "field": "x" },
"y": { "scale": "yscale", "field": "p10" },
"y2": { "scale": "yscale", "field": "p90" },
"opacity": { "value": 0.15 }
}
}
},
{
"type": "area",
"from": { "data": "table" },
"encode": {
"enter": { "fill": { "value": "#4C78A8" } },
"update": {
"interpolate": { "value": "monotone" },
"x": { "scale": "xscale", "field": "x" },
"y": { "scale": "yscale", "field": "p20" },
"y2": { "scale": "yscale", "field": "p80" },
"opacity": { "value": 0.2 }
}
}
},
{
"type": "area",
"from": { "data": "table" },
"encode": {
"enter": { "fill": { "value": "#4C78A8" } },
"update": {
"interpolate": { "value": "monotone" },
"x": { "scale": "xscale", "field": "x" },
"y": { "scale": "yscale", "field": "p30" },
"y2": { "scale": "yscale", "field": "p70" },
"opacity": { "value": 0.2 }
}
}
},
{
"type": "area",
"from": { "data": "table" },
"encode": {
"enter": { "fill": { "value": "#4C78A8" } },
"update": {
"interpolate": { "value": "monotone" },
"x": { "scale": "xscale", "field": "x" },
"y": { "scale": "yscale", "field": "p40" },
"y2": { "scale": "yscale", "field": "p60" },
"opacity": { "value": 0.2 }
}
}
},
{
"type": "line",
"from": { "data": "table" },
"encode": {
"update": {
"interpolate": { "value": "monotone" },
"stroke": { "value": "#4C78A8" },
"strokeWidth": { "value": 2 },
"opacity": { "value": 0.8 },
"x": { "scale": "xscale", "field": "x" },
"y": { "scale": "yscale", "field": "p50" }
}
}
}
]
}

View File

@ -3,13 +3,21 @@ open Distributions;
type t = DistTypes.continuousShape;
let getShape = (t: t) => t.xyShape;
let interpolation = (t: t) => t.interpolation;
let make = (~interpolation=`Linear, ~integralSumCache=None, ~integralCache=None, xyShape): t => {
let make =
(
~interpolation=`Linear,
~integralSumCache=None,
~integralCache=None,
xyShape,
)
: t => {
xyShape,
interpolation,
integralSumCache,
integralCache,
};
let shapeMap = (fn, {xyShape, interpolation, integralSumCache, integralCache}: t): t => {
let shapeMap =
(fn, {xyShape, interpolation, integralSumCache, integralCache}: t): t => {
xyShape: fn(xyShape),
interpolation,
integralSumCache,
@ -19,10 +27,14 @@ let lastY = (t: t) => t |> getShape |> XYShape.T.lastY;
let oShapeMap =
(fn, {xyShape, interpolation, integralSumCache, integralCache}: t)
: option(DistTypes.continuousShape) =>
fn(xyShape) |> E.O.fmap(make(~interpolation, ~integralSumCache, ~integralCache));
fn(xyShape)
|> E.O.fmap(make(~interpolation, ~integralSumCache, ~integralCache));
let emptyIntegral: DistTypes.continuousShape = {
xyShape: {xs: [|neg_infinity|], ys: [|0.0|]},
xyShape: {
xs: [|neg_infinity|],
ys: [|0.0|],
},
interpolation: `Linear,
integralSumCache: Some(0.0),
integralCache: None,
@ -35,14 +47,18 @@ let empty: DistTypes.continuousShape = {
};
let stepwiseToLinear = (t: t): t =>
make(~integralSumCache=t.integralSumCache, ~integralCache=t.integralCache, XYShape.Range.stepwiseToLinear(t.xyShape));
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: DistTypes.distributionType = `PDF,
~integralCachesFn: (t, t) => option(t)=(_, _) => None,
~distributionType: DistTypes.distributionType=`PDF,
fn: (float, float) => float,
t1: DistTypes.continuousShape,
t2: DistTypes.continuousShape,
@ -62,19 +78,22 @@ let combinePointwise =
// 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 (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 extrapolation =
switch (distributionType) {
| `PDF => `UseZero
| `CDF => `UseOutermostPoints
};
let interpolator = XYShape.XtoY.continuousInterpolator(t1.interpolation, extrapolation);
let interpolator =
XYShape.XtoY.continuousInterpolator(t1.interpolation, extrapolation);
make(
~integralSumCache=combinedIntegralSum,
@ -103,10 +122,7 @@ let updateIntegralSumCache = (integralSumCache, t: t): t => {
integralSumCache,
};
let updateIntegralCache = (integralCache, t: t): t => {
...t,
integralCache,
};
let updateIntegralCache = (integralCache, t: t): t => {...t, integralCache};
let reduce =
(
@ -116,11 +132,13 @@ let reduce =
continuousShapes,
) =>
continuousShapes
|> E.A.fold_left(combinePointwise(~integralSumCachesFn, ~integralCachesFn, fn), empty);
|> E.A.fold_left(
combinePointwise(~integralSumCachesFn, ~integralCachesFn, fn),
empty,
);
let mapY = (~integralSumCacheFn=_ => None,
~integralCacheFn=_ => None,
~fn, t: t) => {
let mapY =
(~integralSumCacheFn=_ => None, ~integralCacheFn=_ => None, ~fn, t: t) => {
make(
~interpolation=t.interpolation,
~integralSumCache=t.integralSumCache |> E.O.bind(_, integralSumCacheFn),
@ -130,13 +148,15 @@ let mapY = (~integralSumCacheFn=_ => None,
};
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)));
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)
|> updateIntegralCache(scaledIntegralCache);
};
module T =
@ -171,20 +191,22 @@ module T =
|> XYShape.Zipped.filterByX(x => x >= lc && x <= rc);
let leftNewPoint =
leftCutoff |> E.O.dimap(lc => [|(lc -. epsilon_float, 0.)|], _ => [||]);
leftCutoff
|> E.O.dimap(lc => [|(lc -. epsilon_float, 0.)|], _ => [||]);
let rightNewPoint =
rightCutoff |> E.O.dimap(rc => [|(rc +. epsilon_float, 0.)|], _ => [||]);
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)
make(truncatedShape);
};
// TODO: This should work with stepwise plots.
let integral = (t) =>
let integral = t =>
switch (getShape(t) |> XYShape.T.isEmpty, t.integralCache) {
| (true, _) => emptyIntegral
| (false, Some(cache)) => cache
@ -253,22 +275,37 @@ let combineAlgebraicallyWithDiscrete =
if (XYShape.T.isEmpty(t1s) || XYShape.T.isEmpty(t2s)) {
empty;
} else {
let continuousAsLinear = switch (t1.interpolation) {
| `Linear => t1;
| `Stepwise => stepwiseToLinear(t1)
};
let continuousAsLinear =
switch (t1.interpolation) {
| `Linear => t1
| `Stepwise => stepwiseToLinear(t1)
};
let combinedShape = AlgebraicShapeCombination.combineShapesContinuousDiscrete(op, continuousAsLinear |> getShape, t2s);
let combinedIntegralSum =
Common.combineIntegralSums(
(a, b) => Some(a *. b),
t1.integralSumCache,
t2.integralSumCache,
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)
make(
~interpolation=t1.interpolation,
~integralSumCache=combinedIntegralSum,
combinedShape,
);
};
};

View File

@ -25,6 +25,7 @@ let toMixed =
let combineAlgebraically =
(op: ExpressionTypes.algebraicOperation, t1: t, t2: t): t => {
switch (t1, t2) {
| (Continuous(m1), Continuous(m2)) =>
Continuous.combineAlgebraically(op, m1, m2) |> Continuous.T.toShape;
@ -171,12 +172,13 @@ module T =
));
};
let maxX = mapToAll((Mixed.T.maxX, Discrete.T.maxX, Continuous.T.maxX));
let mapY = (~integralSumCacheFn=previousIntegralSum => None, ~integralCacheFn=previousIntegral=>None, ~fn) =>
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) {

View File

@ -1,27 +1,21 @@
open ExpressionTypes.ExpressionTree;
let toLeaf = (samplingInputs, environment, node: node) => {
node
|> ExpressionTreeEvaluator.toLeaf({
samplingInputs,
environment,
evaluateNode: ExpressionTreeEvaluator.toLeaf,
});
let toString = ExpressionTreeBasic.toString;
let envs = (samplingInputs, environment) => {
{samplingInputs, environment, evaluateNode: ExpressionTreeEvaluator.toLeaf};
};
let toLeaf = (samplingInputs, environment, node: node) =>
ExpressionTreeEvaluator.toLeaf(envs(samplingInputs, environment), node);
let toShape = (samplingInputs, environment, node: node) => {
let renderResult =
`Render(`Normalize(node)) |> toLeaf(samplingInputs, environment);
switch (renderResult) {
switch (toLeaf(samplingInputs, environment, node)) {
| Ok(`RenderedDist(shape)) => Ok(shape)
| Ok(_) => Error("Rendering failed.")
| Error(e) => Error(e)
};
};
let rec toString =
fun
| `SymbolicDist(d) => SymbolicDist.T.toString(d)
| `RenderedDist(_) => "[shape]"
| op => Operation.T.toString(toString, op);
let runFunction = (samplingInputs, environment, inputs, fn: PTypes.Function.t) => {
let params = envs(samplingInputs, environment);
PTypes.Function.run(params, inputs, fn);
};

View File

@ -0,0 +1,35 @@
open ExpressionTypes.ExpressionTree;
let rec toString: node => string =
fun
| `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.T.truncateToString(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(_, ",")
)
++ "}";

View File

@ -49,11 +49,11 @@ module AlgebraicCombination = {
(evaluationParams, algebraicOp, t1: node, t2: node)
: result(node, string) => {
E.R.merge(
SamplingDistribution.renderIfIsNotSamplingDistribution(
PTypes.SamplingDistribution.renderIfIsNotSamplingDistribution(
evaluationParams,
t1,
),
SamplingDistribution.renderIfIsNotSamplingDistribution(
PTypes.SamplingDistribution.renderIfIsNotSamplingDistribution(
evaluationParams,
t2,
),
@ -61,7 +61,7 @@ module AlgebraicCombination = {
|> E.R.bind(_, ((a, b)) =>
switch (choose(a, b)) {
| `Sampling =>
SamplingDistribution.combineShapesUsingSampling(
PTypes.SamplingDistribution.combineShapesUsingSampling(
evaluationParams,
algebraicOp,
a,
@ -91,33 +91,6 @@ module AlgebraicCombination = {
);
};
module VerticalScaling = {
let operationToLeaf =
(evaluationParams: evaluationParams, scaleOp, t, scaleBy) => {
// scaleBy has to be a single float, otherwise we'll return an error.
let fn = Operation.Scale.toFn(scaleOp);
let integralSumCacheFn = Operation.Scale.toIntegralSumCacheFn(scaleOp);
let integralCacheFn = Operation.Scale.toIntegralCacheFn(scaleOp);
let renderedShape = Render.render(evaluationParams, t);
switch (renderedShape, scaleBy) {
| (Ok(`RenderedDist(rs)), `SymbolicDist(`Float(sm))) =>
Ok(
`RenderedDist(
Shape.T.mapY(
~integralSumCacheFn=integralSumCacheFn(sm),
~integralCacheFn=integralCacheFn(sm),
~fn=fn(sm),
rs,
),
),
)
| (Error(e1), _) => Error(e1)
| (_, _) => Error("Can only scale by float values.")
};
};
};
module PointwiseCombination = {
let pointwiseAdd = (evaluationParams: evaluationParams, t1: t, t2: t) => {
switch (
@ -151,7 +124,8 @@ module PointwiseCombination = {
};
};
let pointwiseCombine = (fn, evaluationParams: evaluationParams, t1: t, t2: t) => {
let pointwiseCombine =
(fn, evaluationParams: evaluationParams, t1: t, t2: t) => {
// TODO: construct a function that we can easily sample from, to construct
// a RenderedDist. Use the xMin and xMax of the rendered shapes to tell the sampling function where to look.
// TODO: This should work for symbolic distributions too!
@ -160,7 +134,7 @@ module PointwiseCombination = {
Render.render(evaluationParams, t2),
) {
| (Ok(`RenderedDist(rs1)), Ok(`RenderedDist(rs2))) =>
Ok(`RenderedDist(Shape.combinePointwise(( *. ), rs1, rs2)))
Ok(`RenderedDist(Shape.combinePointwise(fn, rs1, rs2)))
| (Error(e1), _) => Error(e1)
| (_, Error(e2)) => Error(e2)
| _ => Error("Pointwise combination: rendering failed.")
@ -176,8 +150,8 @@ module PointwiseCombination = {
) => {
switch (pointwiseOp) {
| `Add => pointwiseAdd(evaluationParams, t1, t2)
| `Multiply => pointwiseCombine(( *. ),evaluationParams, t1, t2)
| `Exponentiate => pointwiseCombine(( *. ),evaluationParams, t1, t2)
| `Multiply => pointwiseCombine(( *. ), evaluationParams, t1, t2)
| `Exponentiate => pointwiseCombine(( ** ), evaluationParams, t1, t2)
};
};
};
@ -232,6 +206,7 @@ module Truncate = {
module Normalize = {
let rec operationToLeaf = (evaluationParams, t: node): result(node, string) => {
Js.log2("normalize", t);
switch (t) {
| `RenderedDist(s) => Ok(`RenderedDist(Shape.T.normalize(s)))
| `SymbolicDist(_) => Ok(t)
@ -240,36 +215,39 @@ module Normalize = {
};
};
module FloatFromDist = {
let rec operationToLeaf =
(evaluationParams, distToFloatOp: distToFloatOperation, t: node)
: result(node, string) => {
switch (t) {
| `SymbolicDist(s) =>
SymbolicDist.T.operate(distToFloatOp, s)
|> E.R.bind(_, v => Ok(`SymbolicDist(`Float(v))))
| `RenderedDist(rs) =>
Shape.operate(distToFloatOp, rs)
|> (v => Ok(`SymbolicDist(`Float(v))))
| _ =>
t
|> evaluateAndRetry(evaluationParams, r =>
operationToLeaf(r, distToFloatOp)
)
};
};
};
module FunctionCall = {
let _runHardcodedFunction = (name, evaluationParams, args) =>
TypeSystem.Function.Ts.findByNameAndRun(
HardcodedFunctions.all,
name,
evaluationParams,
args,
);
// TODO: This forces things to be floats
let callableFunction = (evaluationParams, name, args) => {
let b =
let _runLocalFunction = (name, evaluationParams: evaluationParams, args) => {
Environment.getFunction(evaluationParams.environment, name)
|> E.R.bind(_, ((argNames, fn)) =>
PTypes.Function.run(evaluationParams, args, (argNames, fn))
);
};
let _runWithEvaluatedInputs =
(
evaluationParams: ExpressionTypes.ExpressionTree.evaluationParams,
name,
args: array(ExpressionTypes.ExpressionTree.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 =>
Render.render(evaluationParams, a)
|> E.R.bind(_, Render.toFloat)
)
|> E.A.R.firstErrorOrOpen;
b |> E.R.bind(_, Functions.fnn(evaluationParams, name));
|> E.A.fmap(a => evaluationParams.evaluateNode(evaluationParams, a))
|> E.A.R.firstErrorOrOpen
|> E.R.bind(_, _runWithEvaluatedInputs(evaluationParams, name));
};
};
module Render = {
@ -280,7 +258,10 @@ module Render = {
| `SymbolicDist(d) =>
Ok(
`RenderedDist(
SymbolicDist.T.toShape(evaluationParams.samplingInputs.shapeLength, d),
SymbolicDist.T.toShape(
evaluationParams.samplingInputs.shapeLength,
d,
),
),
)
| `RenderedDist(_) as t => Ok(t) // already a rendered shape, we're done here
@ -289,29 +270,28 @@ module Render = {
};
};
let run = (node, fnNode) => {
switch (fnNode) {
| `Function(r) => Ok(r(node))
| _ => Error("Not a function")
};
};
/* 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 toLeaf =
(
evaluationParams: ExpressionTypes.ExpressionTree.evaluationParams,
node: t,
)
: result(t, string) => {
let rec toLeaf =
(
evaluationParams: ExpressionTypes.ExpressionTree.evaluationParams,
node: t,
)
: result(t, 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(
@ -327,17 +307,26 @@ let toLeaf =
t1,
t2,
)
| `VerticalScaling(scaleOp, t, scaleBy) =>
VerticalScaling.operationToLeaf(evaluationParams, scaleOp, t, scaleBy)
| `Truncate(leftCutoff, rightCutoff, t) =>
Truncate.operationToLeaf(evaluationParams, leftCutoff, rightCutoff, t)
| `FloatFromDist(distToFloatOp, t) =>
FloatFromDist.operationToLeaf(evaluationParams, distToFloatOp, t)
| `Normalize(t) => Normalize.operationToLeaf(evaluationParams, t)
| `Render(t) => Render.operationToLeaf(evaluationParams, t)
| `Function(_) => Error("Function must be called with params")
| `Symbol(r) => ExpressionTypes.ExpressionTree.Environment.get(evaluationParams.environment, r) |> E.O.toResult("Undeclared variable " ++ r)
| `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) =>
ExpressionTypes.ExpressionTree.Environment.get(
evaluationParams.environment,
r,
)
|> E.O.toResult("Undeclared variable " ++ r)
|> E.R.bind(_, toLeaf(evaluationParams))
| `FunctionCall(name, args) =>
callableFunction(evaluationParams, name, args)
};
FunctionCall.run(evaluationParams, name, args)
|> E.R.bind(_, toLeaf(evaluationParams))
}
};

View File

@ -1,4 +1,10 @@
type algebraicOperation = [ | `Add | `Multiply | `Subtract | `Divide | `Exponentiate];
type algebraicOperation = [
| `Add
| `Multiply
| `Subtract
| `Divide
| `Exponentiate
];
type pointwiseOperation = [ | `Add | `Multiply | `Exponentiate];
type scaleOperation = [ | `Multiply | `Exponentiate | `Log];
type distToFloatOperation = [
@ -10,43 +16,95 @@ type distToFloatOperation = [
];
module ExpressionTree = {
type node = [
type hash = array((string, node))
and node = [
| `SymbolicDist(SymbolicTypes.symbolicDist)
| `RenderedDist(DistTypes.shape)
| `Symbol(string)
| `Hash(hash)
| `Array(array(node))
| `Function(array(string), node)
| `AlgebraicCombination(algebraicOperation, node, node)
| `PointwiseCombination(pointwiseOperation, node, node)
| `VerticalScaling(scaleOperation, node, node)
| `Normalize(node)
| `Render(node)
| `Truncate(option(float), option(float), node)
| `Normalize(node)
| `FloatFromDist(distToFloatOperation, node)
| `Function(array(string), node)
| `FunctionCall(string, array(node))
| `Symbol(string)
];
module Hash = {
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)));
};
// Have nil as option
let getFloat = (node: node) =>
node
|> (
fun
| `RenderedDist(Discrete({xyShape: {xs: [|x|], ys: [|1.0|]}})) =>
Some(x)
| `SymbolicDist(`Float(x)) => Some(x)
| _ => None
);
let toFloatIfNeeded = (node: node) =>
switch (node |> getFloat) {
| Some(float) => `SymbolicDist(`Float(float))
| None => node
};
type samplingInputs = {
sampleCount: int,
outputXYPoints: int,
kernelWidth: option(float),
shapeLength: int
shapeLength: int,
};
module SamplingInputs = {
type t = {
sampleCount: option(int),
outputXYPoints: option(int),
kernelWidth: option(float),
shapeLength: option(int),
};
let withDefaults = (t: t): samplingInputs => {
sampleCount: t.sampleCount |> E.O.default(10000),
outputXYPoints: t.outputXYPoints |> E.O.default(10000),
kernelWidth: t.kernelWidth,
shapeLength: t.shapeLength |> E.O.default(10000),
};
};
type environment = Belt.Map.String.t(node);
module Environment = {
type t = 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 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")
};
};
type evaluationParams = {
samplingInputs,
@ -66,7 +124,7 @@ module ExpressionTree = {
type t = node;
let render = (evaluationParams: evaluationParams, r) =>
`Render(r) |> evaluateNode(evaluationParams);
`Render(r) |> evaluateNode(evaluationParams)
let ensureIsRendered = (params, t) =>
switch (t) {
@ -114,6 +172,9 @@ type simplificationResult = [
];
module Program = {
type statement = [ | `Assignment(string, ExpressionTree.node) | `Expression(ExpressionTree.node)];
type statement = [
| `Assignment(string, ExpressionTree.node)
| `Expression(ExpressionTree.node)
];
type program = array(statement);
}
};

View File

@ -1,122 +0,0 @@
type node = ExpressionTypes.ExpressionTree.node;
let toOkSym = r => Ok(`SymbolicDist(r));
let twoFloats = (fn, n1: node, n2: node): result(node, string) =>
switch (n1, n2) {
| (`SymbolicDist(`Float(a)), `SymbolicDist(`Float(b))) => fn(a, b)
| _ => Error("Variables have wrong type")
};
let threeFloats = (fn, n1: node, n2: node, n3: node): result(node, string) =>
switch (n1, n2, n3) {
| (
`SymbolicDist(`Float(a)),
`SymbolicDist(`Float(b)),
`SymbolicDist(`Float(c)),
) =>
fn(a, b, c)
| _ => Error("Variables have wrong type")
};
let twoFloatsToOkSym = fn => twoFloats((f1, f2) => fn(f1, f2) |> toOkSym);
let threeFloats = fn => threeFloats((f1, f2, f3) => fn(f1, f2, f3));
let apply2 = (fn, args): result(node, string) =>
switch (args) {
| [|a, b|] => fn(a, b)
| _ => Error("Needs 2 args")
};
let apply3 = (fn, args: array(node)): result(node, string) =>
switch (args) {
| [|a, b, c|] => fn(a, b, c)
| _ => Error("Needs 3 args")
};
let to_: array(node) => result(node, string) =
fun
| [|`SymbolicDist(`Float(low)), `SymbolicDist(`Float(high))|]
when low <= 0.0 && low < high => {
Ok(`SymbolicDist(SymbolicDist.Normal.from90PercentCI(low, high)));
}
| [|`SymbolicDist(`Float(low)), `SymbolicDist(`Float(high))|]
when low < high => {
Ok(`SymbolicDist(SymbolicDist.Lognormal.from90PercentCI(low, high)));
}
| [|`SymbolicDist(`Float(_)), `SymbolicDist(_)|] =>
Error("Low value must be less than high value.")
| _ => Error("Requires 2 variables");
let processCustomFn =
(
evaluationParams: ExpressionTypes.ExpressionTree.evaluationParams,
args: array(node),
argNames: array(string),
fnResult: node,
) =>
if (E.A.length(args) == E.A.length(argNames)) {
let newEnvironment =
Belt.Array.zip(argNames, args)
|> ExpressionTypes.ExpressionTree.Environment.fromArray;
let newEvaluationParams: ExpressionTypes.ExpressionTree.evaluationParams = {
samplingInputs: evaluationParams.samplingInputs,
environment:
ExpressionTypes.ExpressionTree.Environment.mergeKeepSecond(
evaluationParams.environment,
newEnvironment,
),
evaluateNode: evaluationParams.evaluateNode,
};
evaluationParams.evaluateNode(newEvaluationParams, fnResult);
} else {
Error("Failure");
};
let fnn =
(
evaluationParams: ExpressionTypes.ExpressionTree.evaluationParams,
name,
args: array(node),
) =>
switch (
name,
ExpressionTypes.ExpressionTree.Environment.get(
evaluationParams.environment,
name,
),
) {
| (_, Some(`Function(argNames, tt))) =>
processCustomFn(evaluationParams, args, argNames, tt)
| ("normal", _) =>
apply2(twoFloatsToOkSym(SymbolicDist.Normal.make), args)
| ("uniform", _) =>
apply2(twoFloatsToOkSym(SymbolicDist.Uniform.make), args)
| ("beta", _) => apply2(twoFloatsToOkSym(SymbolicDist.Beta.make), args)
| ("cauchy", _) =>
apply2(twoFloatsToOkSym(SymbolicDist.Cauchy.make), args)
| ("lognormal", _) =>
apply2(twoFloatsToOkSym(SymbolicDist.Lognormal.make), args)
| ("lognormalFromMeanAndStdDev", _) =>
apply2(twoFloatsToOkSym(SymbolicDist.Lognormal.fromMeanAndStdev), args)
| ("exponential", _) =>
switch (args) {
| [|`SymbolicDist(`Float(a))|] =>
Ok(`SymbolicDist(SymbolicDist.Exponential.make(a)))
| _ => Error("Needs 3 valid arguments")
}
| ("triangular", _) =>
switch (args) {
| [|
`SymbolicDist(`Float(a)),
`SymbolicDist(`Float(b)),
`SymbolicDist(`Float(c)),
|] =>
SymbolicDist.Triangular.make(a, b, c)
|> E.R.fmap(r => `SymbolicDist(r))
| _ => Error("Needs 3 valid arguments")
}
| ("to", _) => to_(args)
| _ => Error("Function not found")
};

View File

@ -1,4 +1,3 @@
[%%debugger.chrome]
module MathJsonToMathJsAdt = {
type arg =
| Symbol(string)
@ -82,17 +81,14 @@ module MathAdtToDistDst = {
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"
| "k" => Some(f *. 1000.)
| "M"
| "m" => Some(f *. 1000000.)
| "B"
| "b" => Some(f *. 1000000000.)
| "T"
| "t" => Some(f *. 1000000000000.)
| "K" => Some(f *. 1000.)
| "M" => Some(f *. 1000000.)
| "B" => Some(f *. 1000000000.)
| "T" => Some(f *. 1000000000000.)
| _ => None
};
let rec run =
@ -127,9 +123,7 @@ module MathAdtToDistDst = {
|> E.R.bind(_, nodeParser);
switch (g("mean"), g("stdev"), g("mu"), g("sigma")) {
| (Ok(mean), Ok(stdev), _, _) =>
Ok(
`FunctionCall(("lognormalFromMeanAndStdDev", [|mean, stdev|])),
)
Ok(`FunctionCall(("lognormalFromMeanAndStdDev", [|mean, stdev|])))
| (_, _, Ok(mu), Ok(sigma)) =>
Ok(`FunctionCall(("lognormal", [|mu, sigma|])))
| _ =>
@ -144,64 +138,35 @@ module MathAdtToDistDst = {
)
};
let multiModal =
(
args: array(result(ExpressionTypes.ExpressionTree.node, string)),
weights: option(array(float)),
) => {
let weights = weights |> E.O.default([||]);
let firstWithError = args |> Belt.Array.getBy(_, Belt.Result.isError);
let withoutErrors = args |> E.A.fmap(E.R.toOption) |> E.A.O.concatSomes;
switch (firstWithError) {
| Some(Error(e)) => Error(e)
| None when withoutErrors |> E.A.length == 0 =>
Error("Multimodals need at least one input")
| _ =>
let components =
withoutErrors
|> E.A.fmapi((index, t) => {
let w = weights |> E.A.get(_, index) |> E.O.default(1.0);
`VerticalScaling((`Multiply, t, `SymbolicDist(`Float(w))));
});
let pointwiseSum =
components
|> Js.Array.sliceFrom(1)
|> E.A.fold_left(
(acc, x) => {`PointwiseCombination((`Add, acc, x))},
E.A.unsafe_get(components, 0),
);
Ok(`Normalize(pointwiseSum));
};
};
// Error("Dotwise exponentiation needs two operands")
// Error("Dotwise exponentiation needs two operands")
let operationParser =
(
name: string,
args: result(array(ExpressionTypes.ExpressionTree.node), string),
):result(ExpressionTypes.ExpressionTree.node,string) => {
)
: result(ExpressionTypes.ExpressionTree.node, string) => {
let toOkAlgebraic = r => Ok(`AlgebraicCombination(r));
let toOkPointwise = r => Ok(`PointwiseCombination(r));
let toOkTruncate = r => Ok(`Truncate(r));
let toOkFloatFromDist = r => Ok(`FloatFromDist(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", [|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")
@ -228,25 +193,41 @@ module MathAdtToDistDst = {
Error(
"truncate needs three arguments: the expression and both cutoffs",
)
| ("pdf", [|d, `SymbolicDist(`Float(v))|]) =>
toOkFloatFromDist((`Pdf(v), d))
| ("cdf", [|d, `SymbolicDist(`Float(v))|]) =>
toOkFloatFromDist((`Cdf(v), d))
| ("inv", [|d, `SymbolicDist(`Float(v))|]) =>
toOkFloatFromDist((`Inv(v), d))
| ("mean", [|d|]) => toOkFloatFromDist((`Mean, d))
| ("sample", [|d|]) => toOkFloatFromDist((`Sample, d))
| _ => Error("This type not currently supported")
}
});
};
let functionParser = (nodeParser, name, args) => {
let functionParser =
(
nodeParser:
MathJsonToMathJsAdt.arg =>
Belt.Result.t(
ProbExample.ExpressionTypes.ExpressionTree.node,
string,
),
name: string,
args: array(MathJsonToMathJsAdt.arg),
)
: result(ExpressionTypes.ExpressionTree.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
@ -254,39 +235,36 @@ module MathAdtToDistDst = {
|> E.O.bind(
_,
fun
| Array(values) => Some(values)
| Array(values) => Some(parseArray(values))
| _ => None,
)
|> E.O.fmap(o =>
o
|> E.A.fmap(
fun
| Value(r) => Some(r)
| _ => None,
)
|> E.A.O.concatSomes
);
let possibleDists =
E.O.isSome(weights)
? Belt.Array.slice(args, ~offset=0, ~len=E.A.length(args) - 1)
: args;
let dists = possibleDists |> E.A.fmap(nodeParser);
multiModal(dists, weights);
| "add"
| "subtract"
| "multiply"
| "dotMultiply"
| "rightLogShift"
| "divide"
| "pow"
| "leftTruncate"
| "rightTruncate"
| "truncate"
| "mean"
| "inv"
| "sample"
| "cdf"
| "pdf" => operationParser(name, parseArgs())
let dists = parseArray(possibleDists);
switch (weights, dists) {
| (Some(Error(r)), _) => Error(r)
| (_, Error(r)) => Error(r)
| (None, Ok(dists)) =>
let hash: ExpressionTypes.ExpressionTree.node =
`FunctionCall(("multimodal", [|`Hash(
[|
("dists", `Array(dists)),
("weights", `Array([||]))
|]
)|]));
Ok(hash);
| (Some(Ok(weights)), Ok(dists)) =>
let hash: ExpressionTypes.ExpressionTree.node =
`FunctionCall(("multimodal", [|`Hash(
[|
("dists", `Array(dists)),
("weights", `Array(weights))
|]
)|]));
Ok(hash);
};
| name =>
parseArgs()
|> E.R.fmap((args: array(ExpressionTypes.ExpressionTree.node)) =>
@ -303,7 +281,7 @@ module MathAdtToDistDst = {
| Symbol(sym) => Ok(`Symbol(sym))
| Fn({name, args}) => functionParser(nodeParser, name, args)
| _ => {
Error("This type not currently supported")
Error("This type not currently supported");
};
// | FunctionAssignment({name, args, expression}) => {
@ -356,6 +334,7 @@ let fromString2 = str => {
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)) {
@ -365,6 +344,7 @@ let fromString2 = str => {
});
let value = E.R.bind(mathJsParse, MathAdtToDistDst.run);
Js.log2(mathJsParse, value);
value;
};

View File

@ -33,6 +33,7 @@ module Pointwise = {
let toString =
fun
| `Add => "+"
| `Exponentiate => "^"
| `Multiply => "*";
let format = (a, b, c) => b ++ " " ++ toString(a) ++ " " ++ c;
@ -51,6 +52,7 @@ module DistToFloat = {
};
};
// Note that different logarithms don't really do anything.
module Scale = {
type t = scaleOperation;
let toFn =

View File

@ -0,0 +1,143 @@
open ExpressionTypes.ExpressionTree;
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: ExpressionTypes.ExpressionTree.evaluationParams,
args: array(node),
t: t,
) =>
if (E.A.length(args) == E.A.length(argumentNames(t))) {
let newEnvironment =
Belt.Array.zip(argumentNames(t), args)
|> ExpressionTypes.ExpressionTree.Environment.fromArray;
let newEvaluationParams: ExpressionTypes.ExpressionTree.evaluationParams = {
samplingInputs: evaluationParams.samplingInputs,
environment:
ExpressionTypes.ExpressionTree.Environment.mergeKeepSecond(
evaluationParams.environment,
newEnvironment,
),
evaluateNode: evaluationParams.evaluateNode,
};
evaluationParams.evaluateNode(newEvaluationParams, internals(t));
} else {
Error("Wrong number of variables");
};
};
module Primative = {
type t = [
| `SymbolicDist(SymbolicTypes.symbolicDist)
| `RenderedDist(DistTypes.shape)
| `Function(array(string), node)
];
let isPrimative: node => bool =
fun
| `SymbolicDist(_)
| `RenderedDist(_)
| `Function(_) => true
| _ => false;
let fromNode: node => option(t) =
fun
| `SymbolicDist(_) as n
| `RenderedDist(_) as n
| `Function(_) as n => Some(n)
| _ => None;
};
module SamplingDistribution = {
type t = [
| `SymbolicDist(SymbolicTypes.symbolicDist)
| `RenderedDist(DistTypes.shape)
];
let isSamplingDistribution: node => bool =
fun
| `SymbolicDist(_) => true
| `RenderedDist(_) => true
| _ => false;
let fromNode: node => result(t, string) =
fun
| `SymbolicDist(n) => Ok(`SymbolicDist(n))
| `RenderedDist(n) => Ok(`RenderedDist(n))
| _ => Error("Not valid type");
let renderIfIsNotSamplingDistribution = (params, t): result(node, string) =>
!isSamplingDistribution(t)
? switch (Render.render(params, t)) {
| Ok(r) => Ok(r)
| Error(e) => Error(e)
}
: Ok(t);
let map = (~renderedDistFn, ~symbolicDistFn, node: node) =>
node
|> (
fun
| `RenderedDist(r) => Some(renderedDistFn(r))
| `SymbolicDist(s) => Some(symbolicDistFn(s))
| _ => None
);
let sampleN = n =>
map(
~renderedDistFn=Shape.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,
);
// todo: This bottom part should probably be somewhere else.
let shape =
samples
|> E.O.fmap(
SamplesToShape.fromSamples(
~samplingInputs=evaluationParams.samplingInputs,
),
)
|> E.O.bind(_, r => r.shape)
|> E.O.toResult("No response");
shape |> E.R.fmap(r => `Normalize(`RenderedDist(r)));
},
);
};
};

View File

@ -1,72 +0,0 @@
open ExpressionTypes.ExpressionTree;
let isSamplingDistribution: node => bool =
fun
| `SymbolicDist(_) => true
| `RenderedDist(_) => true
| _ => false;
let renderIfIsNotSamplingDistribution = (params, t): result(node, string) =>
!isSamplingDistribution(t)
? switch (Render.render(params, t)) {
| Ok(r) => Ok(r)
| Error(e) => Error(e)
}
: Ok(t);
let map = (~renderedDistFn, ~symbolicDistFn, node: node) =>
node
|> (
fun
| `RenderedDist(r) => Some(renderedDistFn(r))
| `SymbolicDist(s) => Some(symbolicDistFn(s))
| _ => None
);
let sampleN = n =>
map(
~renderedDistFn=Shape.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,
);
// todo: This bottom part should probably be somewhere else.
let shape =
samples
|> E.O.fmap(
SamplesToShape.fromSamples(
~samplingInputs=evaluationParams.samplingInputs,
),
)
|> E.O.bind(_, r => r.shape)
|> E.O.toResult("No response");
shape |> E.R.fmap(r => `Normalize(`RenderedDist(r)));
},
);
};

View File

@ -8,7 +8,7 @@ module Inputs = {
shapeLength: option(int),
};
};
let defaultRecommendedLength = 10000;
let defaultRecommendedLength = 100;
let defaultShouldDownsample = true;
type ingredients = {
@ -91,18 +91,16 @@ module Internals = {
};
let makeOutputs = (graph, shape): outputs => {graph, shape};
let makeInputs = (inputs): ExpressionTypes.ExpressionTree.samplingInputs => {
sampleCount: inputs.samplingInputs.sampleCount |> E.O.default(10000),
outputXYPoints:
inputs.samplingInputs.outputXYPoints |> E.O.default(10000),
kernelWidth: inputs.samplingInputs.kernelWidth,
shapeLength: inputs.samplingInputs.shapeLength |> E.O.default(10000),
};
let runNode = (inputs, node) => {
ExpressionTree.toShape(
{
sampleCount: inputs.samplingInputs.sampleCount |> E.O.default(10000),
outputXYPoints:
inputs.samplingInputs.outputXYPoints |> E.O.default(10000),
kernelWidth: inputs.samplingInputs.kernelWidth,
shapeLength: inputs.samplingInputs.shapeLength |> E.O.default(10000),
},
inputs.environment,
node,
);
ExpressionTree.toLeaf(makeInputs(inputs), inputs.environment, node);
};
let runProgram = (inputs: inputs, p: ExpressionTypes.Program.program) => {
@ -114,20 +112,19 @@ module Internals = {
ins := addVariable(ins^, name, node);
None;
}
| `Expression(node) => Some(runNode(ins^, node)),
| `Expression(node) =>
Some(
runNode(ins^, node) |> E.R.fmap(r => (ins^.environment, r)),
),
)
|> E.A.O.concatSomes
|> E.A.R.firstErrorOrOpen;
};
let inputsToShape = (inputs: inputs) => {
let inputsToLeaf = (inputs: inputs) => {
MathJsParser.fromString(inputs.guesstimatorString)
|> E.R.bind(_, g => runProgram(inputs, g))
|> E.R.bind(_, r =>
E.A.last(r)
|> E.O.toResult("No rendered lines")
|> E.R.fmap(Shape.T.normalize)
);
|> E.R.bind(_, r => E.A.last(r) |> E.O.toResult("No rendered lines"));
};
let outputToDistPlus = (inputs: Inputs.inputs, shape: DistTypes.shape) => {
@ -141,9 +138,113 @@ module Internals = {
};
};
let renderIfNeeded =
(inputs, node: ExpressionTypes.ExpressionTree.node)
: result(ExpressionTypes.ExpressionTree.node, string) =>
node
|> (
fun
| `Normalize(_) as n
| `SymbolicDist(_) as n => {
`Render(n)
|> Internals.runNode(Internals.distPlusRenderInputsToInputs(inputs))
|> (
fun
| Ok(`RenderedDist(_)) as r => r
| Error(r) => Error(r)
| _ => Error("Didn't render, but intended to")
);
}
| n => Ok(n)
);
let run = (inputs: Inputs.inputs) => {
inputs
|> Internals.distPlusRenderInputsToInputs
|> Internals.inputsToShape
|> Internals.inputsToLeaf
|> E.R.bind(_, ((lastIns, r)) =>
r
|> renderIfNeeded(inputs)
|> (
fun
| Ok(`RenderedDist(n)) => Ok(n)
| Ok(n) =>
Error(
"Didn't output a rendered distribution. Format:"
++ ExpressionTree.toString(n),
)
| Error(r) => Error(r)
)
)
|> E.R.fmap(Internals.outputToDistPlus(inputs));
};
let exportDistPlus =
(
inputs,
env: ProbExample.ExpressionTypes.ExpressionTree.environment,
node: ExpressionTypes.ExpressionTree.node,
) =>
node
|> renderIfNeeded(inputs)
|> E.R.bind(
_,
fun
| `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((n, env)))
| n =>
Error(
"Didn't output a rendered distribution. Format:"
++ ExpressionTree.toString(n),
),
);
// This isn't ok with floats, which can't be done in a function easily
let exportDistPlus2 =
(
inputs,
env: ProbExample.ExpressionTypes.ExpressionTree.environment,
node: ExpressionTypes.ExpressionTree.node,
) =>
node
|> renderIfNeeded(inputs)
|> E.R.bind(
_,
fun
| `RenderedDist(n) =>
Ok(`DistPlus(Internals.outputToDistPlus(inputs, n)))
| `Function(n) => Ok(`Function((n, env)))
| n =>
Error(
"Didn't output a rendered distribution. Format:"
++ ExpressionTree.toString(n),
),
);
let run2 = (inputs: Inputs.inputs) => {
inputs
|> Internals.distPlusRenderInputsToInputs
|> Internals.inputsToLeaf
|> E.R.bind(_, ((a, b)) => exportDistPlus(inputs, a, b));
};
let runFunction =
(
ins: Inputs.inputs,
fn: (array(string), ExpressionTypes.ExpressionTree.node),
fnInputs,
) => {
let inputs = ins |> Internals.distPlusRenderInputsToInputs;
let output =
ExpressionTree.runFunction(
Internals.makeInputs(inputs),
inputs.environment,
fnInputs,
fn,
);
output |> E.R.bind(_, exportDistPlus2(ins, inputs.environment));
};

View File

@ -0,0 +1,48 @@
// Not yet used
type inputs = {
samplingInputs: ExpressionTypes.ExpressionTree.SamplingInputs.t,
program: string,
environment: ExpressionTypes.ExpressionTree.environment,
};
let addVariable =
({program, samplingInputs, environment}: inputs, str, node): inputs => {
samplingInputs,
program,
environment:
ExpressionTypes.ExpressionTree.Environment.update(environment, str, _ =>
Some(node)
),
};
let runNode = (inputs: inputs, node) => {
ExpressionTree.toLeaf(
ExpressionTypes.ExpressionTree.SamplingInputs.withDefaults(
inputs.samplingInputs,
),
inputs.environment,
node,
);
};
let runProgram = (inputs: inputs, p: ExpressionTypes.Program.program) => {
let ins = ref(inputs);
p
|> E.A.fmap(
fun
| `Assignment(name, node) => {
ins := addVariable(ins^, name, node);
None;
}
| `Expression(node) =>
Some(runNode(ins^, node) |> E.R.fmap(r => (ins, r))),
)
|> E.A.O.concatSomes
|> E.A.R.firstErrorOrOpen;
};
let inputsToLeaf = (inputs: inputs) => {
MathJsParser.fromString(inputs.program)
|> E.R.bind(_, g => runProgram(inputs, g))
|> E.R.bind(_, r => E.A.last(r) |> E.O.toResult("No rendered lines"));
};

View File

@ -0,0 +1,258 @@
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) when low <= 0.0 && low < high =>
Ok(`SymbolicDist(SymbolicDist.Normal.from90PercentCI(low, high)))
| (low, high) when 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=
fun
| [|`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=
fun
| [|`Float(a)|] => Ok(`SymbolicDist(fn(a)))
| e => wrongInputsError(e),
(),
);
let makeDistFloat = (name, fn) =>
Function.T.make(
~name,
~outputType=`SamplingDistribution,
~inputTypes=[|`SamplingDistribution, `Float|],
~run=
fun
| [|`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=
fun
| [|`RenderedDist(a), `Float(b)|] => fn(a, b)
| e => wrongInputsError(e),
(),
);
let makeDist = (name, fn) =>
Function.T.make(
~name,
~outputType=`SamplingDistribution,
~inputTypes=[|`SamplingDistribution|],
~run=
fun
| [|`SamplingDist(a)|] => fn(a)
| [|`RenderedDist(a)|] => fn(`RenderedDist(a))
| e => wrongInputsError(e),
(),
);
let floatFromDist =
(
distToFloatOp: ExpressionTypes.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) =>
Shape.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(
Shape.T.mapY(
~integralSumCacheFn=integralSumCacheFn(scaleBy),
~integralCacheFn=integralCacheFn(scaleBy),
~fn=fn(scaleBy),
rs,
),
),
);
};
module Multimodal = {
let getByNameResult = ExpressionTypes.ExpressionTree.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=
fun
| [|`Float(a), `Float(b)|] => to_(a, b)
| e => wrongInputsError(e),
(),
),
Function.T.make(
~name="triangular",
~outputType=`SamplingDistribution,
~inputTypes=[|`Float, `Float, `Float|],
~run=
fun
| [|`Float(a), `Float(b), `Float(c)|] =>
SymbolicDist.Triangular.make(a, b, c)
|> E.R.fmap(r => `SymbolicDist(r))
| 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=
fun
| [|`RenderedDist(c)|] => Ok(`RenderedDist(c))
| e => wrongInputsError(e),
(),
),
Function.T.make(
~name="normalize",
~outputType=`SamplingDistribution,
~inputTypes=[|`SamplingDistribution|],
~run=
fun
| [|`SamplingDist(`SymbolicDist(c))|] => Ok(`SymbolicDist(c))
| [|`SamplingDist(`RenderedDist(c))|] =>
Ok(`RenderedDist(Shape.T.normalize(c)))
| e => wrongInputsError(e),
(),
),
makeRenderedDistFloat("scaleExp", (dist, float) =>
verticalScaling(`Exponentiate, dist, float)
),
makeRenderedDistFloat("scaleMultiply", (dist, float) =>
verticalScaling(`Multiply, dist, float)
),
makeRenderedDistFloat("scaleLog", (dist, float) =>
verticalScaling(`Log, dist, float)
),
Multimodal._function
|];

View File

@ -0,0 +1,228 @@
type node = ExpressionTypes.ExpressionTree.node;
let getFloat = ExpressionTypes.ExpressionTree.getFloat;
type samplingDist = [
| `SymbolicDist(SymbolicTypes.symbolicDist)
| `RenderedDist(DistTypes.shape)
];
type hashType = array((string, _type))
and _type = [
| `Float
| `SamplingDistribution
| `RenderedDistribution
| `Array(_type)
| `Hash(hashType)
];
type hashTypedValue = array((string, typedValue))
and typedValue = [
| `Float(float)
| `RenderedDist(DistTypes.shape)
| `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 =
fun
| `SamplingDist(_) => "[sampling dist]"
| `RenderedDist(_) => "[rendered Shape]"
| `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: " ++ ExpressionTreeBasic.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, _) =>
PTypes.SamplingDistribution.renderIfIsNotSamplingDistribution(
evaluationParams,
node,
)
|> E.R.bind(_, fromNode)
| (`RenderedDistribution, _) =>{
ExpressionTypes.ExpressionTree.Render.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,
ExpressionTypes.ExpressionTree.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) =
fun
| `Float(x) => Ok(x)
| _ => Error("Not a float");
let toArray: typedValue => result(array('a), string) =
fun
| `Array(x) => Ok(x)
| _ => Error("Not an array");
let toNamed: typedValue => result(hashTypedValue, string) =
fun
| `Hash(x) => Ok(x)
| _ => Error("Not a named item");
let toDist: typedValue => result(node,string) =
fun
| `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,
inputTypes,
outputType,
run,
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: ExpressionTypes.ExpressionTree.evaluationParams,
inputNodes: inputNodes,
t: t,
) => {
_inputLengthCheck(inputNodes, t)
->E.R.bind(
_coerceInputNodes(
evaluationParams,
t.inputTypes,
t.shouldCoerceTypes,
),
)
};
let run =
(
evaluationParams: ExpressionTypes.ExpressionTree.evaluationParams,
inputNodes: inputNodes,
t: t,
) => {
inputsToTypedValues(evaluationParams, inputNodes, t)->E.R.bind(t.run)
|> (
fun
| Ok(i) => Ok(i)
| Error(r) => {
Error("Function " ++ t.name ++ " error: " ++ r);
}
);
};
};
module Ts = {
let findByName = (ts: ts, n: string) =>
ts |> Belt.Array.getBy(_, ({name}) => name == n);
let findByNameAndRun = (ts: ts, n: string, evaluationParams, inputTypes) =>
findByName(ts, n) |> E.O.fmap(T.run(evaluationParams, inputTypes));
};
};

View File

@ -26,6 +26,9 @@ module FloatFloatMap = {
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;
@ -146,6 +149,11 @@ module R = {
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)
@ -157,6 +165,9 @@ module R = {
| 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) =>
@ -263,6 +274,7 @@ module A = {
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));
@ -285,6 +297,16 @@ module A = {
|> 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? */

641
yarn.lock

File diff suppressed because it is too large Load Diff