308 lines
10 KiB
ReasonML
308 lines
10 KiB
ReasonML
open Distributions;
|
|
|
|
type t = DistTypes.mixedShape;
|
|
let make = (~continuous, ~discrete): t => {continuous, discrete};
|
|
|
|
let totalLength = (t: t): int => {
|
|
let continuousLength =
|
|
t.continuous |> Continuous.getShape |> XYShape.T.length;
|
|
let discreteLength = t.discrete |> Discrete.getShape |> XYShape.T.length;
|
|
|
|
continuousLength + discreteLength;
|
|
};
|
|
|
|
let scaleBy = (~scale=1.0, {discrete, continuous}: t): t => {
|
|
let scaledDiscrete = Discrete.scaleBy(~scale, discrete);
|
|
let scaledContinuous = Continuous.scaleBy(~scale, continuous);
|
|
make(~discrete=scaledDiscrete, ~continuous=scaledContinuous);
|
|
};
|
|
|
|
let toContinuous = ({continuous}: t) => Some(continuous);
|
|
let toDiscrete = ({discrete}: t) => Some(discrete);
|
|
|
|
let combinePointwise = (~knownIntegralSumsFn, fn, t1: t, t2: t) => {
|
|
let reducedDiscrete =
|
|
[|t1, t2|]
|
|
|> E.A.fmap(toDiscrete)
|
|
|> E.A.O.concatSomes
|
|
|> Discrete.reduce(~knownIntegralSumsFn, fn);
|
|
|
|
let reducedContinuous =
|
|
[|t1, t2|]
|
|
|> E.A.fmap(toContinuous)
|
|
|> E.A.O.concatSomes
|
|
|> Continuous.reduce(~knownIntegralSumsFn, fn);
|
|
|
|
make(~discrete=reducedDiscrete, ~continuous=reducedContinuous);
|
|
};
|
|
|
|
module T =
|
|
Dist({
|
|
type t = DistTypes.mixedShape;
|
|
type integral = DistTypes.continuousShape;
|
|
let minX = ({continuous, discrete}: t) => {
|
|
min(Continuous.T.minX(continuous), Discrete.T.minX(discrete));
|
|
};
|
|
let maxX = ({continuous, discrete}: t) =>
|
|
max(Continuous.T.maxX(continuous), Discrete.T.maxX(discrete));
|
|
let toShape = (t: t): DistTypes.shape => Mixed(t);
|
|
|
|
let toContinuous = toContinuous;
|
|
let toDiscrete = toDiscrete;
|
|
|
|
let truncate =
|
|
(
|
|
leftCutoff: option(float),
|
|
rightCutoff: option(float),
|
|
{discrete, continuous}: t,
|
|
) => {
|
|
let truncatedContinuous =
|
|
Continuous.T.truncate(leftCutoff, rightCutoff, continuous);
|
|
let truncatedDiscrete =
|
|
Discrete.T.truncate(leftCutoff, rightCutoff, discrete);
|
|
|
|
make(~discrete=truncatedDiscrete, ~continuous=truncatedContinuous);
|
|
};
|
|
|
|
let normalize = (t: t): t => {
|
|
let continuousIntegralSum =
|
|
Continuous.T.Integral.sum(~cache=None, t.continuous);
|
|
let discreteIntegralSum =
|
|
Discrete.T.Integral.sum(~cache=None, t.discrete);
|
|
let totalIntegralSum = continuousIntegralSum +. discreteIntegralSum;
|
|
|
|
let newContinuousSum = continuousIntegralSum /. totalIntegralSum;
|
|
let newDiscreteSum = discreteIntegralSum /. totalIntegralSum;
|
|
|
|
let normalizedContinuous =
|
|
t.continuous
|
|
|> Continuous.scaleBy(~scale=1. /. newContinuousSum)
|
|
|> Continuous.updateKnownIntegralSum(Some(newContinuousSum));
|
|
let normalizedDiscrete =
|
|
t.discrete
|
|
|> Discrete.scaleBy(~scale=1. /. newDiscreteSum)
|
|
|> Discrete.updateKnownIntegralSum(Some(newDiscreteSum));
|
|
|
|
make(~continuous=normalizedContinuous, ~discrete=normalizedDiscrete);
|
|
};
|
|
|
|
let xToY = (x, t: t) => {
|
|
// This evaluates the mixedShape at x, interpolating if necessary.
|
|
// Note that we normalize entire mixedShape first.
|
|
let {continuous, discrete}: t = normalize(t);
|
|
let c = Continuous.T.xToY(x, continuous);
|
|
let d = Discrete.T.xToY(x, discrete);
|
|
DistTypes.MixedPoint.add(c, d); // "add" here just combines the two values into a single MixedPoint.
|
|
};
|
|
|
|
let toDiscreteProbabilityMassFraction = ({discrete, continuous}: t) => {
|
|
let discreteIntegralSum =
|
|
Discrete.T.Integral.sum(~cache=None, discrete);
|
|
let continuousIntegralSum =
|
|
Continuous.T.Integral.sum(~cache=None, continuous);
|
|
let totalIntegralSum = discreteIntegralSum +. continuousIntegralSum;
|
|
|
|
discreteIntegralSum /. totalIntegralSum;
|
|
};
|
|
|
|
let downsample = (~cache=None, count, {discrete, continuous}: t): t => {
|
|
// We will need to distribute the new xs fairly between the discrete and continuous shapes.
|
|
// The easiest way to do this is to simply go by the previous probability masses.
|
|
|
|
// The cache really isn't helpful here, because we would need two separate caches
|
|
let discreteIntegralSum =
|
|
Discrete.T.Integral.sum(~cache=None, discrete);
|
|
let continuousIntegralSum =
|
|
Continuous.T.Integral.sum(~cache=None, continuous);
|
|
let totalIntegralSum = discreteIntegralSum +. continuousIntegralSum;
|
|
|
|
// TODO: figure out what to do when the totalIntegralSum is zero.
|
|
|
|
let downsampledDiscrete =
|
|
Discrete.T.downsample(
|
|
int_of_float(
|
|
float_of_int(count) *. (discreteIntegralSum /. totalIntegralSum),
|
|
),
|
|
discrete,
|
|
);
|
|
|
|
let downsampledContinuous =
|
|
Continuous.T.downsample(
|
|
int_of_float(
|
|
float_of_int(count) *. (continuousIntegralSum /. totalIntegralSum),
|
|
),
|
|
continuous,
|
|
);
|
|
|
|
{discrete: downsampledDiscrete, continuous: downsampledContinuous};
|
|
};
|
|
|
|
let normalizedToContinuous = (t: t) => Some(normalize(t).continuous);
|
|
|
|
let normalizedToDiscrete = ({discrete} as t: t) =>
|
|
Some(normalize(t).discrete);
|
|
|
|
let integral = (~cache, {continuous, discrete}: t) => {
|
|
switch (cache) {
|
|
| Some(cache) => cache
|
|
| None =>
|
|
// note: if the underlying shapes aren't normalized, then these integrals won't be either!
|
|
let continuousIntegral =
|
|
Continuous.T.Integral.get(~cache=None, continuous);
|
|
let discreteIntegral = Discrete.T.Integral.get(~cache=None, discrete);
|
|
|
|
Continuous.make(
|
|
`Linear,
|
|
XYShape.PointwiseCombination.combineLinear(
|
|
~fn=(+.),
|
|
Continuous.getShape(continuousIntegral),
|
|
Continuous.getShape(discreteIntegral),
|
|
),
|
|
None,
|
|
);
|
|
};
|
|
};
|
|
|
|
let integralEndY = (~cache, t: t) => {
|
|
integral(~cache, t) |> Continuous.lastY;
|
|
};
|
|
|
|
let integralXtoY = (~cache, f, t) => {
|
|
t |> integral(~cache) |> Continuous.getShape |> XYShape.XtoY.linear(f);
|
|
};
|
|
|
|
let integralYtoX = (~cache, f, t) => {
|
|
t |> integral(~cache) |> Continuous.getShape |> XYShape.YtoX.linear(f);
|
|
};
|
|
|
|
// This pipes all ys (continuous and discrete) through fn.
|
|
// If mapY is a linear operation, we might be able to update the knownIntegralSums as well;
|
|
// if not, they'll be set to None.
|
|
let mapY =
|
|
(
|
|
~knownIntegralSumFn=previousIntegralSum => None,
|
|
fn,
|
|
{discrete, continuous}: t,
|
|
)
|
|
: t => {
|
|
let u = E.O.bind(_, knownIntegralSumFn);
|
|
|
|
let yMappedDiscrete =
|
|
discrete
|
|
|> Discrete.T.mapY(fn)
|
|
|> Discrete.updateKnownIntegralSum(u(discrete.knownIntegralSum));
|
|
|
|
let yMappedContinuous =
|
|
continuous
|
|
|> Continuous.T.mapY(fn)
|
|
|> Continuous.updateKnownIntegralSum(u(continuous.knownIntegralSum));
|
|
|
|
{
|
|
discrete: yMappedDiscrete,
|
|
continuous: Continuous.T.mapY(fn, continuous),
|
|
};
|
|
};
|
|
|
|
let mean = ({discrete, continuous}: t): float => {
|
|
let discreteMean = Discrete.T.mean(discrete);
|
|
let continuousMean = Continuous.T.mean(continuous);
|
|
|
|
// the combined mean is the weighted sum of the two:
|
|
let discreteIntegralSum =
|
|
Discrete.T.Integral.sum(~cache=None, discrete);
|
|
let continuousIntegralSum =
|
|
Continuous.T.Integral.sum(~cache=None, continuous);
|
|
let totalIntegralSum = discreteIntegralSum +. continuousIntegralSum;
|
|
|
|
(
|
|
discreteMean
|
|
*. discreteIntegralSum
|
|
+. continuousMean
|
|
*. continuousIntegralSum
|
|
)
|
|
/. totalIntegralSum;
|
|
};
|
|
|
|
let variance = ({discrete, continuous} as t: t): float => {
|
|
// the combined mean is the weighted sum of the two:
|
|
let discreteIntegralSum =
|
|
Discrete.T.Integral.sum(~cache=None, discrete);
|
|
let continuousIntegralSum =
|
|
Continuous.T.Integral.sum(~cache=None, continuous);
|
|
let totalIntegralSum = discreteIntegralSum +. continuousIntegralSum;
|
|
|
|
let getMeanOfSquares = ({discrete, continuous}: t) => {
|
|
let discreteMean =
|
|
discrete
|
|
|> Discrete.shapeMap(XYShape.Analysis.squareXYShape)
|
|
|> Discrete.T.mean;
|
|
let continuousMean =
|
|
continuous |> XYShape.Analysis.getMeanOfSquaresContinuousShape;
|
|
(
|
|
discreteMean
|
|
*. discreteIntegralSum
|
|
+. continuousMean
|
|
*. continuousIntegralSum
|
|
)
|
|
/. totalIntegralSum;
|
|
};
|
|
|
|
switch (discreteIntegralSum /. totalIntegralSum) {
|
|
| 1.0 => Discrete.T.variance(discrete)
|
|
| 0.0 => Continuous.T.variance(continuous)
|
|
| _ =>
|
|
XYShape.Analysis.getVarianceDangerously(t, mean, getMeanOfSquares)
|
|
};
|
|
};
|
|
});
|
|
|
|
let combineAlgebraically =
|
|
(~downsample=false, op: ExpressionTypes.algebraicOperation, t1: t, t2: t)
|
|
: t => {
|
|
// Discrete convolution can cause a huge increase in the number of samples,
|
|
// so we'll first downsample.
|
|
|
|
// An alternative (to be explored in the future) may be to first perform the full convolution and then to downsample the result;
|
|
// to use non-uniform fast Fourier transforms (for addition only), add web workers or gpu.js, etc. ...
|
|
|
|
let downsampleIfTooLarge = (t: t) => {
|
|
let sqtl = sqrt(float_of_int(totalLength(t)));
|
|
sqtl > 10. && downsample ? T.downsample(int_of_float(sqtl), t) : t;
|
|
};
|
|
|
|
let t1d = downsampleIfTooLarge(t1);
|
|
let t2d = downsampleIfTooLarge(t2);
|
|
|
|
// continuous (*) continuous => continuous, but also
|
|
// discrete (*) continuous => continuous (and vice versa). We have to take care of all combos and then combine them:
|
|
let ccConvResult =
|
|
Continuous.combineAlgebraically(
|
|
~downsample=false,
|
|
op,
|
|
t1d.continuous,
|
|
t2d.continuous,
|
|
);
|
|
let dcConvResult =
|
|
Continuous.combineAlgebraicallyWithDiscrete(
|
|
~downsample=false,
|
|
op,
|
|
t2d.continuous,
|
|
t1d.discrete,
|
|
);
|
|
let cdConvResult =
|
|
Continuous.combineAlgebraicallyWithDiscrete(
|
|
~downsample=false,
|
|
op,
|
|
t1d.continuous,
|
|
t2d.discrete,
|
|
);
|
|
let continuousConvResult =
|
|
Continuous.reduce((+.), [|ccConvResult, dcConvResult, cdConvResult|]);
|
|
|
|
// ... finally, discrete (*) discrete => discrete, obviously:
|
|
let discreteConvResult =
|
|
Discrete.combineAlgebraically(op, t1d.discrete, t2d.discrete);
|
|
|
|
{discrete: discreteConvResult, continuous: continuousConvResult};
|
|
};
|