| ... | @@ -16,6 +16,8 @@ const math = std.math; | ... | @@ -16,6 +16,8 @@ const math = std.math; |
| 16 | const ziggurat = @import("rand/ziggurat.zig"); | 16 | const ziggurat = @import("rand/ziggurat.zig"); |
| 17 | const maxInt = std.math.maxInt; | 17 | const maxInt = std.math.maxInt; |
| 18 | | 18 | |
| | 19 | const Dilbert = @import("rand/Dilbert.zig"); |
| | 20 | |
| 19 | /// Fast unbiased random numbers. | 21 | /// Fast unbiased random numbers. |
| 20 | pub const DefaultPrng = Xoshiro256; | 22 | pub const DefaultPrng = Xoshiro256; |
| 21 | | 23 | |
| ... | @@ -249,18 +251,51 @@ pub const Random = struct { | ... | @@ -249,18 +251,51 @@ pub const Random = struct { |
| 249 | | 251 | |
| 250 | /// Return a floating point value evenly distributed in the range [0, 1). | 252 | /// Return a floating point value evenly distributed in the range [0, 1). |
| 251 | pub fn float(r: Random, comptime T: type) T { | 253 | pub fn float(r: Random, comptime T: type) T { |
| 252 | // Generate a uniform value between [1, 2) and scale down to [0, 1). | 254 | // Generate a uniformly random value between for the mantissa. |
| 253 | // Note: The lowest mantissa bit is always set to 0 so we only use half the available range. | 255 | // Then generate an exponentially biased random value for the exponent. |
| | 256 | // Over the previous method, this has the advantage of being able to |
| | 257 | // represent every possible value in the available range. |
| 254 | switch (T) { | 258 | switch (T) { |
| 255 | f32 => { | 259 | f32 => { |
| 256 | const s = r.int(u32); | 260 | // Use 23 random bits for the mantissa, and the rest for the exponent. |
| 257 | const repr = (0x7f << 23) | (s >> 9); | 261 | // If all 41 bits are zero, generate additional random bits, until a |
| 258 | return @bitCast(f32, repr) - 1.0; | 262 | // set bit is found, or 126 bits have been generated. |
| | 263 | const rand = r.int(u64); |
| | 264 | var rand_lz = @clz(u64, rand | 0x7FFFFF); |
| | 265 | if (rand_lz == 41) { |
| | 266 | rand_lz += @clz(u64, r.int(u64)); |
| | 267 | if (rand_lz == 41 + 64) { |
| | 268 | // It is astronomically unlikely to reach this point. |
| | 269 | rand_lz += @clz(u32, r.int(u32) | 0x7FF); |
| | 270 | } |
| | 271 | } |
| | 272 | const mantissa = @truncate(u23, rand); |
| | 273 | const exponent = @as(u32, 126 - rand_lz) << 23; |
| | 274 | return @bitCast(f32, exponent | mantissa); |
| 259 | }, | 275 | }, |
| 260 | f64 => { | 276 | f64 => { |
| 261 | const s = r.int(u64); | 277 | // Use 52 random bits for the mantissa, and the rest for the exponent. |
| 262 | const repr = (0x3ff << 52) | (s >> 12); | 278 | // If all 12 bits are zero, generate additional random bits, until a |
| 263 | return @bitCast(f64, repr) - 1.0; | 279 | // set bit is found, or 1022 bits have been generated. |
| | 280 | const rand = r.int(u64); |
| | 281 | var rand_lz: u64 = @clz(u64, rand | 0xFFFFFFFFFFFFF); |
| | 282 | if (rand_lz == 12) { |
| | 283 | while (true) { |
| | 284 | // It is astronomically unlikely for this loop to execute more than once. |
| | 285 | const addl_rand_lz = @clz(u64, r.int(u64)); |
| | 286 | rand_lz += addl_rand_lz; |
| | 287 | if (addl_rand_lz != 64) { |
| | 288 | break; |
| | 289 | } |
| | 290 | if (rand_lz >= 1022) { |
| | 291 | rand_lz = 1022; |
| | 292 | break; |
| | 293 | } |
| | 294 | } |
| | 295 | } |
| | 296 | const mantissa = rand & 0xFFFFFFFFFFFFF; |
| | 297 | const exponent = (1022 - rand_lz) << 52; |
| | 298 | return @bitCast(f64, exponent | mantissa); |
| 264 | }, | 299 | }, |
| 265 | else => @compileError("unknown floating point type"), | 300 | else => @compileError("unknown floating point type"), |
| 266 | } | 301 | } |
| ... | @@ -573,7 +608,7 @@ test "splitmix64 sequence" { | ... | @@ -573,7 +608,7 @@ test "splitmix64 sequence" { |
| 573 | } | 608 | } |
| 574 | | 609 | |
| 575 | // Actual Random helper function tests, pcg engine is assumed correct. | 610 | // Actual Random helper function tests, pcg engine is assumed correct. |
| 576 | test "Random float" { | 611 | test "Random float correctness" { |
| 577 | var prng = DefaultPrng.init(0); | 612 | var prng = DefaultPrng.init(0); |
| 578 | const random = prng.random(); | 613 | const random = prng.random(); |
| 579 | | 614 | |
| ... | @@ -589,6 +624,81 @@ test "Random float" { | ... | @@ -589,6 +624,81 @@ test "Random float" { |
| 589 | } | 624 | } |
| 590 | } | 625 | } |
| 591 | | 626 | |
| | 627 | // Check the "astronomically unlikely" code paths. |
| | 628 | test "Random float coverage" { |
| | 629 | var prng = try Dilbert.init(&[_]u8{0}); |
| | 630 | const random = prng.random(); |
| | 631 | |
| | 632 | const rand_f64 = random.float(f64); |
| | 633 | const rand_f32 = random.float(f32); |
| | 634 | |
| | 635 | try expect(rand_f32 == 0.0); |
| | 636 | try expect(rand_f64 == 0.0); |
| | 637 | } |
| | 638 | |
| | 639 | test "Random float chi-square goodness of fit" { |
| | 640 | const num_numbers = 100000; |
| | 641 | const num_buckets = 1000; |
| | 642 | |
| | 643 | var f32_hist = std.AutoHashMap(u32, u32).init(std.testing.allocator); |
| | 644 | defer f32_hist.deinit(); |
| | 645 | var f64_hist = std.AutoHashMap(u64, u32).init(std.testing.allocator); |
| | 646 | defer f64_hist.deinit(); |
| | 647 | |
| | 648 | var prng = DefaultPrng.init(0); |
| | 649 | const random = prng.random(); |
| | 650 | |
| | 651 | var i: usize = 0; |
| | 652 | while (i < num_numbers) : (i += 1) { |
| | 653 | const rand_f32 = random.float(f32); |
| | 654 | const rand_f64 = random.float(f64); |
| | 655 | var f32_put = try f32_hist.getOrPut(@floatToInt(u32, rand_f32 * @intToFloat(f32, num_buckets))); |
| | 656 | if (f32_put.found_existing) { |
| | 657 | f32_put.value_ptr.* += 1; |
| | 658 | } else { |
| | 659 | f32_put.value_ptr.* = 0; |
| | 660 | } |
| | 661 | var f64_put = try f64_hist.getOrPut(@floatToInt(u32, rand_f64 * @intToFloat(f64, num_buckets))); |
| | 662 | if (f64_put.found_existing) { |
| | 663 | f64_put.value_ptr.* += 1; |
| | 664 | } else { |
| | 665 | f64_put.value_ptr.* = 0; |
| | 666 | } |
| | 667 | } |
| | 668 | |
| | 669 | var f32_total_variance: f64 = 0; |
| | 670 | var f64_total_variance: f64 = 0; |
| | 671 | |
| | 672 | { |
| | 673 | var j: u32 = 0; |
| | 674 | while (j < num_buckets) : (j += 1) { |
| | 675 | const count = @intToFloat(f64, (if (f32_hist.get(j)) |v| v else 0)); |
| | 676 | const expected = @intToFloat(f64, num_numbers) / @intToFloat(f64, num_buckets); |
| | 677 | const delta = count - expected; |
| | 678 | const variance = (delta * delta) / expected; |
| | 679 | f32_total_variance += variance; |
| | 680 | } |
| | 681 | } |
| | 682 | |
| | 683 | { |
| | 684 | var j: u64 = 0; |
| | 685 | while (j < num_buckets) : (j += 1) { |
| | 686 | const count = @intToFloat(f64, (if (f64_hist.get(j)) |v| v else 0)); |
| | 687 | const expected = @intToFloat(f64, num_numbers) / @intToFloat(f64, num_buckets); |
| | 688 | const delta = count - expected; |
| | 689 | const variance = (delta * delta) / expected; |
| | 690 | f64_total_variance += variance; |
| | 691 | } |
| | 692 | } |
| | 693 | |
| | 694 | // Corresponds to a p-value > 0.05. |
| | 695 | // Critical value is calculated by opening a Python interpreter and running: |
| | 696 | // scipy.stats.chi2.isf(0.05, num_buckets - 1) |
| | 697 | const critical_value = 1073.6426506574246; |
| | 698 | try expect(f32_total_variance < critical_value); |
| | 699 | try expect(f64_total_variance < critical_value); |
| | 700 | } |
| | 701 | |
| 592 | test "Random shuffle" { | 702 | test "Random shuffle" { |
| 593 | var prng = DefaultPrng.init(0); | 703 | var prng = DefaultPrng.init(0); |
| 594 | const random = prng.random(); | 704 | const random = prng.random(); |