@@ -16,6 +16,8 @@ const math = std.math;
16
16
const ziggurat = @import ("rand/ziggurat.zig" );
17
17
const maxInt = std .math .maxInt ;
18
18
19
+ const Dilbert = @import ("rand/Dilbert.zig" );
20
+
19
21
/// Fast unbiased random numbers.
20
22
pub const DefaultPrng = Xoshiro256 ;
21
23
@@ -249,18 +251,51 @@ pub const Random = struct {
249
251
250
252
/// Return a floating point value evenly distributed in the range [0, 1).
251
253
pub fn float (r : Random , comptime T : type ) T {
252
- // Generate a uniform value between [1, 2) and scale down to [0, 1).
253
- // Note: The lowest mantissa bit is always set to 0 so we only use half the available range.
254
+ // Generate a uniformly random value between for the mantissa.
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
258
switch (T ) {
255
259
f32 = > {
256
- const s = r .int (u32 );
257
- const repr = (0x7f << 23 ) | (s >> 9 );
258
- return @bitCast (f32 , repr ) - 1.0 ;
260
+ // Use 23 random bits for the mantissa, and the rest for the exponent.
261
+ // If all 41 bits are zero, generate additional random bits, until a
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
276
f64 = > {
261
- const s = r .int (u64 );
262
- const repr = (0x3ff << 52 ) | (s >> 12 );
263
- return @bitCast (f64 , repr ) - 1.0 ;
277
+ // Use 52 random bits for the mantissa, and the rest for the exponent.
278
+ // If all 12 bits are zero, generate additional random bits, until a
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
300
else = > @compileError ("unknown floating point type" ),
266
301
}
@@ -573,7 +608,7 @@ test "splitmix64 sequence" {
573
608
}
574
609
575
610
// Actual Random helper function tests, pcg engine is assumed correct.
576
- test "Random float" {
611
+ test "Random float correctness " {
577
612
var prng = DefaultPrng .init (0 );
578
613
const random = prng .random ();
579
614
@@ -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
702
test "Random shuffle" {
593
703
var prng = DefaultPrng .init (0 );
594
704
const random = prng .random ();
0 commit comments