simple-random 0.10.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -12,6 +12,7 @@ Generate random numbers sampled from the following distributions:
12
12
  * Laplace (double exponential)
13
13
  * Normal
14
14
  * Student t
15
+ * Triangular
15
16
  * Uniform
16
17
  * Weibull
17
18
 
@@ -33,6 +34,14 @@ Distributed under the Code Project Open License, which is similar to MIT or BSD.
33
34
 
34
35
  == History
35
36
 
37
+ === 1.0.0 - 2014-07-08
38
+ * Migrate to new version of Jeweler for gem packaging
39
+ * Merge jwroblewski's changes into a new multi-threaded simple random class
40
+ * Change from Code Project Open License to CDDL-1.0[http://opensource.org/licenses/CDDL-1.0]
41
+
42
+ === 0.10.0 - 2014-03-31
43
+ * Sample from triangular distribution (thanks to benedictleejh[https://github.com/benedictleejh])
44
+
36
45
  === 0.9.3 - 2011-09-16
37
46
  * Sample from Dirichlet distribution with given set of parameters
38
47
 
data/Rakefile CHANGED
@@ -1,22 +1,29 @@
1
+ # encoding: utf-8
2
+
1
3
  require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
2
12
  require 'rake'
3
13
 
4
- begin
5
- require 'jeweler'
6
- Jeweler::Tasks.new do |gem|
7
- gem.name = "simple-random"
8
- gem.summary = %Q{Simple Random Number Generator}
9
- gem.description = %Q{Simple Random Number Generator including Beta, Cauchy, Chi square, Exponential, Gamma, Inverse Gamma, Laplace (double exponential), Normal, Student t, Uniform, and Weibull. Ported from John D. Cook's C# Code.}
10
- gem.email = "jasonmadams@gmail.com"
11
- gem.homepage = "http://github.com/ealdent/simple-random"
12
- gem.authors = ["John D. Cook", "Jason Adams"]
13
- gem.add_development_dependency "shoulda", ">= 0"
14
- # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
- end
16
- Jeweler::GemcutterTasks.new
17
- rescue LoadError
18
- puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
17
+ gem.name = "simple-random"
18
+ gem.homepage = "http://github.com/ealdent/simple-random"
19
+ gem.licenses = "CDDL-1.0"
20
+ gem.summary = %Q{Simple Random Number Generator}
21
+ gem.description = %Q{Simple Random Number Generator including Beta, Cauchy, Chi square, Exponential, Gamma, Inverse Gamma, Laplace (double exponential), Normal, Student t, Uniform, and Weibull. Ported from John D. Cook's C# Code.}
22
+ gem.email = "jasonmadams@gmail.com"
23
+ gem.authors = ["John D. Cook", "Jason Adams"]
24
+ # dependencies defined in Gemfile
19
25
  end
26
+ Jeweler::RubygemsDotOrgTasks.new
20
27
 
21
28
  require 'rake/testtask'
22
29
  Rake::TestTask.new(:test) do |test|
@@ -25,6 +32,20 @@ Rake::TestTask.new(:test) do |test|
25
32
  test.verbose = true
26
33
  end
27
34
 
28
- task :test => :check_dependencies
35
+ desc "Code coverage detail"
36
+ task :simplecov do
37
+ ENV['COVERAGE'] = "true"
38
+ Rake::Task['test'].execute
39
+ end
29
40
 
30
41
  task :default => :test
42
+
43
+ require 'rdoc/task'
44
+ Rake::RDocTask.new do |rdoc|
45
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
46
+
47
+ rdoc.rdoc_dir = 'rdoc'
48
+ rdoc.title = "simple-random #{version}"
49
+ rdoc.rdoc_files.include('README*')
50
+ rdoc.rdoc_files.include('lib/**/*.rb')
51
+ end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.10.0
1
+ 1.0.0
data/lib/simple-random.rb CHANGED
@@ -1,207 +1,2 @@
1
- class SimpleRandom
2
- def initialize
3
- @m_w = 521288629
4
- @m_z = 362436069
5
- end
6
-
7
- def set_seed(*args)
8
- if args.size > 1
9
- @m_w = args.first.to_i if args.first.to_i != 0
10
- @m_z = args.last.to_i if args.last.to_i != 0
11
- elsif args.first.is_a?(Numeric)
12
- @m_w = args.first.to_i if args.first.to_i != 0
13
- elsif args.first.is_a?(Time)
14
- x = (args.first.to_f * 1000000).to_i
15
- @m_w = x >> 16
16
- @m_z = x % 4294967296 # 2 ** 32
17
- else
18
- x = (Time.now.to_f * 1000000).to_i
19
- @m_w = x >> 16
20
- @m_z = x % 4294967296 # 2 ** 32
21
- end
22
-
23
- @m_w %= 4294967296
24
- @m_z %= 4294967296
25
- end
26
-
27
- # Produce a uniform random sample from the open interval (lower, upper).
28
- # The method will not return either end point.
29
- def uniform(lower = 0, upper = 1)
30
- raise 'Invalid range' if upper <= lower
31
- ((get_unsigned_int + 1) * (upper - lower) / 4294967296.0) + lower
32
- end
33
-
34
- # Sample normal distribution with given mean and standard deviation
35
- def normal(mean = 0.0, standard_deviation = 1.0)
36
- raise 'Invalid standard deviation' if standard_deviation <= 0
37
- mean + standard_deviation * ((-2.0 * Math.log(uniform)) ** 0.5) * Math.sin(2.0 * Math::PI * uniform)
38
- end
39
-
40
- # Get exponential random sample with specified mean
41
- def exponential(mean = 1)
42
- raise 'Mean must be positive' if mean <= 0
43
- -1.0 * mean * Math.log(uniform)
44
- end
45
-
46
- # Get triangular random sample with specified lower limit, mode, upper limit
47
- def triangular(lower, mode, upper)
48
- raise 'Upper limit must be larger than lower limit' if upper < lower
49
- raise 'Mode must lie between the upper and lower limits' if (mode < lower || mode > upper)
50
- f_c = (mode - lower) / (upper - lower)
51
- uniform_rand_num = uniform
52
- if uniform_rand_num < f_c
53
- lower + Math.sqrt(uniform_rand_num * (upper - lower) * (mode - lower))
54
- else
55
- upper - Math.sqrt((1 - uniform_rand_num) * (upper - lower) * (upper - mode))
56
- end
57
- end
58
-
59
- # Implementation based on "A Simple Method for Generating Gamma Variables"
60
- # by George Marsaglia and Wai Wan Tsang. ACM Transactions on Mathematical Software
61
- # Vol 26, No 3, September 2000, pages 363-372.
62
- def gamma(shape, scale)
63
- if shape >= 1.0
64
- d = shape - 1.0 / 3.0
65
- c = 1 / ((9 * d) ** 0.5)
66
- while true
67
- v = 0.0
68
- while v <= 0.0
69
- x = normal
70
- v = 1.0 + c * x
71
- end
72
- v = v ** 3
73
- u = uniform
74
- if u < (1.0 - 0.0331 * (x ** 4)) || Math.log(u) < (0.5 * (x ** 2) + d * (1.0 - v + Math.log(v)))
75
- return scale * d * v
76
- end
77
- end
78
- elsif shape <= 0.0
79
- raise 'Shape must be positive'
80
- else
81
- g = gamma(shape + 1.0, 1.0)
82
- w = uniform
83
- return scale * g * (w ** (1.0 / shape))
84
- end
85
- end
86
-
87
- def chi_square(degrees_of_freedom)
88
- gamma(0.5 * degrees_of_freedom, 2.0)
89
- end
90
-
91
- def inverse_gamma(shape, scale)
92
- 1.0 / gamma(shape, 1.0 / scale)
93
- end
94
-
95
- def beta(a, b)
96
- raise "Alpha and beta parameters must be positive. Received a = #{a} and b = #{b}." unless a > 0 && b > 0
97
- u = gamma(a, 1)
98
- v = gamma(b, 1)
99
- u / (u + v)
100
- end
101
-
102
- def weibull(shape, scale)
103
- raise 'Shape and scale must be positive' if shape <= 0.0 || scale <= 0.0
104
-
105
- scale * ((-Math.log(uniform)) ** (1.0 / shape))
106
- end
107
-
108
- def cauchy(median, scale)
109
- raise 'Scale must be positive' if scale <= 0
110
-
111
- median + scale * Math.tan(Math::PI * (uniform - 0.5))
112
- end
113
-
114
- def student_t(degrees_of_freedom)
115
- raise 'Degrees of freedom must be positive' if degrees_of_freedom <= 0
116
-
117
- normal / ((chi_square(degrees_of_freedom) / degrees_of_freedom) ** 0.5)
118
- end
119
-
120
- def laplace(mean, scale)
121
- u = uniform
122
- mean + Math.log(2) + ((u < 0.5 ? 1 : -1) * scale * Math.log(u < 0.5 ? u : 1 - u))
123
- end
124
-
125
- def log_normal(mu, sigma)
126
- Math.exp(normal(mu, sigma))
127
- end
128
-
129
- def dirichlet(*parameters)
130
- sample = parameters.map { |a| gamma(a, 1) }
131
- sum = sample.inject(0.0) { |sum, g| sum + g }
132
- sample.map { |g| g / sum }
133
- end
134
-
135
- private
136
-
137
- # This is the heart of the generator.
138
- # It uses George Marsaglia's MWC algorithm to produce an unsigned integer.
139
- # See http://www.bobwheeler.com/statistics/Password/MarsagliaPost.txt
140
- def get_unsigned_int
141
- @m_z = 36969 * (@m_z & 65535) + (@m_z >> 16);
142
- @m_w = 18000 * (@m_w & 65535) + (@m_w >> 16);
143
- ((@m_z << 16) + (@m_w & 65535)) % 4294967296
144
- end
145
-
146
- def gamma_function(x)
147
- g = [
148
- 1.0,
149
- 0.5772156649015329,
150
- -0.6558780715202538,
151
- -0.420026350340952e-1,
152
- 0.1665386113822915,
153
- -0.421977345555443e-1,
154
- -0.9621971527877e-2,
155
- 0.7218943246663e-2,
156
- -0.11651675918591e-2,
157
- -0.2152416741149e-3,
158
- 0.1280502823882e-3,
159
- -0.201348547807e-4,
160
- -0.12504934821e-5,
161
- 0.1133027232e-5,
162
- -0.2056338417e-6,
163
- 0.6116095e-8,
164
- 0.50020075e-8,
165
- -0.11812746e-8,
166
- 0.1043427e-9,
167
- 0.77823e-11,
168
- -0.36968e-11,
169
- 0.51e-12,
170
- -0.206e-13,
171
- -0.54e-14,
172
- 0.14e-14
173
- ]
174
-
175
- r = 1.0
176
-
177
- return 1e308 if x > 171.0
178
- if x.is_a?(Fixnum) || x == x.to_i
179
- if x > 0
180
- ga = (2...x).inject(1.0) { |prod, i| prod * i }
181
- else
182
- 1e308
183
- end
184
- else
185
- if x.abs > 1.0
186
- r = (1..(x.abs.to_i)).inject(1.0) { |prod, i| prod * (x.abs - i) }
187
- z = x.abs - x.abs.to_i
188
- else
189
- z = x
190
- end
191
-
192
- gr = g[24]
193
- 23.downto(0).each do |i|
194
- gr = gr * z + g[i]
195
- end
196
- ga = 1.0 / (gr * z)
197
- if x.abs > 1
198
- ga *= r
199
- if x < 0
200
- ga = -Math::PI / (x * ga * Math.sin(Math::PI * x))
201
- end
202
- end
203
- end
204
-
205
- ga
206
- end
207
- end
1
+ require 'simple-random/simple_random'
2
+ require 'simple-random/multi_threaded_simple_random'
@@ -0,0 +1,30 @@
1
+ require 'monitor'
2
+
3
+ class MultiThreadedSimpleRandom < SimpleRandom
4
+ class << self
5
+ @instances = nil
6
+
7
+ def instance
8
+
9
+ unless @instances
10
+ extend MonitorMixin
11
+
12
+ self.synchronize do
13
+ @instances ||= {}
14
+ end
15
+ end
16
+
17
+ instance_id = Thread.current.object_id
18
+
19
+ unless @instances[instance_id]
20
+ self.synchronize do
21
+ @instances[instance_id] ||= new
22
+ end
23
+ end
24
+
25
+ @instances[instance_id]
26
+ end
27
+ end
28
+
29
+ private_class_method :new
30
+ end
@@ -0,0 +1,207 @@
1
+ class SimpleRandom
2
+ def initialize
3
+ @m_w = 521288629
4
+ @m_z = 362436069
5
+ end
6
+
7
+ def set_seed(*args)
8
+ if args.size > 1
9
+ @m_w = args.first.to_i if args.first.to_i != 0
10
+ @m_z = args.last.to_i if args.last.to_i != 0
11
+ elsif args.first.is_a?(Numeric)
12
+ @m_w = args.first.to_i if args.first.to_i != 0
13
+ elsif args.first.is_a?(Time)
14
+ x = (args.first.to_f * 1000000).to_i
15
+ @m_w = x >> 16
16
+ @m_z = x % 4294967296 # 2 ** 32
17
+ else
18
+ x = (Time.now.to_f * 1000000).to_i
19
+ @m_w = x >> 16
20
+ @m_z = x % 4294967296 # 2 ** 32
21
+ end
22
+
23
+ @m_w %= 4294967296
24
+ @m_z %= 4294967296
25
+ end
26
+
27
+ # Produce a uniform random sample from the open interval (lower, upper).
28
+ # The method will not return either end point.
29
+ def uniform(lower = 0, upper = 1)
30
+ raise 'Invalid range' if upper <= lower
31
+ ((get_unsigned_int + 1) * (upper - lower) / 4294967296.0) + lower
32
+ end
33
+
34
+ # Sample normal distribution with given mean and standard deviation
35
+ def normal(mean = 0.0, standard_deviation = 1.0)
36
+ raise 'Invalid standard deviation' if standard_deviation <= 0
37
+ mean + standard_deviation * ((-2.0 * Math.log(uniform)) ** 0.5) * Math.sin(2.0 * Math::PI * uniform)
38
+ end
39
+
40
+ # Get exponential random sample with specified mean
41
+ def exponential(mean = 1)
42
+ raise 'Mean must be positive' if mean <= 0
43
+ -1.0 * mean * Math.log(uniform)
44
+ end
45
+
46
+ # Get triangular random sample with specified lower limit, mode, upper limit
47
+ def triangular(lower, mode, upper)
48
+ raise 'Upper limit must be larger than lower limit' if upper < lower
49
+ raise 'Mode must lie between the upper and lower limits' if (mode < lower || mode > upper)
50
+ f_c = (mode - lower) / (upper - lower)
51
+ uniform_rand_num = uniform
52
+ if uniform_rand_num < f_c
53
+ lower + Math.sqrt(uniform_rand_num * (upper - lower) * (mode - lower))
54
+ else
55
+ upper - Math.sqrt((1 - uniform_rand_num) * (upper - lower) * (upper - mode))
56
+ end
57
+ end
58
+
59
+ # Implementation based on "A Simple Method for Generating Gamma Variables"
60
+ # by George Marsaglia and Wai Wan Tsang. ACM Transactions on Mathematical Software
61
+ # Vol 26, No 3, September 2000, pages 363-372.
62
+ def gamma(shape, scale)
63
+ if shape >= 1.0
64
+ d = shape - 1.0 / 3.0
65
+ c = 1 / ((9 * d) ** 0.5)
66
+ while true
67
+ v = 0.0
68
+ while v <= 0.0
69
+ x = normal
70
+ v = 1.0 + c * x
71
+ end
72
+ v = v ** 3
73
+ u = uniform
74
+ if u < (1.0 - 0.0331 * (x ** 4)) || Math.log(u) < (0.5 * (x ** 2) + d * (1.0 - v + Math.log(v)))
75
+ return scale * d * v
76
+ end
77
+ end
78
+ elsif shape <= 0.0
79
+ raise 'Shape must be positive'
80
+ else
81
+ g = gamma(shape + 1.0, 1.0)
82
+ w = uniform
83
+ return scale * g * (w ** (1.0 / shape))
84
+ end
85
+ end
86
+
87
+ def chi_square(degrees_of_freedom)
88
+ gamma(0.5 * degrees_of_freedom, 2.0)
89
+ end
90
+
91
+ def inverse_gamma(shape, scale)
92
+ 1.0 / gamma(shape, 1.0 / scale)
93
+ end
94
+
95
+ def beta(a, b)
96
+ raise "Alpha and beta parameters must be positive. Received a = #{a} and b = #{b}." unless a > 0 && b > 0
97
+ u = gamma(a, 1)
98
+ v = gamma(b, 1)
99
+ u / (u + v)
100
+ end
101
+
102
+ def weibull(shape, scale)
103
+ raise 'Shape and scale must be positive' if shape <= 0.0 || scale <= 0.0
104
+
105
+ scale * ((-Math.log(uniform)) ** (1.0 / shape))
106
+ end
107
+
108
+ def cauchy(median, scale)
109
+ raise 'Scale must be positive' if scale <= 0
110
+
111
+ median + scale * Math.tan(Math::PI * (uniform - 0.5))
112
+ end
113
+
114
+ def student_t(degrees_of_freedom)
115
+ raise 'Degrees of freedom must be positive' if degrees_of_freedom <= 0
116
+
117
+ normal / ((chi_square(degrees_of_freedom) / degrees_of_freedom) ** 0.5)
118
+ end
119
+
120
+ def laplace(mean, scale)
121
+ u = uniform
122
+ mean + Math.log(2) + ((u < 0.5 ? 1 : -1) * scale * Math.log(u < 0.5 ? u : 1 - u))
123
+ end
124
+
125
+ def log_normal(mu, sigma)
126
+ Math.exp(normal(mu, sigma))
127
+ end
128
+
129
+ def dirichlet(*parameters)
130
+ sample = parameters.map { |a| gamma(a, 1) }
131
+ sum = sample.inject(0.0) { |sum, g| sum + g }
132
+ sample.map { |g| g / sum }
133
+ end
134
+
135
+ private
136
+
137
+ # This is the heart of the generator.
138
+ # It uses George Marsaglia's MWC algorithm to produce an unsigned integer.
139
+ # See http://www.bobwheeler.com/statistics/Password/MarsagliaPost.txt
140
+ def get_unsigned_int
141
+ @m_z = 36969 * (@m_z & 65535) + (@m_z >> 16);
142
+ @m_w = 18000 * (@m_w & 65535) + (@m_w >> 16);
143
+ ((@m_z << 16) + (@m_w & 65535)) % 4294967296
144
+ end
145
+
146
+ def gamma_function(x)
147
+ g = [
148
+ 1.0,
149
+ 0.5772156649015329,
150
+ -0.6558780715202538,
151
+ -0.420026350340952e-1,
152
+ 0.1665386113822915,
153
+ -0.421977345555443e-1,
154
+ -0.9621971527877e-2,
155
+ 0.7218943246663e-2,
156
+ -0.11651675918591e-2,
157
+ -0.2152416741149e-3,
158
+ 0.1280502823882e-3,
159
+ -0.201348547807e-4,
160
+ -0.12504934821e-5,
161
+ 0.1133027232e-5,
162
+ -0.2056338417e-6,
163
+ 0.6116095e-8,
164
+ 0.50020075e-8,
165
+ -0.11812746e-8,
166
+ 0.1043427e-9,
167
+ 0.77823e-11,
168
+ -0.36968e-11,
169
+ 0.51e-12,
170
+ -0.206e-13,
171
+ -0.54e-14,
172
+ 0.14e-14
173
+ ]
174
+
175
+ r = 1.0
176
+
177
+ return 1e308 if x > 171.0
178
+ if x.is_a?(Fixnum) || x == x.to_i
179
+ if x > 0
180
+ ga = (2...x).inject(1.0) { |prod, i| prod * i }
181
+ else
182
+ 1e308
183
+ end
184
+ else
185
+ if x.abs > 1.0
186
+ r = (1..(x.abs.to_i)).inject(1.0) { |prod, i| prod * (x.abs - i) }
187
+ z = x.abs - x.abs.to_i
188
+ else
189
+ z = x
190
+ end
191
+
192
+ gr = g[24]
193
+ 23.downto(0).each do |i|
194
+ gr = gr * z + g[i]
195
+ end
196
+ ga = 1.0 / (gr * z)
197
+ if x.abs > 1
198
+ ga *= r
199
+ if x < 0
200
+ ga = -Math::PI / (x * ga * Math.sin(Math::PI * x))
201
+ end
202
+ end
203
+ end
204
+
205
+ ga
206
+ end
207
+ end