open DistributionTypes;
let _lastElement = (a: array('a)) =>
switch (Belt.Array.size(a)) {
| 0 => None
| n => Belt.Array.get(a, n - 1)
};
type pointInRange =
| Unbounded
| X(float);
module XYShape = {
type t = xyShape;
let toJs = (t: t) => {
{"xs": t.xs, "ys": t.ys};
};
let minX = (t: t) => t.xs |> E.A.get(_, 0);
// TODO: Check if this actually gets the last element, I'm not sure it does.
let maxX = (t: t) => t.xs |> (r => E.A.get(r, E.A.length(r) - 1));
let zip = t => Belt.Array.zip(t.xs, t.ys);
let fmap = (t: t, y): t => {xs: t.xs, ys: t.ys |> E.A.fmap(y)};
let scaleCdfTo = (~scaleTo=1., t: t) =>
switch (_lastElement(t.ys)) {
| Some(n) =>
let scaleBy = scaleTo /. n;
fmap(t, r => r *. scaleBy);
| None => t
};
let yFold = (fn, t: t) => {
E.A.fold_left(fn, 0., t.ys);
};
let ySum = yFold((a, b) => a +. b);
let fromArray = ((xs, ys)): t => {xs, ys};
let fromArrays = (xs, ys): t => {xs, ys};
let _transverse = fn =>
Belt.Array.reduce(_, [||], (items, (x, y)) =>
switch (_lastElement(items)) {
| Some((xLast, yLast)) =>
Belt.Array.concat(items, [|(x, fn(y, yLast))|])
| None => [|(x, y)|]
}
);
let _transverseShape = (fn, p: t) => {
Belt.Array.zip(p.xs, p.ys)
|> _transverse(fn)
|> Belt.Array.unzip
|> fromArray;
};
let accumulateYs = _transverseShape((aCurrent, aLast) => aCurrent +. aLast);
let subtractYs = _transverseShape((aCurrent, aLast) => aCurrent -. aLast);
module Range = {
// ((lastX, lastY), (nextX, nextY))
type zippedRange = ((float, float), (float, float));
let floatSum = Belt.Array.reduce(_, 0., (a, b) => a +. b);
let toT = r => r |> Belt.Array.unzip |> fromArray;
let nextX = ((_, (nextX, _)): zippedRange) => nextX;
let rangeAreaAssumingSteps =
(((lastX, lastY), (nextX, _)): zippedRange) =>
(nextX -. lastX) *. lastY;
let rangeAreaAssumingTriangles =
(((lastX, lastY), (nextX, nextY)): zippedRange) =>
(nextX -. lastX) *. (lastY +. nextY) /. 2.;
let delta_y_over_delta_x =
(((lastX, lastY), (nextX, nextY)): zippedRange) =>
(nextY -. lastY) /. (nextX -. lastX);
let inRanges = (mapper, reducer, t: t) => {
Belt.Array.zip(t.xs, t.ys)
|> E.A.toRanges
|> E.R.toOption
|> E.O.fmap(r => r |> Belt.Array.map(_, mapper) |> reducer);
};
let mapYsBasedOnRanges = fn => inRanges(r => (nextX(r), fn(r)), toT);
let integrateWithSteps = z =>
mapYsBasedOnRanges(rangeAreaAssumingSteps, z) |> E.O.fmap(accumulateYs);
let integrateWithTriangles = z =>
mapYsBasedOnRanges(rangeAreaAssumingTriangles, z)
|> E.O.fmap(accumulateYs);
let derivative = mapYsBasedOnRanges(delta_y_over_delta_x);
};
let findY = CdfLibrary.Distribution.findY;
let findX = CdfLibrary.Distribution.findX;
};
module Continuous = {
let minX = XYShape.minX;
let maxX = XYShape.maxX;
let fromArrays = XYShape.fromArrays;
let toJs = XYShape.toJs;
let toPdf = XYShape.Range.derivative;
let toCdf = XYShape.Range.integrateWithTriangles;
let findX = CdfLibrary.Distribution.findX;
let findY = CdfLibrary.Distribution.findY;
let findIntegralY = (f, r) => {
r |> toCdf |> E.O.fmap(findY(f));
};
let normalizeCdf = (continuousShape: continuousShape) =>
continuousShape |> XYShape.scaleCdfTo(~scaleTo=1.0);
let scalePdf = (~scaleTo=1.0, continuousShape: continuousShape) =>
continuousShape
|> toCdf
|> E.O.fmap(XYShape.scaleCdfTo(~scaleTo))
|> E.O.bind(_, toPdf);
};
module Discrete = {
let minX = XYShape.minX;
let maxX = XYShape.maxX;
type t = discreteShape;
let fromArrays = XYShape.fromArrays;
let toJs = XYShape.toJs;
let ySum = XYShape.ySum;
let zip = t => Belt.Array.zip(t.xs, t.ys);
let scaleYToTotal = (totalDesired, t: t): t => {
let difference = totalDesired /. ySum(t);
XYShape.fmap(t, y => y *. difference);
};
let render = (t: t) =>
Belt.Array.zip(t.xs, t.ys)
|> E.A.fmap(((x, y)) =>
{E.Float.toFixed(x)
++ "---"
++ E.Float.with3DigitsPrecision(y *. 100.)
|> ReasonReact.string}
)
|> ReasonReact.array;
let integrate = XYShape.accumulateYs;
let derivative = XYShape.subtractYs;
// TODO: This has a clear bug where it returns the Y value of the first item,
// even if X is less than the X of the first item.
// It has a second bug that it assumes things are triangular, instead of interpolating via steps.
let findIntegralY = (f, t: t) => {
t |> XYShape.accumulateYs |> CdfLibrary.Distribution.findY(f);
};
let findY = (f, t: t) => {
Belt.Array.zip(t.xs, t.ys)
|> E.A.getBy(_, ((x, _)) => x == f)
|> E.O.fmap(((_, y)) => y)
|> E.O.default(0.);
};
};
let min = (f1: option(float), f2: option(float)) =>
switch (f1, f2) {
| (Some(f1), Some(f2)) => Some(f1 < f2 ? f1 : f2)
| (Some(f1), None) => Some(f1)
| (None, Some(f2)) => Some(f2)
| (None, None) => None
};
let max = (f1: option(float), f2: option(float)) =>
switch (f1, f2) {
| (Some(f1), Some(f2)) => Some(f1 > f2 ? f1 : f2)
| (Some(f1), None) => Some(f1)
| (None, Some(f2)) => Some(f2)
| (None, None) => None
};
module Mixed = {
let make = (~continuous, ~discrete, ~discreteProbabilityMassFraction) => {
continuous,
discrete,
discreteProbabilityMassFraction,
};
let minX = (t: DistributionTypes.mixedShape) =>
min(t.continuous |> Continuous.minX, t.discrete |> Discrete.minX);
let maxX = (t: DistributionTypes.mixedShape) => {
max(t.continuous |> Continuous.maxX, t.discrete |> Discrete.maxX);
};
let mixedMultiply =
(
t: DistributionTypes.mixedShape,
continuousComponent,
discreteComponent,
) => {
let diffFn = t.discreteProbabilityMassFraction;
continuousComponent *. (1.0 -. diffFn) +. discreteComponent *. diffFn;
};
type yPdfPoint = {
continuous: option(float),
discrete: option(float),
discreteProbabilityMassFraction: float,
};
let findY = (t: DistributionTypes.mixedShape, x: float): yPdfPoint => {
continuous: Continuous.findY(x, t.continuous) |> E.O.some,
discrete: Discrete.findY(x, t.discrete) |> E.O.some,
discreteProbabilityMassFraction: t.discreteProbabilityMassFraction,
};
let findYIntegral =
(x: float, t: DistributionTypes.mixedShape): option(float) => {
let c = t.continuous |> Continuous.findIntegralY(x);
let d = Discrete.findIntegralY(x, t.discrete);
switch (c, d) {
| (Some(c), d) => Some(mixedMultiply(t, c, d))
| _ => None
};
};
};
module Any = {
type t = DistributionTypes.pointsType;
let y = (t: t, x: float) =>
switch (t) {
| Mixed(m) => `mixed(Mixed.findY(m, x))
| Discrete(discreteShape) => `discrete(Discrete.findY(x, discreteShape))
| Continuous(continuousShape) =>
`continuous(Continuous.findY(x, continuousShape))
};
let yIntegral = (t: t, x: float) =>
switch (t) {
| Mixed(m) => Mixed.findYIntegral(x, m)
| Discrete(discreteShape) =>
Discrete.findIntegralY(x, discreteShape) |> E.O.some
| Continuous(continuousShape) =>
Continuous.findIntegralY(x, continuousShape)
};
let minX = (t: t) =>
switch (t) {
| Mixed(m) => Mixed.minX(m)
| Discrete(discreteShape) => Discrete.minX(discreteShape)
| Continuous(continuousShape) => Continuous.minX(continuousShape)
};
let maxX = (t: t) =>
switch (t) {
| Mixed(m) => Mixed.maxX(m)
| Discrete(discreteShape) => Discrete.maxX(discreteShape)
| Continuous(continuousShape) => Continuous.maxX(continuousShape)
};
// TODO: This is wrong. The discrete component should be made continuous when integrating.
let pdfToCdf = (t: t) =>
switch (t) {
| Mixed({continuous, discrete, discreteProbabilityMassFraction}) =>
Some(
Mixed({
continuous: Continuous.toCdf(continuous) |> E.O.toExt(""),
discrete: discrete |> Discrete.integrate,
discreteProbabilityMassFraction,
}),
)
| Discrete(discrete) => Some(Continuous(discrete |> Discrete.integrate))
| Continuous(continuous) =>
Continuous.toCdf(continuous) |> E.O.fmap(e => Continuous(e))
};
};
module DomainMixed = {
type t = {
mixedShape,
domain,
};
};