discrete_event 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,173 @@
1
+ = discrete_event
2
+
3
+ http://github.com/jdleesmiller/discrete_event
4
+
5
+ == SYNOPSIS
6
+
7
+ This gem provides some tools for discrete event simulation (DES) in ruby. The
8
+ main one is a {DiscreteEvent::EventQueue} that stores actions (ruby blocks) to
9
+ be executed at chosen times.
10
+
11
+ The example below uses the {DiscreteEvent::Simulation} class, which is a
12
+ subclass of {DiscreteEvent::EventQueue}, to simulate an M/M/1 queueing system.
13
+
14
+ require 'rubygems'
15
+ require 'discrete_event'
16
+
17
+ #
18
+ # A single-server queueing system with Markovian arrival and service
19
+ # processes.
20
+ #
21
+ # Note that the simulation runs indefinitely, and that it doesn't collect
22
+ # statistics; this is left to the user. See mm1_queue_demo, below, for
23
+ # an example of how to collect statistics and how to stop the simulation
24
+ # by throwing the :stop symbol.
25
+ #
26
+ class MM1Queue < DiscreteEvent::Simulation
27
+ Customer = Struct.new(:arrival_time, :queue_on_arrival,
28
+ :service_begin, :service_end)
29
+
30
+ attr_reader :arrival_rate, :service_rate, :system, :served
31
+
32
+ def initialize arrival_rate, service_rate
33
+ super()
34
+ @arrival_rate, @service_rate = arrival_rate, service_rate
35
+ @system = []
36
+ @served = []
37
+ end
38
+
39
+ # Sample from Exponential distribution with given mean rate.
40
+ def rand_exp rate
41
+ -Math::log(rand)/rate
42
+ end
43
+
44
+ # Customer arrival process.
45
+ # The after method is provided by {DiscreteEvent::Simulation}.
46
+ # The given action (a Ruby block) will run after the random delay
47
+ # computed by rand_exp. When it runs, the last thing the action does is
48
+ # call new_customer, which creates an event for the next customer.
49
+ def new_customer
50
+ after rand_exp(arrival_rate) do
51
+ system << Customer.new(now, queue_length)
52
+ serve_customer if system.size == 1
53
+ new_customer
54
+ end
55
+ end
56
+
57
+ # Customer service process.
58
+ def serve_customer
59
+ system.first.service_begin = now
60
+ after rand_exp(service_rate) do
61
+ system.first.service_end = now
62
+ served << system.shift
63
+ serve_customer unless system.empty?
64
+ end
65
+ end
66
+
67
+ # Number of customers currently waiting for service (does not include
68
+ # the one (if any) currently being served).
69
+ def queue_length
70
+ if system.empty? then 0 else system.length - 1 end
71
+ end
72
+
73
+ # Called by super.run.
74
+ def start
75
+ new_customer
76
+ end
77
+ end
78
+
79
+ #
80
+ # Run until a fixed number of passengers has been served.
81
+ #
82
+ def mm1_queue_demo arrival_rate, service_rate, num_pax
83
+ # Run simulation and accumulate stats.
84
+ q = MM1Queue.new arrival_rate, service_rate
85
+ num_served = 0
86
+ total_queue = 0.0
87
+ total_wait = 0.0
88
+ q.run do
89
+ unless q.served.empty?
90
+ raise "confused" if q.served.size > 1
91
+ c = q.served.shift
92
+ total_queue += c.queue_on_arrival
93
+ total_wait += c.service_begin - c.arrival_time
94
+ num_served += 1
95
+ end
96
+ throw :stop if num_served >= num_pax
97
+ end
98
+
99
+ # Use standard formulas for comparison.
100
+ rho = arrival_rate / service_rate
101
+ expected_mean_wait = rho / (service_rate - arrival_rate)
102
+ expected_mean_queue = arrival_rate * expected_mean_wait
103
+
104
+ return total_queue / num_served, expected_mean_queue,
105
+ total_wait / num_served, expected_mean_wait
106
+ end
107
+
108
+
109
+
110
+ This and other examples are available in the <tt>test/discrete_event</tt>
111
+ directory.
112
+
113
+ In this example, the whole simulation happens in a single object; if you have
114
+ multiple objects, you can use the {DiscreteEvent::Events} mix-in to make them
115
+ easily share a single event queue.
116
+
117
+ == INSTALLATION
118
+
119
+ gem install discrete_event
120
+
121
+ == REFERENCES
122
+
123
+ * {http://en.wikipedia.org/wiki/Discrete_event_simulation}
124
+
125
+ You may also be interested in the Ruby bindings of the GNU Science Library, which provides a variety of pseudo-random number generators and functions for generating random variates from various distributions. It also provides useful things like histograms.
126
+
127
+ * {http://www.gnu.org/software/gsl/}
128
+ * {http://rb-gsl.rubyforge.org/}
129
+ * The libgsl-ruby package in Debian.
130
+
131
+ == HISTORY
132
+
133
+ <em>1.0.0:</em>
134
+ * split {DiscreteEvent::EventQueue} out of DiscreteEvent::Simulation for
135
+ easier sharing between objects
136
+ * added {DiscreteEvent::Events} mix-in
137
+
138
+ <em>0.3.0:</em>
139
+ * reorganized for compatibility with gemma 2.0; no functional changes
140
+ * added major, minor and patch version constants
141
+
142
+ <em>0.2.0:</em>
143
+ * added DiscreteEvent::Simulation#at_each_index (removed in 1.0.0)
144
+ * added DiscreteEvent::Simulation#recur_after
145
+ * added DiscreteEvent::Simulation#every
146
+ * {DiscreteEvent::FakeRand} now supports the <tt>Kernel::rand(n)</tt> form.
147
+
148
+ <em>0.1.0:</em>
149
+ * first release
150
+
151
+ == LICENSE
152
+
153
+ Copyright (c) 2010-2011 John Lees-Miller
154
+
155
+ Permission is hereby granted, free of charge, to any person obtaining
156
+ a copy of this software and associated documentation files (the
157
+ "Software"), to deal in the Software without restriction, including
158
+ without limitation the rights to use, copy, modify, merge, publish,
159
+ distribute, sublicense, and/or sell copies of the Software, and to
160
+ permit persons to whom the Software is furnished to do so, subject to
161
+ the following conditions:
162
+
163
+ The above copyright notice and this permission notice shall be
164
+ included in all copies or substantial portions of the Software.
165
+
166
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
167
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
168
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
169
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
170
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
171
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
172
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
173
+
@@ -0,0 +1,285 @@
1
+ module DiscreteEvent
2
+ #
3
+ # Queue of pending events; also keeps track of the clock (the current time).
4
+ #
5
+ # There are two key terms:
6
+ # * action: any Ruby block
7
+ # * event: an action to be executed at some specified time in the future
8
+ # Events are usually created using the {#at} and {#after} methods.
9
+ # The methods {#at_each}, {#every} and {#recur_after} make some important
10
+ # special cases more efficient.
11
+ #
12
+ # See the {file:README} for an example.
13
+ #
14
+ class EventQueue
15
+ #
16
+ # Event queue entry for events; you do not need to use this class directly.
17
+ #
18
+ Event = Struct.new(:time, :action)
19
+
20
+ #
21
+ # Current time (taken from the currently executing event, if any). You can
22
+ # use floating point or integer time.
23
+ #
24
+ # @return [Number]
25
+ #
26
+ attr_reader :now
27
+
28
+ #
29
+ # Event queue.
30
+ #
31
+ # @return [PQueue]
32
+ #
33
+ attr_reader :events
34
+
35
+ def initialize now=0.0
36
+ @now = now
37
+ @events = PQueue.new {|a,b| a.time < b.time}
38
+ @recur_interval = nil
39
+ end
40
+
41
+ #
42
+ # Schedule +action+ (a block) to run at the given +time+; +time+ must not be
43
+ # in the past.
44
+ #
45
+ # @param [Number] time at which +action+ should run; must be >= {#now}
46
+ #
47
+ # @yield [] action to be run at +time+
48
+ #
49
+ # @return [nil]
50
+ #
51
+ def at time, &action
52
+ raise "cannot schedule event in the past" if time < now
53
+ @events.push(Event.new(time, action))
54
+ nil
55
+ end
56
+
57
+ #
58
+ # Schedule +action+ (a block) to run after the given +delay+ (with respect
59
+ # to {#now}).
60
+ #
61
+ # @param [Number] delay after which +action+ should run; non-negative
62
+ #
63
+ # @yield [] action to be run after +delay+
64
+ #
65
+ # @return [nil]
66
+ #
67
+ def after delay, &action
68
+ at(@now + delay, &action)
69
+ end
70
+
71
+ #
72
+ # Schedule +action+ (a block) to run for each element in the given list
73
+ # (possibly at different times).
74
+ #
75
+ # This method may be of interest if you have a large number of events that
76
+ # occur at known times. You could use {#at} to add each one to the event
77
+ # queue at the start of the simulation, but this will make adding other
78
+ # events more expensive. Instead, this method adds them one at a time, so
79
+ # only the next event is stored in the event queue.
80
+ #
81
+ # @example
82
+ # Alert = Struct.new(:when, :message)
83
+ # alerts = [Alert.new(12, "ha!"), Alert.new(42, "ah!")] # and many more
84
+ # at_each alerts, :when do |alert|
85
+ # puts alert.message
86
+ # end
87
+ #
88
+ # @param [Enumerable] elements to yield; must be in ascending order
89
+ # according to +time+; note that this method keeps a reference to
90
+ # this object and removes elements as they are executed, so you may
91
+ # want to pass a copy if you plan to change it after this call
92
+ # returns
93
+ #
94
+ # @param [Proc, Symbol, nil] time used to determine when the action will run
95
+ # for a given element; if a +Proc+, the proc must return the
96
+ # appropriate time; if a +Symbol+, each element must respond to
97
+ # +time+; if nil, it is assumed that <tt>element.time</tt> returns
98
+ # the time
99
+ #
100
+ # @yield [element]
101
+ #
102
+ # @yieldparam [Object] element from +elements+
103
+ #
104
+ # @return [nil]
105
+ #
106
+ def at_each elements, time=nil, &action
107
+ raise ArgumentError, 'no action given' unless block_given?
108
+
109
+ unless elements.empty?
110
+ element = elements.shift
111
+ if time.nil?
112
+ element_time = element.time
113
+ elsif time.is_a? Proc
114
+ element_time = time.call(element)
115
+ elsif time.is_a? Symbol
116
+ element_time = element.send(time)
117
+ else
118
+ raise ArgumentError, "bad time"
119
+ end
120
+
121
+ at element_time do
122
+ yield element
123
+ at_each elements, time, &action
124
+ end
125
+ end
126
+ nil
127
+ end
128
+
129
+ #
130
+ # When called from within an action block, repeats the action block after
131
+ # the specified +interval+ has elapsed.
132
+ #
133
+ # Calling this method from outside an action block has no effect.
134
+ # You may call this method at most once in an action block.
135
+ #
136
+ # @example
137
+ # at 5 do
138
+ # puts "now: #{now}"
139
+ # recur_after 10*rand
140
+ # end
141
+ #
142
+ # Note that you can achieve the same effect using {#at} and {#after} and a
143
+ # named method, as in
144
+ # def demo
145
+ # at 5 do
146
+ # puts "now: #{now}"
147
+ # after 10*rand do
148
+ # demo
149
+ # end
150
+ # end
151
+ # end
152
+ # but it is somewhat more efficient to call +recur_after+, and, if you do,
153
+ # the named method is not necessary.
154
+ #
155
+ # @param [Number] interval non-negative
156
+ #
157
+ # @return [nil]
158
+ #
159
+ def recur_after interval
160
+ raise "cannot recur twice" if @recur_interval
161
+ @recur_interval = interval
162
+ nil
163
+ end
164
+
165
+ #
166
+ # Schedule +action+ (a block) to run periodically.
167
+ #
168
+ # This is useful for statistics collection.
169
+ #
170
+ # Note that if you specify one or more events of this kind, the simulation
171
+ # will never run out of events.
172
+ #
173
+ # @example
174
+ # every 5 do
175
+ # if now > 100
176
+ # # record stats
177
+ # end
178
+ # throw :stop if now > 1000
179
+ # end
180
+ #
181
+ # @param [Numeric] interval non-negative
182
+ #
183
+ # @param [Numeric] start block first runs at this time
184
+ #
185
+ # @return [nil]
186
+ #
187
+ def every interval, start=0, &action
188
+ at start do
189
+ yield
190
+ recur_after interval
191
+ end
192
+ nil
193
+ end
194
+
195
+ #
196
+ # The time of the next queued event, or +nil+ if there are no queued events.
197
+ #
198
+ # If this method is called from within an action block, it returns {#now}
199
+ # (that is, the current event hasn't finished yet, so it's still in some
200
+ # sense the next event).
201
+ #
202
+ # @return [Number, nil]
203
+ #
204
+ def next_event_time
205
+ event = @events.top
206
+ if event
207
+ event.time
208
+ else
209
+ nil
210
+ end
211
+ end
212
+
213
+ #
214
+ # Run the action for the next event in the queue.
215
+ #
216
+ # @return [Boolean] false if there are no more events.
217
+ #
218
+ def run_next
219
+ event = @events.top
220
+ if event
221
+ # run the action
222
+ @now = event.time
223
+ event.action.call
224
+
225
+ # recurring events get special treatment: can avoid doing a push and a
226
+ # pop by reusing the Event at the top of the heap, but with a new time
227
+ #
228
+ # NB: this assumes that the top element in the heap can't change due to
229
+ # the event that we just ran, which is the case here, because we don't
230
+ # allow events to be created in the past, and because of the internals
231
+ # of the PQueue datastructure
232
+ if @recur_interval
233
+ event.time = @now + @recur_interval
234
+ @events.replace_top(event)
235
+ @recur_interval = nil
236
+ else
237
+ @events.pop
238
+ end
239
+
240
+ true
241
+ else
242
+ false
243
+ end
244
+ end
245
+
246
+ #
247
+ # Allow for the creation of a ruby +Enumerator+ for the simulation. This
248
+ # yields for each event.
249
+ #
250
+ # @example TODO
251
+ # eq = EventQueue.new
252
+ # eq.at 13 do
253
+ # puts "hi"
254
+ # end
255
+ # eq.at 42 do
256
+ # puts "hello"
257
+ # end
258
+ # for t in eq.to_enum
259
+ # puts t
260
+ # end
261
+ #
262
+ # @yield [now] called immediately after each event runs
263
+ #
264
+ # @yieldparam [Number] now as {#now}
265
+ #
266
+ # @return [self]
267
+ #
268
+ def each
269
+ yield now while run_next
270
+ self
271
+ end
272
+
273
+ #
274
+ # Clear any pending events in the event queue and reset {#now}.
275
+ #
276
+ # @return [self]
277
+ #
278
+ def reset now=0.0
279
+ @now = now
280
+ @events.clear
281
+ self
282
+ end
283
+ end
284
+ end
285
+
@@ -0,0 +1,94 @@
1
+ module DiscreteEvent
2
+ #
3
+ # Mix-in for simulations with multiple objects that have to share the same
4
+ # clock. See the {file:README} for an example.
5
+ #
6
+ # The implementing class must have an instance method <tt>event_queue</tt>
7
+ # that returns the {EventQueue} to use; this method may be private.
8
+ #
9
+ module Events
10
+ #
11
+ # See {EventQueue#at}.
12
+ #
13
+ # @param [Number] time at which +action+ should run; must be >= {#now}
14
+ #
15
+ # @yield [] action to be run at +time+
16
+ #
17
+ # @return [nil]
18
+ #
19
+ def at time, &action
20
+ event_queue.at(time, &action)
21
+ end
22
+
23
+ #
24
+ # See {EventQueue#after}.
25
+ #
26
+ # @param [Number] delay after which +action+ should run; non-negative
27
+ #
28
+ # @yield [] action to be run after +delay+
29
+ #
30
+ # @return [nil]
31
+ #
32
+ def after delay, &action
33
+ event_queue.after(delay, &action)
34
+ end
35
+
36
+ #
37
+ # See {EventQueue#at_each}.
38
+ #
39
+ # @param [Enumerable] elements to yield; must be in ascending order
40
+ # according to +time+; note that this method keeps a reference to
41
+ # this object and removes elements as they are executed, so you may
42
+ # want to pass a copy if you plan to change it after this call
43
+ # returns
44
+ #
45
+ # @param [Proc, Symbol, nil] time used to determine when the action will run
46
+ # for a given element; if a +Proc+, the proc must return the
47
+ # appropriate time; if a +Symbol+, each element must respond to
48
+ # +time+; if nil, it is assumed that <tt>element.time</tt> returns
49
+ # the time
50
+ #
51
+ # @yield [element]
52
+ #
53
+ # @yieldparam [Object] element from +elements+
54
+ #
55
+ # @return [nil]
56
+ #
57
+ def at_each elements, time=nil, &action
58
+ event_queue.at_each(elements, time, &action)
59
+ end
60
+
61
+ #
62
+ # See {EventQueue#recur_after}.
63
+ #
64
+ # @param [Number] interval non-negative
65
+ #
66
+ # @return [nil]
67
+ #
68
+ def recur_after interval
69
+ event_queue.recur_after(interval)
70
+ end
71
+
72
+ #
73
+ # See {EventQueue#every}.
74
+ #
75
+ # @param [Numeric] interval non-negative
76
+ #
77
+ # @param [Numeric] start block first runs at this time
78
+ #
79
+ # @return [nil]
80
+ def every interval, start=0, &action
81
+ event_queue.every(interval, start, &action)
82
+ end
83
+
84
+ #
85
+ # See {EventQueue#now}.
86
+ #
87
+ # @return [Number]
88
+ #
89
+ def now
90
+ event_queue.now
91
+ end
92
+ end
93
+ end
94
+
@@ -0,0 +1,73 @@
1
+ module DiscreteEvent
2
+ #
3
+ # A utility for testing objects that use the built-in Ruby pseudorandom
4
+ # number generator (+Kernel::rand+); use it to specify a particular sequence
5
+ # of (non-random) numbers to be returned by +rand+.
6
+ #
7
+ # Using this utility may be better than running tests with a fixed seed,
8
+ # because you can specify random numbers that produce particular behavior.
9
+ #
10
+ # The sequence is specific to the object that you give to {.for}; this means
11
+ # that you must specify a separate fake sequence for each object in the
12
+ # simulation (which is usually easier than trying to specify one sequence for
13
+ # the whole sim, anyway).
14
+ #
15
+ # @example
16
+ # class Foo
17
+ # def do_stuff
18
+ # # NB: FakeRand.for won't work if you write "Kernel::rand" instead of
19
+ # # just "rand," here.
20
+ # puts rand
21
+ # end
22
+ # end
23
+ # foo = Foo.new
24
+ # foo.do_stuff # outputs a pseudorandom number
25
+ # DiscreteEvent::FakeRand.for(foo, 0.0, 0.1)
26
+ # foo.do_stuff # outputs 0.0
27
+ # foo.do_stuff # outputs 0.1
28
+ # foo.do_stuff # raises an exception
29
+ #
30
+ module FakeRand
31
+ #
32
+ # Create a method +rand+ in +object+'s singleton class that returns the
33
+ # given fake "random numbers;" it raises an error if it runs out of fakes.
34
+ #
35
+ # @param [Object] object to modify
36
+ # @param [Array] fakes sequence of numbers to return
37
+ # @return [nil]
38
+ #
39
+ def self.for object, *fakes
40
+ undo_for(object) # in case rand is already faked
41
+ (class << object; self; end).instance_eval do
42
+ define_method :rand do |*args|
43
+ raise "out of fake_rand numbers" if fakes.empty?
44
+ r = fakes.shift
45
+
46
+ # can be either the rand() or rand(n) form
47
+ n = args.shift || 0
48
+ if n == 0
49
+ r
50
+ else
51
+ (r * n).to_i
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ #
58
+ # Reverse the effects of {.for}.
59
+ # If object has its own +rand+, it is restored; otherwise, the object
60
+ # goes back to using +Kernel::rand+.
61
+ #
62
+ # @param [Object] object to modify
63
+ # @return [nil]
64
+ #
65
+ def self.undo_for object
66
+ if object.methods.map(&:to_s).member?('rand')
67
+ (class << object; self; end).instance_eval do
68
+ remove_method :rand
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,37 @@
1
+ module DiscreteEvent
2
+ #
3
+ # A simulation, including an {EventQueue}, the current time, and various
4
+ # helpers.
5
+ #
6
+ # See the {file:README} for an example.
7
+ #
8
+ class Simulation < EventQueue
9
+ #
10
+ # Called by +run+ when beginning a new simulation; you will probably want
11
+ # to override this.
12
+ #
13
+ # @abstract
14
+ #
15
+ def start
16
+ end
17
+
18
+ #
19
+ # Run (or continue, if there are existing events) the simulation until
20
+ # +:stop+ is thrown, or there are no more events.
21
+ #
22
+ # @yield [] after each event runs
23
+ # @return [nil]
24
+ #
25
+ def run &block
26
+ start if @events.empty?
27
+ catch :stop do
28
+ if block_given?
29
+ yield while run_next
30
+ else
31
+ nil while run_next
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,6 @@
1
+ module DiscreteEvent
2
+ VERSION_MAJOR = 1
3
+ VERSION_MINOR = 0
4
+ VERSION_PATCH = 0
5
+ VERSION = [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH].join('.')
6
+ end
@@ -0,0 +1,22 @@
1
+ require 'pqueue'
2
+
3
+ require 'discrete_event/event_queue'
4
+ require 'discrete_event/events'
5
+ require 'discrete_event/simulation'
6
+ require 'discrete_event/fake_rand'
7
+
8
+ #
9
+ # Root module; see {file:README}.
10
+ #
11
+ module DiscreteEvent
12
+ #
13
+ # Short form for creating a {Simulation} object.
14
+ #
15
+ # @return [Simulation]
16
+ #
17
+ def self.simulation *args, &block
18
+ sim = DiscreteEvent::Simulation.new(*args)
19
+ sim.instance_eval(&block)
20
+ sim
21
+ end
22
+ end
@@ -0,0 +1,257 @@
1
+ require 'discrete_event/test_helper'
2
+
3
+ require 'discrete_event/ex_consumer.rb'
4
+ require 'discrete_event/ex_mm1_queue.rb'
5
+
6
+ include DiscreteEvent
7
+ include DiscreteEvent::Example
8
+
9
+ class TestDiscreteEvent < Test::Unit::TestCase
10
+ def assert_near expected, observed, tol=1e-6
11
+ assert((expected - observed).abs < tol,
12
+ "expected |#{expected} - #{observed}| < #{tol}")
13
+ end
14
+
15
+ def test_fake_rand
16
+ c = ConsumerSim.new 3
17
+
18
+ # Before faking.
19
+ c.run
20
+ assert_equal 3, c.consumed.size
21
+
22
+ # Fake rand.
23
+ FakeRand.for(c, 0.125, 0.25, 0.5)
24
+ c.reset.run
25
+ assert_equal [0.125, 0.375, 0.875], c.consumed
26
+
27
+ # Now have run out of fakes.
28
+ assert_raise(RuntimeError){ c.reset.run }
29
+
30
+ # See what happens if we fake twice.
31
+ FakeRand.for(c, 0.5, 0.25, 0.125)
32
+ c.reset.run
33
+ assert_equal [0.5, 0.75, 0.875], c.consumed
34
+
35
+ # Now have run out of fakes, again.
36
+ assert_raise(RuntimeError){ c.reset.run }
37
+
38
+ # Can undo and get original behavior back.
39
+ FakeRand.undo_for(c)
40
+ c.reset.run # no exception
41
+ assert_equal 3, c.consumed.size
42
+ end
43
+
44
+ def test_fake_rand_n
45
+ # Test that we can also fake random integers (for Kernel::rand(n)).
46
+ o = Object.new
47
+ class <<o
48
+ def test
49
+ rand(11)
50
+ end
51
+ end
52
+
53
+ FakeRand.for(o, 0.0, 0.1, 0.5, 0.99)
54
+ assert_equal 0, o.test
55
+ assert_equal 1, o.test
56
+ assert_equal 5, o.test
57
+ assert_equal 10, o.test
58
+
59
+ # Now have run out of fakes.
60
+ assert_raise(RuntimeError){ o.test }
61
+ end
62
+
63
+ def test_mm1_queue_not_busy
64
+ # Service begins immediately when queue is not busy.
65
+ q = MM1Queue.new 0.5, 1.0
66
+ fakes = [1, 1, 1, 1, 1].map {|x| 1/Math::E**x}
67
+ FakeRand.for(q, *fakes)
68
+ q.run do
69
+ throw :stop if q.served.size >= 2
70
+ end
71
+ assert_near 2.0, q.served[0].arrival_time
72
+ assert_near 2.0, q.served[0].service_begin
73
+ assert_near 3.0, q.served[0].service_end
74
+ assert_near 4.0, q.served[1].arrival_time
75
+ assert_near 4.0, q.served[1].service_begin
76
+ assert_near 5.0, q.served[1].service_end
77
+ end
78
+
79
+ def test_mm1_queue_busy
80
+ # Service begins after previous customer when queue is busy.
81
+ q = MM1Queue.new 0.5, 1.0
82
+ fakes = [0.1, 0.1, # arrival, service for first customer
83
+ 0.01, 0.01, # arrival times for second two customers
84
+ 1, # arrival for forth customer
85
+ 0.1, 0.1, # service times for second two customers
86
+ 1].map {|x| 1/Math::E**x}
87
+ FakeRand.for(q, *fakes)
88
+ q.run do
89
+ throw :stop if q.served.size >= 3
90
+ end
91
+ assert_near 0.2, q.served[0].arrival_time
92
+ assert_near 0.2, q.served[0].service_begin
93
+ assert_near 0.3, q.served[0].service_end
94
+ assert_near 0.22, q.served[1].arrival_time
95
+ assert_near 0.3, q.served[1].service_begin
96
+ assert_near 0.4, q.served[1].service_end
97
+ assert_near 0.24, q.served[2].arrival_time
98
+ assert_near 0.4, q.served[2].service_begin
99
+ assert_near 0.5, q.served[2].service_end
100
+ end
101
+
102
+ def test_recur_after
103
+ output = []
104
+ DiscreteEvent.simulation {
105
+ at 0 do
106
+ output << now
107
+ recur_after 5 if now < 20
108
+ end
109
+
110
+ run
111
+ }
112
+ assert_equal [0, 5, 10, 15, 20], output
113
+ end
114
+
115
+ def test_recur_after_with_after_0
116
+ # Putting a new event in the queue and then calling recur_after should not
117
+ # displace the root element, even if you call after(0), which is just an
118
+ # edge case anyway.
119
+ output = []
120
+ DiscreteEvent.simulation {
121
+ at 0 do
122
+ output << now
123
+ after 0 do
124
+ output << 42
125
+ end
126
+ after 1 do
127
+ output << 13
128
+ end
129
+ recur_after 5 if now < 10
130
+ end
131
+
132
+ run
133
+ }
134
+ assert_equal [0, 42, 13, 5, 42, 13, 10, 42, 13], output
135
+ end
136
+
137
+ def test_every
138
+ output = []
139
+ DiscreteEvent.simulation {
140
+ every 3 do
141
+ output << now
142
+ throw :stop if now > 10
143
+ end
144
+ run
145
+ }
146
+ assert_equal [0,3,6,9,12], output
147
+ end
148
+
149
+ Alert = Struct.new(:when, :message)
150
+
151
+ def test_at_each_with_symbol
152
+ output = []
153
+ DiscreteEvent.simulation {
154
+ alerts = [Alert.new(12, "ha!"), Alert.new(42, "ah!")] # and many more
155
+ at_each alerts, :when do |alert|
156
+ output << now << alert.message
157
+ end
158
+ run
159
+ }
160
+ assert_equal [12, 'ha!', 42, 'ah!'], output
161
+ end
162
+
163
+ def test_at_each_with_proc
164
+ output = []
165
+ DiscreteEvent.simulation {
166
+ alerts = [Alert.new(12, "ha!"), Alert.new(42, "ah!")] # and many more
167
+ at_each(alerts, proc{|alert| alert.when}) do |alert|
168
+ output << now << alert.message
169
+ end
170
+ run
171
+ }
172
+ assert_equal [12, 'ha!', 42, 'ah!'], output
173
+ end
174
+
175
+ Alert2 = Struct.new(:time, :message)
176
+ def test_at_each_with_default
177
+ output = []
178
+ DiscreteEvent.simulation {
179
+ alerts = [Alert2.new(12, "ha!"), Alert2.new(42, "ah!")] # and many more
180
+ at_each alerts do |alert|
181
+ output << now << alert.message
182
+ end
183
+ run
184
+ }
185
+ assert_equal [12, 'ha!', 42, 'ah!'], output
186
+ end
187
+
188
+ def test_next_event_time
189
+ output= []
190
+ s = DiscreteEvent.simulation {
191
+ at 0 do
192
+ output << next_event_time
193
+ end
194
+
195
+ at 5 do
196
+ output << next_event_time
197
+ end
198
+ }
199
+ assert_equal 0, s.next_event_time
200
+ assert s.run_next
201
+ assert_equal 5, s.next_event_time
202
+ assert s.run_next
203
+ assert_nil s.next_event_time
204
+
205
+ # as currently implemented, the "next" event includes the current event
206
+ assert_equal [0, 5], output
207
+ end
208
+
209
+ def test_enumerator
210
+ output = []
211
+ output_times = []
212
+ eq = EventQueue.new
213
+ eq.at 13 do
214
+ output << 'hi'
215
+ end
216
+ eq.at 42 do
217
+ output << 'bye'
218
+ end
219
+ for t in eq.to_enum
220
+ output_times << t
221
+ end
222
+ assert_equal %w(hi bye), output
223
+ assert_equal [13, 42], output_times
224
+ end
225
+
226
+ def test_mm1_queue_demo
227
+ # Just run the demo... 1000 isn't enough to get a reliable average.
228
+ obs_q, exp_q, obs_w, exp_w= mm1_queue_demo(0.25, 0.5, 1000)
229
+ assert_near exp_q, 0.5 # mean queue = rho^2 / (1 - rho)
230
+ assert_near exp_w, 2.0 # mean wait = rho / (mu - lambda)
231
+ end
232
+
233
+ def test_producer_consumer
234
+ event_queue = EventQueue.new(0)
235
+ consumer = Consumer.new(event_queue)
236
+ producer = Producer.new(event_queue, %w(a b c d), consumer)
237
+
238
+ FakeRand.for(consumer, 2, 2, 2, 2)
239
+ FakeRand.for(producer, 1, 1, 1, 1)
240
+
241
+ producer.produce
242
+ output = []
243
+ event_queue.each do |now|
244
+ output << [now, consumer.consumed.dup]
245
+ end
246
+ assert_equal [
247
+ [1, []], # first object produced
248
+ [2, []], # second object produced
249
+ [3, ["a"]], # third object produced / first consumed
250
+ [3, ["a"]],
251
+ [4, ["a", "b"]], # fourth object produced / second consumed
252
+ [4, ["a", "b"]],
253
+ [5, ["a", "b", "c"]], # third and fourth objects consumed
254
+ [6, ["a", "b", "c", "d"]]], output
255
+ end
256
+ end
257
+
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: discrete_event
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - John Lees-Miller
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: pqueue
16
+ requirement: &80923720 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.0.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *80923720
25
+ - !ruby/object:Gem::Dependency
26
+ name: gemma
27
+ requirement: &80923460 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 2.0.0
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *80923460
36
+ description: Some simple primitives for event-based discrete event simulation.
37
+ email:
38
+ - jdleesmiller@gmail.com
39
+ executables: []
40
+ extensions: []
41
+ extra_rdoc_files:
42
+ - README.rdoc
43
+ files:
44
+ - lib/discrete_event.rb
45
+ - lib/discrete_event/events.rb
46
+ - lib/discrete_event/version.rb
47
+ - lib/discrete_event/event_queue.rb
48
+ - lib/discrete_event/simulation.rb
49
+ - lib/discrete_event/fake_rand.rb
50
+ - README.rdoc
51
+ - test/discrete_event/discrete_event_test.rb
52
+ homepage: http://github.com/jdleesmiller/discrete_event
53
+ licenses: []
54
+ post_install_message:
55
+ rdoc_options:
56
+ - --main
57
+ - README.rdoc
58
+ - --title
59
+ - discrete_event-1.0.0 Documentation
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ segments:
69
+ - 0
70
+ hash: -360047181
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ segments:
78
+ - 0
79
+ hash: -360047181
80
+ requirements: []
81
+ rubyforge_project: discrete_event
82
+ rubygems_version: 1.8.17
83
+ signing_key:
84
+ specification_version: 3
85
+ summary: Event-based discrete event simulation.
86
+ test_files:
87
+ - test/discrete_event/discrete_event_test.rb
88
+ has_rdoc: