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 directory: "/" # Location of package manifests
schedule: schedule:
interval: "daily" interval: "daily"
commit-message:
prefix: "⬆️"

View File

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

View File

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

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ yarn-error.log
.DS_Store .DS_Store
**/.sync.ffs_db **/.sync.ffs_db
.direnv .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/*/node_modules
packages/website/.docusaurus packages/website/.docusaurus
packages/squiggle-lang/lib 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 # 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 - 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) - [Squiggle documentation](https://www.squiggle-language.com/docs/Language)
- [Rescript documentation](https://rescript-lang.org/docs/manual/latest/introduction) - [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 # 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 - **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. - **squiggle-lang** is where the magic happens: probability distributions, the interpreter, etc.
- **website** is the site `squiggle-language.com` - **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 ## 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 ```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 # Pull request protocol

View File

@ -34,7 +34,7 @@ The playground depends on the components library which then depends on the langu
# Develop # 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 ```sh
yarn 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", "name": "@quri/squiggle-components",
"version": "0.1.8", "version": "0.2.9",
"licence": "MIT",
"dependencies": { "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", "@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/jest-dom": "^5.16.4",
"@testing-library/react": "^13.0.1", "@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^14.0.4", "@testing-library/user-event": "^14.1.1",
"@types/jest": "^27.4.0", "@types/jest": "^27.4.0",
"@types/lodash": "^4.14.181", "@types/lodash": "^4.14.182",
"@types/node": "^17.0.24", "@types/node": "^17.0.29",
"@types/react": "^18.0.3", "@types/react": "^18.0.3",
"@types/react-dom": "^18.0.1", "@types/react-dom": "^18.0.2",
"antd": "^4.19.3",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "^18.0.0", "react": "^18.1.0",
"react-ace": "10.0.0",
"react-dom": "^18.0.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-vega": "^7.5.0", "react-vega": "^7.5.0",
"styled-components": "^5.3.5",
"tsconfig-paths-webpack-plugin": "^3.5.2", "tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.6.3", "typescript": "^4.6.3",
"vega": "^5.22.1", "vega": "^5.22.1",
@ -65,25 +84,6 @@
"last 1 safari version" "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": { "resolutions": {
"@types/react": "17.0.43" "@types/react": "17.0.43"
}, },

View File

@ -6,7 +6,7 @@ import {
errorValueToString, errorValueToString,
squiggleExpression, squiggleExpression,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import type { samplingParams, exportEnv } from "@quri/squiggle-lang"; import type { samplingParams } from "@quri/squiggle-lang";
import { NumberShower } from "./NumberShower"; import { NumberShower } from "./NumberShower";
import { DistributionChart } from "./DistributionChart"; import { DistributionChart } from "./DistributionChart";
import { ErrorBox } from "./ErrorBox"; 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 */ /** If the result is a function, how many points along the function it samples */
diagramCount?: number; diagramCount?: number;
/** variables declared before this expression */ /** variables declared before this expression */
environment?: exportEnv; environment?: unknown;
/** When the environment changes */ /** When the environment changes */
onEnvChange?(env: exportEnv): void; onChange?(expr: squiggleExpression): void;
/** CSS width of the element */ /** CSS width of the element */
width?: number; width?: number;
height?: number; height?: number;
@ -141,8 +141,7 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
squiggleString = "", squiggleString = "",
sampleCount = 1000, sampleCount = 1000,
outputXYPoints = 1000, outputXYPoints = 1000,
environment = [], onChange = () => {},
onEnvChange = () => {},
height = 60, height = 60,
width = NaN, width = NaN,
}: SquiggleChartProps) => { }: SquiggleChartProps) => {
@ -155,11 +154,11 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
sampleCount: sampleCount, sampleCount: sampleCount,
xyPointLength: outputXYPoints, xyPointLength: outputXYPoints,
}; };
let expressionResult = run(squiggleString, samplingInputs, environment); let expressionResult = run(squiggleString, samplingInputs);
let internal: JSX.Element; let internal: JSX.Element;
if (expressionResult.tag === "Ok") { if (expressionResult.tag === "Ok") {
onEnvChange(environment);
let expression = expressionResult.value; let expression = expressionResult.value;
onChange(expression);
internal = ( internal = (
<SquiggleItem expression={expression} width={_width} height={height} /> <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 * as ReactDOM from "react-dom";
import { SquiggleChart } from "./SquiggleChart"; import { SquiggleChart } from "./SquiggleChart";
import { CodeEditor } from "./CodeEditor"; import { CodeEditor } from "./CodeEditor";
import type { exportEnv } from "@quri/squiggle-lang";
import styled from "styled-components"; import styled from "styled-components";
import type { squiggleExpression } from "@quri/squiggle-lang";
export interface SquiggleEditorProps { export interface SquiggleEditorProps {
/** The input string for squiggle */ /** 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 */ /** If the result is a function, how many points along the function it samples */
diagramCount?: number; diagramCount?: number;
/** The environment, other variables that were already declared */ /** The environment, other variables that were already declared */
environment?: exportEnv; environment?: unknown;
/** when the environment changes. Used again for notebook magic*/ /** when the environment changes. Used again for notebook magic*/
onEnvChange?(env: exportEnv): void; onChange?(expr: squiggleExpression): void;
/** The width of the element */ /** The width of the element */
width: number; width: number;
} }
@ -44,7 +44,7 @@ export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({
diagramStart, diagramStart,
diagramStop, diagramStop,
diagramCount, diagramCount,
onEnvChange, onChange,
environment, environment,
}: SquiggleEditorProps) => { }: SquiggleEditorProps) => {
let [expression, setExpression] = React.useState(initialSquiggleString); let [expression, setExpression] = React.useState(initialSquiggleString);
@ -70,7 +70,7 @@ export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({
diagramStop={diagramStop} diagramStop={diagramStop}
diagramCount={diagramCount} diagramCount={diagramCount}
environment={environment} environment={environment}
onEnvChange={onEnvChange} onChange={onChange}
/> />
</div> </div>
); );
@ -81,7 +81,7 @@ export function renderSquiggleEditorToDom(props: SquiggleEditorProps) {
ReactDOM.render( ReactDOM.render(
<SquiggleEditor <SquiggleEditor
{...props} {...props}
onEnvChange={(env) => { onChange={(expr) => {
// Typescript complains on two levels here. // Typescript complains on two levels here.
// - Div elements don't have a value property // - Div elements don't have a value property
// - Even if it did (like it was an input element), it would have to // - 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)') // viewof env = cell('normal(0,1)')
// to work // to work
// @ts-ignore // @ts-ignore
parent.value = env; parent.value = expr;
parent.dispatchEvent(new CustomEvent("input")); parent.dispatchEvent(new CustomEvent("input"));
if (props.onEnvChange) props.onEnvChange(env); if (props.onChange) props.onChange(expr);
}} }}
/>, />,
parent parent

View File

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

View File

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

View File

@ -2,3 +2,6 @@ dist
lib lib
*.bs.js *.bs.js
*.gen.tsx *.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", () => { describe("sparkline", () => {
let runTest = ( let runTest = (
name: string, name: string,
dist: GenericDist_Types.genericDist, dist: DistributionTypes.genericDist,
expected: DistributionOperation.outputType, expected: DistributionOperation.outputType,
) => { ) => {
test(name, () => { test(name, () => {

View File

@ -1,14 +1,14 @@
let normalDist5: GenericDist_Types.genericDist = Symbolic(#Normal({mean: 5.0, stdev: 2.0})) let normalDist5: DistributionTypes.genericDist = Symbolic(#Normal({mean: 5.0, stdev: 2.0}))
let normalDist10: GenericDist_Types.genericDist = Symbolic(#Normal({mean: 10.0, stdev: 2.0})) let normalDist10: DistributionTypes.genericDist = Symbolic(#Normal({mean: 10.0, stdev: 2.0}))
let normalDist20: GenericDist_Types.genericDist = Symbolic(#Normal({mean: 20.0, stdev: 2.0})) let normalDist20: DistributionTypes.genericDist = Symbolic(#Normal({mean: 20.0, stdev: 2.0}))
let normalDist: GenericDist_Types.genericDist = normalDist5 let normalDist: DistributionTypes.genericDist = normalDist5
let betaDist: GenericDist_Types.genericDist = Symbolic(#Beta({alpha: 2.0, beta: 5.0})) let betaDist: DistributionTypes.genericDist = Symbolic(#Beta({alpha: 2.0, beta: 5.0}))
let lognormalDist: GenericDist_Types.genericDist = Symbolic(#Lognormal({mu: 0.0, sigma: 1.0})) let lognormalDist: DistributionTypes.genericDist = Symbolic(#Lognormal({mu: 0.0, sigma: 1.0}))
let cauchyDist: GenericDist_Types.genericDist = Symbolic(#Cauchy({local: 1.0, scale: 1.0})) let cauchyDist: DistributionTypes.genericDist = Symbolic(#Cauchy({local: 1.0, scale: 1.0}))
let triangularDist: GenericDist_Types.genericDist = Symbolic( let triangularDist: DistributionTypes.genericDist = Symbolic(
#Triangular({low: 1.0, medium: 2.0, high: 3.0}), #Triangular({low: 1.0, medium: 2.0, high: 3.0}),
) )
let exponentialDist: GenericDist_Types.genericDist = Symbolic(#Exponential({rate: 2.0})) let exponentialDist: DistributionTypes.genericDist = Symbolic(#Exponential({rate: 2.0}))
let uniformDist: GenericDist_Types.genericDist = Symbolic(#Uniform({low: 9.0, high: 10.0})) let uniformDist: DistributionTypes.genericDist = Symbolic(#Uniform({low: 9.0, high: 10.0}))
let floatDist: GenericDist_Types.genericDist = Symbolic(#Float(1e1)) 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)", () => { test("normal(mean=5) + normal(mean=20)", () => {
normalDist5 normalDist5
->algebraicAdd(normalDist20) ->algebraicAdd(normalDist20)
->E.R2.fmap(GenericDist_Types.Constructors.UsingDists.mean) ->E.R2.fmap(DistributionTypes.Constructors.UsingDists.mean)
->E.R2.fmap(run) ->E.R2.fmap(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toExn ->E.R.toExn("Expected float", _)
->expect ->expect
->toBe(Some(2.5e1)) ->toBe(Some(2.5e1))
}) })
@ -57,15 +57,15 @@ describe("(Algebraic) addition of distributions", () => {
let received = let received =
uniformDist uniformDist
->algebraicAdd(betaDist) ->algebraicAdd(betaDist)
->E.R2.fmap(GenericDist_Types.Constructors.UsingDists.mean) ->E.R2.fmap(DistributionTypes.Constructors.UsingDists.mean)
->E.R2.fmap(run) ->E.R2.fmap(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toExn ->E.R.toExn("Expected float", _)
switch received { switch received {
| None => "algebraicAdd has"->expect->toBe("failed") | 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. // 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. // 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)", () => { test("beta(alpha=2, beta=5) + uniform(low=9, high=10)", () => {
@ -74,15 +74,15 @@ describe("(Algebraic) addition of distributions", () => {
let received = let received =
betaDist betaDist
->algebraicAdd(uniformDist) ->algebraicAdd(uniformDist)
->E.R2.fmap(GenericDist_Types.Constructors.UsingDists.mean) ->E.R2.fmap(DistributionTypes.Constructors.UsingDists.mean)
->E.R2.fmap(run) ->E.R2.fmap(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toExn ->E.R.toExn("Expected float", _)
switch received { switch received {
| None => "algebraicAdd has"->expect->toBe("failed") | 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. // 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. // 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 = let received =
normalDist10 // this should be normal(10, sqrt(8)) normalDist10 // this should be normal(10, sqrt(8))
->Ok ->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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toOption ->E.R.toOption
@ -103,7 +103,7 @@ describe("(Algebraic) addition of distributions", () => {
let calculated = let calculated =
normalDist5 normalDist5
->algebraicAdd(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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toOption ->E.R.toOption
@ -126,7 +126,7 @@ describe("(Algebraic) addition of distributions", () => {
let received = let received =
normalDist20 normalDist20
->Ok ->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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toOption ->E.R.toOption
@ -134,7 +134,7 @@ describe("(Algebraic) addition of distributions", () => {
let calculated = let calculated =
normalDist10 normalDist10
->algebraicAdd(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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toOption ->E.R.toOption
@ -155,30 +155,30 @@ describe("(Algebraic) addition of distributions", () => {
let received = let received =
uniformDist uniformDist
->algebraicAdd(betaDist) ->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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toExn ->E.R.toExn("Expected float", _)
switch received { switch received {
| None => "algebraicAdd has"->expect->toBe("failed") | 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. // 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. // 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)", () => { test("(beta(alpha=2, beta=5) + uniform(low=9, high=10)).pdf(10)", () => {
let received = let received =
betaDist betaDist
->algebraicAdd(uniformDist) ->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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toExn ->E.R.toExn("Expected float", _)
switch received { switch received {
| None => "algebraicAdd has"->expect->toBe("failed") | 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. // This is nondeterministic.
// sometimes it works with ~digits=4. | Some(x) => x->expect->toBeSoCloseTo(0.979023, ~digits=0)
| Some(x) => x->expect->toBeSoCloseTo(0.001978994877226945, ~digits=3)
} }
}) })
}) })
@ -187,7 +187,7 @@ describe("(Algebraic) addition of distributions", () => {
let received = let received =
normalDist10 normalDist10
->Ok ->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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toOption ->E.R.toOption
@ -195,7 +195,7 @@ describe("(Algebraic) addition of distributions", () => {
let calculated = let calculated =
normalDist5 normalDist5
->algebraicAdd(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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toOption ->E.R.toOption
@ -217,7 +217,7 @@ describe("(Algebraic) addition of distributions", () => {
let received = let received =
normalDist20 normalDist20
->Ok ->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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toOption ->E.R.toOption
@ -225,7 +225,7 @@ describe("(Algebraic) addition of distributions", () => {
let calculated = let calculated =
normalDist10 normalDist10
->algebraicAdd(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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toOption ->E.R.toOption
@ -246,30 +246,30 @@ describe("(Algebraic) addition of distributions", () => {
let received = let received =
uniformDist uniformDist
->algebraicAdd(betaDist) ->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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toExn ->E.R.toExn("Expected float", _)
switch received { switch received {
| None => "algebraicAdd has"->expect->toBe("failed") | 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. // 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. // The value was calculated externally using a python script
| Some(x) => x->expect->toBeSoCloseTo(0.0013961779932477507, ~digits=3) | Some(x) => x->expect->toBeSoCloseTo(0.71148, ~digits=1)
} }
}) })
test("(beta(alpha=2, beta=5) + uniform(low=9, high=10)).cdf(10)", () => { test("(beta(alpha=2, beta=5) + uniform(low=9, high=10)).cdf(10)", () => {
let received = let received =
betaDist betaDist
->algebraicAdd(uniformDist) ->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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toExn ->E.R.toExn("Expected float", _)
switch received { switch received {
| None => "algebraicAdd has"->expect->toBe("failed") | 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. // 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. // The value was calculated externally using a python script
| Some(x) => x->expect->toBeSoCloseTo(0.001388898111625753, ~digits=3) | Some(x) => x->expect->toBeSoCloseTo(0.71148, ~digits=1)
} }
}) })
}) })
@ -279,7 +279,7 @@ describe("(Algebraic) addition of distributions", () => {
let received = let received =
normalDist10 normalDist10
->Ok ->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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toOption ->E.R.toOption
@ -287,7 +287,7 @@ describe("(Algebraic) addition of distributions", () => {
let calculated = let calculated =
normalDist5 normalDist5
->algebraicAdd(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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toOption ->E.R.toOption
@ -309,7 +309,7 @@ describe("(Algebraic) addition of distributions", () => {
let received = let received =
normalDist20 normalDist20
->Ok ->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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toOption ->E.R.toOption
@ -317,7 +317,7 @@ describe("(Algebraic) addition of distributions", () => {
let calculated = let calculated =
normalDist10 normalDist10
->algebraicAdd(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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toOption ->E.R.toOption
@ -338,30 +338,30 @@ describe("(Algebraic) addition of distributions", () => {
let received = let received =
uniformDist uniformDist
->algebraicAdd(betaDist) ->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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toExn ->E.R.toExn("Expected float", _)
switch received { switch received {
| None => "algebraicAdd has"->expect->toBe("failed") | 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. // 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. // 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)", () => { test("(beta(alpha=2, beta=5) + uniform(low=9, high=10)).inv(2e-2)", () => {
let received = let received =
betaDist betaDist
->algebraicAdd(uniformDist) ->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(run)
->E.R2.fmap(toFloat) ->E.R2.fmap(toFloat)
->E.R.toExn ->E.R.toExn("Expected float", _)
switch received { switch received {
| None => "algebraicAdd has"->expect->toBe("failed") | 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. // 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. // 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 = { module Internals = {
let epsilon = 5e1 let epsilon = 5e1
let mean = GenericDist_Types.Constructors.UsingDists.mean let mean = DistributionTypes.Constructors.UsingDists.mean
let expectImpossiblePath: string => assertion = algebraicOp => let expectImpossiblePath: string => assertion = algebraicOp =>
`${algebraicOp} has`->expect->toEqual("failed") `${algebraicOp} has`->expect->toEqual("failed")
@ -50,7 +50,11 @@ module Internals = {
let dist1 = dist1'->DistributionTypes.Symbolic let dist1 = dist1'->DistributionTypes.Symbolic
let dist2 = dist2'->DistributionTypes.Symbolic let dist2 = dist2'->DistributionTypes.Symbolic
let received = 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)) let expected = floatOp(runMean(dist1), runMean(dist2))
switch received { switch received {
| None => expectImpossiblePath(description) | None => expectImpossiblePath(description)
@ -80,14 +84,16 @@ let {testOperationMean, distributions, pairsOfDifferentDistributions, epsilon} =
describe("Means are invariant", () => { describe("Means are invariant", () => {
describe("for addition", () => { describe("for addition", () => {
let testAdditionMean = testOperationMean(algebraicAdd, "algebraicAdd", \"+.", ~epsilon) 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 => { 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 => { testAll("with two different distributions", pairsOfDifferentDistributions, dists => {
let (dist1, dist2) = dists let (dist1, dist2) = dists
E.R.liftM2(testAdditionMean, dist1, dist2)->E.R.toExn testAddInvariant(dist1, dist2)
}) })
testAll( testAll(
@ -95,7 +101,7 @@ describe("Means are invariant", () => {
pairsOfDifferentDistributions, pairsOfDifferentDistributions,
dists => { dists => {
let (dist1, dist2) = 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, ~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 => { 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 => { testAll("with two different distributions", pairsOfDifferentDistributions, dists => {
let (dist1, dist2) = dists let (dist1, dist2) = dists
E.R.liftM2(testSubtractionMean, dist1, dist2)->E.R.toExn testSubtractInvariant(dist1, dist2)
}) })
testAll( testAll(
@ -122,7 +130,7 @@ describe("Means are invariant", () => {
pairsOfDifferentDistributions, pairsOfDifferentDistributions,
dists => { dists => {
let (dist1, dist2) = 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, ~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 => { 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 => { testAll("with two different distributions", pairsOfDifferentDistributions, dists => {
let (dist1, dist2) = dists let (dist1, dist2) = dists
E.R.liftM2(testMultiplicationMean, dist1, dist2)->E.R.toExn testMultiplicationInvariant(dist1, dist2)
}) })
testAll( testAll(
@ -149,7 +159,7 @@ describe("Means are invariant", () => {
pairsOfDifferentDistributions, pairsOfDifferentDistributions,
dists => { dists => {
let (dist1, dist2) = 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) => let expectParseToBe = (expr: string, answer: string) =>
Reducer.parse(expr)->Expression.toStringResult->expect->toBe(answer) 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) => let expectEvalToBe = (expr: string, answer: string) =>
Reducer.evaluate(expr)->ExpressionValue.toStringResult->expect->toBe(answer) 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 Jest
open Reducer_TestHelpers 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", () => { describe("reducer using mathjs parse", () => {
// Test the MathJs parser compatibility // Test the MathJs parser compatibility
// Those tests toString that there is a semantic mapping from MathJs to Expression // 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", () => { describe("unaryMinus", () => {
testEval("mean(-normal(5,2))", "Ok(-5)") testEval("mean(-normal(5,2))", "Ok(-5)")
testEval("-normal(5,2)", "Ok(Normal(-5,2))")
}) })
describe("to", () => { 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,5)", "Ok(Lognormal(1.1512925464970227,0.27853260523016377))")
testEval("to(-2,2)", "Ok(Normal(0,1.2159136638235384))") testEval("to(-2,2)", "Ok(Normal(0,1.2159136638235384))")
}) })
@ -53,6 +54,7 @@ describe("eval on distribution functions", () => {
describe("subtract", () => { describe("subtract", () => {
testEval("10 - normal(5, 1)", "Ok(Normal(5,1))") testEval("10 - normal(5, 1)", "Ok(Normal(5,1))")
testEval("normal(5, 1) - 10", "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", () => { describe("multiply", () => {
testEval("normal(10, 2) * 2", "Ok(Normal(20,4))") 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(10,2) / lognormal(5,2)", "Ok(Lognormal(5,2.8284271247461903))")
testEval("lognormal(5, 2) / 2", "Ok(Lognormal(4.306852819440055,2))") testEval("lognormal(5, 2) / 2", "Ok(Lognormal(4.306852819440055,2))")
testEval("2 / lognormal(5, 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))") testEval("normal(10, 2) / 2", "Ok(Normal(5,1))")
}) })
describe("truncate", () => { describe("truncate", () => {
@ -77,27 +79,27 @@ describe("eval on distribution functions", () => {
}) })
describe("exp", () => { describe("exp", () => {
testEval("exp(normal(5,2))", "Ok(Point Set Distribution)") testEval("exp(normal(5,2))", "Ok(Sample Set Distribution)")
}) })
describe("pow", () => { describe("pow", () => {
testEval("pow(3, uniform(5,8))", "Ok(Point Set Distribution)") testEval("pow(3, uniform(5,8))", "Ok(Sample Set Distribution)")
testEval("pow(uniform(5,8), 3)", "Ok(Point Set Distribution)") testEval("pow(uniform(5,8), 3)", "Ok(Sample Set Distribution)")
testEval("pow(uniform(5,8), uniform(9, 10))", "Ok(Sample Set Distribution)") testEval("pow(uniform(5,8), uniform(9, 10))", "Ok(Sample Set Distribution)")
}) })
describe("log", () => { describe("log", () => {
testEval("log(2, uniform(5,8))", "Ok(Point Set Distribution)") testEval("log(2, uniform(5,8))", "Ok(Sample Set Distribution)")
testEval("log(normal(5,2), 3)", "Ok(Point Set Distribution)") testEval(
testEval("log(normal(5,2), normal(10,1))", "Ok(Sample Set Distribution)") "log(normal(5,2), 3)",
testEval("log(uniform(5,8))", "Ok(Point Set Distribution)") "Error(Distribution Math Error: Logarithm of input error: First input must be completely greater than 0)",
testEval("log10(uniform(5,8))", "Ok(Point Set Distribution)") )
}) testEval(
"log(normal(5,2), normal(10,1))",
describe("dotLog", () => { "Error(Distribution Math Error: Logarithm of input error: First input must be completely greater than 0)",
testEval("dotLog(normal(5,2), 3)", "Ok(Point Set Distribution)") )
testEval("dotLog(normal(5,2), 3)", "Ok(Point Set Distribution)") testEval("log(uniform(5,8))", "Ok(Sample Set Distribution)")
testEval("dotLog(normal(5,2), normal(10,1))", "Ok(Point Set Distribution)") testEval("log10(uniform(5,8))", "Ok(Sample Set Distribution)")
}) })
describe("dotAdd", () => { describe("dotAdd", () => {

View File

@ -4,7 +4,7 @@ import {
resultMap, resultMap,
squiggleExpression, squiggleExpression,
errorValueToString, errorValueToString,
} from "../src/js/index"; } from "../../src/js/index";
let testRun = (x: string): squiggleExpression => { let testRun = (x: string): squiggleExpression => {
let result = run(x, { sampleCount: 100, xyPointLength: 100 }); 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 //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. //Also, note, the value should be created using makeSampleSetDist() later on.
let env = { sampleCount: 8, xyPointLength: 100 }; 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( let dist = new Distribution(
{ tag: "SampleSet", value: [3, 4, 5, 6, 6, 7, 10, 15, 30] }, { tag: "SampleSet", value: [3, 4, 5, 6, 6, 7, 10, 15, 30] },
env env
@ -56,16 +58,19 @@ describe("Distribution", () => {
); );
test("mean", () => { test("mean", () => {
expect(dist.mean().value).toBeCloseTo(3.737); expect(dist.mean().value).toBeCloseTo(9.5555555);
}); });
test("pdf", () => { test("pdf", () => {
expect(dist.pdf(5.0).value).toBeCloseTo(0.0431); expect(dist.pdf(5.0).value).toBeCloseTo(0.10499097598222966, 1);
}); });
test("cdf", () => { 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", () => { test("inv", () => {
expect(dist.inv(0.5).value).toBeCloseTo(9.458); expect(dist.inv(0.5).value).toBeCloseTo(6);
}); });
test("toPointSet", () => { test("toPointSet", () => {
expect( expect(
@ -73,7 +78,7 @@ describe("Distribution", () => {
).toEqual(Ok("Point Set Distribution")); ).toEqual(Ok("Point Set Distribution"));
}); });
test("toSparkline", () => { test("toSparkline", () => {
expect(dist.toSparkline(20).value).toEqual("▁▁▃▅███▆▄▃▂▁▁▂▂▃▂▁▁▁"); expect(dist.toSparkline(20).value).toEqual("▁▁▃▇█▇▄▂▂▂▁▁▁▁▁▂▂▁▁▁");
}); });
test("algebraicAdd", () => { test("algebraicAdd", () => {
expect( expect(
@ -87,6 +92,6 @@ describe("Distribution", () => {
resultMap(dist.pointwiseAdd(dist2), (r: Distribution) => resultMap(dist.pointwiseAdd(dist2), (r: Distribution) =>
r.toSparkline(20) r.toSparkline(20)
).value ).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 fnImage = (theFn, inps) => Js.Array.map(theFn, inps)
let env: DistributionOperation.env = { let env: DistributionOperation.env = {
sampleCount: 10000, sampleCount: MagicNumbers.Environment.defaultSampleCount,
xyPointLength: 1000, xyPointLength: MagicNumbers.Environment.defaultXYPointLength,
} }
let run = DistributionOperation.run(~env) 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", "suffix": ".bs.js",
"namespace": true, "namespace": true,
"bs-dependencies": [ "bs-dependencies": ["@glennsl/rescript-jest", "bisect_ppx"],
"@glennsl/rescript-jest",
"@glennsl/bs-json",
"rationale",
"bisect_ppx"
],
"gentypeconfig": { "gentypeconfig": {
"language": "typescript", "language": "typescript",
"module": "commonjs", "module": "commonjs",
@ -37,7 +32,7 @@
}, },
"refmt": 3, "refmt": 3,
"warnings": { "warnings": {
"number": "+A-42-48-9-30-4-102-20-27-41" "number": "+A-42-48-9-30-4"
}, },
"ppx-flags": [ "ppx-flags": [
["../../node_modules/bisect_ppx/ppx", "--exclude-files", ".*_test\\.res$$"] ["../../node_modules/bisect_ppx/ppx", "--exclude-files", ".*_test\\.res$$"]

View File

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

View File

@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
# Hat tip to @dfalling # 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 # https://forum.rescript-lang.org/t/rescript-9-1-how-can-we-format-to-standard-out/1590/2?u=quinn-dougherty
@ -38,4 +38,4 @@ then
exit 1 exit 1
else else
echo "All files pass lint" echo "All files pass lint"
fi fi

View File

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

View File

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

View File

@ -1,4 +1,4 @@
type functionCallInfo = GenericDist_Types.Operation.genericFunctionCallInfo type functionCallInfo = DistributionTypes.DistributionOperation.genericFunctionCallInfo
type genericDist = DistributionTypes.genericDist type genericDist = DistributionTypes.genericDist
type error = DistributionTypes.error type error = DistributionTypes.error
@ -120,7 +120,10 @@ let rec run = (~env, functionCallInfo: functionCallInfo): outputType => {
(), (),
)->OutputLocal.toDistR )->OutputLocal.toDistR
let fromDistFn = (subFnName: GenericDist_Types.Operation.fromDist, dist: genericDist) => { let fromDistFn = (
subFnName: DistributionTypes.DistributionOperation.fromDist,
dist: genericDist,
) => {
let response = switch subFnName { let response = switch subFnName {
| ToFloat(distToFloatOperation) => | ToFloat(distToFloatOperation) =>
GenericDist.toFloatOperation(dist, ~toPointSetFn, ~distToFloatOperation) GenericDist.toFloatOperation(dist, ~toPointSetFn, ~distToFloatOperation)
@ -151,20 +154,26 @@ let rec run = (~env, functionCallInfo: functionCallInfo): outputType => {
->GenericDist.toPointSet(~xyPointLength, ~sampleCount, ()) ->GenericDist.toPointSet(~xyPointLength, ~sampleCount, ())
->E.R2.fmap(r => Dist(PointSet(r))) ->E.R2.fmap(r => Dist(PointSet(r)))
->OutputLocal.fromResult ->OutputLocal.fromResult
| ToDistCombination(Algebraic, _, #Float(_)) => GenDistError(NotYetImplemented) | ToDistCombination(Algebraic(_), _, #Float(_)) => GenDistError(NotYetImplemented)
| ToDistCombination(Algebraic, arithmeticOperation, #Dist(t2)) => | ToDistCombination(Algebraic(strategy), arithmeticOperation, #Dist(t2)) =>
dist dist
->GenericDist.algebraicCombination(~toPointSetFn, ~toSampleSetFn, ~arithmeticOperation, ~t2) ->GenericDist.algebraicCombination(
~strategy,
~toPointSetFn,
~toSampleSetFn,
~arithmeticOperation,
~t2,
)
->E.R2.fmap(r => Dist(r)) ->E.R2.fmap(r => Dist(r))
->OutputLocal.fromResult ->OutputLocal.fromResult
| ToDistCombination(Pointwise, arithmeticOperation, #Dist(t2)) => | ToDistCombination(Pointwise, algebraicCombination, #Dist(t2)) =>
dist dist
->GenericDist.pointwiseCombination(~toPointSetFn, ~arithmeticOperation, ~t2) ->GenericDist.pointwiseCombination(~toPointSetFn, ~algebraicCombination, ~t2)
->E.R2.fmap(r => Dist(r)) ->E.R2.fmap(r => Dist(r))
->OutputLocal.fromResult ->OutputLocal.fromResult
| ToDistCombination(Pointwise, arithmeticOperation, #Float(float)) => | ToDistCombination(Pointwise, algebraicCombination, #Float(f)) =>
dist dist
->GenericDist.pointwiseCombinationFloat(~toPointSetFn, ~arithmeticOperation, ~float) ->GenericDist.pointwiseCombinationFloat(~toPointSetFn, ~algebraicCombination, ~f)
->E.R2.fmap(r => Dist(r)) ->E.R2.fmap(r => Dist(r))
->OutputLocal.fromResult ->OutputLocal.fromResult
} }
@ -192,24 +201,24 @@ module Output = {
let fmap = ( let fmap = (
~env, ~env,
input: outputType, input: outputType,
functionCallInfo: GenericDist_Types.Operation.singleParamaterFunction, functionCallInfo: DistributionTypes.DistributionOperation.singleParamaterFunction,
): outputType => { ): outputType => {
let newFnCall: result<functionCallInfo, error> = switch (functionCallInfo, input) { let newFnCall: result<functionCallInfo, error> = switch (functionCallInfo, input) {
| (FromDist(fromDist), Dist(o)) => Ok(FromDist(fromDist, o)) | (FromDist(fromDist), Dist(o)) => Ok(FromDist(fromDist, o))
| (FromFloat(fromDist), Float(o)) => Ok(FromFloat(fromDist, o)) | (FromFloat(fromDist), Float(o)) => Ok(FromFloat(fromDist, o))
| (_, GenDistError(r)) => Error(r) | (_, GenDistError(r)) => Error(r)
| (FromDist(_), _) => Error(Other("Expected dist, got something else")) | (FromDist(_), _) => Error(OtherError("Expected dist, got something else"))
| (FromFloat(_), _) => Error(Other("Expected float, got something else")) | (FromFloat(_), _) => Error(OtherError("Expected float, got something else"))
} }
newFnCall->E.R2.fmap(run(~env))->OutputLocal.fromResult 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 // 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 Constructors = {
module C = GenericDist_Types.Constructors.UsingDists module C = DistributionTypes.Constructors.UsingDists
open OutputLocal open OutputLocal
let mean = (~env, dist) => C.mean(dist)->run(~env)->toFloatR let mean = (~env, dist) => C.mean(dist)->run(~env)->toFloatR
let sample = (~env, dist) => C.sample(dist)->run(~env)->toFloatR let sample = (~env, dist) => C.sample(dist)->run(~env)->toFloatR

View File

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

View File

@ -4,38 +4,62 @@ type genericDist =
| SampleSet(SampleSetDist.t) | SampleSet(SampleSetDist.t)
| Symbolic(SymbolicDistTypes.symbolicDist) | Symbolic(SymbolicDistTypes.symbolicDist)
type asAlgebraicCombinationStrategy = AsDefault | AsSymbolic | AsMonteCarlo | AsConvolution
@genType @genType
type error = type error =
| NotYetImplemented | NotYetImplemented
| Unreachable | Unreachable
| DistributionVerticalShiftIsInvalid | DistributionVerticalShiftIsInvalid
| TooFewSamples
| ArgumentError(string) | 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 = { @genType
type direction = module Error = {
| Algebraic type t = error
| Pointwise
type arithmeticOperation = [ let fromString = (s: string): t => OtherError(s)
| #Add
| #Multiply
| #Subtract
| #Divide
| #Power
| #Logarithm
]
let arithmeticToFn = (arithmetic: arithmeticOperation) => @genType
switch arithmetic { let toString = (err: error): string =>
| #Add => \"+." switch err {
| #Multiply => \"*." | NotYetImplemented => "Function Not Yet Implemented"
| #Subtract => \"-." | Unreachable => "Unreachable"
| #Power => \"**" | DistributionVerticalShiftIsInvalid => "Distribution Vertical Shift is Invalid"
| #Divide => \"/." | ArgumentError(s) => `Argument Error ${s}`
| #Logarithm => (a, b) => log(a) /. log(b) | 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 = [ type toFloat = [
| #Cdf(float) | #Cdf(float)
| #Inv(float) | #Inv(float)
@ -43,9 +67,7 @@ module Operation = {
| #Mean | #Mean
| #Sample | #Sample
] ]
}
module DistributionOperation = {
type toDist = type toDist =
| Normalize | Normalize
| ToPointSet | ToPointSet
@ -55,15 +77,18 @@ module DistributionOperation = {
type toFloatArray = Sample(int) type toFloatArray = Sample(int)
type fromDist = type toBool = IsNormalized
| ToFloat(Operation.toFloat)
| ToDist(toDist) type toString =
| ToDistCombination(
Operation.direction,
Operation.arithmeticOperation,
[#Dist(genericDist) | #Float(float)],
)
| 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 = type singleParamaterFunction =
| FromDist(fromDist) | FromDist(fromDist)
@ -86,8 +111,10 @@ module DistributionOperation = {
| ToDist(ToSampleSet(r)) => `toSampleSet(${E.I.toString(r)})` | ToDist(ToSampleSet(r)) => `toSampleSet(${E.I.toString(r)})`
| ToDist(Truncate(_, _)) => `truncate` | ToDist(Truncate(_, _)) => `truncate`
| ToDist(Inspect) => `inspect` | ToDist(Inspect) => `inspect`
| ToString => `toString` | ToString(ToString) => `toString`
| ToDistCombination(Algebraic, _, _) => `algebraic` | ToString(ToSparkline(n)) => `toSparkline(${E.I.toString(n)})`
| ToBool(IsNormalized) => `isNormalized`
| ToDistCombination(Algebraic(_), _, _) => `algebraic`
| ToDistCombination(Pointwise, _, _) => `pointwise` | ToDistCombination(Pointwise, _, _) => `pointwise`
} }
@ -97,3 +124,71 @@ module DistributionOperation = {
| Mixture(_) => `mixture` | 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 scaleMultiplyFn = (t, float) => result<t, error>
type pointwiseAddFn = (t, t) => 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) => let sampleN = (t: t, n) =>
switch t { switch t {
| PointSet(r) => PointSetDist.sampleNRendered(n, r) | PointSet(r) => PointSetDist.sampleNRendered(n, r)
@ -14,7 +32,7 @@ let sampleN = (t: t, n) =>
} }
let toSampleSetDist = (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)) let fromFloat = (f: float): t => Symbolic(SymbolicDist.Float.make(f))
@ -46,18 +64,25 @@ let toFloatOperation = (
~toPointSetFn: toPointSetFn, ~toPointSetFn: toPointSetFn,
~distToFloatOperation: Operation.distToFloatOperation, ~distToFloatOperation: Operation.distToFloatOperation,
) => { ) => {
let symbolicSolution = switch (t: t) { let trySymbolicSolution = switch (t: t) {
| Symbolic(r) => | Symbolic(r) => SymbolicDist.T.operate(distToFloatOperation, r)->E.R.toOption
switch SymbolicDist.T.operate(distToFloatOperation, r) {
| Ok(f) => Some(f)
| _ => None
}
| _ => None | _ => 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) | 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, t,
~xyPointLength, ~xyPointLength,
~sampleCount, ~sampleCount,
~xSelection: GenericDist_Types.Operation.pointsetXSelection=#ByWeight, ~xSelection: DistributionTypes.DistributionOperation.pointsetXSelection=#ByWeight,
unit, (),
): result<PointSetTypes.pointSetDist, error> => { ): result<PointSetTypes.pointSetDist, error> => {
switch (t: t) { switch (t: t) {
| PointSet(pointSet) => Ok(pointSet) | PointSet(pointSet) => Ok(pointSet)
@ -83,7 +108,7 @@ let toPointSet = (
pointSetDistLength: xyPointLength, pointSetDistLength: xyPointLength,
kernelWidth: None, 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 xyPointLength to be a bit longer than the eventual toSparkline downsampling. I chose 3
fairly arbitrarily. 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 t
->toPointSet(~xSelection=#Linear, ~xyPointLength=bucketCount * 3, ~sampleCount, ()) ->toPointSet(~xSelection=#Linear, ~xyPointLength=bucketCount * 3, ~sampleCount, ())
->E.R.bind(r => ->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 = { 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) { switch (leftCutoff, rightCutoff, t) {
| (None, None, _) => None | (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)))) Some(Symbolic(#Uniform(SymbolicDist.Uniform.truncate(lc, rc, u))))
| _ => None | _ => None
} }
@ -137,85 +168,190 @@ let truncate = Truncate.run
of a new variable that is the result of the operation on A and B. 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). For instance, normal(0, 1) + normal(1, 1) -> normal(1, 2).
In general, this is implemented via convolution. 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 = { module AlgebraicCombination = {
let tryAnalyticalSimplification = ( module InputValidator = {
arithmeticOperation: GenericDist_Types.Operation.arithmeticOperation, /*
t1: t, It would be good to also do a check to make sure that probability mass for the second
t2: t, operand, at value 1.0, is 0 (or approximately 0). However, we'd ideally want to check
): option<result<SymbolicDistTypes.symbolicDist, string>> => that both the probability mass and the probability density are greater than zero.
switch (arithmeticOperation, t1, t2) { Right now we don't yet have a way of getting probability mass, so I'll leave this for later.
| (arithmeticOperation, Symbolic(d1), Symbolic(d2)) => */
switch SymbolicDist.T.tryAnalyticalSimplification(d1, d2, arithmeticOperation) { let getLogarithmInputError = (t1: t, t2: t, ~toPointSetFn: toPointSetFn): option<error> => {
| #AnalyticalSolution(symbolicDist) => Some(Ok(symbolicDist)) let firstOperandIsGreaterThanZero =
| #Error(er) => Some(Error(er)) toFloatOperation(
| #NoSolution => None 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 = ( let run = (t1: t, t2: t, ~toPointSetFn: toPointSetFn, ~arithmeticOperation): option<error> => {
toPointSet: toPointSetFn, if arithmeticOperation == #Logarithm {
arithmeticOperation: GenericDist_Types.Operation.arithmeticOperation, getLogarithmInputError(t1, t2, ~toPointSetFn)
t1: t, } else {
t2: t, None
) => }
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))
} }
//I'm (Ozzie) really just guessing here, very little idea what's best module StrategyCallOnValidatedInputs = {
let expectedConvolutionCost: t => int = x => let convolution = (
switch x { toPointSet: toPointSetFn,
| Symbolic(#Float(_)) => 1 arithmeticOperation: Operation.convolutionOperation,
| Symbolic(_) => 1000 t1: t,
| PointSet(Discrete(m)) => m.xyShape->XYShape.T.length t2: t,
| PointSet(Mixed(_)) => 1000 ): result<t, error> =>
| PointSet(Continuous(_)) => 1000 E.R.merge(toPointSet(t1), toPointSet(t2))
| _ => 1000 ->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) => let symbolic = (
expectedConvolutionCost(t1) * expectedConvolutionCost(t2) > 10000 arithmeticOperation: Operation.algebraicOperation,
? #CalculateWithMonteCarlo t1: t,
: #CalculateWithConvolution 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 = ( let run = (
~strategy: DistributionTypes.asAlgebraicCombinationStrategy,
t1: t, t1: t,
~toPointSetFn: toPointSetFn, ~toPointSetFn: toPointSetFn,
~toSampleSetFn: toSampleSetFn, ~toSampleSetFn: toSampleSetFn,
~arithmeticOperation, ~arithmeticOperation: Operation.algebraicOperation,
~t2: t, ~t2: t,
): result<t, error> => { ): result<t, error> => {
switch tryAnalyticalSimplification(arithmeticOperation, t1, t2) { let invalidOperationError = InputValidator.run(t1, t2, ~arithmeticOperation, ~toPointSetFn)
| Some(Ok(symbolicDist)) => Ok(Symbolic(symbolicDist)) switch (invalidOperationError, strategy) {
| Some(Error(e)) => Error(Other(e)) | (Some(e), _) => Error(e)
| None => | (None, AsDefault) => {
switch chooseConvolutionOrMonteCarlo(t1, t2) { let chooseStrategy = StrategyChooser.run(~arithmeticOperation, ~t1, ~t2)
| #CalculateWithMonteCarlo => runMonteCarlo(toSampleSetFn, arithmeticOperation, t1, t2) runStrategyOnValidatedInputs(
| #CalculateWithConvolution => ~t1,
runConvolution( ~t2,
toPointSetFn, ~strategy=chooseStrategy,
arithmeticOperation, ~arithmeticOperation,
t1, ~toPointSetFn,
t2, ~toSampleSetFn,
)->E.R2.fmap(r => DistributionTypes.PointSet(r)) )
}
| (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 = ( let pointwiseCombination = (
t1: t, t1: t,
~toPointSetFn: toPointSetFn, ~toPointSetFn: toPointSetFn,
~arithmeticOperation, ~algebraicCombination: Operation.algebraicOperation,
~t2: t, ~t2: t,
): result<t, error> => { ): result<t, error> => {
E.R.merge(toPointSetFn(t1), toPointSetFn(t2)) E.R.merge(toPointSetFn(t1), toPointSetFn(t2))->E.R.bind(((t1, t2)) =>
->E.R2.fmap(((t1, t2)) => PointSetDist.combinePointwise(Operation.Algebraic.toFn(algebraicCombination), t1, t2)
PointSetDist.combinePointwise( ->E.R2.fmap(r => DistributionTypes.PointSet(r))
GenericDist_Types.Operation.arithmeticToFn(arithmeticOperation), ->E.R2.errMap(err => DistributionTypes.OperationError(err))
t1,
t2,
)
) )
->E.R2.fmap(r => DistributionTypes.PointSet(r))
} }
let pointwiseCombinationFloat = ( let pointwiseCombinationFloat = (
t: t, t: t,
~toPointSetFn: toPointSetFn, ~toPointSetFn: toPointSetFn,
~arithmeticOperation: GenericDist_Types.Operation.arithmeticOperation, ~algebraicCombination: Operation.algebraicOperation,
~float: float, ~f: float,
): result<t, error> => { ): result<t, error> => {
let m = switch arithmeticOperation { let m = switch algebraicCombination {
| #Add | #Subtract => Error(DistributionTypes.DistributionVerticalShiftIsInvalid) | #Add | #Subtract => Error(DistributionTypes.DistributionVerticalShiftIsInvalid)
| (#Multiply | #Divide | #Power | #Logarithm) as arithmeticOperation => | (#Multiply | #Divide | #Power | #Logarithm) as arithmeticOperation =>
toPointSetFn(t)->E.R2.fmap(t => { toPointSetFn(t)->E.R.bind(t => {
//TODO: Move to PointSet codebase //TODO: Move to PointSet codebase
let fn = (secondary, main) => Operation.Scale.toFn(arithmeticOperation, main, secondary) let fn = (secondary, main) => Operation.Scale.toFn(arithmeticOperation, main, secondary)
let integralSumCacheFn = Operation.Scale.toIntegralSumCacheFn(arithmeticOperation) let integralSumCacheFn = Operation.Scale.toIntegralSumCacheFn(arithmeticOperation)
let integralCacheFn = Operation.Scale.toIntegralCacheFn(arithmeticOperation) let integralCacheFn = Operation.Scale.toIntegralCacheFn(arithmeticOperation)
PointSetDist.T.mapY( PointSetDist.T.mapYResult(
~integralSumCacheFn=integralSumCacheFn(float), ~integralSumCacheFn=integralSumCacheFn(f),
~integralCacheFn=integralCacheFn(float), ~integralCacheFn=integralCacheFn(f),
~fn=fn(float), ~fn=fn(f),
t, t,
) )->E.R2.errMap(x => DistributionTypes.OperationError(x))
}) })
} }
m->E.R2.fmap(r => DistributionTypes.PointSet(r)) m->E.R2.fmap(r => DistributionTypes.PointSet(r))
@ -274,7 +406,7 @@ let mixture = (
~pointwiseAddFn: pointwiseAddFn, ~pointwiseAddFn: pointwiseAddFn,
) => { ) => {
if E.A.length(values) == 0 { 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 { } else {
let totalWeight = values->E.A2.fmap(E.Tuple2.second)->E.A.Floats.sum let totalWeight = values->E.A2.fmap(E.Tuple2.second)->E.A.Floats.sum
let properlyWeightedValues = let properlyWeightedValues =

View File

@ -1,5 +1,5 @@
type t = GenericDist_Types.genericDist type t = DistributionTypes.genericDist
type error = GenericDist_Types.error type error = DistributionTypes.error
type toPointSetFn = t => result<PointSetTypes.pointSetDist, error> type toPointSetFn = t => result<PointSetTypes.pointSetDist, error>
type toSampleSetFn = t => result<SampleSetDist.t, error> type toSampleSetFn = t => result<SampleSetDist.t, error>
type scaleMultiplyFn = (t, float) => result<t, error> type scaleMultiplyFn = (t, float) => result<t, error>
@ -28,7 +28,7 @@ let toPointSet: (
t, t,
~xyPointLength: int, ~xyPointLength: int,
~sampleCount: int, ~sampleCount: int,
~xSelection: GenericDist_Types.Operation.pointsetXSelection=?, ~xSelection: DistributionTypes.DistributionOperation.pointsetXSelection=?,
unit, unit,
) => result<PointSetTypes.pointSetDist, error> ) => result<PointSetTypes.pointSetDist, error>
let toSparkline: (t, ~sampleCount: int, ~bucketCount: int=?, unit) => result<string, error> let toSparkline: (t, ~sampleCount: int, ~bucketCount: int=?, unit) => result<string, error>
@ -42,25 +42,26 @@ let truncate: (
) => result<t, error> ) => result<t, error>
let algebraicCombination: ( let algebraicCombination: (
~strategy: DistributionTypes.asAlgebraicCombinationStrategy,
t, t,
~toPointSetFn: toPointSetFn, ~toPointSetFn: toPointSetFn,
~toSampleSetFn: toSampleSetFn, ~toSampleSetFn: toSampleSetFn,
~arithmeticOperation: GenericDist_Types.Operation.arithmeticOperation, ~arithmeticOperation: Operation.algebraicOperation,
~t2: t, ~t2: t,
) => result<t, error> ) => result<t, error>
let pointwiseCombination: ( let pointwiseCombination: (
t, t,
~toPointSetFn: toPointSetFn, ~toPointSetFn: toPointSetFn,
~arithmeticOperation: GenericDist_Types.Operation.arithmeticOperation, ~algebraicCombination: Operation.algebraicOperation,
~t2: t, ~t2: t,
) => result<t, error> ) => result<t, error>
let pointwiseCombinationFloat: ( let pointwiseCombinationFloat: (
t, t,
~toPointSetFn: toPointSetFn, ~toPointSetFn: toPointSetFn,
~arithmeticOperation: GenericDist_Types.Operation.arithmeticOperation, ~algebraicCombination: Operation.algebraicOperation,
~float: float, ~f: float,
) => result<t, error> ) => result<t, error>
let mixture: ( let mixture: (
@ -68,3 +69,6 @@ let mixture: (
~scaleMultiplyFn: scaleMultiplyFn, ~scaleMultiplyFn: scaleMultiplyFn,
~pointwiseAddFn: pointwiseAddFn, ~pointwiseAddFn: pointwiseAddFn,
) => result<t, error> ) => 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 = ( let combineShapesContinuousContinuous = (
op: Operation.algebraicOperation, op: Operation.convolutionOperation,
s1: PointSetTypes.xyShape, s1: PointSetTypes.xyShape,
s2: PointSetTypes.xyShape, s2: PointSetTypes.xyShape,
): PointSetTypes.xyShape => { ): PointSetTypes.xyShape => {
// if we add the two distributions, we should probably use normal filters. // if we add the two distributions, we should probably use normal filters.
// if we multiply the two distributions, we should probably use lognormal filters. // if we multiply the two distributions, we should probably use lognormal filters.
let t1m = toDiscretePointMassesFromTriangulars(s1) let t1m = toDiscretePointMassesFromTriangulars(s1)
let t2m = switch op { let t2m = toDiscretePointMassesFromTriangulars(~inverse=false, s2)
| #Divide => toDiscretePointMassesFromTriangulars(~inverse=true, s2)
| _ => toDiscretePointMassesFromTriangulars(~inverse=false, s2)
}
let combineMeansFn = switch op { let combineMeansFn = switch op {
| #Add => (m1, m2) => m1 +. m2 | #Add => (m1, m2) => m1 +. m2
| #Subtract => (m1, m2) => m1 -. m2 | #Subtract => (m1, m2) => m1 -. m2
| #Multiply => (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) } // 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 { let combineVariancesFn = switch op {
| #Add => (v1, v2, _, _) => v1 +. v2 | #Add => (v1, v2, _, _) => v1 +. v2
| #Subtract => (v1, v2, _, _) => v1 +. v2 | #Subtract => (v1, v2, _, _) => v1 +. v2
| #Multiply => (v1, v2, m1, m2) => v1 *. v2 +. v1 *. m2 ** 2. +. v2 *. m1 ** 2. | #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 // 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} {n: n, masses: masses, means: means, variances: variances}
} }
type argumentPosition = First | Second
let combineShapesContinuousDiscrete = ( let combineShapesContinuousDiscrete = (
op: Operation.algebraicOperation, op: Operation.convolutionOperation,
continuousShape: PointSetTypes.xyShape, continuousShape: PointSetTypes.xyShape,
discreteShape: PointSetTypes.xyShape, discreteShape: PointSetTypes.xyShape,
~discretePosition: argumentPosition,
): PointSetTypes.xyShape => { ): PointSetTypes.xyShape => {
let t1n = continuousShape |> XYShape.T.length let t1n = continuousShape |> XYShape.T.length
let t2n = discreteShape |> XYShape.T.length let t2n = discreteShape |> XYShape.T.length
// each x pair is added/subtracted // 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) 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. // 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) let dxyShape: array<(float, float)> = Belt.Array.makeUninitializedUnsafe(t1n)
for i in 0 to t1n - 1 { 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( Belt.Array.set(
dxyShape, dxyShape,
i, index,
( (
fn(continuousShape.xs[i], discreteShape.xs[j]), fn(continuousShape.xs[i], discreteShape.xs[j]),
continuousShape.ys[i] *. discreteShape.ys[j], continuousShape.ys[i] *. discreteShape.ys[j],
), ),
) |> ignore ) |> ignore
()
} }
Belt.Array.set(outXYShapes, j, dxyShape) |> ignore Belt.Array.set(outXYShapes, j, dxyShape) |> ignore
() ()
} }
| #Multiply | #Multiply =>
| #Power
| #Logarithm
| #Divide =>
for j in 0 to t2n - 1 { for j in 0 to t2n - 1 {
// creates a new continuous shape for each one of the discrete points, and collects them in outXYShapes. // 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) let dxyShape: array<(float, float)> = Belt.Array.makeUninitializedUnsafe(t1n)
for i in 0 to t1n - 1 { 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( Belt.Array.set(
dxyShape, dxyShape,
i, index,
( (
fn(continuousShape.xs[i], discreteShape.xs[j]), 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 ) |> ignore
() ()
} }
Belt.Array.set(outXYShapes, j, dxyShape) |> ignore Belt.Array.set(outXYShapes, j, dxyShape) |> ignore
()
} }
} }
outXYShapes outXYShapes
|> E.A.fmap(XYShape.T.fromZippedArray) |> E.A.fmap(XYShape.T.fromZippedArray)
|> E.A.fold_left( |> E.A.fold_left(
XYShape.PointwiseCombination.combine( (acc, x) =>
\"+.", XYShape.PointwiseCombination.addCombine(
XYShape.XtoY.continuousInterpolator(#Linear, #UseZero), XYShape.XtoY.continuousInterpolator(#Linear, #UseZero),
), acc,
x,
),
XYShape.T.empty, 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. // Note: This results in a distribution with as many points as the sum of those in t1 and t2.
let combinePointwise = ( let combinePointwise = (
~integralSumCachesFn=(_, _) => None, ~integralSumCachesFn=(_, _) => None,
~integralCachesFn: (t, t) => option<t>=(_, _) => None,
~distributionType: PointSetTypes.distributionType=#PDF, ~distributionType: PointSetTypes.distributionType=#PDF,
fn: (float, float) => float, fn: (float, float) => result<float, Operation.Error.t>,
t1: PointSetTypes.continuousShape, t1: PointSetTypes.continuousShape,
t2: 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 // 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. // can just sum them up. Otherwise, all bets are off.
let combinedIntegralSum = Common.combineIntegralSums( let combinedIntegralSum = Common.combineIntegralSums(
@ -120,9 +119,8 @@ let combinePointwise = (
let interpolator = XYShape.XtoY.continuousInterpolator(t1.interpolation, extrapolation) let interpolator = XYShape.XtoY.continuousInterpolator(t1.interpolation, extrapolation)
make( XYShape.PointwiseCombination.combine(fn, interpolator, t1.xyShape, t2.xyShape)->E.R2.fmap(x =>
~integralSumCache=combinedIntegralSum, make(~integralSumCache=combinedIntegralSum, x)
XYShape.PointwiseCombination.combine(fn, interpolator, t1.xyShape, t2.xyShape),
) )
} }
@ -141,18 +139,47 @@ let updateIntegralSumCache = (integralSumCache, t: t): t => {
let updateIntegralCache = (integralCache, t: t): t => {...t, integralCache: integralCache} let updateIntegralCache = (integralCache, t: t): t => {...t, integralCache: integralCache}
let reduce = ( let sum = (
~integralSumCachesFn: (float, float) => option<float>=(_, _) => None, ~integralSumCachesFn: (float, float) => option<float>=(_, _) => None,
~integralCachesFn: (t, t) => option<t>=(_, _) => None,
fn,
continuousShapes, continuousShapes,
) => ): t =>
continuousShapes |> E.A.fold_left( 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, 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( make(
~interpolation=t.interpolation, ~interpolation=t.interpolation,
~integralSumCache=t.integralSumCache |> E.O.bind(_, integralSumCacheFn), ~integralSumCache=t.integralSumCache |> E.O.bind(_, integralSumCacheFn),
@ -176,6 +203,7 @@ module T = Dist({
let minX = shapeFn(XYShape.T.minX) let minX = shapeFn(XYShape.T.minX)
let maxX = shapeFn(XYShape.T.maxX) let maxX = shapeFn(XYShape.T.maxX)
let mapY = mapY let mapY = mapY
let mapYResult = mapYResult
let updateIntegralCache = updateIntegralCache let updateIntegralCache = updateIntegralCache
let toDiscreteProbabilityMassFraction = _ => 0.0 let toDiscreteProbabilityMassFraction = _ => 0.0
let toPointSetDist = (t: t): PointSetTypes.pointSetDist => Continuous(t) let toPointSetDist = (t: t): PointSetTypes.pointSetDist => Continuous(t)
@ -241,15 +269,21 @@ module T = Dist({
XYShape.Analysis.getVarianceDangerously(t, mean, Analysis.getMeanOfSquares) 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 => let downsampleEquallyOverX = (length, t): t =>
t |> shapeMap(XYShape.XsConversion.proportionEquallyOverX(length)) t |> shapeMap(XYShape.XsConversion.proportionEquallyOverX(length))
/* This simply creates multiple copies of the continuous distribution, scaled and shifted according to /* This simply creates multiple copies of the continuous distribution, scaled and shifted according to
each discrete data point, and then adds them all together. */ each discrete data point, and then adds them all together. */
let combineAlgebraicallyWithDiscrete = ( let combineAlgebraicallyWithDiscrete = (
op: Operation.algebraicOperation, op: Operation.convolutionOperation,
t1: t, t1: t,
t2: PointSetTypes.discreteShape, t2: PointSetTypes.discreteShape,
~discretePosition: AlgebraicShapeCombination.argumentPosition,
) => { ) => {
let t1s = t1 |> getShape 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 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, op,
continuousAsLinear |> getShape, continuousAsLinear |> getShape,
t2s, t2s,
~discretePosition,
) )
let combinedIntegralSum = switch op { let combinedIntegralSum = switch op {
| #Multiply | #Multiply =>
| #Divide =>
Common.combineIntegralSums((a, b) => Some(a *. b), t1.integralSumCache, t2.integralSumCache) Common.combineIntegralSums((a, b) => Some(a *. b), t1.integralSumCache, t2.integralSumCache)
| _ => None | _ => 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 s1 = t1 |> getShape
let s2 = t2 |> getShape let s2 = t2 |> getShape
let t1n = s1 |> XYShape.T.length let t1n = s1 |> XYShape.T.length

View File

@ -34,11 +34,6 @@ let lastY = (t: t) => t |> getShape |> XYShape.T.lastY
let combinePointwise = ( let combinePointwise = (
~integralSumCachesFn=(_, _) => None, ~integralSumCachesFn=(_, _) => None,
~integralCachesFn: (
PointSetTypes.continuousShape,
PointSetTypes.continuousShape,
) => option<PointSetTypes.continuousShape>=(_, _) => None,
fn,
t1: PointSetTypes.discreteShape, t1: PointSetTypes.discreteShape,
t2: PointSetTypes.discreteShape, t2: PointSetTypes.discreteShape,
): PointSetTypes.discreteShape => { ): PointSetTypes.discreteShape => {
@ -54,24 +49,16 @@ let combinePointwise = (
make( make(
~integralSumCache=combinedIntegralSum, ~integralSumCache=combinedIntegralSum,
XYShape.PointwiseCombination.combine( XYShape.PointwiseCombination.combine(
\"+.", (a, b) => Ok(a +. b),
XYShape.XtoY.discreteInterpolator, XYShape.XtoY.discreteInterpolator,
t1.xyShape, t1.xyShape,
t2.xyShape, t2.xyShape,
), )->E.R.toExn("Addition operation should never fail", _),
) )
} }
let reduce = ( let reduce = (~integralSumCachesFn=(_, _) => None, discreteShapes): PointSetTypes.discreteShape =>
~integralSumCachesFn=(_, _) => None, discreteShapes |> E.A.fold_left(combinePointwise(~integralSumCachesFn), empty)
~integralCachesFn=(_, _) => None,
fn,
discreteShapes,
): PointSetTypes.discreteShape =>
discreteShapes |> E.A.fold_left(
combinePointwise(~integralSumCachesFn, ~integralCachesFn, fn),
empty,
)
let updateIntegralSumCache = (integralSumCache, t: t): t => { let updateIntegralSumCache = (integralSumCache, t: 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. /* 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. */ 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 t1s = t1 |> getShape
let t2s = t2 |> getShape let t2s = t2 |> getShape
let t1n = t1s |> XYShape.T.length let t1n = t1s |> XYShape.T.length
@ -97,7 +84,7 @@ let combineAlgebraically = (op: Operation.algebraicOperation, t1: t, t2: t): t =
t2.integralSumCache, t2.integralSumCache,
) )
let fn = Operation.Algebraic.toFn(op) let fn = Operation.Convolution.toFn(op)
let xToYMap = E.FloatFloatMap.empty() let xToYMap = E.FloatFloatMap.empty()
for i in 0 to t1n - 1 { 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) 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( make(
~integralSumCache=t.integralSumCache |> E.O.bind(_, integralSumCacheFn), ~integralSumCache=t.integralSumCache |> E.O.bind(_, integralSumCacheFn),
~integralCache=t.integralCache |> E.O.bind(_, integralCacheFn), ~integralCache=t.integralCache |> E.O.bind(_, integralCacheFn),
@ -156,6 +162,7 @@ module T = Dist({
let maxX = shapeFn(XYShape.T.maxX) let maxX = shapeFn(XYShape.T.maxX)
let toDiscreteProbabilityMassFraction = _ => 1.0 let toDiscreteProbabilityMassFraction = _ => 1.0
let mapY = mapY let mapY = mapY
let mapYResult = mapYResult
let updateIntegralCache = updateIntegralCache let updateIntegralCache = updateIntegralCache
let toPointSetDist = (t: t): PointSetTypes.pointSetDist => Discrete(t) let toPointSetDist = (t: t): PointSetTypes.pointSetDist => Discrete(t)
let toContinuous = _ => None let toContinuous = _ => None

View File

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

View File

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

View File

@ -16,6 +16,13 @@ let fmap = ((fn1, fn2, fn3), t: t): t =>
| Continuous(m) => Continuous(fn3(m)) | 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(( let toMixed = mapToAll((
m => m, m => m,
d => d =>
@ -35,13 +42,24 @@ let toMixed = mapToAll((
)) ))
//TODO WARNING: The combineAlgebraicallyWithDiscrete will break for subtraction and division, like, discrete - continous //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) { switch (t1, t2) {
| (Continuous(m1), Continuous(m2)) => | (Continuous(m1), Continuous(m2)) =>
Continuous.combineAlgebraically(op, m1, m2) |> Continuous.T.toPointSetDist Continuous.combineAlgebraically(op, m1, m2) |> Continuous.T.toPointSetDist
| (Continuous(m1), Discrete(m2)) | (Discrete(m1), Continuous(m2)) =>
| (Discrete(m2), Continuous(m1)) => Continuous.combineAlgebraicallyWithDiscrete(
Continuous.combineAlgebraicallyWithDiscrete(op, m1, m2) |> Continuous.T.toPointSetDist 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(m1), Discrete(m2)) =>
Discrete.combineAlgebraically(op, m1, m2) |> Discrete.T.toPointSetDist Discrete.combineAlgebraically(op, m1, m2) |> Discrete.T.toPointSetDist
| (m1, m2) => Mixed.combineAlgebraically(op, toMixed(m1), toMixed(m2)) |> Mixed.T.toPointSetDist | (m1, m2) => Mixed.combineAlgebraically(op, toMixed(m1), toMixed(m2)) |> Mixed.T.toPointSetDist
@ -53,23 +71,28 @@ let combinePointwise = (
PointSetTypes.continuousShape, PointSetTypes.continuousShape,
PointSetTypes.continuousShape, PointSetTypes.continuousShape,
) => option<PointSetTypes.continuousShape>=(_, _) => None, ) => option<PointSetTypes.continuousShape>=(_, _) => None,
fn, fn: (float, float) => result<float, Operation.Error.t>,
t1: t, t1: t,
t2: t, t2: t,
) => ): result<PointSetTypes.pointSetDist, Operation.Error.t> =>
switch (t1, t2) { switch (t1, t2) {
| (Continuous(m1), Continuous(m2)) => | (Continuous(m1), Continuous(m2)) =>
PointSetTypes.Continuous( Continuous.combinePointwise(
Continuous.combinePointwise(~integralSumCachesFn, ~integralCachesFn, fn, m1, m2), ~integralSumCachesFn,
) fn,
m1,
m2,
)->E.R2.fmap(x => PointSetTypes.Continuous(x))
| (Discrete(m1), Discrete(m2)) => | (Discrete(m1), Discrete(m2)) =>
PointSetTypes.Discrete( Ok(PointSetTypes.Discrete(Discrete.combinePointwise(~integralSumCachesFn, m1, m2)))
Discrete.combinePointwise(~integralSumCachesFn, ~integralCachesFn, fn, m1, m2),
)
| (m1, m2) => | (m1, m2) =>
PointSetTypes.Mixed( Mixed.combinePointwise(
Mixed.combinePointwise(~integralSumCachesFn, ~integralCachesFn, fn, toMixed(m1), toMixed(m2)), ~integralSumCachesFn,
) ~integralCachesFn,
fn,
toMixed(m1),
toMixed(m2),
)->E.R2.fmap(x => PointSetTypes.Mixed(x))
} }
module T = Dist({ module T = Dist({
@ -134,10 +157,8 @@ module T = Dist({
let integralYtoX = f => let integralYtoX = f =>
mapToAll((Mixed.T.Integral.yToX(f), Discrete.T.Integral.yToX(f), Continuous.T.Integral.yToX(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 maxX = mapToAll((Mixed.T.maxX, Discrete.T.maxX, Continuous.T.maxX))
let mapY = ( let mapY = (~integralSumCacheFn=_ => None, ~integralCacheFn=_ => None, ~fn: float => float): (
~integralSumCacheFn=previousIntegralSum => None, t => t
~integralCacheFn=previousIntegral => None,
~fn,
) => ) =>
fmap(( fmap((
Mixed.T.mapY(~integralSumCacheFn, ~integralCacheFn, ~fn), Mixed.T.mapY(~integralSumCacheFn, ~integralCacheFn, ~fn),
@ -145,6 +166,17 @@ module T = Dist({
Continuous.T.mapY(~integralSumCacheFn, ~integralCacheFn, ~fn), 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 => let mean = (t: t): float =>
switch t { switch t {
| Mixed(m) => Mixed.T.mean(m) | Mixed(m) => Mixed.T.mean(m)
@ -203,8 +235,8 @@ let operate = (distToFloatOp: Operation.distToFloatOperation, s): float =>
| #Mean => T.mean(s) | #Mean => T.mean(s)
} }
let toSparkline = (t: t, bucketCount) => let toSparkline = (t: t, bucketCount): result<string, PointSetTypes.sparklineError> =>
T.toContinuous(t) T.toContinuous(t)
->E.O2.fmap(Continuous.downsampleEquallyOverX(bucketCount)) ->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()) ->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) 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)) { if (_.isFinite(max)) {
_samples = _.filter(_samples, (r) => r < 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 }); 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 = { 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 is used as a smart constructor. The only way to create a SampleSetDist.t is to call
this constructor. this constructor.
@ -8,7 +29,7 @@ module T: {
//When we get a good functional library in TS, we could refactor that out. //When we get a good functional library in TS, we could refactor that out.
@genType @genType
type t = array<float> type t = array<float>
let make: array<float> => result<t, string> let make: array<float> => result<t, sampleSetError>
let get: t => array<float> let get: t => array<float>
} = { } = {
type t = array<float> type t = array<float>
@ -16,7 +37,7 @@ module T: {
if E.A.length(a) > 5 { if E.A.length(a) > 5 {
Ok(a) Ok(a)
} else { } else {
Error("too small") Error(TooFewSamples)
} }
let get = (a: t) => a let get = (a: t) => a
} }
@ -31,13 +52,13 @@ some refactoring.
*/ */
let toPointSetDist = (~samples: t, ~samplingInputs: SamplingInputs.samplingInputs): result< let toPointSetDist = (~samples: t, ~samplingInputs: SamplingInputs.samplingInputs): result<
PointSetTypes.pointSetDist, PointSetTypes.pointSetDist,
string, pointsetConversionError,
> => > =>
SampleSetDist_ToPointSet.toPointSetDist( SampleSetDist_ToPointSet.toPointSetDist(
~samples=get(samples), ~samples=get(samples),
~samplingInputs, ~samplingInputs,
(), (),
).pointSetDist->E.O2.toResult("Failed to convert to PointSetDist") ).pointSetDist->E.O2.toResult(TooFewSamplesForConversionToPointSet)
//Randomly get one sample from the distribution //Randomly get one sample from the distribution
let sample = (t: t): float => { 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. //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)) 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 = { module T = {
type t = array<float> 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 xWidthToUnitWidth = (samples, outputXYPoints, xWidth) => {
let xyPointRange = E.A.Sorted.range(samples) |> E.O.default(0.0) let xyPointRange = E.A.Sorted.range(samples) |> E.O.default(0.0)
let xyPointWidth = xyPointRange /. float_of_int(outputXYPoints) let xyPointWidth = xyPointRange /. float_of_int(outputXYPoints)
@ -83,9 +61,13 @@ let toPointSetDist = (
~samples: Internals.T.t, ~samples: Internals.T.t,
~samplingInputs: SamplingInputs.samplingInputs, ~samplingInputs: SamplingInputs.samplingInputs,
(), (),
) => { ): Internals.Types.outputs => {
Array.fast_sort(compare, samples) 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 length = samples |> E.A.length |> float_of_int
let discrete: PointSetTypes.discreteShape = let discrete: PointSetTypes.discreteShape =
discretePart discretePart
@ -133,9 +115,17 @@ let toPointSetDist = (
~discrete=Some(discrete), ~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 = { let samplesParse: Internals.Types.outputs = {
continuousParseParams: pdf |> E.O.fmap(snd), continuousParseParams: pdf |> E.O.fmap(snd),
pointSetDist: pointSetDist, pointSetDist: normalizedPointSet,
} }
samplesParse samplesParse

View File

@ -52,7 +52,7 @@ module Normal = {
switch operation { switch operation {
| #Add => Some(#Normal({mean: n1 +. n2.mean, stdev: n2.stdev})) | #Add => Some(#Normal({mean: n1 +. n2.mean, stdev: n2.stdev}))
| #Subtract => 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 | _ => None
} }
@ -60,8 +60,8 @@ module Normal = {
switch operation { switch operation {
| #Add => Some(#Normal({mean: n1.mean +. n2, stdev: n1.stdev})) | #Add => Some(#Normal({mean: n1.mean +. n2, stdev: n1.stdev}))
| #Subtract => 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})) | #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 /. n2})) | #Divide => Some(#Normal({mean: n1.mean /. n2, stdev: n1.stdev /. Js.Math.abs_float(n2)}))
| _ => None | _ => None
} }
} }
@ -86,7 +86,10 @@ module Exponential = {
module Cauchy = { module Cauchy = {
type t = 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 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 cdf = (x, t: t) => Jstat.Cauchy.cdf(x, t.local, t.scale)
let inv = (p, t: t) => Jstat.Cauchy.inv(p, t.local, t.scale) let inv = (p, t: t) => Jstat.Cauchy.inv(p, t.local, t.scale)
@ -377,7 +380,7 @@ module T = {
): analyticalSimplificationResult => ): analyticalSimplificationResult =>
switch (d1, d2) { switch (d1, d2) {
| (#Float(v1), #Float(v2)) => | (#Float(v1), #Float(v2)) =>
switch Operation.Algebraic.applyFn(op, v1, v2) { switch Operation.Algebraic.toFn(op, v1, v2) {
| Ok(r) => #AnalyticalSolution(#Float(r)) | Ok(r) => #AnalyticalSolution(#Float(r))
| Error(n) => #Error(n) | Error(n) => #Error(n)
} }

View File

@ -45,6 +45,6 @@ type symbolicDist = [
type analyticalSimplificationResult = [ type analyticalSimplificationResult = [
| #AnalyticalSolution(symbolicDist) | #AnalyticalSolution(symbolicDist)
| #Error(string) | #Error(Operation.Error.t)
| #NoSolution | #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 module MathJs = Reducer_MathJs
type expressionValue = Reducer_Expression.expressionValue type expressionValue = Reducer_Expression.expressionValue
type externalBindings = Expression.externalBindings
let evaluate = Expression.eval let evaluate = Expression.eval
let evaluateUsingExternalBindings = Expression.evalUsingExternalBindings
let evaluatePartialUsingExternalBindings = Expression.evalPartialUsingExternalBindings
let parse = Expression.parse let parse = Expression.parse
let parseOuter = Expression.parseOuter
let parsePartial = Expression.parsePartial

View File

@ -7,7 +7,20 @@ module MathJs = Reducer_MathJs
@genType @genType
type expressionValue = ReducerInterface_ExpressionValue.expressionValue type expressionValue = ReducerInterface_ExpressionValue.expressionValue
@genType
type externalBindings = ReducerInterface_ExpressionValue.externalBindings
@genType @genType
let evaluate: string => result<expressionValue, Reducer_ErrorValue.errorValue> 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 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 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) | RERecordPropertyNotFound(string, string)
| RESymbolNotFound(string) | RESymbolNotFound(string)
| RESyntaxError(string) | RESyntaxError(string)
| REDistributionError(DistributionTypes.error)
| RETodo(string) // To do | RETodo(string) // To do
type t = errorValue type t = errorValue
@ -20,6 +21,7 @@ let errorToString = err =>
| REAssignmentExpected => "Assignment expected" | REAssignmentExpected => "Assignment expected"
| REExpressionExpected => "Expression expected" | REExpressionExpected => "Expression expected"
| REFunctionExpected(msg) => `Function expected: ${msg}` | REFunctionExpected(msg) => `Function expected: ${msg}`
| REDistributionError(err) => `Distribution Math Error: ${DistributionTypes.Error.toString(err)}`
| REJavaScriptExn(omsg, oname) => { | REJavaScriptExn(omsg, oname) => {
let answer = "JS Exception:" let answer = "JS Exception:"
let answer = switch oname { let answer = switch oname {

View File

@ -15,7 +15,7 @@ type t = expression
*/ */
let rec toString = expression => let rec toString = expression =>
switch expression { switch expression {
| T.EBindings(bindings) => "$$bound" | T.EBindings(_) => "$$bound"
| T.EList(aList) => | T.EList(aList) =>
`(${Belt.List.map(aList, aValue => toString(aValue)) `(${Belt.List.map(aList, aValue => toString(aValue))
->Extra.List.interperse(" ") ->Extra.List.interperse(" ")
@ -39,12 +39,26 @@ let parse_ = (expr: string, parser, converter): result<t, errorValue> =>
let parse = (mathJsCode: string): result<t, errorValue> => let parse = (mathJsCode: string): result<t, errorValue> =>
mathJsCode->parse_(MathJs.Parse.parse, MathJs.ToExpression.fromNode) 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 let defaultBindings: T.bindings = Belt.Map.String.empty
/* /*
Recursively evaluate/reduce the expression (Lisp AST) Recursively evaluate/reduce the expression (Lisp AST)
*/ */
let rec reduceExpression = (expression: t, bindings: T.bindings): result<expressionValue, 'e> => { 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 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 | _ => 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> => let rec seekMacros = (expression: t, bindings: T.bindings): result<t, 'e> =>
switch expression { switch expression {
| T.EValue(value) => expression->Ok | T.EValue(_value) => expression->Ok
| T.EBindings(_value) => expression->Ok
| T.EList(list) => { | T.EList(list) => {
let racc: result<list<t>, 'e> = list->Belt.List.reduceReverse(Ok(list{}), ( let racc: result<list<t>, 'e> = list->Belt.List.reduceReverse(Ok(list{}), (
racc, racc,
@ -155,6 +107,7 @@ let rec reduceExpression = (expression: t, bindings: T.bindings): result<express
) )
racc->Result.flatMap(acc => acc->reduceValueList) 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) 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) 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) => { let evalPartialUsingExternalBindings_ = (codeText: string, bindings: T.bindings) => {
parse(codeText)->Result.flatMap(code => code->evalWBindingsExpression(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. 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 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 Call MathJs evaluate and return as a variant

View File

@ -9,6 +9,22 @@ type expression = ExpressionT.expression
type expressionValue = ExpressionValue.expressionValue type expressionValue = ExpressionValue.expressionValue
type errorValue = ErrorValue.errorValue 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> => let rec fromNode = (mathJsNode: Parse.node): result<expression, errorValue> =>
Parse.castNodeType(mathJsNode)->Result.flatMap(typedMathJsNode => { Parse.castNodeType(mathJsNode)->Result.flatMap(typedMathJsNode => {
let fromNodeList = (nodeList: list<Parse.node>): result<list<expression>, 'e> => 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 => let toEvSymbolValue = (name: string): expression =>
name->ExpressionValue.EvSymbol->ExpressionT.EValue 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 caseFunctionNode = fNode => {
let lispArgs = fNode["args"]->Belt.List.fromArray->fromNodeList let lispArgs = fNode["args"]->Belt.List.fromArray->fromNodeList
passToFunction(fNode->Parse.nameOfFunctionNode, lispArgs) 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)) 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 { let rFinalExpression: result<expression, errorValue> = switch typedMathJsNode {
| MjAccessorNode(aNode) => caseAccessorNode(aNode["object"], aNode["index"]) | MjAccessorNode(aNode) => caseAccessorNode(aNode["object"], aNode["index"])
| MjArrayNode(aNode) => caseArrayNode(aNode) | MjArrayNode(aNode) => caseArrayNode(aNode)
@ -124,8 +112,7 @@ let rec fromNode = (mathJsNode: Parse.node): result<expression, errorValue> =>
let rExpr: result<expression, errorValue> = expr->Ok let rExpr: result<expression, errorValue> = expr->Ok
rExpr rExpr
} }
| MjBlockNode(bNode) => caseBlockNode(bNode) | MjBlockNode(bNode) => bNode["blocks"]->Belt.Array.map(toTagOrNode)->caseTagOrNodes
// | MjBlockNode(bNode) => "statement"->toEvSymbolValue->Ok
| MjConstantNode(cNode) => | MjConstantNode(cNode) =>
cNode["value"]->JavaScript.Gate.jsToEv->Result.flatMap(v => v->ExpressionT.EValue->Ok) cNode["value"]->JavaScript.Gate.jsToEv->Result.flatMap(v => v->ExpressionT.EValue->Ok)
| MjFunctionNode(fNode) => fNode->caseFunctionNode | MjFunctionNode(fNode) => fNode->caseFunctionNode
@ -136,3 +123,73 @@ let rec fromNode = (mathJsNode: Parse.node): result<expression, errorValue> =>
} }
rFinalExpression 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>) | EvArray(array<expressionValue>)
| EvBool(bool) | EvBool(bool)
| EvCall(string) // External function call | EvCall(string) // External function call
| EvDistribution(GenericDist_Types.genericDist) | EvDistribution(DistributionTypes.genericDist)
| EvNumber(float) | EvNumber(float)
| EvRecord(Js.Dict.t<expressionValue>) | EvRecord(Js.Dict.t<expressionValue>)
| EvString(string) | EvString(string)
| EvSymbol(string) | EvSymbol(string)
@genType
type externalBindings = Js.Dict.t<expressionValue>
type functionCall = (string, array<expressionValue>) type functionCall = (string, array<expressionValue>)
let rec toString = aValue => let rec toString = aValue =>
@ -33,17 +36,18 @@ let rec toString = aValue =>
->Js.String.concatMany("") ->Js.String.concatMany("")
`[${args}]` `[${args}]`
} }
| EvRecord(aRecord) => { | EvRecord(aRecord) => aRecord->toStringRecord
let pairs =
aRecord
->Js.Dict.entries
->Belt.Array.map(((eachKey, eachValue)) => `${eachKey}: ${toString(eachValue)}`)
->Extra_Array.interperse(", ")
->Js.String.concatMany("")
`{${pairs}}`
}
| EvDistribution(dist) => GenericDist.toString(dist) | 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 => let toStringWithType = aValue =>
switch aValue { switch aValue {
@ -68,3 +72,9 @@ let toStringResult = x =>
| Ok(a) => `Ok(${toString(a)})` | Ok(a) => `Ok(${toString(a)})`
| Error(m) => `Error(${ErrorValue.errorToString(m)})` | 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 type expressionValue = ExpressionValue.expressionValue
module Sample = { // module Sample = {
// In real life real libraries should be somewhere else // // In real life real libraries should be somewhere else
/* // /*
For an example of mapping polymorphic custom functions. To be deleted after real integration // For an example of mapping polymorphic custom functions. To be deleted after real integration
*/ // */
let customAdd = (a: float, b: float): float => {a +. b} // let customAdd = (a: float, b: float): float => {a +. b}
} // }
/* /*
Map external calls of Reducer Map external calls of Reducer

View File

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

View File

@ -31,6 +31,9 @@ let makeSampleSetDist = SampleSetDist.make
@genType @genType
let evaluate = Reducer.evaluate let evaluate = Reducer.evaluate
@genType
let evaluateUsingExternalBindings = Reducer.evaluateUsingExternalBindings
@genType @genType
type expressionValue = ReducerInterface_ExpressionValue.expressionValue type expressionValue = ReducerInterface_ExpressionValue.expressionValue
@ -53,4 +56,4 @@ type continuousShape = PointSetTypes.continuousShape
let errorValueToString = Reducer_ErrorValue.errorToString let errorValueToString = Reducer_ErrorValue.errorToString
@genType @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 FloatFloatMap = {
module Id = Belt.Id.MakeComparable({ module Id = Belt.Id.MakeComparable({
type t = float type t = float
@ -8,7 +11,7 @@ module FloatFloatMap = {
type t = Belt.MutableMap.t<Id.t, float, Id.identity> type t = Belt.MutableMap.t<Id.t, float, Id.identity>
let fromArray = (ar: array<(float, float)>) => Belt.MutableMap.fromArray(ar, ~id=module(Id)) 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 empty = () => Belt.MutableMap.make(~id=module(Id))
let increment = (el, t: t) => let increment = (el, t: t) =>
Belt.MutableMap.update(t, el, x => Belt.MutableMap.update(t, el, x =>
@ -20,6 +23,10 @@ module FloatFloatMap = {
let get = (el, t: t) => Belt.MutableMap.get(t, el) let get = (el, t: t) => Belt.MutableMap.get(t, el)
let fmap = (fn, t: t) => Belt.MutableMap.map(t, fn) 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 = { module Int = {
@ -28,7 +35,7 @@ module Int = {
} }
/* Utils */ /* Utils */
module U = { module U = {
let isEqual = (a, b) => a == b let isEqual = \"=="
let toA = a => [a] let toA = a => [a]
let id = e => e let id = e => e
} }
@ -51,17 +58,59 @@ module O = {
| None => rFn() | None => rFn()
} }
() ()
let fmap = Rationale.Option.fmap let fmap = (f: 'a => 'b, x: option<'a>): option<'b> => {
let bind = Rationale.Option.bind switch x {
let default = Rationale.Option.default | None => None
let isSome = Rationale.Option.isSome | Some(x') => Some(f(x'))
let isNone = Rationale.Option.isNone }
let toExn = Rationale.Option.toExn }
let some = Rationale.Option.some let bind = (o, f) =>
let firstSome = Rationale.Option.firstSome switch o {
let toExt = Rationale.Option.toExn // wanna flag this-- looks like a typo but `Rationale.OptiontoExt` doesn't exist. | None => None
let flatApply = (fn, b) => Rationale.Option.apply(fn, Some(b)) |> Rationale.Option.flatten | Some(a) => f(a)
let flatten = Rationale.Option.flatten }
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 => let toBool = opt =>
switch opt { switch opt {
@ -109,6 +158,11 @@ module O2 = {
/* Functions */ /* Functions */
module F = { 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 apply = (a, e) => a |> e
let flatten2Callbacks = (fn1, fn2, fnlast) => let flatten2Callbacks = (fn1, fn2, fnlast) =>
@ -152,13 +206,35 @@ module I = {
let toString = Js.Int.toString let toString = Js.Int.toString
} }
exception Assertion(string)
/* R for Result */ /* R for Result */
module R = { 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 id = e => e |> result(U.id, U.id)
let fmap = Rationale.Result.fmap let fmap = (f: 'a => 'b, r: result<'a, 'c>): result<'b, 'c> => {
let bind = Rationale.Result.bind switch r {
let toExn = Belt.Result.getExn | 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>) => let default = (default, res: Belt.Result.t<'a, 'b>) =>
switch res { switch res {
| Ok(r) => r | Ok(r) => r
@ -179,13 +255,17 @@ module R = {
let errorIfCondition = (errorCondition, errorMessage, r) => let errorIfCondition = (errorCondition, errorMessage, r) =>
errorCondition(r) ? Error(errorMessage) : Ok(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) => let ap' = (r, a) =>
switch r { switch r {
| Ok(f) => fmap(f, a) | Ok(f) => fmap(f, a)
| Error(err) => Error(err) | 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) => { let liftM2: (('a, 'b) => 'c, result<'a, 'd>, result<'b, 'd>) => result<'c, 'd> = (op, xR, yR) => {
ap'(fmap(op, xR), yR) ap'(fmap(op, xR), yR)
} }
@ -210,10 +290,10 @@ module R2 = {
let bind = (a, b) => R.bind(b, a) let bind = (a, b) => R.bind(b, a)
//Converts result type to change error type only //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 { switch a {
| Ok(r) => Ok(r) | Ok(r) => Ok(r)
| Error(e) => map(e) | Error(e) => Error(map(e))
} }
let fmap2 = (xR, f) => let fmap2 = (xR, f) =>
@ -235,7 +315,7 @@ module S = {
} }
module J = { module J = {
let toString = \"||>"(Js.Json.decodeString, O.default("")) let toString = F.pipe(Js.Json.decodeString, O.default(""))
let fromString = Js.Json.string let fromString = Js.Json.string
let fromNumber = Js.Json.number let fromNumber = Js.Json.number
@ -248,7 +328,7 @@ module J = {
let toString = (str: option<'a>) => let toString = (str: option<'a>) =>
switch str { switch str {
| Some(str) => Some(str |> \"||>"(Js.Json.decodeString, O.default(""))) | Some(str) => Some(str |> F.pipe(Js.Json.decodeString, O.default("")))
| _ => None | _ => None
} }
} }
@ -263,34 +343,132 @@ module JsDate = {
/* List */ /* List */
module L = { module L = {
module Util = {
let eq = (a, b) => a == b
}
let fmap = List.map let fmap = List.map
let get = Belt.List.get let get = Belt.List.get
let toArray = Array.of_list let toArray = Array.of_list
let fmapi = List.mapi let fmapi = List.mapi
let concat = List.concat let concat = List.concat
let drop = Rationale.RList.drop let concat' = (xs, ys) => List.append(ys, xs)
let remove = Rationale.RList.remove
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 find = List.find
let filter = List.filter let filter = List.filter
let for_all = List.for_all let for_all = List.for_all
let exists = List.exists let exists = List.exists
let sort = List.sort let sort = List.sort
let length = List.length let length = List.length
let filter_opt = Rationale.RList.filter_opt
let uniqBy = Rationale.RList.uniqBy let filter_opt = xs => {
let join = Rationale.RList.join let rec loop = (l, acc) =>
let head = Rationale.RList.head switch l {
let uniq = Rationale.RList.uniq | 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 flatten = List.flatten
let last = Rationale.RList.last let last = xs => xs |> List.rev |> head
let append = List.append let append = List.append
let getBy = Belt.List.getBy let getBy = Belt.List.getBy
let dropLast = Rationale.RList.dropLast let dropLast = (i, xs) => take(List.length(xs) - i, xs)
let contains = Rationale.RList.contains let containsWith = f => List.exists(f)
let without = Rationale.RList.without let contains = x => containsWith(Util.eq(x))
let update = Rationale.RList.update
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 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 headSafe = Belt.List.head
let tailSafe = Belt.List.tail let tailSafe = Belt.List.tail
let headExn = Belt.List.headExn let headExn = Belt.List.headExn
@ -340,8 +518,6 @@ module A = {
let reduce = Belt.Array.reduce let reduce = Belt.Array.reduce
let reducei = Belt.Array.reduceWithIndex let reducei = Belt.Array.reduceWithIndex
let isEmpty = r => length(r) < 1 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 stableSortBy = Belt.SortArray.stableSortBy
let toRanges = (a: array<'a>) => let toRanges = (a: array<'a>) =>
switch a |> Belt.Array.length { switch a |> Belt.Array.length {
@ -354,9 +530,12 @@ module A = {
Belt.Array.getUnsafe(a, index), Belt.Array.getUnsafe(a, index),
Belt.Array.getUnsafe(a, index + 1), 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. // This zips while taking the longest elements of each array.
let zipMaxLength = (array1, array2) => { let zipMaxLength = (array1, array2) => {
let maxLength = Int.max(length(array1), length(array2)) let maxLength = Int.max(length(array1), length(array2))
@ -415,8 +594,8 @@ module A = {
module O = { module O = {
let concatSomes = (optionals: array<option<'a>>): array<'a> => let concatSomes = (optionals: array<option<'a>>): array<'a> =>
optionals optionals
|> Js.Array.filter(Rationale.Option.isSome) |> Js.Array.filter(O.isSome)
|> Js.Array.map(Rationale.Option.toExn("Warning: This should not have happened")) |> Js.Array.map(O.toExn("Warning: This should not have happened"))
let defaultEmpty = (o: option<array<'a>>): array<'a> => let defaultEmpty = (o: option<array<'a>>): array<'a> =>
switch o { switch o {
| Some(o) => o | Some(o) => o
@ -438,6 +617,32 @@ module A = {
r |> Belt.Array.map(_, r => Belt.Result.getExn(r)) r |> Belt.Array.map(_, r => Belt.Result.getExn(r))
bringErrorUp |> Belt.Result.map(_, forceOpen) 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 = { module Sorted = {
@ -448,8 +653,11 @@ module A = {
| (Some(min), Some(max)) => Some(max -. min) | (Some(min), Some(max)) => Some(max -. min)
| _ => None | _ => None
} }
let floatCompare: (float, float) => int = compare
let binarySearchFirstElementGreaterIndex = (ar: array<'a>, el: 'a) => { 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 let el = el < 0 ? el * -1 - 1 : el
switch el { switch el {
| e if e >= length(ar) => #overMax | e if e >= length(ar) => #overMax
@ -460,25 +668,33 @@ module A = {
let concat = (t1: array<'a>, t2: array<'a>) => { let concat = (t1: array<'a>, t2: array<'a>) => {
let ts = Belt.Array.concat(t1, t2) let ts = Belt.Array.concat(t1, t2)
ts |> Array.fast_sort(compare) ts |> Array.fast_sort(floatCompare)
ts ts
} }
let concatMany = (t1: array<array<'a>>) => { let concatMany = (t1: array<array<'a>>) => {
let ts = Belt.Array.concatMany(t1) let ts = Belt.Array.concatMany(t1)
ts |> Array.fast_sort(compare) ts |> Array.fast_sort(floatCompare)
ts ts
} }
module Floats = { module Floats = {
let isSorted = (ar: array<float>): bool =>
reduce(zip(ar, tail(ar)), true, (acc, (first, second)) => acc && first < second)
let makeIncrementalUp = (a, b) => let makeIncrementalUp = (a, b) =>
Array.make(b - a + 1, a) |> Array.mapi((i, c) => c + i) |> Belt.Array.map(_, float_of_int) Array.make(b - a + 1, a) |> Array.mapi((i, c) => c + i) |> Belt.Array.map(_, float_of_int)
let makeIncrementalDown = (a, b) => let makeIncrementalDown = (a, b) =>
Array.make(a - b + 1, a) |> Array.mapi((i, c) => c - i) |> Belt.Array.map(_, float_of_int) 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() let discrete = FloatFloatMap.empty()
Belt.Array.forEachWithIndex(sortedArray, (index, element) => { Belt.Array.forEachWithIndex(sortedArray, (index, element) => {
let maxIndex = (sortedArray |> Array.length) - 1 let maxIndex = (sortedArray |> Array.length) - 1
@ -499,14 +715,48 @@ module A = {
(continuous, discrete) (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 = { module Floats = {
let sum = Belt.Array.reduce(_, 0., (i, j) => i +. j) let mean = Jstat.mean
let mean = a => sum(a) /. (Array.length(a) |> float_of_int) 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 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 // Gives an array with all the differences between values
// diff([1,5,3,7]) = [4,-2,4] // diff([1,5,3,7]) = [4,-2,4]
let diff = (arr: array<float>): array<float> => let diff = (arr: array<float>): array<float> =>
@ -525,6 +775,9 @@ module A = {
let diff = (max -. min) /. Belt.Float.fromInt(n - 1) let diff = (max -. min) /. Belt.Float.fromInt(n - 1)
Belt.Array.makeBy(n, i => min +. Belt.Float.fromInt(i) *. diff) 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 = { module JsArray = {
let concatSomes = (optionals: Js.Array.t<option<'a>>): Js.Array.t<'a> => let concatSomes = (optionals: Js.Array.t<option<'a>>): Js.Array.t<'a> =>
optionals optionals
|> Js.Array.filter(Rationale.Option.isSome) |> Js.Array.filter(O.isSome)
|> Js.Array.map(Rationale.Option.toExn("Warning: This should not have happened")) |> Js.Array.map(O.toExn("Warning: This should not have happened"))
let filter = Js.Array.filter let filter = Js.Array.filter
} }

View File

@ -9,6 +9,13 @@ type algebraicOperation = [
| #Power | #Power
| #Logarithm | #Logarithm
] ]
type convolutionOperation = [
| #Add
| #Multiply
| #Subtract
]
@genType @genType
type pointwiseOperation = [#Add | #Multiply | #Power] type pointwiseOperation = [#Add | #Multiply | #Power]
type scaleOperation = [#Multiply | #Power | #Logarithm | #Divide] type scaleOperation = [#Multiply | #Power | #Logarithm | #Divide]
@ -20,22 +27,81 @@ type distToFloatOperation = [
| #Sample | #Sample
] ]
module Algebraic = { module Convolution = {
type t = algebraicOperation 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 => let toFn: (t, float, float) => float = x =>
switch x { switch x {
| #Add => \"+." | #Add => \"+."
| #Subtract => \"-." | #Subtract => \"-."
| #Multiply => \"*." | #Multiply => \"*."
| #Power => \"**"
| #Divide => \"/."
| #Logarithm => (a, b) => log(a) /. log(b)
} }
}
let applyFn = (t, f1, f2) => type operationError =
switch (t, f1, f2) { | DivisionByZeroError
| (#Divide, _, 0.) => Error("Cannot divide $v1 by zero.") | ComplexNumberError
| _ => Ok(toFn(t, f1, f2))
@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 => let toString = x =>
@ -79,12 +145,12 @@ module DistToFloat = {
// Note that different logarithms don't really do anything. // Note that different logarithms don't really do anything.
module Scale = { module Scale = {
type t = scaleOperation type t = scaleOperation
let toFn = x => let toFn = (x: t, a: float, b: float): result<float, Error.t> =>
switch x { switch x {
| #Multiply => \"*." | #Multiply => Ok(a *. b)
| #Divide => \"/." | #Divide => divide(a, b)
| #Power => \"**" | #Power => power(a, b)
| #Logarithm => (a, b) => log(a) /. log(b) | #Logarithm => logarithm(a, b)
} }
let format = (operation: t, value, scaleBy) => let format = (operation: t, value, scaleBy) =>

View File

@ -16,7 +16,7 @@ let create = (relativeHeights: array<float>, ~maximum=?, ()) => {
if E.A.length(relativeHeights) === 0 { if E.A.length(relativeHeights) === 0 {
"" ""
} else { } 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 relativeHeights
->E.A2.fmap(_heightToTickIndex(maximum)) ->E.A2.fmap(_heightToTickIndex(maximum))

View File

@ -43,6 +43,10 @@ module T = {
let xTotalRange = (t: t) => maxX(t) -. minX(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 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 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 square = mapX(x => x ** 2.0)
let zip = ({xs, ys}: t) => Belt.Array.zip(xs, ys) let zip = ({xs, ys}: t) => Belt.Array.zip(xs, ys)
let fromArray = ((xs, ys)): t => {xs: xs, ys: ys} let fromArray = ((xs, ys)): t => {xs: xs, ys: ys}
@ -60,8 +64,8 @@ module T = {
module Ts = { module Ts = {
type t = T.ts type t = T.ts
let minX = (t: t) => t |> E.A.fmap(T.minX) |> E.A.min |> 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.max |> extImp 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 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 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. /* 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. */ 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 = { module XsConversion = {
@ -220,8 +224,8 @@ module XsConversion = {
module Zipped = { module Zipped = {
type zipped = array<(float, float)> type zipped = array<(float, float)>
let compareYs = ((_, y1), (_, y2)) => y1 > y2 ? 1 : 0 let compareYs = ((_, y1): (float, float), (_, y2): (float, float)) => y1 > y2 ? 1 : 0
let compareXs = ((x1, _), (x2, _)) => x1 > x2 ? 1 : 0 let compareXs = ((x1, _): (float, float), (x2, _): (float, float)) => x1 > x2 ? 1 : 0
let sortByY = (t: zipped) => t |> E.A.stableSortBy(_, compareYs) let sortByY = (t: zipped) => t |> E.A.stableSortBy(_, compareYs)
let sortByX = (t: zipped) => t |> E.A.stableSortBy(_, compareXs) let sortByX = (t: zipped) => t |> E.A.stableSortBy(_, compareXs)
let filterByX = (testFn: float => bool, t: zipped) => t |> E.A.filter(((x, _)) => testFn(x)) let filterByX = (testFn: float => bool, t: zipped) => t |> E.A.filter(((x, _)) => testFn(x))
@ -229,7 +233,12 @@ module Zipped = {
module PointwiseCombination = { module PointwiseCombination = {
// t1Interpolator and t2Interpolator are functions from XYShape.XtoY, e.g. linearBetweenPointsExtrapolateFlat. // 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. // 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, // 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. // and interpolates the value on the other side, thus accumulating xs and ys.
@ -277,13 +286,28 @@ module PointwiseCombination = {
} }
outX.push(x); 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) => let combineEvenXs = (~fn, ~xToYSelection, sampleCount, t1: T.t, t2: T.t) =>
switch (E.A.length(t1.xs), E.A.length(t2.xs)) { switch (E.A.length(t1.xs), E.A.length(t2.xs)) {
| (0, 0) => T.empty | (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 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. 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. _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 sidebar_position: 7
--- ---
import { SquiggleEditor } from "../../src/components/SquiggleEditor"; 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 ### Normal distribution
@ -15,6 +18,10 @@ and standard deviation.
<SquiggleEditor initialSquiggleString="normal(5, 1)" /> <SquiggleEditor initialSquiggleString="normal(5, 1)" />
#### Validity
- `sd > 0`
### Uniform distribution ### Uniform distribution
The `uniform(low, high)` function creates a uniform distribution between the The `uniform(low, high)` function creates a uniform distribution between the
@ -22,86 +29,271 @@ two given numbers.
<SquiggleEditor initialSquiggleString="uniform(3, 7)" /> <SquiggleEditor initialSquiggleString="uniform(3, 7)" />
#### Validity
- `low < high`
### Lognormal distribution ### Lognormal distribution
The `lognormal(mu, sigma)` returns the log of a normal distribution with parameters 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 `mu` and `sigma`. The log of `lognormal(mu, sigma)` is a normal distribution with mean `mu` and standard deviation `sigma`.
mean mu and standard deviation sigma.
<SquiggleEditor initialSquiggleString="lognormal(0, 0.7)" /> <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 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" /> <SquiggleEditor initialSquiggleString="2 to 10" />
#### Future feature:
Furthermore, it's also possible to create a lognormal from it's actual mean Furthermore, it's also possible to create a lognormal from it's actual mean
and standard deviation, using `lognormalFromMeanAndStdDev`. and standard deviation, using `lognormalFromMeanAndStdDev`.
TODO: interpreter/parser doesn't provide this in current `develop` branch
<SquiggleEditor initialSquiggleString="lognormalFromMeanAndStdDev(20, 10)" /> <SquiggleEditor initialSquiggleString="lognormalFromMeanAndStdDev(20, 10)" />
#### Validity
- `sigma > 0`
- In `x to y` notation, `x < y`
### Beta distribution ### 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 ### Exponential distribution
The `exponential(mean)` function creates an exponential distribution with the given The `exponential(rate)` function creates an exponential distribution with the given
mean. 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 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)" /> <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 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 to be combined, and the last argument is how much to weigh every distribution in the
combination. 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. 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: 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. given point x.
<SquiggleEditor initialSquiggleString="pdf(normal(0,1),0)" /> <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 - `x` must be a scalar
lower than x is equal to prob. It is the inverse of `cdf`. - `dist` must be a distribution
<SquiggleEditor initialSquiggleString="inv(normal(0,1),0.5)" /> ### Cumulative density function
### CDF of a distribution The `cdf(dist, x)` gives the cumulative probability of the distribution
The `cdf(distribution,x)` gives the cumulative probability of the distribution
or all values lower than x. It is the inverse of `inv`. or all values lower than x. It is the inverse of `inv`.
<SquiggleEditor initialSquiggleString="cdf(normal(0,1),0)" /> <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. 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. The `sample(distribution)` samples a given distribution.
<SquiggleEditor initialSquiggleString="sample(normal(0, 10))" /> <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 sidebar_position: 2
title: Language Basics
--- ---
import { SquiggleEditor } from "../../src/components/SquiggleEditor"; import { SquiggleEditor } from "../../src/components/SquiggleEditor";
# Squiggle Language ## Expressions
The squiggle language has a very simple syntax. The best way to get to understand A distribution
it is by simply looking at examples.
## 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 <SquiggleEditor
initialSquiggleString={`value_of_work = 10 to 70 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 ### Functions
(the lone `value_of_work` line). Variables can be used later in a squiggle program
and even in other notebooks!
An export is rendered to the output view so you can see your result. We can define functions
the exports can be expressions, such as:
<SquiggleEditor initialSquiggleString="normal(0,1)" />
## Functions
Squiggle supports functions, including the rendering of functions:
<SquiggleEditor <SquiggleEditor
initialSquiggleString={`ozzie_estimate(t) = lognormal({mean: 3 + (t+.1)^2.5, stdev: 8}) initialSquiggleString={`ozzie_estimate(t) = lognormal(1, t ^ 1.01)
ozzie_estimate 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 sidebar_position: 3
title: Node Packages
--- ---
# Javascript Libraries
There are two JavaScript packages currently available for Squiggle: There are two JavaScript packages currently available for Squiggle:
- [`@quri/squiggle-lang`](https://www.npmjs.com/package/@quri/squiggle-lang) - [`@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) - [`@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. 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 call. Passing this environment will mean that all previously declared variables
in the previous environment will be made available. in the previous environment will be made available.
The return type of `run` is a bit complicated, and comes from auto generated js 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 code that comes from rescript. We highly recommend using typescript when using
this library to help navigate the return type. this library to help navigate the return type.
## Squiggle Components ## 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