discrete_event 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 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: