Merge pull request #419 from quantified-uncertainty/develop

`master` <-  `develop` sync apr28
This commit is contained in:
Quinn 2022-04-28 14:13:30 -04:00 committed by GitHub
commit 1806ba80fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
116 changed files with 3745 additions and 3193 deletions

View File

@ -9,3 +9,5 @@ updates:
directory: "/" # Location of package manifests
schedule:
interval: "daily"
commit-message:
prefix: "⬆️"

View File

@ -9,6 +9,7 @@ on:
branches:
- master
- develop
- reducer-dev
jobs:
pre_check:
@ -71,12 +72,16 @@ jobs:
run: cd ../../ && yarn
- name: Build rescript codebase
run: yarn build
- name: Run tests
run: yarn test
- name: Run rescript tests
run: yarn test:rescript
- name: Run typescript tests
run: yarn test:ts
- name: Run webpack
run: yarn bundle
- name: Upload coverage report
run: yarn coverage:ci
- name: Upload rescript coverage report
run: yarn coverage:rescript:ci
- name: Upload typescript coverage report
run: yarn coverage:ts:ci
components-lint:
name: Components lint

View File

@ -18,13 +18,6 @@ on:
- production
- staging
- develop
pull_request:
# The branches below must be a subset of the branches above
branches:
- master
- production
- staging
- develop
schedule:
- cron: "42 19 * * 0"

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ yarn-error.log
.DS_Store
**/.sync.ffs_db
.direnv
.log

View File

@ -1,6 +0,0 @@
{
"extends": "@parcel/config-default",
"transformers": {
"*.res": ["@parcel/transformer-raw"]
}
}

View File

@ -7,3 +7,7 @@ node_modules
packages/*/node_modules
packages/website/.docusaurus
packages/squiggle-lang/lib
packages/squiggle-lang/.nyc_output/
packages/squiggle-lang/coverage/
packages/squiggle-lang/.cache/
packages/website/build/

View File

@ -8,7 +8,7 @@ Squiggle is currently pre-alpha.
# Quick links
- [Roadmap to the alpha](https://github.com/QURIresearch/squiggle/projects/2)
- [Roadmap to the alpha](https://github.com/orgs/quantified-uncertainty/projects/1)
- The team presently communicates via the **EA Forecasting and Epistemics** slack (channels `#squiggle` and `#squiggle-ops`), you can track down an invite by reaching out to Ozzie Gooen
- [Squiggle documentation](https://www.squiggle-language.com/docs/Language)
- [Rescript documentation](https://rescript-lang.org/docs/manual/latest/introduction)
@ -20,10 +20,9 @@ Anyone (with a github account) can file an issue at any time. Please allow Quinn
# Project structure
Squiggle is a **monorepo** with four **packages**.
Squiggle is a **monorepo** with three **packages**.
- **components** is where we improve reactive interfacing with Squiggle
- **playground** is the site `playground.squiggle-language.com`
- **squiggle-lang** is where the magic happens: probability distributions, the interpreter, etc.
- **website** is the site `squiggle-language.com`
@ -41,13 +40,17 @@ We aspire for `ci.yaml` and `README.md`s to be in one-to-one correspondence.
## If you're on NixOS
You'll need to run a command like this in order to get `yarn build` to run, especially in `packages/squiggle-lang`.
You can't run `yarn` outside of a FHS shell. Additionally, you need to `patchelf` some things. A script does everything for you.
```sh
patchelf --set-interpreter $(patchelf --print-interpreter $(which mkdir)) ./node_modules/gentype/gentype.exe
./nixos.sh
```
See [here](https://github.com/NixOS/nixpkgs/issues/107375)
Reasons for this are comments in the script. Then, you should be able to do all the package-level `yarn run` commands/scripts.
# Try not to push directly to develop
If you absolutely must, please prefix your commit message with `hotfix: `.
# Pull request protocol

View File

@ -34,7 +34,7 @@ The playground depends on the components library which then depends on the langu
# Develop
For any project in the repo, begin by running `yarn` in the top level (TODO: is this true?)
For any project in the repo, begin by running `yarn` in the top level
```sh
yarn

20
examples/decay.squiggle Normal file
View File

@ -0,0 +1,20 @@
# The following code was provided by Nuño Sempere, it comes directly from the post https://www.lesswrong.com/s/rDe8QE5NvXcZYzgZ3/p/j8o6sgRerE3tqNWdj
## Initial setup
yearly_probability_max = 0.95
yearly_probability_min = 0.66
period_probability_function(epsilon, yearly_probability) = 1 - (1 - yearly_probability) ^ (1 / epsilon)
probability_decayed(t, time_periods, period_probability) = 1 - (1 - period_probability) ^ (time_periods - t)
## Monthly decomposition
months_in_a_year=12
monthly_probability_min = period_probability_function(months_in_a_year, yearly_probability_min)
monthly_probability_max = period_probability_function(months_in_a_year, yearly_probability_max)
probability_decayed_monthly_min(t) = probability_decayed(t, months_in_a_year, monthly_probability_min)
probability_decayed_monthly_max(t) = probability_decayed(t, months_in_a_year, monthly_probability_max)
probability_decayed_monthly(t) = probability_decayed_monthly_min(t) to probability_decayed_monthly_max(t)
probability_decayed_monthly
## probability_decayed_monthly(6)
## mean(probability_decayed_monthly(6))

View File

@ -0,0 +1,38 @@
# This is a cost effectiveness analysis of givedirectly, originally done by givewell, and translated into Squiggle by Sam Nolan
donation_size = 10000
proportion_of_funding_available = beta(10, 2)
total_funding_available = donation_size * proportion_of_funding_available
household_size = 3.7 to 5.7
size_of_transfer = 800 to 1200
size_of_transfer_per_person = size_of_transfer / household_size
portion_invested = 0.3 to 0.5
amount_invested = portion_invested * size_of_transfer_per_person
amount_consumed = (1 - portion_invested) * size_of_transfer_per_person
return_on_investment = 0.08 to 0.12
increase_in_consumption_from_investments = return_on_investment * amount_invested
baseline_consumption = 200 to 350
log_increase_in_consumption = log(amount_consumed + baseline_consumption) + log(baseline_consumption)
log_increase_in_consumption_from_investment = log(increase_in_consumption_from_investments + baseline_consumption) + log(baseline_consumption)
investment_duration = 8 to 12
discount_rate = beta(1.004, 20)
present_value_excluding_last_year = log_increase_in_consumption_from_investment * (1 - (1 + discount_rate) ^ (-investment_duration)) / (log(1 + discount_rate))
percent_of_investment_returned = 0.15 to 0.25
pv_consumption_last_year = (log(baseline_consumption + amount_invested * (return_on_investment + percent_of_investment_returned)) - log(baseline_consumption)) / (1 + discount_rate)^investment_duration
total_pv_of_cash_transfer = pv_consumption_last_year + present_value_excluding_last_year + log_increase_in_consumption
discount_negative_spoiler = 0.03 to 0.07
value_discounting_spoiler = discount_negative_spoiler * total_pv_of_cash_transfer
consumption_increase_per_household = value_discounting_spoiler * household_size
amount_of_transfers_made = total_funding_available / size_of_transfer
total_increase_in_ln_consumption = amount_of_transfers_made * consumption_increase_per_household
total_increase_in_ln_consumption

View File

@ -0,0 +1,3 @@
xY1 = 99
aBa3 = xY1 * 2 + 1
aBa3 * xY1 + aBa3

18
nixos.sh Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
# This script is only relevant if you're rolling nixos.
# Esy (a bisect_ppx dependency/build tool) is borked on nixos without using an FHS shell. https://github.com/esy/esy/issues/858
# We need to patchelf rescript executables. https://github.com/NixOS/nixpkgs/issues/107375
set -x
fhsShellName="squiggle-development"
fhsShellDotNix="{pkgs ? import <nixpkgs> {} }: (pkgs.buildFHSUserEnv { name = \"${fhsShellName}\"; targetPkgs = pkgs: [pkgs.yarn]; runScript = \"yarn\"; }).env"
nix-shell - <<<"$fhsShellDotNix"
theLd=$(patchelf --print-interpreter $(which mkdir))
patchelf --set-interpreter $theLd ./node_modules/gentype/gentype.exe
patchelf --set-interpreter $theLd ./node_modules/rescript/linux/*.exe
patchelf --set-interpreter $theLd ./node_modules/bisect_ppx/ppx
patchelf --set-interpreter $theLd ./node_moduels/bisect_ppx/bisect-ppx-report
theSo=$(find /nix/store/*$fhsShellName*/lib64 -name libstdc++.so.6 | grep $fhsShellName | head -n 1)
patchelf --replace-needed libstdc++.so.6 $theSo ./node_modules/rescript/linux/ninja.exe

View File

@ -0,0 +1,2 @@
dist/
storybook-static

View File

@ -1,26 +1,45 @@
{
"name": "@quri/squiggle-components",
"version": "0.1.8",
"version": "0.2.9",
"licence": "MIT",
"dependencies": {
"@quri/squiggle-lang": "0.2.2",
"antd": "^4.20.1",
"react-ace": "10.1.0",
"react-dom": "^18.1.0",
"@react-hook/size": "^2.1.2",
"styled-components": "^5.3.5"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.16.7",
"@storybook/addon-actions": "^6.4.22",
"@storybook/addon-essentials": "^6.4.22",
"@storybook/addon-links": "^6.4.22",
"@storybook/builder-webpack5": "^6.4.22",
"@storybook/manager-webpack5": "^6.4.22",
"@storybook/node-logger": "^6.4.22",
"@storybook/preset-create-react-app": "^4.1.0",
"@storybook/react": "^6.4.22",
"@types/styled-components": "^5.1.24",
"@types/webpack": "^5.28.0",
"style-loader": "^3.3.1",
"ts-loader": "^9.2.9",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.8.1",
"@quri/squiggle-lang": "0.2.5",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.0.1",
"@testing-library/user-event": "^14.0.4",
"@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^14.1.1",
"@types/jest": "^27.4.0",
"@types/lodash": "^4.14.181",
"@types/node": "^17.0.24",
"@types/lodash": "^4.14.182",
"@types/node": "^17.0.29",
"@types/react": "^18.0.3",
"@types/react-dom": "^18.0.1",
"antd": "^4.19.3",
"@types/react-dom": "^18.0.2",
"cross-env": "^7.0.3",
"lodash": "^4.17.21",
"react": "^18.0.0",
"react-ace": "10.0.0",
"react-dom": "^18.0.0",
"react": "^18.1.0",
"react-scripts": "5.0.1",
"react-vega": "^7.5.0",
"styled-components": "^5.3.5",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.6.3",
"vega": "^5.22.1",
@ -65,25 +84,6 @@
"last 1 safari version"
]
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.16.7",
"@storybook/addon-actions": "^6.4.22",
"@storybook/addon-essentials": "^6.4.22",
"@storybook/addon-links": "^6.4.22",
"@storybook/builder-webpack5": "^6.4.22",
"@storybook/manager-webpack5": "^6.4.22",
"@storybook/node-logger": "^6.4.22",
"@storybook/preset-create-react-app": "^4.1.0",
"@storybook/react": "^6.4.22",
"@types/styled-components": "^5.1.24",
"@types/webpack": "^5.28.0",
"react-codejar": "^1.1.2",
"style-loader": "^3.3.1",
"ts-loader": "^9.2.8",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.8.1"
},
"resolutions": {
"@types/react": "17.0.43"
},

View File

