simplekit 0.4.5 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|