using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace Amphora.RandomComparisons { /// /// Checks the checks. The statistics are compared with values computed independently by scipy, fractions and a /// separate Python implementation. The generators and controls are run through a quick battery. Every control /// must fail what it is known to fail, and no real generator may fail anything. /// public static class SelfCheck { private sealed class Reporter { private readonly TextWriter output; public int Checks; public int Failures; public Reporter(TextWriter output) { this.output = output; } public void Check(string name, bool passed, string detail = "") { Checks++; if (!passed) { Failures++; output.WriteLine("FAILED " + name + (detail == "" ? "" : ": " + detail)); } } public void Near(string name, double expected, double actual, double relativeTolerance) { double allowed = Math.Max(Math.Abs(expected) * relativeTolerance, 1e-300); Check(name, Math.Abs(expected - actual) <= allowed, "expected " + expected + " but was " + actual); } } private static readonly (double Statistic, double Degrees, double P)[] ChiSquareCases = { (65871.5, 65535, 0.3525158799117868), (16000.0, 16383, 0.03326359930771983), (262228.0, 262143, 0.9058307485331057), (50.0, 100, 1.3906610495232403e-05), (5.3, 6, 0.9880980009419972), (1060.3, 999, 0.17409941759607617), (4981699.5, 262143, 0.0), (3.0, 3, 0.7832503525421755), (0.5, 3, 0.16221717669064836), (12.0, 3, 0.014766321010719539), (66150.9, 65535, 0.08955455299533015), (64611.4, 65535, 0.010426067734684415), (16384.0, 16383, 0.9926537570520125), (1000000.0, 999999, 0.9990596839762254), }; private static readonly (double Z, double P)[] NormalCases = { (0.0, 1.0), (0.5, 0.6170750774519738), (2.0, 0.0455002638963584), (3.0, 0.002699796063260186), (5.0, 5.733031437583867e-07), (8.0, 1.244192114854348e-15), }; private static byte[] BitsOf(ulong seed, int length, Window window) { var generator = new SplitMix64(seed); var returnMe = new byte[length]; for (int i = 0; i < length; i++) { returnMe[i] = (byte)Windows.EdgeBit(generator.NextUInt64(), window); } return returnMe; } private static ulong[] RowsOf(ulong seed, int size, Window window) { var generator = new SplitMix64(seed); var returnMe = new ulong[size]; for (int i = 0; i < size; i++) { ulong word = generator.NextUInt64(); returnMe[i] = size == 64 ? word : Windows.Take(word, window, size); } return returnMe; } private static double Mean(double[] probabilities) { double returnMe = 0; for (int total = 0; total < probabilities.Length; total++) { returnMe += total * probabilities[total]; } return returnMe; } private static void CheckDiceDistributions(Reporter reporter) { double[] threeD6 = DistributionChecks.SumProbabilities(3, 6); reporter.Near("3d6 at 10", 0.125, threeD6[10], 1e-12); reporter.Near("3d6 at 3", 1.0 / 216, threeD6[3], 1e-12); reporter.Near("3d6 adds to one", 1, threeD6.Sum(), 1e-12); double[] dropLowest = DistributionChecks.KeptProbabilities(4, 6, 3, highest: true); reporter.Near("4d6 dropping the lowest at 18", 7.0 / 432, dropLowest[18], 1e-12); reporter.Near("4d6 dropping the lowest at 3", 1.0 / 1296, dropLowest[3], 1e-12); reporter.Near("4d6 dropping the lowest, mean", 12.244598765432098, Mean(dropLowest), 1e-12); reporter.Near("4d6 dropping the lowest adds to one", 1, dropLowest.Sum(), 1e-12); double[] advantage = DistributionChecks.KeptProbabilities(2, 20, 1, highest: true); reporter.Near("best of two d20 at 20", 0.0975, advantage[20], 1e-12); reporter.Near("best of two d20 at 1", 0.0025, advantage[1], 1e-12); double[] disadvantage = DistributionChecks.KeptProbabilities(3, 20, 1, highest: false); reporter.Near("worst of three d20 at 1", 0.142625, disadvantage[1], 1e-12); reporter.Near("worst of three d20 at 20", 0.000125, disadvantage[20], 1e-12); reporter.Near("worst of three d20, mean", 5.5125, Mean(disadvantage), 1e-12); double[] tenD6 = DistributionChecks.KeptProbabilities(10, 6, 5, highest: true); reporter.Near("best five of ten d6 at 30", 0.015461966703500482, tenD6[30], 1e-9); reporter.Near("best five of ten d6, mean", 24.179310264303798, Mean(tenD6), 1e-9); reporter.Near("best five of ten d6 adds to one", 1, tenD6.Sum(), 1e-9); } private static void CheckStatistics(Reporter reporter) { foreach (var testCase in ChiSquareCases) { reporter.Near("chi-square " + testCase.Statistic + " on " + testCase.Degrees + " df", testCase.P, Stats.ChiSquareTwoSided(testCase.Statistic, testCase.Degrees), 1e-6); } foreach (var testCase in NormalCases) { reporter.Near("normal " + testCase.Z, testCase.P, Stats.NormalTwoSided(testCase.Z), 1e-6); } double[] eight = Stats.PoissonProbabilities(8, 16); reporter.Near("Poisson(8) at 0", 0.00033546262790251185, eight[0], 1e-9); reporter.Near("Poisson(8) at 5", 0.09160366159257921, eight[5], 1e-9); reporter.Near("Poisson(8) at 8", 0.13958653195059664, eight[8], 1e-9); reporter.Near("Poisson(8) at 15", 0.009025979411121507, eight[15], 1e-9); double[] four = Stats.PoissonProbabilities(4, 16); reporter.Near("Poisson(4) at 1", 0.07326255555493673, four[1], 1e-9); reporter.Near("Poisson(4) at 8", 0.029770181304863145, four[8], 1e-9); reporter.Near("32x32 full rank", 0.28878809515384113, Stats.BinaryRankProbability(32, 32), 1e-9); reporter.Near("32x32 rank 31", 0.5775761901732048, Stats.BinaryRankProbability(32, 31), 1e-9); reporter.Near("32x32 rank 30", 0.12835026442316666, Stats.BinaryRankProbability(32, 30), 1e-9); reporter.Near("64x64 full rank", 0.2887880950866024, Stats.BinaryRankProbability(64, 64), 1e-9); reporter.Near("64x64 rank 63", 0.5775761901732048, Stats.BinaryRankProbability(64, 63), 1e-9); reporter.Near("64x64 rank 62", 0.1283502644829344, Stats.BinaryRankProbability(64, 62), 1e-9); reporter.Near("worst of 8 at 0.01", 1 - Math.Pow(0.99, 8), Stats.WorstOf(0.01, 8), 1e-12); reporter.Near("worst of 8 at 1e-12", 8e-12, Stats.WorstOf(1e-12, 8), 1e-9); reporter.Near("worst of 8 at 1", 1, Stats.WorstOf(1, 8), 1e-12); CheckDiceDistributions(reporter); double statistic; double degrees = Stats.ChiSquareOfCategories(new double[] { 10, 1, 1, 8 }, new double[] { 9, 2, 2, 7 }, 5, out statistic); reporter.Check("categories merge until each is large enough", degrees == 1, "degrees " + degrees + " but expected 1"); reporter.Near("pooled statistic", 1.0 / 9 + 1.0 / 11, statistic, 1e-12); } private static void CheckLinearAlgebra(Reporter reporter) { var machine = new BerlekampMassey(2000); reporter.Check("complexity of 50 bits", machine.Complexity(BitsOf(1, 50, Window.High), 50) == 26); reporter.Check("complexity of 300 bits", machine.Complexity(BitsOf(2, 300, Window.High), 300) == 150); reporter.Check("complexity of 2000 bits", machine.Complexity(BitsOf(3, 2000, Window.High), 2000) == 1001); reporter.Check("complexity of another 2000 bits", machine.Complexity(BitsOf(4, 2000, Window.High), 2000) == 1000); reporter.Check("complexity of 2000 low bits", machine.Complexity(BitsOf(5, 2000, Window.Low), 2000) == 1000); var lfsr = new byte[400]; int[] start = { 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0 }; for (int i = 0; i < lfsr.Length; i++) { lfsr[i] = i < start.Length ? (byte)start[i] : (byte)(lfsr[i - 3] ^ lfsr[i - 17]); } reporter.Check("a degree 17 register has complexity 17", machine.Complexity(lfsr, lfsr.Length) == 17); reporter.Check("zeros have complexity 0", machine.Complexity(new byte[100], 100) == 0); var lateOne = new byte[100]; lateOne[99] = 1; reporter.Check("a lone late one has full complexity", machine.Complexity(lateOne, 100) == 100); var alternating = new byte[100]; for (int i = 0; i < alternating.Length; i++) { alternating[i] = (byte)(i % 2); } reporter.Check("alternating bits have complexity 2", machine.Complexity(alternating, 100) == 2); reporter.Check("a machine can be reused", machine.Complexity(lfsr, lfsr.Length) == 17); int[] lowRanks = { 31, 32, 31, 31, 31, 30 }; for (int seed = 1; seed <= lowRanks.Length; seed++) { reporter.Check("rank of 32x32 matrix " + seed, BinaryRank.Rank(RowsOf((ulong)seed, 32, Window.Low), 32) == lowRanks[seed - 1]); } for (int seed = 1; seed <= 3; seed++) { reporter.Check("rank of 64x64 matrix " + seed, BinaryRank.Rank(RowsOf((ulong)seed, 64, Window.High), 64) == 63); } var identity = new ulong[32]; for (int i = 0; i < identity.Length; i++) { identity[i] = 1UL << i; } reporter.Check("the identity has full rank", BinaryRank.Rank(identity, 32) == 32); reporter.Check("zeros have rank 0", BinaryRank.Rank(new ulong[32], 32) == 0); identity[5] = identity[4]; reporter.Check("a repeated row costs a rank", BinaryRank.Rank(identity, 32) == 31); int[] expectedCategories = { 0, 1, 3, 4, 5, 6 }; int[] complexities = { 997, 998, 1000, 1001, 1002, 1003 }; for (int i = 0; i < complexities.Length; i++) { reporter.Check("complexity " + complexities[i] + " of 2000 falls in category " + expectedCategories[i], LinearComplexity.Category(complexities[i], 2000) == expectedCategories[i]); } reporter.Near("complexity categories cover everything", 1, LinearComplexity.Probabilities.Sum(), 1e-12); reporter.Check("popcount of zero", Bits.PopCount(0) == 0); reporter.Check("popcount of all ones", Bits.PopCount(ulong.MaxValue) == 64); reporter.Check("popcount of a pattern", Bits.PopCount(0xF0F0F0F0F0F0F0F1UL) == 33); } private static void CheckWindowsAndSources(Reporter reporter) { const ulong word = 0xF000000000000001UL; reporter.Check("high bits", Windows.Take(word, Window.High, 4) == 0xF); reporter.Check("low bits", Windows.Take(word, Window.Low, 4) == 1); reporter.Check("high edge bit", Windows.EdgeBit(word, Window.High) == 1); reporter.Check("low edge bit", Windows.EdgeBit(word ^ 1, Window.Low) == 0); Func make = seed => new GeneratorWords(new SplitMix64(seed)); var adjacent = new WordStream(new AdjacentSeedWords(make, firstSeed: 10, wordsPerSeed: 2)); var expectedFirst = new SplitMix64(10); var expectedSecond = new SplitMix64(11); bool adjacentMatches = adjacent.Next() == expectedFirst.NextUInt64() && adjacent.Next() == expectedFirst.NextUInt64() && adjacent.Next() == expectedSecond.NextUInt64() && adjacent.Next() == expectedSecond.NextUInt64(); reporter.Check("adjacent seeds give each seed's first words in turn", adjacentMatches); bool threw = false; try { new AdjacentSeedWords(make, 1, 3).Fill(new ulong[10], 0, 10); } catch (ArgumentException) { threw = true; } reporter.Check("a fill must hold whole seeds", threw); var interleaved = new WordStream(new InterleavedWords(new[] { make(1), make(2) })); var one = new SplitMix64(1); var two = new SplitMix64(2); bool interleavedMatches = interleaved.Next() == one.NextUInt64() && interleaved.Next() == two.NextUInt64() && interleaved.Next() == one.NextUInt64() && interleaved.Next() == two.NextUInt64(); reporter.Check("interleaved streams take one word each in turn", interleavedMatches); var system = new WordStream(new SystemRandomWords(5)); var again = new WordStream(new SystemRandomWords(5)); reporter.Check("System.Random words repeat for a seed", system.Next() == again.Next()); reporter.Check("a verdict under the failure line", new Outcome(1e-7, "").Verdict == "FAIL"); reporter.Check("a verdict under the suspect line", new Outcome(1e-4, "").Verdict == "SUSPECT"); reporter.Check("a verdict above both", new Outcome(0.5, "").Verdict == "ok"); reporter.Check("information has no verdict", Outcome.Information("x").Verdict == "info"); var even = new List(); for (int i = 0; i < 200; i++) { even.Add((i + 0.5) / 200); } reporter.Check("an even spread is uniform", Calibration.UniformityP(even) > 0.99); reporter.Check("a pile at zero is not uniform", Calibration.UniformityP(new List(new double[50])) < 1e-6); } private static int SettledDraws(Outcome outcome) { string marker = "look fair: "; int start = outcome.Detail.IndexOf(marker, StringComparison.Ordinal) + marker.Length; int end = outcome.Detail.IndexOf(' ', start); return int.Parse(outcome.Detail.Substring(start, end - start)); } private static void CheckControlsAndGenerators(Reporter reporter, TextWriter output) { var failsEach = new Dictionary { ["RANDU"] = new[] { "serial, high bits", "birthday spacings 2^32, high bits", "binary rank 32x32, high bits" }, ["MINSTD"] = new[] { "birthday spacings 2^28, high bits", "birthday spacings 2^32, high bits", "binary rank 32x32, high bits" }, ["LCG64"] = new[] { "serial, low bits", "linear complexity, low bits" }, ["xorshift64"] = new[] { "linear complexity, high bits", "linear complexity, low bits", "binary rank 64x64" }, ["xoshiro256+"] = new[] { "linear complexity, low bits" }, ["System.Random"] = new[] { "first 8 draws of each seed, high bits", "first draw of consecutive seeds, high bits" }, }; List subjects = Subjects.All(); output.WriteLine("Running the quick battery on " + subjects.Count + " subjects."); List[] outcomes = Battery.RunAll(subjects, new Scale(Program.QuickDivisor), Program.DefaultSeed, subject => { }); for (int i = 0; i < subjects.Count; i++) { Subject subject = subjects[i]; if (subject.IsControl) { foreach (string name in failsEach[subject.Name]) { NamedOutcome found = outcomes[i].FirstOrDefault(outcome => outcome.Name == name); reporter.Check(subject.Name + " fails " + name, found != null && found.Outcome.Verdict == "FAIL", found == null ? "no such check" : found.Outcome.PText); } continue; } foreach (NamedOutcome outcome in outcomes[i]) { if (outcome.ExpectsFailure) { reporter.Check(subject.Name + " fails its control '" + outcome.Name + "'", outcome.Outcome.Verdict == "FAIL"); } else { reporter.Check(subject.Name + " passes " + outcome.Name, outcome.Outcome.Verdict != "FAIL", outcome.Outcome.PText); } } } int splitMixIndex = subjects.FindIndex(subject => subject.Name == "SplitMix64"); int xoshiroIndex = subjects.FindIndex(subject => subject.Name == "xoshiro256++"); Func settled = index => SettledDraws(outcomes[index].First( outcome => outcome.Name == "recovery from single-bit states").Outcome); reporter.Check("SplitMix64 mixes a single-bit state at once", settled(splitMixIndex) == 0); reporter.Check("xoshiro256++ needs some draws to mix a single-bit state", settled(xoshiroIndex) > 0); } private static void CheckRanges(Reporter reporter) { reporter.Check("gap below 1.5", RangeChecks.GapBelow(1.5, RangeChecks.DoubleMantissaBits) == Math.Pow(2, -52)); reporter.Check("gap below a power of two is the finer one", RangeChecks.GapBelow(1, RangeChecks.DoubleMantissaBits) == Math.Pow(2, -53)); reporter.Check("gap below 7.25", RangeChecks.GapBelow(7.25, RangeChecks.DoubleMantissaBits) == Math.Pow(2, -50)); reporter.Check("gap below a negative end", RangeChecks.GapBelow(-1000, RangeChecks.DoubleMantissaBits) == Math.Pow(2, -43)); reporter.Check("float gap below 1.5", RangeChecks.GapBelow(1.5, RangeChecks.FloatMantissaBits) == Math.Pow(2, -23)); reporter.Check("float gap below 2", RangeChecks.GapBelow(2, RangeChecks.FloatMantissaBits) == Math.Pow(2, -23)); var largest = new LargestWords(); reporter.Check("the largest double is one step under 1", largest.NextDouble() == 1 - Math.Pow(2, -53)); reporter.Check("the largest float is one step under 1", largest.NextFloat() == 1 - (float)Math.Pow(2, -24)); var generator = new Sfc64(1); double gap = Math.Pow(2, -52); NamedOutcome even = RangeChecks.OnGrid("even", 1, 2, mantissaBits: RangeChecks.DoubleMantissaBits, draws: 1 << 20, draw: () => 1 + generator.NextUInt64(1UL << 52) * gap); reporter.Check("draws uniform over the cells pass", even.Outcome.Verdict == "ok", even.Outcome.PText); NamedOutcome negative = RangeChecks.OnGrid("negative", -1.5, -1, mantissaBits: RangeChecks.DoubleMantissaBits, draws: 1 << 20, draw: () => -1.5 + generator.NextUInt64(1UL << 51) * gap); reporter.Check("draws uniform over negative cells pass", negative.Outcome.Verdict == "ok", negative.Outcome.PText); NamedOutcome evenCells = RangeChecks.OnGrid("even cells", 1, 2, mantissaBits: RangeChecks.DoubleMantissaBits, draws: 1 << 20, draw: () => 1 + generator.NextUInt64(1UL << 51) * 2 * gap); reporter.Check("draws that never land on an odd cell fail", evenCells.Outcome.Verdict == "FAIL"); } public static int Run(TextWriter output) { var reporter = new Reporter(output); CheckStatistics(reporter); CheckRanges(reporter); CheckLinearAlgebra(reporter); CheckWindowsAndSources(reporter); CheckControlsAndGenerators(reporter, output); output.WriteLine(reporter.Failures == 0 ? "All " + reporter.Checks + " self-checks passed." : reporter.Failures + " of " + reporter.Checks + " self-checks FAILED."); return reporter.Failures == 0 ? 0 : 1; } } }