rubylabs 0.9.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/randomlab.rb CHANGED
@@ -1,58 +1,104 @@
1
+ module RubyLabs
1
2
 
2
3
  =begin rdoc
3
4
 
4
5
  == RandomLab
5
6
 
6
- Random number generators and associated methods.
7
-
8
- =end
7
+ The RandomLab module has definitions of classes and methods used in the projects for Chapter 9
8
+ of <em>Explorations in Computing</em>. The module has methods used in experiments with pseudorandom
9
+ number generators.
9
10
 
10
- =begin
11
- TODO number line -- mini-histogram, e.g. second tick drawn above first? else explain in lab manual / reference why two values per tickmark
12
- TODO another thing for documentation: diff between p.random(x,y) and random(x,y) [latter uses Ruby's PRNG]
13
11
  =end
14
-
15
- module RubyLabs
16
-
12
+
17
13
  module RandomLab
18
14
 
19
15
  require "permute.rb"
20
16
 
17
+ NumberLine = Struct.new(:line, :npoints, :options)
18
+ Histogram = Struct.new(:bins, :max, :keys, :counts, :base, :options)
19
+ DotPlot = Struct.new(:max, :options)
20
+
21
+ @@numberLineOptions = {
22
+ :lineThickness => 3,
23
+ :lineColor => '#777777',
24
+ :tickHeight => 20,
25
+ :tickWidth => 1,
26
+ :tickColor => '#0000FF',
27
+ }
28
+
29
+ @@histogramOptions = {
30
+ :binColor => '#000080',
31
+ :boxIncrement => 8.0,
32
+ :rescaleTrigger => 50,
33
+ }
34
+
35
+ @@dotPlotOptions = {
36
+ :dotColor => '#000080',
37
+ :dotRadius => 1.0,
38
+ }
39
+
40
+ @@drawing = nil
41
+ @@delay = 0.01
42
+
21
43
  =begin rdoc
22
- Pseudo-random number generator. Constraints on a, c, and m:
23
- * c and m must be relatively prime
24
- * a-1 divisible by prime factors of m
25
- * if m is multiple of 4, a-1 must also be a multiple of 4
26
44
 
27
- Note: some good values (for small m) from Numerical Recipes:
28
- m = 53125, a = 171, c = 11213
45
+ == PRNG
46
+
47
+ A PRNG object is a pseudorandom number generator based on the mixed congruential method.
48
+ Sequences generated by a PRNG are defined by three constants, named +a+, +c+, and +m+.
49
+ If <tt>x[i]</tt> is the current item in the sequence, the equation for the next item
50
+ <tt>x[i+1]</tt> is
51
+ x[i+1] = (a * x[i] + c) % m
52
+
29
53
  =end
30
54
 
31
55
  class PRNG
32
56
 
33
57
  attr_accessor :a, :c, :m, :x
34
58
 
59
+ # Make a new pseudorandom number generator using constants +a+, +c+, and +m+.
60
+ #
61
+ # Example -- a random number generator that has the full period of m = 1000 but some
62
+ # surprising correlations between successive values:
63
+ # p = PRNG.new(81, 337, 1000)
64
+ # => #<RandomLab::PRNG a: 81 c: 337 m: 1000>
65
+ #
66
+ # Example: a better PRNG, using values suggested in <em>Numerical Recipes</em> (Press, et al):
67
+ # >> p = PRNG.new(171, 11213, 53125)
68
+ # => #<RandomLab::PRNG a: 171 c: 11213 m: 53125>
69
+
35
70
  def initialize(a, c, m)
36
71
  @a = a
37
72
  @c = c
38
73
  @m = m
39
74
  @x = 0
40
75
  end
76
+
77
+ # Return the current item in the pseudorandom sequence (the most recently generated random
78
+ # number).
41
79
 
42
80
  def state
43
81
  return @x
44
82
  end
83
+
84
+ # Set the state of the pseudorandom sequence to +val+.
45
85
 
46
86
  def seed(val)
47
87
  @x = val
48
88
  end
49
-
89
+
90
+ # Get the next pseudorandom number in the sequence defined by this PRNG object.
91
+ #--
50
92
  # :begin :advance
51
93
  def advance
52
94
  @x = (@x * @a + @c) % @m
53
95
  end
54
96
  # :end :advance
55
97
 
98
+ # Get a random integer between +min+ and +max+ from this PRNG object. Calls
99
+ # advance the get the next value from the pseudorandom sequence, then maps it
100
+ # to an integer between +min+ and +max+.
101
+ #--
56
102
  # :begin :random
57
103
  def random(min, max)
58
104
  return nil if max <= min
@@ -61,6 +107,7 @@ module RandomLab
61
107
  end
62
108
  # :end :random
63
109
 
110
+ # Create a string that describes the attributes of this PRNG object.
64
111
  def inspect
65
112
  sprintf "#<RandomLab::PRNG a: #{@a} c: #{@c} m: #{@m}>"
66
113
  end
@@ -69,7 +116,12 @@ module RandomLab
69
116
 
70
117
  end # class PRNG
71
118
 
72
- # :begin :prng_sequence
119
+ # Return an array of +m+ numbers defined by the pseudorandom sequence with
120
+ # parameters +a+, +c+, and +m+. The first number in the sequence is 0, and
121
+ # the remaining numbers are defined by the recurrence
122
+ # x[i+1] = (a * x[i] + c) % m
123
+ #--
124
+ # :begin :prng_sequence
73
125
  def prng_sequence(a, c, m)
74
126
  seq = [0]
75
127
  (m-1).times do
@@ -77,35 +129,34 @@ module RandomLab
77
129
  end
78
130
  return seq
79
131
  end
80
- # :end :prng_sequence
81
-
82
- =begin rdoc
83
- Make a new deck of cards
84
- =end
132
+ # :end :prng_sequence
85
133
 
134
+ # Make a new deck of cards. Returns an array of 52 card objects, arranged
135
+ # in order from card #0 (the ace of spades) through card #51 (the two of clubs).
136
+
86
137
  def new_deck
87
138
  (0..51).map { |i| Card.new(i) }
88
139
  end
89
140
 
90
141
 
91
- =begin rdoc
92
- Compute the probability of a duplicate item in a collection of n items
93
- drawn from the range [1..d]. Example: call pdup(5,52) to compute the
94
- probability if drawing the same card twice when sampling with replacement
95
- 5 times from a deck of 52 cards.
96
- =end
142
+ # Compute the probability of a duplicate item in a collection of +n+ items
143
+ # drawn from the range [1..d].
144
+ #
145
+ # Example -- to compute the
146
+ # probability of drawing the same card twice when sampling with replacement
147
+ # 5 times from a deck of 52 cards:
148
+ # >> pdup(5, 52)
149
+ # => 0.179716221420819
97
150
 
98
151
  def pdup(n, d)
99
152
  return 1.0 if n > d
100
153
  return 1.0 - (1..(n-1)).inject(1.0) { |p, k| p * (1.0 - (k / 52.0)) }
101
154
  end
102
155
 
103
- =begin rdoc
104
- A "helper method" that can be called via a probe, to print the contents
105
- of an array during the execution of the permute! method
106
- =end
107
-
108
- # Note: permute! moved to own source file, permute.rb
156
+ # A helper method intended to be called via a probe to print the contents
157
+ # of an array during the execution of the <tt>permute!</tt> method. The arguments
158
+ # are the array to display, the location of a left bracket, and the item
159
+ # location where the item next to the bracket will be moved on the next swap.
109
160
 
110
161
  def brackets(a, i, r)
111
162
  res = "#{r}: "
@@ -121,19 +172,17 @@ module RandomLab
121
172
  return res
122
173
  end
123
174
 
124
-
125
- =begin rdoc
126
- Classify a poker hand -- returns a symbol describing a hand. Return values:
127
- :pair
128
- :two_pair
129
- :three_of_a_kind
130
- :full_house
131
- :straight
132
- :flush
133
- :four_of_a_kind
134
- :straight_flush
135
- =end
136
-
175
+ # Given an array of 5 Card objects, determine what type of poker hand is
176
+ # represented by the cards. The return value is a symbol, e.g. <tt>:pair</tt>,
177
+ # <tt>:full_house</tt>, etc. (call poker_rankings to see the complete list of
178
+ # symbols).
179
+ #
180
+ # Example (assuming +d+ is a complete deck of 52 Card objects):
181
+ # >> h = permute!(d).first(5)
182
+ # => [AH, 6S, 7C, JH, AC]
183
+ # >> poker_rank(h)
184
+ # => :pair
185
+
137
186
  def poker_rank(a)
138
187
  rcount = Array.new(Card::Ranks.length, 0)
139
188
  scount = Array.new(Card::Suits.length, 0)
@@ -158,28 +207,35 @@ module RandomLab
158
207
  return :pair
159
208
  end
160
209
  end
210
+
211
+ # Return a list of symbols used to classify poker hands.
161
212
 
162
213
  def poker_rankings
163
214
  return [:high_card, :pair, :two_pair, :three_of_a_kind, :straight, :flush, :full_house, :four_of_a_kind, :straight_flush]
164
215
  end
165
216
 
217
+ # Initialize a Hash object with keys that are poker rank symbols and values that are all initially 0.
218
+ # Used in experiments that count the number of times various hands are dealt.
219
+ #
220
+ # Example:
221
+ # >> poker_counts
222
+ # => {:flush=>0, :full_house=>0, ... :high_card=>0}
223
+
166
224
  def poker_counts
167
225
  h = Hash.new
168
226
  poker_rankings.each { |x| h[x] = 0 }
169
227
  return h
170
228
  end
171
229
 
172
-
173
230
  =begin rdoc
174
- Class to represent cards from a standard 52-card deck. Includes comparators
175
- to sort by rank or suit.
176
-
177
- Call Card.new to get a random card, or Card.new(id) to get a specific card
178
- where id is a number between 0 and 51.
179
- =end
180
231
 
181
- # To test the expression that assigns suits and ranks:
182
- # 52.times { |x| puts (x/13).to_s + " " + (x%13).to_s }
232
+ == Card
233
+
234
+ A Card object represents a single card from a standard 52-card deck. The two
235
+ attributes of a card are its rank (represented by a symbol such as :ace, :king, :three, etc)
236
+ and its suit (:spades, :hearts, etc).
237
+
238
+ =end
183
239
 
184
240
  class Card
185
241
  attr_accessor :rank, :suit
@@ -192,16 +248,43 @@ module RandomLab
192
248
  Ranks = [:ace, :king, :queen, :jack, :ten, :nine, :eight, :seven, :six, :five, :four, :three, :two]
193
249
  end
194
250
 
251
+ # Create a new Card object. Cards are ordered from 0 to 51, with 0 being the ace of spades
252
+ # and 51 being the two of clubs. If no argument is passed to new, a random card is chosen.
253
+ # If an +id+ between 0 and 51 is passed the specified card is created.
254
+ #
255
+ # Example:
256
+ # >> Card.new
257
+ # => 5H
258
+ # >> Card.new(0)
259
+ # => AS
260
+ # >> Card.new(1)
261
+ # => KS
262
+ # >> Card.new(51)
263
+ # => 2C
264
+
195
265
  def initialize(id = nil)
196
266
  id = rand(52) if id.nil?
197
267
  raise "card must be between 0 and 51" if id < 0 || id > 51
198
268
  @suit = Suits[id / 13]
199
269
  @rank = Ranks[id % 13]
200
270
  end
271
+
272
+ # Compare this Card with Card x. Two cards are equal if they have the same
273
+ # suit and same rank.
201
274
 
202
275
  def ==(x)
203
276
  return @suit == x.suit && @rank == x.rank
204
277
  end
278
+
279
+ # Define the sort ordering for Card objects. Cards are compared first by suit,
280
+ # and then by rank. The result of sorting a hand (and array of Card objects) is
281
+ # the common situation for most card games, where cards are grouped by suit.
282
+ #
283
+ # Example (assuming +d+ is a complete deck of 52 cards):
284
+ # >> h = permute!(d).first(13)
285
+ # => [2C, QS, 8S, 6C, 10H, JS, AD, AS, 7D, 8D, JC, 4C, AH]
286
+ # >> h.sort
287
+ # => [AS, QS, JS, 8S, AH, 10H, AD, 8D, 7D, JC, 6C, 4C, 2C]
205
288
 
206
289
  def <=>(x)
207
290
  r0 = Ranks.index(@rank); r1 = Ranks.index(x.rank)
@@ -215,6 +298,11 @@ module RandomLab
215
298
 
216
299
  @@outputform = :utf8
217
300
 
301
+ # Make a string to display a Card objects on the terminal. Checks
302
+ # Ruby's $KCODE global variable to see if the system is using UTF-8. If so, the code for a glyph that
303
+ # shows a spade, heart, diamond, or club is inserted into the string, otherwise a letter is
304
+ # used.
305
+
218
306
  def inspect
219
307
  s = ""
220
308
  s << case @rank
@@ -252,7 +340,6 @@ module RandomLab
252
340
  end
253
341
  end
254
342
  return s
255
- # "#{@rank} #{@suit}"
256
343
  end
257
344
 
258
345
  alias to_s inspect
@@ -267,21 +354,18 @@ module RandomLab
267
354
 
268
355
  end # class Card
269
356
 
270
- =begin rdoc
271
- Visualization methods called by students to test a PRNG object:
272
-
273
- view_numberline(n) make a number line for integers between 0 and n-1
274
- tick_mark(i) draw a tick mark at location i on the number line
275
-
276
- view_histogram(n, max) make a histogram with n bins for value from 0 to max
277
- view_histogram(a) make a histogram with one bin for each item in a
278
- update_bin(x) add 1 to the count of items in bin x
279
- get_counts get a copy of the bins (array of counts)
280
-
281
- view_dotplot(n) initialize an n x n dotplot
282
- plot_point(x,y) add a dot at (x,y) to the dotplot
283
- =end
284
-
357
+ # Initialize the RubyLabs Canvas with a drawing of a number line for integers from 0 to +npoints+-1.
358
+ # Options that control the appearance of the display, and their default values, are:
359
+ # :lineThickness => 3
360
+ # :lineColor => '#777777'
361
+ # :tickHeight => 20
362
+ # :tickWidth => 1
363
+ # :tickColor => '#0000FF'
364
+ #
365
+ # Example:
366
+ # >> view_numberline(500, :lineColor => 'blue', :lineThickness => 1)
367
+ # => true
368
+
285
369
  def view_numberline(npoints, userOptions = {})
286
370
  Canvas.init(500, 100, "RandomLab::NumberLine")
287
371
  options = @@numberLineOptions.merge(userOptions)
@@ -290,21 +374,48 @@ module RandomLab
290
374
  return true
291
375
  end
292
376
 
293
- def tick_mark(i)
294
- if @@drawing.class != NumberLine
295
- puts "call view_numberline to initialize the number line"
296
- elsif i < 0 || i >= @@drawing.npoints
297
- puts "tick_mark: 0 <= i < #{@@drawing.npoints}"
298
- else
299
- x0, y0, x1, y1 = @@drawing.line.coords
300
- tx = (i.to_f / @@drawing.npoints) * (x1-x0)
301
- ty = y0 - @@drawing.options[:tickHeight]
302
- Canvas::Line.new(tx, y0, tx, ty, :width => @@drawing.options[:tickWidth], :fill => @@drawing.options[:tickColor])
303
- sleep(@@delay)
304
- end
305
- return true
306
- end
307
-
377
+ # Draw a tick mark on the RubyLabs Canvas, provided the canvas has been initialized with a call to
378
+ # view_numberline.
379
+
380
+ def tick_mark(i)
381
+ if @@drawing.class != NumberLine
382
+ puts "call view_numberline to initialize the number line"
383
+ elsif i < 0 || i >= @@drawing.npoints
384
+ puts "tick_mark: 0 <= i < #{@@drawing.npoints}"
385
+ else
386
+ x0, y0, x1, y1 = @@drawing.line.coords
387
+ tx = (i.to_f / @@drawing.npoints) * (x1-x0)
388
+ ty = y0 - @@drawing.options[:tickHeight]
389
+ Canvas::Line.new(tx, y0, tx, ty, :width => @@drawing.options[:tickWidth], :fill => @@drawing.options[:tickColor])
390
+ sleep(@@delay)
391
+ end
392
+ return true
393
+ end
394
+
395
+ # Initialize the RubyLabs Canvas to show a histogram with the specified bins. In the
396
+ # initial drawing each bin is represented by a rectangle one pixel tall, i.e. a horizontal line.
397
+ # As items are added to a bin the rectangle will grow in height. If any rectangle reaches the
398
+ # maximum height, all the rectangles are rescaled so the bins can continue to grow.
399
+ #
400
+ # The argument to view_histogram can either be an integer, which specifies the number of bins,
401
+ # or an array of symbols, in which case there will be one bin for each symbol. If the argument is an integer,
402
+ # a second argument can specify a maximum data value; for example, calling <tt>view_histogram(10,100)</tt>
403
+ # will make a histogram with 10 bins for numbers between 0 and 99, so that data values 0 through
404
+ # 9 will go in the first bin, 10 through 19 in the second bin, and so on.
405
+ #
406
+ # Display options and their default values are:
407
+ # :binColor => '#000080'
408
+ # :boxIncrement => 8.0
409
+ # :rescaleTrigger => 50
410
+ #
411
+ # Example: make a histogram for the numbers between 0 and 5:
412
+ # >> view_histogram(6)
413
+ # => true
414
+ # Example: make a histogram to count the number of times each type of poker hand is seen in an
415
+ # experiment:
416
+ # >> view_histogram(poker_rankings, :binColor => 'darkgreen')
417
+ # => true
418
+
308
419
  def view_histogram(*args)
309
420
  begin
310
421
  if args[0].class == Array
@@ -341,7 +452,10 @@ module RandomLab
341
452
 
342
453
  return true
343
454
  end
344
-
455
+
456
+ # Update the bin for data item +x+, presuming the RubyLabs Canvas has been initialized to show
457
+ # a histogram for data of this type. The bin for +x+ increases in height. If it reaches
458
+ # the maximum height, rescale all the bins and increase the maximum height.
345
459
 
346
460
  def update_bin(x)
347
461
  if @@drawing.class != Histogram
@@ -380,6 +494,8 @@ module RandomLab
380
494
  sleep(@@delay)
381
495
  return true
382
496
  end
497
+
498
+ # Return an array of counts for the bins in the histogram currently on the RubyLabs Canvas.
383
499
 
384
500
  def get_counts
385
501
  if @@drawing.class == Histogram
@@ -389,55 +505,45 @@ module RandomLab
389
505
  return nil
390
506
  end
391
507
  end
392
-
508
+
509
+ # Initialize the RubyLabs Canvas to show a dot plot with +npoints+ in both the +x+
510
+ # and +y+ dimension. Drawing options and their defaults are
511
+ # :dotColor => '#000080'
512
+ # :dotRadius => 1.0
513
+ #
514
+ # Example: intialize the drawing for a 250 x 250 dot plot with green dots:
515
+ # >> view_dotplot(250, :dotColor => 'darkgreen')
516
+ # => true
517
+
393
518
  def view_dotplot(npoints, userOptions = {})
394
519
  Canvas.init(500, 500, "RandomLab::DotPlot")
395
520
  options = @@dotPlotOptions.merge(userOptions)
396
521
  @@drawing = DotPlot.new(npoints, options)
397
522
  return true
398
523
  end
524
+
525
+ # Add a point at location 'x', 'y' to the dot plot on the RubyLabs Canvas.
526
+ #
527
+ # Example: if the canvas was initialized to show a 250 x 250 plot, this call
528
+ # will display a point in the center of the drawing:
529
+ # >> plot_point(125,125)
530
+ # => nil
399
531
 
400
532
  def plot_point(x,y)
401
533
  if @@drawing.class != DotPlot
402
534
  puts "call view_dotplot to initialize a dot plot"
403
- elsif x < 0 || x >= @@drawing.max || y < 0 || y >= @@drawing.max
404
- puts "plot_point: 0 <= x, y < #{@@drawing.max}"
405
- else
535
+ elsif x < 0 || x >= @@drawing.max || y < 0 || y >= @@drawing.max
536
+ puts "plot_point: 0 <= x, y < #{@@drawing.max}"
537
+ else
406
538
  px = (x.to_f / @@drawing.max) * Canvas.width
407
539
  py = (y.to_f / @@drawing.max) * Canvas.height
408
540
  r = @@drawing.options[:dotRadius]
409
541
  color = @@drawing.options[:dotColor]
410
542
  Canvas::Circle.new( px, py, r, :outline => color, :fill => color )
411
- end
412
- return nil
543
+ end
544
+ return nil
413
545
  end
414
-
415
- NumberLine = Struct.new(:line, :npoints, :options)
416
- Histogram = Struct.new(:bins, :max, :keys, :counts, :base, :options)
417
- DotPlot = Struct.new(:max, :options)
418
-
419
- @@numberLineOptions = {
420
- :lineThickness => 3,
421
- :lineColor => '#777777',
422
- :tickHeight => 20,
423
- :tickWidth => 1,
424
- :tickColor => '#0000FF',
425
- }
426
-
427
- @@histogramOptions = {
428
- :binColor => '#000080',
429
- :boxIncrement => 8.0,
430
- :rescaleTrigger => 50,
431
- }
432
-
433
- @@dotPlotOptions = {
434
- :dotColor => '#000080',
435
- :dotRadius => 1.0,
436
- }
437
-
438
- @@drawing = nil
439
- @@delay = 0.01
440
-
546
+
441
547
  end # RandomLab
442
548
 
443
549
  end # RubyLabs