simplekit 0.4.5 → 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.
- checksums.yaml +5 -5
- data/demos/CancelQ.rb +22 -0
- data/demos/MMk.rb +76 -0
- data/demos/MMk_reference_output.txt +1475 -0
- data/demos/MMk_shutdown.rb +80 -0
- data/demos/sptf.rb +86 -0
- data/lib/priority_queue.rb +5 -0
- data/lib/simplekit.rb +90 -24
- data/simplekit.gemspec +8 -3
- metadata +9 -4
@@ -0,0 +1,80 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../lib/simplekit'
|
4
|
+
|
5
|
+
# Demonstration model of an M/M/k queueing system. There are k servers
|
6
|
+
# and both the arrival and service processes are memoryless (exponential).
|
7
|
+
class MMk
|
8
|
+
include SimpleKit
|
9
|
+
|
10
|
+
# Constructor - initializes the model parameters.
|
11
|
+
# param: arrival_rate - The rate at which customers arrive to the system.
|
12
|
+
# param: service_rate - The rate at which individual servers serve.
|
13
|
+
# param: max_servers - The total number of servers in the system.
|
14
|
+
def initialize(arrival_rate, service_rate, max_servers)
|
15
|
+
@arrival_rate = arrival_rate
|
16
|
+
@service_rate = service_rate
|
17
|
+
@max_servers = max_servers
|
18
|
+
end
|
19
|
+
|
20
|
+
# Initialize the model state and schedule any necessary events.
|
21
|
+
# Note that this particular model will terminate based on
|
22
|
+
# time by scheduling a halt 100 time units in the future.
|
23
|
+
def init
|
24
|
+
@num_available_servers = @max_servers
|
25
|
+
@q_length = 0
|
26
|
+
schedule(:arrival, 0.0)
|
27
|
+
schedule(:close_doors, 100.0)
|
28
|
+
dump_state('init')
|
29
|
+
end
|
30
|
+
|
31
|
+
# An arrival event increments the queue length, schedules the next
|
32
|
+
# arrival, and schedules a begin_service event if a server is available.
|
33
|
+
def arrival
|
34
|
+
@q_length += 1
|
35
|
+
schedule(:arrival, exponential(@arrival_rate))
|
36
|
+
schedule(:begin_service, 0.0) if @num_available_servers > 0
|
37
|
+
dump_state('arrival')
|
38
|
+
end
|
39
|
+
|
40
|
+
# Start service for the first customer in line, removing that
|
41
|
+
# customer from the queue and utilizing one of the available servers.
|
42
|
+
# An end_service will be scheduled.
|
43
|
+
def begin_service
|
44
|
+
@q_length -= 1
|
45
|
+
@num_available_servers -= 1
|
46
|
+
schedule(:end_service, exponential(@service_rate))
|
47
|
+
dump_state('begin svc')
|
48
|
+
end
|
49
|
+
|
50
|
+
# Frees up an available server, and schedules a begin_service if
|
51
|
+
# anybody is waiting in line.
|
52
|
+
def end_service
|
53
|
+
@num_available_servers += 1
|
54
|
+
schedule(:begin_service, 0.0) if @q_length > 0
|
55
|
+
dump_state('end svc')
|
56
|
+
end
|
57
|
+
|
58
|
+
def close_doors
|
59
|
+
cancel(:arrival)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Exponential random variate generator.
|
63
|
+
# param: rate - The rate (= 1 / mean) of the distribution.
|
64
|
+
# returns: A realization of the specified distribution.
|
65
|
+
def exponential(rate)
|
66
|
+
-Math.log(rand) / rate
|
67
|
+
end
|
68
|
+
|
69
|
+
# A report mechanism which dumps the time, current event, and values
|
70
|
+
# of the state variables to the console.
|
71
|
+
# param: event - The name of the event which invoked this method.
|
72
|
+
def dump_state(event)
|
73
|
+
printf "Time: %8.3f\t%10s - Q: %d\tServers Available: %d\n",
|
74
|
+
model_time, event, @q_length, @num_available_servers
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Instantiate an MMk object with a particular parameterization and run it.
|
79
|
+
srand 7_654_321
|
80
|
+
MMk.new(4.5, 1.0, 5).run
|
data/demos/sptf.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../lib/simplekit'
|
4
|
+
|
5
|
+
# Create a Customer Struct that will store arrival times and processing times.
|
6
|
+
Customer = Struct.new(:arrival_time, :processing_time) do
|
7
|
+
include Comparable
|
8
|
+
# rank customers by their processing times, smallest first.
|
9
|
+
def <=>(other)
|
10
|
+
processing_time <=> other.processing_time
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Demonstration model of a shortest-processing-time-first queueing system.
|
15
|
+
# There are k servers and both the arrival and service processes could
|
16
|
+
# be anything.
|
17
|
+
class SPTF
|
18
|
+
include SimpleKit
|
19
|
+
|
20
|
+
# Constructor - initializes the model parameters.
|
21
|
+
# param: arrivalRate - The rate at which customers arrive to the system.
|
22
|
+
# param: serviceRate - The rate at which individual servers serve.
|
23
|
+
# param: maxServers - The total number of servers in the system.
|
24
|
+
# param: closeTime - The time the server would like to shut down.
|
25
|
+
def initialize(arrivalRate, serviceRate, maxServers, closeTime)
|
26
|
+
@arrivalRate = arrivalRate
|
27
|
+
@serviceRate = serviceRate
|
28
|
+
@maxServers = maxServers
|
29
|
+
@closeTime = closeTime
|
30
|
+
end
|
31
|
+
|
32
|
+
# Initialize the model state and schedule any necessary events.
|
33
|
+
def init
|
34
|
+
@numAvailableServers = @maxServers
|
35
|
+
@q = PriorityQueue.new
|
36
|
+
schedule(:arrival, 0.0)
|
37
|
+
schedule(:close_doors, @closeTime)
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
# An arrival event generates a customer and their associated service
|
42
|
+
# time, adds the customer to the queue, schedules the next arrival,
|
43
|
+
# and schedules a beginService event if a server is available.
|
44
|
+
def arrival
|
45
|
+
@q.push Customer.new(model_time, exponential(@serviceRate))
|
46
|
+
schedule(:arrival, exponential(@arrivalRate))
|
47
|
+
schedule(:beginService, 0.0) if (@numAvailableServers > 0)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Start service for the first customer in line, removing that
|
51
|
+
# customer from the queue and utilizing one of the available
|
52
|
+
# servers. An endService will be scheduled. Report the current
|
53
|
+
# time and how long this customer spent in line.
|
54
|
+
def beginService
|
55
|
+
current_customer = @q.pop
|
56
|
+
@numAvailableServers -= 1
|
57
|
+
schedule(:endService, current_customer.processing_time)
|
58
|
+
printf "%f,%f\n", model_time, model_time - current_customer.arrival_time
|
59
|
+
end
|
60
|
+
|
61
|
+
# Frees up an available server, and schedules a beginService if
|
62
|
+
# anybody is waiting in line. If the line is empty and it's after
|
63
|
+
# the desired closing time, halt the simulation.
|
64
|
+
def endService
|
65
|
+
@numAvailableServers += 1
|
66
|
+
unless @q.empty?
|
67
|
+
schedule(:beginService, 0.0)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Commence shutdown by denying the next :arrival
|
72
|
+
def close_doors
|
73
|
+
cancel :arrival
|
74
|
+
end
|
75
|
+
|
76
|
+
# Exponential random variate generator.
|
77
|
+
# param: rate - The rate (= 1 / mean) of the distribution.
|
78
|
+
# returns: A realization of the specified distribution.
|
79
|
+
def exponential(rate)
|
80
|
+
-Math.log(rand) / rate
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Instantiate an SPTF object with a particular parameterization and run it.
|
85
|
+
srand(9876543) # set seed for repeatability
|
86
|
+
SPTF.new(6, 1.0, 5, 20.0).run
|
data/lib/priority_queue.rb
CHANGED
@@ -4,6 +4,7 @@ class PriorityQueue
|
|
4
4
|
clear
|
5
5
|
end
|
6
6
|
|
7
|
+
# Push +element+ onto the priority queue.
|
7
8
|
def <<(element)
|
8
9
|
@elements << element
|
9
10
|
# bubble up the element that we just added
|
@@ -12,11 +13,13 @@ class PriorityQueue
|
|
12
13
|
|
13
14
|
alias push <<
|
14
15
|
|
16
|
+
# Inspect the element at the head of the queue.
|
15
17
|
def peek
|
16
18
|
# the first element will always be the min, because of the heap constraint
|
17
19
|
@elements[1]
|
18
20
|
end
|
19
21
|
|
22
|
+
# Remove and return the next element from the queue, determined by priority.
|
20
23
|
def pop
|
21
24
|
# remove the last element of the list
|
22
25
|
min = @elements[1]
|
@@ -26,10 +29,12 @@ class PriorityQueue
|
|
26
29
|
min
|
27
30
|
end
|
28
31
|
|
32
|
+
# Reset the priority queue to empty.
|
29
33
|
def clear
|
30
34
|
@elements = [nil]
|
31
35
|
end
|
32
36
|
|
37
|
+
# Return a boolean indicating whether the queue is empty or not
|
33
38
|
def empty?
|
34
39
|
@elements.length < 2
|
35
40
|
end
|
data/lib/simplekit.rb
CHANGED
@@ -1,15 +1,18 @@
|
|
1
|
+
require 'set'
|
1
2
|
require_relative 'priority_queue'
|
2
3
|
|
3
4
|
# The +SimpleKit+ module provides basic event scheduling capabilities.
|
4
5
|
#
|
5
6
|
# Including +SimpleKit+ in your simulation model gives you methods +:run+,
|
6
|
-
# +:model_time+, +:schedule+, and +:halt+ as mixins.
|
7
|
-
#
|
8
|
-
# All but +:run+ are delegated to the +EventScheduler+ class.
|
7
|
+
# +:model_time+, +:schedule+, +:cancel+, +:cancel_all+, and +:halt+ as mixins.
|
8
|
+
# <b>DO NOT</b> create your own implementations of methods with these names
|
9
|
+
# in your model. All but +:run+ are delegated to the +EventScheduler+ class.
|
9
10
|
module SimpleKit
|
10
11
|
# The set of module methods to be passed to the EventScheduler
|
11
12
|
# if not found in the model class.
|
12
|
-
DELEGATED_METHODS = [
|
13
|
+
DELEGATED_METHODS = %i[
|
14
|
+
model_time schedule cancel cancel_all halt
|
15
|
+
].freeze
|
13
16
|
|
14
17
|
# Run your model by creating a new +EventScheduler+ and invoking its
|
15
18
|
# +run+ method.
|
@@ -21,11 +24,7 @@ module SimpleKit
|
|
21
24
|
# If a method doesn't exist in the model class, try to delegate it
|
22
25
|
# to +EventScheduler+.
|
23
26
|
def method_missing(name, *args)
|
24
|
-
|
25
|
-
@my_sim.send(name, *args)
|
26
|
-
else
|
27
|
-
super
|
28
|
-
end
|
27
|
+
DELEGATED_METHODS.include?(name) ? @my_sim.send(name, *args) : super
|
29
28
|
end
|
30
29
|
|
31
30
|
# Class +EventScheduler+ provides the computation engine for a
|
@@ -43,6 +42,7 @@ module SimpleKit
|
|
43
42
|
def initialize(the_model)
|
44
43
|
@user_model = the_model
|
45
44
|
@event_list = PriorityQueue.new
|
45
|
+
@cancel_set = {}
|
46
46
|
end
|
47
47
|
|
48
48
|
# Add an event to the pending events list.
|
@@ -51,24 +51,53 @@ module SimpleKit
|
|
51
51
|
# - +event+ -> the event to be scheduled.
|
52
52
|
# - +delay+ -> the amount of time which should elapse before
|
53
53
|
# the event executes.
|
54
|
-
# - +args+ ->
|
55
|
-
# at invocation time.
|
56
|
-
|
54
|
+
# - +args+ -> zero or more named arguments to pass to the event
|
55
|
+
# at invocation time. These should be specified with labels, and
|
56
|
+
# consequently they can be placed in any order.
|
57
|
+
def schedule(event, delay, **args)
|
57
58
|
raise 'Model scheduled event with negative delay.' if delay < 0
|
58
|
-
@event_list.push EventNotice.new(event, @model_time
|
59
|
+
@event_list.push EventNotice.new(event, @model_time, delay, args)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Cancel an individual occurrence of event type +event+.
|
63
|
+
# If no +args+ are provided, the next scheduled occurrence of +event+
|
64
|
+
# is targeted. If a subset of the event's +args+ is provided, they must
|
65
|
+
# all be a match with the corresponding +args+ of the scheduled event
|
66
|
+
# in order for the cancellation to apply, but +args+ which are not
|
67
|
+
# specified do not affect the target event matching.
|
68
|
+
def cancel(event, **args)
|
69
|
+
@cancel_set[event] ||= Set.new
|
70
|
+
@cancel_set[event].add(args.empty? ? nil : args)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Cancel all currently scheduled events of type +event+.
|
74
|
+
def cancel_all(event)
|
75
|
+
if event
|
76
|
+
PriorityQueue.new.tap do |pq|
|
77
|
+
while (event_notice = @event_list.pop)
|
78
|
+
pq.push event_notice unless event_notice.event == event
|
79
|
+
end
|
80
|
+
@event_list = pq
|
81
|
+
end
|
82
|
+
end
|
59
83
|
end
|
60
84
|
|
61
85
|
# Start execution of a model. The simulation +model_time+ is initialized
|
62
86
|
# to zero and the model is initialized via the mandatory +init+ method.
|
63
|
-
# Then loop while events are pending on the +event_list+. The event with
|
64
|
-
#
|
65
|
-
#
|
87
|
+
# Then loop while events are pending on the +event_list+. The event with the
|
88
|
+
# smallest time is popped, +model_time+ is updated to the event time, and
|
89
|
+
# the event method is invoked with the +args+, if any, set by +schedule+.
|
66
90
|
def run
|
67
91
|
@model_time = 0.0
|
68
92
|
@user_model.init
|
69
93
|
while (current_event = @event_list.pop)
|
94
|
+
next if should_cancel?(current_event)
|
70
95
|
@model_time = current_event.time
|
71
|
-
|
96
|
+
if current_event.args.empty?
|
97
|
+
@user_model.send(current_event.event)
|
98
|
+
else
|
99
|
+
@user_model.send(current_event.event, current_event.args)
|
100
|
+
end
|
72
101
|
end
|
73
102
|
end
|
74
103
|
|
@@ -77,14 +106,51 @@ module SimpleKit
|
|
77
106
|
def halt
|
78
107
|
@event_list.clear
|
79
108
|
end
|
80
|
-
end
|
81
109
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
def
|
87
|
-
|
110
|
+
private
|
111
|
+
|
112
|
+
# Private method that returns a boolean to determine if +event_notice+
|
113
|
+
# represents an event subject to cancellation.
|
114
|
+
def should_cancel?(event_notice)
|
115
|
+
e = event_notice.event
|
116
|
+
if @cancel_set.key? e
|
117
|
+
if @cancel_set[e].include? nil
|
118
|
+
@cancel_set[e].delete nil
|
119
|
+
@cancel_set.delete e if @cancel_set[e].empty?
|
120
|
+
return true
|
121
|
+
else
|
122
|
+
for hsh in @cancel_set[e] do
|
123
|
+
next unless event_notice.args >= hsh
|
124
|
+
@cancel_set[e].delete hsh
|
125
|
+
@cancel_set.delete e if @cancel_set[e].empty?
|
126
|
+
return true
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
false
|
131
|
+
end
|
132
|
+
|
133
|
+
# This is a private helper class for the EventScheduler class.
|
134
|
+
# Users should never try to access this directly.
|
135
|
+
class EventNotice
|
136
|
+
attr_reader :event, :time, :time_stamp, :priority, :args
|
137
|
+
|
138
|
+
def initialize(event, time, delay, args)
|
139
|
+
@event = event
|
140
|
+
@time_stamp = time
|
141
|
+
@time = time + delay
|
142
|
+
@args = args
|
143
|
+
@priority = @args && @args.key?(:priority) ? @args.delete(:priority) : 10
|
144
|
+
end
|
145
|
+
|
146
|
+
include Comparable
|
147
|
+
# Compare EventNotice objects for ordering, first by time,
|
148
|
+
# breaking ties using priority.
|
149
|
+
def <=>(other)
|
150
|
+
(time <=> other.time).tap do |outcome|
|
151
|
+
return priority <=> other.priority if outcome == 0
|
152
|
+
end
|
153
|
+
end
|
88
154
|
end
|
89
155
|
end
|
90
156
|
end
|
data/simplekit.gemspec
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# -*- ruby -*-
|
2
|
-
_VERSION = "0.
|
2
|
+
_VERSION = "1.0.0"
|
3
3
|
|
4
4
|
Gem::Specification.new do |s|
|
5
5
|
s.name = "simplekit"
|
6
6
|
s.version = _VERSION
|
7
|
-
s.date = "
|
7
|
+
s.date = "2018-07-12"
|
8
8
|
s.summary = "Discrete event simulation engine."
|
9
9
|
s.homepage = "https://gitlab.nps.edu/pjsanche/simplekit-ruby.git"
|
10
10
|
s.email = "pjs@alum.mit.edu"
|
@@ -14,7 +14,12 @@ Gem::Specification.new do |s|
|
|
14
14
|
simplekit.gemspec
|
15
15
|
lib/simplekit.rb
|
16
16
|
lib/priority_queue.rb
|
17
|
+
demos/MMk.rb
|
18
|
+
demos/MMk_reference_output.txt
|
19
|
+
demos/MMk_shutdown.rb
|
20
|
+
demos/CancelQ.rb
|
21
|
+
demos/sptf.rb
|
17
22
|
]
|
18
|
-
s.required_ruby_version = '>=
|
23
|
+
s.required_ruby_version = '>= 2.3.0'
|
19
24
|
s.license = 'MIT'
|
20
25
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simplekit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paul J Sanchez
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-07-12 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: This is a minimal discrete event simulation scheduling algorithm for
|
14
14
|
educational use.
|
@@ -17,6 +17,11 @@ executables: []
|
|
17
17
|
extensions: []
|
18
18
|
extra_rdoc_files: []
|
19
19
|
files:
|
20
|
+
- demos/CancelQ.rb
|
21
|
+
- demos/MMk.rb
|
22
|
+
- demos/MMk_reference_output.txt
|
23
|
+
- demos/MMk_shutdown.rb
|
24
|
+
- demos/sptf.rb
|
20
25
|
- lib/priority_queue.rb
|
21
26
|
- lib/simplekit.rb
|
22
27
|
- simplekit.gemspec
|
@@ -32,7 +37,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
32
37
|
requirements:
|
33
38
|
- - ">="
|
34
39
|
- !ruby/object:Gem::Version
|
35
|
-
version:
|
40
|
+
version: 2.3.0
|
36
41
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
37
42
|
requirements:
|
38
43
|
- - ">="
|
@@ -40,7 +45,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
40
45
|
version: '0'
|
41
46
|
requirements: []
|
42
47
|
rubyforge_project:
|
43
|
-
rubygems_version: 2.
|
48
|
+
rubygems_version: 2.7.7
|
44
49
|
signing_key:
|
45
50
|
specification_version: 4
|
46
51
|
summary: Discrete event simulation engine.
|