using System;
using System.Collections.Generic;
namespace Amphora.RandomComparisons
{
///
/// Checks the shape of the draws games build on a generator: event times and counts, bell curves, points in a
/// circle, dice and picks. Each is compared with its exact distribution. Most are also run against a tempting
/// wrong way of making the same draw, which must fail.
///
public static class DistributionChecks
{
public const int Parts = 256;
public const int PolarSide = 16;
public const double TwoPi = 2 * Math.PI;
private static NamedOutcome Expecting(NamedOutcome outcome, bool expectsFailure)
{
return new NamedOutcome(outcome.Name, outcome.Outcome, expectsFailure);
}
private static int PartOf(double fraction, int parts)
{
int part = (int)(fraction * parts);
if (part < 0)
{
return 0;
}
return part >= parts ? parts - 1 : part;
}
///
/// Passes each draw through the curve's cumulative distribution, which makes a fair draw uniform, and tests the
/// result for being uniform.
///
private static NamedOutcome Shape(string name, long draws, Func draw, Func cumulative,
bool expectsFailure = false)
{
var counts = new int[Parts];
for (long i = 0; i < draws; i++)
{
counts[PartOf(cumulative(draw()), Parts)]++;
}
return Expecting(BoundedChecks.Uniform(name, counts, draws), expectsFailure);
}
private static NamedOutcome Categories(string name, double[] observed, double[] probabilities, long draws,
bool expectsFailure = false)
{
var expected = new double[probabilities.Length];
for (int i = 0; i < expected.Length; i++)
{
expected[i] = probabilities[i] * draws;
}
double statistic;
double degrees = Stats.ChiSquareOfCategories(observed, expected, minimumExpected: 5, out statistic);
string detail = (degrees + 1) + " categories, chi-square " + statistic.ToString("0.0");
return new NamedOutcome(name, new Outcome(Stats.ChiSquareTwoSided(statistic, degrees), detail),
expectsFailure);
}
private static NamedOutcome StaysIn(string name, long outside, string where)
{
return new NamedOutcome(name + " stays in its range",
new Outcome(outside == 0 ? 1 : 0, outside + " draws outside " + where));
}
private static NamedOutcome CountsOf(string name, long draws, double lambda, Func sample,
bool expectsFailure = false)
{
int categories = (int)(lambda + 12 * Math.Sqrt(lambda) + 20);
double[] probabilities = Stats.PoissonProbabilities(lambda, categories);
double tail = 1;
for (int i = 0; i < categories - 1; i++)
{
tail -= probabilities[i];
}
probabilities[categories - 1] = Math.Max(0, tail);
var observed = new double[categories];
for (long i = 0; i < draws; i++)
{
observed[Math.Min(sample(), categories - 1)]++;
}
return Categories(name, observed, probabilities, draws, expectsFailure);
}
private static void Events(RandOf rand, long draws, List into)
where GeneratorType : struct, IRandomGenerator
{
const double mean = 10;
Func exponential = value => -Math.Exp(-value / mean) + 1;
into.Add(Shape("NextEvent(10)", draws, () => rand.NextEvent(mean), exponential));
into.Add(Shape("uniform from 0 to twice the mean instead", draws, () => rand.NextDouble() * 2 * mean,
exponential, expectsFailure: true));
double[] means = { 10, 1000000, 0.1 };
double[] spans = { 15, 1, 1 };
foreach (int index in new[] { 0, 1, 2 })
{
double eventMean = means[index];
double span = spans[index];
Func truncated = value =>
(1 - Math.Exp(-value / eventMean)) / (1 - Math.Exp(-span / eventMean));
string name = "NextEventWithin(" + eventMean + ", " + span + ")";
long outside = 0;
into.Add(Shape(name, draws, () =>
{
double time = rand.NextEventWithin(eventMean, span);
if (time < 0 || time >= span)
{
outside++;
}
return time;
}, truncated));
into.Add(StaysIn(name, outside, "[0, " + span + ")"));
if (index == 0)
{
into.Add(Shape("uniform over the span instead", draws, () => rand.NextDouble() * span, truncated,
expectsFailure: true));
}
}
}
private static void EventLists(RandOf rand, long draws, List into)
where GeneratorType : struct, IRandomGenerator
{
const double span = 8;
const int count = 5;
long calls = draws / count;
var times = new List();
var pooled = new int[Parts];
var firsts = new int[Parts];
var lasts = new int[Parts];
var unspared = new int[Parts];
var unsparedLasts = new int[Parts];
long outside = 0;
for (long call = 0; call < calls; call++)
{
times.Clear();
rand.NextEventsWithin(span, count, times);
for (int i = 0; i < count; i++)
{
if (times[i] < 0 || times[i] >= span || (i > 0 && times[i] < times[i - 1]))
{
outside++;
}
pooled[PartOf(times[i] / span, Parts)]++;
}
// The smallest of five even draws falls below t with chance 1 - (1 - t/span)^5, the largest with chance
// (t/span)^5.
firsts[PartOf(1 - Math.Pow(1 - times[0] / span, count), Parts)]++;
lasts[PartOf(Math.Pow(times[count - 1] / span, count), Parts)]++;
// The tempting mistake: five gaps scaled so the fifth event is at the span, with no gap after it.
double total = 0;
var running = new double[count];
for (int i = 0; i < count; i++)
{
total += rand.NextEvent(1.0);
running[i] = total;
}
for (int i = 0; i < count; i++)
{
unspared[PartOf(running[i] / total, Parts)]++;
}
unsparedLasts[PartOf(Math.Pow(running[count - 1] / total, count), Parts)]++;
}
long positions = calls * count;
into.Add(BoundedChecks.Uniform("NextEventsWithin(8, 5), all times", pooled, positions));
into.Add(BoundedChecks.Uniform("NextEventsWithin(8, 5), earliest", firsts, calls));
into.Add(BoundedChecks.Uniform("NextEventsWithin(8, 5), latest", lasts, calls));
into.Add(StaysIn("NextEventsWithin(8, 5)", outside, "[0, 8) or out of order"));
into.Add(Expecting(BoundedChecks.Uniform("five gaps scaled to the span instead, all times", unspared,
positions), expectsFailure: true));
into.Add(Expecting(BoundedChecks.Uniform("five gaps scaled to the span instead, latest", unsparedLasts,
calls), expectsFailure: true));
}
private static void Counts(RandOf rand, long draws, List into)
where GeneratorType : struct, IRandomGenerator
{
double[] means = { 2, 1, 1, 0.5 };
double[] spans = { 1, 3, 12, 12.5 };
for (int index = 0; index < means.Length; index++)
{
double mean = means[index];
double span = spans[index];
into.Add(CountsOf("EventCount(" + mean + ", " + span + ")", draws, span / mean,
() => rand.EventCount(mean, span)));
}
into.Add(CountsOf("one too many events instead", draws, 3, () => rand.EventCount(1, 3) + 1,
expectsFailure: true));
// The chance of an event within a span is the share of waits that fit in it. The mean for a chance is the
// one whose waits fit in the interval that often.
long hits = 0;
for (long i = 0; i < draws; i++)
{
if (rand.NextEvent(8) <= 5)
{
hits++;
}
}
into.Add(BoundedChecks.Proportion("ChanceWithin(8, 5) against the waits", hits, draws,
EventRate.ChanceWithin(8, 5)));
into.Add(BoundedChecks.Proportion("a linear chance, span over mean, instead", hits, draws, 5.0 / 8,
expectsFailure: true));
double meanFor = EventRate.MeanFor(0.3, 10);
hits = 0;
for (long i = 0; i < draws; i++)
{
if (rand.NextEvent(meanFor) <= 10)
{
hits++;
}
}
into.Add(BoundedChecks.Proportion("MeanFor(0.3, 10) against the waits", hits, draws, 0.3));
}
private static Func BellCumulative(double min, double max, double mode, double tightness)
{
double modeFraction = (mode - min) / (max - min);
double a = 1 + tightness * modeFraction;
double b = modeFraction == 0 ? 1 + tightness : (1 + tightness * Math.Pow(modeFraction, -tightness *
modeFraction)) / a;
return value => 1 - Math.Pow(1 - Math.Pow((value - min) / (max - min), a), b);
}
private static void Bells(RandOf rand, long draws, List into)
where GeneratorType : struct, IRandomGenerator
{
double[] shapeA = { 2, 0.5, 5, 1, 30 };
double[] shapeB = { 3, 2, 0.5, 1, 30 };
for (int index = 0; index < shapeA.Length; index++)
{
double a = shapeA[index];
double b = shapeB[index];
into.Add(Shape("Kumaraswamy(" + a + ", " + b + ")", draws, () => rand.Kumaraswamy(a, b),
value => 1 - Math.Pow(1 - Math.Pow(value, a), b)));
}
into.Add(Shape("the shape numbers swapped instead", draws, () => rand.Kumaraswamy(3, 2),
value => 1 - Math.Pow(1 - Math.Pow(value, 2), 3), expectsFailure: true));
double[] mins = { 2, -5, 0, 0, 0 };
double[] maxes = { 10, 5, 1, 1, 100 };
double[] modes = { 4, 0, 1, 0, 70 };
double[] tightnesses = { 4, 10, 4, 4, 40 };
for (int index = 0; index < mins.Length; index++)
{
double min = mins[index];
double max = maxes[index];
double mode = modes[index];
double tightness = tightnesses[index];
string name = "Bell(" + min + ", " + max + ", " + mode + ", " + tightness + ")";
long outside = 0;
into.Add(Shape(name, draws, () =>
{
double value = rand.Bell(min, max, mode, tightness);
if (value < min || value >= max)
{
outside++;
}
return value;
}, BellCumulative(min, max, mode, tightness)));
into.Add(StaysIn(name, outside, "[" + min + ", " + max + ")"));
}
into.Add(Shape("a hill in the middle whatever the mode, instead", draws,
() => 2 + 8 * rand.Kumaraswamy(5, 5), BellCumulative(2, 10, 4, 4), expectsFailure: true));
}
private static double AngleInTurns(double x, double y)
{
double angle = Math.Atan2(y, x) / TwoPi;
return angle < 0 ? angle + 1 : angle;
}
///
/// Counts points in a grid of rings of equal area by wedges of equal angle, which a fair point in a disc or a
/// ring lands in evenly.
///
private static NamedOutcome Polar(string name, long draws, double innerRadius, double outerRadius,
Action draw, bool expectsFailure = false)
{
var counts = new int[PolarSide * PolarSide];
var point = new double[2];
double innerSquared = innerRadius * innerRadius;
double areaSpan = outerRadius * outerRadius - innerSquared;
for (long i = 0; i < draws; i++)
{
draw(point);
double ring = (point[0] * point[0] + point[1] * point[1] - innerSquared) / areaSpan;
counts[PartOf(ring, PolarSide) * PolarSide + PartOf(AngleInTurns(point[0], point[1]), PolarSide)]++;
}
return Expecting(BoundedChecks.Uniform(name, counts, draws), expectsFailure);
}
private static void Circles(RandOf rand, long draws, List into)
where GeneratorType : struct, IRandomGenerator
{
const double radius = 3;
long outside = 0;
into.Add(Polar("InCircle(3)", draws, 0, radius, point =>
{
double x;
double y;
rand.InCircle(radius, out x, out y);
if (x * x + y * y > radius * radius * (1 + 1e-12))
{
outside++;
}
point[0] = x;
point[1] = y;
}));
into.Add(StaysIn("InCircle(3)", outside, "the disc"));
into.Add(Polar("a distance that is the radius times a draw, instead", draws, 0, radius, point =>
{
double distance = radius * rand.NextDouble();
double angle = rand.NextDouble() * TwoPi;
point[0] = distance * Math.Cos(angle);
point[1] = distance * Math.Sin(angle);
}, expectsFailure: true));
outside = 0;
into.Add(Polar("InRing(1, 3)", draws, 1, radius, point =>
{
double x;
double y;
rand.InRing(1, radius, out x, out y);
double distanceSquared = x * x + y * y;
if (distanceSquared > radius * radius * (1 + 1e-12) || distanceSquared < 1 - 1e-12)
{
outside++;
}
point[0] = x;
point[1] = y;
}));
into.Add(StaysIn("InRing(1, 3)", outside, "the ring"));
into.Add(Polar("a distance spread evenly from 1 to 3, instead", draws, 1, radius, point =>
{
double distance = 1 + 2 * rand.NextDouble();
double angle = rand.NextDouble() * TwoPi;
point[0] = distance * Math.Cos(angle);
point[1] = distance * Math.Sin(angle);
}, expectsFailure: true));
outside = 0;
var directions = new int[Parts];
var squareDirections = new int[Parts];
for (long i = 0; i < draws; i++)
{
double x;
double y;
rand.OnCircle(radius, out x, out y);
if (Math.Abs(Math.Sqrt(x * x + y * y) - radius) > 1e-12 * radius)
{
outside++;
}
directions[PartOf(AngleInTurns(x, y), Parts)]++;
// The tempting mistake: a point in a square, scaled to the circle, bunches up along the diagonals.
double squareX = rand.NextDouble() * 2 - 1;
double squareY = rand.NextDouble() * 2 - 1;
squareDirections[PartOf(AngleInTurns(squareX, squareY), Parts)]++;
}
into.Add(BoundedChecks.Uniform("OnCircle(3), by angle", directions, draws));
into.Add(StaysIn("OnCircle(3)", outside, "the circle"));
into.Add(Expecting(BoundedChecks.Uniform("a point in a square, scaled to the circle, instead",
squareDirections, draws), expectsFailure: true));
}
///
/// Returns the chance of each total of dice, found by adding one die at a time.
///
public static double[] SumProbabilities(int dice, int sides)
{
var returnMe = new double[dice * sides + 1];
returnMe[0] = 1;
for (int die = 1; die <= dice; die++)
{
var next = new double[returnMe.Length];
for (int total = 0; total < returnMe.Length; total++)
{
if (returnMe[total] == 0)
{
continue;
}
for (int face = 1; face <= sides; face++)
{
next[total + face] += returnMe[total] / sides;
}
}
returnMe = next;
}
return returnMe;
}
private static void Walk(int dice, int sides, int keep, bool highest, int face, int remaining, int[] counts,
double[] probabilities, double[] factorials)
{
if (face == sides - 1)
{
counts[face] = remaining;
double ways = factorials[dice] / Math.Pow(sides, dice);
foreach (int count in counts)
{
ways /= factorials[count];
}
int kept = keep;
int total = 0;
for (int step = 0; step < sides && kept > 0; step++)
{
int which = highest ? sides - 1 - step : step;
int taken = Math.Min(counts[which], kept);
total += taken * (which + 1);
kept -= taken;
}
probabilities[total] += ways;
return;
}
for (int count = 0; count <= remaining; count++)
{
counts[face] = count;
Walk(dice, sides, keep, highest, face + 1, remaining - count, counts, probabilities, factorials);
}
}
///
/// Returns the chance of each total of the highest or lowest few of some dice, by going through every way the
/// dice can land in faces.
///
public static double[] KeptProbabilities(int dice, int sides, int keep, bool highest)
{
var factorials = new double[dice + 1];
factorials[0] = 1;
for (int i = 1; i <= dice; i++)
{
factorials[i] = factorials[i - 1] * i;
}
var returnMe = new double[dice * sides + 1];
Walk(dice, sides, keep, highest, 0, dice, new int[sides], returnMe, factorials);
return returnMe;
}
private static NamedOutcome Totals(string name, long draws, double[] probabilities, Func roll,
bool expectsFailure = false)
{
var observed = new double[probabilities.Length];
for (long i = 0; i < draws; i++)
{
observed[roll()]++;
}
return Categories(name, observed, probabilities, draws, expectsFailure);
}
private static void Dice(RandOf rand, long draws, List into)
where GeneratorType : struct, IRandomGenerator
{
into.Add(Totals("Roll(3, 6)", draws, SumProbabilities(3, 6), () => rand.Roll(3, 6)));
into.Add(Totals("one draw from 3 to 18, instead", draws, SumProbabilities(3, 6),
() => rand.NextInt(3, 19), expectsFailure: true));
into.Add(Totals("RollKeepHighest(4, 6, 3)", draws, KeptProbabilities(4, 6, 3, true),
() => rand.RollKeepHighest(4, 6, 3)));
into.Add(Totals("the first die dropped instead of the lowest", draws, KeptProbabilities(4, 6, 3, true),
() => rand.Roll(3, 6), expectsFailure: true));
into.Add(Totals("RollKeepHighest(2, 20, 1)", draws, KeptProbabilities(2, 20, 1, true),
() => rand.RollKeepHighest(2, 20, 1)));
into.Add(Totals("a single die instead of the best of two", draws, KeptProbabilities(2, 20, 1, true),
() => rand.Roll(1, 20), expectsFailure: true));
into.Add(Totals("RollKeepLowest(3, 20, 1)", draws, KeptProbabilities(3, 20, 1, false),
() => rand.RollKeepLowest(3, 20, 1)));
into.Add(Totals("RollKeepHighest(10, 6, 5)", draws, KeptProbabilities(10, 6, 5, true),
() => rand.RollKeepHighest(10, 6, 5)));
}
private static List KeysOfPermutations()
{
var returnMe = new List();
for (int first = 0; first < 4; first++)
{
for (int second = 0; second < 4; second++)
{
for (int third = 0; third < 4; third++)
{
for (int fourth = 0; fourth < 4; fourth++)
{
if (first != second && first != third && first != fourth && second != third &&
second != fourth && third != fourth)
{
returnMe.Add(first * 64 + second * 16 + third * 4 + fourth);
}
}
}
}
}
return returnMe;
}
private static NamedOutcome Even(string name, long draws, int[] counts, List keys,
bool expectsFailure = false)
{
var observed = new double[keys.Count];
var probabilities = new double[keys.Count];
for (int i = 0; i < keys.Count; i++)
{
observed[i] = counts[keys[i]];
probabilities[i] = 1.0 / keys.Count;
}
return Categories(name, observed, probabilities, draws, expectsFailure);
}
private static void Picks(RandOf rand, long draws, List into)
where GeneratorType : struct, IRandomGenerator
{
var weights = new List { 1, 2, 3, 4 };
var weighted = new double[4];
var unweighted = new double[4];
for (long i = 0; i < draws; i++)
{
weighted[rand.PickWeightedIndex(weights)]++;
unweighted[rand.NextInt(4)]++;
}
var shares = new[] { 0.1, 0.2, 0.3, 0.4 };
into.Add(Categories("PickWeightedIndex(1, 2, 3, 4)", weighted, shares, draws));
into.Add(Categories("an even pick instead", unweighted, shares, draws, expectsFailure: true));
var permutations = KeysOfPermutations();
var shuffled = new int[256];
var swappedAnywhere = new int[256];
var items = new int[4];
for (long i = 0; i < draws; i++)
{
for (int item = 0; item < 4; item++)
{
items[item] = item;
}
rand.Shuffle(items);
shuffled[items[0] * 64 + items[1] * 16 + items[2] * 4 + items[3]]++;
// The tempting mistake: swap each place with any place, which makes some orders likelier.
for (int item = 0; item < 4; item++)
{
items[item] = item;
}
for (int place = 0; place < 4; place++)
{
int other = rand.NextInt(4);
int held = items[place];
items[place] = items[other];
items[other] = held;
}
swappedAnywhere[items[0] * 64 + items[1] * 16 + items[2] * 4 + items[3]]++;
}
into.Add(Even("Shuffle of 4 items", draws, shuffled, permutations));
into.Add(Even("swapping each place with any place instead", draws, swappedAnywhere, permutations,
expectsFailure: true));
var pairs = new List();
for (int mask = 0; mask < 32; mask++)
{
if (Bits.PopCount((ulong)mask) == 2)
{
pairs.Add(mask);
}
}
var sampled = new int[32];
var consecutive = new int[32];
var five = new[] { 0, 1, 2, 3, 4 };
var chosen = new List();
for (long i = 0; i < draws; i++)
{
chosen.Clear();
rand.Sample(five, 2, chosen);
sampled[(1 << chosen[0]) | (1 << chosen[1])]++;
int start = rand.NextInt(4);
consecutive[(1 << start) | (1 << (start + 1))]++;
}
into.Add(Even("Sample of 2 from 5", draws, sampled, pairs));
into.Add(Even("two neighbors from a random start instead", draws, consecutive, pairs,
expectsFailure: true));
long bools = 0;
long chances = 0;
for (long i = 0; i < draws; i++)
{
if (rand.NextBool())
{
bools++;
}
if (rand.Chance(0.3))
{
chances++;
}
}
into.Add(BoundedChecks.Proportion("NextBool()", bools, draws, 0.5));
into.Add(BoundedChecks.Proportion("Chance(0.3)", chances, draws, 0.3));
}
public static List Run(GeneratorType start, Scale scale)
where GeneratorType : struct, IRandomGenerator
{
var rand = new RandOf(start);
long draws = scale.Count(1L << 21, minimum: 1L << 17);
var returnMe = new List();
Events(rand, draws, returnMe);
EventLists(rand, draws, returnMe);
Counts(rand, scale.Count(1L << 19, minimum: 1L << 15), returnMe);
Bells(rand, draws, returnMe);
Circles(rand, draws, returnMe);
Dice(rand, scale.Count(1L << 20, minimum: 1L << 16), returnMe);
Picks(rand, scale.Count(1L << 20, minimum: 1L << 16), returnMe);
return returnMe;
}
}
}