%mathpiper,def="RandomSeed;Random;Rng;RngSeed;RngCreate"

/* def file definitions

RandomSeed
Random
Rng
RngSeed
RngCreate
*/

/*
Random number generators implemented in an object-oriented manner.

Old interface (still works):

	RandomSeed(123);
	Random(); Random();

It provides only one global RNG with a globally assigned seed.

New interface allows creating many RNG objects:

	r1:=RngCreate();	// create a default RNG object, assign structure to r1
	r2:=RngCreate(12345);	// create RNG object with given seed
	r3:=RngCreate(seed->0, engine->advanced, dist->gauss); 	// extended options: specify seed, type of RNG engine and the type of statistical distribution
	Rng(r1); Rng(r1); Rng(r2);	// generate some floating-point numbers
	RngSeed(r1, 12345);	// r1 is re-initialized with given seed, r2 is unaffected

More "RNG engines" and "RNG distribution adaptors" can be defined later (at run time).

RngCreate() will return an object of the following structure:
	{SomeDist, SomeEngine, state }

here SomeEngine is a function atom that describes the RNG engine,
SomeDist is a function atom that specifies the distribution adaptor,
and state is a "RNG state object", e.g. a list of all numbers that specify the current RNG state (seeds, temporaries, etc.).

RngSeed(r1, seed) expects an integer seed.
It will re-initialize the RNG object r1 with the given seed.

The "RNG engine API": calling RngCreate with engine->SomeEngine expects that:
	SomeEngine(seed_IsInteger) will create and initialize a state object with given seed and return the new state object (a list). SomeEngine can assume that "seed" is a positive integer.
	SomeEngine(state1_IsList) will update the RNG state object state1 and return the pair {new state object, new number}.

The "RNG distribution adaptor API": calling RngCreate with distribution->SomeDist expects that:
	SomeDist(r1) will update the RNG object r1 and return the pair {new state object, new number}. r1 is a full RNG object, not just a state object.


*/

//////////////////////////////////////////////////
/// lists of defined RNG entities
//////////////////////////////////////////////////

/// The idea is that options must be easy to type, but procedure names could be long.

LocalSymbols(knownRNGEngines, knownRNGDists) [
  knownRNGEngines :=
  {
    { "default", "RNGEngine'LCG'2"},
    { "advanced", "RNGEngine'L'Ecuyer"},
  };

  knownRNGDists :=
  {
    {"default", "FlatRNGDist"},
    {"flat", "FlatRNGDist"},
  //	{"uniform", "FlatRNGDist"},	// we probably don't need this alias...
    {"gauss", "GaussianRNGDist"},
  };

  KnownRNGDists() := knownRNGDists;
  KnownRNGEngines() := knownRNGEngines;
];


//////////////////////////////////////////////////
/// RNG object API
//////////////////////////////////////////////////

Function() RngCreate();
Function() RngCreate(seed, ...);
//HoldArgument("RngCreate", seed);	// this is needed to prevent evaluation of = and also to prevent substitution of variables, e.g. if "seed" is defined
//UnFence("RngCreate", 0);
//UnFence("RngCreate", 1);
Function() RngSeed(r, seed);
//UnFence("RngSeed", 2);
/// accessor for RNG objects
Function() Rng(r);
//UnFence("Rng", 1);


RngCreate() <-- RngCreate(0);

10 # RngCreate(a'seed_IsInteger) <-- (RngCreate @ {seed -> a'seed});

// a single option given: convert explicitly to a list
20 # RngCreate(_key -> _value) <-- `(RngCreate({@key -> value}));

