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
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
|
@ -31,14 +31,14 @@ describe("Continuous and discrete splits", () => {
|
|||
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),
|
||||
~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(
|
||||
let (_c, discrete2) = E.A.Floats.Sorted.splitContinuousAndDiscreteForMinWeight(
|
||||
makeDuplicatedArray(500),
|
||||
~minDiscreteWeight=2,
|
||||
)
|
||||
|
|
|
@ -18,7 +18,26 @@ let pointSetDist3: PointSetTypes.xyShape = {
|
|||
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("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", () => {
|
||||
makeTest("When identical", XYShape.logScorePoint(30, pointSetDist1, pointSetDist1), Some(0.0))
|
||||
makeTest(
|
||||
|
@ -32,16 +51,6 @@ describe("XYShapes", () => {
|
|||
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", () =>
|
||||
makeTest(
|
||||
"integrates correctly",
|
||||
|
|
|
@ -19,6 +19,7 @@ type error =
|
|||
| RequestedStrategyInvalidError(string)
|
||||
| LogarithmOfDistributionError(string)
|
||||
| OtherError(string)
|
||||
| XYShapeError(XYShape.error)
|
||||
|
||||
@genType
|
||||
module Error = {
|
||||
|
@ -39,6 +40,7 @@ module Error = {
|
|||
| PointSetConversionError(err) => SampleSetDist.pointsetConversionErrorToString(err)
|
||||
| SparklineError(err) => PointSetTypes.sparklineErrorToString(err)
|
||||
| RequestedStrategyInvalidError(err) => `Requested strategy invalid: ${err}`
|
||||
| XYShapeError(err) => `XY Shape Error: ${XYShape.Error.toString(err)}`
|
||||
| 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 => {
|
||||
Array.fast_sort(compare, samples)
|
||||
let minDiscreteToKeep = MagicNumbers.ToPointSet.minDiscreteToKeep(samples)
|
||||
let (continuousPart, discretePart) = E.A.Sorted.Floats.splitContinuousAndDiscreteForMinWeight(
|
||||
let (continuousPart, discretePart) = E.A.Floats.Sorted.splitContinuousAndDiscreteForMinWeight(
|
||||
samples,
|
||||
~minDiscreteWeight=minDiscreteToKeep,
|
||||
)
|
||||
|
|
|
@ -217,6 +217,12 @@ module R = {
|
|||
| Error(err) => errF(err)
|
||||
}
|
||||
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> => {
|
||||
switch r {
|
||||
| Ok(r') => Ok(f(r'))
|
||||
|
@ -645,42 +651,81 @@ module A = {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
module Floats = {
|
||||
type t = array<float>
|
||||
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
|
||||
|
||||
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 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 getNonFinite = (t: t) => Belt.Array.getBy(t, r => !Js.Float.isFinite(r))
|
||||
let getBelowZero = (t: t) => Belt.Array.getBy(t, r => r < 0.0)
|
||||
|
||||
let isSorted = (t: t): bool =>
|
||||
if Array.length(t) < 1 {
|
||||
true
|
||||
} else {
|
||||
reduce(zip(t, tail(t)), true, (acc, (first, second)) => acc && first < second)
|
||||
}
|
||||
}
|
||||
|
||||
let concat = (t1: array<'a>, t2: array<'a>) => {
|
||||
let ts = Belt.Array.concat(t1, t2)
|
||||
ts |> Array.fast_sort(floatCompare)
|
||||
ts
|
||||
}
|
||||
//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)
|
||||
|
||||
let concatMany = (t1: array<array<'a>>) => {
|
||||
let ts = Belt.Array.concatMany(t1)
|
||||
ts |> Array.fast_sort(floatCompare)
|
||||
ts
|
||||
}
|
||||
// Gives an array with all the differences between values
|
||||
// diff([1,5,3,7]) = [4,-2,4]
|
||||
let diff = (t: t): array<float> =>
|
||||
Belt.Array.zipBy(t, Belt.Array.sliceToEnd(t, 1), (left, right) => right -. left)
|
||||
|
||||
module Floats = {
|
||||
let isSorted = (ar: array<float>): bool =>
|
||||
reduce(zip(ar, tail(ar)), true, (acc, (first, second)) => acc && first < second)
|
||||
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 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) =>
|
||||
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 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 Sorted = Floats.Sorted
|
||||
}
|
||||
|
||||
module A2 = {
|
||||
let fmap = (a, b) => A.fmap(b, a)
|
||||
let joinWith = (a, b) => A.joinWith(b, a)
|
||||
let filter = (a, b) => A.filter(b, a)
|
||||
}
|
||||
|
||||
module JsArray = {
|
||||
|
|
|
@ -4,6 +4,42 @@ type xyShape = {
|
|||
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
|
||||
type interpolationStrategy = [
|
||||
| #Stepwise
|
||||
|
@ -60,6 +96,44 @@ module T = {
|
|||
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 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 = {
|
||||
|
|
Loading…
Reference in New Issue
Block a user