123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364(** This module implements "Fast Splittable Pseudorandom Number Generators" by Steele et.
al. (1). The paper's algorithm provides decent randomness for most purposes, but
sacrifices cryptographic-quality randomness in favor of performance. The original
implementation was tested with DieHarder and BigCrush; see the paper for details.
Our implementation is a port from Java to OCaml of the paper's algorithm. Other than
the choice of initial seed for [create], our port should be faithful. We have not
re-run the DieHarder or BigCrush tests on our implementation. Our port is also not as
performant as the original; two factors that hurt us are boxed [int64] values and lack
of a POPCNT primitive.
(1) http://2014.splashcon.org/event/oopsla2014-fast-splittable-pseudorandom-number-generators
(also mirrored at http://gee.cs.oswego.edu/dl/papers/oopsla14.pdf)
Beware when implementing this interface; it is easy to implement a [split] operation
whose output is not as "independent" as it seems (2). This bug caused problems for
Haskell's Quickcheck library for a long time.
(2) Schaathun, "Evaluation of splittable pseudo-random generators", JFP 2015.
http://www.hg.schaathun.net/research/Papers/hgs2015jfp.pdf
*)open!BaseopenInt64.Oletis_oddx=xlor1L=xletpopcount=Int64.popcountmoduleState=structtypet={mutableseed:int64;odd_gamma:int64}letgolden_gamma=0x9e37_79b9_7f4a_7c15Lletof_intseed={seed=Int64.of_intseed;odd_gamma=golden_gamma}letcopy{seed;odd_gamma}={seed;odd_gamma}letmix_bitszn=zlxor(zlsrn)letmix64z=letz=(mix_bitsz33)*0xff51_afd7_ed55_8ccdLinletz=(mix_bitsz33)*0xc4ce_b9fe_1a85_ec53Linmix_bitsz33letmix64_variant13z=letz=(mix_bitsz30)*0xbf58_476d_1ce4_e5b9Linletz=(mix_bitsz27)*0x94d0_49bb_1331_11ebLinmix_bitsz31letmix_odd_gammaz=letz=(mix64_variant13z)lor1Linletn=popcount(zlxor(zlsr1))in(* The original paper uses [>=] in the conditional immediately below; however this is
a typo, and we correct it by using [<]. This was fixed in response to [1] and [2].
[1] https://github.com/janestreet/splittable_random/issues/1
[2] http://www.pcg-random.org/posts/bugs-in-splitmix.html
*)ifInt.(<)n24thenzlxor0xaaaa_aaaa_aaaa_aaaaLelsezlet%test_unit"odd gamma"=forinput=-1_000_000to1_000_000doletoutput=mix_odd_gamma(Int64.of_intinput)inifnot(is_oddoutput)thenError.raise_s[%message"gamma value is not odd"(input:int)(output:int64)]doneletnext_seedt=letnext=t.seed+t.odd_gammaint.seed<-next;nextletof_seed_and_gamma~seed~gamma=letseed=mix64seedinletodd_gamma=mix_odd_gammagammain{seed;odd_gamma}letrandom_int64random_state=Random.State.int64_inclrandom_stateInt64.min_valueInt64.max_valueletcreaterandom_state=letseed=random_int64random_stateinletgamma=random_int64random_stateinof_seed_and_gamma~seed~gammaletsplitt=letseed=next_seedtinletgamma=next_seedtinof_seed_and_gamma~seed~gammaletnext_int64t=mix64(next_seedt)(* [perturb] is not from any external source, but provides a way to mix in external
entropy with a pseudo-random state. *)letperturbtsalt=letnext=t.seed+mix64(Int64.of_intsalt)int.seed<-nextendletboolstate=is_odd(State.next_int64state)(* We abuse terminology and refer to individual values as biased or unbiased. More
properly, what is unbiased is the sampler that results if we keep only these "unbiased"
values. *)letremainder_is_unbiased~draw~remainder~draw_maximum~remainder_maximum=letopenInt64.Oindraw-remainder<=draw_maximum-remainder_maximumlet%test_unit"remainder_is_unbiased"=(* choosing a range of 10 values based on a range of 105 values *)letdraw_maximum=104Linletremainder_maximum=9Linletis_unbiaseddraw=letremainder=Int64.remdraw(Int64.succremainder_maximum)inremainder_is_unbiased~draw~remainder~draw_maximum~remainder_maximuminfori=0to99do[%test_result:bool](is_unbiased(Int64.of_inti))~expect:true~message:(Int.to_stringi)done;fori=100to104do[%test_result:bool](is_unbiased(Int64.of_inti))~expect:false~message:(Int.to_stringi)done(* This implementation of bounded randomness is adapted from [Random.State.int*] in the
OCaml standard library. The purpose is to use the minimum number of calls to
[next_int64] to produce a number uniformly chosen within the given range. *)letint64=letopenInt64.Oinletrecbetweenstate~lo~hi=letdraw=State.next_int64stateiniflo<=draw&&draw<=hithendrawelsebetweenstate~lo~hiinletrecnon_negative_up_tostatemaximum=letdraw=State.next_int64statelandInt64.max_valueinletremainder=Int64.remdraw(Int64.succmaximum)inifremainder_is_unbiased~draw~remainder~draw_maximum:Int64.max_value~remainder_maximum:maximumthenremainderelsenon_negative_up_tostatemaximuminfunstate~lo~hi->iflo>hithenbeginError.raise_s[%message"int64: crossed bounds"(lo:int64)(hi:int64)]end;letdiff=hi-loinifdiff=Int64.max_valuethen((State.next_int64state)landInt64.max_value)+loelseifdiff>=0Lthen(non_negative_up_tostatediff)+loelsebetweenstate~lo~hiletintstate~lo~hi=letlo=Int64.of_intloinlethi=Int64.of_inthiin(* truncate unneeded bits *)Int64.to_int_trunc(int64state~lo~hi)letint32state~lo~hi=letlo=Int64.of_int32loinlethi=Int64.of_int32hiin(* truncate unneeded bits *)Int64.to_int32_trunc(int64state~lo~hi)letnativeintstate~lo~hi=letlo=Int64.of_nativeintloinlethi=Int64.of_nativeinthiin(* truncate unneeded bits *)Int64.to_nativeint_trunc(int64state~lo~hi)letint63state~lo~hi=letlo=Int63.to_int64loinlethi=Int63.to_int64hiin(* truncate unneeded bits *)Int63.of_int64_trunc(int64state~lo~hi)letdouble_ulp=2.**.-53.let%test_unit"double_ulp"=letopenFloat.OinmatchWord_size.word_sizewith|W64->assert(1.0-.double_ulp<1.0);assert(1.0-.(double_ulp/.2.0)=1.0)|W32->(* 32-bit OCaml uses a 64-bit float representation but 80-bit float instructions, so
rounding works differently due to the conversion back and forth. *)assert(1.0-.double_ulp<1.0);assert(1.0-.(double_ulp/.2.0)<=1.0)letunit_float_from_int64int64=(Int64.to_float(int64lsr11))*.double_ulplet%test_unit"unit_float_from_int64"=beginletopenFloat.Oinassert(unit_float_from_int640x0000_0000_0000_0000L=0.);assert(unit_float_from_int640xffff_ffff_ffff_ffffL<1.0);assert(unit_float_from_int640xffff_ffff_ffff_ffffL=(1.0-.double_ulp));endletunit_floatstate=unit_float_from_int64(State.next_int64state)(* Note about roundoff error:
Although [float state ~lo ~hi] is nominally inclusive of endpoints, we are relying on
the fact that [unit_float] never returns 1., because there are pairs [(lo,hi)] for
which [lo +. 1. *. (hi -. lo) > hi]. There are also pairs [(lo,hi)] and values of [x]
with [x < 1.] such that [lo +. x *. (hi -. lo) = hi], so it would not be correct to
document this as being exclusive of [hi].
*)letfloat=letrecfinite_floatstate~lo~hi=letrange=hi-.loinifFloat.is_finiterangethen(lo+.(unit_floatstate*.range))elsebegin(* If [hi - lo] is infinite, then [hi + lo] is finite because [hi] and [lo] have
opposite signs. *)letmid=(hi+.lo)/.2.inifboolstate(* Depending on rounding, the recursion with [~hi:mid] might be inclusive of [mid],
which would mean the two cases overlap on [mid]. The alternative is to increment
or decrement [mid] using [one_ulp] in either of the calls, but then if the first
case is exclusive we leave a "gap" between the two ranges. There's no perfectly
uniform solution, so we use the simpler code that does not call [one_ulp]. *)thenfinite_floatstate~lo~hi:midelsefinite_floatstate~lo:mid~hiendinfunstate~lo~hi->ifnot(Float.is_finitelo&&Float.is_finitehi)thenbeginraise_s[%message"float: bounds are not finite numbers"(lo:float)(hi:float)]end;ifFloat.(>)lohithenbeginraise_s[%message"float: bounds are crossed"(lo:float)(hi:float)]end;finite_floatstate~lo~hilet%bench_fun"unit_float_from_int64"=letint64=1Linfun()->unit_float_from_int64int64moduleLog_uniform=structmoduleMake(M:sigincludeInt.Svaluniform:State.t->lo:t->hi:t->tend):sigvallog_uniform:State.t->lo:M.t->hi:M.t->M.tend=structopenMletbits_to_representt=assert(t>=zero);lett=reftinletn=ref0inwhile!t>zerodot:=shift_right!t1;Int.incrn;done;!nlet%test_unit"bits_to_represent"=lettestnexpect=[%test_result:int](bits_to_representn)~expectintest(M.of_int_exn0)0;test(M.of_int_exn1)1;test(M.of_int_exn2)2;test(M.of_int_exn3)2;test(M.of_int_exn4)3;test(M.of_int_exn5)3;test(M.of_int_exn6)3;test(M.of_int_exn7)3;test(M.of_int_exn8)4;test(M.of_int_exn100)7;testM.max_value(Int.predM.num_bits);;;letmin_represented_by_n_bitsn=ifInt.equaln0thenzeroelseshift_leftone(Int.predn)let%test_unit"min_represented_by_n_bits"=lettestnexpect=[%test_result:M.t](min_represented_by_n_bitsn)~expectintest0(M.of_int_exn0);test1(M.of_int_exn1);test2(M.of_int_exn2);test3(M.of_int_exn4);test4(M.of_int_exn8);test7(M.of_int_exn64);test(Int.predM.num_bits)(M.shift_right_logicalM.min_value1);;;letmax_represented_by_n_bitsn=pred(shift_leftonen)let%test_unit"max_represented_by_n_bits"=lettestnexpect=[%test_result:M.t](max_represented_by_n_bitsn)~expectintest0(M.of_int_exn0);test1(M.of_int_exn1);test2(M.of_int_exn3);test3(M.of_int_exn7);test4(M.of_int_exn15);test7(M.of_int_exn127);test(Int.predM.num_bits)M.max_value;;;letlog_uniformstate~lo~hi=letmin_bits=bits_to_representloinletmax_bits=bits_to_representhiinletbits=intstate~lo:min_bits~hi:max_bitsinuniformstate~lo:(min_represented_by_n_bitsbits|>maxlo)~hi:(max_represented_by_n_bitsbits|>minhi)endmoduleFor_int=Make(structincludeIntletuniform=intend)moduleFor_int32=Make(structincludeInt32letuniform=int32end)moduleFor_int63=Make(structincludeInt63letuniform=int63end)moduleFor_int64=Make(structincludeInt64letuniform=int64end)moduleFor_nativeint=Make(structincludeNativeintletuniform=nativeintend)letint=For_int.log_uniformletint32=For_int32.log_uniformletint63=For_int63.log_uniformletint64=For_int64.log_uniformletnativeint=For_nativeint.log_uniformend