simplekit 0.4.5 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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
@@ -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. You <b>MUST NOT</b>
7
- # provide your own implementations of methods with these names in your model.
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 = [:model_time, :schedule, :halt].freeze
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
- if DELEGATED_METHODS.include?(name)
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+ -> an optional list of arguments to pass to the event
55
- # at invocation time.
56
- def schedule(event, delay, *args)
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 + delay, args)
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
- # the smallest time is popped, +model_time+ is updated to the event time,
65
- # and the event method is invoked.
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
- @user_model.send(current_event.event, *current_event.args)
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
- # This is a private helper Struct for the EventScheduler class.
83
- # Users should never try to access this directly.
84
- EventNotice = Struct.new(:event, :time, *:args) do
85
- include Comparable
86
- def <=>(other)
87
- time <=> other.time
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
@@ -1,10 +1,10 @@
1
1
  # -*- ruby -*-
2
- _VERSION = "0.4.5"
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 = "2017-12-01"
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 = '>= 1.8.1'
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.5
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: 2017-12-01 00:00:00.000000000 Z
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: 1.8.1
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.6.14
48
+ rubygems_version: 2.7.7
44
49
  signing_key:
45
50
  specification_version: 4
46
51
  summary: Discrete event simulation engine.