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