# random.tcl --
#     Create procedures that return various types of pseudo-random
#     number generators (PRNGs)
#
# Copyright (c) 2007 by Arjen Markus <arjenmarkus@users.sourceforge.net>
#
# See the file "license.terms" for information on usage and redistribution
# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
#
# TODO:
# - Beta
# - Weighted discrete
# - Poisson
# - Cauchy
# - Binomial
#
#     Note:
#     Several formulae and algorithms come from "Monte Carlo Simulation"
#     by C. Mooney (Sage Publications, 1997)
#
# RCS: @(#) $Id: random.tcl,v 1.5 2012/08/15 04:38:48 arjenmarkus Exp $
#------------------------------------------------------------------------------

package require Tcl 8.4

# ::simulation::random --
#     Create the namespace
#
namespace eval ::simulation::random {
    variable count 0
    variable pi    [expr {4.0*atan(1.0)}]
}


# prng_Bernoulli --
#     Create a PRNG with a Bernoulli distribution
#
# Arguments:
#     p         Probability that the outcome will be 1
#
# Result:
#     Name of a procedure that returns a Bernoulli-distributed random number
#
proc ::simulation::random::prng_Bernoulli {p} {
    variable count

    incr count

    set name ::simulation::random::PRNG_$count
    proc $name {} [string map [list P $p] {return [expr {rand()<P? 1 : 0}]}]

    return $name
}


# prng_Uniform --
#     Create a PRNG with a uniform distribution in a given range
#
# Arguments:
#     min       Minimum value
#     max       Maximum value
#
# Result:
#     Name of a procedure that returns a uniformly distributed
#     random number
#
proc ::simulation::random::prng_Uniform {min max} {
    variable count

    incr count

    set name ::simulation::random::PRNG_$count
    set range [expr {$max-$min}]
    proc $name {} [string map [list MIN $min RANGE $range] {return [expr {MIN+RANGE*rand()}]}]

    return $name
}


# prng_Exponential --
#     Create a PRNG with an exponential distribution with given mean
#
# Arguments:
#     min       Minimum value
#     mean      Mean value
#
# Result:
#     Name of a procedure that returns an exponentially distributed
#     random number
#
proc ::simulation::random::prng_Exponential {min mean} {
    variable count

    incr count

    set name ::simulation::random::PRNG_$count
    set b [expr {$mean-$min}]
    proc $name {} [string map [list MIN $min B $b] {return [expr {MIN-B*log(rand())}]}]

    return $name
}


# prng_Discrete --
#     Create a PRNG with a uniform but discrete distribution
#
# Arguments:
#     n         Outcome is an integer between 0 and n-1
#
# Result:
#     Name of a procedure that returns such a random number
#
proc ::simulation::random::prng_Discrete {n} {
    variable count

    incr count

    set name ::simulation::random::PRNG_$count
    proc $name {} [string map [list N $n] {return [expr {int(N*rand())}]}]

    return $name
}


# prng_Poisson --
#     Create a PRNG with a Poisson distribution
#
# Arguments:
#     lambda    The one parameter of the Poisson distribution
#
# Result:
#     Name of a procedure that returns such a random number
#
proc ::simulation::random::prng_Poisson {lambda} {
    variable count

    incr count

    set name ::simulation::random::PRNG_$count

    set explambda [expr {exp(-$lambda)}]

    proc $name {} [string map [list INIT $explambda LAMBDA $lambda] {
        set r      [expr {rand()}]
        set number 0
        set sum    INIT
        set rfact  INIT

        while { $r > $sum } {
            set rfact [expr {$rfact * LAMBDA /($number+1.0)}]
            set sum [expr {$sum + $rfact}]
            incr number
        }
        return $number
    }]

    return $name
}


