simple-random 0.10.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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