Merge pull request #416 from quantified-uncertainty/shape-validators

XYShape validator
This commit is contained in:
Quinn 2022-04-28 18:51:31 -04:00 committed by GitHub
commit 5f78399760
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 176 additions and 80 deletions

View File

@ -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,
)

View File

@ -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",

View File

@ -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
}

View File

@ -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)

View File

@ -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,
)

View File

@ -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 = {

View File

@ -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 = {