@ -6,7 +6,7 @@ import {
errorValueToString,
squiggleExpression,
} from "@quri/squiggle-lang";
import type { samplingParams, exportEnv } from "@quri/squiggle-lang";
import type { samplingParams } from "@quri/squiggle-lang";
import { NumberShower } from "./NumberShower";
import { DistributionChart } from "./DistributionChart";
import { ErrorBox } from "./ErrorBox";
@ -129,9 +129,9 @@ export interface SquiggleChartProps {
/** If the result is a function, how many points along the function it samples */
diagramCount?: number;
/** variables declared before this expression */
environment?: exportEnv;
environment?: unknown;
/** When the environment changes */
onEnvChange?(env: exportEnv): void;
onChange?(expr: squiggleExpression): void;
/** CSS width of the element */
width?: number;
height?: number;
@ -141,8 +141,7 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
squiggleString = "",
sampleCount = 1000,
outputXYPoints = 1000,
environment = [],
onEnvChange = () => {},
onChange = () => {},
height = 60,
width = NaN,
}: SquiggleChartProps) => {
@ -155,11 +154,11 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
sampleCount: sampleCount,
xyPointLength: outputXYPoints,
};
let expressionResult = run(squiggleString, samplingInputs, environment);
let expressionResult = run(squiggleString, samplingInputs);
let internal: JSX.Element;
if (expressionResult.tag === "Ok") {
onEnvChange(environment);
let expression = expressionResult.value;
onChange(expression);
internal = (
<SquiggleItem expression={expression} width={_width} height={height} />
);

View File

@ -2,8 +2,8 @@ import * as React from "react";
import * as ReactDOM from "react-dom";
import { SquiggleChart } from "./SquiggleChart";
import { CodeEditor } from "./CodeEditor";
import type { exportEnv } from "@quri/squiggle-lang";
import styled from "styled-components";
import type { squiggleExpression } from "@quri/squiggle-lang";
export interface SquiggleEditorProps {
/** The input string for squiggle */
@ -21,9 +21,9 @@ export interface SquiggleEditorProps {
/** If the result is a function, how many points along the function it samples */
diagramCount?: number;
/** The environment, other variables that were already declared */
environment?: exportEnv;
environment?: unknown;
/** when the environment changes. Used again for notebook magic*/
onEnvChange?(env: exportEnv): void;
onChange?(expr: squiggleExpression): void;
/** The width of the element */
width: number;
}
@ -44,7 +44,7 @@ export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({
diagramStart,
diagramStop,
diagramCount,
onEnvChange,
onChange,
environment,
}: SquiggleEditorProps) => {
let [expression, setExpression] = React.useState(initialSquiggleString);
@ -70,7 +70,7 @@ export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({
diagramStop={diagramStop}
diagramCount={diagramCount}
environment={environment}
onEnvChange={onEnvChange}
onChange={onChange}
/>
</div>
);
@ -81,7 +81,7 @@ export function renderSquiggleEditorToDom(props: SquiggleEditorProps) {
ReactDOM.render(
<SquiggleEditor
{...props}
onEnvChange={(env) => {
onChange={(expr) => {
// Typescript complains on two levels here.
// - Div elements don't have a value property
// - Even if it did (like it was an input element), it would have to
@ -97,10 +97,10 @@ export function renderSquiggleEditorToDom(props: SquiggleEditorProps) {
// viewof env = cell('normal(0,1)')
// to work
// @ts-ignore
parent.value = env;
parent.value = expr;
parent.dispatchEvent(new CustomEvent("input"));
if (props.onEnvChange) props.onEnvChange(env);
if (props.onChange) props.onChange(expr);
}}
/>,
parent

View File

@ -19,3 +19,5 @@ yarn-error.log
dist
*.coverage
_coverage
coverage
.nyc_output/

View File

@ -9,3 +9,7 @@ examples
yarn.nix
bsconfig.json
tsconfig.json
.nyc_outputs
*.coverage
_coverage
coverage

View File

@ -2,3 +2,6 @@ dist
lib
*.bs.js
*.gen.tsx
.nyc_output/
coverage/
.cache/

View File

@ -0,0 +1,17 @@
open Jest
open TestHelpers
describe("Combining Continuous and Discrete Distributions", () => {
makeTest(
"keep order of xs when multiplying by negative number",
AlgebraicShapeCombination.isOrdered(
AlgebraicShapeCombination.combineShapesContinuousDiscrete(
#Multiply,
{xs: [0., 1.], ys: [1., 1.]},
{xs: [-1.], ys: [1.]},
~discretePosition=Second,
),
), // Multiply distribution by -1
true,
)
})

View File

@ -30,7 +30,7 @@ let toExt: option<'a> => 'a = E.O.toExt(
describe("sparkline", () => {
let runTest = (
name: string,
dist: GenericDist_Types.genericDist,
dist: DistributionTypes.genericDist,
expected: DistributionOperation.outputType,
) => {
test(name, () => {

View File

@ -1,14 +1,14 @@
let normalDist5: GenericDist_Types.genericDist = Symbolic(#Normal({mean: 5.0, stdev: 2.0}))
let normalDist10: GenericDist_Types.genericDist = Symbolic(#Normal({mean: 10.0, stdev: 2.0}))
let normalDist20: GenericDist_Types.genericDist = Symbolic(#Normal({mean: 20.0, stdev: 2.0}))
let normalDist: GenericDist_Types.genericDist = normalDist5
let normalDist5: DistributionTypes.genericDist = Symbolic(#Normal({mean: 5.0, stdev: 2.0}))
let normalDist10: DistributionTypes.genericDist = Symbolic(#Normal({mean: 10.0, stdev: 2.0}))
let normalDist20: DistributionTypes.genericDist = Symbolic(#Normal({mean: 20.0, stdev: 2.0}))
let normalDist: DistributionTypes.genericDist = normalDist5
let betaDist: GenericDist_Types.genericDist = Symbolic(#Beta({alpha: 2.0, beta: 5.0}))
let lognormalDist: GenericDist_Types.genericDist = Symbolic(#Lognormal({mu: 0.0, sigma: 1.0}))
let cauchyDist: GenericDist_Types.genericDist = Symbolic(#Cauchy({local: 1.0, scale: 1.0}))
let triangularDist: GenericDist_Types.genericDist = Symbolic(
let betaDist: DistributionTypes.genericDist = Symbolic(#Beta({alpha: 2.0, beta: 5.0}))
let lognormalDist: DistributionTypes.genericDist = Symbolic(#Lognormal({mu: 0.0, sigma: 1.0}))
let cauchyDist: DistributionTypes.genericDist = Symbolic(#Cauchy({local: 1.0, scale: 1.0}))
let triangularDist: DistributionTypes.genericDist = Symbolic(
#Triangular({low: 1.0, medium: 2.0, high: 3.0}),
)
let exponentialDist: GenericDist_Types.genericDist = Symbolic(#Exponential({rate: 2.0}))
let uniformDist: GenericDist_Types.genericDist = Symbolic(#Uniform({low: 9.0, high: 10.0}))
let floatDist: GenericDist_Types.genericDist = Symbolic(#Float(1e1))
let exponentialDist: DistributionTypes.genericDist = Symbolic(#Exponential({rate: 2.0}))
let uniformDist: DistributionTypes.genericDist = Symbolic(#Uniform({low: 9.0, high: 10.0}))
let floatDist: DistributionTypes.genericDist = Symbolic(#Float(1e1))

View File

@ -43,10 +43,10 @@ describe("(Algebraic) addition of distributions", () => {
test("normal(mean=5) + normal(mean=20)", () => {
normalDist5
->algebraicAdd(normalDist20)
->E.R2.fmap(GenericDist_Types.Constructors.UsingDists.mean)
->E.R2.fmap(DistributionTypes.Constructors.UsingDists.mean)
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn
->E.R.toExn("Expected float", _)
->expect
->toBe(Some(2.5e1))
})
@ -57,15 +57,15 @@ describe("(Algebraic) addition of distributions", () => {
let received =
uniformDist
->algebraicAdd(betaDist)
->E.R2.fmap(GenericDist_Types.Constructors.UsingDists.mean)
->E.R2.fmap(DistributionTypes.Constructors.UsingDists.mean)
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn
->E.R.toExn("Expected float", _)
switch received {
| None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad.
// sometimes it works with ~digits=2.
| Some(x) => x->expect->toBeSoCloseTo(0.01927225696028752, ~digits=1) // (uniformMean +. betaMean)
| Some(x) => x->expect->toBeSoCloseTo(9.786831807237022, ~digits=1) // (uniformMean +. betaMean)
}
})
test("beta(alpha=2, beta=5) + uniform(low=9, high=10)", () => {
@ -74,15 +74,15 @@ describe("(Algebraic) addition of distributions", () => {
let received =
betaDist
->algebraicAdd(uniformDist)
->E.R2.fmap(GenericDist_Types.Constructors.UsingDists.mean)
->E.R2.fmap(DistributionTypes.Constructors.UsingDists.mean)
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn
->E.R.toExn("Expected float", _)
switch received {
| None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad.
// sometimes it works with ~digits=2.
| Some(x) => x->expect->toBeSoCloseTo(0.019275414920485248, ~digits=1) // (uniformMean +. betaMean)
| Some(x) => x->expect->toBeSoCloseTo(9.784290207736126, ~digits=1) // (uniformMean +. betaMean)
}
})
})
@ -95,7 +95,7 @@ describe("(Algebraic) addition of distributions", () => {
let received =
normalDist10 // this should be normal(10, sqrt(8))
->Ok
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.pdf(d, x))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.pdf(d, x))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
@ -103,7 +103,7 @@ describe("(Algebraic) addition of distributions", () => {
let calculated =
normalDist5
->algebraicAdd(normalDist5)
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.pdf(d, x))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.pdf(d, x))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
@ -126,7 +126,7 @@ describe("(Algebraic) addition of distributions", () => {
let received =
normalDist20
->Ok
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.pdf(d, 1.9e1))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.pdf(d, 1.9e1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
@ -134,7 +134,7 @@ describe("(Algebraic) addition of distributions", () => {
let calculated =
normalDist10
->algebraicAdd(normalDist10)
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.pdf(d, 1.9e1))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.pdf(d, 1.9e1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
@ -155,30 +155,30 @@ describe("(Algebraic) addition of distributions", () => {
let received =
uniformDist
->algebraicAdd(betaDist)
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.pdf(d, 1e1))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.pdf(d, 1e1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn
->E.R.toExn("Expected float", _)
switch received {
| None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad.
// sometimes it works with ~digits=4.
| Some(x) => x->expect->toBeSoCloseTo(0.001978994877226945, ~digits=3)
// This value was calculated by a python script
| Some(x) => x->expect->toBeSoCloseTo(0.979023, ~digits=0)
}
})
test("(beta(alpha=2, beta=5) + uniform(low=9, high=10)).pdf(10)", () => {
let received =
betaDist
->algebraicAdd(uniformDist)
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.pdf(d, 1e1))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.pdf(d, 1e1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn
->E.R.toExn("Expected float", _)
switch received {
| None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad.
// sometimes it works with ~digits=4.
| Some(x) => x->expect->toBeSoCloseTo(0.001978994877226945, ~digits=3)
// This is nondeterministic.
| Some(x) => x->expect->toBeSoCloseTo(0.979023, ~digits=0)
}
})
})
@ -187,7 +187,7 @@ describe("(Algebraic) addition of distributions", () => {
let received =
normalDist10
->Ok
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.cdf(d, x))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, x))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
@ -195,7 +195,7 @@ describe("(Algebraic) addition of distributions", () => {
let calculated =
normalDist5
->algebraicAdd(normalDist5)
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.cdf(d, x))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, x))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
@ -217,7 +217,7 @@ describe("(Algebraic) addition of distributions", () => {
let received =
normalDist20
->Ok
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.cdf(d, 1.25e1))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, 1.25e1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
@ -225,7 +225,7 @@ describe("(Algebraic) addition of distributions", () => {
let calculated =
normalDist10
->algebraicAdd(normalDist10)
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.cdf(d, 1.25e1))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, 1.25e1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
@ -246,30 +246,30 @@ describe("(Algebraic) addition of distributions", () => {
let received =
uniformDist
->algebraicAdd(betaDist)
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.cdf(d, 1e1))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, 1e1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn
->E.R.toExn("Expected float", _)
switch received {
| None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad.
// sometimes it works with ~digits=4.
| Some(x) => x->expect->toBeSoCloseTo(0.0013961779932477507, ~digits=3)
// The value was calculated externally using a python script
| Some(x) => x->expect->toBeSoCloseTo(0.71148, ~digits=1)
}
})
test("(beta(alpha=2, beta=5) + uniform(low=9, high=10)).cdf(10)", () => {
let received =
betaDist
->algebraicAdd(uniformDist)
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.cdf(d, 1e1))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.cdf(d, 1e1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn
->E.R.toExn("Expected float", _)
switch received {
| None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad.
// sometimes it works with ~digits=4.
| Some(x) => x->expect->toBeSoCloseTo(0.001388898111625753, ~digits=3)
// The value was calculated externally using a python script
| Some(x) => x->expect->toBeSoCloseTo(0.71148, ~digits=1)
}
})
})
@ -279,7 +279,7 @@ describe("(Algebraic) addition of distributions", () => {
let received =
normalDist10
->Ok
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.inv(d, x))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, x))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
@ -287,7 +287,7 @@ describe("(Algebraic) addition of distributions", () => {
let calculated =
normalDist5
->algebraicAdd(normalDist5)
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.inv(d, x))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, x))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
@ -309,7 +309,7 @@ describe("(Algebraic) addition of distributions", () => {
let received =
normalDist20
->Ok
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.inv(d, 1e-1))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, 1e-1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
@ -317,7 +317,7 @@ describe("(Algebraic) addition of distributions", () => {
let calculated =
normalDist10
->algebraicAdd(normalDist10)
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.inv(d, 1e-1))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, 1e-1))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toOption
@ -338,30 +338,30 @@ describe("(Algebraic) addition of distributions", () => {
let received =
uniformDist
->algebraicAdd(betaDist)
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.inv(d, 2e-2))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, 2e-2))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn
->E.R.toExn("Expected float", _)
switch received {
| None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad.
// sometimes it works with ~digits=2.
| Some(x) => x->expect->toBeSoCloseTo(10.927078217530806, ~digits=0)
| Some(x) => x->expect->toBeSoCloseTo(9.179319623146968, ~digits=0)
}
})
test("(beta(alpha=2, beta=5) + uniform(low=9, high=10)).inv(2e-2)", () => {
let received =
betaDist
->algebraicAdd(uniformDist)
->E.R2.fmap(d => GenericDist_Types.Constructors.UsingDists.inv(d, 2e-2))
->E.R2.fmap(d => DistributionTypes.Constructors.UsingDists.inv(d, 2e-2))
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn
->E.R.toExn("Expected float", _)
switch received {
| None => "algebraicAdd has"->expect->toBe("failed")
// This is nondeterministic, we could be in a situation where ci fails but you click rerun and it passes, which is bad.
// sometimes it works with ~digits=2.
| Some(x) => x->expect->toBeSoCloseTo(10.915396627014363, ~digits=0)
| Some(x) => x->expect->toBeSoCloseTo(9.190872365862756, ~digits=0)
}
})
})

View File

@ -15,7 +15,7 @@ open TestHelpers
module Internals = {
let epsilon = 5e1
let mean = GenericDist_Types.Constructors.UsingDists.mean
let mean = DistributionTypes.Constructors.UsingDists.mean
let expectImpossiblePath: string => assertion = algebraicOp =>
`${algebraicOp} has`->expect->toEqual("failed")
@ -50,7 +50,11 @@ module Internals = {
let dist1 = dist1'->DistributionTypes.Symbolic
let dist2 = dist2'->DistributionTypes.Symbolic
let received =
distOp(dist1, dist2)->E.R2.fmap(mean)->E.R2.fmap(run)->E.R2.fmap(toFloat)->E.R.toExn
distOp(dist1, dist2)
->E.R2.fmap(mean)
->E.R2.fmap(run)
->E.R2.fmap(toFloat)
->E.R.toExn("Expected float", _)
let expected = floatOp(runMean(dist1), runMean(dist2))
switch received {
| None => expectImpossiblePath(description)
@ -80,14 +84,16 @@ let {testOperationMean, distributions, pairsOfDifferentDistributions, epsilon} =
describe("Means are invariant", () => {
describe("for addition", () => {
let testAdditionMean = testOperationMean(algebraicAdd, "algebraicAdd", \"+.", ~epsilon)
let testAddInvariant = (t1, t2) =>
E.R.liftM2(testAdditionMean, t1, t2)->E.R.toExn("Means were not invariant", _)
testAll("with two of the same distribution", distributions, dist => {
E.R.liftM2(testAdditionMean, dist, dist)->E.R.toExn
testAddInvariant(dist, dist)
})
testAll("with two different distributions", pairsOfDifferentDistributions, dists => {
let (dist1, dist2) = dists
E.R.liftM2(testAdditionMean, dist1, dist2)->E.R.toExn
testAddInvariant(dist1, dist2)
})
testAll(
@ -95,7 +101,7 @@ describe("Means are invariant", () => {
pairsOfDifferentDistributions,
dists => {
let (dist1, dist2) = dists
E.R.liftM2(testAdditionMean, dist2, dist1)->E.R.toExn
testAddInvariant(dist1, dist2)
},
)
})
@ -107,14 +113,16 @@ describe("Means are invariant", () => {
\"-.",
~epsilon,
)
let testSubtractInvariant = (t1, t2) =>
E.R.liftM2(testSubtractionMean, t1, t2)->E.R.toExn("Means were not invariant", _)
testAll("with two of the same distribution", distributions, dist => {
E.R.liftM2(testSubtractionMean, dist, dist)->E.R.toExn
testSubtractInvariant(dist, dist)
})
testAll("with two different distributions", pairsOfDifferentDistributions, dists => {
let (dist1, dist2) = dists
E.R.liftM2(testSubtractionMean, dist1, dist2)->E.R.toExn
testSubtractInvariant(dist1, dist2)
})
testAll(
@ -122,7 +130,7 @@ describe("Means are invariant", () => {
pairsOfDifferentDistributions,
dists => {
let (dist1, dist2) = dists
E.R.liftM2(testSubtractionMean, dist2, dist1)->E.R.toExn
testSubtractInvariant(dist1, dist2)
},
)
})
@ -134,14 +142,16 @@ describe("Means are invariant", () => {
\"*.",
~epsilon,
)
let testMultiplicationInvariant = (t1, t2) =>
E.R.liftM2(testMultiplicationMean, t1, t2)->E.R.toExn("Means were not invariant", _)
testAll("with two of the same distribution", distributions, dist => {
E.R.liftM2(testMultiplicationMean, dist, dist)->E.R.toExn
testMultiplicationInvariant(dist, dist)
})
testAll("with two different distributions", pairsOfDifferentDistributions, dists => {
let (dist1, dist2) = dists
E.R.liftM2(testMultiplicationMean, dist1, dist2)->E.R.toExn
testMultiplicationInvariant(dist1, dist2)
})
testAll(
@ -149,7 +159,7 @@ describe("Means are invariant", () => {
pairsOfDifferentDistributions,
dists => {
let (dist1, dist2) = dists
E.R.liftM2(testMultiplicationMean, dist2, dist1)->E.R.toExn
testMultiplicationInvariant(dist1, dist2)
},
)
})

View File

@ -0,0 +1,20 @@
open Jest
open Expect
describe("Converting from a sample set distribution", () => {
test("Should be normalized", () => {
let outputXYShape = SampleSetDist_ToPointSet.Internals.KDE.normalSampling(
[1., 2., 3., 3., 4., 5., 5., 5., 6., 8., 9., 9.],
50,
2,
)
let c: PointSetTypes.continuousShape = {
xyShape: outputXYShape,
interpolation: #Linear,
integralSumCache: None,
integralCache: None,
}
expect(Continuous.isNormalized(c))->toBe(true)
})
})

View File

@ -1,40 +0,0 @@
open Jest
open TestHelpers
describe("Continuous and discrete splits", () => {
makeTest(
"splits (1)",
SampleSetDist_ToPointSet.Internals.T.splitContinuousAndDiscrete([1.432, 1.33455, 2.0]),
([1.432, 1.33455, 2.0], E.FloatFloatMap.empty()),
)
makeTest(
"splits (2)",
SampleSetDist_ToPointSet.Internals.T.splitContinuousAndDiscrete([
1.432,
1.33455,
2.0,
2.0,
2.0,
2.0,
]) |> (((c, disc)) => (c, disc |> E.FloatFloatMap.toArray)),
([1.432, 1.33455], [(2.0, 4.0)]),
)
let makeDuplicatedArray = count => {
let arr = Belt.Array.range(1, count) |> E.A.fmap(float_of_int)
let sorted = arr |> Belt.SortArray.stableSortBy(_, compare)
E.A.concatMany([sorted, sorted, sorted, sorted]) |> Belt.SortArray.stableSortBy(_, compare)
}
let (_, discrete1) = SampleSetDist_ToPointSet.Internals.T.splitContinuousAndDiscrete(
makeDuplicatedArray(10),
)
let toArr1 = discrete1 |> E.FloatFloatMap.toArray
makeTest("splitMedium at count=10", toArr1 |> Belt.Array.length, 10)
let (_c, discrete2) = SampleSetDist_ToPointSet.Internals.T.splitContinuousAndDiscrete(
makeDuplicatedArray(500),
)
let toArr2 = discrete2 |> E.FloatFloatMap.toArray
makeTest("splitMedium at count=500", toArr2 |> Belt.Array.length, 500)
})

View File

@ -0,0 +1,48 @@
open Jest
open TestHelpers
let prepareInputs = (ar, minWeight) =>
E.A.Sorted.Floats.splitContinuousAndDiscreteForMinWeight(ar, ~minDiscreteWeight=minWeight) |> (
((c, disc)) => (c, disc |> E.FloatFloatMap.toArray)
)
describe("Continuous and discrete splits", () => {
makeTest(
"is empty, with no common elements",
prepareInputs([1.432, 1.33455, 2.0], 2),
([1.33455, 1.432, 2.0], []),
)
makeTest(
"only stores 3.5 as discrete when minWeight is 3",
prepareInputs([1.432, 1.33455, 2.0, 2.0, 3.5, 3.5, 3.5], 3),
([1.33455, 1.432, 2.0, 2.0], [(3.5, 3.0)]),
)
makeTest(
"doesn't store 3.5 as discrete when minWeight is 5",
prepareInputs([1.432, 1.33455, 2.0, 2.0, 3.5, 3.5, 3.5], 5),
([1.33455, 1.432, 2.0, 2.0, 3.5, 3.5, 3.5], []),
)
let makeDuplicatedArray = count => {
let arr = Belt.Array.range(1, count) |> E.A.fmap(float_of_int)
let sorted = arr |> Belt.SortArray.stableSortBy(_, compare)
E.A.concatMany([sorted, sorted, sorted, sorted]) |> Belt.SortArray.stableSortBy(_, compare)
}
let (_, discrete1) = E.A.Sorted.Floats.splitContinuousAndDiscreteForMinWeight(
makeDuplicatedArray(10),
~minDiscreteWeight=2,
)
let toArr1 = discrete1 |> E.FloatFloatMap.toArray
makeTest("splitMedium at count=10", toArr1 |> Belt.Array.length, 10)
let (_c, discrete2) = E.A.Sorted.Floats.splitContinuousAndDiscreteForMinWeight(
makeDuplicatedArray(500),
~minDiscreteWeight=2,
)
let toArr2 = discrete2 |> E.FloatFloatMap.toArray
makeTest("splitMedium at count=500", toArr2 |> Belt.Array.length, 500)
// makeTest("foo", [] |> Belt.Array.length, 500)
})

View File

@ -7,5 +7,69 @@ open Expect
let expectParseToBe = (expr: string, answer: string) =>
Reducer.parse(expr)->Expression.toStringResult->expect->toBe(answer)
let expectParseOuterToBe = (expr: string, answer: string) =>
Reducer.parseOuter(expr)->Expression.toStringResult->expect->toBe(answer)
let expectParsePartialToBe = (expr: string, answer: string) =>
Reducer.parsePartial(expr)->Expression.toStringResult->expect->toBe(answer)
let expectEvalToBe = (expr: string, answer: string) =>
Reducer.evaluate(expr)->ExpressionValue.toStringResult->expect->toBe(answer)
let expectEvalBindingsToBe = (expr: string, bindings: Reducer.externalBindings, answer: string) =>
Reducer.evaluateUsingExternalBindings(expr, bindings)
->ExpressionValue.toStringResult
->expect
->toBe(answer)
let expectEvalPartialBindingsToBe = (
expr: string,
bindings: Reducer.externalBindings,
answer: string,
) =>
Reducer.evaluatePartialUsingExternalBindings(expr, bindings)
->ExpressionValue.toStringResultRecord
->expect
->toBe(answer)
let testParseToBe = (expr, answer) => test(expr, () => expectParseToBe(expr, answer))
let testParseOuterToBe = (expr, answer) => test(expr, () => expectParseOuterToBe(expr, answer))
let testParsePartialToBe = (expr, answer) => test(expr, () => expectParsePartialToBe(expr, answer))
let testDescriptionParseToBe = (desc, expr, answer) =>
test(desc, () => expectParseToBe(expr, answer))
let testEvalToBe = (expr, answer) => test(expr, () => expectEvalToBe(expr, answer))
let testDescriptionEvalToBe = (desc, expr, answer) => test(desc, () => expectEvalToBe(expr, answer))
let testEvalBindingsToBe = (expr, bindingsList, answer) =>
test(expr, () => expectEvalBindingsToBe(expr, bindingsList->Js.Dict.fromList, answer))
let testEvalPartialBindingsToBe = (expr, bindingsList, answer) =>
test(expr, () => expectEvalPartialBindingsToBe(expr, bindingsList->Js.Dict.fromList, answer))
module MySkip = {
let testParseToBe = (expr, answer) => Skip.test(expr, () => expectParseToBe(expr, answer))
let testParseOuterToBe = (expr, answer) =>
Skip.test(expr, () => expectParseOuterToBe(expr, answer))
let testParsePartialToBe = (expr, answer) =>
Skip.test(expr, () => expectParsePartialToBe(expr, answer))
let testEvalToBe = (expr, answer) => Skip.test(expr, () => expectEvalToBe(expr, answer))
let testEvalBindingsToBe = (expr, bindingsList, answer) =>
Skip.test(expr, () => expectEvalBindingsToBe(expr, bindingsList->Js.Dict.fromList, answer))
let testEvalPartialBindingsToBe = (expr, bindingsList, answer) =>
Skip.test(expr, () =>
expectEvalPartialBindingsToBe(expr, bindingsList->Js.Dict.fromList, answer)
)
}
module MyOnly = {
let testParseToBe = (expr, answer) => Only.test(expr, () => expectParseToBe(expr, answer))
let testParseOuterToBe = (expr, answer) =>
Only.test(expr, () => expectParseOuterToBe(expr, answer))
let testParsePartialToBe = (expr, answer) =>
Only.test(expr, () => expectParsePartialToBe(expr, answer))
let testEvalToBe = (expr, answer) => Only.test(expr, () => expectEvalToBe(expr, answer))
let testEvalBindingsToBe = (expr, bindingsList, answer) =>
Only.test(expr, () => expectEvalBindingsToBe(expr, bindingsList->Js.Dict.fromList, answer))
let testEvalPartialBindingsToBe = (expr, bindingsList, answer) =>
Only.test(expr, () =>
expectEvalPartialBindingsToBe(expr, bindingsList->Js.Dict.fromList, answer)
)
}

View File

@ -0,0 +1,60 @@
open Jest
open Reducer_TestHelpers
describe("Parse for Bindings", () => {
testParseOuterToBe("x", "Ok((:$$bindExpression (:$$bindings) :x))")
testParseOuterToBe("x+1", "Ok((:$$bindExpression (:$$bindings) (:add :x 1)))")
testParseOuterToBe(
"y = x+1; y",
"Ok((:$$bindExpression (:$$bindStatement (:$$bindings) (:$let :y (:add :x 1))) :y))",
)
})
describe("Parse Partial", () => {
testParsePartialToBe(
"x",
"Ok((:$$bindExpression (:$$bindStatement (:$$bindings) :x) (:$exportVariablesExpression)))",
)
testParsePartialToBe(
"y=x",
"Ok((:$$bindExpression (:$$bindStatement (:$$bindings) (:$let :y :x)) (:$exportVariablesExpression)))",
)
testParsePartialToBe(
"y=x+1",
"Ok((:$$bindExpression (:$$bindStatement (:$$bindings) (:$let :y (:add :x 1))) (:$exportVariablesExpression)))",
)
testParsePartialToBe(
"y = x+1; z = y",
"Ok((:$$bindExpression (:$$bindStatement (:$$bindStatement (:$$bindings) (:$let :y (:add :x 1))) (:$let :z :y)) (:$exportVariablesExpression)))",
)
})
describe("Eval with Bindings", () => {
testEvalBindingsToBe("x", list{("x", ExpressionValue.EvNumber(1.))}, "Ok(1)")
testEvalBindingsToBe("x+1", list{("x", ExpressionValue.EvNumber(1.))}, "Ok(2)")
testEvalBindingsToBe("y = x+1; y", list{("x", ExpressionValue.EvNumber(1.))}, "Ok(2)")
})
/*
Partial code is a partial code fragment that is cut out from a larger code.
Therefore it does not end with an expression.
*/
describe("Eval Partial", () => {
testEvalPartialBindingsToBe(
// A partial cannot end with an expression
"x",
list{("x", ExpressionValue.EvNumber(1.))},
"Error(Assignment expected)",
)
testEvalPartialBindingsToBe("y=x", list{("x", ExpressionValue.EvNumber(1.))}, "Ok({x: 1, y: 1})")
testEvalPartialBindingsToBe(
"y=x+1",
list{("x", ExpressionValue.EvNumber(1.))},
"Ok({x: 1, y: 2})",
)
testEvalPartialBindingsToBe(
"y = x+1; z = y",
list{("x", ExpressionValue.EvNumber(1.))},
"Ok({x: 1, y: 2, z: 2})",
)
})

View File

@ -0,0 +1,12 @@
open Jest
open Reducer_TestHelpers
Skip.describe("Parse ternary operator", () => {
testParseToBe("true ? 'YES' : 'NO'", "Ok('YES')")
testParseToBe("false ? 'YES' : 'NO'", "Ok('NO')")
})
Skip.describe("Evaluate ternary operator", () => {
testEvalToBe("true ? 'YES' : 'NO'", "Ok('YES')")
testEvalToBe("false ? 'YES' : 'NO'", "Ok('NO')")
})

View File

@ -1,15 +1,6 @@
open Jest
open Reducer_TestHelpers
let testParseToBe = (expr, answer) => test(expr, () => expectParseToBe(expr, answer))
let testDescriptionParseToBe = (desc, expr, answer) =>
test(desc, () => expectParseToBe(expr, answer))
let testEvalToBe = (expr, answer) => test(expr, () => expectEvalToBe(expr, answer))
let testDescriptionEvalToBe = (desc, expr, answer) => test(desc, () => expectEvalToBe(expr, answer))
describe("reducer using mathjs parse", () => {
// Test the MathJs parser compatibility
// Those tests toString that there is a semantic mapping from MathJs to Expression

View File

@ -20,9 +20,10 @@ describe("eval on distribution functions", () => {
})
describe("unaryMinus", () => {
testEval("mean(-normal(5,2))", "Ok(-5)")
testEval("-normal(5,2)", "Ok(Normal(-5,2))")
})
describe("to", () => {
testEval("5 to 2", "Error(TODO: Low value must be less than high value.)")
testEval("5 to 2", "Error(Distribution Math Error: Low value must be less than high value.)")
testEval("to(2,5)", "Ok(Lognormal(1.1512925464970227,0.27853260523016377))")
testEval("to(-2,2)", "Ok(Normal(0,1.2159136638235384))")
})
@ -53,6 +54,7 @@ describe("eval on distribution functions", () => {
describe("subtract", () => {
testEval("10 - normal(5, 1)", "Ok(Normal(5,1))")
testEval("normal(5, 1) - 10", "Ok(Normal(-5,1))")
testEval("mean(1 - toPointSet(normal(5, 2)))", "Ok(-4.002309896304692)")
})
describe("multiply", () => {
testEval("normal(10, 2) * 2", "Ok(Normal(20,4))")
@ -67,7 +69,7 @@ describe("eval on distribution functions", () => {
testEval("lognormal(10,2) / lognormal(5,2)", "Ok(Lognormal(5,2.8284271247461903))")
testEval("lognormal(5, 2) / 2", "Ok(Lognormal(4.306852819440055,2))")
testEval("2 / lognormal(5, 2)", "Ok(Lognormal(-4.306852819440055,2))")
testEval("2 / normal(10, 2)", "Ok(Point Set Distribution)")
testEval("2 / normal(10, 2)", "Ok(Sample Set Distribution)")
testEval("normal(10, 2) / 2", "Ok(Normal(5,1))")
})
describe("truncate", () => {
@ -77,27 +79,27 @@ describe("eval on distribution functions", () => {
})
describe("exp", () => {
testEval("exp(normal(5,2))", "Ok(Point Set Distribution)")
testEval("exp(normal(5,2))", "Ok(Sample Set Distribution)")
})
describe("pow", () => {
testEval("pow(3, uniform(5,8))", "Ok(Point Set Distribution)")
testEval("pow(uniform(5,8), 3)", "Ok(Point Set Distribution)")
testEval("pow(3, uniform(5,8))", "Ok(Sample Set Distribution)")
testEval("pow(uniform(5,8), 3)", "Ok(Sample Set Distribution)")
testEval("pow(uniform(5,8), uniform(9, 10))", "Ok(Sample Set Distribution)")
})
describe("log", () => {
testEval("log(2, uniform(5,8))", "Ok(Point Set Distribution)")
testEval("log(normal(5,2), 3)", "Ok(Point Set Distribution)")
testEval("log(normal(5,2), normal(10,1))", "Ok(Sample Set Distribution)")
testEval("log(uniform(5,8))", "Ok(Point Set Distribution)")
testEval("log10(uniform(5,8))", "Ok(Point Set Distribution)")
})
describe("dotLog", () => {
testEval("dotLog(normal(5,2), 3)", "Ok(Point Set Distribution)")
testEval("dotLog(normal(5,2), 3)", "Ok(Point Set Distribution)")
testEval("dotLog(normal(5,2), normal(10,1))", "Ok(Point Set Distribution)")
testEval("log(2, uniform(5,8))", "Ok(Sample Set Distribution)")
testEval(
"log(normal(5,2), 3)",
"Error(Distribution Math Error: Logarithm of input error: First input must be completely greater than 0)",
)
testEval(
"log(normal(5,2), normal(10,1))",
"Error(Distribution Math Error: Logarithm of input error: First input must be completely greater than 0)",
)
testEval("log(uniform(5,8))", "Ok(Sample Set Distribution)")
testEval("log10(uniform(5,8))", "Ok(Sample Set Distribution)")
})
describe("dotAdd", () => {

View File

@ -4,7 +4,7 @@ import {
resultMap,
squiggleExpression,
errorValueToString,
} from "../src/js/index";
} from "../../src/js/index";
let testRun = (x: string): squiggleExpression => {
let result = run(x, { sampleCount: 100, xyPointLength: 100 });
@ -46,6 +46,8 @@ describe("Distribution", () => {
//It's important that sampleCount is less than 9. If it's more, than that will create randomness
//Also, note, the value should be created using makeSampleSetDist() later on.
let env = { sampleCount: 8, xyPointLength: 100 };
let dist1Samples = [3, 4, 5, 6, 6, 7, 10, 15, 30];
let dist1SampleCount = dist1Samples.length;
let dist = new Distribution(
{ tag: "SampleSet", value: [3, 4, 5, 6, 6, 7, 10, 15, 30] },
env
@ -56,16 +58,19 @@ describe("Distribution", () => {
);
test("mean", () => {
expect(dist.mean().value).toBeCloseTo(3.737);
expect(dist.mean().value).toBeCloseTo(9.5555555);
});
test("pdf", () => {
expect(dist.pdf(5.0).value).toBeCloseTo(0.0431);
expect(dist.pdf(5.0).value).toBeCloseTo(0.10499097598222966, 1);
});
test("cdf", () => {
expect(dist.cdf(5.0).value).toBeCloseTo(0.155);
expect(dist.cdf(5.0).value).toBeCloseTo(
dist1Samples.filter((x) => x <= 5).length / dist1SampleCount,
1
);
});
test("inv", () => {
expect(dist.inv(0.5).value).toBeCloseTo(9.458);
expect(dist.inv(0.5).value).toBeCloseTo(6);
});
test("toPointSet", () => {
expect(
@ -73,7 +78,7 @@ describe("Distribution", () => {
).toEqual(Ok("Point Set Distribution"));
});
test("toSparkline", () => {
expect(dist.toSparkline(20).value).toEqual("▁▁▃▅███▆▄▃▂▁▁▂▂▃▂▁▁▁");
expect(dist.toSparkline(20).value).toEqual("▁▁▃▇█▇▄▂▂▂▁▁▁▁▁▂▂▁▁▁");
});
test("algebraicAdd", () => {
expect(
@ -87,6 +92,6 @@ describe("Distribution", () => {
resultMap(dist.pointwiseAdd(dist2), (r: Distribution) =>
r.toSparkline(20)
).value
).toEqual(Ok("▁▂▅██▅▅▅▆▇█▆▅▃▃▂▂▁▁▁"));
).toEqual(Ok("▁▂██▃▃▃▃▄▅▄▃▃▂▂▂▁▁▁▁"));
});
});

View File

@ -0,0 +1,27 @@
// import { errorValueToString } from "../../src/js/index";
import * as fc from "fast-check";
import { testRun } from "./TestHelpers";
describe("cumulative density function of a normal distribution", () => {
test("at 3 stdevs to the right of the mean is near 1", () => {
fc.assert(
fc.property(fc.float(), fc.float({ min: 1e-7 }), (mean, stdev) => {
let threeStdevsAboveMean = mean + 3 * stdev;
let squiggleString = `cdf(normal(${mean}, ${stdev}), ${threeStdevsAboveMean})`;
let squiggleResult = testRun(squiggleString);
expect(squiggleResult.value).toBeCloseTo(1);
})
);
});
test("at 3 stdevs to the left of the mean is near 0", () => {
fc.assert(
fc.property(fc.float(), fc.float({ min: 1e-7 }), (mean, stdev) => {
let threeStdevsBelowMean = mean - 3 * stdev;
let squiggleString = `cdf(normal(${mean}, ${stdev}), ${threeStdevsBelowMean})`;
let squiggleResult = testRun(squiggleString);
expect(squiggleResult.value).toBeCloseTo(0);
})
);
});
});

View File

@ -0,0 +1,52 @@
import {
run,
squiggleExpression,
errorValue,
result,
} from "../../src/js/index";
import { testRun } from "./TestHelpers";
import * as fc from "fast-check";
describe("Squiggle's parser is whitespace insensitive", () => {
test("when assigning a distribution to a name and calling that name", () => {
// intersperse varying amounts of whitespace in a squiggle string
let squiggleString = (
a: string,
b: string,
c: string,
d: string,
e: string,
f: string,
g: string,
h: string
): string => {
return `theDist${a}=${b}beta(${c}4${d},${e}5e1)${f};${g}theDist${h}`;
};
let squiggleOutput = testRun(
squiggleString("", "", "", "", "", "", "", "")
);
// Add "\n" to this when multiline is introduced.
let whitespaceGen = () => {
return fc.constantFrom("", " ", "\t", " ", " ", " ", " ");
};
fc.assert(
fc.property(
whitespaceGen(),
whitespaceGen(),
whitespaceGen(),
whitespaceGen(),
whitespaceGen(),
whitespaceGen(),
whitespaceGen(),
whitespaceGen(),
(a, b, c, d, e, f, g, h) => {
expect(testRun(squiggleString(a, b, c, d, e, f, g, h))).toEqual(
squiggleOutput
);
}
)
);
});
});

View File

@ -0,0 +1,37 @@
// import { errorValueToString } from "../../src/js/index";
import { testRun, expectErrorToBeBounded } from "./TestHelpers";
import * as fc from "fast-check";
describe("Mean of mixture is weighted average of means", () => {
test.skip("mx(beta(a,b), lognormal(m,s), [x,y])", () => {
fc.assert(
fc.property(
fc.float({ min: 1e-1 }), // alpha
fc.float({ min: 1 }), // beta
fc.float(), // mu
fc.float({ min: 1e-1 }), // sigma
fc.float({ min: 1e-7 }),
fc.float({ min: 1e-7 }),
(a, b, m, s, x, y) => {
let squiggleString = `mean(mixture(beta(${a},${b}), lognormal(${m},${s}), [${x}, ${y}]))`;
let res = testRun(squiggleString);
let weightDenom = x + y;
let betaWeight = x / weightDenom;
let lognormalWeight = y / weightDenom;
let betaMean = 1 / (1 + b / a);
let lognormalMean = m + s ** 2 / 2;
if (res.tag == "number") {
expectErrorToBeBounded(
res.value,
betaWeight * betaMean + lognormalWeight * lognormalMean,
1,
2
);
} else {
expect(res.value).toEqual("some error message");
}
}
)
);
});
});

View File

@ -0,0 +1,214 @@
import { Distribution } from "../../src/js/index";
import { expectErrorToBeBounded, failDefault } from "./TestHelpers";
import * as fc from "fast-check";
// Beware: float64Array makes it appear in an infinite loop.
let arrayGen = () =>
fc.float32Array({
minLength: 10,
maxLength: 10000,
noDefaultInfinity: true,
noNaN: true,
});
describe("cumulative density function", () => {
let n = 10000;
// We should fix this.
test.skip("'s codomain is bounded above", () => {
fc.assert(
fc.property(arrayGen(), fc.float(), (xs_, x) => {
let xs = Array.from(xs_);
// Should compute with squiggle strings once interpreter has `sample`
let dist = new Distribution(
{ tag: "SampleSet", value: xs },
{ sampleCount: n, xyPointLength: 100 }
);
let cdfValue = dist.cdf(x).value;
let epsilon = 5e-7;
expect(cdfValue).toBeLessThanOrEqual(1 + epsilon);
})
);
});
test("'s codomain is bounded below", () => {
fc.assert(
fc.property(arrayGen(), fc.float(), (xs_, x) => {
let xs = Array.from(xs_);
// Should compute with squiggle strings once interpreter has `sample`
let dist = new Distribution(
{ tag: "SampleSet", value: xs },
{ sampleCount: n, xyPointLength: 100 }
);
let cdfValue = dist.cdf(x).value;
expect(cdfValue).toBeGreaterThanOrEqual(0);
})
);
});
// This may not be true due to KDE estimating there to be mass above the
// highest value. These tests fail
test.skip("at the highest number in the sample is close to 1", () => {
fc.assert(
fc.property(arrayGen(), (xs_) => {
let xs = Array.from(xs_);
let max = Math.max(...xs);
// Should compute with squiggle strings once interpreter has `sample`
let dist = new Distribution(
{ tag: "SampleSet", value: xs },
{ sampleCount: n, xyPointLength: 100 }
);
let cdfValue = dist.cdf(max).value;
expect(cdfValue).toBeCloseTo(1.0, 2);
})
);
});
// I may simply be mistaken about the math here.
test.skip("at the lowest number in the distribution is within epsilon of 0", () => {
fc.assert(
fc.property(arrayGen(), (xs_) => {
let xs = Array.from(xs_);
let min = Math.min(...xs);
// Should compute with squiggle strings once interpreter has `sample`
let dist = new Distribution(
{ tag: "SampleSet", value: xs },
{ sampleCount: n, xyPointLength: 100 }
);
let cdfValue = dist.cdf(min).value;
let max = Math.max(...xs);
let epsilon = 5e-3;
if (max - min < epsilon) {
expect(cdfValue).toBeGreaterThan(4 * epsilon);
} else {
expect(cdfValue).toBeLessThan(4 * epsilon);
}
})
);
});
// I believe this is true, but due to bugs can't get the test to pass.
test.skip("is <= 1 everywhere with equality when x is higher than the max", () => {
fc.assert(
fc.property(arrayGen(), fc.float(), (xs_, x) => {
let xs = Array.from(xs_);
let dist = new Distribution(
{ tag: "SampleSet", value: xs },
{ sampleCount: n, xyPointLength: 100 }
);
let cdfValue = dist.cdf(x).value;
let max = Math.max(...xs);
if (x > max) {
let epsilon = (x - max) / x;
expect(cdfValue).toBeGreaterThan(1 * (1 - epsilon));
} else if (typeof cdfValue == "number") {
expect(Math.round(1e5 * cdfValue) / 1e5).toBeLessThanOrEqual(1);
} else {
failDefault();
}
})
);
});
test("is non-negative everywhere with zero when x is lower than the min", () => {
fc.assert(
fc.property(arrayGen(), fc.float(), (xs_, x) => {
let xs = Array.from(xs_);
let dist = new Distribution(
{ tag: "SampleSet", value: xs },
{ sampleCount: n, xyPointLength: 100 }
);
let cdfValue = dist.cdf(x).value;
if (x < Math.min(...xs)) {
expect(cdfValue).toEqual(0);
} else {
expect(cdfValue).toBeGreaterThan(0);
}
})
);
});
});
// I no longer believe this is true.
describe("probability density function", () => {
let n = 1000;
test.skip("assigns to the max at most the weight of the mean", () => {
fc.assert(
fc.property(arrayGen(), (xs_) => {
let xs = Array.from(xs_);
let max = Math.max(...xs);
let mean = xs.reduce((a, b) => a + b, 0.0) / xs.length;
// Should be from squiggleString once interpreter exposes sampleset
let dist = new Distribution(
{ tag: "SampleSet", value: xs },
{ sampleCount: n, xyPointLength: 100 }
);
let pdfValueMean = dist.pdf(mean).value;
let pdfValueMax = dist.pdf(max).value;
if (typeof pdfValueMean == "number" && typeof pdfValueMax == "number") {
expect(pdfValueMax).toBeLessThanOrEqual(pdfValueMean);
} else {
expect(pdfValueMax).toEqual(pdfValueMean);
}
})
);
});
});
// // This should be true, but I can't get it to work.
describe("mean is mean", () => {
test.skip("when sampling twice as widely as the input", () => {
fc.assert(
fc.property(
fc.float64Array({ minLength: 10, maxLength: 100000 }),
(xs_) => {
let xs = Array.from(xs_);
let n = xs.length;
let dist = new Distribution(
{ tag: "SampleSet", value: xs },
{ sampleCount: 2 * n, xyPointLength: 4 * n }
);
let mean = dist.mean();
if (typeof mean.value == "number") {
expectErrorToBeBounded(
mean.value,
xs.reduce((a, b) => a + b, 0.0) / n,
5e-1,
1
);
} else {
failDefault();
}
}
)
);
});
test.skip("when sampling half as widely as the input", () => {
fc.assert(
fc.property(
fc.float64Array({ minLength: 10, maxLength: 100000 }),
(xs_) => {
let xs = Array.from(xs_);
let n = xs.length;
let dist = new Distribution(
{ tag: "SampleSet", value: xs },
{ sampleCount: Math.floor(n / 2), xyPointLength: 4 * n }
);
let mean = dist.mean();
if (typeof mean.value == "number") {
expectErrorToBeBounded(
mean.value,
xs.reduce((a, b) => a + b, 0.0) / n,
5e-1,
1
);
} else {
failDefault();
}
}
)
);
});
});

View File

@ -0,0 +1,33 @@
// import { errorValueToString } from "../../src/js/index";
import { testRun } from "./TestHelpers";
import * as fc from "fast-check";
describe("Scalar manipulation is well-modeled by javascript math", () => {
test("in the case of natural logarithms", () => {
fc.assert(
fc.property(fc.float(), (x) => {
let squiggleString = `log(${x})`;
let squiggleResult = testRun(squiggleString);
if (x == 0) {
expect(squiggleResult.value).toEqual(-Infinity);
} else if (x < 0) {
expect(squiggleResult.value).toEqual(
"somemessage (confused why a test case hasn't pointed out to me that this message is bogus)"
);
} else {
expect(squiggleResult.value).toEqual(Math.log(x));
}
})
);
});
test("in the case of addition (with assignment)", () => {
fc.assert(
fc.property(fc.float(), fc.float(), fc.float(), (x, y, z) => {
let squiggleString = `x = ${x}; y = ${y}; z = ${z}; x + y + z`;
let squiggleResult = testRun(squiggleString);
expect(squiggleResult.value).toBeCloseTo(x + y + z);
})
);
});
});

View File

@ -0,0 +1,22 @@
import { errorValueToString } from "../../src/js/index";
import { testRun } from "./TestHelpers";
import * as fc from "fast-check";
describe("Symbolic mean", () => {
test("mean(triangular(x,y,z))", () => {
fc.assert(
fc.property(fc.float(), fc.float(), fc.float(), (x, y, z) => {
if (!(x < y && y < z)) {
try {
let squiggleResult = testRun(`mean(triangular(${x},${y},${z}))`);
expect(squiggleResult.value).toBeCloseTo((x + y + z) / 3);
} catch (err) {
expect((err as Error).message).toEqual(
"Expected squiggle expression to evaluate but got error: Distribution Math Error: Triangular values must be increasing order."
);
}
}
})
);
});
});

View File

@ -0,0 +1,45 @@
import {
run,
// Distribution,
squiggleExpression,
errorValueToString,
// errorValue,
// result,
} from "../../src/js/index";
export function testRun(x: string): squiggleExpression {
let squiggleResult = run(x, { sampleCount: 1000, xyPointLength: 100 });
// return squiggleResult.value
if (squiggleResult.tag === "Ok") {
return squiggleResult.value;
} else {
throw new Error(
`Expected squiggle expression to evaluate but got error: ${errorValueToString(
squiggleResult.value
)}`
);
}
}
export function failDefault() {
expect("be reached").toBe("codepath should never");
}
/**
* This appears also in `TestHelpers.res`. According to https://www.math.net/percent-error, it computes
* absolute error when numerical stability concerns make me not want to compute relative error.
* */
export function expectErrorToBeBounded(
received: number,
expected: number,
epsilon: number,
digits: number
) {
let distance = Math.abs(received - expected);
let expectedAbs = Math.abs(expected);
let normalizingDenom = Math.max(expectedAbs, 1);
let error = distance / normalizingDenom;
expect(Math.round(10 ** digits * error) / 10 ** digits).toBeLessThanOrEqual(
epsilon
);
}

View File

@ -30,8 +30,8 @@ let {toFloat, toDist, toString, toError, fmap} = module(DistributionOperation.Ou
let fnImage = (theFn, inps) => Js.Array.map(theFn, inps)
let env: DistributionOperation.env = {
sampleCount: 10000,
xyPointLength: 1000,
sampleCount: MagicNumbers.Environment.defaultSampleCount,
xyPointLength: MagicNumbers.Environment.defaultXYPointLength,
}
let run = DistributionOperation.run(~env)

View File

@ -0,0 +1,46 @@
import { distributions, generateInt, generateFloatRange } from "./generators";
import { test, expectEqual } from "./lib";
let checkDistributionSame = (
distribution: string,
operation: (arg: string) => string
): void => {
expectEqual(
operation(distribution),
operation(`toPointSet(${distribution})`)
);
expectEqual(
operation(distribution),
operation(`toSampleSet(${distribution})`)
);
};
Object.entries(distributions).map(([key, generator]) => {
let distribution = generator();
test(`mean is the same for ${key} distribution under all distribution types`, () =>
checkDistributionSame(distribution, (d: string) => `mean(${d})`));
test(`cdf is the same for ${key} distribution under all distribution types`, () => {
let cdf_value = generateInt();
checkDistributionSame(
distribution,
(d: string) => `cdf(${d}, ${cdf_value})`
);
});
test(`pdf is the same for ${key} distribution under all distribution types`, () => {
let pdf_value = generateInt();
checkDistributionSame(
distribution,
(d: string) => `pdf(${d}, ${pdf_value})`
);
});
test(`inv is the same for ${key} distribution under all distribution types`, () => {
let inv_value = generateFloatRange(0, 1);
checkDistributionSame(
distribution,
(d: string) => `inv(${d}, ${inv_value})`
);
});
});

View File

@ -0,0 +1,48 @@
export let generateFloatRange = (min: number, max: number): number =>
Math.random() * (max - min) + min;
export let generateIntRange = (min: number, max: number): number =>
Math.floor(generateFloatRange(min, max));
export let generateIntMin = (min: number): number => generateIntRange(min, 100);
export let generateInt = (): number => generateIntMin(-100);
let generatePositiveInt = (): number => generateIntMin(1);
export let generateNormal = (): string =>
`normal(${generateInt()}, ${generatePositiveInt()})`;
export let generateBeta = (): string =>
`beta(${generatePositiveInt()}, ${generatePositiveInt()})`;
export let generateLognormal = (): string =>
`lognormal(${generateInt()}, ${generatePositiveInt()})`;
export let generateExponential = (): string =>
`exponential(${generatePositiveInt()})`;
export let generateUniform = (): string => {
let a = generateInt();
let b = generateIntMin(a + 1);
return `uniform(${a}, ${b})`;
};
export let generateCauchy = (): string => {
return `cauchy(${generateInt()}, ${generatePositiveInt()})`;
};
export let generateTriangular = (): string => {
let a = generateInt();
let b = generateIntMin(a + 1);
let c = generateIntMin(b + 1);
return `triangular(${a}, ${b}, ${c})`;
};
export let distributions: { [key: string]: () => string } = {
normal: generateNormal,
beta: generateBeta,
lognormal: generateLognormal,
exponential: generateExponential,
triangular: generateTriangular,
cauchy: generateCauchy,
uniform: generateUniform,
};

View File

@ -0,0 +1,42 @@
import { run, squiggleExpression, errorValueToString } from "../src/js/index";
import * as chalk from "chalk";
let testRun = (x: string): squiggleExpression => {
let result = run(x, { sampleCount: 100, xyPointLength: 100 });
if (result.tag === "Ok") {
return result.value;
} else {
throw Error(
"Expected squiggle expression to evaluate but got error: " +
errorValueToString(result.value)
);
}
};
export function test(name: string, fn: () => void) {
console.log(chalk.cyan.bold(name));
fn();
}
export function expectEqual(expression1: string, expression2: string) {
let result1 = testRun(expression1);
let result2 = testRun(expression2);
if (result1.tag === "number" && result2.tag === "number") {
let logloss = Math.log(Math.abs(result1.value - result2.value));
let isBadLogless = logloss > 1;
console.log(chalk.blue(`${expression1} = ${expression2}`));
console.log(`${result1.value} = ${result2.value}`);
console.log(
`logloss = ${
isBadLogless
? chalk.red(logloss.toFixed(2))
: chalk.green(logloss.toFixed(2))
}`
);
console.log();
} else {
throw Error(
`Expected both to be number, but got ${result1.tag} and ${result2.tag}`
);
}
}

View File

@ -20,12 +20,7 @@
],
"suffix": ".bs.js",
"namespace": true,
"bs-dependencies": [
"@glennsl/rescript-jest",
"@glennsl/bs-json",
"rationale",
"bisect_ppx"
],
"bs-dependencies": ["@glennsl/rescript-jest", "bisect_ppx"],
"gentypeconfig": {
"language": "typescript",
"module": "commonjs",
@ -37,7 +32,7 @@
},
"refmt": 3,
"warnings": {
"number": "+A-42-48-9-30-4-102-20-27-41"
"number": "+A-42-48-9-30-4"
},
"ppx-flags": [
["../../node_modules/bisect_ppx/ppx", "--exclude-files", ".*_test\\.res$$"]

View File

@ -9,5 +9,6 @@ module.exports = {
".*Fixtures.bs.js",
"/node_modules/",
".*Helpers.bs.js",
".*Helpers.ts",
],
};

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Hat tip to @dfalling
# https://forum.rescript-lang.org/t/rescript-9-1-how-can-we-format-to-standard-out/1590/2?u=quinn-dougherty

View File

@ -1,22 +1,29 @@
{
"name": "@quri/squiggle-lang",
"version": "0.2.2",
"version": "0.2.5",
"homepage": "https://squiggle-language.com",
"licence": "MIT",
"scripts": {
"build": "rescript build -with-deps",
"bundle": "webpack",
"start": "rescript build -w -with-deps",
"clean": "rescript clean",
"test:reducer": "jest --testPathPattern '.*__tests__/Reducer.*'",
"test:reducer": "jest __tests__/Reducer*/",
"benchmark": "ts-node benchmark/conversion_tests.ts",
"test": "jest",
"test:ts": "jest __tests__/TS/",
"test:rescript": "jest --modulePathIgnorePatterns=__tests__/TS/*",
"test:watch": "jest --watchAll",
"test:quick": "jest --modulePathIgnorePatterns=__tests__/Distributions/Invariants/*",
"coverage": "rm -f *.coverage; yarn clean; BISECT_ENABLE=yes yarn build; yarn test; bisect-ppx-report html",
"coverage:ci": "yarn clean; BISECT_ENABLE=yes yarn build; yarn test; bisect-ppx-report send-to Codecov",
"coverage:rescript": "rm -f *.coverage; yarn clean; BISECT_ENABLE=yes yarn build; yarn test:rescript; bisect-ppx-report html",
"coverage:ts": "yarn clean; yarn build; nyc --reporter=lcov yarn test:ts",
"coverage:rescript:ci": "yarn clean; BISECT_ENABLE=yes yarn build; yarn test:rescript; bisect-ppx-report send-to Codecov",
"coverage:ts:ci": "yarn coverage:ts && codecov",
"lint:rescript": "./lint.sh",
"lint:prettier": "prettier --check .",
"lint": "yarn lint:rescript && yarn lint:prettier",
"format": "rescript format -all && prettier --write .",
"format:rescript": "rescript format -all",
"format:prettier": "prettier --write .",
"format": "yarn format:rescript && yarn format:prettier",
"all": "yarn build && yarn bundle && yarn test"
},
"keywords": [
@ -24,26 +31,29 @@
],
"author": "Quantified Uncertainty Research Institute",
"license": "MIT",
"dependencies": {
"@glennsl/bs-json": "^5.0.2",
"devDependencies": {
"bisect_ppx": "^2.7.1",
"jstat": "^1.9.5",
"lodash": "4.17.21",
"mathjs": "10.4.3",
"pdfast": "^0.2.0",
"rationale": "0.2.0",
"rescript": "^9.1.4",
"bisect_ppx": "^2.7.1"
},
"devDependencies": {
"rescript-fast-check": "^1.1.1",
"@glennsl/rescript-jest": "^0.9.0",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@types/jest": "^27.4.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"docsify": "^4.12.2",
"chalk": "^5.0.1",
"codecov": "3.8.3",
"fast-check": "2.25.0",
"gentype": "^4.3.0",
"jest": "^27.5.1",
"mathjs": "10.5.0",
"moduleserve": "0.9.1",
"nyc": "^15.1.0",
"pdfast": "^0.2.0",
"reanalyze": "^2.19.0",
"ts-jest": "^27.1.4",
"ts-loader": "^9.2.8",
"ts-node": "^10.7.0",
"typescript": "^4.6.3",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2"

View File

@ -1,9 +1,4 @@
import * as _ from "lodash";
import type {
exportEnv,
exportDistribution,
} from "../rescript/ProgramEvaluator.gen";
export type { exportEnv, exportDistribution };
import {
genericDist,
samplingParams,
@ -48,7 +43,6 @@ import {
Constructors_pointwiseLogarithm,
Constructors_pointwisePower,
} from "../rescript/Distributions/DistributionOperation/DistributionOperation.gen";
import { pointSetDistFn } from "../rescript/OldInterpreter/DistPlus.bs";
export type { samplingParams, errorValue };
export let defaultSamplingInputs: samplingParams = {
@ -98,8 +92,7 @@ export type squiggleExpression =
| tagged<"record", { [key: string]: squiggleExpression }>;
export function run(
squiggleString: string,
samplingInputs?: samplingParams,
_environment?: exportEnv
samplingInputs?: samplingParams
): result<squiggleExpression, errorValue> {
let si: samplingParams = samplingInputs
? samplingInputs

View File

@ -1,4 +1,4 @@
type functionCallInfo = GenericDist_Types.Operation.genericFunctionCallInfo
type functionCallInfo = DistributionTypes.DistributionOperation.genericFunctionCallInfo
type genericDist = DistributionTypes.genericDist
type error = DistributionTypes.error
@ -120,7 +120,10 @@ let rec run = (~env, functionCallInfo: functionCallInfo): outputType => {
(),
)->OutputLocal.toDistR
let fromDistFn = (subFnName: GenericDist_Types.Operation.fromDist, dist: genericDist) => {
let fromDistFn = (
subFnName: DistributionTypes.DistributionOperation.fromDist,
dist: genericDist,
) => {
let response = switch subFnName {
| ToFloat(distToFloatOperation) =>
GenericDist.toFloatOperation(dist, ~toPointSetFn, ~distToFloatOperation)
@ -151,20 +154,26 @@ let rec run = (~env, functionCallInfo: functionCallInfo): outputType => {
->GenericDist.toPointSet(~xyPointLength, ~sampleCount, ())
->E.R2.fmap(r => Dist(PointSet(r)))
->OutputLocal.fromResult
| ToDistCombination(Algebraic, _, #Float(_)) => GenDistError(NotYetImplemented)
| ToDistCombination(Algebraic, arithmeticOperation, #Dist(t2)) =>
| ToDistCombination(Algebraic(_), _, #Float(_)) => GenDistError(NotYetImplemented)
| ToDistCombination(Algebraic(strategy), arithmeticOperation, #Dist(t2)) =>
dist
->GenericDist.algebraicCombination(~toPointSetFn, ~toSampleSetFn, ~arithmeticOperation, ~t2)
->GenericDist.algebraicCombination(
~strategy,
~toPointSetFn,
~toSampleSetFn,
~arithmeticOperation,
~t2,
)
->E.R2.fmap(r => Dist(r))
->OutputLocal.fromResult
| ToDistCombination(Pointwise, arithmeticOperation, #Dist(t2)) =>
| ToDistCombination(Pointwise, algebraicCombination, #Dist(t2)) =>
dist
->GenericDist.pointwiseCombination(~toPointSetFn, ~arithmeticOperation, ~t2)
->GenericDist.pointwiseCombination(~toPointSetFn, ~algebraicCombination, ~t2)
->E.R2.fmap(r => Dist(r))
->OutputLocal.fromResult
| ToDistCombination(Pointwise, arithmeticOperation, #Float(float)) =>
| ToDistCombination(Pointwise, algebraicCombination, #Float(f)) =>
dist
->GenericDist.pointwiseCombinationFloat(~toPointSetFn, ~arithmeticOperation, ~float)
->GenericDist.pointwiseCombinationFloat(~toPointSetFn, ~algebraicCombination, ~f)
->E.R2.fmap(r => Dist(r))
->OutputLocal.fromResult
}
@ -192,24 +201,24 @@ module Output = {
let fmap = (
~env,
input: outputType,
functionCallInfo: GenericDist_Types.Operation.singleParamaterFunction,
functionCallInfo: DistributionTypes.DistributionOperation.singleParamaterFunction,
): outputType => {
let newFnCall: result<functionCallInfo, error> = switch (functionCallInfo, input) {
| (FromDist(fromDist), Dist(o)) => Ok(FromDist(fromDist, o))
| (FromFloat(fromDist), Float(o)) => Ok(FromFloat(fromDist, o))
| (_, GenDistError(r)) => Error(r)
| (FromDist(_), _) => Error(Other("Expected dist, got something else"))
| (FromFloat(_), _) => Error(Other("Expected float, got something else"))
| (FromDist(_), _) => Error(OtherError("Expected dist, got something else"))
| (FromFloat(_), _) => Error(OtherError("Expected float, got something else"))
}
newFnCall->E.R2.fmap(run(~env))->OutputLocal.fromResult
}
}
// See comment above GenericDist_Types.Constructors to explain the purpose of this module.
// See comment above DistributionTypes.Constructors to explain the purpose of this module.
// I tried having another internal module called UsingDists, similar to how its done in
// GenericDist_Types.Constructors. However, this broke GenType for me, so beware.
// DistributionTypes.Constructors. However, this broke GenType for me, so beware.
module Constructors = {
module C = GenericDist_Types.Constructors.UsingDists
module C = DistributionTypes.Constructors.UsingDists
open OutputLocal
let mean = (~env, dist) => C.mean(dist)->run(~env)->toFloatR
let sample = (~env, dist) => C.sample(dist)->run(~env)->toFloatR

View File

@ -4,7 +4,7 @@ type env = {
xyPointLength: int,
}
open GenericDist_Types
open DistributionTypes
@genType
type outputType =
@ -15,15 +15,15 @@ type outputType =
| GenDistError(error)
@genType
let run: (~env: env, GenericDist_Types.Operation.genericFunctionCallInfo) => outputType
let run: (~env: env, DistributionTypes.DistributionOperation.genericFunctionCallInfo) => outputType
let runFromDist: (
~env: env,
~functionCallInfo: GenericDist_Types.Operation.fromDist,
~functionCallInfo: DistributionTypes.DistributionOperation.fromDist,
genericDist,
) => outputType
let runFromFloat: (
~env: env,
~functionCallInfo: GenericDist_Types.Operation.fromDist,
~functionCallInfo: DistributionTypes.DistributionOperation.fromDist,
float,
) => outputType
@ -38,7 +38,7 @@ module Output: {
let toBool: t => option<bool>
let toBoolR: t => result<bool, error>
let toError: t => option<error>
let fmap: (~env: env, t, GenericDist_Types.Operation.singleParamaterFunction) => t
let fmap: (~env: env, t, DistributionTypes.DistributionOperation.singleParamaterFunction) => t
}
module Constructors: {

View File

@ -4,38 +4,62 @@ type genericDist =
| SampleSet(SampleSetDist.t)
| Symbolic(SymbolicDistTypes.symbolicDist)
type asAlgebraicCombinationStrategy = AsDefault | AsSymbolic | AsMonteCarlo | AsConvolution
@genType
type error =
| NotYetImplemented
| Unreachable
| DistributionVerticalShiftIsInvalid
| TooFewSamples
| ArgumentError(string)
| Other(string)
| OperationError(Operation.Error.t)
| PointSetConversionError(SampleSetDist.pointsetConversionError)
| SparklineError(PointSetTypes.sparklineError) // This type of error is for when we find a sparkline of a discrete distribution. This should probably at some point be actually implemented
| RequestedStrategyInvalidError(string)
| LogarithmOfDistributionError(string)
| OtherError(string)
module Operation = {
type direction =
| Algebraic
| Pointwise
@genType
module Error = {
type t = error
type arithmeticOperation = [
| #Add
| #Multiply
| #Subtract
| #Divide
| #Power
| #Logarithm
]
let fromString = (s: string): t => OtherError(s)
let arithmeticToFn = (arithmetic: arithmeticOperation) =>
switch arithmetic {
| #Add => \"+."
| #Multiply => \"*."
| #Subtract => \"-."
| #Power => \"**"
| #Divide => \"/."
| #Logarithm => (a, b) => log(a) /. log(b)
@genType
let toString = (err: error): string =>
switch err {
| NotYetImplemented => "Function Not Yet Implemented"
| Unreachable => "Unreachable"
| DistributionVerticalShiftIsInvalid => "Distribution Vertical Shift is Invalid"
| ArgumentError(s) => `Argument Error ${s}`
| LogarithmOfDistributionError(s) => `Logarithm of input error: ${s}`
| TooFewSamples => "Too Few Samples"
| OperationError(err) => Operation.Error.toString(err)
| PointSetConversionError(err) => SampleSetDist.pointsetConversionErrorToString(err)
| SparklineError(err) => PointSetTypes.sparklineErrorToString(err)
| RequestedStrategyInvalidError(err) => `Requested strategy invalid: ${err}`
| OtherError(s) => s
}
let resultStringToResultError: result<'a, string> => result<'a, error> = n =>
n->E.R2.errMap(r => r->fromString)
let sampleErrorToDistErr = (err: SampleSetDist.sampleSetError): error =>
switch err {
| TooFewSamples => TooFewSamples
}
}
@genType
module DistributionOperation = {
@genType
type pointsetXSelection = [#Linear | #ByWeight]
type direction =
| Algebraic(asAlgebraicCombinationStrategy)
| Pointwise
type toFloat = [
| #Cdf(float)
| #Inv(float)
@ -43,9 +67,7 @@ module Operation = {
| #Mean
| #Sample
]
}
module DistributionOperation = {
type toDist =
| Normalize
| ToPointSet
@ -55,15 +77,18 @@ module DistributionOperation = {
type toFloatArray = Sample(int)
type fromDist =
| ToFloat(Operation.toFloat)
| ToDist(toDist)
| ToDistCombination(
Operation.direction,
Operation.arithmeticOperation,
[#Dist(genericDist) | #Float(float)],
)
type toBool = IsNormalized
type toString =
| ToString
| ToSparkline(int)
type fromDist =
| ToFloat(toFloat)
| ToDist(toDist)
| ToDistCombination(direction, Operation.Algebraic.t, [#Dist(genericDist) | #Float(float)])
| ToString(toString)
| ToBool(toBool)
type singleParamaterFunction =
| FromDist(fromDist)
@ -86,8 +111,10 @@ module DistributionOperation = {
| ToDist(ToSampleSet(r)) => `toSampleSet(${E.I.toString(r)})`
| ToDist(Truncate(_, _)) => `truncate`
| ToDist(Inspect) => `inspect`
| ToString => `toString`
| ToDistCombination(Algebraic, _, _) => `algebraic`
| ToString(ToString) => `toString`
| ToString(ToSparkline(n)) => `toSparkline(${E.I.toString(n)})`
| ToBool(IsNormalized) => `isNormalized`
| ToDistCombination(Algebraic(_), _, _) => `algebraic`
| ToDistCombination(Pointwise, _, _) => `pointwise`
}
@ -97,3 +124,71 @@ module DistributionOperation = {
| Mixture(_) => `mixture`
}
}
module Constructors = {
type t = DistributionOperation.genericFunctionCallInfo
module UsingDists = {
@genType
let mean = (dist): t => FromDist(ToFloat(#Mean), dist)
let sample = (dist): t => FromDist(ToFloat(#Sample), dist)
let cdf = (dist, x): t => FromDist(ToFloat(#Cdf(x)), dist)
let inv = (dist, x): t => FromDist(ToFloat(#Inv(x)), dist)
let pdf = (dist, x): t => FromDist(ToFloat(#Pdf(x)), dist)
let normalize = (dist): t => FromDist(ToDist(Normalize), dist)
let isNormalized = (dist): t => FromDist(ToBool(IsNormalized), dist)
let toPointSet = (dist): t => FromDist(ToDist(ToPointSet), dist)
let toSampleSet = (dist, r): t => FromDist(ToDist(ToSampleSet(r)), dist)
let truncate = (dist, left, right): t => FromDist(ToDist(Truncate(left, right)), dist)
let inspect = (dist): t => FromDist(ToDist(Inspect), dist)
let toString = (dist): t => FromDist(ToString(ToString), dist)
let toSparkline = (dist, n): t => FromDist(ToString(ToSparkline(n)), dist)
let algebraicAdd = (dist1, dist2: genericDist): t => FromDist(
ToDistCombination(Algebraic(AsDefault), #Add, #Dist(dist2)),
dist1,
)
let algebraicMultiply = (dist1, dist2): t => FromDist(
ToDistCombination(Algebraic(AsDefault), #Multiply, #Dist(dist2)),
dist1,
)
let algebraicDivide = (dist1, dist2): t => FromDist(
ToDistCombination(Algebraic(AsDefault), #Divide, #Dist(dist2)),
dist1,
)
let algebraicSubtract = (dist1, dist2): t => FromDist(
ToDistCombination(Algebraic(AsDefault), #Subtract, #Dist(dist2)),
dist1,
)
let algebraicLogarithm = (dist1, dist2): t => FromDist(
ToDistCombination(Algebraic(AsDefault), #Logarithm, #Dist(dist2)),
dist1,
)
let algebraicPower = (dist1, dist2): t => FromDist(
ToDistCombination(Algebraic(AsDefault), #Power, #Dist(dist2)),
dist1,
)
let pointwiseAdd = (dist1, dist2): t => FromDist(
ToDistCombination(Pointwise, #Add, #Dist(dist2)),
dist1,
)
let pointwiseMultiply = (dist1, dist2): t => FromDist(
ToDistCombination(Pointwise, #Multiply, #Dist(dist2)),
dist1,
)
let pointwiseDivide = (dist1, dist2): t => FromDist(
ToDistCombination(Pointwise, #Divide, #Dist(dist2)),
dist1,
)
let pointwiseSubtract = (dist1, dist2): t => FromDist(
ToDistCombination(Pointwise, #Subtract, #Dist(dist2)),
dist1,
)
let pointwiseLogarithm = (dist1, dist2): t => FromDist(
ToDistCombination(Pointwise, #Logarithm, #Dist(dist2)),
dist1,
)
let pointwisePower = (dist1, dist2): t => FromDist(
ToDistCombination(Pointwise, #Power, #Dist(dist2)),
dist1,
)
}
}

View File

@ -6,6 +6,24 @@ type toSampleSetFn = t => result<SampleSetDist.t, error>
type scaleMultiplyFn = (t, float) => result<t, error>
type pointwiseAddFn = (t, t) => result<t, error>
let isPointSet = (t: t) =>
switch t {
| PointSet(_) => true
| _ => false
}
let isSampleSetSet = (t: t) =>
switch t {
| SampleSet(_) => true
| _ => false
}
let isSymbolic = (t: t) =>
switch t {
| Symbolic(_) => true
| _ => false
}
let sampleN = (t: t, n) =>
switch t {
| PointSet(r) => PointSetDist.sampleNRendered(n, r)
@ -14,7 +32,7 @@ let sampleN = (t: t, n) =>
}
let toSampleSetDist = (t: t, n) =>
SampleSetDist.make(sampleN(t, n))->GenericDist_Types.Error.resultStringToResultError
SampleSetDist.make(sampleN(t, n))->E.R2.errMap(DistributionTypes.Error.sampleErrorToDistErr)
let fromFloat = (f: float): t => Symbolic(SymbolicDist.Float.make(f))
@ -46,18 +64,25 @@ let toFloatOperation = (
~toPointSetFn: toPointSetFn,
~distToFloatOperation: Operation.distToFloatOperation,
) => {
let symbolicSolution = switch (t: t) {
| Symbolic(r) =>
switch SymbolicDist.T.operate(distToFloatOperation, r) {
| Ok(f) => Some(f)
| _ => None
}
let trySymbolicSolution = switch (t: t) {
| Symbolic(r) => SymbolicDist.T.operate(distToFloatOperation, r)->E.R.toOption
| _ => None
}
switch symbolicSolution {
let trySampleSetSolution = switch ((t: t), distToFloatOperation) {
| (SampleSet(sampleSet), #Mean) => SampleSetDist.mean(sampleSet)->Some
| (SampleSet(sampleSet), #Sample) => SampleSetDist.sample(sampleSet)->Some
| (SampleSet(sampleSet), #Inv(r)) => SampleSetDist.percentile(sampleSet, r)->Some
| _ => None
}
switch trySymbolicSolution {
| Some(r) => Ok(r)
| None => toPointSetFn(t)->E.R2.fmap(PointSetDist.operate(distToFloatOperation))
| None =>
switch trySampleSetSolution {
| Some(r) => Ok(r)
| None => toPointSetFn(t)->E.R2.fmap(PointSetDist.operate(distToFloatOperation))
}
}
}
@ -68,8 +93,8 @@ let toPointSet = (
t,
~xyPointLength,
~sampleCount,
~xSelection: GenericDist_Types.Operation.pointsetXSelection=#ByWeight,
unit,
~xSelection: DistributionTypes.DistributionOperation.pointsetXSelection=#ByWeight,
(),
): result<PointSetTypes.pointSetDist, error> => {
switch (t: t) {
| PointSet(pointSet) => Ok(pointSet)
@ -83,7 +108,7 @@ let toPointSet = (
pointSetDistLength: xyPointLength,
kernelWidth: None,
},
)->GenericDist_Types.Error.resultStringToResultError
)->E.R2.errMap(x => DistributionTypes.PointSetConversionError(x))
}
}
@ -93,18 +118,24 @@ let toPointSet = (
xyPointLength to be a bit longer than the eventual toSparkline downsampling. I chose 3
fairly arbitrarily.
*/
let toSparkline = (t: t, ~sampleCount: int, ~bucketCount: int=20, unit): result<string, error> =>
let toSparkline = (t: t, ~sampleCount: int, ~bucketCount: int=20, ()): result<string, error> =>
t
->toPointSet(~xSelection=#Linear, ~xyPointLength=bucketCount * 3, ~sampleCount, ())
->E.R.bind(r =>
r->PointSetDist.toSparkline(bucketCount)->GenericDist_Types.Error.resultStringToResultError
r->PointSetDist.toSparkline(bucketCount)->E.R2.errMap(x => DistributionTypes.SparklineError(x))
)
module Truncate = {
let trySymbolicSimplification = (leftCutoff, rightCutoff, t: t): option<t> =>
let trySymbolicSimplification = (
leftCutoff: option<float>,
rightCutoff: option<float>,
t: t,
): option<t> =>
switch (leftCutoff, rightCutoff, t) {
| (None, None, _) => None
| (lc, rc, Symbolic(#Uniform(u))) if lc < rc =>
| (Some(lc), Some(rc), Symbolic(#Uniform(u))) if lc < rc =>
Some(Symbolic(#Uniform(SymbolicDist.Uniform.truncate(Some(lc), Some(rc), u))))
| (lc, rc, Symbolic(#Uniform(u))) =>
Some(Symbolic(#Uniform(SymbolicDist.Uniform.truncate(lc, rc, u))))
| _ => None
}
@ -137,85 +168,190 @@ let truncate = Truncate.run
of a new variable that is the result of the operation on A and B.
For instance, normal(0, 1) + normal(1, 1) -> normal(1, 2).
In general, this is implemented via convolution.
TODO: It would be useful to be able to pass in a paramater to get this to run either with convolution or monte carlo.
*/
module AlgebraicCombination = {
let tryAnalyticalSimplification = (
arithmeticOperation: GenericDist_Types.Operation.arithmeticOperation,
t1: t,
t2: t,
): option<result<SymbolicDistTypes.symbolicDist, string>> =>
switch (arithmeticOperation, t1, t2) {
| (arithmeticOperation, Symbolic(d1), Symbolic(d2)) =>
switch SymbolicDist.T.tryAnalyticalSimplification(d1, d2, arithmeticOperation) {
| #AnalyticalSolution(symbolicDist) => Some(Ok(symbolicDist))
| #Error(er) => Some(Error(er))
| #NoSolution => None
module InputValidator = {
/*
It would be good to also do a check to make sure that probability mass for the second
operand, at value 1.0, is 0 (or approximately 0). However, we'd ideally want to check
that both the probability mass and the probability density are greater than zero.
Right now we don't yet have a way of getting probability mass, so I'll leave this for later.
*/
let getLogarithmInputError = (t1: t, t2: t, ~toPointSetFn: toPointSetFn): option<error> => {
let firstOperandIsGreaterThanZero =
toFloatOperation(
t1,
~toPointSetFn,
~distToFloatOperation=#Cdf(MagicNumbers.Epsilon.ten),
) |> E.R.fmap(r => r > 0.)
let secondOperandIsGreaterThanZero =
toFloatOperation(
t2,
~toPointSetFn,
~distToFloatOperation=#Cdf(MagicNumbers.Epsilon.ten),
) |> E.R.fmap(r => r > 0.)
let items = E.A.R.firstErrorOrOpen([
firstOperandIsGreaterThanZero,
secondOperandIsGreaterThanZero,
])
switch items {
| Error(r) => Some(r)
| Ok([true, _]) =>
Some(LogarithmOfDistributionError("First input must be completely greater than 0"))
| Ok([false, true]) =>
Some(LogarithmOfDistributionError("Second input must be completely greater than 0"))
| Ok([false, false]) => None
| Ok(_) => Some(Unreachable)
}
| _ => None
}
let runConvolution = (
toPointSet: toPointSetFn,
arithmeticOperation: GenericDist_Types.Operation.arithmeticOperation,
t1: t,
t2: t,
) =>
E.R.merge(toPointSet(t1), toPointSet(t2))->E.R2.fmap(((a, b)) =>
PointSetDist.combineAlgebraically(arithmeticOperation, a, b)
)
let runMonteCarlo = (
toSampleSet: toSampleSetFn,
arithmeticOperation: GenericDist_Types.Operation.arithmeticOperation,
t1: t,
t2: t,
) => {
let fn = Operation.Algebraic.toFn(arithmeticOperation)
E.R.merge(toSampleSet(t1), toSampleSet(t2))
->E.R.bind(((t1, t2)) => {
SampleSetDist.map2(~fn, ~t1, ~t2)->GenericDist_Types.Error.resultStringToResultError
})
->E.R2.fmap(r => DistributionTypes.SampleSet(r))
let run = (t1: t, t2: t, ~toPointSetFn: toPointSetFn, ~arithmeticOperation): option<error> => {
if arithmeticOperation == #Logarithm {
getLogarithmInputError(t1, t2, ~toPointSetFn)
} else {
None
}
}
}
//I'm (Ozzie) really just guessing here, very little idea what's best
let expectedConvolutionCost: t => int = x =>
switch x {
| Symbolic(#Float(_)) => 1
| Symbolic(_) => 1000
| PointSet(Discrete(m)) => m.xyShape->XYShape.T.length
| PointSet(Mixed(_)) => 1000
| PointSet(Continuous(_)) => 1000
| _ => 1000
module StrategyCallOnValidatedInputs = {
let convolution = (
toPointSet: toPointSetFn,
arithmeticOperation: Operation.convolutionOperation,
t1: t,
t2: t,
): result<t, error> =>
E.R.merge(toPointSet(t1), toPointSet(t2))
->E.R2.fmap(((a, b)) => PointSetDist.combineAlgebraically(arithmeticOperation, a, b))
->E.R2.fmap(r => DistributionTypes.PointSet(r))
let monteCarlo = (
toSampleSet: toSampleSetFn,
arithmeticOperation: Operation.algebraicOperation,
t1: t,
t2: t,
): result<t, error> => {
let fn = Operation.Algebraic.toFn(arithmeticOperation)
E.R.merge(toSampleSet(t1), toSampleSet(t2))
->E.R.bind(((t1, t2)) => {
SampleSetDist.map2(~fn, ~t1, ~t2)->E.R2.errMap(x => DistributionTypes.OperationError(x))
})
->E.R2.fmap(r => DistributionTypes.SampleSet(r))
}
let chooseConvolutionOrMonteCarlo = (t2: t, t1: t) =>
expectedConvolutionCost(t1) * expectedConvolutionCost(t2) > 10000
? #CalculateWithMonteCarlo
: #CalculateWithConvolution
let symbolic = (
arithmeticOperation: Operation.algebraicOperation,
t1: t,
t2: t,
): SymbolicDistTypes.analyticalSimplificationResult => {
switch (t1, t2) {
| (DistributionTypes.Symbolic(d1), DistributionTypes.Symbolic(d2)) =>
SymbolicDist.T.tryAnalyticalSimplification(d1, d2, arithmeticOperation)
| _ => #NoSolution
}
}
}
module StrategyChooser = {
type specificStrategy = [#AsSymbolic | #AsMonteCarlo | #AsConvolution]
//I'm (Ozzie) really just guessing here, very little idea what's best
let expectedConvolutionCost: t => int = x =>
switch x {
| Symbolic(#Float(_)) => MagicNumbers.OpCost.floatCost
| Symbolic(_) => MagicNumbers.OpCost.symbolicCost
| PointSet(Discrete(m)) => m.xyShape->XYShape.T.length
| PointSet(Mixed(_)) => MagicNumbers.OpCost.mixedCost
| PointSet(Continuous(_)) => MagicNumbers.OpCost.continuousCost
| _ => MagicNumbers.OpCost.wildcardCost
}
let hasSampleSetDist = (t1: t, t2: t): bool => isSampleSetSet(t1) || isSampleSetSet(t2)
let convolutionIsFasterThanMonteCarlo = (t1: t, t2: t): bool =>
expectedConvolutionCost(t1) * expectedConvolutionCost(t2) < MagicNumbers.OpCost.monteCarloCost
let preferConvolutionToMonteCarlo = (t1, t2, arithmeticOperation) => {
!hasSampleSetDist(t1, t2) &&
Operation.Convolution.canDoAlgebraicOperation(arithmeticOperation) &&
convolutionIsFasterThanMonteCarlo(t1, t2)
}
let run = (~t1: t, ~t2: t, ~arithmeticOperation): specificStrategy => {
switch StrategyCallOnValidatedInputs.symbolic(arithmeticOperation, t1, t2) {
| #AnalyticalSolution(_)
| #Error(_) =>
#AsSymbolic
| #NoSolution =>
preferConvolutionToMonteCarlo(t1, t2, arithmeticOperation) ? #AsConvolution : #AsMonteCarlo
}
}
}
let runStrategyOnValidatedInputs = (
~t1: t,
~t2: t,
~arithmeticOperation,
~strategy: StrategyChooser.specificStrategy,
~toPointSetFn: toPointSetFn,
~toSampleSetFn: toSampleSetFn,
): result<t, error> => {
switch strategy {
| #AsMonteCarlo =>
StrategyCallOnValidatedInputs.monteCarlo(toSampleSetFn, arithmeticOperation, t1, t2)
| #AsSymbolic =>
switch StrategyCallOnValidatedInputs.symbolic(arithmeticOperation, t1, t2) {
| #AnalyticalSolution(symbolicDist) => Ok(Symbolic(symbolicDist))
| #Error(e) => Error(OperationError(e))
| #NoSolution => Error(Unreachable)
}
| #AsConvolution =>
switch Operation.Convolution.fromAlgebraicOperation(arithmeticOperation) {
| Some(convOp) => StrategyCallOnValidatedInputs.convolution(toPointSetFn, convOp, t1, t2)
| None => Error(Unreachable)
}
}
}
let run = (
~strategy: DistributionTypes.asAlgebraicCombinationStrategy,
t1: t,
~toPointSetFn: toPointSetFn,
~toSampleSetFn: toSampleSetFn,
~arithmeticOperation,
~arithmeticOperation: Operation.algebraicOperation,
~t2: t,
): result<t, error> => {
switch tryAnalyticalSimplification(arithmeticOperation, t1, t2) {
| Some(Ok(symbolicDist)) => Ok(Symbolic(symbolicDist))
| Some(Error(e)) => Error(Other(e))
| None =>
switch chooseConvolutionOrMonteCarlo(t1, t2) {
| #CalculateWithMonteCarlo => runMonteCarlo(toSampleSetFn, arithmeticOperation, t1, t2)
| #CalculateWithConvolution =>
runConvolution(
toPointSetFn,
arithmeticOperation,
t1,
t2,
)->E.R2.fmap(r => DistributionTypes.PointSet(r))
let invalidOperationError = InputValidator.run(t1, t2, ~arithmeticOperation, ~toPointSetFn)
switch (invalidOperationError, strategy) {
| (Some(e), _) => Error(e)
| (None, AsDefault) => {
let chooseStrategy = StrategyChooser.run(~arithmeticOperation, ~t1, ~t2)
runStrategyOnValidatedInputs(
~t1,
~t2,
~strategy=chooseStrategy,
~arithmeticOperation,
~toPointSetFn,
~toSampleSetFn,
)
}
| (None, AsMonteCarlo) =>
StrategyCallOnValidatedInputs.monteCarlo(toSampleSetFn, arithmeticOperation, t1, t2)
| (None, AsSymbolic) =>
switch StrategyCallOnValidatedInputs.symbolic(arithmeticOperation, t1, t2) {
| #AnalyticalSolution(symbolicDist) => Ok(Symbolic(symbolicDist))
| #NoSolution => Error(RequestedStrategyInvalidError(`No analytic solution for inputs`))
| #Error(err) => Error(OperationError(err))
}
| (None, AsConvolution) =>
switch Operation.Convolution.fromAlgebraicOperation(arithmeticOperation) {
| None => {
let errString = `Convolution not supported for ${Operation.Algebraic.toString(
arithmeticOperation,
)}`
Error(RequestedStrategyInvalidError(errString))
}
| Some(convOp) => StrategyCallOnValidatedInputs.convolution(toPointSetFn, convOp, t1, t2)
}
}
}
@ -227,40 +363,36 @@ let algebraicCombination = AlgebraicCombination.run
let pointwiseCombination = (
t1: t,
~toPointSetFn: toPointSetFn,
~arithmeticOperation,
~algebraicCombination: Operation.algebraicOperation,
~t2: t,
): result<t, error> => {
E.R.merge(toPointSetFn(t1), toPointSetFn(t2))
->E.R2.fmap(((t1, t2)) =>
PointSetDist.combinePointwise(
GenericDist_Types.Operation.arithmeticToFn(arithmeticOperation),
t1,
t2,
)
E.R.merge(toPointSetFn(t1), toPointSetFn(t2))->E.R.bind(((t1, t2)) =>
PointSetDist.combinePointwise(Operation.Algebraic.toFn(algebraicCombination), t1, t2)
->E.R2.fmap(r => DistributionTypes.PointSet(r))
->E.R2.errMap(err => DistributionTypes.OperationError(err))
)
->E.R2.fmap(r => DistributionTypes.PointSet(r))
}
let pointwiseCombinationFloat = (
t: t,
~toPointSetFn: toPointSetFn,
~arithmeticOperation: GenericDist_Types.Operation.arithmeticOperation,
~float: float,
~algebraicCombination: Operation.algebraicOperation,
~f: float,
): result<t, error> => {
let m = switch arithmeticOperation {
let m = switch algebraicCombination {
| #Add | #Subtract => Error(DistributionTypes.DistributionVerticalShiftIsInvalid)
| (#Multiply | #Divide | #Power | #Logarithm) as arithmeticOperation =>
toPointSetFn(t)->E.R2.fmap(t => {
toPointSetFn(t)->E.R.bind(t => {
//TODO: Move to PointSet codebase
let fn = (secondary, main) => Operation.Scale.toFn(arithmeticOperation, main, secondary)
let integralSumCacheFn = Operation.Scale.toIntegralSumCacheFn(arithmeticOperation)
let integralCacheFn = Operation.Scale.toIntegralCacheFn(arithmeticOperation)
PointSetDist.T.mapY(
~integralSumCacheFn=integralSumCacheFn(float),
~integralCacheFn=integralCacheFn(float),
~fn=fn(float),
PointSetDist.T.mapYResult(
~integralSumCacheFn=integralSumCacheFn(f),
~integralCacheFn=integralCacheFn(f),
~fn=fn(f),
t,
)
)->E.R2.errMap(x => DistributionTypes.OperationError(x))
})
}
m->E.R2.fmap(r => DistributionTypes.PointSet(r))
@ -274,7 +406,7 @@ let mixture = (
~pointwiseAddFn: pointwiseAddFn,
) => {
if E.A.length(values) == 0 {
Error(DistributionTypes.Other("Mixture error: mixture must have at least 1 element"))
Error(DistributionTypes.OtherError("Mixture error: mixture must have at least 1 element"))
} else {
let totalWeight = values->E.A2.fmap(E.Tuple2.second)->E.A.Floats.sum
let properlyWeightedValues =

View File

@ -1,5 +1,5 @@
type t = GenericDist_Types.genericDist
type error = GenericDist_Types.error
type t = DistributionTypes.genericDist
type error = DistributionTypes.error
type toPointSetFn = t => result<PointSetTypes.pointSetDist, error>
type toSampleSetFn = t => result<SampleSetDist.t, error>
type scaleMultiplyFn = (t, float) => result<t, error>
@ -28,7 +28,7 @@ let toPointSet: (
t,
~xyPointLength: int,
~sampleCount: int,
~xSelection: GenericDist_Types.Operation.pointsetXSelection=?,
~xSelection: DistributionTypes.DistributionOperation.pointsetXSelection=?,
unit,
) => result<PointSetTypes.pointSetDist, error>
let toSparkline: (t, ~sampleCount: int, ~bucketCount: int=?, unit) => result<string, error>
@ -42,25 +42,26 @@ let truncate: (
) => result<t, error>
let algebraicCombination: (
~strategy: DistributionTypes.asAlgebraicCombinationStrategy,
t,
~toPointSetFn: toPointSetFn,
~toSampleSetFn: toSampleSetFn,
~arithmeticOperation: GenericDist_Types.Operation.arithmeticOperation,
~arithmeticOperation: Operation.algebraicOperation,
~t2: t,
) => result<t, error>
let pointwiseCombination: (
t,
~toPointSetFn: toPointSetFn,
~arithmeticOperation: GenericDist_Types.Operation.arithmeticOperation,
~algebraicCombination: Operation.algebraicOperation,
~t2: t,
) => result<t, error>
let pointwiseCombinationFloat: (
t,
~toPointSetFn: toPointSetFn,
~arithmeticOperation: GenericDist_Types.Operation.arithmeticOperation,
~float: float,
~algebraicCombination: Operation.algebraicOperation,
~f: float,
) => result<t, error>
let mixture: (
@ -68,3 +69,6 @@ let mixture: (
~scaleMultiplyFn: scaleMultiplyFn,
~pointwiseAddFn: pointwiseAddFn,
) => result<t, error>
let isSymbolic: t => bool
let isPointSet: t => bool

View File

@ -1,194 +0,0 @@
type genericDist = DistributionTypes.genericDist
@genType
type error = DistributionTypes.error
@genType
module Error = {
type t = error
let fromString = (s: string): t => Other(s)
@genType
let toString = (x: t) => {
switch x {
| NotYetImplemented => "Not Yet Implemented"
| Unreachable => "Unreachable"
| DistributionVerticalShiftIsInvalid => "Distribution Vertical Shift Is Invalid"
| ArgumentError(x) => `Argument Error: ${x}`
| Other(s) => s
}
}
let resultStringToResultError: result<'a, string> => result<'a, error> = n =>
n->E.R2.errMap(r => r->fromString->Error)
}
module Operation = {
type direction =
| Algebraic
| Pointwise
type arithmeticOperation = [
| #Add
| #Multiply
| #Subtract
| #Divide
| #Power
| #Logarithm
]
let arithmeticToFn = (arithmetic: arithmeticOperation) =>
switch arithmetic {
| #Add => \"+."
| #Multiply => \"*."
| #Subtract => \"-."
| #Power => \"**"
| #Divide => \"/."
| #Logarithm => (a, b) => log(a) /. log(b)
}
type toFloat = [
| #Cdf(float)
| #Inv(float)
| #Mean
| #Pdf(float)
| #Sample
]
@genType
type pointsetXSelection = [#Linear | #ByWeight]
type toDist =
| Normalize
| ToPointSet
| ToSampleSet(int)
| Truncate(option<float>, option<float>)
| Inspect
type toFloatArray = Sample(int)
type toString =
| ToString
| ToSparkline(int)
type toBool = IsNormalized
type fromDist =
| ToFloat(toFloat)
| ToDist(toDist)
| ToDistCombination(direction, arithmeticOperation, [#Dist(genericDist) | #Float(float)])
| ToString(toString)
| ToBool(toBool)
type singleParamaterFunction =
| FromDist(fromDist)
| FromFloat(fromDist)
@genType
type genericFunctionCallInfo =
| FromDist(fromDist, genericDist)
| FromFloat(fromDist, float)
| Mixture(array<(genericDist, float)>)
let distCallToString = (distFunction: fromDist): string =>
switch distFunction {
| ToFloat(#Cdf(r)) => `cdf(${E.Float.toFixed(r)})`
| ToFloat(#Inv(r)) => `inv(${E.Float.toFixed(r)})`
| ToFloat(#Mean) => `mean`
| ToFloat(#Pdf(r)) => `pdf(${E.Float.toFixed(r)})`
| ToFloat(#Sample) => `sample`
| ToDist(Normalize) => `normalize`
| ToDist(ToPointSet) => `toPointSet`
| ToDist(ToSampleSet(r)) => `toSampleSet(${E.I.toString(r)})`
| ToDist(Truncate(_, _)) => `truncate`
| ToDist(Inspect) => `inspect`
| ToString(ToString) => `toString`
| ToString(ToSparkline(n)) => `toSparkline(${E.I.toString(n)})`
| ToBool(IsNormalized) => `isNormalized`
| ToDistCombination(Algebraic, _, _) => `algebraic`
| ToDistCombination(Pointwise, _, _) => `pointwise`
}
let toString = (d: genericFunctionCallInfo): string =>
switch d {
| FromDist(f, _) | FromFloat(f, _) => distCallToString(f)
| Mixture(_) => `mixture`
}
}
/*
It can be a pain to write out the genericFunctionCallInfo. The constructors help with this.
This code only covers some of genericFunctionCallInfo: many arguments could be called with either a
float or a distribution. The "UsingDists" module assumes that everything is a distribution.
This is a tradeoff of some generality in order to get a bit more simplicity.
I could see having a longer interface in the future, but it could be messy.
Like, algebraicAddDistFloat vs. algebraicAddDistDist
*/
module Constructors = {
type t = Operation.genericFunctionCallInfo
module UsingDists = {
@genType
let mean = (dist): t => FromDist(ToFloat(#Mean), dist)
let sample = (dist): t => FromDist(ToFloat(#Sample), dist)
let cdf = (dist, x): t => FromDist(ToFloat(#Cdf(x)), dist)
let inv = (dist, x): t => FromDist(ToFloat(#Inv(x)), dist)
let pdf = (dist, x): t => FromDist(ToFloat(#Pdf(x)), dist)
let normalize = (dist): t => FromDist(ToDist(Normalize), dist)
let isNormalized = (dist): t => FromDist(ToBool(IsNormalized), dist)
let toPointSet = (dist): t => FromDist(ToDist(ToPointSet), dist)
let toSampleSet = (dist, r): t => FromDist(ToDist(ToSampleSet(r)), dist)
let truncate = (dist, left, right): t => FromDist(ToDist(Truncate(left, right)), dist)
let inspect = (dist): t => FromDist(ToDist(Inspect), dist)
let toString = (dist): t => FromDist(ToString(ToString), dist)
let toSparkline = (dist, n): t => FromDist(ToString(ToSparkline(n)), dist)
let algebraicAdd = (dist1, dist2: genericDist): t => FromDist(
ToDistCombination(Algebraic, #Add, #Dist(dist2)),
dist1,
)
let algebraicMultiply = (dist1, dist2): t => FromDist(
ToDistCombination(Algebraic, #Multiply, #Dist(dist2)),
dist1,
)
let algebraicDivide = (dist1, dist2): t => FromDist(
ToDistCombination(Algebraic, #Divide, #Dist(dist2)),
dist1,
)
let algebraicSubtract = (dist1, dist2): t => FromDist(
ToDistCombination(Algebraic, #Subtract, #Dist(dist2)),
dist1,
)
let algebraicLogarithm = (dist1, dist2): t => FromDist(
ToDistCombination(Algebraic, #Logarithm, #Dist(dist2)),
dist1,
)
let algebraicPower = (dist1, dist2): t => FromDist(
ToDistCombination(Algebraic, #Power, #Dist(dist2)),
dist1,
)
let pointwiseAdd = (dist1, dist2): t => FromDist(
ToDistCombination(Pointwise, #Add, #Dist(dist2)),
dist1,
)
let pointwiseMultiply = (dist1, dist2): t => FromDist(
ToDistCombination(Pointwise, #Multiply, #Dist(dist2)),
dist1,
)
let pointwiseDivide = (dist1, dist2): t => FromDist(
ToDistCombination(Pointwise, #Divide, #Dist(dist2)),
dist1,
)
let pointwiseSubtract = (dist1, dist2): t => FromDist(
ToDistCombination(Pointwise, #Subtract, #Dist(dist2)),
dist1,
)
let pointwiseLogarithm = (dist1, dist2): t => FromDist(
ToDistCombination(Pointwise, #Logarithm, #Dist(dist2)),
dist1,
)
let pointwisePower = (dist1, dist2): t => FromDist(
ToDistCombination(Pointwise, #Power, #Dist(dist2)),
dist1,
)
}
}

View File

@ -96,36 +96,25 @@ let toDiscretePointMassesFromTriangulars = (
}
let combineShapesContinuousContinuous = (
op: Operation.algebraicOperation,
op: Operation.convolutionOperation,
s1: PointSetTypes.xyShape,
s2: PointSetTypes.xyShape,
): PointSetTypes.xyShape => {
// if we add the two distributions, we should probably use normal filters.
// if we multiply the two distributions, we should probably use lognormal filters.
let t1m = toDiscretePointMassesFromTriangulars(s1)
let t2m = switch op {
| #Divide => toDiscretePointMassesFromTriangulars(~inverse=true, s2)
| _ => toDiscretePointMassesFromTriangulars(~inverse=false, s2)
}
let t2m = toDiscretePointMassesFromTriangulars(~inverse=false, s2)
let combineMeansFn = switch op {
| #Add => (m1, m2) => m1 +. m2
| #Subtract => (m1, m2) => m1 -. m2
| #Multiply => (m1, m2) => m1 *. m2
| #Divide => (m1, mInv2) => m1 *. mInv2
| #Power => (m1, mInv2) => m1 ** mInv2
| #Logarithm => (m1, m2) => log(m1) /. log(m2)
} // note: here, mInv2 = mean(1 / t2) ~= 1 / mean(t2)
// TODO: Variances are for exponentatiation or logarithms are almost totally made up and very likely very wrong.
// converts the variances and means of the two inputs into the variance of the output
let combineVariancesFn = switch op {
| #Add => (v1, v2, _, _) => v1 +. v2
| #Subtract => (v1, v2, _, _) => v1 +. v2
| #Multiply => (v1, v2, m1, m2) => v1 *. v2 +. v1 *. m2 ** 2. +. v2 *. m1 ** 2.
| #Power => (v1, v2, m1, m2) => v1 *. v2 +. v1 *. m2 ** 2. +. v2 *. m1 ** 2.
| #Logarithm => (v1, v2, m1, m2) => v1 *. v2 +. v1 *. m2 ** 2. +. v2 *. m1 ** 2.
| #Divide => (v1, vInv2, m1, mInv2) => v1 *. vInv2 +. v1 *. mInv2 ** 2. +. vInv2 *. m1 ** 2.
}
// TODO: If operating on two positive-domain distributions, we should take that into account
@ -198,16 +187,20 @@ let toDiscretePointMassesFromDiscrete = (s: PointSetTypes.xyShape): pointMassesW
{n: n, masses: masses, means: means, variances: variances}
}
type argumentPosition = First | Second
let combineShapesContinuousDiscrete = (
op: Operation.algebraicOperation,
op: Operation.convolutionOperation,
continuousShape: PointSetTypes.xyShape,
discreteShape: PointSetTypes.xyShape,
~discretePosition: argumentPosition,
): PointSetTypes.xyShape => {
let t1n = continuousShape |> XYShape.T.length
let t2n = discreteShape |> XYShape.T.length
// each x pair is added/subtracted
let fn = Operation.Algebraic.toFn(op)
let opFunc = Operation.Convolution.toFn(op)
let fn = discretePosition == First ? (a, b) => opFunc(b, a) : opFunc
let outXYShapes: array<array<(float, float)>> = Belt.Array.makeUninitializedUnsafe(t2n)
@ -218,49 +211,56 @@ let combineShapesContinuousDiscrete = (
// creates a new continuous shape for each one of the discrete points, and collects them in outXYShapes.
let dxyShape: array<(float, float)> = Belt.Array.makeUninitializedUnsafe(t1n)
for i in 0 to t1n - 1 {
// When this operation is flipped (like 1 - normal(5, 2)) then the
// x axis coordinates would all come out the wrong order. So we need
// to fill them out in the opposite direction
let index = discretePosition == First ? t1n - 1 - i : i
Belt.Array.set(
dxyShape,
i,
index,
(
fn(continuousShape.xs[i], discreteShape.xs[j]),
continuousShape.ys[i] *. discreteShape.ys[j],
),
) |> ignore
()
}
Belt.Array.set(outXYShapes, j, dxyShape) |> ignore
()
}
| #Multiply
| #Power
| #Logarithm
| #Divide =>
| #Multiply =>
for j in 0 to t2n - 1 {
// creates a new continuous shape for each one of the discrete points, and collects them in outXYShapes.
let dxyShape: array<(float, float)> = Belt.Array.makeUninitializedUnsafe(t1n)
for i in 0 to t1n - 1 {
// If this operation would flip the x axis (such as -1 * normal(5, 2)),
// then we want to fill the shape in backwards to ensure all the points
// are still in the right order
let index = discreteShape.xs[j] > 0.0 ? i : t1n - 1 - i
Belt.Array.set(
dxyShape,
i,
index,
(
fn(continuousShape.xs[i], discreteShape.xs[j]),
continuousShape.ys[i] *. discreteShape.ys[j] /. discreteShape.xs[j],
continuousShape.ys[i] *. discreteShape.ys[j] /. Js.Math.abs_float(discreteShape.xs[j]),
),
) |> ignore
()
}
Belt.Array.set(outXYShapes, j, dxyShape) |> ignore
()
}
}
outXYShapes
|> E.A.fmap(XYShape.T.fromZippedArray)
|> E.A.fold_left(
XYShape.PointwiseCombination.combine(
\"+.",
XYShape.XtoY.continuousInterpolator(#Linear, #UseZero),
),
(acc, x) =>
XYShape.PointwiseCombination.addCombine(
XYShape.XtoY.continuousInterpolator(#Linear, #UseZero),
acc,
x,
),
XYShape.T.empty,
)
}
let isOrdered = (a: XYShape.T.t): bool => E.A.Sorted.Floats.isSorted(a.xs)

View File

@ -87,12 +87,11 @@ let stepwiseToLinear = (t: t): t =>
// Note: This results in a distribution with as many points as the sum of those in t1 and t2.
let combinePointwise = (
~integralSumCachesFn=(_, _) => None,
~integralCachesFn: (t, t) => option<t>=(_, _) => None,
~distributionType: PointSetTypes.distributionType=#PDF,
fn: (float, float) => float,
fn: (float, float) => result<float, Operation.Error.t>,
t1: PointSetTypes.continuousShape,
t2: PointSetTypes.continuousShape,
): PointSetTypes.continuousShape => {
): result<PointSetTypes.continuousShape, 'e> => {
// If we're adding the distributions, and we know the total of each, then we
// can just sum them up. Otherwise, all bets are off.
let combinedIntegralSum = Common.combineIntegralSums(
@ -120,9 +119,8 @@ let combinePointwise = (
let interpolator = XYShape.XtoY.continuousInterpolator(t1.interpolation, extrapolation)
make(
~integralSumCache=combinedIntegralSum,
XYShape.PointwiseCombination.combine(fn, interpolator, t1.xyShape, t2.xyShape),
XYShape.PointwiseCombination.combine(fn, interpolator, t1.xyShape, t2.xyShape)->E.R2.fmap(x =>
make(~integralSumCache=combinedIntegralSum, x)
)
}
@ -141,18 +139,47 @@ let updateIntegralSumCache = (integralSumCache, t: t): t => {
let updateIntegralCache = (integralCache, t: t): t => {...t, integralCache: integralCache}
let reduce = (
let sum = (
~integralSumCachesFn: (float, float) => option<float>=(_, _) => None,
~integralCachesFn: (t, t) => option<t>=(_, _) => None,
fn,
continuousShapes,
) =>
): t =>
continuousShapes |> E.A.fold_left(
combinePointwise(~integralSumCachesFn, ~integralCachesFn, fn),
(x, y) =>
combinePointwise(~integralSumCachesFn, (a, b) => Ok(a +. b), x, y)->E.R.toExn(
"Addition should never fail",
_,
),
empty,
)
let mapY = (~integralSumCacheFn=_ => None, ~integralCacheFn=_ => None, ~fn, t: t) =>
let reduce = (
~integralSumCachesFn: (float, float) => option<float>=(_, _) => None,
fn: (float, float) => result<float, 'e>,
continuousShapes,
): result<t, 'e> =>
continuousShapes |> E.A.R.foldM(combinePointwise(~integralSumCachesFn, fn), empty)
let mapYResult = (
~integralSumCacheFn=_ => None,
~integralCacheFn=_ => None,
~fn: float => result<float, 'e>,
t: t,
): result<t, 'e> =>
XYShape.T.mapYResult(fn, getShape(t))->E.R2.fmap(x =>
make(
~interpolation=t.interpolation,
~integralSumCache=t.integralSumCache |> E.O.bind(_, integralSumCacheFn),
~integralCache=t.integralCache |> E.O.bind(_, integralCacheFn),
x,
)
)
let mapY = (
~integralSumCacheFn=_ => None,
~integralCacheFn=_ => None,
~fn: float => float,
t: t,
): t =>
make(
~interpolation=t.interpolation,
~integralSumCache=t.integralSumCache |> E.O.bind(_, integralSumCacheFn),
@ -176,6 +203,7 @@ module T = Dist({
let minX = shapeFn(XYShape.T.minX)
let maxX = shapeFn(XYShape.T.maxX)
let mapY = mapY
let mapYResult = mapYResult
let updateIntegralCache = updateIntegralCache
let toDiscreteProbabilityMassFraction = _ => 0.0
let toPointSetDist = (t: t): PointSetTypes.pointSetDist => Continuous(t)
@ -241,15 +269,21 @@ module T = Dist({
XYShape.Analysis.getVarianceDangerously(t, mean, Analysis.getMeanOfSquares)
})
let isNormalized = (t: t): bool => {
let areaUnderIntegral = t |> updateIntegralCache(Some(T.integral(t))) |> T.integralEndY
areaUnderIntegral < 1. +. 1e-7 && areaUnderIntegral > 1. -. 1e-7
}
let downsampleEquallyOverX = (length, t): t =>
t |> shapeMap(XYShape.XsConversion.proportionEquallyOverX(length))
/* This simply creates multiple copies of the continuous distribution, scaled and shifted according to
each discrete data point, and then adds them all together. */
let combineAlgebraicallyWithDiscrete = (
op: Operation.algebraicOperation,
op: Operation.convolutionOperation,
t1: t,
t2: PointSetTypes.discreteShape,
~discretePosition: AlgebraicShapeCombination.argumentPosition,
) => {
let t1s = t1 |> getShape
let t2s = t2.xyShape // TODO would like to use Discrete.getShape here, but current file structure doesn't allow for that
@ -266,11 +300,11 @@ let combineAlgebraicallyWithDiscrete = (
op,
continuousAsLinear |> getShape,
t2s,
~discretePosition,
)
let combinedIntegralSum = switch op {
| #Multiply
| #Divide =>
| #Multiply =>
Common.combineIntegralSums((a, b) => Some(a *. b), t1.integralSumCache, t2.integralSumCache)
| _ => None
}
@ -280,7 +314,7 @@ let combineAlgebraicallyWithDiscrete = (
}
}
let combineAlgebraically = (op: Operation.algebraicOperation, t1: t, t2: t) => {
let combineAlgebraically = (op: Operation.convolutionOperation, t1: t, t2: t) => {
let s1 = t1 |> getShape
let s2 = t2 |> getShape
let t1n = s1 |> XYShape.T.length

View File

@ -34,11 +34,6 @@ let lastY = (t: t) => t |> getShape |> XYShape.T.lastY
let combinePointwise = (
~integralSumCachesFn=(_, _) => None,
~integralCachesFn: (
PointSetTypes.continuousShape,
PointSetTypes.continuousShape,
) => option<PointSetTypes.continuousShape>=(_, _) => None,
fn,
t1: PointSetTypes.discreteShape,
t2: PointSetTypes.discreteShape,
): PointSetTypes.discreteShape => {
@ -54,24 +49,16 @@ let combinePointwise = (
make(
~integralSumCache=combinedIntegralSum,
XYShape.PointwiseCombination.combine(
\"+.",
(a, b) => Ok(a +. b),
XYShape.XtoY.discreteInterpolator,
t1.xyShape,
t2.xyShape,
),
)->E.R.toExn("Addition operation should never fail", _),
)
}
let reduce = (
~integralSumCachesFn=(_, _) => None,
~integralCachesFn=(_, _) => None,
fn,
discreteShapes,
): PointSetTypes.discreteShape =>
discreteShapes |> E.A.fold_left(
combinePointwise(~integralSumCachesFn, ~integralCachesFn, fn),
empty,
)
let reduce = (~integralSumCachesFn=(_, _) => None, discreteShapes): PointSetTypes.discreteShape =>
discreteShapes |> E.A.fold_left(combinePointwise(~integralSumCachesFn), empty)
let updateIntegralSumCache = (integralSumCache, t: t): t => {
...t,
@ -85,7 +72,7 @@ let updateIntegralCache = (integralCache, t: t): t => {
/* This multiples all of the data points together and creates a new discrete distribution from the results.
Data points at the same xs get added together. It may be a good idea to downsample t1 and t2 before and/or the result after. */
let combineAlgebraically = (op: Operation.algebraicOperation, t1: t, t2: t): t => {
let combineAlgebraically = (op: Operation.convolutionOperation, t1: t, t2: t): t => {
let t1s = t1 |> getShape
let t2s = t2 |> getShape
let t1n = t1s |> XYShape.T.length
@ -97,7 +84,7 @@ let combineAlgebraically = (op: Operation.algebraicOperation, t1: t, t2: t): t =
t2.integralSumCache,
)
let fn = Operation.Algebraic.toFn(op)
let fn = Operation.Convolution.toFn(op)
let xToYMap = E.FloatFloatMap.empty()
for i in 0 to t1n - 1 {
@ -116,7 +103,26 @@ let combineAlgebraically = (op: Operation.algebraicOperation, t1: t, t2: t): t =
make(~integralSumCache=combinedIntegralSum, combinedShape)
}
let mapY = (~integralSumCacheFn=_ => None, ~integralCacheFn=_ => None, ~fn, t: t) =>
let mapYResult = (
~integralSumCacheFn=_ => None,
~integralCacheFn=_ => None,
~fn: float => result<float, 'e>,
t: t,
): result<t, 'e> =>
XYShape.T.mapYResult(fn, getShape(t))->E.R2.fmap(x =>
make(
~integralSumCache=t.integralSumCache |> E.O.bind(_, integralSumCacheFn),
~integralCache=t.integralCache |> E.O.bind(_, integralCacheFn),
x,
)
)
let mapY = (
~integralSumCacheFn=_ => None,
~integralCacheFn=_ => None,
~fn: float => float,
t: t,
): t =>
make(
~integralSumCache=t.integralSumCache |> E.O.bind(_, integralSumCacheFn),
~integralCache=t.integralCache |> E.O.bind(_, integralCacheFn),
@ -156,6 +162,7 @@ module T = Dist({
let maxX = shapeFn(XYShape.T.maxX)
let toDiscreteProbabilityMassFraction = _ => 1.0
let mapY = mapY
let mapYResult = mapYResult
let updateIntegralCache = updateIntegralCache
let toPointSetDist = (t: t): PointSetTypes.pointSetDist => Discrete(t)
let toContinuous = _ => None

View File

@ -9,6 +9,12 @@ module type dist = {
~fn: float => float,
t,
) => t
let mapYResult: (
~integralSumCacheFn: float => option<float>=?,
~integralCacheFn: PointSetTypes.continuousShape => option<PointSetTypes.continuousShape>=?,
~fn: float => result<float, 'e>,
t,
) => result<t, 'e>
let xToY: (float, t) => PointSetTypes.mixedPoint
let toPointSetDist: t => PointSetTypes.pointSetDist
let toContinuous: t => option<PointSetTypes.continuousShape>
@ -37,6 +43,7 @@ module Dist = (T: dist) => {
let integral = T.integral
let xTotalRange = (t: t) => maxX(t) -. minX(t)
let mapY = T.mapY
let mapYResult = T.mapYResult
let xToY = T.xToY
let downsample = T.downsample
let toPointSetDist = T.toPointSetDist

View File

@ -146,8 +146,7 @@ module T = Dist({
let discreteIntegral = Continuous.stepwiseToLinear(Discrete.T.Integral.get(t.discrete))
Continuous.make(
XYShape.PointwiseCombination.combine(
\"+.",
XYShape.PointwiseCombination.addCombine(
XYShape.XtoY.continuousInterpolator(#Linear, #UseOutermostPoints),
Continuous.getShape(continuousIntegral),
Continuous.getShape(discreteIntegral),
@ -161,24 +160,20 @@ module T = Dist({
let integralYtoX = (f, t) => t |> integral |> Continuous.getShape |> XYShape.YtoX.linear(f)
// This pipes all ys (continuous and discrete) through fn.
// If mapY is a linear operation, we might be able to update the integralSumCaches as well;
// if not, they'll be set to None.
let mapY = (
~integralSumCacheFn=previousIntegralSum => None,
~integralCacheFn=previousIntegral => None,
~fn,
let createMixedFromContinuousDiscrete = (
~integralSumCacheFn=_ => None,
~integralCacheFn=_ => None,
t: t,
discrete: PointSetTypes.discreteShape,
continuous: PointSetTypes.continuousShape,
): t => {
let yMappedDiscrete: PointSetTypes.discreteShape =
t.discrete
|> Discrete.T.mapY(~fn)
discrete
|> Discrete.updateIntegralSumCache(E.O.bind(t.discrete.integralSumCache, integralSumCacheFn))
|> Discrete.updateIntegralCache(E.O.bind(t.discrete.integralCache, integralCacheFn))
let yMappedContinuous: PointSetTypes.continuousShape =
t.continuous
|> Continuous.T.mapY(~fn)
continuous
|> Continuous.updateIntegralSumCache(
E.O.bind(t.continuous.integralSumCache, integralSumCacheFn),
)
@ -192,6 +187,46 @@ module T = Dist({
}
}
// This pipes all ys (continuous and discrete) through fn.
// If mapY is a linear operation, we might be able to update the integralSumCaches as well;
// if not, they'll be set to None.
let mapY = (
~integralSumCacheFn=_ => None,
~integralCacheFn=_ => None,
~fn: float => float,
t: t,
): t => {
let discrete = t.discrete |> Discrete.T.mapY(~fn)
let continuous = t.continuous |> Continuous.T.mapY(~fn)
createMixedFromContinuousDiscrete(
~integralCacheFn,
~integralSumCacheFn,
t,
discrete,
continuous,
)
}
let mapYResult = (
~integralSumCacheFn=_ => None,
~integralCacheFn=_ => None,
~fn: float => result<float, 'e>,
t: t,
): result<t, 'e> => {
E.R.merge(
Discrete.T.mapYResult(~fn, t.discrete),
Continuous.T.mapYResult(~fn, t.continuous),
)->E.R2.fmap(((discreteMapped, continuousMapped)) => {
createMixedFromContinuousDiscrete(
~integralCacheFn,
~integralSumCacheFn,
t,
discreteMapped,
continuousMapped,
)
})
}
let mean = ({discrete, continuous}: t): float => {
let discreteMean = Discrete.T.mean(discrete)
let continuousMean = Continuous.T.mean(continuous)
@ -226,7 +261,7 @@ module T = Dist({
}
})
let combineAlgebraically = (op: Operation.algebraicOperation, t1: t, t2: t): t => {
let combineAlgebraically = (op: Operation.convolutionOperation, t1: t, t2: t): t => {
// Discrete convolution can cause a huge increase in the number of samples,
// so we'll first downsample.
@ -242,9 +277,19 @@ let combineAlgebraically = (op: Operation.algebraicOperation, t1: t, t2: t): t =
// continuous (*) continuous => continuous, but also
// discrete (*) continuous => continuous (and vice versa). We have to take care of all combos and then combine them:
let ccConvResult = Continuous.combineAlgebraically(op, t1.continuous, t2.continuous)
let dcConvResult = Continuous.combineAlgebraicallyWithDiscrete(op, t2.continuous, t1.discrete)
let cdConvResult = Continuous.combineAlgebraicallyWithDiscrete(op, t1.continuous, t2.discrete)
let continuousConvResult = Continuous.reduce(\"+.", [ccConvResult, dcConvResult, cdConvResult])
let dcConvResult = Continuous.combineAlgebraicallyWithDiscrete(
op,
t2.continuous,
t1.discrete,
~discretePosition=First,
)
let cdConvResult = Continuous.combineAlgebraicallyWithDiscrete(
op,
t1.continuous,
t2.discrete,
~discretePosition=Second,
)
let continuousConvResult = Continuous.sum([ccConvResult, dcConvResult, cdConvResult])
// ... finally, discrete (*) discrete => discrete, obviously:
let discreteConvResult = Discrete.combineAlgebraically(op, t1.discrete, t2.discrete)
@ -266,21 +311,18 @@ let combineAlgebraically = (op: Operation.algebraicOperation, t1: t, t2: t): t =
let combinePointwise = (
~integralSumCachesFn=(_, _) => None,
~integralCachesFn=(_, _) => None,
fn,
fn: (float, float) => result<float, 'e>,
t1: t,
t2: t,
): t => {
): result<t, 'e> => {
let reducedDiscrete =
[t1, t2]
|> E.A.fmap(toDiscrete)
|> E.A.O.concatSomes
|> Discrete.reduce(~integralSumCachesFn, ~integralCachesFn, fn)
[t1, t2] |> E.A.fmap(toDiscrete) |> E.A.O.concatSomes |> Discrete.reduce(~integralSumCachesFn)
let reducedContinuous =
[t1, t2]
|> E.A.fmap(toContinuous)
|> E.A.O.concatSomes
|> Continuous.reduce(~integralSumCachesFn, ~integralCachesFn, fn)
|> Continuous.reduce(~integralSumCachesFn, fn)
let combinedIntegralSum = Common.combineIntegralSums(
integralSumCachesFn,
@ -293,11 +335,12 @@ let combinePointwise = (
t1.integralCache,
t2.integralCache,
)
make(
~integralSumCache=combinedIntegralSum,
~integralCache=combinedIntegral,
~discrete=reducedDiscrete,
~continuous=reducedContinuous,
reducedContinuous->E.R2.fmap(continuous =>
make(
~integralSumCache=combinedIntegralSum,
~integralCache=combinedIntegral,
~discrete=reducedDiscrete,
~continuous,
)
)
}

View File

@ -16,6 +16,13 @@ let fmap = ((fn1, fn2, fn3), t: t): t =>
| Continuous(m) => Continuous(fn3(m))
}
let fmapResult = ((fn1, fn2, fn3), t: t): result<t, 'e> =>
switch t {
| Mixed(m) => fn1(m)->E.R2.fmap(x => PointSetTypes.Mixed(x))
| Discrete(m) => fn2(m)->E.R2.fmap(x => PointSetTypes.Discrete(x))
| Continuous(m) => fn3(m)->E.R2.fmap(x => PointSetTypes.Continuous(x))
}
let toMixed = mapToAll((
m => m,
d =>
@ -35,13 +42,24 @@ let toMixed = mapToAll((
))
//TODO WARNING: The combineAlgebraicallyWithDiscrete will break for subtraction and division, like, discrete - continous
let combineAlgebraically = (op: Operation.algebraicOperation, t1: t, t2: t): t =>
let combineAlgebraically = (op: Operation.convolutionOperation, t1: t, t2: t): t =>
switch (t1, t2) {
| (Continuous(m1), Continuous(m2)) =>
Continuous.combineAlgebraically(op, m1, m2) |> Continuous.T.toPointSetDist
| (Continuous(m1), Discrete(m2))
| (Discrete(m2), Continuous(m1)) =>
Continuous.combineAlgebraicallyWithDiscrete(op, m1, m2) |> Continuous.T.toPointSetDist
| (Discrete(m1), Continuous(m2)) =>
Continuous.combineAlgebraicallyWithDiscrete(
op,
m2,
m1,
~discretePosition=First,
) |> Continuous.T.toPointSetDist
| (Continuous(m1), Discrete(m2)) =>
Continuous.combineAlgebraicallyWithDiscrete(
op,
m1,
m2,
~discretePosition=Second,
) |> Continuous.T.toPointSetDist
| (Discrete(m1), Discrete(m2)) =>
Discrete.combineAlgebraically(op, m1, m2) |> Discrete.T.toPointSetDist
| (m1, m2) => Mixed.combineAlgebraically(op, toMixed(m1), toMixed(m2)) |> Mixed.T.toPointSetDist
@ -53,23 +71,28 @@ let combinePointwise = (
PointSetTypes.continuousShape,
PointSetTypes.continuousShape,
) => option<PointSetTypes.continuousShape>=(_, _) => None,
fn,
fn: (float, float) => result<float, Operation.Error.t>,
t1: t,
t2: t,
) =>
): result<PointSetTypes.pointSetDist, Operation.Error.t> =>
switch (t1, t2) {
| (Continuous(m1), Continuous(m2)) =>
PointSetTypes.Continuous(
Continuous.combinePointwise(~integralSumCachesFn, ~integralCachesFn, fn, m1, m2),
)
Continuous.combinePointwise(
~integralSumCachesFn,
fn,
m1,
m2,
)->E.R2.fmap(x => PointSetTypes.Continuous(x))
| (Discrete(m1), Discrete(m2)) =>
PointSetTypes.Discrete(
Discrete.combinePointwise(~integralSumCachesFn, ~integralCachesFn, fn, m1, m2),
)
Ok(PointSetTypes.Discrete(Discrete.combinePointwise(~integralSumCachesFn, m1, m2)))
| (m1, m2) =>
PointSetTypes.Mixed(
Mixed.combinePointwise(~integralSumCachesFn, ~integralCachesFn, fn, toMixed(m1), toMixed(m2)),
)
Mixed.combinePointwise(
~integralSumCachesFn,
~integralCachesFn,
fn,
toMixed(m1),
toMixed(m2),
)->E.R2.fmap(x => PointSetTypes.Mixed(x))
}
module T = Dist({
@ -134,10 +157,8 @@ module T = Dist({
let integralYtoX = f =>
mapToAll((Mixed.T.Integral.yToX(f), Discrete.T.Integral.yToX(f), Continuous.T.Integral.yToX(f)))
let maxX = mapToAll((Mixed.T.maxX, Discrete.T.maxX, Continuous.T.maxX))
let mapY = (
~integralSumCacheFn=previousIntegralSum => None,
~integralCacheFn=previousIntegral => None,
~fn,
let mapY = (~integralSumCacheFn=_ => None, ~integralCacheFn=_ => None, ~fn: float => float): (
t => t
) =>
fmap((
Mixed.T.mapY(~integralSumCacheFn, ~integralCacheFn, ~fn),
@ -145,6 +166,17 @@ module T = Dist({
Continuous.T.mapY(~integralSumCacheFn, ~integralCacheFn, ~fn),
))
let mapYResult = (
~integralSumCacheFn=_ => None,
~integralCacheFn=_ => None,
~fn: float => result<float, 'e>,
): (t => result<t, 'e>) =>
fmapResult((
Mixed.T.mapYResult(~integralSumCacheFn, ~integralCacheFn, ~fn),
Discrete.T.mapYResult(~integralSumCacheFn, ~integralCacheFn, ~fn),
Continuous.T.mapYResult(~integralSumCacheFn, ~integralCacheFn, ~fn),
))
let mean = (t: t): float =>
switch t {
| Mixed(m) => Mixed.T.mean(m)
@ -203,8 +235,8 @@ let operate = (distToFloatOp: Operation.distToFloatOperation, s): float =>
| #Mean => T.mean(s)
}
let toSparkline = (t: t, bucketCount) =>
let toSparkline = (t: t, bucketCount): result<string, PointSetTypes.sparklineError> =>
T.toContinuous(t)
->E.O2.fmap(Continuous.downsampleEquallyOverX(bucketCount))
->E.O2.toResult("toContinous Error: Could not convert into continuous distribution")
->E.O2.toResult(PointSetTypes.CannotSparklineDiscrete)
->E.R2.fmap(r => Continuous.getShape(r).ys->Sparklines.create())

View File

@ -94,3 +94,11 @@ module MixedPoint = {
let add = combine2((a, b) => a +. b)
}
@genType
type sparklineError = CannotSparklineDiscrete
let sparklineErrorToString = (err: sparklineError): string =>
switch err {
| CannotSparklineDiscrete => "Cannot find the sparkline of a discrete distribution"
}

View File

@ -15,8 +15,18 @@ const samplesToContinuousPdf = (
if (_.isFinite(max)) {
_samples = _.filter(_samples, (r) => r < max);
}
// The pdf that's created from this function is not a pdf but a pmf. y values
// being probability mass and not density.
// This is awkward, because our code assumes later that y is a density
let pdf = pdfast.create(_samples, { size, width });
return { xs: pdf.map((r) => r.x), ys: pdf.map((r) => r.y) };
// To convert this to a density, we need to find the step size. This is kept
// constant for all y values
let stepSize = pdf[1].x - pdf[0].x;
// We then adjust the y values to density
return { xs: pdf.map((r) => r.x), ys: pdf.map((r) => r.y / stepSize) };
};
module.exports = {

View File

@ -1,3 +1,24 @@
@genType
module Error = {
@genType
type sampleSetError = TooFewSamples
let sampleSetErrorToString = (err: sampleSetError): string =>
switch err {
| TooFewSamples => "Too few samples when constructing sample set"
}
@genType
type pointsetConversionError = TooFewSamplesForConversionToPointSet
let pointsetConversionErrorToString = (err: pointsetConversionError) =>
switch err {
| TooFewSamplesForConversionToPointSet => "Too Few Samples to convert to point set"
}
}
include Error
/*
This is used as a smart constructor. The only way to create a SampleSetDist.t is to call
this constructor.
@ -8,7 +29,7 @@ module T: {
//When we get a good functional library in TS, we could refactor that out.
@genType
type t = array<float>
let make: array<float> => result<t, string>
let make: array<float> => result<t, sampleSetError>
let get: t => array<float>
} = {
type t = array<float>
@ -16,7 +37,7 @@ module T: {
if E.A.length(a) > 5 {
Ok(a)
} else {
Error("too small")
Error(TooFewSamples)
}
let get = (a: t) => a
}
@ -31,13 +52,13 @@ some refactoring.
*/
let toPointSetDist = (~samples: t, ~samplingInputs: SamplingInputs.samplingInputs): result<
PointSetTypes.pointSetDist,
string,
pointsetConversionError,
> =>
SampleSetDist_ToPointSet.toPointSetDist(
~samples=get(samples),
~samplingInputs,
(),
).pointSetDist->E.O2.toResult("Failed to convert to PointSetDist")
).pointSetDist->E.O2.toResult(TooFewSamplesForConversionToPointSet)
//Randomly get one sample from the distribution
let sample = (t: t): float => {
@ -62,7 +83,28 @@ let sampleN = (t: t, n) => {
}
//TODO: Figure out what to do if distributions are different lengths. ``zip`` is kind of inelegant for this.
let map2 = (~fn: (float, float) => float, ~t1: t, ~t2: t) => {
let map2 = (~fn: (float, float) => result<float, Operation.Error.t>, ~t1: t, ~t2: t): result<
t,
Operation.Error.t,
> => {
let samples = Belt.Array.zip(get(t1), get(t2))->E.A2.fmap(((a, b)) => fn(a, b))
make(samples)
// This assertion should never be reached. In order for it to be reached, one
// of the input parameters would need to be a sample set distribution with less
// than 6 samples. Which should be impossible due to the smart constructor.
// I could prove this to the type system (say, creating a {first: float, second: float, ..., fifth: float, rest: array<float>}
// But doing so would take too much time, so I'll leave it as an assertion
E.A.R.firstErrorOrOpen(samples)->E.R2.fmap(x =>
E.R.toExn("Input of samples should be larger than 5", make(x))
)
}
let mean = t => T.get(t)->E.A.Floats.mean
let geomean = t => T.get(t)->E.A.Floats.geomean
let mode = t => T.get(t)->E.A.Floats.mode
let sum = t => T.get(t)->E.A.Floats.sum
let min = t => T.get(t)->E.A.Floats.min
let max = t => T.get(t)->E.A.Floats.max
let stdev = t => T.get(t)->E.A.Floats.stdev
let variance = t => T.get(t)->E.A.Floats.variance
let percentile = (t, f) => T.get(t)->E.A.Floats.percentile(f)

View File

@ -39,28 +39,6 @@ module Internals = {
module T = {
type t = array<float>
let splitContinuousAndDiscrete = (sortedArray: t) => {
let continuous = []
let discrete = E.FloatFloatMap.empty()
Belt.Array.forEachWithIndex(sortedArray, (index, element) => {
let maxIndex = (sortedArray |> Array.length) - 1
let possiblySimilarElements = switch index {
| 0 => [index + 1]
| n if n == maxIndex => [index - 1]
| _ => [index - 1, index + 1]
} |> Belt.Array.map(_, r => sortedArray[r])
let hasSimilarElement = Belt.Array.some(possiblySimilarElements, r => r == element)
hasSimilarElement
? E.FloatFloatMap.increment(element, discrete)
: {
let _ = Js.Array.push(element, continuous)
}
()
})
(continuous, discrete)
}
let xWidthToUnitWidth = (samples, outputXYPoints, xWidth) => {
let xyPointRange = E.A.Sorted.range(samples) |> E.O.default(0.0)
let xyPointWidth = xyPointRange /. float_of_int(outputXYPoints)
@ -83,9 +61,13 @@ let toPointSetDist = (
~samples: Internals.T.t,
~samplingInputs: SamplingInputs.samplingInputs,
(),
) => {
): Internals.Types.outputs => {
Array.fast_sort(compare, samples)
let (continuousPart, discretePart) = E.A.Sorted.Floats.split(samples)
let minDiscreteToKeep = MagicNumbers.ToPointSet.minDiscreteToKeep(samples)
let (continuousPart, discretePart) = E.A.Sorted.Floats.splitContinuousAndDiscreteForMinWeight(
samples,
~minDiscreteWeight=minDiscreteToKeep,
)
let length = samples |> E.A.length |> float_of_int
let discrete: PointSetTypes.discreteShape =
discretePart
@ -133,9 +115,17 @@ let toPointSetDist = (
~discrete=Some(discrete),
)
/*
I'm surprised that this doesn't come out normalized. My guess is that the KDE library
we're using is standardizing on something else. If we ever change that library, we should
check to see if we still need to do this.
*/
let normalizedPointSet = pointSetDist->E.O2.fmap(PointSetDist.T.normalize)
let samplesParse: Internals.Types.outputs = {
continuousParseParams: pdf |> E.O.fmap(snd),
pointSetDist: pointSetDist,
pointSetDist: normalizedPointSet,
}
samplesParse

View File

@ -52,7 +52,7 @@ module Normal = {
switch operation {
| #Add => Some(#Normal({mean: n1 +. n2.mean, stdev: n2.stdev}))
| #Subtract => Some(#Normal({mean: n1 -. n2.mean, stdev: n2.stdev}))
| #Multiply => Some(#Normal({mean: n1 *. n2.mean, stdev: n1 *. n2.stdev}))
| #Multiply => Some(#Normal({mean: n1 *. n2.mean, stdev: Js.Math.abs_float(n1) *. n2.stdev}))
| _ => None
}
@ -60,8 +60,8 @@ module Normal = {
switch operation {
| #Add => Some(#Normal({mean: n1.mean +. n2, stdev: n1.stdev}))
| #Subtract => Some(#Normal({mean: n1.mean -. n2, stdev: n1.stdev}))
| #Multiply => Some(#Normal({mean: n1.mean *. n2, stdev: n1.stdev *. n2}))
| #Divide => Some(#Normal({mean: n1.mean /. n2, stdev: n1.stdev /. n2}))
| #Multiply => Some(#Normal({mean: n1.mean *. n2, stdev: n1.stdev *. Js.Math.abs_float(n2)}))
| #Divide => Some(#Normal({mean: n1.mean /. n2, stdev: n1.stdev /. Js.Math.abs_float(n2)}))
| _ => None
}
}
@ -86,7 +86,10 @@ module Exponential = {
module Cauchy = {
type t = cauchy
let make = (local, scale): symbolicDist => #Cauchy({local: local, scale: scale})
let make = (local, scale): result<symbolicDist, string> =>
scale > 0.0
? Ok(#Cauchy({local: local, scale: scale}))
: Error("Cauchy distribution scale parameter must larger than 0.")
let pdf = (x, t: t) => Jstat.Cauchy.pdf(x, t.local, t.scale)
let cdf = (x, t: t) => Jstat.Cauchy.cdf(x, t.local, t.scale)
let inv = (p, t: t) => Jstat.Cauchy.inv(p, t.local, t.scale)
@ -377,7 +380,7 @@ module T = {
): analyticalSimplificationResult =>
switch (d1, d2) {
| (#Float(v1), #Float(v2)) =>
switch Operation.Algebraic.applyFn(op, v1, v2) {
switch Operation.Algebraic.toFn(op, v1, v2) {
| Ok(r) => #AnalyticalSolution(#Float(r))
| Error(n) => #Error(n)
}

View File

@ -45,6 +45,6 @@ type symbolicDist = [
type analyticalSimplificationResult = [
| #AnalyticalSolution(symbolicDist)
| #Error(string)
| #Error(Operation.Error.t)
| #NoSolution
]

View File

@ -0,0 +1,37 @@
module Math = {
let e = Js.Math._E
let pi = Js.Math._PI
}
module Epsilon = {
let ten = 1e-10
let seven = 1e-7
}
module Environment = {
let defaultXYPointLength = 1000
let defaultSampleCount = 10000
}
module OpCost = {
let floatCost = 1
let symbolicCost = 1000
// Discrete cost is the length of the xyShape
let mixedCost = 1000
let continuousCost = 1000
let wildcardCost = 1000
let monteCarloCost = Environment.defaultSampleCount
}
module ToPointSet = {
/*
This function chooses the minimum amount of duplicate samples that need
to exist in order for this to be considered discrete. The tricky thing
is that there are some operations that create duplicate continuous samples,
so we can't guarantee that these only will occur because the fundamental
structure is meant to be discrete. I chose this heuristic because I think
it would strike a reasonable trade-off, but Im really unsure whats
best right now.
*/
let minDiscreteToKeep = samples => max(20, E.A.length(samples) / 50)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,5 +6,10 @@ module Js = Reducer_Js
module MathJs = Reducer_MathJs
type expressionValue = Reducer_Expression.expressionValue
type externalBindings = Expression.externalBindings
let evaluate = Expression.eval
let evaluateUsingExternalBindings = Expression.evalUsingExternalBindings
let evaluatePartialUsingExternalBindings = Expression.evalPartialUsingExternalBindings
let parse = Expression.parse
let parseOuter = Expression.parseOuter
let parsePartial = Expression.parsePartial

View File

@ -7,7 +7,20 @@ module MathJs = Reducer_MathJs
@genType
type expressionValue = ReducerInterface_ExpressionValue.expressionValue
@genType
type externalBindings = ReducerInterface_ExpressionValue.externalBindings
@genType
let evaluate: string => result<expressionValue, Reducer_ErrorValue.errorValue>
@genType
let evaluateUsingExternalBindings: (
string,
externalBindings,
) => result<expressionValue, Reducer_ErrorValue.errorValue>
@genType
let evaluatePartialUsingExternalBindings: (
string,
externalBindings,
) => result<externalBindings, Reducer_ErrorValue.errorValue>
let parse: string => result<Expression.expression, ErrorValue.errorValue>
let parseOuter: string => result<Expression.expression, ErrorValue.errorValue>
let parsePartial: string => result<Expression.expression, ErrorValue.errorValue>

View File

@ -1 +1,2 @@
module Builtin = Reducer_Dispatch_BuiltIn
module BuiltinMacros = Reducer_Dispatch_BuiltInMacros

View File

@ -0,0 +1,122 @@
/*
Macros are like functions but instead of taking values as parameters,
they take expressions as parameters and return a new expression.
Macros are used to define language building blocks. They are like Lisp macros.
*/
module ExpressionT = Reducer_Expression_T
module ExpressionValue = ReducerInterface.ExpressionValue
module Result = Belt.Result
open Reducer_ErrorValue
type expression = ExpressionT.expression
type reducerFn = (
expression,
ExpressionT.bindings,
) => result<ExpressionValue.expressionValue, errorValue>
let dispatchMacroCall = (
list: list<expression>,
bindings: ExpressionT.bindings,
reduceExpression: reducerFn,
): result<expression, 'e> => {
let rec replaceSymbols = (expression: expression, bindings: ExpressionT.bindings): result<
expression,
errorValue,
> =>
switch expression {
| ExpressionT.EValue(EvSymbol(aSymbol)) =>
switch bindings->Belt.Map.String.get(aSymbol) {
| Some(boundExpression) => boundExpression->Ok
| None => RESymbolNotFound(aSymbol)->Error
}
| ExpressionT.EValue(_) => expression->Ok
| ExpressionT.EBindings(_) => expression->Ok
| ExpressionT.EList(list) => {
let racc = list->Belt.List.reduceReverse(Ok(list{}), (racc, each: expression) =>
racc->Result.flatMap(acc => {
each
->replaceSymbols(bindings)
->Result.flatMap(newNode => {
acc->Belt.List.add(newNode)->Ok
})
})
)
racc->Result.map(acc => acc->ExpressionT.EList)
}
}
let doBindStatement = (statement: expression, bindings: ExpressionT.bindings) => {
switch statement {
| ExpressionT.EList(list{
ExpressionT.EValue(EvCall("$let")),
ExpressionT.EValue(EvSymbol(aSymbol)),
expressionToReduce,
}) => {
let rNewExpressionToReduce = replaceSymbols(expressionToReduce, bindings)
let rNewValue =
rNewExpressionToReduce->Result.flatMap(newExpressionToReduce =>
reduceExpression(newExpressionToReduce, bindings)
)
let rNewExpression = rNewValue->Result.map(newValue => ExpressionT.EValue(newValue))
rNewExpression->Result.map(newExpression =>
Belt.Map.String.set(bindings, aSymbol, newExpression)->ExpressionT.EBindings
)
}
| _ => REAssignmentExpected->Error
}
}
let doExportVariableExpression = (bindings: ExpressionT.bindings) => {
let emptyDictionary: Js.Dict.t<ExpressionValue.expressionValue> = Js.Dict.empty()
let reducedBindings = bindings->Belt.Map.String.keep((_key, value) =>
switch value {
| ExpressionT.EValue(_) => true
| _ => false
}
)
let externalBindings = reducedBindings->Belt.Map.String.reduce(emptyDictionary, (
acc,
key,
expressionValue,
) => {
let value = switch expressionValue {
| EValue(aValue) => aValue
| _ => EvSymbol("internal")
}
Js.Dict.set(acc, key, value)
acc
})
externalBindings->ExpressionValue.EvRecord->ExpressionT.EValue->Ok
}
let doBindExpression = (expression: expression, bindings: ExpressionT.bindings) =>
switch expression {
| ExpressionT.EList(list{ExpressionT.EValue(EvCall("$let")), ..._}) =>
REExpressionExpected->Error
| ExpressionT.EList(list{ExpressionT.EValue(EvCall("$exportVariablesExpression"))}) =>
doExportVariableExpression(bindings)
| _ => replaceSymbols(expression, bindings)
}
switch list {
| list{ExpressionT.EValue(EvCall("$$bindings"))} => bindings->ExpressionT.EBindings->Ok
| list{
ExpressionT.EValue(EvCall("$$bindStatement")),
ExpressionT.EBindings(bindings),
statement,
} =>
doBindStatement(statement, bindings)
| list{
ExpressionT.EValue(EvCall("$$bindExpression")),
ExpressionT.EBindings(bindings),
expression,
} =>
doBindExpression(expression, bindings)
| _ => list->ExpressionT.EList->Ok
}
}

View File

@ -9,6 +9,7 @@ type errorValue =
| RERecordPropertyNotFound(string, string)
| RESymbolNotFound(string)
| RESyntaxError(string)
| REDistributionError(DistributionTypes.error)
| RETodo(string) // To do
type t = errorValue
@ -20,6 +21,7 @@ let errorToString = err =>
| REAssignmentExpected => "Assignment expected"
| REExpressionExpected => "Expression expected"
| REFunctionExpected(msg) => `Function expected: ${msg}`
| REDistributionError(err) => `Distribution Math Error: ${DistributionTypes.Error.toString(err)}`
| REJavaScriptExn(omsg, oname) => {
let answer = "JS Exception:"
let answer = switch oname {

View File

@ -15,7 +15,7 @@ type t = expression
*/
let rec toString = expression =>
switch expression {
| T.EBindings(bindings) => "$$bound"
| T.EBindings(_) => "$$bound"
| T.EList(aList) =>
`(${Belt.List.map(aList, aValue => toString(aValue))
->Extra.List.interperse(" ")
@ -39,12 +39,26 @@ let parse_ = (expr: string, parser, converter): result<t, errorValue> =>
let parse = (mathJsCode: string): result<t, errorValue> =>
mathJsCode->parse_(MathJs.Parse.parse, MathJs.ToExpression.fromNode)
let parsePartial = (mathJsCode: string): result<t, errorValue> =>
mathJsCode->parse_(MathJs.Parse.parse, MathJs.ToExpression.fromPartialNode)
let parseOuter = (mathJsCode: string): result<t, errorValue> =>
mathJsCode->parse_(MathJs.Parse.parse, MathJs.ToExpression.fromOuterNode)
let defaultBindings: T.bindings = Belt.Map.String.empty
/*
Recursively evaluate/reduce the expression (Lisp AST)
*/
let rec reduceExpression = (expression: t, bindings: T.bindings): result<expressionValue, 'e> => {
/*
Macros are like functions but instead of taking values as parameters,
they take expressions as parameters and return a new expression.
Macros are used to define language building blocks. They are like Lisp macros.
*/
let doMacroCall = (list: list<t>, bindings: T.bindings): result<t, 'e> =>
Reducer_Dispatch_BuiltInMacros.dispatchMacroCall(list, bindings, reduceExpression)
/*
After reducing each level of expression(Lisp AST), we have a value list to evaluate
*/
@ -54,72 +68,10 @@ let rec reduceExpression = (expression: t, bindings: T.bindings): result<express
| _ => valueList->Belt.List.toArray->ExpressionValue.EvArray->Ok
}
/*
Macros are like functions but instead of taking values as parameters,
they take expressions as parameters and return a new expression.
Macros are used to define language building blocks. They are like Lisp macros.
*/
let doMacroCall = (list: list<t>, bindings: T.bindings): result<t, 'e> => {
let dispatchMacroCall = (list: list<t>, bindings: T.bindings): result<t, 'e> => {
let rec replaceSymbols = (expression: t, bindings: T.bindings): result<t, errorValue> =>
switch expression {
| T.EValue(EvSymbol(aSymbol)) =>
switch bindings->Belt.Map.String.get(aSymbol) {
| Some(boundExpression) => boundExpression->Ok
| None => RESymbolNotFound(aSymbol)->Error
}
| T.EValue(_) => expression->Ok
| T.EBindings(_) => expression->Ok
| T.EList(list) => {
let racc = list->Belt.List.reduceReverse(Ok(list{}), (racc, each: expression) =>
racc->Result.flatMap(acc => {
each
->replaceSymbols(bindings)
->Result.flatMap(newNode => {
acc->Belt.List.add(newNode)->Ok
})
})
)
racc->Result.map(acc => acc->T.EList)
}
}
let doBindStatement = (statement: t, bindings: T.bindings) => {
switch statement {
| T.EList(list{T.EValue(EvCall("$let")), T.EValue(EvSymbol(aSymbol)), expression}) => {
let rNewExpression = replaceSymbols(expression, bindings)
rNewExpression->Result.map(newExpression =>
Belt.Map.String.set(bindings, aSymbol, newExpression)->T.EBindings
)
}
| _ => REAssignmentExpected->Error
}
}
let doBindExpression = (expression: t, bindings: T.bindings) => {
switch expression {
| T.EList(list{T.EValue(EvCall("$let")), ..._}) => REExpressionExpected->Error
| _ => replaceSymbols(expression, bindings)
}
}
switch list {
| list{T.EValue(EvCall("$$bindings"))} => bindings->T.EBindings->Ok
| list{T.EValue(EvCall("$$bindStatement")), T.EBindings(bindings), statement} =>
doBindStatement(statement, bindings)
| list{T.EValue(EvCall("$$bindExpression")), T.EBindings(bindings), expression} =>
doBindExpression(expression, bindings)
| _ => list->T.EList->Ok
}
}
list->dispatchMacroCall(bindings)
}
let rec seekMacros = (expression: t, bindings: T.bindings): result<t, 'e> =>
switch expression {
| T.EValue(value) => expression->Ok
| T.EValue(_value) => expression->Ok
| T.EBindings(_value) => expression->Ok
| T.EList(list) => {
let racc: result<list<t>, 'e> = list->Belt.List.reduceReverse(Ok(list{}), (
racc,
@ -155,6 +107,7 @@ let rec reduceExpression = (expression: t, bindings: T.bindings): result<express
)
racc->Result.flatMap(acc => acc->reduceValueList)
}
| EBindings(_bindings) => RETodo("Error: Bindings cannot be reduced to values")->Error
}
let rExpandedExpression: result<t, 'e> = expression->seekMacros(bindings)
@ -163,17 +116,71 @@ let rec reduceExpression = (expression: t, bindings: T.bindings): result<express
)
}
let evalWBindingsExpression = (aExpression, bindings): result<expressionValue, 'e> =>
let evalUsingExternalBindingsExpression_ = (aExpression, bindings): result<expressionValue, 'e> =>
reduceExpression(aExpression, bindings)
/*
Evaluates MathJs code via Reducer using bindings and answers the result
Evaluates MathJs code via Reducer using bindings and answers the result.
When bindings are used, the code is a partial code as if it is cut from a larger code.
Therefore all statements are assignments.
*/
let evalWBindings = (codeText: string, bindings: T.bindings) => {
parse(codeText)->Result.flatMap(code => code->evalWBindingsExpression(bindings))
let evalPartialUsingExternalBindings_ = (codeText: string, bindings: T.bindings) => {
parsePartial(codeText)->Result.flatMap(expression =>
expression->evalUsingExternalBindingsExpression_(bindings)
)
}
/*
Evaluates MathJs code via Reducer and answers the result
Evaluates MathJs code via Reducer using bindings and answers the result.
When bindings are used, the code is a partial code as if it is cut from a larger code.
Therefore all statments are assignments.
*/
let eval = (code: string) => evalWBindings(code, defaultBindings)
let evalOuterWBindings_ = (codeText: string, bindings: T.bindings) => {
parseOuter(codeText)->Result.flatMap(expression =>
expression->evalUsingExternalBindingsExpression_(bindings)
)
}
/*
Evaluates MathJs code and bindings via Reducer and answers the result
*/
let eval = (codeText: string) => {
parse(codeText)->Result.flatMap(expression =>
expression->evalUsingExternalBindingsExpression_(defaultBindings)
)
}
type externalBindings = ReducerInterface.ExpressionValue.externalBindings //Js.Dict.t<expressionValue>
let externalBindingsToBindings = (externalBindings: externalBindings): T.bindings => {
let keys = Js.Dict.keys(externalBindings)
keys->Belt.Array.reduce(defaultBindings, (acc, key) => {
let value = Js.Dict.unsafeGet(externalBindings, key)
acc->Belt.Map.String.set(key, T.EValue(value))
})
}
/*
Evaluates code with external bindings. External bindings are a record of expression values.
*/
let evalUsingExternalBindings = (code: string, externalBindings: externalBindings) => {
let bindings = externalBindings->externalBindingsToBindings
evalOuterWBindings_(code, bindings)
}
/*
Evaluates code with external bindings. External bindings are a record of expression values.
The code is a partial code as if it is cut from a larger code. Therefore all statments are assignments.
*/
let evalPartialUsingExternalBindings = (code: string, externalBindings: externalBindings): result<
externalBindings,
'e,
> => {
let bindings = externalBindings->externalBindingsToBindings
let answer = evalPartialUsingExternalBindings_(code, bindings)
answer->Result.flatMap(answer =>
switch answer {
| EvRecord(aRecord) => Ok(aRecord)
| _ => RETodo("TODO: External bindings must be returned")->Error
}
)
}

View File

@ -12,7 +12,7 @@ type answer = {"value": unit}
Rescript cannot type cast on basic values passed on their own.
This is why we call evalua inside Javascript and wrap the result in an Object
*/
let eval__ = %raw(`function (expr) { return {value: Mathjs.evaluate(expr)}; }`)
let eval__: string => 'a = %raw(`function (expr) { return {value: Mathjs.evaluate(expr)}; }`)
/*
Call MathJs evaluate and return as a variant

View File

@ -9,6 +9,22 @@ type expression = ExpressionT.expression
type expressionValue = ExpressionValue.expressionValue
type errorValue = ErrorValue.errorValue
let passToFunction = (fName: string, rLispArgs): result<expression, errorValue> => {
let toEvCallValue = (name: string): expression => name->ExpressionValue.EvCall->ExpressionT.EValue
let fn = fName->toEvCallValue
rLispArgs->Result.flatMap(lispArgs => list{fn, ...lispArgs}->ExpressionT.EList->Ok)
}
type blockTag =
| ImportVariablesStatement
| ExportVariablesExpression
type tagOrNode =
| BlockTag(blockTag)
| BlockNode(Parse.node)
let toTagOrNode = block => BlockNode(block["node"])
let rec fromNode = (mathJsNode: Parse.node): result<expression, errorValue> =>
Parse.castNodeType(mathJsNode)->Result.flatMap(typedMathJsNode => {
let fromNodeList = (nodeList: list<Parse.node>): result<list<expression>, 'e> =>
@ -18,16 +34,9 @@ let rec fromNode = (mathJsNode: Parse.node): result<expression, errorValue> =>
)
)
let toEvCallValue = (name: string): expression =>
name->ExpressionValue.EvCall->ExpressionT.EValue
let toEvSymbolValue = (name: string): expression =>
name->ExpressionValue.EvSymbol->ExpressionT.EValue
let passToFunction = (fName: string, rLispArgs): result<expression, errorValue> => {
let fn = fName->toEvCallValue
rLispArgs->Result.flatMap(lispArgs => list{fn, ...lispArgs}->ExpressionT.EList->Ok)
}
let caseFunctionNode = fNode => {
let lispArgs = fNode["args"]->Belt.List.fromArray->fromNodeList
passToFunction(fNode->Parse.nameOfFunctionNode, lispArgs)
@ -94,27 +103,6 @@ let rec fromNode = (mathJsNode: Parse.node): result<expression, errorValue> =>
aNode["items"]->Belt.List.fromArray->fromNodeList->Result.map(list => ExpressionT.EList(list))
}
let caseBlockNode = (bNode): result<expression, errorValue> => {
let blocks = bNode["blocks"]
let initialBindings = passToFunction("$$bindings", list{}->Ok)
let lastIndex = Belt.Array.length(blocks) - 1
blocks->Belt.Array.reduceWithIndex(initialBindings, (rPreviousBindings, block, i) => {
rPreviousBindings->Result.flatMap(previousBindings => {
let node = block["node"]
let rStatement: result<expression, errorValue> = node->fromNode
let bindName = if i == lastIndex {
"$$bindExpression"
} else {
"$$bindStatement"
}
rStatement->Result.flatMap((statement: expression) => {
let lispArgs = list{previousBindings, statement}->Ok
passToFunction(bindName, lispArgs)
})
})
})
}
let rFinalExpression: result<expression, errorValue> = switch typedMathJsNode {
| MjAccessorNode(aNode) => caseAccessorNode(aNode["object"], aNode["index"])
| MjArrayNode(aNode) => caseArrayNode(aNode)
@ -124,8 +112,7 @@ let rec fromNode = (mathJsNode: Parse.node): result<expression, errorValue> =>
let rExpr: result<expression, errorValue> = expr->Ok
rExpr
}
| MjBlockNode(bNode) => caseBlockNode(bNode)
// | MjBlockNode(bNode) => "statement"->toEvSymbolValue->Ok
| MjBlockNode(bNode) => bNode["blocks"]->Belt.Array.map(toTagOrNode)->caseTagOrNodes
| MjConstantNode(cNode) =>
cNode["value"]->JavaScript.Gate.jsToEv->Result.flatMap(v => v->ExpressionT.EValue->Ok)
| MjFunctionNode(fNode) => fNode->caseFunctionNode
@ -136,3 +123,73 @@ let rec fromNode = (mathJsNode: Parse.node): result<expression, errorValue> =>
}
rFinalExpression
})
and caseTagOrNodes = (tagOrNodes): result<expression, errorValue> => {
let initialBindings = passToFunction("$$bindings", list{}->Ok)
let lastIndex = Belt.Array.length(tagOrNodes) - 1
tagOrNodes->Belt.Array.reduceWithIndex(initialBindings, (rPreviousBindings, tagOrNode, i) => {
rPreviousBindings->Result.flatMap(previousBindings => {
let rStatement: result<expression, errorValue> = switch tagOrNode {
| BlockNode(node) => fromNode(node)
| BlockTag(tag) =>
switch tag {
| ImportVariablesStatement => passToFunction("$importVariablesStatement", list{}->Ok)
| ExportVariablesExpression => passToFunction("$exportVariablesExpression", list{}->Ok)
}
}
let bindName = if i == lastIndex {
"$$bindExpression"
} else {
"$$bindStatement"
}
rStatement->Result.flatMap((statement: expression) => {
let lispArgs = list{previousBindings, statement}->Ok
passToFunction(bindName, lispArgs)
})
})
})
}
let fromPartialNode = (mathJsNode: Parse.node): result<expression, errorValue> => {
Parse.castNodeType(mathJsNode)->Result.flatMap(typedMathJsNode => {
let casePartialBlockNode = (bNode: Parse.blockNode) => {
let blocksOrTags = bNode["blocks"]->Belt.Array.map(toTagOrNode)
let completed = Js.Array2.concat(blocksOrTags, [BlockTag(ExportVariablesExpression)])
completed->caseTagOrNodes
}
let casePartialExpression = (node: Parse.node) => {
let completed = [BlockNode(node), BlockTag(ExportVariablesExpression)]
completed->caseTagOrNodes
}
let rFinalExpression: result<expression, errorValue> = switch typedMathJsNode {
| MjBlockNode(bNode) => casePartialBlockNode(bNode)
| _ => casePartialExpression(mathJsNode)
}
rFinalExpression
})
}
let fromOuterNode = (mathJsNode: Parse.node): result<expression, errorValue> => {
Parse.castNodeType(mathJsNode)->Result.flatMap(typedMathJsNode => {
let casePartialBlockNode = (bNode: Parse.blockNode) => {
let blocksOrTags = bNode["blocks"]->Belt.Array.map(toTagOrNode)
let completed = blocksOrTags
completed->caseTagOrNodes
}
let casePartialExpression = (node: Parse.node) => {
let completed = [BlockNode(node)]
completed->caseTagOrNodes
}
let rFinalExpression: result<expression, errorValue> = switch typedMathJsNode {
| MjBlockNode(bNode) => casePartialBlockNode(bNode)
| _ => casePartialExpression(mathJsNode)
}
rFinalExpression
})
}

View File

@ -10,12 +10,15 @@ type rec expressionValue =
| EvArray(array<expressionValue>)
| EvBool(bool)
| EvCall(string) // External function call
| EvDistribution(GenericDist_Types.genericDist)
| EvDistribution(DistributionTypes.genericDist)
| EvNumber(float)
| EvRecord(Js.Dict.t<expressionValue>)
| EvString(string)
| EvSymbol(string)
@genType
type externalBindings = Js.Dict.t<expressionValue>
type functionCall = (string, array<expressionValue>)
let rec toString = aValue =>
@ -33,17 +36,18 @@ let rec toString = aValue =>
->Js.String.concatMany("")
`[${args}]`
}
| EvRecord(aRecord) => {
let pairs =
aRecord
->Js.Dict.entries
->Belt.Array.map(((eachKey, eachValue)) => `${eachKey}: ${toString(eachValue)}`)
->Extra_Array.interperse(", ")
->Js.String.concatMany("")
`{${pairs}}`
}
| EvRecord(aRecord) => aRecord->toStringRecord
| EvDistribution(dist) => GenericDist.toString(dist)
}
and toStringRecord = aRecord => {
let pairs =
aRecord
->Js.Dict.entries
->Belt.Array.map(((eachKey, eachValue)) => `${eachKey}: ${toString(eachValue)}`)
->Extra_Array.interperse(", ")
->Js.String.concatMany("")
`{${pairs}}`
}
let toStringWithType = aValue =>
switch aValue {
@ -68,3 +72,9 @@ let toStringResult = x =>
| Ok(a) => `Ok(${toString(a)})`
| Error(m) => `Error(${ErrorValue.errorToString(m)})`
}
let toStringResultRecord = x =>
switch x {
| Ok(a) => `Ok(${toStringRecord(a)})`
| Error(m) => `Error(${ErrorValue.errorToString(m)})`
}

View File

@ -2,13 +2,13 @@ module ExpressionValue = ReducerInterface_ExpressionValue
type expressionValue = ExpressionValue.expressionValue
module Sample = {
// In real life real libraries should be somewhere else
/*
For an example of mapping polymorphic custom functions. To be deleted after real integration
*/
let customAdd = (a: float, b: float): float => {a +. b}
}
// module Sample = {
// // In real life real libraries should be somewhere else
// /*
// For an example of mapping polymorphic custom functions. To be deleted after real integration
// */
// let customAdd = (a: float, b: float): float => {a +. b}
// }
/*
Map external calls of Reducer

View File

@ -1,12 +1,10 @@
module ExpressionValue = ReducerInterface_ExpressionValue
type expressionValue = ReducerInterface_ExpressionValue.expressionValue
let defaultSampleCount = 10000
let runGenericOperation = DistributionOperation.run(
~env={
sampleCount: defaultSampleCount,
xyPointLength: 1000,
sampleCount: MagicNumbers.Environment.defaultSampleCount,
xyPointLength: MagicNumbers.Environment.defaultXYPointLength,
},
)
@ -24,13 +22,12 @@ module Helpers = {
| "dotPow" => #Power
| "multiply" => #Multiply
| "dotMultiply" => #Multiply
| "dotLog" => #Logarithm
| _ => #Multiply
}
let catchAndConvertTwoArgsToDists = (args: array<expressionValue>): option<(
GenericDist_Types.genericDist,
GenericDist_Types.genericDist,
DistributionTypes.genericDist,
DistributionTypes.genericDist,
)> => {
switch args {
| [EvDistribution(a), EvDistribution(b)] => Some((a, b))
@ -41,33 +38,41 @@ module Helpers = {
}
let toFloatFn = (
fnCall: GenericDist_Types.Operation.toFloat,
dist: GenericDist_Types.genericDist,
fnCall: DistributionTypes.DistributionOperation.toFloat,
dist: DistributionTypes.genericDist,
) => {
FromDist(GenericDist_Types.Operation.ToFloat(fnCall), dist)->runGenericOperation->Some
FromDist(DistributionTypes.DistributionOperation.ToFloat(fnCall), dist)
->runGenericOperation
->Some
}
let toStringFn = (
fnCall: GenericDist_Types.Operation.toString,
dist: GenericDist_Types.genericDist,
fnCall: DistributionTypes.DistributionOperation.toString,
dist: DistributionTypes.genericDist,
) => {
FromDist(GenericDist_Types.Operation.ToString(fnCall), dist)->runGenericOperation->Some
FromDist(DistributionTypes.DistributionOperation.ToString(fnCall), dist)
->runGenericOperation
->Some
}
let toBoolFn = (
fnCall: GenericDist_Types.Operation.toBool,
dist: GenericDist_Types.genericDist,
fnCall: DistributionTypes.DistributionOperation.toBool,
dist: DistributionTypes.genericDist,
) => {
FromDist(GenericDist_Types.Operation.ToBool(fnCall), dist)->runGenericOperation->Some
FromDist(DistributionTypes.DistributionOperation.ToBool(fnCall), dist)
->runGenericOperation
->Some
}
let toDistFn = (fnCall: GenericDist_Types.Operation.toDist, dist) => {
FromDist(GenericDist_Types.Operation.ToDist(fnCall), dist)->runGenericOperation->Some
let toDistFn = (fnCall: DistributionTypes.DistributionOperation.toDist, dist) => {
FromDist(DistributionTypes.DistributionOperation.ToDist(fnCall), dist)
->runGenericOperation
->Some
}
let twoDiststoDistFn = (direction, arithmetic, dist1, dist2) => {
FromDist(
GenericDist_Types.Operation.ToDistCombination(
DistributionTypes.DistributionOperation.ToDistCombination(
direction,
arithmeticMap(arithmetic),
#Dist(dist2),
@ -84,7 +89,7 @@ module Helpers = {
let parseNumberArray = (ags: array<expressionValue>): Belt.Result.t<array<float>, string> =>
E.A.fmap(parseNumber, ags) |> E.A.R.firstErrorOrOpen
let parseDist = (args: expressionValue): Belt.Result.t<GenericDist_Types.genericDist, string> =>
let parseDist = (args: expressionValue): Belt.Result.t<DistributionTypes.genericDist, string> =>
switch args {
| EvDistribution(x) => Ok(x)
| EvNumber(x) => Ok(GenericDist.fromFloat(x))
@ -92,12 +97,12 @@ module Helpers = {
}
let parseDistributionArray = (ags: array<expressionValue>): Belt.Result.t<
array<GenericDist_Types.genericDist>,
array<DistributionTypes.genericDist>,
string,
> => E.A.fmap(parseDist, ags) |> E.A.R.firstErrorOrOpen
let mixtureWithGivenWeights = (
distributions: array<GenericDist_Types.genericDist>,
distributions: array<DistributionTypes.genericDist>,
weights: array<float>,
): DistributionOperation.outputType =>
E.A.length(distributions) == E.A.length(weights)
@ -107,7 +112,7 @@ module Helpers = {
)
let mixtureWithDefaultWeights = (
distributions: array<GenericDist_Types.genericDist>,
distributions: array<DistributionTypes.genericDist>,
): DistributionOperation.outputType => {
let length = E.A.length(distributions)
let weights = Belt.Array.make(length, 1.0 /. Belt.Int.toFloat(length))
@ -126,7 +131,7 @@ module Helpers = {
| Error(err) => GenDistError(ArgumentError(err))
}
}
| Some(EvDistribution(b)) =>
| Some(EvDistribution(_)) =>
switch parseDistributionArray(args) {
| Ok(distributions) => mixtureWithDefaultWeights(distributions)
| Error(err) => GenDistError(ArgumentError(err))
@ -149,6 +154,7 @@ module SymbolicConstructors = {
| "uniform" => Ok(SymbolicDist.Uniform.make)
| "beta" => Ok(SymbolicDist.Beta.make)
| "lognormal" => Ok(SymbolicDist.Lognormal.make)
| "cauchy" => Ok(SymbolicDist.Cauchy.make)
| "to" => Ok(SymbolicDist.From90thPercentile.make)
| _ => Error("Unreachable state")
}
@ -164,14 +170,10 @@ module SymbolicConstructors = {
): option<DistributionOperation.outputType> =>
switch symbolicResult {
| Ok(r) => Some(Dist(Symbolic(r)))
| Error(r) => Some(GenDistError(Other(r)))
| Error(r) => Some(GenDistError(OtherError(r)))
}
}
module Math = {
let e = 2.718281828459
}
let dispatchToGenericOutput = (call: ExpressionValue.functionCall): option<
DistributionOperation.outputType,
> => {
@ -182,7 +184,7 @@ let dispatchToGenericOutput = (call: ExpressionValue.functionCall): option<
->E.R.bind(r => r(f1))
->SymbolicConstructors.symbolicResultToOutput
| (
("normal" | "uniform" | "beta" | "lognormal" | "to") as fnName,
("normal" | "uniform" | "beta" | "lognormal" | "cauchy" | "to") as fnName,
[EvNumber(f1), EvNumber(f2)],
) =>
SymbolicConstructors.twoFloat(fnName)
@ -200,7 +202,12 @@ let dispatchToGenericOutput = (call: ExpressionValue.functionCall): option<
Helpers.toStringFn(ToSparkline(Belt.Float.toInt(n)), dist)
| ("exp", [EvDistribution(a)]) =>
// https://mathjs.org/docs/reference/functions/exp.html
Helpers.twoDiststoDistFn(Algebraic, "pow", GenericDist.fromFloat(Math.e), a)->Some
Helpers.twoDiststoDistFn(
Algebraic(AsDefault),
"pow",
GenericDist.fromFloat(MagicNumbers.Math.e),
a,
)->Some
| ("normalize", [EvDistribution(dist)]) => Helpers.toDistFn(Normalize, dist)
| ("isNormalized", [EvDistribution(dist)]) => Helpers.toBoolFn(IsNormalized, dist)
| ("toPointSet", [EvDistribution(dist)]) => Helpers.toDistFn(ToPointSet, dist)
@ -210,7 +217,7 @@ let dispatchToGenericOutput = (call: ExpressionValue.functionCall): option<
| ("toSampleSet", [EvDistribution(dist), EvNumber(float)]) =>
Helpers.toDistFn(ToSampleSet(Belt.Int.fromFloat(float)), dist)
| ("toSampleSet", [EvDistribution(dist)]) =>
Helpers.toDistFn(ToSampleSet(defaultSampleCount), dist)
Helpers.toDistFn(ToSampleSet(MagicNumbers.Environment.defaultSampleCount), dist)
| ("inspect", [EvDistribution(dist)]) => Helpers.toDistFn(Inspect, dist)
| ("truncateLeft", [EvDistribution(dist), EvNumber(float)]) =>
Helpers.toDistFn(Truncate(Some(float), None), dist)
@ -220,31 +227,38 @@ let dispatchToGenericOutput = (call: ExpressionValue.functionCall): option<
Helpers.toDistFn(Truncate(Some(float1), Some(float2)), dist)
| ("mx" | "mixture", args) => Helpers.mixture(args)->Some
| ("log", [EvDistribution(a)]) =>
Helpers.twoDiststoDistFn(Algebraic, "log", a, GenericDist.fromFloat(Math.e))->Some
Helpers.twoDiststoDistFn(
Algebraic(AsDefault),
"log",
a,
GenericDist.fromFloat(MagicNumbers.Math.e),
)->Some
| ("log10", [EvDistribution(a)]) =>
Helpers.twoDiststoDistFn(Algebraic, "log", a, GenericDist.fromFloat(10.0))->Some
Helpers.twoDiststoDistFn(Algebraic(AsDefault), "log", a, GenericDist.fromFloat(10.0))->Some
| ("unaryMinus", [EvDistribution(a)]) =>
Helpers.twoDiststoDistFn(Algebraic, "multiply", a, GenericDist.fromFloat(-1.0))->Some
| (("add" | "multiply" | "subtract" | "divide" | "pow" | "log") as arithmetic, [a, b] as args) =>
Helpers.twoDiststoDistFn(Algebraic(AsDefault), "multiply", a, GenericDist.fromFloat(-1.0))->Some
| (("add" | "multiply" | "subtract" | "divide" | "pow" | "log") as arithmetic, [_, _] as args) =>
Helpers.catchAndConvertTwoArgsToDists(args)->E.O2.fmap(((fst, snd)) =>
Helpers.twoDiststoDistFn(Algebraic, arithmetic, fst, snd)
Helpers.twoDiststoDistFn(Algebraic(AsDefault), arithmetic, fst, snd)
)
| (
("dotAdd"
| "dotMultiply"
| "dotSubtract"
| "dotDivide"
| "dotPow"
| "dotLog") as arithmetic,
[a, b] as args,
| "dotPow") as arithmetic,
[_, _] as args,
) =>
Helpers.catchAndConvertTwoArgsToDists(args)->E.O2.fmap(((fst, snd)) =>
Helpers.twoDiststoDistFn(Pointwise, arithmetic, fst, snd)
)
| ("dotLog", [EvDistribution(a)]) =>
Helpers.twoDiststoDistFn(Pointwise, "dotLog", a, GenericDist.fromFloat(Math.e))->Some
| ("dotExp", [EvDistribution(a)]) =>
Helpers.twoDiststoDistFn(Pointwise, "dotPow", GenericDist.fromFloat(Math.e), a)->Some
Helpers.twoDiststoDistFn(
Pointwise,
"dotPow",
GenericDist.fromFloat(MagicNumbers.Math.e),
a,
)->Some
| _ => None
}
}
@ -258,12 +272,7 @@ let genericOutputToReducerValue = (o: DistributionOperation.outputType): result<
| Float(d) => Ok(EvNumber(d))
| String(d) => Ok(EvString(d))
| Bool(d) => Ok(EvBool(d))
| GenDistError(NotYetImplemented) => Error(RETodo("Function not yet implemented"))
| GenDistError(Unreachable) => Error(RETodo("Unreachable"))
| GenDistError(DistributionVerticalShiftIsInvalid) =>
Error(RETodo("Distribution Vertical Shift Is Invalid"))
| GenDistError(ArgumentError(err)) => Error(RETodo("Argument Error: " ++ err))
| GenDistError(Other(s)) => Error(RETodo(s))
| GenDistError(err) => Error(REDistributionError(err))
}
let dispatch = call => {

View File

@ -31,6 +31,9 @@ let makeSampleSetDist = SampleSetDist.make
@genType
let evaluate = Reducer.evaluate
@genType
let evaluateUsingExternalBindings = Reducer.evaluateUsingExternalBindings
@genType
type expressionValue = ReducerInterface_ExpressionValue.expressionValue
@ -53,4 +56,4 @@ type continuousShape = PointSetTypes.continuousShape
let errorValueToString = Reducer_ErrorValue.errorToString
@genType
let distributionErrorToString = GenericDist_Types.Error.toString
let distributionErrorToString = DistributionTypes.Error.toString

View File

@ -1,4 +1,7 @@
open Rationale.Function.Infix
/*
Some functions from modules `L`, `O`, and `R` below were copied directly from
running `rescript convert -all` on Rationale https://github.com/jonlaing/rationale
*/
module FloatFloatMap = {
module Id = Belt.Id.MakeComparable({
type t = float
@ -8,7 +11,7 @@ module FloatFloatMap = {
type t = Belt.MutableMap.t<Id.t, float, Id.identity>
let fromArray = (ar: array<(float, float)>) => Belt.MutableMap.fromArray(ar, ~id=module(Id))
let toArray = (t: t) => Belt.MutableMap.toArray(t)
let toArray = (t: t): array<(float, float)> => Belt.MutableMap.toArray(t)
let empty = () => Belt.MutableMap.make(~id=module(Id))
let increment = (el, t: t) =>
Belt.MutableMap.update(t, el, x =>
@ -20,6 +23,10 @@ module FloatFloatMap = {
let get = (el, t: t) => Belt.MutableMap.get(t, el)
let fmap = (fn, t: t) => Belt.MutableMap.map(t, fn)
let partition = (fn, t: t) => {
let (match, noMatch) = Belt.Array.partition(toArray(t), fn)
(fromArray(match), fromArray(noMatch))
}
}
module Int = {
@ -28,7 +35,7 @@ module Int = {
}
/* Utils */
module U = {
let isEqual = (a, b) => a == b
let isEqual = \"=="
let toA = a => [a]
let id = e => e
}
@ -51,17 +58,59 @@ module O = {
| None => rFn()
}
()
let fmap = Rationale.Option.fmap
let bind = Rationale.Option.bind
let default = Rationale.Option.default
let isSome = Rationale.Option.isSome
let isNone = Rationale.Option.isNone
let toExn = Rationale.Option.toExn
let some = Rationale.Option.some
let firstSome = Rationale.Option.firstSome
let toExt = Rationale.Option.toExn // wanna flag this-- looks like a typo but `Rationale.OptiontoExt` doesn't exist.
let flatApply = (fn, b) => Rationale.Option.apply(fn, Some(b)) |> Rationale.Option.flatten
let flatten = Rationale.Option.flatten
let fmap = (f: 'a => 'b, x: option<'a>): option<'b> => {
switch x {
| None => None
| Some(x') => Some(f(x'))
}
}
let bind = (o, f) =>
switch o {
| None => None
| Some(a) => f(a)
}
let default = (d, o) =>
switch o {
| None => d
| Some(a) => a
}
let isSome = o =>
switch o {
| Some(_) => true
| _ => false
}
let isNone = o =>
switch o {
| None => true
| _ => false
}
let toExn = (err, o) =>
switch o {
| None => raise(Failure(err))
| Some(a) => a
}
let some = a => Some(a)
let firstSome = (a, b) =>
switch a {
| None => b
| _ => a
}
let toExt = toExn
let flatten = o =>
switch o {
| None => None
| Some(x) => x
}
let apply = (o, a) =>
switch o {
| Some(f) => bind(a, b => some(f(b)))
| _ => None
}
let flatApply = (fn, b) => apply(fn, Some(b)) |> flatten
let toBool = opt =>
switch opt {
@ -109,6 +158,11 @@ module O2 = {
/* Functions */
module F = {
let pipe = (f, g, x) => g(f(x))
let compose = (f, g, x) => f(g(x))
let flip = (f, a, b) => f(b, a)
let always = (x, _y) => x
let apply = (a, e) => a |> e
let flatten2Callbacks = (fn1, fn2, fnlast) =>
@ -152,13 +206,35 @@ module I = {
let toString = Js.Int.toString
}
exception Assertion(string)
/* R for Result */
module R = {
let result = Rationale.Result.result
open Belt.Result
let result = (okF, errF, r) =>
switch r {
| Ok(a) => okF(a)
| Error(err) => errF(err)
}
let id = e => e |> result(U.id, U.id)
let fmap = Rationale.Result.fmap
let bind = Rationale.Result.bind
let toExn = Belt.Result.getExn
let fmap = (f: 'a => 'b, r: result<'a, 'c>): result<'b, 'c> => {
switch r {
| Ok(r') => Ok(f(r'))
| Error(err) => Error(err)
}
}
let bind = (r, f) =>
switch r {
| Ok(a) => f(a)
| Error(err) => Error(err)
}
let toExn = (msg: string, x: result<'a, 'b>): 'a =>
switch x {
| Ok(r) => r
| Error(_) => raise(Assertion(msg))
}
let default = (default, res: Belt.Result.t<'a, 'b>) =>
switch res {
| Ok(r) => r
@ -179,13 +255,17 @@ module R = {
let errorIfCondition = (errorCondition, errorMessage, r) =>
errorCondition(r) ? Error(errorMessage) : Ok(r)
let ap = Rationale.Result.ap
let ap = (r, a) =>
switch r {
| Ok(f) => Ok(f(a))
| Error(err) => Error(err)
}
let ap' = (r, a) =>
switch r {
| Ok(f) => fmap(f, a)
| Error(err) => Error(err)
}
// (a1 -> a2 -> r) -> m a1 -> m a2 -> m r // not in Rationale
let liftM2: (('a, 'b) => 'c, result<'a, 'd>, result<'b, 'd>) => result<'c, 'd> = (op, xR, yR) => {
ap'(fmap(op, xR), yR)
}
@ -210,10 +290,10 @@ module R2 = {
let bind = (a, b) => R.bind(b, a)
//Converts result type to change error type only
let errMap = (a, map) =>
let errMap = (a: result<'a, 'b>, map: 'b => 'c): result<'a, 'c> =>
switch a {
| Ok(r) => Ok(r)
| Error(e) => map(e)
| Error(e) => Error(map(e))
}
let fmap2 = (xR, f) =>
@ -235,7 +315,7 @@ module S = {
}
module J = {
let toString = \"||>"(Js.Json.decodeString, O.default(""))
let toString = F.pipe(Js.Json.decodeString, O.default(""))
let fromString = Js.Json.string
let fromNumber = Js.Json.number
@ -248,7 +328,7 @@ module J = {
let toString = (str: option<'a>) =>
switch str {
| Some(str) => Some(str |> \"||>"(Js.Json.decodeString, O.default("")))
| Some(str) => Some(str |> F.pipe(Js.Json.decodeString, O.default("")))
| _ => None
}
}
@ -263,34 +343,132 @@ module JsDate = {
/* List */
module L = {
module Util = {
let eq = (a, b) => a == b
}
let fmap = List.map
let get = Belt.List.get
let toArray = Array.of_list
let fmapi = List.mapi
let concat = List.concat
let drop = Rationale.RList.drop
let remove = Rationale.RList.remove
let concat' = (xs, ys) => List.append(ys, xs)
let rec drop = (i, xs) =>
switch (i, xs) {
| (_, list{}) => list{}
| (i, _) if i <= 0 => xs
| (i, list{_, ...b}) => drop(i - 1, b)
}
let append = (a, xs) => List.append(xs, list{a})
let take = {
let rec loop = (i, xs, acc) =>
switch (i, xs) {
| (i, _) if i <= 0 => acc
| (_, list{}) => acc
| (i, list{a, ...b}) => loop(i - 1, b, append(a, acc))
}
(i, xs) => loop(i, xs, list{})
}
let takeLast = (i, xs) => List.rev(xs) |> take(i) |> List.rev
let splitAt = (i, xs) => (take(i, xs), takeLast(List.length(xs) - i, xs))
let remove = (i, n, xs) => {
let (a, b) = splitAt(i, xs)
\"@"(a, drop(n, b))
}
let find = List.find
let filter = List.filter
let for_all = List.for_all
let exists = List.exists
let sort = List.sort
let length = List.length
let filter_opt = Rationale.RList.filter_opt
let uniqBy = Rationale.RList.uniqBy
let join = Rationale.RList.join
let head = Rationale.RList.head
let uniq = Rationale.RList.uniq
let filter_opt = xs => {
let rec loop = (l, acc) =>
switch l {
| list{} => acc
| list{hd, ...tl} =>
switch hd {
| None => loop(tl, acc)
| Some(x) => loop(tl, list{x, ...acc})
}
}
List.rev(loop(xs, list{}))
}
let containsWith = f => List.exists(f)
let uniqWithBy = (eq, f, xs) =>
List.fold_left(
((acc, tacc), v) =>
containsWith(eq(f(v)), tacc) ? (acc, tacc) : (append(v, acc), append(f(v), tacc)),
(list{}, list{}),
xs,
) |> fst
let uniqBy = (f, xs) => uniqWithBy(Util.eq, f, xs)
let join = j => List.fold_left((acc, v) => String.length(acc) == 0 ? v : acc ++ (j ++ v), "")
let head = xs =>
switch List.hd(xs) {
| exception _ => None
| a => Some(a)
}
let uniq = xs => uniqBy(x => x, xs)
let flatten = List.flatten
let last = Rationale.RList.last
let last = xs => xs |> List.rev |> head
let append = List.append
let getBy = Belt.List.getBy
let dropLast = Rationale.RList.dropLast
let contains = Rationale.RList.contains
let without = Rationale.RList.without
let update = Rationale.RList.update
let dropLast = (i, xs) => take(List.length(xs) - i, xs)
let containsWith = f => List.exists(f)
let contains = x => containsWith(Util.eq(x))
let reject = pred => List.filter(x => !pred(x))
let tail = xs =>
switch List.tl(xs) {
| exception _ => None
| a => Some(a)
}
let init = xs => {
O.fmap(List.rev, xs |> List.rev |> tail)
}
let singleton = (x: 'a): list<'a> => list{x}
let adjust = (f, i, xs) => {
let (a, b) = splitAt(i + 1, xs)
switch a {
| _ if i < 0 => xs
| _ if i >= List.length(xs) => xs
| list{} => b
| list{a} => list{f(a), ...b}
| a =>
O.fmap(
concat'(b),
O.bind(init(a), x =>
O.fmap(F.flip(append, x), O.fmap(fmap(f), O.fmap(singleton, last(a))))
),
) |> O.default(xs)
}
}
let without = (exclude, xs) => reject(x => contains(x, exclude), xs)
let update = (x, i, xs) => adjust(F.always(x), i, xs)
let iter = List.iter
let findIndex = Rationale.RList.findIndex
let findIndex = {
let rec loop = (pred, xs, i) =>
switch xs {
| list{} => None
| list{a, ...b} => pred(a) ? Some(i) : loop(pred, b, i + 1)
}
(pred, xs) => loop(pred, xs, 0)
}
let headSafe = Belt.List.head
let tailSafe = Belt.List.tail
let headExn = Belt.List.headExn
@ -340,8 +518,6 @@ module A = {
let reduce = Belt.Array.reduce
let reducei = Belt.Array.reduceWithIndex
let isEmpty = r => length(r) < 1
let min = a => get(a, 0) |> O.fmap(first => Belt.Array.reduce(a, first, (i, j) => i < j ? i : j))
let max = a => get(a, 0) |> O.fmap(first => Belt.Array.reduce(a, first, (i, j) => i > j ? i : j))
let stableSortBy = Belt.SortArray.stableSortBy
let toRanges = (a: array<'a>) =>
switch a |> Belt.Array.length {
@ -354,9 +530,12 @@ module A = {
Belt.Array.getUnsafe(a, index),
Belt.Array.getUnsafe(a, index + 1),
))
|> Rationale.Result.return
|> (x => Ok(x))
}
let tail = Belt.Array.sliceToEnd(_, 1)
let zip = Belt.Array.zip
// This zips while taking the longest elements of each array.
let zipMaxLength = (array1, array2) => {
let maxLength = Int.max(length(array1), length(array2))
@ -415,8 +594,8 @@ module A = {
module O = {
let concatSomes = (optionals: array<option<'a>>): array<'a> =>
optionals
|> Js.Array.filter(Rationale.Option.isSome)
|> Js.Array.map(Rationale.Option.toExn("Warning: This should not have happened"))
|> Js.Array.filter(O.isSome)
|> Js.Array.map(O.toExn("Warning: This should not have happened"))
let defaultEmpty = (o: option<array<'a>>): array<'a> =>
switch o {
| Some(o) => o
@ -438,6 +617,32 @@ module A = {
r |> Belt.Array.map(_, r => Belt.Result.getExn(r))
bringErrorUp |> Belt.Result.map(_, forceOpen)
}
let filterOk = (x: array<result<'a, 'b>>): array<'a> => fmap(R.toOption, x)->O.concatSomes
let forM = (x: array<'a>, fn: 'a => result<'b, 'c>): result<array<'b>, 'c> =>
firstErrorOrOpen(fmap(fn, x))
let foldM = (fn: ('c, 'a) => result<'b, 'e>, init: 'c, x: array<'a>): result<'c, 'e> => {
let acc = ref(init)
let final = ref(Ok())
let break = ref(false)
let i = ref(0)
while break.contents != true && i.contents < length(x) {
switch fn(acc.contents, x[i.contents]) {
| Ok(r) => acc := r
| Error(err) => {
final := Error(err)
break := true
}
}
i := i.contents + 1
}
switch final.contents {
| Ok(_) => Ok(acc.contents)
| Error(err) => Error(err)
}
}
}
module Sorted = {
@ -448,8 +653,11 @@ module A = {
| (Some(min), Some(max)) => Some(max -. min)
| _ => None
}
let floatCompare: (float, float) => int = compare
let binarySearchFirstElementGreaterIndex = (ar: array<'a>, el: 'a) => {
let el = Belt.SortArray.binarySearchBy(ar, el, compare)
let el = Belt.SortArray.binarySearchBy(ar, el, floatCompare)
let el = el < 0 ? el * -1 - 1 : el
switch el {
| e if e >= length(ar) => #overMax
@ -460,25 +668,33 @@ module A = {
let concat = (t1: array<'a>, t2: array<'a>) => {
let ts = Belt.Array.concat(t1, t2)
ts |> Array.fast_sort(compare)
ts |> Array.fast_sort(floatCompare)
ts
}
let concatMany = (t1: array<array<'a>>) => {
let ts = Belt.Array.concatMany(t1)
ts |> Array.fast_sort(compare)
ts |> Array.fast_sort(floatCompare)
ts
}
module Floats = {
let isSorted = (ar: array<float>): bool =>
reduce(zip(ar, tail(ar)), true, (acc, (first, second)) => acc && first < second)
let makeIncrementalUp = (a, b) =>
Array.make(b - a + 1, a) |> Array.mapi((i, c) => c + i) |> Belt.Array.map(_, float_of_int)
let makeIncrementalDown = (a, b) =>
Array.make(a - b + 1, a) |> Array.mapi((i, c) => c - i) |> Belt.Array.map(_, float_of_int)
let split = (sortedArray: array<float>) => {
let continuous = []
/*
This function goes through a sorted array and divides it into two different clusters:
continuous samples and discrete samples. The discrete samples are stored in a mutable map.
Samples are thought to be discrete if they have any duplicates.
*/
let _splitContinuousAndDiscreteForDuplicates = (sortedArray: array<float>) => {
let continuous: array<float> = []
let discrete = FloatFloatMap.empty()
Belt.Array.forEachWithIndex(sortedArray, (index, element) => {
let maxIndex = (sortedArray |> Array.length) - 1
@ -499,14 +715,48 @@ module A = {
(continuous, discrete)
}
/*
This function works very similarly to splitContinuousAndDiscreteForDuplicates. The one major difference
is that you can specify a minDiscreteWeight. If the min discreet weight is 4, that would mean that
at least four elements needed from a specific value for that to be kept as discrete. This is important
because in some cases, we can expect that some common elements will be generated by regular operations.
The final continous array will be sorted.
*/
let splitContinuousAndDiscreteForMinWeight = (
sortedArray: array<float>,
~minDiscreteWeight: int,
) => {
let (continuous, discrete) = _splitContinuousAndDiscreteForDuplicates(sortedArray)
let keepFn = v => Belt.Float.toInt(v) >= minDiscreteWeight
let (discreteToKeep, discreteToIntegrate) = FloatFloatMap.partition(
((_, v)) => keepFn(v),
discrete,
)
let newContinousSamples =
discreteToIntegrate->FloatFloatMap.toArray
|> fmap(((k, v)) => Belt.Array.makeBy(Belt.Float.toInt(v), _ => k))
|> Belt.Array.concatMany
let newContinuous = concat(continuous, newContinousSamples)
newContinuous |> Array.fast_sort(floatCompare)
(newContinuous, discreteToKeep)
}
}
}
module Floats = {
let sum = Belt.Array.reduce(_, 0., (i, j) => i +. j)
let mean = a => sum(a) /. (Array.length(a) |> float_of_int)
let mean = Jstat.mean
let geomean = Jstat.geomean
let mode = Jstat.mode
let variance = Jstat.variance
let stdev = Jstat.stdev
let sum = Jstat.sum
let random = Js.Math.random_int
//Passing true for the exclusive parameter excludes both endpoints of the range.
//https://jstat.github.io/all.html
let percentile = (a, b) => Jstat.percentile(a, b, false)
// Gives an array with all the differences between values
// diff([1,5,3,7]) = [4,-2,4]
let diff = (arr: array<float>): array<float> =>
@ -525,6 +775,9 @@ module A = {
let diff = (max -. min) /. Belt.Float.fromInt(n - 1)
Belt.Array.makeBy(n, i => min +. Belt.Float.fromInt(i) *. diff)
}
let min = Js.Math.minMany_float
let max = Js.Math.maxMany_float
}
}
@ -536,7 +789,7 @@ module A2 = {
module JsArray = {
let concatSomes = (optionals: Js.Array.t<option<'a>>): Js.Array.t<'a> =>
optionals
|> Js.Array.filter(Rationale.Option.isSome)
|> Js.Array.map(Rationale.Option.toExn("Warning: This should not have happened"))
|> Js.Array.filter(O.isSome)
|> Js.Array.map(O.toExn("Warning: This should not have happened"))
let filter = Js.Array.filter
}

View File

@ -9,6 +9,13 @@ type algebraicOperation = [
| #Power
| #Logarithm
]
type convolutionOperation = [
| #Add
| #Multiply
| #Subtract
]
@genType
type pointwiseOperation = [#Add | #Multiply | #Power]
type scaleOperation = [#Multiply | #Power | #Logarithm | #Divide]
@ -20,22 +27,81 @@ type distToFloatOperation = [
| #Sample
]
module Algebraic = {
type t = algebraicOperation
module Convolution = {
type t = convolutionOperation
//Only a selection of operations are supported by convolution.
let fromAlgebraicOperation = (op: algebraicOperation): option<convolutionOperation> =>
switch op {
| #Add => Some(#Add)
| #Subtract => Some(#Subtract)
| #Multiply => Some(#Multiply)
| #Divide | #Power | #Logarithm => None
}
let canDoAlgebraicOperation = (op: algebraicOperation): bool =>
fromAlgebraicOperation(op)->E.O.isSome
let toFn: (t, float, float) => float = x =>
switch x {
| #Add => \"+."
| #Subtract => \"-."
| #Multiply => \"*."
| #Power => \"**"
| #Divide => \"/."
| #Logarithm => (a, b) => log(a) /. log(b)
}
}
let applyFn = (t, f1, f2) =>
switch (t, f1, f2) {
| (#Divide, _, 0.) => Error("Cannot divide $v1 by zero.")
| _ => Ok(toFn(t, f1, f2))
type operationError =
| DivisionByZeroError
| ComplexNumberError
@genType
module Error = {
@genType
type t = operationError
let toString = (err: t): string =>
switch err {
| DivisionByZeroError => "Cannot divide by zero"
| ComplexNumberError => "Operation returned complex result"
}
}
let power = (a: float, b: float): result<float, Error.t> =>
if a >= 0.0 {
Ok(a ** b)
} else {
Error(ComplexNumberError)
}
let divide = (a: float, b: float): result<float, Error.t> =>
if b != 0.0 {
Ok(a /. b)
} else {
Error(DivisionByZeroError)
}
let logarithm = (a: float, b: float): result<float, Error.t> =>
if b == 1. {
Error(DivisionByZeroError)
} else if b == 0. {
Ok(0.)
} else if a > 0.0 && b > 0.0 {
Ok(log(a) /. log(b))
} else {
Error(ComplexNumberError)
}
@genType
module Algebraic = {
@genType
type t = algebraicOperation
let toFn: (t, float, float) => result<float, Error.t> = (x, a, b) =>
switch x {
| #Add => Ok(a +. b)
| #Subtract => Ok(a -. b)
| #Multiply => Ok(a *. b)
| #Power => power(a, b)
| #Divide => divide(a, b)
| #Logarithm => logarithm(a, b)
}
let toString = x =>
@ -79,12 +145,12 @@ module DistToFloat = {
// Note that different logarithms don't really do anything.
module Scale = {
type t = scaleOperation
let toFn = x =>
let toFn = (x: t, a: float, b: float): result<float, Error.t> =>
switch x {
| #Multiply => \"*."
| #Divide => \"/."
| #Power => \"**"
| #Logarithm => (a, b) => log(a) /. log(b)
| #Multiply => Ok(a *. b)
| #Divide => divide(a, b)
| #Power => power(a, b)
| #Logarithm => logarithm(a, b)
}
let format = (operation: t, value, scaleBy) =>

View File

@ -16,7 +16,7 @@ let create = (relativeHeights: array<float>, ~maximum=?, ()) => {
if E.A.length(relativeHeights) === 0 {
""
} else {
let maximum = maximum->E.O2.default(E.A.max(relativeHeights)->E.O2.toExn(""))
let maximum = maximum->E.O2.default(E.A.Floats.max(relativeHeights))
relativeHeights
->E.A2.fmap(_heightToTickIndex(maximum))

View File

@ -43,6 +43,10 @@ module T = {
let xTotalRange = (t: t) => maxX(t) -. minX(t)
let mapX = (fn, t: t): t => {xs: E.A.fmap(fn, t.xs), ys: t.ys}
let mapY = (fn, t: t): t => {xs: t.xs, ys: E.A.fmap(fn, t.ys)}
let mapYResult = (fn: float => result<float, 'e>, t: t): result<t, 'e> => {
let mappedYs = E.A.fmap(fn, t.ys)
E.A.R.firstErrorOrOpen(mappedYs)->E.R2.fmap(y => {xs: t.xs, ys: y})
}
let square = mapX(x => x ** 2.0)
let zip = ({xs, ys}: t) => Belt.Array.zip(xs, ys)
let fromArray = ((xs, ys)): t => {xs: xs, ys: ys}
@ -60,8 +64,8 @@ module T = {
module Ts = {
type t = T.ts
let minX = (t: t) => t |> E.A.fmap(T.minX) |> E.A.min |> extImp
let maxX = (t: t) => t |> E.A.fmap(T.maxX) |> E.A.max |> extImp
let minX = (t: t) => t |> E.A.fmap(T.minX) |> E.A.Floats.min
let maxX = (t: t) => t |> E.A.fmap(T.maxX) |> E.A.Floats.max
let equallyDividedXs = (t: t, newLength) => E.A.Floats.range(minX(t), maxX(t), newLength)
let allXs = (t: t) => t |> E.A.fmap(T.xs) |> E.A.Sorted.concatMany
}
@ -199,7 +203,7 @@ module XtoY = {
/* Returns a between-points-interpolating function that can be used with PointwiseCombination.combine.
For discrete distributions, the probability density between points is zero, so we just return zero here. */
let discreteInterpolator: interpolator = (t: T.t, leftIndex: int, x: float) => 0.0
let discreteInterpolator: interpolator = (_: T.t, _: int, _: float) => 0.0
}
module XsConversion = {
@ -220,8 +224,8 @@ module XsConversion = {
module Zipped = {
type zipped = array<(float, float)>
let compareYs = ((_, y1), (_, y2)) => y1 > y2 ? 1 : 0
let compareXs = ((x1, _), (x2, _)) => x1 > x2 ? 1 : 0
let compareYs = ((_, y1): (float, float), (_, y2): (float, float)) => y1 > y2 ? 1 : 0
let compareXs = ((x1, _): (float, float), (x2, _): (float, float)) => x1 > x2 ? 1 : 0
let sortByY = (t: zipped) => t |> E.A.stableSortBy(_, compareYs)
let sortByX = (t: zipped) => t |> E.A.stableSortBy(_, compareXs)
let filterByX = (testFn: float => bool, t: zipped) => t |> E.A.filter(((x, _)) => testFn(x))
@ -229,7 +233,12 @@ module Zipped = {
module PointwiseCombination = {
// t1Interpolator and t2Interpolator are functions from XYShape.XtoY, e.g. linearBetweenPointsExtrapolateFlat.
let combine = %raw(` // : (float => float => float, T.t, T.t, bool) => T.t
let combine: (
(float, float) => result<float, Operation.Error.t>,
interpolator,
T.t,
T.t,
) => result<T.t, Operation.Error.t> = %raw(`
// This function combines two xyShapes by looping through both of them simultaneously.
// It always moves on to the next smallest x, whether that's in the first or second input's xs,
// and interpolates the value on the other side, thus accumulating xs and ys.
@ -277,13 +286,28 @@ module PointwiseCombination = {
}
outX.push(x);
outY.push(fn(ya, yb));
// Here I check whether the operation was a success. If it was
// keep going. Otherwise, stop and throw the error back to user
let newY = fn(ya, yb);
if(newY.TAG === 0){
outY.push(newY._0);
}
else {
return newY;
}
}
return {xs: outX, ys: outY};
return {TAG: 0, _0: {xs: outX, ys: outY}, [Symbol.for("name")]: "Ok"};
}
`)
let addCombine = (interpolator: interpolator, t1: T.t, t2: T.t): T.t =>
combine((a, b) => Ok(a +. b), interpolator, t1, t2)->E.R.toExn(
"Add operation should never fail",
_,
)
let combineEvenXs = (~fn, ~xToYSelection, sampleCount, t1: T.t, t2: T.t) =>
switch (E.A.length(t1.xs), E.A.length(t2.xs)) {
| (0, 0) => T.empty

View File

@ -0,0 +1,2 @@
.docusaurus
build

View File

@ -0,0 +1,36 @@
---
title: "Known Bugs"
sidebar_position: 6
---
import { SquiggleEditor } from "../../src/components/SquiggleEditor";
Much of the Squiggle math is imprecise. This can cause significant errors, so watch out.
Below are some specific examples to watch for. We'll work on improving these over time and adding much better warnings and error management.
## Mixtures of distributions with very different means
If you take the pointwise mixture of two distributions with very different means, then the value of that gets fairly warped.
In the following case, the mean of the mixture should be equal to the sum of the means of the parts. These are shown as the first two displayed variables. These variables diverge as the underlying distributions change.
<SquiggleEditor
initialSquiggleString={`dist1 = {value: normal(1,1), weight: 1}
dist2 = {value: normal(100000000000,1), weight: 1}
totalWeight = dist1.weight + dist2.weight
distMixture = mixture(dist1.value, dist2.value, [dist1.weight, dist2.weight])
mixtureMean = mean(distMixture)
separateMeansCombined = (mean(dist1.value) * (dist1.weight) + mean(dist2.value) * (dist2.weight))/totalWeight
[mixtureMean, separateMeansCombined, distMixture]`}
/>
## Means of Sample Set Distributions
The means of sample set distributions can vary dramatically, especially as the numbers get high.
<SquiggleEditor
initialSquiggleString={`symbolicDist = 5 to 50333333
sampleSetDist = toSampleSet(symbolicDist)
[mean(symbolicDist), mean(sampleSetDist), symbolicDist, sampleSetDist]`}
/>

View File

@ -0,0 +1,7 @@
---
sidebar_position: 6
title: Gallery
---
- [Adjusting probabilities for the passage of time](https://www.lesswrong.com/s/rDe8QE5NvXcZYzgZ3/p/j8o6sgRerE3tqNWdj) by Nuño Sempere
- [GiveWell's GiveDirectly cost effectiveness analysis](https://observablehq.com/@hazelfire/givewells-givedirectly-cost-effectiveness-analysis) by Sam Nolan

View File

@ -1,12 +1,10 @@
---
sidebar_position: 5
title: Three Formats of Distributions
author: Ozzie Gooen
date: 02-19-2022
---
# Three Formats of Distributions
_Author: Ozzie Gooen_
_Written on: Feb 19, 2022_
Probability distributions have several subtle possible formats. Three important ones that we deal with in Squiggle are symbolic, sample set, and graph formats.
_Symbolic_ formats are just the math equations. `normal(5,3)` is the symbolic representation of a normal distribution.

View File

@ -1,12 +1,15 @@
---
title: "Functions Reference"
sidebar_position: 7
---
import { SquiggleEditor } from "../../src/components/SquiggleEditor";
# Squiggle Functions Reference
_The source of truth for this document is [this file of code](https://github.com/quantified-uncertainty/squiggle/blob/develop/packages/squiggle-lang/src/rescript/ReducerInterface/ReducerInterface_GenericDistribution.res)_
## Distributions
## Inventory distributions
We provide starter distributions, computed symbolically.
### Normal distribution
@ -15,6 +18,10 @@ and standard deviation.
<SquiggleEditor initialSquiggleString="normal(5, 1)" />
#### Validity
- `sd > 0`
### Uniform distribution
The `uniform(low, high)` function creates a uniform distribution between the
@ -22,86 +29,271 @@ two given numbers.
<SquiggleEditor initialSquiggleString="uniform(3, 7)" />
#### Validity
- `low < high`
### Lognormal distribution
The `lognormal(mu, sigma)` returns the log of a normal distribution with parameters
mu and sigma. The log of lognormal(mu, sigma) is a normal distribution with parameters
mean mu and standard deviation sigma.
`mu` and `sigma`. The log of `lognormal(mu, sigma)` is a normal distribution with mean `mu` and standard deviation `sigma`.
<SquiggleEditor initialSquiggleString="lognormal(0, 0.7)" />
An alternative format is also available. The "to" notation creates a lognormal
An alternative format is also available. The `to` notation creates a lognormal
distribution with a 90% confidence interval between the two numbers. We add
this convinience as lognormal distributions are commonly used in practice.
this convenience as lognormal distributions are commonly used in practice.
<SquiggleEditor initialSquiggleString="2 to 10" />
#### Future feature:
Furthermore, it's also possible to create a lognormal from it's actual mean
and standard deviation, using `lognormalFromMeanAndStdDev`.
TODO: interpreter/parser doesn't provide this in current `develop` branch
<SquiggleEditor initialSquiggleString="lognormalFromMeanAndStdDev(20, 10)" />
#### Validity
- `sigma > 0`
- In `x to y` notation, `x < y`
### Beta distribution
The `beta(a, b)` function creates a beta distribution with parameters a and b:
The `beta(a, b)` function creates a beta distribution with parameters `a` and `b`:
<SquiggleEditor initialSquiggleString="beta(20, 20)" />
<SquiggleEditor initialSquiggleString="beta(10, 20)" />
#### Validity
- `a > 0`
- `b > 0`
- Empirically, we have noticed that numerical instability arises when `a < 1` or `b < 1`
### Exponential distribution
The `exponential(mean)` function creates an exponential distribution with the given
mean.
The `exponential(rate)` function creates an exponential distribution with the given
rate.
<SquiggleEditor initialSquiggleString="exponential(1)" />
<SquiggleEditor initialSquiggleString="exponential(1.11)" />
### The Triangular distribution
#### Validity
- `rate > 0`
### Triangular distribution
The `triangular(a,b,c)` function creates a triangular distribution with lower
bound a, mode b and upper bound c.
bound `a`, mode `b` and upper bound `c`.
#### Validity
- `a < b < c`
<SquiggleEditor initialSquiggleString="triangular(1, 2, 4)" />
### Multimodal distriutions
### Scalar (constant dist)
The multimodal function combines 2 or more other distributions to create a weighted
Squiggle, when the context is right, automatically casts a float to a constant distribution.
## Operating on distributions
Here are the ways we combine distributions.
### Mixture of distributions
The `mixture` function combines 2 or more other distributions to create a weighted
combination of the two. The first positional arguments represent the distributions
to be combined, and the last argument is how much to weigh every distribution in the
combination.
<SquiggleEditor initialSquiggleString="mx(uniform(0,1), normal(1,1), [0.5, 0.5])" />
<SquiggleEditor initialSquiggleString="mixture(uniform(0,1), normal(1,1), [0.5, 0.5])" />
It's possible to create discrete distributions using this method.
<SquiggleEditor initialSquiggleString="mx(0, 1, [0.2,0.8])" />
<SquiggleEditor initialSquiggleString="mixture(0, 1, [0.2,0.8])" />
As well as mixed distributions:
<SquiggleEditor initialSquiggleString="mx(3, 8, 1 to 10, [0.2, 0.3, 0.5])" />
<SquiggleEditor initialSquiggleString="mixture(3, 8, 1 to 10, [0.2, 0.3, 0.5])" />
## Other Functions
An alias of `mixture` is `mx`
### PDF of a distribution
#### Validity
The `pdf(distribution, x)` function returns the density of a distribution at the
Using javascript's variable arguments notation, consider `mx(...dists, weights)`:
- `dists.length == weights.length`
### Addition
A horizontal right shift
<SquiggleEditor
initialSquiggleString={`dist1 = 1 to 10
dist2 = triangular(1,2,3)
dist1 + dist2`}
/>
### Subtraction
A horizontal left shift
<SquiggleEditor
initialSquiggleString={`dist1 = 1 to 10
dist2 = triangular(1,2,3)
dist1 - dist2`}
/>
### Multiplication
TODO: provide intuition pump for the semantics
<SquiggleEditor
initialSquiggleString={`dist1 = 1 to 10
dist2 = triangular(1,2,3)
dist1 * dist2`}
/>
We also provide concatenation of two distributions as a syntax sugar for `*`
<SquiggleEditor initialSquiggleString="(0.1 to 1) triangular(1,2,3)" />
### Division
TODO: provide intuition pump for the semantics
<SquiggleEditor
initialSquiggleString={`dist1 = 1 to 10
dist2 = triangular(1,2,3)
dist1 / dist2`}
/>
### Exponentiation
TODO: provide intuition pump for the semantics
<SquiggleEditor initialSquiggleString={`(0.1 to 1) ^ beta(2, 3)`} />
### Taking the base `e` exponential
<SquiggleEditor
initialSquiggleString={`dist = triangular(1,2,3)
exp(dist)`}
/>
### Taking logarithms
<SquiggleEditor
initialSquiggleString={`dist = triangular(1,2,3)
log(dist)`}
/>
<SquiggleEditor
initialSquiggleString={`dist = beta(1,2)
log10(dist)`}
/>
Base `x`
<SquiggleEditor
initialSquiggleString={`x = 2
dist = beta(2,3)
log(dist, x)`}
/>
#### Validity
- `x` must be a scalar
- See [the current discourse](https://github.com/quantified-uncertainty/squiggle/issues/304)
### Pointwise addition
**Pointwise operations are done with `PointSetDist` internals rather than `SampleSetDist` internals**.
TODO: this isn't in the new interpreter/parser yet.
<SquiggleEditor
initialSquiggleString={`dist1 = 1 to 10
dist2 = triangular(1,2,3)
dist1 .+ dist2`}
/>
### Pointwise subtraction
TODO: this isn't in the new interpreter/parser yet.
<SquiggleEditor
initialSquiggleString={`dist1 = 1 to 10
dist2 = triangular(1,2,3)
dist1 .- dist2`}
/>
### Pointwise multiplication
<SquiggleEditor
initialSquiggleString={`dist1 = 1 to 10
dist2 = triangular(1,2,3)
dist1 .* dist2`}
/>
### Pointwise division
<SquiggleEditor
initialSquiggleString={`dist1 = 1 to 10
dist2 = triangular(1,2,3)
dist1 ./ dist2`}
/>
### Pointwise exponentiation
<SquiggleEditor
initialSquiggleString={`dist1 = 1 to 10
dist2 = triangular(1,2,3)
dist1 .^ dist2`}
/>
## Standard functions on distributions
### Probability density function
The `pdf(dist, x)` function returns the density of a distribution at the
given point x.
<SquiggleEditor initialSquiggleString="pdf(normal(0,1),0)" />
### Inverse of a distribution
#### Validity
The `inv(distribution, prob)` gives the value x or which the probability for all values
lower than x is equal to prob. It is the inverse of `cdf`.
- `x` must be a scalar
- `dist` must be a distribution
<SquiggleEditor initialSquiggleString="inv(normal(0,1),0.5)" />
### Cumulative density function
### CDF of a distribution
The `cdf(distribution,x)` gives the cumulative probability of the distribution
The `cdf(dist, x)` gives the cumulative probability of the distribution
or all values lower than x. It is the inverse of `inv`.
<SquiggleEditor initialSquiggleString="cdf(normal(0,1),0)" />
### Mean of a distribution
#### Validity
- `x` must be a scalar
- `dist` must be a distribution
### Inverse CDF
The `inv(dist, prob)` gives the value x or which the probability for all values
lower than x is equal to prob. It is the inverse of `cdf`.
<SquiggleEditor initialSquiggleString="inv(normal(0,1),0.5)" />
#### Validity
- `prob` must be a scalar (please only put it in `(0,1)`)
- `dist` must be a distribution
### Mean
The `mean(distribution)` function gives the mean (expected value) of a distribution.
@ -112,3 +304,65 @@ The `mean(distribution)` function gives the mean (expected value) of a distribut
The `sample(distribution)` samples a given distribution.
<SquiggleEditor initialSquiggleString="sample(normal(0, 10))" />
## Converting between distribution formats
Recall the [three formats of distributions](https://develop--squiggle-documentation.netlify.app/docs/Discussions/Three-Types-Of-Distributions). We can force any distribution into `SampleSet` format
<SquiggleEditor initialSquiggleString="toSampleSet(normal(5, 10))" />
Or `PointSet` format
<SquiggleEditor initialSquiggleString="toPointSet(normal(5, 10))" />
## Normalization
Some distribution operations (like horizontal shift) return an unnormalized distriibution.
We provide a `normalize` function
<SquiggleEditor initialSquiggleString="normalize((0.1 to 1) + triangular(0.1, 1, 10))" />
#### Validity - Input to `normalize` must be a dist
We provide a predicate `isNormalized`, for when we have simple control flow
<SquiggleEditor initialSquiggleString="isNormalized((0.1 to 1) * triangular(0.1, 1, 10))" />
#### Validity
- Input to `isNormalized` must be a dist
## Convert any distribution to a sample set distribution
`toSampleSet` has two signatures
It is unary when you use an internal hardcoded number of samples
<SquiggleEditor initialSquiggleString="toSampleSet(0.1 to 1)" />
And binary when you provide a number of samples (floored)
<SquiggleEditor initialSquiggleString="toSampleSet(0.1 to 1, 100)" />
## `inspect`
You may like to debug by right clicking your browser and using the _inspect_ functionality on the webpage, and viewing the _console_ tab. Then, wrap your squiggle output with `inspect` to log an internal representation.
<SquiggleEditor initialSquiggleString="inspect(toSampleSet(0.1 to 1, 100))" />
Save for a logging side effect, `inspect` does nothing to input and returns it.
## Truncate
You can cut off from the left
<SquiggleEditor initialSquiggleString="truncateLeft(0.1 to 1, 0.5)" />
You can cut off from the right
<SquiggleEditor initialSquiggleString="truncateRight(0.1 to 1, 10)" />
You can cut off from both sides
<SquiggleEditor initialSquiggleString="truncate(0.1 to 1, 0.5, 1.5)" />

View File

@ -1,39 +1,53 @@
---
sidebar_position: 2
title: Language Basics
---
import { SquiggleEditor } from "../../src/components/SquiggleEditor";
# Squiggle Language
## Expressions
The squiggle language has a very simple syntax. The best way to get to understand
it is by simply looking at examples.
A distribution
## Basic Language
<SquiggleEditor initialSquiggleString={`mixture(1 to 2, 3, [0.3, 0.7])`} />
As an example:
A number
<SquiggleEditor initialSquiggleString="4.321e-3" />
Arrays
<SquiggleEditor
initialSquiggleString={`[beta(1,10), 4, isNormalized(toSampleSet(1 to 2))]`}
/>
Records
<SquiggleEditor
initialSquiggleString={`d = {dist: triangular(0, 1, 2), weight: 0.25}
d.dist`}
/>
## Statements
A statement assigns expressions to names. It looks like `<symbol> = <expression>`
<SquiggleEditor
initialSquiggleString={`value_of_work = 10 to 70
value_of_work`}
5 + value_of_work / 75`}
/>
Squiggle can declare variables (`value_of_work = 10 to 70`) and declare exports
(the lone `value_of_work` line). Variables can be used later in a squiggle program
and even in other notebooks!
### Functions
An export is rendered to the output view so you can see your result.
the exports can be expressions, such as:
<SquiggleEditor initialSquiggleString="normal(0,1)" />
## Functions
Squiggle supports functions, including the rendering of functions:
We can define functions
<SquiggleEditor
initialSquiggleString={`ozzie_estimate(t) = lognormal({mean: 3 + (t+.1)^2.5, stdev: 8})
ozzie_estimate
`}
initialSquiggleString={`ozzie_estimate(t) = lognormal(1, t ^ 1.01)
nuño_estimate(t, m) = mixture(0.5 to 2, normal(m, t ^ 1.25))
ozzie_estimate(5) * nuño_estimate(5.01, 1)`}
/>
## See more
- [Functions reference](https://squiggle-language.com/docs/Features/Functions)
- [Gallery](https://squiggle-language.com/docs/Discussions/Gallery)

View File

@ -1,13 +1,12 @@
---
sidebar_position: 3
title: Node Packages
---
# Javascript Libraries
There are two JavaScript packages currently available for Squiggle:
- [`@quri/squiggle-lang`](https://www.npmjs.com/package/@quri/squiggle-lang)
- [`@quri/squiggle-components`](https://www.npmjs.com/package/@quri/squiggle-components)
- [`@quri/squiggle-lang`](https://www.npmjs.com/package/@quri/squiggle-lang) ![npm version](https://badge.fury.io/js/@quri%2Fsquiggle-lang.svg)
- [`@quri/squiggle-components`](https://www.npmjs.com/package/@quri/squiggle-components) ![npm version](https://badge.fury.io/js/@quri%2Fsquiggle-components.svg)
Types are available for both packages.
@ -23,8 +22,8 @@ argument allows you to pass an environment previously created by another `run`
call. Passing this environment will mean that all previously declared variables
in the previous environment will be made available.
The return type of `run` is a bit complicated, and comes from auto generated js
code that comes from rescript. I highly recommend using typescript when using
The return type of `run` is a bit complicated, and comes from auto generated `js`
code that comes from rescript. We highly recommend using typescript when using
this library to help navigate the return type.
## Squiggle Components

View File

@ -0,0 +1,56 @@
---
title: Grammar
author:
- Quinn Dougherty
---
Formal grammar specification, reference material for parser implementation.
_In all likelihood the reference will have to be debugged as we see what tests pass and don't pass during implementation_.
## Lexical descriptions of constants and identifiers
```
<number> ::= [-]? [0-9]+ (.[0-9]+)? | [-]? [0-9]+ (.[0-9]+)? [e] [-]? [0-9]+
<symbol> ::= [a-zA-Z]+ [a-zA-Z0-9]?
<bool> ::= true | false
```
## Expressions
The following gives no typing information. You can obey the grammar and still write nonsensical code.
Think of javascript's list unpacking notation to read our variable-argument function `mixture`.
```
<expr> ::= <term> + <expr> | <term> - <expr> | <expr> .+ <expr> | <expr> .- <expr> | <term>
<term> ::= <power> * <term> | <power> / <term> | <power> .* <term> | <power ./ <term> | <power>
<power> ::= <factor> ^ <power> | <factor> .^ <power> | <factor>
<factor> ::= <number> | <bool> | <symbol> | ( <expr> ) | <array> | <record> | <record>.<symbol> | <symbol> => <expr> | (<symbol>, <symbol>) => <expr> | ... | <symbol>(<symbol>) | <symbol>(<symbol>, <symbol>) | ...
```
## Data structures
```
<array> ::= [] | [<expr>] | [<expr>, <expr>] | ...
<record> ::= {} | {<symbol>: <expr>} | {<symbol>: <expr>, <symbol>: <expr>} | ...
```
## Statements
```
<statement> ::= <assign> | <assignFunction>
<assign> ::= <symbol> = <expr>
<assignFunction> ::= <symbol>(<symbol>) = <expr> | <symbol>(<symbol>, <symbol>) = <expr> | ...
```
## A squiggle file
To be valid and raise no errors as of current (apr22) interpreter,
```
<delim> ::= ; | \n
<code> ::= <expr> | <statement> <delim> <expr> | <statement> <delim> <statement> <delim> <expr> | ...
```
This isn't strictly speaking true; the interpreter allows expressions outside of the final line.

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