diff --git a/packages/squiggle-lang/__tests__/E/splitContinuousAndDiscrete_test.res b/packages/squiggle-lang/__tests__/E/splitContinuousAndDiscrete_test.res index 449787dd..a52227ee 100644 --- a/packages/squiggle-lang/__tests__/E/splitContinuousAndDiscrete_test.res +++ b/packages/squiggle-lang/__tests__/E/splitContinuousAndDiscrete_test.res @@ -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, ) diff --git a/packages/squiggle-lang/__tests__/XYShape_test.res b/packages/squiggle-lang/__tests__/XYShape_test.res index 701d82e1..38535020 100644 --- a/packages/squiggle-lang/__tests__/XYShape_test.res +++ b/packages/squiggle-lang/__tests__/XYShape_test.res @@ -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", diff --git a/packages/squiggle-lang/src/rescript/Distributions/DistributionTypes.res b/packages/squiggle-lang/src/rescript/Distributions/DistributionTypes.res index e27a138d..93f86798 100644 --- a/packages/squiggle-lang/src/rescript/Distributions/DistributionTypes.res +++ b/packages/squiggle-lang/src/rescript/Distributions/DistributionTypes.res @@ -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 } diff --git a/packages/squiggle-lang/src/rescript/Distributions/PointSetDist/AlgebraicShapeCombination.res b/packages/squiggle-lang/src/rescript/Distributions/PointSetDist/AlgebraicShapeCombination.res index 63600e43..a51de00d 100644 --- a/packages/squiggle-lang/src/rescript/Distributions/PointSetDist/AlgebraicShapeCombination.res +++ b/packages/squiggle-lang/src/rescript/Distributions/PointSetDist/AlgebraicShapeCombination.res @@ -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) diff --git a/packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/SampleSetDist_ToPointSet.res b/packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/SampleSetDist_ToPointSet.res index 142f10b2..ec2bf0d0 100644 --- a/packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/SampleSetDist_ToPointSet.res +++ b/packages/squiggle-lang/src/rescript/Distributions/SampleSetDist/SampleSetDist_ToPointSet.res @@ -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, ) diff --git a/packages/squiggle-lang/src/rescript/Utility/E.res b/packages/squiggle-lang/src/rescript/Utility/E.res index a110f1b7..e0bcaf5c 100644 --- a/packages/squiggle-lang/src/rescript/Utility/E.res +++ b/packages/squiggle-lang/src/rescript/Utility/E.res @@ -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 + 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>) => { - 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 => + Belt.Array.zipBy(t, Belt.Array.sliceToEnd(t, 1), (left, right) => right -. left) - module Floats = { - let isSorted = (ar: array): 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 => + 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>) => 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): array => - 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 => - 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 = { diff --git a/packages/squiggle-lang/src/rescript/Utility/XYShape.res b/packages/squiggle-lang/src/rescript/Utility/XYShape.res index 97974884..1f1e87ca 100644 --- a/packages/squiggle-lang/src/rescript/Utility/XYShape.res +++ b/packages/squiggle-lang/src/rescript/Utility/XYShape.res @@ -4,6 +4,42 @@ type xyShape = { ys: array, } +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) + +@genType +module Error = { + let mapErrorArrayToError = (errors: array): option => { + 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, ~ys: array) => { + let attempt: t = {xs: xs, ys: ys} + switch Validator.validate(attempt) { + | Some(error) => Error(error) + | None => Ok(attempt) + } + } } module Ts = {