shrewd 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.rspec +2 -0
- data/Gemfile +11 -0
- data/README.md +416 -0
- data/Rakefile +0 -0
- data/lib/shrewd.rb +1 -0
- data/lib/shrewd/entities/event.rb +56 -0
- data/lib/shrewd/entities/process.rb +83 -0
- data/lib/shrewd/entities/variable.rb +58 -0
- data/lib/shrewd/simulation.rb +166 -0
- data/shrewd.gemspec +14 -0
- data/spec/integration/simple_spec.rb +145 -0
- data/spec/models/event_spec.rb +50 -0
- data/spec/models/process_spec.rb +51 -0
- data/spec/models/simulation_spec.rb +180 -0
- data/spec/models/variable_spec.rb +195 -0
- data/spec/spec_helper.rb +15 -0
- metadata +61 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1fb0db5cb8f287f162dd3832976cc02b28ff950a
|
4
|
+
data.tar.gz: 68db1125777aa925467df4a711ea63f3207690db
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6b54c374656059836ec26c9d92f7a279f64107bbb2fe605159973b4904b6eb74a5dab34a34fb79dab68518448ee88e7c45c228846d9e06bda3a3fd1ccab669c3
|
7
|
+
data.tar.gz: 876aa24eb1be625b5b9a131a6c1de3fb7c533ef8669e3c08a79738144cb56ce37ec0c4e6da6b0abc914eaee23c7217f02e79d99315ee40531672431b21d4a30c
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,416 @@
|
|
1
|
+
# Shrewd
|
2
|
+
Shrewd is a Ruby gem that helps you build discrete event simulations
|
3
|
+
using a event-based workflow.
|
4
|
+
|
5
|
+
## Installing Shrewd
|
6
|
+
Shrewd can be found on RubyGems at [this link](http://rubygems.org/gems/shrewd).
|
7
|
+
Install Shrewd with RubyGems:
|
8
|
+
|
9
|
+
```shell
|
10
|
+
gem install shrewd
|
11
|
+
```
|
12
|
+
|
13
|
+
Include in your Gemfile:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'shrewd'
|
17
|
+
```
|
18
|
+
|
19
|
+
## What is a Discrete Event Simulation?
|
20
|
+
A [discrete event simulation](https://en.wikipedia.org/wiki/Discrete_event_simulation)
|
21
|
+
is a method of simulating a system whose state fluctuates over time.
|
22
|
+
Discrete event simulations are known as such because, unlike continuous
|
23
|
+
simulations, the state of the simulation is modified at discrete points in time
|
24
|
+
rather than continuously. These simulations are used to model thousands of
|
25
|
+
different systems, including traffic congestion, manufacturing throughput,
|
26
|
+
aircraft wind resistance, economic policies, etc. Simulations can provide
|
27
|
+
insights into the effects of resource scarcity and constraints in ways that
|
28
|
+
other forms of statistical analysis cannot provide. They're also extremely fun
|
29
|
+
to model and visualize!
|
30
|
+
|
31
|
+
## How does a Discrete Event Simulation Work?
|
32
|
+
Discrete event simulations can be modeled in a number of different ways. This
|
33
|
+
gem uses an approach known as event-based modeling.
|
34
|
+
|
35
|
+
Event-based modeling involves several core concepts: Events, Processes, and
|
36
|
+
State Variables.
|
37
|
+
|
38
|
+
**State Variables** hold the state of the simulated system. They can represent
|
39
|
+
everything from the length of a queue for a bathroom in Disneyland, to the
|
40
|
+
number of available taxis in New York City, to a binary value representing a
|
41
|
+
factory in a state of downtime. The rationale for these variables and their
|
42
|
+
underlying values ultimately represent and define the system that you are
|
43
|
+
simulating.
|
44
|
+
|
45
|
+
The state of the system is modified in a simulation by **events**. Events occur at
|
46
|
+
discrete points in time and do two things: modify the state of the system by
|
47
|
+
modifying system state variables and initiate processes.
|
48
|
+
|
49
|
+
An example of an event might be an "arrival" event which represents a
|
50
|
+
customer "arriving" in line a retail store. At this point in time, the state of
|
51
|
+
the system is modified by incrementing the queue by 1.
|
52
|
+
|
53
|
+
In order to trigger future events, events often initiate **processes** that schedule
|
54
|
+
future events. A process can have boolean conditions that verify the process
|
55
|
+
should be initiated. These might verify, for example, that there is a cashier
|
56
|
+
able to handle a newly arrived customer. If the conditions pass, the process
|
57
|
+
schedules a given event at a given point in the future.
|
58
|
+
|
59
|
+
In the example of our "arrival" event above, an arrival event might have a
|
60
|
+
process that it initiates after incrementing the queue to schedule a "service"
|
61
|
+
event. If there is an available cashier, the customer will be helped
|
62
|
+
immediately by immediately scheduling a service event, which would decrement
|
63
|
+
the queue by 1. If there is not a cashier available, then the "service" event is
|
64
|
+
not scheduled by the process.
|
65
|
+
|
66
|
+
## Your First Simulation
|
67
|
+
Let's put these concepts into practice. In this example, we will model a
|
68
|
+
simple grocery store queue. For the sake of simplicity, there will be one queue
|
69
|
+
and the person at the front of the queue will be sent to the first available
|
70
|
+
employee (5 total), where the customer will be serviced over a period of 1 to 4
|
71
|
+
minutes (uniformly randomly distributed). Customers will arrive to the queue
|
72
|
+
every 0 to 5 minutes (uniformly randomly distributed).
|
73
|
+
|
74
|
+
First, we'll initialize the state variables for our simulation. The state of
|
75
|
+
this system is determined by the number of the customers in the queue and the
|
76
|
+
number of available employees.
|
77
|
+
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
# Create system variables
|
81
|
+
queue = Shrewd::Variable.new('Queue', '# of customers in the queue.', 0)
|
82
|
+
employees = Shrewd::Variable.new('Employees', '# of available employees.', 5)
|
83
|
+
variables = [queue, employees]
|
84
|
+
```
|
85
|
+
|
86
|
+
We will then initialize the events in the simulation. The main events of the
|
87
|
+
simulation are the arrival of customers to the queue every 0 to 5 minutes, the
|
88
|
+
start of a customer service, and finally the end of the customer service 1 to 4
|
89
|
+
minutes after the start.
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
# Create events
|
93
|
+
customer_arrival = Shrewd::Event.new('Customer Arrival',
|
94
|
+
'Customer arrives to grocery store.')
|
95
|
+
customer_arrival.action = Proc.new { |vars| vars[:queue].increment }
|
96
|
+
|
97
|
+
start = Shrewd::Event.new('Start Service', 'Start customer service.')
|
98
|
+
start.action = Proc.new do |vars|
|
99
|
+
vars[:queue].decrement
|
100
|
+
vars[:employees].decrement
|
101
|
+
end
|
102
|
+
|
103
|
+
finish = Shrewd::Event.new('Finish Service', 'Finish customer service.')
|
104
|
+
finish.action = Proc.new { |vars| vars[:employees].increment }
|
105
|
+
```
|
106
|
+
|
107
|
+
We now define the processes for the simulation. The processes include the
|
108
|
+
arrival process which schedules a new arrival event, a start process that
|
109
|
+
schedules the service start event, and a service process that schedules the
|
110
|
+
finish service event.
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
# Create processes
|
114
|
+
arrival = Shrewd::Process.new('Arrival', 'Arrival of a customer.')
|
115
|
+
arrival.delay = Proc.new { |vars| Random.new.rand * 5 }
|
116
|
+
arrival.event = customer_arrival
|
117
|
+
|
118
|
+
start_service = Shrewd::Process.new('Start Service',
|
119
|
+
'Start customer service.')
|
120
|
+
start_service.delay = Proc.new { |vars| 0 }
|
121
|
+
start_service.conditions = Proc.new do |vars|
|
122
|
+
vars[:employees] > 0 && vars[:queue] > 0
|
123
|
+
end
|
124
|
+
start_service.event = start
|
125
|
+
|
126
|
+
service_widget = Shrewd::Process.new('Service', 'Servicing a customer.')
|
127
|
+
service_widget.delay = Proc.new { |vars| Random.new.rand * 3 + 1 }
|
128
|
+
service_widget.event = finish
|
129
|
+
```
|
130
|
+
|
131
|
+
We then add these processes to the events that trigger them.
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
# Add processes to events
|
135
|
+
customer_arrival.processes << arrival << start_service
|
136
|
+
start.processes << service_widget
|
137
|
+
finish.processes << start_service
|
138
|
+
initial_processes = [ arrival ]
|
139
|
+
```
|
140
|
+
|
141
|
+
Finally, we can initialize and run the simulation for 10,000 minutes.
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
# Create simulation
|
145
|
+
simulation = Shrewd::Simulation.new(variables, initial_processes)
|
146
|
+
simulation.start(10000)
|
147
|
+
log = simulation.print_log
|
148
|
+
log.each do |entry|
|
149
|
+
puts "#{entry[:time]} - #{entry[:event].name}"
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
153
|
+
## Simulations
|
154
|
+
`Shrewd::Simulation` is the core of the Shrewd gem. Its instances
|
155
|
+
orchestrate simulations by providing an interface to build a discrete event
|
156
|
+
simulation, hold the state of the simulation during runtime, manage the
|
157
|
+
simulation during runtime, and track historical data of the simulation.
|
158
|
+
At any time, a simulation instance can only hold the state for a single
|
159
|
+
simulation. If you wish to run multiple concurrent simulations, you must
|
160
|
+
instantiate multiple simulation instances.
|
161
|
+
|
162
|
+
### Creating a Simulation
|
163
|
+
You can create a simulation by initializing a simulation object with an array
|
164
|
+
of the state variables that will be used during the simulation and an array of
|
165
|
+
the process that should be run at the start of the simulation.
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
sim_variables = [Shrewd::Variable instances...]
|
169
|
+
initial_processes = [Shrewd::Process instances...]
|
170
|
+
my_sim = Shrewd::Simulation.new(sim_variables, initial_processes)
|
171
|
+
```
|
172
|
+
|
173
|
+
### Modifying a Simulation
|
174
|
+
While you cannot add or remove state variables from a simulation once created,
|
175
|
+
you can add or remove processes by modifying the `initial_processes` attribute.
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
# add an initial process
|
179
|
+
my_sim.initial_processes << Shrewd::Process.new('New Process')
|
180
|
+
|
181
|
+
# remove an initial process
|
182
|
+
last_process = my_sim.initial_processes.pop
|
183
|
+
```
|
184
|
+
|
185
|
+
### Running a Simulation
|
186
|
+
You can start a simulation by calling the `start` method and providing the
|
187
|
+
length of simulation time that the simulation should run. For example, if you
|
188
|
+
were to run your simulation for 10,000 units of time you would start the
|
189
|
+
simulation with:
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
my_sim.start(10000)
|
193
|
+
```
|
194
|
+
|
195
|
+
The default simulation length, if a time length is not provided to the `start`
|
196
|
+
method, is 1000 time units.
|
197
|
+
|
198
|
+
### Printing the Simulation Results
|
199
|
+
You can see the results of the simulation by retrieving the simulation log.
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
my_sim.print_log
|
203
|
+
```
|
204
|
+
|
205
|
+
The log will have will have an array of event hashes of the format:
|
206
|
+
`{ event: event_object, time: event_time }`.
|
207
|
+
|
208
|
+
### Resetting a Simulation
|
209
|
+
If you wish to reset your simulation to run another instance, you can use the
|
210
|
+
reset method.
|
211
|
+
```ruby
|
212
|
+
my_sim.reset
|
213
|
+
```
|
214
|
+
This will clear the simulation logs, reset the clock to 0, and will return the
|
215
|
+
state variables to their initial state.
|
216
|
+
|
217
|
+
## Variables
|
218
|
+
`Shrewd::Variable` is a finite simulation state variable, which must be
|
219
|
+
initialized before the simulation is started. State variables are modified in
|
220
|
+
quantity by `Shrewd::Event` instances through the course of the simulation. They
|
221
|
+
are commonly used in the conditions for `Shrewd::Process` instances, determining
|
222
|
+
whether or not there are available resources to schedule the corresponding
|
223
|
+
simulation event (see how Processes work in the documentation below).
|
224
|
+
|
225
|
+
An example of a state variable might be the number of cashiers at a grocery
|
226
|
+
store. Once a customer arrives, a cashier become occupied for a time to help the
|
227
|
+
customer, thereby decreasing the number of the available cashier variables by
|
228
|
+
one. If customers arrive and there are no available cashiers to help them, the
|
229
|
+
customers are added to a queue. This queue would be represented by another
|
230
|
+
state variable that is incremented as customers arrive and decremented as
|
231
|
+
customers are served.
|
232
|
+
|
233
|
+
State variables can also be parameterized so that multiple instances of a
|
234
|
+
Variable can exist. This proves useful in, for example, a simulation of a
|
235
|
+
manufacturing plant which processes multiple different types of products. Each
|
236
|
+
product must under go the same events in the manufacturing system, e.g.
|
237
|
+
assembly, washing, testing, etc. It is much cleaner and easier in the case to
|
238
|
+
parameterize a single variable rather than create instance variables to
|
239
|
+
represent each different product.
|
240
|
+
|
241
|
+
### Creating a State Variable
|
242
|
+
You can create a state variable by passing in the name, description, and
|
243
|
+
initial values of the variables.
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
my_var = Shrewd::Variable.new("Queue", "A queue at a grocery store.", 0)
|
247
|
+
```
|
248
|
+
|
249
|
+
Only the name is required. If a value is not specified, the state variable will
|
250
|
+
default to 0.
|
251
|
+
|
252
|
+
### Modifying a State Variable
|
253
|
+
You can modify the name, description, and value of a state variable at any
|
254
|
+
point before or during the simulation.
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
# modify name
|
258
|
+
my_var.name = "Employees"
|
259
|
+
|
260
|
+
# modify description
|
261
|
+
my_var.description = "Number of available employees in a grocery store."
|
262
|
+
|
263
|
+
# modify value
|
264
|
+
my_var.value = 20
|
265
|
+
```
|
266
|
+
|
267
|
+
### Incrementing a State Variable
|
268
|
+
You can increment a state variable using the increment helper.
|
269
|
+
|
270
|
+
```ruby
|
271
|
+
# increment variable by 1
|
272
|
+
my_var.increment
|
273
|
+
|
274
|
+
# increment variable by 10
|
275
|
+
my_var.increment(10)
|
276
|
+
```
|
277
|
+
|
278
|
+
### Decrementing a State Variable
|
279
|
+
You can decrement a state variable using the decrement helper.
|
280
|
+
|
281
|
+
```ruby
|
282
|
+
# decrement variable by 1
|
283
|
+
my_var.decrement
|
284
|
+
|
285
|
+
# decrement variable by 10
|
286
|
+
my_var.decrement(10)
|
287
|
+
```
|
288
|
+
|
289
|
+
### Comparing State Variables
|
290
|
+
You can compare state variables with number values.
|
291
|
+
|
292
|
+
```ruby
|
293
|
+
if my_var > 10
|
294
|
+
puts "Greater than 10."
|
295
|
+
elsif my_var == 10
|
296
|
+
puts "Equal to 10."
|
297
|
+
end
|
298
|
+
```
|
299
|
+
|
300
|
+
## Events
|
301
|
+
`Shrewd::Event` denotes an event in time during a discrete event simulation.
|
302
|
+
Events can change the state of the system by modifying state variables.
|
303
|
+
during simulation runtime. Events are scheduled by `Shrewd::Process` instances.
|
304
|
+
A historical record of events for a simulation run is recorded by the
|
305
|
+
`Shrewd::Simulation` instance. After an event changes the system state by
|
306
|
+
modifying simulation state variables, it can execute one or many processes,
|
307
|
+
which in turn schedules future events.
|
308
|
+
|
309
|
+
### Creating an Event
|
310
|
+
You can create an event by providing a name, description, an array of
|
311
|
+
processes that this event will enqueue when it is triggered (after the action
|
312
|
+
is complete), and a ruby block that defines the action the event will take
|
313
|
+
when triggered.
|
314
|
+
|
315
|
+
```ruby
|
316
|
+
# define processes that this event will trigger
|
317
|
+
processes = [Shrewd::Process.new("Service")]
|
318
|
+
|
319
|
+
# define action that will occurr when event is triggered
|
320
|
+
action = proc { |vars| vars[:queue].increment }
|
321
|
+
|
322
|
+
# create event
|
323
|
+
my_event = Shrewd::Event.new("Arrival", "Description", processes, action)
|
324
|
+
```
|
325
|
+
|
326
|
+
|
327
|
+
The name attribute is the only required attribute to create an event.
|
328
|
+
The simulation state variables will be injected into the event action block.
|
329
|
+
The event action block is the primary way by which you modify the state of the
|
330
|
+
system throughout the simulation.
|
331
|
+
|
332
|
+
### Modifying an Event
|
333
|
+
The name, description, processes, and action attributes of events are all
|
334
|
+
modifiable before and event during the simulation. It is not recommended to
|
335
|
+
modify events during the simulation.
|
336
|
+
|
337
|
+
```ruby
|
338
|
+
# modify name
|
339
|
+
my_event.name = "Start Service"
|
340
|
+
|
341
|
+
# modify description
|
342
|
+
my_event.description = "Start the service for a customer in the queue."
|
343
|
+
|
344
|
+
# modify processes
|
345
|
+
my_event.processes = [Shrewd::Process.new("Finish")]
|
346
|
+
|
347
|
+
# modify action
|
348
|
+
my_event.action = proc do |vars|
|
349
|
+
vars[:queue].decrement
|
350
|
+
vars[:employees].decrement
|
351
|
+
end
|
352
|
+
```
|
353
|
+
|
354
|
+
## Processes
|
355
|
+
`Shrewd::Process` is a process that is ultimately triggered by an event. Each
|
356
|
+
process can have conditions that determine whether the process should carry out.
|
357
|
+
If it passes the conditions, the process schedules one `Shrewd::Event` in a
|
358
|
+
provided time in the future.
|
359
|
+
|
360
|
+
The conditions for a process can use state variables as well as randomized
|
361
|
+
numbers, and are defined by a Ruby block. The time in the future at which the
|
362
|
+
corresponding event should be schedule is also defined by a ruby block. This
|
363
|
+
allows you greater control over this time variable, allowing you to use random
|
364
|
+
distributions or define the time conditioned on the state of the system.
|
365
|
+
|
366
|
+
### Creating a Process
|
367
|
+
You can create a process by providing the name, description, the
|
368
|
+
`Shrewd::Event` to schedule, a ruby conditions block , and a ruby time block.
|
369
|
+
|
370
|
+
The conditions block should provide a boolean return. A value of `true` means
|
371
|
+
that the process passed the validations and can schedule the event. A value of
|
372
|
+
`false` will prevent the process from scheduling the event. If a conditions
|
373
|
+
block is not provided, then the process will always schedule the event.
|
374
|
+
|
375
|
+
The time block should return the value of time in the future at which the
|
376
|
+
future event should be scheduled. If not provided, the time block will default
|
377
|
+
to 0 (event will be scheduled immediately).
|
378
|
+
|
379
|
+
```ruby
|
380
|
+
# define event that this process will enqueue
|
381
|
+
event = Shrewd::Event.new("Start Service")
|
382
|
+
|
383
|
+
# define conditions - conditions must return boolean
|
384
|
+
conditions = proc { |vars| vars[:queue] > 0 }
|
385
|
+
|
386
|
+
# define time until event is enqueued - time must return a
|
387
|
+
time = proc { |vars| Random.new.rand > 0.5 }
|
388
|
+
|
389
|
+
# create the process
|
390
|
+
my_process = Shrewd::Process.new("Service", "Servicing a customer.", event, conditions, time)
|
391
|
+
```
|
392
|
+
|
393
|
+
The name attribute is the only required attribute to create a process. If an
|
394
|
+
event is not provided, then no events will be queued by the process.
|
395
|
+
|
396
|
+
### Modifying a Process
|
397
|
+
You can modify the name, description, event, conditions block, and time block
|
398
|
+
of a process at any point before or during a simulation. It is not recommended
|
399
|
+
to modify a process during a simulation.
|
400
|
+
|
401
|
+
```ruby
|
402
|
+
# modify name
|
403
|
+
my_process.name = "Finish"
|
404
|
+
|
405
|
+
# modify description
|
406
|
+
my_process.description = "Finishing a customer's service."
|
407
|
+
|
408
|
+
# modify event
|
409
|
+
my_process.event = Shrewd::Event.new("Finish Service")
|
410
|
+
|
411
|
+
# modify conditions block
|
412
|
+
my_process.conditions = proc { |vars| vars[:queue] > 0 }
|
413
|
+
|
414
|
+
# modify time block
|
415
|
+
my_process.time = proc { |vars| Random.new.rand < 0.1 }
|
416
|
+
```
|
data/Rakefile
ADDED
File without changes
|
data/lib/shrewd.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'shrewd/simulation'
|