Merge pull request #416 from quantified-uncertainty/shape-validators
XYShape validator
This commit is contained in:
commit
5f78399760
|
@ -2,7 +2,7 @@ open Jest
|
||||||
open TestHelpers
|
open TestHelpers
|
||||||
|
|
||||||
let prepareInputs = (ar, minWeight) =>
|
let prepareInputs = (ar, minWeight) =>
|
||||||
E.A.Sorted.Floats.splitContinuousAndDiscreteForMinWeight(ar, ~minDiscreteWeight=minWeight) |> (
|
E.A.Floats.Sorted.splitContinuousAndDiscreteForMinWeight(ar, ~minDiscreteWeight=minWeight) |> (
|
||||||
((c, disc)) => (c, disc |> E.FloatFloatMap.toArray)
|
((c, disc)) => (c, disc |> E.FloatFloatMap.toArray)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,14 +31,14 @@ describe("Continuous and discrete splits", () => {
|
||||||
E.A.concatMany([sorted, sorted, sorted, sorted]) |> Belt.SortArray.stableSortBy(_, compare)
|
E.A.concatMany([sorted, sorted, sorted, sorted]) |> Belt.SortArray.stableSortBy(_, compare)
|
||||||
}
|
}
|
||||||
|
|
||||||
let (_, discrete1) = E.A.Sorted.Floats.splitContinuousAndDiscreteForMinWeight(
|
let (_, discrete1) = E.A.Floats.Sorted.splitContinuousAndDiscreteForMinWeight(
|
||||||
makeDuplicatedArray(10),
|
makeDuplicatedArray(10),
|
||||||
~minDiscreteWeight=2,
|
~minDiscreteWeight=2,
|
||||||
)
|
)
|
||||||
let toArr1 = discrete1 |> E.FloatFloatMap.toArray
|
let toArr1 = discrete1 |> E.FloatFloatMap.toArray
|
||||||
makeTest("splitMedium at count=10", toArr1 |> Belt.Array.length, 10)
|
makeTest("splitMedium at count=10", toArr1 |> Belt.Array.length, 10)
|
||||||
|
|
||||||
let (_c, discrete2) = E.A.Sorted.Floats.splitContinuousAndDiscreteForMinWeight(
|
let (_c, discrete2) = E.A.Floats.Sorted.splitContinuousAndDiscreteForMinWeight(
|
||||||
makeDuplicatedArray(500),
|
makeDuplicatedArray(500),
|
||||||
~minDiscreteWeight=2,
|
~minDiscreteWeight=2,
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,7 +18,26 @@ let pointSetDist3: PointSetTypes.xyShape = {
|
||||||
ys: [0.2, 0.5, 0.8],
|
ys: [0.2, 0.5, 0.8],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let makeAndGetErrorString = (~xs, ~ys) =>
|
||||||
|
XYShape.T.make(~xs, ~ys)->E.R.getError->E.O2.fmap(XYShape.Error.toString)
|
||||||
|
|
||||||
describe("XYShapes", () => {
|
describe("XYShapes", () => {
|
||||||
|
describe("Validator", () => {
|
||||||
|
makeTest(
|
||||||
|
"with no errors",
|
||||||
|
makeAndGetErrorString(~xs=[1.0, 4.0, 8.0], ~ys=[0.2, 0.4, 0.8]),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
makeTest("when empty", makeAndGetErrorString(~xs=[], ~ys=[]), Some("Xs is empty"))
|
||||||
|
makeTest(
|
||||||
|
"when not sorted, different lengths, and not finite",
|
||||||
|
makeAndGetErrorString(~xs=[2.0, 1.0, infinity, 0.0], ~ys=[3.0, Js.Float._NaN]),
|
||||||
|
Some(
|
||||||
|
"Multiple Errors: [Xs is not sorted], [Xs and Ys have different lengths. Xs has length 4 and Ys has length 2], [Xs is not finite. Example value: Infinity], [Ys is not finite. Example value: NaN]",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
describe("logScorePoint", () => {
|
describe("logScorePoint", () => {
|
||||||
makeTest("When identical", XYShape.logScorePoint(30, pointSetDist1, pointSetDist1), Some(0.0))
|
makeTest("When identical", XYShape.logScorePoint(30, pointSetDist1, pointSetDist1), Some(0.0))
|
||||||
makeTest(
|
makeTest(
|
||||||
|
@ -32,16 +51,6 @@ describe("XYShapes", () => {
|
||||||
Some(210.3721280423322),
|
Some(210.3721280423322),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
// describe("transverse", () => {
|
|
||||||
// makeTest(
|
|
||||||
// "When very different",
|
|
||||||
// XYShape.Transversal._transverse(
|
|
||||||
// (aCurrent, aLast) => aCurrent +. aLast,
|
|
||||||
// [|1.0, 2.0, 3.0, 4.0|],
|
|
||||||
// ),
|
|
||||||
// [|1.0, 3.0, 6.0, 10.0|],
|
|
||||||
// )
|
|
||||||
// });
|
|
||||||
describe("integrateWithTriangles", () =>
|
describe("integrateWithTriangles", () =>
|
||||||
makeTest(
|
makeTest(
|
||||||
"integrates correctly",
|
"integrates correctly",
|
||||||
|
|
|
@ -19,6 +19,7 @@ type error =
|
||||||
| RequestedStrategyInvalidError(string)
|
| RequestedStrategyInvalidError(string)
|
||||||
| LogarithmOfDistributionError(string)
|
| LogarithmOfDistributionError(string)
|
||||||
| OtherError(string)
|
| OtherError(string)
|
||||||
|
| XYShapeError(XYShape.error)
|
||||||
|
|
||||||
@genType
|
@genType
|
||||||
module Error = {
|
module Error = {
|
||||||
|
@ -39,6 +40,7 @@ module Error = {
|
||||||
| PointSetConversionError(err) => SampleSetDist.pointsetConversionErrorToString(err)
|
| PointSetConversionError(err) => SampleSetDist.pointsetConversionErrorToString(err)
|
||||||
| SparklineError(err) => PointSetTypes.sparklineErrorToString(err)
|
| SparklineError(err) => PointSetTypes.sparklineErrorToString(err)
|
||||||
| RequestedStrategyInvalidError(err) => `Requested strategy invalid: ${err}`
|
| RequestedStrategyInvalidError(err) => `Requested strategy invalid: ${err}`
|
||||||
|
| XYShapeError(err) => `XY Shape Error: ${XYShape.Error.toString(err)}`
|
||||||
| OtherError(s) => s
|
| OtherError(s) => s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -263,4 +263,4 @@ let combineShapesContinuousDiscrete = (
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let isOrdered = (a: XYShape.T.t): bool => E.A.Sorted.Floats.isSorted(a.xs)
|
let isOrdered = (a: XYShape.T.t): bool => E.A.Floats.isSorted(a.xs)
|
||||||
|
|
|
@ -64,7 +64,7 @@ let toPointSetDist = (
|
||||||
): Internals.Types.outputs => {
|
): Internals.Types.outputs => {
|
||||||
Array.fast_sort(compare, samples)
|
Array.fast_sort(compare, samples)
|
||||||
let minDiscreteToKeep = MagicNumbers.ToPointSet.minDiscreteToKeep(samples)
|
let minDiscreteToKeep = MagicNumbers.ToPointSet.minDiscreteToKeep(samples)
|
||||||
let (continuousPart, discretePart) = E.A.Sorted.Floats.splitContinuousAndDiscreteForMinWeight(
|
let (continuousPart, discretePart) = E.A.Floats.Sorted.splitContinuousAndDiscreteForMinWeight(
|
||||||
samples,
|
samples,
|
||||||
~minDiscreteWeight=minDiscreteToKeep,
|
~minDiscreteWeight=minDiscreteToKeep,
|
||||||
)
|
)
|
||||||
|
|
|
@ -217,6 +217,12 @@ module R = {
|
||||||
| Error(err) => errF(err)
|
| Error(err) => errF(err)
|
||||||
}
|
}
|
||||||
let id = e => e |> result(U.id, U.id)
|
let id = e => e |> result(U.id, U.id)
|
||||||
|
let isOk = Belt.Result.isOk
|
||||||
|
let getError = (r: result<'a, 'b>) =>
|
||||||
|
switch r {
|
||||||
|
| Ok(_) => None
|
||||||
|
| Error(e) => Some(e)
|
||||||
|
}
|
||||||
let fmap = (f: 'a => 'b, r: result<'a, 'c>): result<'b, 'c> => {
|
let fmap = (f: 'a => 'b, r: result<'a, 'c>): result<'b, 'c> => {
|
||||||
switch r {
|
switch r {
|
||||||
| Ok(r') => Ok(f(r'))
|
| Ok(r') => Ok(f(r'))
|
||||||
|
@ -645,42 +651,81 @@ module A = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module Sorted = {
|
module Floats = {
|
||||||
let min = first
|
type t = array<float>
|
||||||
let max = last
|
let mean = Jstat.mean
|
||||||
let range = (~min=min, ~max=max, a) =>
|
let geomean = Jstat.geomean
|
||||||
switch (min(a), max(a)) {
|
let mode = Jstat.mode
|
||||||
| (Some(min), Some(max)) => Some(max -. min)
|
let variance = Jstat.variance
|
||||||
| _ => None
|
let stdev = Jstat.stdev
|
||||||
}
|
let sum = Jstat.sum
|
||||||
|
let random = Js.Math.random_int
|
||||||
|
|
||||||
let floatCompare: (float, float) => int = compare
|
let floatCompare: (float, float) => int = compare
|
||||||
|
let sort = t => {
|
||||||
|
let r = t
|
||||||
|
r |> Array.fast_sort(floatCompare)
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
let binarySearchFirstElementGreaterIndex = (ar: array<'a>, el: 'a) => {
|
let getNonFinite = (t: t) => Belt.Array.getBy(t, r => !Js.Float.isFinite(r))
|
||||||
let el = Belt.SortArray.binarySearchBy(ar, el, floatCompare)
|
let getBelowZero = (t: t) => Belt.Array.getBy(t, r => r < 0.0)
|
||||||
let el = el < 0 ? el * -1 - 1 : el
|
|
||||||
switch el {
|
let isSorted = (t: t): bool =>
|
||||||
| e if e >= length(ar) => #overMax
|
if Array.length(t) < 1 {
|
||||||
| e if e == 0 => #underMin
|
true
|
||||||
| e => #firstHigher(e)
|
} else {
|
||||||
|
reduce(zip(t, tail(t)), true, (acc, (first, second)) => acc && first < second)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let concat = (t1: array<'a>, t2: array<'a>) => {
|
//Passing true for the exclusive parameter excludes both endpoints of the range.
|
||||||
let ts = Belt.Array.concat(t1, t2)
|
//https://jstat.github.io/all.html
|
||||||
ts |> Array.fast_sort(floatCompare)
|
let percentile = (a, b) => Jstat.percentile(a, b, false)
|
||||||
ts
|
|
||||||
}
|
|
||||||
|
|
||||||
let concatMany = (t1: array<array<'a>>) => {
|
// Gives an array with all the differences between values
|
||||||
let ts = Belt.Array.concatMany(t1)
|
// diff([1,5,3,7]) = [4,-2,4]
|
||||||
ts |> Array.fast_sort(floatCompare)
|
let diff = (t: t): array<float> =>
|
||||||
ts
|
Belt.Array.zipBy(t, Belt.Array.sliceToEnd(t, 1), (left, right) => right -. left)
|
||||||
}
|
|
||||||
|
|
||||||
module Floats = {
|
exception RangeError(string)
|
||||||
let isSorted = (ar: array<float>): bool =>
|
let range = (min: float, max: float, n: int): array<float> =>
|
||||||
reduce(zip(ar, tail(ar)), true, (acc, (first, second)) => acc && first < second)
|
switch n {
|
||||||
|
| 0 => []
|
||||||
|
| 1 => [min]
|
||||||
|
| 2 => [min, max]
|
||||||
|
| _ if min == max => Belt.Array.make(n, min)
|
||||||
|
| _ if n < 0 => raise(RangeError("n must be greater than 0"))
|
||||||
|
| _ if min > max => raise(RangeError("Min value is less then max value"))
|
||||||
|
| _ =>
|
||||||
|
let diff = (max -. min) /. Belt.Float.fromInt(n - 1)
|
||||||
|
Belt.Array.makeBy(n, i => min +. Belt.Float.fromInt(i) *. diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
let min = Js.Math.minMany_float
|
||||||
|
let max = Js.Math.maxMany_float
|
||||||
|
|
||||||
|
module Sorted = {
|
||||||
|
let min = first
|
||||||
|
let max = last
|
||||||
|
let range = (~min=min, ~max=max, a) =>
|
||||||
|
switch (min(a), max(a)) {
|
||||||
|
| (Some(min), Some(max)) => Some(max -. min)
|
||||||
|
| _ => None
|
||||||
|
}
|
||||||
|
|
||||||
|
let binarySearchFirstElementGreaterIndex = (ar: array<'a>, el: 'a) => {
|
||||||
|
let el = Belt.SortArray.binarySearchBy(ar, el, floatCompare)
|
||||||
|
let el = el < 0 ? el * -1 - 1 : el
|
||||||
|
switch el {
|
||||||
|
| e if e >= length(ar) => #overMax
|
||||||
|
| e if e == 0 => #underMin
|
||||||
|
| e => #firstHigher(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let concat = (t1: array<'a>, t2: array<'a>) => Belt.Array.concat(t1, t2)->sort
|
||||||
|
|
||||||
|
let concatMany = (t1: array<array<'a>>) => Belt.Array.concatMany(t1)->sort
|
||||||
|
|
||||||
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)
|
||||||
|
@ -743,47 +788,13 @@ module A = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
module Sorted = Floats.Sorted
|
||||||
module Floats = {
|
|
||||||
let mean = Jstat.mean
|
|
||||||
let geomean = Jstat.geomean
|
|
||||||
let mode = Jstat.mode
|
|
||||||
let variance = Jstat.variance
|
|
||||||
let stdev = Jstat.stdev
|
|
||||||
let sum = Jstat.sum
|
|
||||||
let random = Js.Math.random_int
|
|
||||||
|
|
||||||
//Passing true for the exclusive parameter excludes both endpoints of the range.
|
|
||||||
//https://jstat.github.io/all.html
|
|
||||||
let percentile = (a, b) => Jstat.percentile(a, b, false)
|
|
||||||
|
|
||||||
// Gives an array with all the differences between values
|
|
||||||
// diff([1,5,3,7]) = [4,-2,4]
|
|
||||||
let diff = (arr: array<float>): array<float> =>
|
|
||||||
Belt.Array.zipBy(arr, Belt.Array.sliceToEnd(arr, 1), (left, right) => right -. left)
|
|
||||||
|
|
||||||
exception RangeError(string)
|
|
||||||
let range = (min: float, max: float, n: int): array<float> =>
|
|
||||||
switch n {
|
|
||||||
| 0 => []
|
|
||||||
| 1 => [min]
|
|
||||||
| 2 => [min, max]
|
|
||||||
| _ if min == max => Belt.Array.make(n, min)
|
|
||||||
| _ if n < 0 => raise(RangeError("n must be greater than 0"))
|
|
||||||
| _ if min > max => raise(RangeError("Min value is less then max value"))
|
|
||||||
| _ =>
|
|
||||||
let diff = (max -. min) /. Belt.Float.fromInt(n - 1)
|
|
||||||
Belt.Array.makeBy(n, i => min +. Belt.Float.fromInt(i) *. diff)
|
|
||||||
}
|
|
||||||
|
|
||||||
let min = Js.Math.minMany_float
|
|
||||||
let max = Js.Math.maxMany_float
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module A2 = {
|
module A2 = {
|
||||||
let fmap = (a, b) => A.fmap(b, a)
|
let fmap = (a, b) => A.fmap(b, a)
|
||||||
let joinWith = (a, b) => A.joinWith(b, a)
|
let joinWith = (a, b) => A.joinWith(b, a)
|
||||||
|
let filter = (a, b) => A.filter(b, a)
|
||||||
}
|
}
|
||||||
|
|
||||||
module JsArray = {
|
module JsArray = {
|
||||||
|
|
|
@ -4,6 +4,42 @@ type xyShape = {
|
||||||
ys: array<float>,
|
ys: array<float>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type propertyName = string
|
||||||
|
|
||||||
|
@genType
|
||||||
|
type rec error =
|
||||||
|
| NotSorted(propertyName)
|
||||||
|
| IsEmpty(propertyName)
|
||||||
|
| NotFinite(propertyName, float)
|
||||||
|
| DifferentLengths({p1Name: string, p2Name: string, p1Length: int, p2Length: int})
|
||||||
|
| MultipleErrors(array<error>)
|
||||||
|
|
||||||
|
@genType
|
||||||
|
module Error = {
|
||||||
|
let mapErrorArrayToError = (errors: array<error>): option<error> => {
|
||||||
|
switch errors {
|
||||||
|
| [] => None
|
||||||
|
| [error] => Some(error)
|
||||||
|
| _ => Some(MultipleErrors(errors))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rec toString = (t: error) =>
|
||||||
|
switch t {
|
||||||
|
| NotSorted(propertyName) => `${propertyName} is not sorted`
|
||||||
|
| IsEmpty(propertyName) => `${propertyName} is empty`
|
||||||
|
| NotFinite(propertyName, exampleValue) =>
|
||||||
|
`${propertyName} is not finite. Example value: ${E.Float.toString(exampleValue)}`
|
||||||
|
| DifferentLengths({p1Name, p2Name, p1Length, p2Length}) =>
|
||||||
|
`${p1Name} and ${p2Name} have different lengths. ${p1Name} has length ${E.I.toString(
|
||||||
|
p1Length,
|
||||||
|
)} and ${p2Name} has length ${E.I.toString(p2Length)}`
|
||||||
|
| MultipleErrors(errors) =>
|
||||||
|
`Multiple Errors: ${E.A2.fmap(errors, toString)->E.A2.fmap(r => `[${r}]`)
|
||||||
|
|> E.A.joinWith(", ")}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@genType
|
@genType
|
||||||
type interpolationStrategy = [
|
type interpolationStrategy = [
|
||||||
| #Stepwise
|
| #Stepwise
|
||||||
|
@ -60,6 +96,44 @@ module T = {
|
||||||
let fromZippedArray = (pairs: array<(float, float)>): t => pairs |> Belt.Array.unzip |> fromArray
|
let fromZippedArray = (pairs: array<(float, float)>): t => pairs |> Belt.Array.unzip |> fromArray
|
||||||
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 toJs = (t: t) => {"xs": t.xs, "ys": t.ys}
|
let toJs = (t: t) => {"xs": t.xs, "ys": t.ys}
|
||||||
|
|
||||||
|
module Validator = {
|
||||||
|
let fnName = "XYShape validate"
|
||||||
|
let notSortedError = (p: string): error => NotSorted(p)
|
||||||
|
let notFiniteError = (p, exampleValue): error => NotFinite(p, exampleValue)
|
||||||
|
let isEmptyError = (propertyName): error => IsEmpty(propertyName)
|
||||||
|
let differentLengthsError = (t): error => DifferentLengths({
|
||||||
|
p1Name: "Xs",
|
||||||
|
p2Name: "Ys",
|
||||||
|
p1Length: E.A.length(xs(t)),
|
||||||
|
p2Length: E.A.length(ys(t)),
|
||||||
|
})
|
||||||
|
|
||||||
|
let areXsSorted = (t: t) => E.A.Floats.isSorted(xs(t))
|
||||||
|
let areXsEmpty = (t: t) => E.A.length(xs(t)) == 0
|
||||||
|
let getNonFiniteXs = (t: t) => t->xs->E.A.Floats.getNonFinite
|
||||||
|
let getNonFiniteYs = (t: t) => t->ys->E.A.Floats.getNonFinite
|
||||||
|
|
||||||
|
let validate = (t: t) => {
|
||||||
|
let xsNotSorted = areXsSorted(t) ? None : Some(notSortedError("Xs"))
|
||||||
|
let xsEmpty = areXsEmpty(t) ? Some(isEmptyError("Xs")) : None
|
||||||
|
let differentLengths =
|
||||||
|
E.A.length(xs(t)) !== E.A.length(ys(t)) ? Some(differentLengthsError(t)) : None
|
||||||
|
let xsNotFinite = getNonFiniteXs(t)->E.O2.fmap(notFiniteError("Xs"))
|
||||||
|
let ysNotFinite = getNonFiniteYs(t)->E.O2.fmap(notFiniteError("Ys"))
|
||||||
|
[xsNotSorted, xsEmpty, differentLengths, xsNotFinite, ysNotFinite]
|
||||||
|
->E.A.O.concatSomes
|
||||||
|
->Error.mapErrorArrayToError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let make = (~xs: array<float>, ~ys: array<float>) => {
|
||||||
|
let attempt: t = {xs: xs, ys: ys}
|
||||||
|
switch Validator.validate(attempt) {
|
||||||
|
| Some(error) => Error(error)
|
||||||
|
| None => Ok(attempt)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module Ts = {
|
module Ts = {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user