shrewd 0.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.
- 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'
|