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.
@@ -0,0 +1,56 @@
1
+ # Event
2
+ #
3
+ # An Event changes the state of one or more simulation resources.
4
+ # Each event also has one or more processes.
5
+ #
6
+ # e.g. When modeling a queue, an Event would exist that denotes the
7
+ # arrival of a new customer, thereby changing the state of the system
8
+ # by incrementing the queue by 1.
9
+ #
10
+ # Instance Variables:
11
+ #
12
+ # - @name Name of the event.
13
+ # e.g. @name = 'Customer Arrival'
14
+ #
15
+ # - @description Description of the event.
16
+ # e.g. @description = 'The arrival of a single customer
17
+ # to the queue.'
18
+ #
19
+ # - @processes An array of Shrewd::Process objects that are to be
20
+ # executed following this event. These processes will
21
+ # schedule future events.
22
+ # e.g. @processes = [ Shrewd::Process.new('Process') ]
23
+ #
24
+ # - @action A proc that is called to modify system and local state.
25
+ # This is the method by which events modify the system.
26
+ # The system state variables are injected.
27
+ # e.g. @action = Proc.new do |variables|
28
+ # variables[:queue].decrement
29
+ # variables[:employees].decrement
30
+ # end
31
+
32
+ class Shrewd::Event
33
+ attr_accessor :name, :description, :processes, :action
34
+
35
+ # Initialize the event
36
+ #
37
+ # Input:
38
+ # - name name of the event
39
+ # - description description of the event (optional)
40
+ # - processes processes to be executed after event
41
+ # - action proc that changes the state of system variables
42
+ def initialize(name, description = "", processes = [], action = nil)
43
+ @name = name
44
+ @description = description
45
+ @processes = processes
46
+ @action = action
47
+ end
48
+
49
+ # Trigger the event's action
50
+ #
51
+ # Input:
52
+ # - state_variables system state variables
53
+ def trigger(state_variables)
54
+ @action.call(state_variables) if @action
55
+ end
56
+ end
@@ -0,0 +1,83 @@
1
+ # Process
2
+ #
3
+ # A Process is triggered by an Event to schedule another Event.
4
+ # Each Process schedules an event in a given (or randomized) period of time.
5
+ # A Process can have conditions that determine if the associated Event
6
+ # should be queued.
7
+ #
8
+ # e.g. When modeling customer arrivals to a retail store, each customer
9
+ # arrival Event would create a process that schedules a service Event,
10
+ # in which a employee would service the customer.
11
+ #
12
+ # Instance Variables:
13
+ #
14
+ # - @name Name of the process.
15
+ # e.g. @name = 'Service Customer'
16
+ #
17
+ # - @description Description of the process.
18
+ # e.g. @description = 'An employee servicing a customer.'
19
+ #
20
+ # - @event Event that the process triggers after checking conditions.
21
+ # e.g. @event = Shrewd::Event.new('Target')
22
+ #
23
+ # - @conditions A proc that returns a boolean. If the result is true then
24
+ # the @event is added to the event queue. The proc is
25
+ # provided the system state variables.
26
+ # e.g. @conditions = Proc.new do |variables|
27
+ # if variables[:employees] > 0
28
+ # true
29
+ # else
30
+ # false
31
+ # end
32
+ # end
33
+ #
34
+ # - @delay A proc that returns a number. This number determines how
35
+ # far in the future the event should be scheduled to
36
+ # execute (relative to current time).
37
+ # e.g. @delay = Proc.new do |variables|
38
+ # if variables[:queue] > 5
39
+ # 15
40
+ # else
41
+ # 10
42
+ # end
43
+ # end
44
+
45
+ class Shrewd::Process
46
+ attr_accessor :name, :description, :event,
47
+ :conditions, :delay
48
+
49
+ # Initialize the Process
50
+ #
51
+ # Input:
52
+ # - name name of the process
53
+ # - description description of the process
54
+ # - event event that this process will trigger
55
+ # - conditions proc that returns bool to validate event scheduling
56
+ # - delay proc that returns the time to schedule the event
57
+ def initialize(name, description = '', event = nil,
58
+ conditions = nil, delay = nil)
59
+ @name = name
60
+ @description = description
61
+ @event = event
62
+ @conditions = conditions
63
+ @delay = delay
64
+ end
65
+
66
+ # Validate the conditions to determine if event should be scheduled
67
+ def validated?(state_variables)
68
+ if @conditions
69
+ @conditions.call(state_variables)
70
+ else
71
+ true
72
+ end
73
+ end
74
+
75
+ # Retrieve the calculated delay time
76
+ def delay_time(state_variables)
77
+ if @delay
78
+ @delay.call(state_variables)
79
+ else
80
+ 0
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,58 @@
1
+ # Variable
2
+ #
3
+ # A Variable represents a state variable of the simulation system.
4
+ # Each Variable has a finite quantity and can be incremented, decremented, or
5
+ # assigned over the course of the simulation. A Variable can also have a boolean
6
+ # value.
7
+ #
8
+ # e.g. For a supermarket checkout simulation, a finite Variable
9
+ # would be the number of cashiers.
10
+ #
11
+ # Instance Variables:
12
+ # - @value integer value of variable
13
+ # e.g. @value = 5
14
+ # - @name name of the variable
15
+ # e.g. @name = 'Queue'
16
+ # - @description description of the variable
17
+ # e.g. @description = 'Number of customers in line.'
18
+
19
+ class Shrewd::Variable
20
+ include Comparable
21
+ attr_accessor :name, :description, :value
22
+
23
+ # Initialize the variable
24
+ #
25
+ # Input:
26
+ # - name name of the variable
27
+ # - description description of the variable (optional)
28
+ # - default default value for variable (optional)
29
+ def initialize(name, description = '', default = 0)
30
+ @value = default
31
+ @name = name
32
+ @description = description
33
+ end
34
+
35
+ # Increment the value of the variable
36
+ #
37
+ # Input:
38
+ # - amount the amount to increment the variable (optional)
39
+ def increment(amount = 1)
40
+ @value += amount
41
+ end
42
+
43
+ # Decrement the value of the variable
44
+ #
45
+ # Input:
46
+ # - amount the amount to decrement the variable (optional)
47
+ def decrement(amount = 1)
48
+ @value -= amount
49
+ end
50
+
51
+ # Compare variable value to number
52
+ #
53
+ # Input:
54
+ # - val the value to be compared with
55
+ def <=>(val)
56
+ @value <=> val
57
+ end
58
+ end
@@ -0,0 +1,166 @@
1
+ # Simulation
2
+ #
3
+ # A Simulation is a container for a simulation that holds the current
4
+ # state of the system via state variables as well as holds the pending
5
+ # and historical events of the simulation.
6
+ #
7
+ # A simulation can be started and stopped with a Simulation instance.
8
+ #
9
+ # One or multiple initial Processes must be provided to the Simulation
10
+ # which will started at time 0.
11
+ #
12
+ # A Simulation can also be stepped through once stopped to observe the system
13
+ # state changes one event at a time.
14
+ #
15
+ # Instance Variables:
16
+ # - @variables a hash of system variables whose key is the snake case
17
+ # name of the provided variable, and the value is the
18
+ # variable
19
+ #
20
+ # e.g. @variables = [ Shrewd::Variable.new('Queue')]
21
+ #
22
+ # - @initial_processes the initial process
23
+ # e.g. @initial_processes = [Shrewd::Process.new('proc')]
24
+ #
25
+ # - @queue array of hashes of events to be processed and their
26
+ # respective times of execution
27
+ # e.g. @queue = [ { time: @clock,
28
+ # event: Shrewd::Event.new('Event') }]
29
+ #
30
+ # - @log array of events and their time of execution
31
+ # e.g. @log << { event: Shrewd::Event.new, time: 15 }
32
+ #
33
+ # - @clock int value representing current clock time
34
+ # e.g. @clock = 5
35
+ module Shrewd
36
+ class Simulation
37
+ attr_accessor :initial_processes
38
+
39
+ # Initialize the simulation
40
+ #
41
+ # Input:
42
+ # - variables hash of the initial state of simulation state variables
43
+ # - initial initial process that will be triggered
44
+ #
45
+ # Return:
46
+ # - [Shrewd::Simulation]: new simulation instance
47
+ def initialize(variables = [], initial = nil)
48
+ @variables = {}
49
+ @initial_processes = initial
50
+ @queue = []
51
+ @log = []
52
+ @clock = 0
53
+
54
+ # assign variables
55
+ variables.each do |variable|
56
+ name = variable.name.downcase.gsub(/\s+/,"_").to_sym
57
+ @variables[name] = variable
58
+ end
59
+
60
+ # save initial state
61
+ @initial_state = @variables
62
+ end
63
+
64
+ # Start the simulation with a default length of 1000
65
+ def start(length = 1000)
66
+ create_initial_event
67
+
68
+ while @queue.any?
69
+ # Set the clock
70
+ event = @queue.shift
71
+ @clock = event[:time].to_i
72
+ break if @clock > length
73
+
74
+ # Retrieve current event
75
+ current_event = event[:event]
76
+ current_event.trigger(@variables)
77
+
78
+ # Queue events spawned by processes
79
+ current_event.processes.each do |process|
80
+ if process.validated?(@variables)
81
+ process_time = process.delay_time(@variables)
82
+ add_to_queue({ event: process.event, time: @clock + process_time })
83
+ end
84
+ end
85
+
86
+ # Log the event
87
+ @log << { event: current_event, time: @clock }
88
+ end
89
+ end
90
+
91
+ # Print a log of the simulation events
92
+ def print_log
93
+ @log
94
+ end
95
+
96
+ # Reset the simulation
97
+ def reset
98
+ @log = []
99
+ @queue = []
100
+ @clock = 0
101
+ @variables = @initial_state
102
+ end
103
+
104
+ private
105
+ # Create the 'start' event of the simulation. The start event will call
106
+ # the initial process.
107
+ def create_initial_event
108
+ name = 'Start'
109
+ description = 'The first event of the simulation.'
110
+
111
+ initial_event = Shrewd::Event.new(name, description)
112
+ initial_event.processes = @initial_processes
113
+ add_to_queue({ event: initial_event, time: @count })
114
+ end
115
+
116
+ # Add event to queue according to time of event.
117
+ #
118
+ # Input:
119
+ # - event the event/time hash to add to the queue
120
+ def add_to_queue(event)
121
+ size = @queue.size
122
+ return @queue << event if size == 0
123
+
124
+ # binary search
125
+ index = search(event, 0, @queue.size - 1)
126
+ if index > @queue.size - 1
127
+ @queue.push(event)
128
+ else
129
+ @queue.insert(index, event)
130
+ end
131
+ end
132
+
133
+ # Binary search to identify location in queue to add event, based on
134
+ # the time the event will be triggered.
135
+ #
136
+ # Input:
137
+ # - event the event/time hash to add to the queue
138
+ # - lo low index in the search
139
+ # - hi high index in the search
140
+ def search(event, lo, hi)
141
+ # Low and high values
142
+ low_time = @queue[lo][:time]
143
+ high_time = @queue[hi][:time]
144
+
145
+ # Event time is higher or lower than bounds
146
+ return lo if low_time > event[:time]
147
+ return hi + 1 if high_time <= event[:time]
148
+ return lo if hi == lo
149
+
150
+ # Midpoint
151
+ midpoint = ((lo + hi) / 2).floor
152
+
153
+ # Compare event to midpoint
154
+ if event[:time] < @queue[midpoint][:time]
155
+ search(event, 0, midpoint - 1)
156
+ else
157
+ search(event, midpoint + 1, @queue.size - 1)
158
+ end
159
+ end
160
+ end
161
+ end
162
+
163
+ # require simulation entities
164
+ require_relative 'entities/event'
165
+ require_relative 'entities/process'
166
+ require_relative 'entities/variable'
data/shrewd.gemspec ADDED
@@ -0,0 +1,14 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'shrewd'
3
+ s.version = '0.0.0'
4
+ s.date = '2015-10-23'
5
+ s.summary = 'A gem for event-based discrete event simulations.'
6
+ s.description = 'A gem for that provides an easy interface to build ' +
7
+ 'and run discrete event simulations. Shrewd uses an ' +
8
+ 'event-based approach to designing simulations.'
9
+ s.authors = 'Sherbie'
10
+ s.files = `git ls-files`.split("\n")
11
+ s.require_path = 'lib'
12
+ s.homepage = 'http://rubygems.org/gems/shrewd'
13
+ s.license = 'MIT'
14
+ end
@@ -0,0 +1,145 @@
1
+ # SimpleSpec
2
+ #
3
+ # Simulation integrated tests through examples.
4
+
5
+ require 'shrewd/simulation'
6
+
7
+ describe "simulation" do
8
+
9
+ # Basic manufacturing example
10
+ #
11
+ # Uses a widget manufacturing example to test a simple simulation. Widgets
12
+ # will arrive into the system at a uniformly randomized time. An available
13
+ # machine will service each incoming widget. If there are not enough available
14
+ # machines at the time a widget arrives, then the widget will sit in the
15
+ # queue. The simulation will be run for a short amount of time, and its
16
+ # results verified.
17
+ context "manufacturing" do
18
+ before :all do
19
+ # Create system variables
20
+ queue = Shrewd::Variable.new('Queue')
21
+ machines = Shrewd::Variable.new('Machines', 'Machines', 5)
22
+ variables = [queue, machines]
23
+
24
+ # Create events
25
+ widget_arrival = Shrewd::Event.new('Widget Arrival',
26
+ 'Widget arrives to manufacturer.')
27
+ widget_arrival.action = Proc.new do |variables|
28
+ variables[:queue].increment
29
+ end
30
+
31
+ start = Shrewd::Event.new('Start Service', 'Start widget service')
32
+ start.action = Proc.new do |variables|
33
+ variables[:queue].decrement
34
+ variables[:machines].decrement
35
+ end
36
+
37
+ finish = Shrewd::Event.new('Finish Service', 'Finish widget service')
38
+ finish.action = Proc.new do |variables|
39
+ variables[:machines].increment
40
+ end
41
+
42
+ # create processes
43
+ arrival = Shrewd::Process.new('Arrival', 'Arrival of a Widget')
44
+ arrival.delay = Proc.new { |variables| 10 }
45
+ arrival.event = widget_arrival
46
+
47
+ start_service = Shrewd::Process.new('Start Service',
48
+ 'Start Widget Service.')
49
+ start_service.delay = Proc.new { |variables| 0 }
50
+ start_service.conditions = Proc.new do |variables|
51
+ variables[:machines] > 0 && variables[:queue] > 0
52
+ end
53
+ start_service.event = start
54
+
55
+ service_widget = Shrewd::Process.new('Service', 'Servicing a Widget')
56
+ service_widget.delay = Proc.new { |variables| 15 }
57
+ service_widget.event = finish
58
+
59
+ # Add processes to events
60
+ widget_arrival.processes << arrival << start_service
61
+ start.processes << service_widget
62
+ finish.processes << start_service
63
+ initial_processes = [ arrival ]
64
+
65
+ # Create simulation
66
+ @simulation = Shrewd::Simulation.new(variables, initial_processes)
67
+ end
68
+
69
+ it "creates a simulation" do
70
+ expect(@simulation).to be_an_instance_of Shrewd::Simulation
71
+ end
72
+
73
+ it "initializes processes" do
74
+ expect(@simulation.initial_processes).to be_an_instance_of Array
75
+ @simulation.initial_processes.each do |process|
76
+ expect(process).to be_an_instance_of Shrewd::Process
77
+ end
78
+ end
79
+
80
+ describe "when run" do
81
+ before :all do
82
+ @simulation_length = 1000
83
+ @simulation.start(@simulation_length)
84
+ @log = @simulation.print_log
85
+ end
86
+
87
+ it "does not last longer than simulation length" do
88
+ expect(@log.last[:time]).to be <= @simulation_length
89
+ end
90
+
91
+ it "has more or equal widgets starts than finishes" do
92
+ starts = 0
93
+ finishes = 0
94
+
95
+ @log.each do |entry|
96
+ if entry[:event].name == 'Start Service'
97
+ starts += 1
98
+ elsif entry[:event].name == 'Finish Service'
99
+ finishes += 1
100
+ end
101
+
102
+ expect(starts).to be >= finishes
103
+ end
104
+
105
+ expect(starts).to be > 0
106
+ expect(finishes).to be > 0
107
+ expect(starts).to be >= finishes
108
+ end
109
+
110
+ it "has more or equal widget arrivals than starts" do
111
+ starts = 0
112
+ arrivals = 0
113
+
114
+ @log.each do |entry|
115
+ if entry[:event].name == 'Start Service'
116
+ starts += 1
117
+ elsif entry[:event].name == 'Widget Arrival'
118
+ arrivals += 1
119
+ end
120
+
121
+ expect(arrivals).to be >= starts
122
+ end
123
+
124
+ expect(arrivals).to be > 0
125
+ expect(starts).to be > 0
126
+ expect(arrivals).to be >= starts
127
+ end
128
+
129
+ it "executes events in order" do
130
+ clock = 0
131
+
132
+ @log.each do |entry|
133
+ expect(entry[:time]).to be >= clock
134
+ clock = entry[:time]
135
+ end
136
+ end
137
+
138
+ after :all do
139
+ @log.each do |entry|
140
+ puts "time #{entry[:time]} - #{entry[:event].name}"
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end