symian 0.1.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,97 @@
1
+ require 'symian/work_shift'
2
+
3
+
4
+ module Symian
5
+ class Activity < Struct.new(:iid, :start_time, :end_time)
6
+ end
7
+
8
+
9
+ class Operator
10
+
11
+ extend Forwardable
12
+
13
+ REQUIRED_ATTRIBUTES = [
14
+ :oid,
15
+ :support_group_id,
16
+ ]
17
+
18
+ OTHER_ATTRIBUTES = [
19
+ :workshift,
20
+ :specialization,
21
+ :work_record,
22
+ :support_group_id,
23
+ ]
24
+
25
+ attr_reader *(REQUIRED_ATTRIBUTES + OTHER_ATTRIBUTES)
26
+
27
+ def_delegators :@workshift, :active_at?, :secs_to_begin_of_shift, :secs_to_end_of_shift
28
+ def_delegator :@workshift, :duration, :workshift_duration
29
+
30
+
31
+ def initialize(oid, support_group_id, opts={})
32
+ @oid = oid
33
+ @support_group_id = support_group_id
34
+
35
+ # set correspondent instance variables for optional arguments
36
+ opts.each do |k, v|
37
+ # ignore invalid attributes
38
+ instance_variable_set("@#{k}", v) if OTHER_ATTRIBUTES.include?(k)
39
+ end
40
+
41
+ # support :workshift => :all_day_long shortcut
42
+ if @workshift == :all_day_long
43
+ @workshift = WorkShift::WORKSHIFT_24x7
44
+ end
45
+
46
+ # default workshift is 24x7
47
+ @workshift ||= WorkShift::WORKSHIFT_24x7
48
+
49
+ @specialization ||= {}
50
+ @work_record ||= []
51
+ end
52
+
53
+
54
+ def assign(incident, incident_info, time)
55
+
56
+ # initialize incident start work time if needed
57
+ incident.start_work_time ||= time
58
+
59
+ # calculate time to end of shift
60
+ tteos = @workshift.secs_to_end_of_shift(time)
61
+ raise "tteos: #{tteos}" if tteos < 0.0
62
+
63
+ # specialization
64
+ specialization = @specialization[incident.category] || 1.0
65
+
66
+ # calculate time to incident escalation
67
+ ttie = incident_info[:needed_work_time] / specialization.to_f
68
+ raise "ttie: #{ttie}" if ttie < 0.0
69
+
70
+ # handle incident
71
+ if tteos < ttie # end of shift first
72
+ work_time = tteos
73
+ reason = :operator_off_duty
74
+ else # escalation first
75
+ work_time = ttie
76
+ reason = :incident_escalation
77
+ end
78
+
79
+ # update needed (effective) incident work time
80
+ incident_info[:needed_work_time] -= work_time * specialization
81
+
82
+ # update incident tracking
83
+ incident.add_tracking_information(:type => :work,
84
+ :at => time,
85
+ :duration => work_time,
86
+ :sg => @support_group_id,
87
+ :operator => @oid)
88
+
89
+ # update operator work record
90
+ @work_record << Activity.new(incident.iid, time, time + work_time)
91
+
92
+ # return [ reason, time_when_operator_stops_working ]
93
+ [ reason, time + work_time ]
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,60 @@
1
+ require 'symian/event'
2
+ require 'symian/generator'
3
+ require 'symian/incident'
4
+ require 'symian/operator'
5
+ require 'symian/support_group'
6
+
7
+
8
+ module Symian
9
+ class PerformanceAnalyzer
10
+ def initialize(config)
11
+ @warmup_threshold = config.start_time + config.warmup_duration
12
+ end
13
+
14
+ def calculate_kpis(trace)
15
+ raise ArgumentError, 'Argument must be a TraceCollector' unless TraceCollector === trace
16
+
17
+ kpis = {}
18
+
19
+ # these metrics are considered as kpis
20
+ kpis[:all_incidents] = trace.incidents
21
+ kpis[:incidents_considered] = 0
22
+ kpis[:closed_incidents] = 0
23
+ kpis[:mean_ttr] = 0
24
+ kpis[:max_ttr] = 0
25
+ kpis[:mean_waiting_time] = 0
26
+
27
+ max_ttr = 0
28
+ ttr_sum = 0
29
+ wt_sum = 0
30
+ trace.with_incidents do |i|
31
+
32
+ next if @warmup_threshold and i.arrival_time < @warmup_threshold
33
+
34
+ kpis[:incidents_considered] += 1
35
+
36
+ if i.closed?
37
+ kpis[:closed_incidents] += 1
38
+ ttr = i.total_work_time
39
+ ttr_sum += ttr
40
+ wt_sum += i.total_queue_time
41
+ max_ttr = ttr if ttr > max_ttr
42
+ end
43
+
44
+ end
45
+
46
+ kpis[:max_ttr] = max_ttr
47
+ if kpis[:closed_incidents] == 0
48
+ kpis[:mean_ttr] = Float::MAX
49
+ kpis[:mean_waiting_time] = Float::MAX
50
+ else
51
+ kpis[:mean_ttr] = ttr_sum / kpis[:closed_incidents]
52
+ kpis[:mean_waiting_time] = wt_sum / kpis[:closed_incidents]
53
+ end
54
+
55
+ # return kpis
56
+ kpis
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,171 @@
1
+ require 'symian/event'
2
+ require 'symian/generator'
3
+ require 'symian/operator'
4
+ require 'symian/performance_analyzer'
5
+ require 'symian/sorted_array'
6
+ require 'symian/support_group'
7
+ require 'symian/transition_matrix'
8
+ require 'symian/trace_collector'
9
+
10
+
11
+ module Symian
12
+ class Simulation
13
+
14
+ ATTRIBUTES = [ :start_time ]
15
+
16
+ attr_reader *ATTRIBUTES
17
+
18
+
19
+ def initialize(configuration, performance_analyzer, trace=TraceCollector.new(:memory))
20
+ @configuration = configuration
21
+
22
+ # setup performance analyzer and simulation trace
23
+ @performance_analyzer = performance_analyzer
24
+ @trace = trace
25
+
26
+ # setup simulation start and current time
27
+ @current_time = @start_time = @configuration.start_time
28
+
29
+ # create support groups
30
+ @support_groups = {}
31
+ @configuration.support_groups.each do |name,conf|
32
+ @support_groups[name] = SupportGroup.new(name, self, conf[:work_time], conf[:operators])
33
+ end
34
+
35
+ # create transition matrix
36
+ @transition_matrix = TransitionMatrix.new(@configuration.transition_matrix)
37
+
38
+ # create event queue
39
+ @event_queue = SortedArray.new
40
+ end
41
+
42
+
43
+ def new_event(type, data, time, destination)
44
+ @event_queue << Event.new(type, data, time, destination)
45
+ end
46
+
47
+
48
+ def now
49
+ @current_time
50
+ end
51
+
52
+
53
+ def run
54
+
55
+ # initialize support groups
56
+ @support_groups.values.each do |sg|
57
+ sg.initialize_at(@configuration.start_time)
58
+ end
59
+
60
+ # generate first incident
61
+ ig = IncidentGenerator.new(self, @configuration.incident_generation)
62
+ ig.generate
63
+
64
+ # schedule end of simulation
65
+ unless @configuration.end_time.nil?
66
+ # puts "Simulation ends at: #{@configuration.end_time}"
67
+ new_event(Event::ET_END_OF_SIMULATION, nil, @configuration.end_time, nil)
68
+ end
69
+
70
+ # calculate warmup threshold
71
+ warmup_threshold = @configuration.start_time + @configuration.warmup_duration
72
+
73
+ @incidents_being_worked_on ||= []
74
+
75
+ # launch simulation
76
+ until @event_queue.empty?
77
+ e = @event_queue.shift
78
+
79
+ # sanity check on simulation time flow
80
+ if @current_time > e.time
81
+ raise 'Error: simulation time inconsistency for event ' +
82
+ "e.type=#{e.type} @current_time=#{@current_time}, e.time=#{e.time}"
83
+ end
84
+
85
+ @current_time = e.time
86
+
87
+ #Trace.new_event e
88
+ case e.type
89
+ when Event::ET_INCIDENT_ARRIVAL
90
+
91
+ sg_name = @transition_matrix.escalation('In')
92
+
93
+ sg = @support_groups[sg_name]
94
+ sg.new_incident(e.data, e.time)
95
+
96
+ @incidents_being_worked_on << e.data
97
+
98
+ # generate next incident
99
+ ig.generate
100
+
101
+ # TODO: pinpoint instant for calculation of time spent in enqueued state
102
+
103
+
104
+ when Event::ET_INCIDENT_ASSIGNMENT
105
+
106
+ # TODO: implement calculation of time spent in suspended state
107
+
108
+
109
+ when Event::ET_OPERATOR_ACTIVITY_STARTS
110
+
111
+
112
+ when Event::ET_OPERATOR_ACTIVITY_FINISHES
113
+
114
+ sg = @support_groups[e.destination]
115
+ sg.operator_finished_working(e.data[0], e.time)
116
+
117
+
118
+ when Event::ET_INCIDENT_RESCHEDULING
119
+
120
+ sg = @support_groups[e.destination]
121
+ sg.schedule_incident_for_reassignment(e.data[0], e.data[1], e.time)
122
+
123
+ # TODO: pinpoint instant for calculation of time spent in suspended state
124
+
125
+
126
+ when Event::ET_INCIDENT_ESCALATION
127
+ inc = e.data
128
+
129
+ sg_name = @transition_matrix.escalation(e.destination)
130
+ if sg_name == "Out"
131
+ inc.closure_time = e.time
132
+
133
+ # remove incident from list of incidents being worked on and add it to trace
134
+ @incidents_being_worked_on.delete(inc)
135
+ @trace.record_incidents(inc)
136
+ else
137
+ sg = @support_groups[sg_name]
138
+ sg.new_incident(inc, e.time)
139
+ end
140
+
141
+
142
+ when Event::ET_OPERATOR_LEAVING
143
+
144
+ sg = @support_groups[e.destination]
145
+ sg.operator_going_home(e.data, e.time)
146
+
147
+
148
+ when Event::ET_OPERATOR_RETURNING
149
+
150
+ sg = @support_groups[e.destination]
151
+ sg.operator_arrived_at_work(e.data, e.time)
152
+
153
+
154
+ when Event::ET_SUPPORT_GROUP_QUEUE_SIZE_CHANGE
155
+
156
+
157
+ when Event::ET_END_OF_SIMULATION
158
+ break
159
+
160
+ end
161
+ end
162
+
163
+ # save trace file
164
+ @trace.save_and_close
165
+ kpis = @performance_analyzer.calculate_kpis(@trace)
166
+ kpis.merge(incidents_being_worked_on: @incidents_being_worked_on.size)
167
+
168
+ end
169
+
170
+ end
171
+ end
@@ -0,0 +1,57 @@
1
+ module Symian
2
+ # the SortedArray class was taken from the ruby cookbook
3
+ class SortedArray < Array
4
+ def initialize(*args, &sort_by)
5
+ @sort_by = sort_by || Proc.new { |x,y| x <=> y }
6
+ super(*args)
7
+ sort! &sort_by
8
+ end
9
+
10
+ def insert(i, v)
11
+ if size == 0 or v < self[0]
12
+ super(0, v)
13
+ elsif v > self[-1]
14
+ super(-1, v)
15
+ else
16
+ left = 0
17
+ right = size - 1
18
+ middle = (left + right)/2
19
+ while left < right
20
+ if v >= self[middle]
21
+ left = middle + 1
22
+ else
23
+ right = middle
24
+ end
25
+ middle = (left + right)/2
26
+ end
27
+ super(middle, v)
28
+ end
29
+ end
30
+
31
+ def <<(el)
32
+ insert(0, el)
33
+ end
34
+
35
+ alias push <<
36
+ alias unshift <<
37
+
38
+ # some methods, like collect!, can modify the items in an array,
39
+ # taking them out of sort order. we need to redefine those
40
+ # methods.
41
+ # we can't use define_method to define these methods because in
42
+ # Ruby 1.8 you can't use define_method to create a method that
43
+ # takes a block argument.
44
+ [ "collect!", "flatten!", "[]=" ].each do |method_name|
45
+ class_eval %{
46
+ def #{method_name}(*args)
47
+ super
48
+ sort! &@sort_by
49
+ end
50
+ }
51
+ end
52
+
53
+ def reverse!
54
+ # do nothing: reversing the array would disorder it.
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,18 @@
1
+ # taken from Jim Freeze's excellent article "Creating DSLs with Ruby"
2
+ # http://www.artima.com/rubycs/articles/ruby_as_dsl.html
3
+
4
+ class Module
5
+ def dsl_accessor(*symbols)
6
+ symbols.each { |sym|
7
+ class_eval %{
8
+ def #{sym}(*val)
9
+ if val.empty?
10
+ @#{sym}
11
+ else
12
+ @#{sym} = val.size == 1 ? val[0] : val
13
+ end
14
+ end
15
+ }
16
+ }
17
+ end
18
+ end
@@ -0,0 +1,9 @@
1
+ require 'yaml'
2
+
3
+ module Symian
4
+ module YAMLSerializable
5
+ def to_yaml_properties
6
+ self.class.const_get('TRACED_ATTRIBUTES').map{|attr| "@#{attr.to_s}"}
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,182 @@
1
+ require 'erv'
2
+
3
+ require 'symian/event'
4
+ require 'symian/operator'
5
+ require 'symian/work_shift'
6
+
7
+ require 'symian/support/yaml_io'
8
+
9
+
10
+ module Symian
11
+
12
+ class SupportGroup
13
+
14
+ # setup readable/accessible attributes
15
+ ATTRIBUTES = [ :sgid, :operators ]
16
+
17
+ attr_reader *ATTRIBUTES
18
+
19
+
20
+ # setup attributes saved in traces
21
+ include YAMLSerializable
22
+
23
+ TRACED_ATTRIBUTES = ATTRIBUTES + [ :incident_queue_info ]
24
+
25
+
26
+ def initialize(support_group_id, simulation, work_time_characterization, operator_characterizations)
27
+ @sgid = support_group_id
28
+ @simulation = simulation
29
+
30
+ # initialize needed_work_time_rng
31
+ @needed_work_time_rv = ERV::RandomVariable.new(work_time_characterization)
32
+
33
+ # create operators
34
+ @operators = []
35
+
36
+ if operator_characterizations.kind_of?(Hash)
37
+ operator_characterizations = [ operator_characterizations ]
38
+ end
39
+
40
+ next_op_id = 1
41
+ operator_characterizations.each do |x|
42
+ x[:number].times do |y|
43
+ op = Operator.new("OP#{next_op_id}_#{@sgid}", @sgid, x.reject { |k, v| k == :number })
44
+ @operators << op
45
+ next_op_id = next_op_id + 1
46
+ end
47
+ end
48
+
49
+ # initialize incident queue and related tracking information
50
+ @incident_queue = []
51
+ @incident_queue_info = []
52
+ end
53
+
54
+
55
+ def initialize_at(time)
56
+ # find out which operators are off duty and schedule their comeback
57
+ @operators_off_work = @operators.select do |x|
58
+ !x.workshift.active_at?(time)
59
+ end
60
+ @operators_off_work.each do |op|
61
+ t = op.workshift.secs_to_begin_of_shift(time)
62
+ @simulation.new_event(Event::ET_OPERATOR_RETURNING, op.oid,
63
+ time + t, @sgid)
64
+ end
65
+
66
+ # find out which operators are on duty and schedule their leaving
67
+ @available_operators = @operators - @operators_off_work
68
+ @available_operators.each do |op|
69
+ t = op.workshift.secs_to_end_of_shift(time)
70
+ @simulation.new_event(Event::ET_OPERATOR_LEAVING, op.oid,
71
+ time + t, @sgid) unless t == WorkShift::Infinity
72
+ end
73
+ end
74
+
75
+
76
+ def new_incident(incident, time)
77
+ # increase number of visited SGs
78
+ incident.visited_support_groups += 1
79
+
80
+ incident_info = {
81
+ # set up incident needed work time
82
+ :needed_work_time => @needed_work_time_rv.next,
83
+ # # reset queue_time_at_last_sg attribute
84
+ # :queue_time => 0
85
+ }
86
+
87
+ # put incident at the end of the queue
88
+ @incident_queue << [ incident, incident_info, time ]
89
+
90
+ # update queue size tracking information
91
+ @incident_queue_info << { :size => @incident_queue.size, :time => time }
92
+ @simulation.new_event(Event::ET_SUPPORT_GROUP_QUEUE_SIZE_CHANGE, @incident_queue.size,
93
+ time, @sgid)
94
+
95
+ # try to allocate operator
96
+ try_to_allocate_operator(time)
97
+ end
98
+
99
+
100
+ def schedule_incident_for_reassignment(incident, incident_info, time)
101
+ @incident_queue.unshift [ incident, incident_info, time ]
102
+ # update queue size tracking information
103
+ @incident_queue_info << { :size => @incident_queue.size, :time => time }
104
+ try_to_allocate_operator(time)
105
+ end
106
+
107
+
108
+ def operator_going_home(operator_id, time)
109
+ op = @operators.find{|x| x.oid == operator_id }
110
+ @available_operators.delete_if{|x| x.oid == operator_id }
111
+ @operators_off_work << op
112
+ @simulation.new_event(Event::ET_OPERATOR_RETURNING, op.oid,
113
+ time + 86400 - op.workshift.duration, @sgid)
114
+ end
115
+
116
+
117
+ def operator_arrived_at_work(operator_id, time)
118
+ op = @operators.find{|x| x.oid == operator_id }
119
+ @operators_off_work.delete(op)
120
+ @available_operators << op
121
+ try_to_allocate_operator(time)
122
+ @simulation.new_event(Event::ET_OPERATOR_LEAVING, op.oid,
123
+ time + op.workshift.duration, @sgid)
124
+ end
125
+
126
+
127
+ def operator_finished_working(operator_id, time)
128
+ op = @operators.find{|x| x.oid == operator_id }
129
+ if op.workshift.secs_to_end_of_shift(time) > 0
130
+ @available_operators << op
131
+ end
132
+ try_to_allocate_operator(time)
133
+ end
134
+
135
+
136
+ private
137
+
138
+ def try_to_allocate_operator(time)
139
+ if !@available_operators.empty? and !@incident_queue.empty?
140
+ op = @available_operators.shift
141
+ i, inc_info, t = @incident_queue.shift
142
+
143
+ # update incident tracking information
144
+ queue_time = time.to_i - t.to_i
145
+ i.add_tracking_information(:type => :queue,
146
+ :at => t,
147
+ :duration => queue_time,
148
+ :sg => @support_group_id)
149
+
150
+
151
+ @simulation.new_event(Event::ET_SUPPORT_GROUP_QUEUE_SIZE_CHANGE,
152
+ @incident_queue.size, time, @sgid)
153
+ @simulation.new_event(Event::ET_INCIDENT_ASSIGNMENT,
154
+ [ i.iid, op.oid ], time, @sgid)
155
+ @simulation.new_event(Event::ET_OPERATOR_ACTIVITY_STARTS,
156
+ [ op.oid, i.iid ], time, @sgid)
157
+
158
+ # assign updates inc_info
159
+ report = op.assign(i, inc_info, time)
160
+ finish_time = report[1]
161
+
162
+ puts "finish_time: #{finish_time}, time: #{time}" if time > finish_time
163
+ @simulation.new_event(Event::ET_OPERATOR_ACTIVITY_FINISHES,
164
+ [ op.oid, i.iid ], finish_time, @sgid)
165
+
166
+ case report[0]
167
+ when :incident_escalation
168
+ @simulation.new_event(Event::ET_INCIDENT_ESCALATION,
169
+ i, finish_time, @sgid)
170
+ when :operator_off_duty
171
+ # TODO: implement configurable rescheduling policy
172
+ @incident_queue << [ i, inc_info, finish_time ]
173
+ @simulation.new_event(Event::ET_INCIDENT_RESCHEDULING,
174
+ [ i, inc_info, op.oid ], finish_time, @sgid)
175
+ end
176
+ end
177
+ end
178
+
179
+ end
180
+
181
+ end
182
+