using System; using System.Collections.Generic; namespace Amphora.RandomComparisons { /// /// A generator whose every word is all ones, so the draws built on it come out as large as they can. /// public struct LargestWords : IRandomGenerator { public ulong NextUInt64() { return ulong.MaxValue; } public uint NextUInt32() { return uint.MaxValue; } } /// /// Checks the draws from a range of doubles or floats against the two quick ways of making them, scaling a draw /// from [0, 1) and interpolating between the ends. Both round where the spacing of the draw and the spacing of the /// range disagree, so they are biased and can return the maximum. /// public static class RangeChecks { public const int Residues = 16; public const int DoubleMantissaBits = 52; public const int FloatMantissaBits = 23; private static readonly (double Minimum, double MaximumExclusive)[] Ranges = { (1, 2), (-3.5, 7.25), (-1000, 1000), }; private static readonly double[] EdgeValues = { 0, 0.001, 0.1, 0.3, 0.5, 1, 1.5, 2, 3, 3.5, 7.25, 10, 100, 1000, 1000000, -0.001, -0.1, -0.3, -0.5, -1, -1.5, -2, -3, -3.5, -7.25, -10, -100, -1000, -1000000, }; /// /// Returns the gap between the doubles just below the magnitude of , for a format with /// the given number of mantissa bits. It is the coarsest spacing a draw over a range ending there can use. /// public static double GapBelow(double top, int mantissaBits) { long bits = BitConverter.DoubleToInt64Bits(Math.Abs(top)); int exponent = (int)(bits >> 52) - 1023; bool isPowerOfTwo = (bits & 0xFFFFFFFFFFFFFL) == 0; return Math.Pow(2, exponent - mantissaBits - (isPowerOfTwo ? 1 : 0)); } /// /// Counts the draws in each cell of the grid, which is as many cells as there are gaps, by the cell's number /// modulo . Every cell is as likely as any other, so every residue must be. /// public static NamedOutcome OnGrid(string name, double minimum, double maximumExclusive, int mantissaBits, long draws, Func draw, bool expectsFailure = false) { double farthest = Math.Abs(minimum) <= Math.Abs(maximumExclusive) ? maximumExclusive : minimum; double gap = GapBelow(farthest, mantissaBits); var counts = new long[Residues]; for (long i = 0; i < draws; i++) { long cell = (long)Math.Floor(draw() / gap); counts[(int)(((cell % Residues) + Residues) % Residues)]++; } double expected = (double)draws / Residues; double statistic = 0; long busiest = 0; foreach (long count in counts) { statistic += (count - expected) * (count - expected) / expected; busiest = Math.Max(busiest, count); } string detail = "busiest of " + Residues + " residues holds " + (busiest / expected).ToString("0.000") + " times its share"; return new NamedOutcome(name, new Outcome(Stats.ChiSquareTwoSided(statistic, Residues - 1), detail), expectsFailure); } private static NamedOutcome StaysBelowMaximum(string name, int exceeding, int ranges, bool expectsFailure) { string detail = exceeding + " of " + ranges + " ranges returned the maximum"; return new NamedOutcome(name, new Outcome(exceeding == 0 ? 1 : 0, detail), expectsFailure); } private static void AddLargestDraws(List outcomes) { var largest = new LargestWords(); double largestDouble = largest.NextDouble(); float largestFloat = largest.NextFloat(); var exceeding = new int[6]; int ranges = 0; foreach (double minimum in EdgeValues) { foreach (double maximum in EdgeValues) { float floatMinimum = (float)minimum; float floatMaximum = (float)maximum; if (!(minimum < maximum) || !(floatMinimum < floatMaximum)) { continue; } ranges++; exceeding[0] += largest.NextDouble(minimum, maximum) >= maximum ? 1 : 0; exceeding[1] += minimum + (maximum - minimum) * largestDouble >= maximum ? 1 : 0; exceeding[2] += minimum * (1 - largestDouble) + maximum * largestDouble >= maximum ? 1 : 0; exceeding[3] += largest.NextFloat(floatMinimum, floatMaximum) >= floatMaximum ? 1 : 0; exceeding[4] += floatMinimum + (floatMaximum - floatMinimum) * largestFloat >= floatMaximum ? 1 : 0; exceeding[5] += floatMinimum * (1 - largestFloat) + floatMaximum * largestFloat >= floatMaximum ? 1 : 0; } } outcomes.Add(StaysBelowMaximum("NextDouble, largest draw below the maximum", exceeding[0], ranges, false)); outcomes.Add(StaysBelowMaximum("scaled double, largest draw below the max", exceeding[1], ranges, true)); outcomes.Add(StaysBelowMaximum("lerped double, largest draw below the max", exceeding[2], ranges, true)); outcomes.Add(StaysBelowMaximum("NextFloat, largest draw below the maximum", exceeding[3], ranges, false)); outcomes.Add(StaysBelowMaximum("scaled float, largest draw below the max", exceeding[4], ranges, true)); outcomes.Add(StaysBelowMaximum("lerped float, largest draw below the max", exceeding[5], ranges, true)); } public static List Run(GeneratorType start, Scale scale) where GeneratorType : struct, IRandomGenerator { GeneratorType generator = start; long draws = scale.Count(1L << 24, minimum: 1L << 20); var returnMe = new List(); foreach ((double minimum, double maximumExclusive) in Ranges) { float floatMinimum = (float)minimum; float floatMaximum = (float)maximumExclusive; string doubleRange = "NextDouble(" + minimum + ", " + maximumExclusive + ")"; returnMe.Add(OnGrid(doubleRange + " grid cells", minimum, maximumExclusive, DoubleMantissaBits, draws, () => generator.NextDouble(minimum, maximumExclusive))); returnMe.Add(OnGrid(doubleRange + " by scaling instead", minimum, maximumExclusive, DoubleMantissaBits, draws, () => minimum + (maximumExclusive - minimum) * generator.NextDouble(), expectsFailure: true)); returnMe.Add(OnGrid(doubleRange + " by lerping instead", minimum, maximumExclusive, DoubleMantissaBits, draws, () => { double fraction = generator.NextDouble(); return minimum * (1 - fraction) + maximumExclusive * fraction; }, expectsFailure: true)); string floatRange = "NextFloat(" + minimum + ", " + maximumExclusive + ")"; returnMe.Add(OnGrid(floatRange + " grid cells", minimum, maximumExclusive, FloatMantissaBits, draws, () => generator.NextFloat(floatMinimum, floatMaximum))); returnMe.Add(OnGrid(floatRange + " by scaling instead", minimum, maximumExclusive, FloatMantissaBits, draws, () => floatMinimum + (floatMaximum - floatMinimum) * generator.NextFloat(), expectsFailure: true)); returnMe.Add(OnGrid(floatRange + " by lerping instead", minimum, maximumExclusive, FloatMantissaBits, draws, () => { float fraction = generator.NextFloat(); return floatMinimum * (1 - fraction) + floatMaximum * fraction; }, expectsFailure: true)); } AddLargestDraws(returnMe); return returnMe; } } }