// expect a list of options
30 # RngCreate(options_IsList) <--
[
	options := OptionsListToHash @ {options};

	// check options and assign defaults
	If(
		options["seed"] = Empty Or options["seed"] <= 0,
		options["seed"] := 76544321	// some default seed out of the blue sky
	);
	If(
		options["engine"] = Empty Or Not (Assert("warning", {"RngCreate: invalid engine", options["engine"]}) KnownRNGEngines()[options["engine"] ] != Empty),
		options["engine"] := "default"
	);
	If(
		options["dist"] = Empty Or Not (Assert("warning", {"RngCreate: invalid distribution", options["dist"]}) KnownRNGDists()[options["dist"] ] != Empty),
		options["dist"] := "default"
	);

	// construct a new RNG object
	// a RNG object has the form {"SomeDist", "SomeEngine", {state}}
	{
		KnownRNGDists()[options["dist"] ], KnownRNGEngines()[options["engine"] ],
		// initialize object with given seed using "SomeEngine"(seed)
		KnownRNGEngines()[options["engine"] ] @ { options["seed"] }
	};
];

/// accessor function: will call SomeDist(r) and update r
Rng(_r) <--
[
	Local(state, result);
	{state, result} := (r[1] @ {r});	// this calls SomeDist(r)
	DestructiveReplace(r, 3, state);	// update RNG object
	result;	// return floating-point number
];

/// set seed: will call SomeEngine(r, seed) and update r
RngSeed(_r, seed_IsInteger) <--
[
	Local(state);
	(Assert("warning", {"RngSeed: seed must be positive", seed}) seed > 0
	) Or (seed:=76544321);
	state := (r[2] @ {seed});	// this calls SomeEngine(r)
	DestructiveReplace(r, 3, state);	// update object
	True;
];

//////////////////////////////////////////////////
/// RNG distribution adaptors
//////////////////////////////////////////////////

/// trivial distribution adaptor: flat distribution, simply calls SomeEngine(r)
/* we have to return whole objects; we can't use references b/c the core
function ApplyFast will not work properly on references, i.e. if r = {"", "", {1}} so that
r[3] = {1}, then LCG'2(r[3]) modifies r[3], but LCG'2 @ r[3] or
ApplyFast("LCG'2", {r[3]}) do not actually modify r[3].
*/

// return pair {state, number}
FlatRNGDist(_r) <-- (r[2] @ {r[3]});	// this calls SomeEngine(state)

/// Gaussian distribution adaptor, returns a complex number with normal distribution with unit variance, i.e. Re and Im are independent and both have unit variance
/* Gaussian random number, Using the Box-Muller transform, from Knuth,
   "The Art of Computer Programming",
   Volume 2 (Seminumerical algorithms, third edition), section 3.4.1
 */
GaussianRNGDist(_rng) <--
[
	// a Gaussian distributed complex number p + I*q is made up of two uniformly distributed numbers x,y according to the formula:
	// a:=2*x-1, b:=2*y-1, m:=a^2+b^2; p = a*Sqrt(-2*Ln(m)/m); q:=b*Sqrt(-2*Ln(m)/m);
	// here we need to make sure that m is nonzero and strictly less than 1.
	Local(a,b,m, new'state, rnumber);
	new'state := rng[3];	// this will be updated at the end
	m:=0;
	While(m=0 Or m>=1)	// repeat generating new x,y  - should not take more than one iteration really
	[
		{new'state, rnumber} := (rng[2] @ {new'state});
		a:=2*rnumber-1;
		{new'state, rnumber} := (rng[2] @ {new'state});
		b:=2*rnumber-1;
		m:=a*a+b*b;
	];
	{new'state, (a+I*b)*SqrtN(-2*DivideN(Internal'LnNum(m),m))};
];


//////////////////////////////////////////////////
/// RNG engines
//////////////////////////////////////////////////

/// default RNG engine: the LCG generator

// first method: initialize a state object with given seed
RNGEngine'LCG'1(seed_IsInteger) <-- {seed};
// second method: update state object and return new number
RNGEngine'LCG'1(state_IsList) <-- LCG'1(state);

// first method: initialize a state object with given seed
RNGEngine'LCG'2(seed_IsInteger) <-- {seed};
// second method: update state object and return new number
RNGEngine'LCG'2(state_IsList) <-- LCG'2(state);