# prng_Normal --
#     Create a PRNG with a normal distribution
#
# Arguments:
#     mean      Mean of the distribution
#     stdev     Standard deviation of the distribution
#
# Result:
#     Name of a procedure that returns such a random number
#
# Note:
#     Use the Box-Mueller method to generate a normal random number
#
proc ::simulation::random::prng_Normal {mean stdev} {
    variable count

    incr count

    set name ::simulation::random::PRNG_$count
    proc $name {} [string map [list MEAN $mean STDEV $stdev] \
    {
        variable pi
        set rad [expr {sqrt(-2.0*log(rand()))}]
        set phi [expr {2.0*$pi*rand()}]
        set r   [expr {$rad*cos($phi)}]
        return [expr {MEAN + STDEV*$r}]
    }]

    return $name
}


# prng_Pareto --
#     Create a PRNG with a Pareto distribution
#
# Arguments:
#     min       Minimum value for the distribution
#     steep     Steepness of the descent
#
# Result:
#     Name of a procedure that returns a Pareto-distributed number
#
proc ::simulation::random::prng_Pareto {min steep} {
    variable count

    incr count

    set name ::simulation::random::PRNG_$count

    set rsteep [expr {1.0/$steep}]
    proc $name {} [string map [list MIN $min RSTEEP $rsteep] \
    {
        return [expr {MIN * pow(1.0-rand(),RSTEEP)}]
    }]

    return $name
}


# prng_Gumbel --
#     Create a PRNG with a Gumbel distribution
#
# Arguments:
#     min       Minimum value for the distribution
#     f         Factor to scale the value
#
# Result:
#     Name of a procedure that returns a Gumbel-distributed number
#
# Note:
#     The chance P(v) = exp( -exp( f*(v-min) ) )
#
proc ::simulation::random::prng_Gumbel {min f} {
    variable count

    incr count

    set name ::simulation::random::PRNG_$count

    proc $name {} [string map [list MIN $min F $f] \
    {
        return [expr {MIN + log( -log(1.0-rand()) ) / F}]
    }]

    return $name
}


# prng_chiSquared --
#     Create a PRNG with a chi-squared distribution
#
# Arguments:
#     df        Degrees of freedom
#
# Result:
#     Name of a procedure that returns a chi-squared distributed number
#     with mean 0 and standard deviation 1
#
proc ::simulation::random::prng_chiSquared {df} {
    variable count

    incr count

    set name ::simulation::random::PRNG_$count
    proc $name {} [string map [list DF $df] \
    {
        variable pi
        set y 0.0
        for { set i 0 } { $i < DF } { incr i } {
            set rad [expr {sqrt(-log(rand()))}]
            set phi [expr {2.0*$pi*rand()}]
            set r   [expr {$rad*cos($phi)}]
            set y   [expr {$y+$r*$r}]
        }
        return [expr {($y-DF)/sqrt(2.0*DF)}]
    }]

    return $name
}


# prng_Disk --
#     Create a PRNG with a uniform distribution of points on a disk
#
# Arguments:
#     rad       Radius of the disk
#
# Result:
#     Name of a procedure that returns the x- and y-coordinates of
#     such a random point
#
proc ::simulation::random::prng_Disk {rad} {
    variable count

    incr count

    set name ::simulation::random::PRNG_$count
    proc $name {} [string map [list RAD $rad] \
    {
        variable pi
        set rad [expr {RAD*sqrt(rand())}]
        set phi [expr {2.0*$pi*rand()}]
        set x   [expr {$rad*cos($phi)}]
        set y   [expr {$rad*sin($phi)}]
        return [list $x $y]
    }]

    return $name
}


# prng_Ball --
#     Create a PRNG with a uniform distribution of points within a ball
#
# Arguments:
#     rad       Radius of the ball
#
# Result:
#     Name of a procedure that returns the x-, y- and z-coordinates of
#     such a random point
#
proc ::simulation::random::prng_Ball {rad} {
    variable count

    incr count

    set name ::simulation::random::PRNG_$count
    proc $name {} [string map [list RAD $rad] \
    {
        variable pi
        set rad   [expr {RAD*pow(rand(),0.333333333333)}]
        set phi   [expr {2.0*$pi*rand()}]
        set theta [expr {acos(2.0*rand()-1.0)}]
        set x     [expr {$rad*cos($phi)*cos($theta)}]
        set y     [expr {$rad*sin($phi)*cos($theta)}]
        set z     [expr {$rad*sin($theta)}]
        return [list $x $y $z]
    }]

    return $name
}


