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.
@@ -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.