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 +173 -0
- data/lib/discrete_event/event_queue.rb +285 -0
- data/lib/discrete_event/events.rb +94 -0
- data/lib/discrete_event/fake_rand.rb +73 -0
- data/lib/discrete_event/simulation.rb +37 -0
- data/lib/discrete_event/version.rb +6 -0
- data/lib/discrete_event.rb +22 -0
- data/test/discrete_event/discrete_event_test.rb +257 -0
- metadata +88 -0
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,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:
|