# prng_Sphere --
#     Create a PRNG with a uniform distribution of points on the surface
#     of a sphere
#
# Arguments:
#     rad       Radius of the sphere
#
# Result:
#     Name of a procedure that returns the x-, y- and z-coordinates of
#     such a random point
#
proc ::simulation::random::prng_Sphere {rad} {
    variable count

    incr count

    set name ::simulation::random::PRNG_$count
    proc $name {} [string map [list RAD $rad] \
    {
        variable pi
        set phi   [expr {2.0*$pi*rand()}]
        set theta [expr {acos(2.0*rand()-1.0)}]
        set x     [expr {RAD*cos($phi)*cos($theta)}]
        set y     [expr {RAD*sin($phi)*cos($theta)}]
        set z     [expr {RAD*sin($theta)}]
        return [list $x $y $z]
    }]

    return $name
}


# prng_Rectangle --
#     Create a PRNG with a uniform distribution of points in a rectangle
#
# Arguments:
#     length    Length of the rectangle (x-direction)
#     width     Width of the rectangle (y-direction)
#
# Result:
#     Name of a procedure that returns the x- and y-coordinates of
#     such a random point
#
proc ::simulation::random::prng_Rectangle {length width} {
    variable count

    incr count

    set name ::simulation::random::PRNG_$count

    proc $name {} [string map [list LENGTH $length WIDTH $width] \
     {
         set x [expr {LENGTH*rand()}]
         set y [expr {WIDTH*rand()}]
         return [list $x $y]
    }]

    return $name
}


# prng_Block --
#     Create a PRNG with a uniform distribution of points in a block
#
# Arguments:
#     length    Length of the block (x-direction)
#     width     Width of the block (y-direction)
#     depth     Depth of the block (y-direction)
#
# Result:
#     Name of a procedure that returns the x-, y- and z-coordinates of
#     such a random point
#
proc ::simulation::random::prng_Block {length width depth} {
    variable count

    incr count

    set name ::simulation::random::PRNG_$count

    proc $name {} [string map [list LENGTH $length WIDTH $width DEPTH $depth] \
     {
         set x [expr {LENGTH*rand()}]
         set y [expr {WIDTH*rand()}]
         set z [expr {DEPTH*rand()}]
         return [list $x $y $z]
    }]

    return $name
}


# prng_Triangle --
#     Create a PRNG with a triangular distribution of points on an interval.
#     If the argument min is lower than the argument max, then smaller
#     values have higher probability and vice versa.
#
# Arguments:
#     min       Minimum value
#     max       Maximum value
#
# Result:
#     Name of a procedure that returns the random point
#
proc ::simulation::random::prng_Triangle {min max} {
    variable count

    incr count

    set name ::simulation::random::PRNG_$count

    set diff [expr {$max-$min}]

    if { $diff > 0.0 } {
        proc $name {} [string map [list MIN $min DIFF $diff] \
         {
            set r [expr {1.0 - sqrt(1.0 - rand())}]
            set x [expr {MIN + DIFF*$r}]
            return $x
        }]
    } else {
        proc $name {} [string map [list MAX $max DIFF $diff] \
         {
            set x [expr {MAX - DIFF*sqrt(rand())}]
            return $x
        }]
    }

    return $name
}


# prng_SymmetricTriangle --
#     Create a PRNG with a symmetric triangular distribution of points on an interval.
#
# Arguments:
#     min       Minimum value
#     max       Maximum value
#
# Result:
#     Name of a procedure that returns the random point
#
proc ::simulation::random::prng_SymmetricTriangle {min max} {
    variable count

    incr count

    set name ::simulation::random::PRNG_$count

    set diff2 [expr {0.5 *($max-$min)}]

    proc $name {} [string map [list MIN $min DIFF2 $diff2] \
     {
        return [expr {MIN + DIFF2 * (rand() + rand())}]
    }]

    return $name
}

