diff --git a/demos/MMk_shutdown.rb b/demos/MMk_shutdown.rb new file mode 100644 index 0000000000000000000000000000000000000000..78c24487c2ccf5555b6dacf48014174f15bc183e --- /dev/null +++ b/demos/MMk_shutdown.rb @@ -0,0 +1,80 @@ +#!/usr/bin/env ruby + +require_relative '../lib/simplekit' + +# Demonstration model of an M/M/k queueing system. There are k servers +# and both the arrival and service processes are memoryless (exponential). +class MMk + include SimpleKit + + # Constructor - initializes the model parameters. + # param: arrival_rate - The rate at which customers arrive to the system. + # param: service_rate - The rate at which individual servers serve. + # param: max_servers - The total number of servers in the system. + def initialize(arrival_rate, service_rate, max_servers) + @arrival_rate = arrival_rate + @service_rate = service_rate + @max_servers = max_servers + end + + # Initialize the model state and schedule any necessary events. + # Note that this particular model will terminate based on + # time by scheduling a halt 100 time units in the future. + def init + @num_available_servers = @max_servers + @q_length = 0 + schedule(:arrival, 0.0) + schedule(:close_doors, 100.0) + dump_state('init') + end + + # An arrival event increments the queue length, schedules the next + # arrival, and schedules a begin_service event if a server is available. + def arrival + @q_length += 1 + schedule(:arrival, exponential(@arrival_rate)) + schedule(:begin_service, 0.0) if @num_available_servers > 0 + dump_state('arrival') + end + + # Start service for the first customer in line, removing that + # customer from the queue and utilizing one of the available servers. + # An end_service will be scheduled. + def begin_service + @q_length -= 1 + @num_available_servers -= 1 + schedule(:end_service, exponential(@service_rate)) + dump_state('begin svc') + end + + # Frees up an available server, and schedules a begin_service if + # anybody is waiting in line. + def end_service + @num_available_servers += 1 + schedule(:begin_service, 0.0) if @q_length > 0 + dump_state('end svc') + end + + def close_doors + cancel(:arrival) + end + + # Exponential random variate generator. + # param: rate - The rate (= 1 / mean) of the distribution. + # returns: A realization of the specified distribution. + def exponential(rate) + -Math.log(rand) / rate + end + + # A report mechanism which dumps the time, current event, and values + # of the state variables to the console. + # param: event - The name of the event which invoked this method. + def dump_state(event) + printf "Time: %8.3f\t%10s - Q: %d\tServers Available: %d\n", + model_time, event, @q_length, @num_available_servers + end +end + +# Instantiate an MMk object with a particular parameterization and run it. +srand 7_654_321 +MMk.new(4.5, 1.0, 5).run diff --git a/demos/MyModelArgs.rb b/demos/MyModelArgs.rb index d75ff655a4bbb07f6a8e5a950e03466923e56f07..db78ae0dafacd36f44dac3bd12eddd2a9a9287af 100644 --- a/demos/MyModelArgs.rb +++ b/demos/MyModelArgs.rb @@ -12,9 +12,9 @@ class MyModel def increment(n:, char:) @x += n - schedule(:increment, 2.0 * rand(2), n: @x, char: char - 1) + schedule(:increment, 2.0 * rand(2), n: @x, char: char - 1, priority: 3) printf "%f, %f, %c\n", model_time, @x, char - schedule(:halt, 0.0) if model_time > 10 + cancel_all :increment if model_time > 10 end end diff --git a/lib/simplekit.rb b/lib/simplekit.rb index 96b5fd1afcc8b8ec342435442b12dfbf6f454314..0826328d8ad988147a44b8bc6d017431ea5776b7 100644 --- a/lib/simplekit.rb +++ b/lib/simplekit.rb @@ -9,7 +9,9 @@ require_relative 'priority_queue' module SimpleKit # The set of module methods to be passed to the EventScheduler # if not found in the model class. - DELEGATED_METHODS = [:model_time, :schedule, :halt].freeze + DELEGATED_METHODS = %i[ + model_time schedule cancel cancel_all halt + ].freeze # Run your model by creating a new +EventScheduler+ and invoking its # +run+ method. @@ -21,11 +23,7 @@ module SimpleKit # If a method doesn't exist in the model class, try to delegate it # to +EventScheduler+. def method_missing(name, *args) - if DELEGATED_METHODS.include?(name) - @my_sim.send(name, *args) - else - super - end + DELEGATED_METHODS.include?(name) ? @my_sim.send(name, *args) : super end # Class +EventScheduler+ provides the computation engine for a @@ -43,6 +41,7 @@ module SimpleKit def initialize(the_model) @user_model = the_model @event_list = PriorityQueue.new + @cancel_set = {} end # Add an event to the pending events list. @@ -55,7 +54,22 @@ module SimpleKit # at invocation time. def schedule(event, delay, **args) raise 'Model scheduled event with negative delay.' if delay < 0 - @event_list.push EventNotice.new(event, @model_time, delay, **args) + @event_list.push EventNotice.new(event, @model_time, delay, args) + end + + def cancel(event, **args) + @cancel_set[event] = args + end + + def cancel_all(event) + if event + PriorityQueue.new.tap do |pq| + while (event_notice = @event_list.pop) + pq.push event_notice unless event_notice.event == event + end + @event_list = pq + end + end end # Start execution of a model. The simulation +model_time+ is initialized @@ -67,11 +81,16 @@ module SimpleKit @model_time = 0.0 @user_model.init while (current_event = @event_list.pop) + e = current_event.event + if @cancel_set.key? e + @cancel_set.delete e # if @cancel_set[e].empty? + next + end @model_time = current_event.time if current_event.args.empty? @user_model.send(current_event.event) else - @user_model.send(current_event.event, **current_event.args) + @user_model.send(current_event.event, current_event.args) end end end @@ -83,21 +102,26 @@ module SimpleKit end end + private + # This is a private helper class for the EventScheduler class. # Users should never try to access this directly. - private class EventNotice - attr_reader :event, :time, :time_stamp, :args + class EventNotice + attr_reader :event, :time, :time_stamp, :priority, :args def initialize(event, time, delay, args) @event = event @time_stamp = time @time = time + delay @args = args + @priority = (@args && @args.key?(:priority)) ? @args.delete(:priority) : 10 end include Comparable def <=>(other) - time <=> other.time + (time <=> other.time).tap do |outcome| + return priority <=> other.priority if outcome == 0 + end end end end