// first method: initialize a state object with given seed
RNGEngine'LCG'3(seed_IsInteger) <-- {seed};
// second method: update state object and return new number
RNGEngine'LCG'3(state_IsList) <-- LCG'3(state);

// first method: initialize a state object with given seed
RNGEngine'LCG'4(seed_IsInteger) <-- {seed};
// second method: update state object and return new number
RNGEngine'LCG'4(state_IsList) <-- LCG'4(state);

/// parameters from P. Hellekalek, 1994; see G. S. Fishman, Math. Comp. vol. 54, 331 (1990)
LCG'1(state) := RandomLCG(state, 2147483647,950706376,0);
LCG'2(state) := RandomLCG(state, 4294967296,1099087573,0);
LCG'3(state) := RandomLCG(state, 281474976710656,68909602460261,0);
LCG'4(state) := RandomLCG(state, 18014398509481984,2783377640906189,0);

/// Linear congruential generator engine: backend
// state is a list with one element
RandomLCG(_state, _im, _ia, _ic) <--
{
	DestructiveReplace(state,1, ModuloN(state[1]*ia+ic,im)),
	DivideN(state[1], im)	// division should never give 1
};

/// Advanced RNG engine due to L'Ecuyer et al.
/// RNG from P. L'ecuyer et al (2000). Period approximately 2^191
// state information: 6 32-bit integers, corresponding to {x3,x2,x1,y3,y2,y1}