# Announce the package
#
package provide simulation::random 0.4.0


# main --
#     Test code
#
if { 0 } {
set bin [::simulation::random::prng_Bernoulli 0.2]

set ones  0
set zeros 0
for { set i 0} {$i < 100000} {incr i} {
    if { [$bin] } {
        incr ones
    } else {
        incr zeros
    }
}

puts "Bernoulli: $ones - $zeros"

set discrete [::simulation::random::prng_Discrete 10]

for { set i 0} {$i < 100000} {incr i} {
    set v [$discrete]

    if { [info exists count($v)] } {
        incr count($v)
    } else {
        set count($v) 1
    }
}

puts "Discrete:"
parray count

set rect [::simulation::random::prng_Rectangle 10 3]

puts "Rectangle:"
for { set i 0} {$i < 10} {incr i} {
    puts [$rect]
}

set normal [::simulation::random::prng_Normal 0 1]

puts "Normal:"
for { set i 0} {$i < 10} {incr i} {
    puts [$normal]
}

#
# Timing: how fast is the normal random number generator?
#
# Surprising speed: 15 million numbers per minute!
#
puts "Normal random number generator:"
puts "[time {set value [$normal]} 30000]"
set result [lindex [time {set value [$normal]} 30000] 0]
puts "[expr {60.0e6/$result}] numbers per minute"
puts "Creating a long list: [time {lappend value [$normal]} 30000]"
puts "[lrange $value 0 20] - [llength $value] numbers in total"
set value {}
set result [lindex [time {lappend value [$normal]} 30000] 0]
puts "[expr {60.0e6/$result}] numbers per minute"

puts "Points in a rectangle:"
puts "[time {set value [$rect]} 30000]"
set result [lindex [time {set value [$rect]} 30000] 0]
puts "[expr {60.0e6/$result}] numbers per minute"

#
# A more formal test
#
package require math
unset count
set samples 100000

set lambda 10.0
set poisson [::simulation::random::prng_Poisson $lambda]
for { set i 0 } { $i < $samples } { incr i } {
    set number [$poisson]
    if { [info exists count($number)] } {
        incr count($number)
    } else {
        set count($number) 1
    }
}
parray count
for { set i 0 } { $i < 30 } { incr i } {
    set expected [expr {int($samples * pow($lambda,$i) * exp(-$lambda) / [::math::factorial $i])}]
    set exp_error [expr {sqrt($expected)}]
    if { [info exists count($i)] } {
        if { $expected-$exp_error < $count($i) &&
             $expected+$exp_error > $count($i) } {
            set okay "okay"
        } else {
            set okay "difference too large"
        }
        puts "$i $expected $count($i) - [expr {$expected/double($count($i))}] - $okay"
    } else {
        puts "$i $expected none"
    }
}
}

#
# Test hypothesis concerning rectangle
#
if { 0 } {
set r2 [::simulation::random::prng_Rectangle2 10 1]

set count_down  0
set count_up    0
set count_left  0
set count_right 0
for { set i 0 } { $i < 1000000 } { incr i } {

    foreach {x y} [$r2] {
        if { $x < 2.0 } { incr count_left  }
        if { $x > 8.0 } { incr count_right }
        if { $y < 0.2 } { incr count_down  }
        if { $y > 0.8 } { incr count_up    }
    }
}
puts "Left-right:\t$count_left\t$count_right"
puts "Up-down:   \t$count_up\t$count_down"
}

#
# Check normal distribution
#
if { 0 } {
    package require math::statistics
    set normal [::simulation::random::prng_Normal 0 1]

    for { set i 0} {$i < 1000} {incr i} {
        lappend numbers [$normal]
    }
    puts "Mean:  [::math::statistics::mean  $numbers]"
    puts "Stdev: [::math::statistics::stdev $numbers]"
}
