rubylabs 0.9.0 → 0.9.1

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/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