// first method: initialize a state object with given seed
RNGEngine'L'Ecuyer(a'seed_IsInteger) <--
[
	// use LCG'2 as auxiliary RNG to fill the seeds
	Local(rng'aux, result);
	rng'aux := (RngCreate @ {a'seed});
	// this will be the state vector
	result:=ZeroVector(6);
	// fill the state object with random numbers
	Local(i);
	For(i:=1, i<=6, i++)
	[
		Rng(rng'aux);
		result[i] := rng'aux[3][1];	// hack to get the integer part
	];
	// return the state object
	result;
];

// second method: update state object and return a new random number (floating-point)
RNGEngine'L'Ecuyer(state_IsList) <--
[
	Local(new'state, result);
	new'state := {
		Modulo(1403580*state[2]-810728*state[3], 4294967087), state[1], state[2],
		Modulo(527612*state[4]-1370589*state[6], 4294944433), state[4], state[5]
	};
	result:=Modulo(state[1]-state[4], 4294967087);
	{
		new'state,
		DivideN(If(result=0, 4294967087, result), 4294967088)
	};
];

//////////////////////////////////////////////////
/// old interface: using one global RNG object
//////////////////////////////////////////////////
/* this is a little slower but entirely equivalent to the code below
GlobalRNG := RngCreate(76544321);
Random() := Rng(GlobalRNG);
RandomSeed(seed) := RngSeed(GlobalRNG, seed);
*/

LocalSymbols(RandSeed) [
  // initial seed should be nonzero
  RandSeed := SystemTimer(); //Was 76544321.

  /// assign random seed
  Function("RandomSeed", {seed}) Bind(RandSeed, seed);

  /// Linear congruential generator
  RandomLCG(_im, _ia, _ic) <--
  [
    RandSeed:=ModuloN(RandSeed*ia+ic,im);
    DivideN(RandSeed,im);	// should never give 1
  ];
]; // LocalSymbols(RandSeed)


Function("Random1",{}) RandomLCG(4294967296,1103515245,12345);
Function("Random6",{}) RandomLCG(1771875,2416,374441);
/// parameters from P. Hellekalek, 1994; see G. S. Fishman, Math. Comp. vol. 54, 331 (1990)
Function("Random2",{}) RandomLCG(2147483647,950706376,0);
Function("Random3",{}) RandomLCG(4294967296,1099087573,0);
Function("Random4",{}) RandomLCG(281474976710656,68909602460261,0);
Function("Random5",{}) RandomLCG(18014398509481984,2783377640906189,0);

// select one of them
Function("Random",{}) Random3();


%/mathpiper





%mathpiper_docs,name="Random;RandomSeed",categories="User Functions;Numbers (Random)"
*CMD Random, RandomSeed --- (pseudo-) random number generator
*STD
*CALL
	Random()
	RandomSeed(init)

*PARAMS
{init} -- positive integer, initial random seed

*DESC

The function {Random} returns a random number, uniformly distributed in the
interval between 0 and 1. The same sequence of random numbers is
generated in every MathPiper session.

The random number generator can be initialized by calling {RandomSeed} with an integer value.
Each seed value will result in the same sequence of pseudo-random numbers.

*SEE RandomInteger, RandomPoly, Rng, Random, RandomIntegerList, RandomIntegerMatrix
%/mathpiper_docs





%mathpiper_docs,name="RngCreate;RngSeed;Rng",categories="User Functions;Numbers (Random)"
*CMD RngCreate --- manipulate random number generators as objects
*CMD RngSeed --- manipulate random number generators as objects
*CMD Rng --- manipulate random number generators as objects
*STD
*CALL
	RngCreate()
	RngCreate(init)
	RngCreate(option->value,...)
	RngSeed(r, init)
	Rng(r)
	
*PARMS
{init} -- integer, initial seed value

{option} -- atom, option name

{value} -- atom, option value

{r} -- a list, RNG object

*DESC
These commands are an object-oriented interface to (pseudo-)random number generators (RNGs).

{RngCreate} returns a list which is a well-formed RNG object.
Its value should be saved in a variable and used to call {Rng} and {RngSeed}.

{Rng(r)} returns a floating-point random number between 0 and 1 and updates the RNG object {r}.
(Currently, the Gaussian option makes a RNG return a <i>complex</i> random number instead of a real random number.)

{RngSeed(r,init)} re-initializes the RNG object {r} with the seed value {init}.
The seed value should be a positive integer.

The {RngCreate} function accepts several options as arguments.
Currently the following options are available:

*	{seed} -- specify initial seed value, must be a positive integer
*	{dist} -- specify the distribution of the random number; currently {flat} and {gauss} are implemented, and the default is the flat (uniform) distribution
*	{engine} -- specify the RNG engine; currently {default} and {advanced} are available ("advanced" is slower but has much longer period)

If the initial seed is not specified, the value of 76544321 will be used.

The {gauss} option will create a RNG object that generates pairs of Gaussian distributed random numbers as a complex random number.
The real and the imaginary parts of this number are independent random numbers taken from a Gaussian (i.e. "normal") distribution with unit variance.

For the Gaussian distribution, the Box-Muller transform method is used.
A good description of this method, along with the proof that the method
generates normally distributed random numbers, can be found in Knuth, 
"The Art of Computer Programming", Volume 2 (Seminumerical algorithms, third 
edition), section 3.4.1 

Note that unlike the global {Random} function, the RNG objects created with {RngCreate} are independent RNGs and do not affect each other.
They generate independent streams of pseudo-random numbers.
However, the {Random} function is slightly faster.

*E.G.

In> r1:=RngCreate(seed->1,dist->gauss)
Result: {"GaussianRNGDist","RNGEngine'LCG'2",{1}}
In> Rng(r1)
Result: Complex(-1.6668466417,0.228904004);
In> Rng(r1);
Result: Complex(0.0279296109,-0.5382405341);
The second RNG gives a uniform distribution (default option) but uses a more complicated algorithm:
In> [r2:=RngCreate(engine->advanced);Rng(r2);]
Result: 0.3653615377;
The generator {r1} can be re-initialized with seed 1 again to obtain the same sequence:
In> RngSeed(r1, 1)
Result: True;
In> Rng(r1)
Result: Complex(-1.6668466417,0.228904004);
	

*SEE Random, RandomInteger, RandomIntegerList, RandomIntegerMatrix
%/mathpiper_docs



