feldtruby 0.3.18 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,7 +8,7 @@ require 'feldtruby/annotations'
8
8
  # Make all Ruby objects Annotateable so we can attach information to the
9
9
  # individuals being optimized.
10
10
  class Object
11
- include FeldtRuby::Annotateable
11
+ include FeldtRuby::Annotateable
12
12
  end
13
13
 
14
14
  module FeldtRuby::Optimize
@@ -32,370 +32,393 @@ module FeldtRuby::Optimize
32
32
  # as the comparator. But the mapper and comparator to be used can, of course,
33
33
  # be changed.
34
34
  class Objective
35
- include FeldtRuby::Logging
35
+ include FeldtRuby::Logging
36
36
 
37
- # Current version of this objective. Is updated when the min or max values
38
- # for a sub-objective has been updated or when the weights are changed.
39
- # Candidates are always compared based on the latest version of an objective.
40
- attr_accessor :current_version
37
+ # Current version of this objective. Is updated when the min or max values
38
+ # for a sub-objective has been updated or when the weights are changed.
39
+ # Candidates are always compared based on the latest version of an objective.
40
+ attr_accessor :current_version
41
41
 
42
- # A quality mapper maps the goal values of a candidate to a single number.
43
- attr_accessor :quality_mapper
42
+ attr_reader :global_min_values_per_aspect, :global_max_values_per_aspect
44
43
 
45
- # A comparator compares two or more candidates and ranks them based on their
46
- # qualities.
47
- attr_accessor :comparator
44
+ def initialize(qualityAggregator = WeightedSumAggregator.new,
45
+ comparator = LowerAggregateQualityIsBetterComparator.new)
46
+
47
+ # A quality aggregator maps the goal values of a candidate to a single number.
48
+ @aggregator = qualityAggregator
49
+ @aggregator.objective = self
50
+
51
+ # A comparator compares two or more candidates and ranks them based on their
52
+ # qualities.
53
+ @comparator = comparator
54
+ @comparator.objective = self
48
55
 
49
- attr_reader :global_min_values_per_aspect, :global_max_values_per_aspect
56
+ self.current_version = 0
50
57
 
51
- def initialize(qualityMapper = nil, comparator = nil)
58
+ # We set all mins to INFINITY. This ensures that the first value seen will
59
+ # be smaller, and thus set as the new min.
60
+ @global_min_values_per_aspect = [Float::INFINITY] * num_goals
52
61
 
53
- @quality_mapper = qualityMapper || WeightedSumQualityMapper.new
54
- @comparator = comparator || Comparator.new
55
- @quality_mapper.objective = self
56
- @comparator.objective = self
62
+ # We set all maxs to -INFINITY. This ensures that the first value seen will
63
+ # be larger, and thus set as the new max.
64
+ @global_max_values_per_aspect = [-Float::INFINITY] * num_goals
57
65
 
58
- self.current_version = 0
66
+ setup_logger_and_distribute_to_instance_variables()
59
67
 
60
- # We set all mins to INFINITY. This ensures that the first value seen will
61
- # be smaller, and thus set as the new min.
62
- @global_min_values_per_aspect = [Float::INFINITY] * num_goals
68
+ # An array to keep the best object per goal.
69
+ @best_objects = Array.new
63
70
 
64
- # We set all maxs to -INFINITY. This ensures that the first value seen will
65
- # be larger, and thus set as the new max.
66
- @global_max_values_per_aspect = [-Float::INFINITY] * num_goals
71
+ end
67
72
 
68
- setup_logger_and_distribute_to_instance_variables()
73
+ # Return the number of goals of this objective.
74
+ def num_goals
75
+ @num_goals ||= goal_methods.length
76
+ end
69
77
 
70
- # An array to keep the best object per goal.
71
- @best_objects = Array.new
78
+ # Return the names of the goal methods.
79
+ def goal_methods
80
+ @goal_methods ||= self.methods.select {|m| is_goal_method?(m)}
81
+ end
72
82
 
73
- end
83
+ # Return true iff the goal method with the given index is a minimizing goal.
84
+ def is_min_goal?(index)
85
+ (@is_min_goal ||= (goal_methods.map {|m| is_min_goal_method?(m)}))[index]
86
+ end
74
87
 
75
- # Return the number of goals of this objective.
76
- def num_goals
77
- @num_goals ||= goal_methods.length
78
- end
88
+ # Return true iff the method with the given name is a goal method.
89
+ def is_goal_method?(methodNameAsSymbolOrString)
90
+ (methodNameAsSymbolOrString.to_s =~ /^(goal|objective)_(min|max)_([\w_]+)$/) != nil
91
+ end
79
92
 
80
- # Return the names of the goal methods.
81
- def goal_methods
82
- @goal_methods ||= self.methods.select {|m| is_goal_method?(m)}
83
- end
93
+ # Return true iff the method with the given name is a goal method.
94
+ def is_min_goal_method?(methodNameAsSymbolOrString)
95
+ (methodNameAsSymbolOrString.to_s =~ /^(goal|objective)_min_([\w_]+)$/) != nil
96
+ end
84
97
 
85
- # Return true iff the goal method with the given index is a minimizing goal.
86
- def is_min_goal?(index)
87
- (@is_min_goal ||= (goal_methods.map {|m| is_min_goal_method?(m)}))[index]
88
- end
98
+ # The candidate objects can be mapped to another object before we call the goal
99
+ # methods to calc the quality values. Default is no mapping but subclasses
100
+ # can override this for more complex evaluation schemes.
101
+ def map_candidate_to_object_to_be_evaluated(candidate)
102
+ candidate
103
+ end
104
+
105
+ # Weights is a map from goal method names to a number that represents the
106
+ # weight for that goal. Default is to set all weights to 1.
107
+ def weights
108
+ @weights ||= ([1] * num_goals)
109
+ end
110
+
111
+ # Set the weights given a hash mapping each goal method name to a number.
112
+ # The mapper and/or comparator can use the weights in their calculations.
113
+ def weights=(goalNameToNumberHash)
114
+
115
+ raise "Must be same number of weights as there are goals (#{num_aspects}), but is #{weights.length}" unless weights.length == num_goals
116
+
117
+ weights = goal_methods.map {|gm| goalNameToNumberHash[gm]}
118
+
119
+ logger.log_value :objective_weights_changed,
120
+ {"New weights" => goalNameToNumberHash},
121
+ "Weights updated from #{@weights} to #{weights}"
122
+
123
+ inc_version_number
124
+
125
+ @weights = weights
126
+
127
+ end
128
+
129
+ # Return a vector of the "raw" quality values, i.e. the fitness value for each
130
+ # goal. If we have already calculated the sub-qualities we just return them.
131
+ # If not we calculate them.
132
+ def sub_qualities_of(candidate, updateGlobals = true)
133
+ # Return the sub_qualities if we already have calculated them.
134
+ sqs = sub_qualities_if_already_calculated?(candidate)
135
+ return sqs if sqs
136
+ calculate_sub_qualities_of candidate, updateGlobals
137
+ end
138
+
139
+ # Calculate the sub-qualities from scratch, i.e. by mapping the candidate
140
+ # to an object to be evaluated and then calculate the value of each goal.
141
+ def calculate_sub_qualities_of(candidate, updateGlobals = true)
142
+ obj = map_candidate_to_object_to_be_evaluated(candidate)
143
+ sub_qualitites = goal_methods.map {|gmethod| self.send(gmethod, obj)}
144
+ update_global_mins_and_maxs(sub_qualitites, candidate) if updateGlobals
145
+ sub_qualitites
146
+ end
89
147
 
90
- # Return true iff the method with the given name is a goal method.
91
- def is_goal_method?(methodNameAsSymbolOrString)
92
- (methodNameAsSymbolOrString.to_s =~ /^(goal|objective)_(min|max)_([\w_]+)$/) != nil
93
- end
148
+ # Return a quality value for a given candidate and weights for the whole
149
+ # objective for a given candidate. Updates the best candidate if this
150
+ # is the best seen so far.
151
+ def quality_of(candidate, weights = self.weights)
94
152
 
95
- # Return true iff the method with the given name is a goal method.
96
- def is_min_goal_method?(methodNameAsSymbolOrString)
97
- (methodNameAsSymbolOrString.to_s =~ /^(goal|objective)_min_([\w_]+)$/) != nil
98
- end
153
+ q = quality_in_object(candidate)
154
+ return q if q
99
155
 
100
- # The candidate objects can be mapped to another object before we call the goal
101
- # methods to calc the quality values. Default is no mapping but subclasses
102
- # can override this for more complex evaluation schemes.
103
- def map_candidate_to_object_to_be_evaluated(candidate)
104
- candidate
105
- end
156
+ sub_qualities = sub_qualities_of(candidate)
106
157
 
107
- # Weights is a map from goal method names to a number that represents the
108
- # weight for that goal. Default is to set all weights to 1.
109
- def weights
110
- @weights ||= ([1] * num_goals)
111
- end
158
+ qv = update_quality_value_of candidate, sub_qualities, weights
159
+
160
+ update_best_candidate candidate, qv
161
+
162
+ qv
163
+
164
+ end
112
165
 
113
- # Set the weights given a hash mapping each goal method name to a number.
114
- # The mapper and/or comparator can use the weights in their calculations.
115
- def weights=(goalNameToNumberHash)
166
+ # Rank candidates from best to worst. Candidates that have the same quality
167
+ # are randomly ordered.
168
+ def rank_candidates(candidates, weights = self.weights)
169
+ # We first ensure all candidates have calculated sub-qualities, then we
170
+ # call the comparator. This ensures that the gobals are already correctly
171
+ # updated on each quality value.
172
+ candidates.map {|c| sub_qualities_of(c, true)}
173
+
174
+ # Now just let the comparator rank the candidates
175
+ @comparator.rank_candidates candidates, weights
176
+ end
116
177
 
117
- raise "Must be same number of weights as there are goals (#{num_aspects}), but is #{weights.length}" unless weights.length == num_goals
178
+ # Return true iff candidate1 is better than candidate2. Will update their
179
+ # quality values if they are out of date.
180
+ def is_better_than?(candidate1, candidate2)
181
+ @comparator.is_better_than?(candidate1, candidate2)
182
+ end
118
183
 
119
- weights = goal_methods.map {|gm| goalNameToNumberHash[gm]}
184
+ # Return true iff candidate1 is better than candidate2. Will update their
185
+ # quality values if they are out of date.
186
+ def hat_compare(candidate1, candidate2)
187
+ @comparator.hat_compare(candidate1, candidate2)
188
+ end
120
189
 
121
- #log_value( :objective_weights_changed, goalNameToNumberHash,
122
- # "Weights updated from #{@weights} to #{weights}" )
190
+ # Return true iff candidate1 is better than candidate2 for goal _index_.
191
+ # Will update their quality values if they are out of date.
192
+ def is_better_than_for_goal?(index, candidate1, candidate2)
193
+ @comparator.is_better_than_for_goal?(index, candidate1, candidate2)
194
+ end
123
195
 
124
- inc_version_number
196
+ # Return the aggregated quality value given sub qualities.
197
+ def aggregated_quality(subQualities)
198
+ @aggregator.aggregate_from_sub_qualities(subQualities, weights)
199
+ end
125
200
 
126
- @weights = weights
201
+ def note_end_of_optimization(optimizer)
202
+ logger.log_data :objective_optimization_ends, {
203
+ "Best object, aggregated" => @best_candidate,
204
+ "Quality of best object" => @best_quality_value,
205
+ "Version" => current_version,
206
+ "Comparator" => @comparator,
207
+ "Aggregator" => @aggregator,
208
+ "Best objects per goal" => @best_objects
209
+ }, "Objective: Report at end of optimization", true
210
+ end
127
211
 
128
- end
212
+ attr_reader :best_candidate
129
213
 
130
- # Return a vector of the "raw" quality values, i.e. the fitness value for each
131
- # goal.
132
- def sub_qualities_of(candidate, updateGlobals = true)
133
- obj = map_candidate_to_object_to_be_evaluated(candidate)
134
- sub_qualitites = goal_methods.map {|gmethod| self.send(gmethod, obj)}
135
- update_global_mins_and_maxs(sub_qualitites, candidate) if updateGlobals
136
- sub_qualitites
137
- end
214
+ private
138
215
 
139
- # Return a quality value for a given candidate and weights for the whole
140
- # objective for a given candidate. Updates the best candidate if this
141
- # is the best seen so far.
142
- def quality_of(candidate, weights = self.weights)
216
+ def update_quality_value_of(candidate, subQualities, weights)
217
+ qv = QualityValue.new subQualities, candidate, self
218
+ update_quality_value_in_object candidate, qv
219
+ end
143
220
 
144
- q = quality_if_up_to_date?(candidate)
145
- return q if q
221
+ def update_best_candidate candidate, qv
222
+ if @best_candidate == nil || (qv.value < @best_quality_value.value)
223
+ set_new_best_candidate candidate, qv
224
+ end
225
+ end
146
226
 
147
- sub_qualities = sub_qualities_of(candidate)
227
+ def set_new_best_candidate candidate, qualityValue
148
228
 
149
- qv = update_quality_value_of candidate, sub_qualities, weights
229
+ @best_candidate = candidate
230
+ @best_quality_value = qualityValue
150
231
 
151
- update_best_candidate candidate, qv
232
+ logger.log_data :objective_new_best_candidate, {
233
+ :candidate => candidate,
234
+ :quality_value => qualityValue
235
+ }, "Objective: New best ever found", true
152
236
 
153
- qv
237
+ end
154
238
 
155
- end
239
+ def inc_version_number
156
240
 
157
- # Rank candidates from best to worst. Updates the quality value of each
158
- # candidate.
159
- def rank_candidates(candidates, weights = self.weights)
241
+ new_version = self.current_version + 1
160
242
 
161
- # Map each candidate to its sub-qualities without updating the globals.
162
- # We will update once for the whole set below.
163
- sqvss = candidates.map {|c| sub_qualities_of(c, false)}
243
+ logger.log_value :objective_version_number, new_version,
244
+ "New version of objective: version = #{new_version}"
164
245
 
165
- # Update the global mins and maxs based on the set of sub-qualities.
166
- # Note! This must be done once for the whole set otherwise when we later
167
- # compare the cnadidates based on their quality values they
168
- # might be for different versions of the objective.
169
- sqvss.each_with_index do |sqvs, i|
170
- update_global_mins_and_maxs sqvs, candidates[i]
171
- end
246
+ self.current_version = new_version
172
247
 
173
- # Update the quality value of each candidate.
174
- sqvss.each_with_index do |sqvs, i|
175
- update_quality_value_of candidates[i], sqvs, weights
176
- end
248
+ end
177
249
 
178
- # Now use the comparator to rank the candidates.
179
- comparator.rank_candidates candidates, weights
250
+ # Update the min and max values for each goal in case the values in the
251
+ # supplied array are outside the previously seen min and max.
252
+ def update_global_mins_and_maxs subQualityValues, candidate
253
+ subQualityValues.each_with_index do |sqv,i|
254
+ update_global_min_and_max(i, sqv, candidate)
255
+ end
256
+ end
180
257
 
181
- end
258
+ # Update the global min and max for the goal method with _index_ if
259
+ # the _qValue_ is less than or
260
+ def update_global_min_and_max(index, qValue, candidate)
261
+ min = @global_min_values_per_aspect[index]
262
+ max = @global_max_values_per_aspect[index]
182
263
 
183
- # Return true iff candidate1 is better than candidate2. Will update their
184
- # quality values if they are out of date.
185
- def is_better_than?(candidate1, candidate2)
186
- quality_of(candidate1) < quality_of(candidate2)
187
- end
264
+ if qValue < min
188
265
 
189
- # Return true iff candidate1 is better than candidate2 for goal _index_.
190
- # Will update their quality values if they are out of date.
191
- def is_better_than_for_goal?(index, candidate1, candidate2)
192
- qv1 = quality_of(candidate1)
193
- qv2 = quality_of(candidate2)
194
- qv1.sub_quality(index, true) <= qv2.sub_quality(index, true)
195
- end
266
+ @global_min_values_per_aspect[index] = qValue
196
267
 
197
- def note_end_of_optimization(optimizer)
198
- nil
199
- end
268
+ reset_quality_scale candidate, index, :min
200
269
 
201
- attr_reader :best_candidate
270
+ end
271
+ if qValue > max
202
272
 
203
- private
273
+ @global_max_values_per_aspect[index] = qValue
204
274
 
205
- def update_quality_value_of(candidate, subQualities, weights)
275
+ reset_quality_scale candidate, index, :max
206
276
 
207
- q = quality_mapper.map_from_sub_qualities(subQualities, weights)
277
+ end
278
+ end
208
279
 
209
- qv = QualityValue.new q, subQualities, candidate, self
280
+ # Reset the quality scale if the updated min or max value
281
+ # was the best quality value seen for the goal with given _index_.
282
+ def reset_quality_scale(candidate, index, typeOfReset)
210
283
 
211
- update_quality_value_in_object candidate, qv
284
+ is_min = is_min_goal?(index)
212
285
 
213
- end
286
+ if (typeOfReset == :min && is_min) || (typeOfReset == :max && !is_min)
214
287
 
215
- def update_best_candidate candidate, qv
216
- if @best_candidate == nil || (qv < @best_quality_value)
217
- set_new_best_candidate candidate, qv
218
- end
219
- end
288
+ @best_objects[index] = candidate
220
289
 
221
- def set_new_best_candidate candidate, qualityValue
290
+ logger.log_data :objective_better_object_for_goal, {
291
+ :better_candidate => candidate,
292
+ :type_of_improvement => typeOfReset
293
+ }, "Better candidate found for goal #{goal_methods[index]}"
222
294
 
223
- @best_candidate = candidate
224
- @best_quality_value = qualityValue
295
+ # Reset the best object since we have a new scale
296
+ @best_candidate = nil
225
297
 
226
- #log_data :objective_new_best_candidate, {
227
- # :candidate => candidate,
228
- # :quality_value => qualityValue
229
- #}, "New best candidate found"
298
+ end
230
299
 
231
- end
300
+ inc_version_number
232
301
 
233
- def inc_version_number
302
+ end
234
303
 
235
- new_version = @current_version + 1
304
+ def sub_qualities_if_already_calculated?(candidate)
305
+ qv = quality_in_object candidate
306
+ !qv.nil? ? qv.sub_qualities : nil
307
+ end
236
308
 
237
- #log_value :objective_version_number, new_version,
238
- # "New version of objective:\n#{self.to_s}"
309
+ # Get the hash of annotations that we have done to this object.
310
+ def my_annotations(object)
311
+ object._annotations[self] ||= Hash.new
312
+ end
239
313
 
240
- @current_version = new_version
314
+ def quality_in_object(object)
315
+ my_annotations(object)[:quality]
316
+ end
241
317
 
242
- end
243
-
244
- # Update the min and max values for each goal in case the values in the
245
- # supplied array are outside the previously seen min and max.
246
- def update_global_mins_and_maxs subQualityValues, candidate
247
- subQualityValues.each_with_index do |sqv,i|
248
- update_global_min_and_max(i, sqv, candidate)
249
- end
250
- end
251
-
252
- # Update the global min and max for the goal method with _index_ if
253
- # the _qValue_ is less than or
254
- def update_global_min_and_max(index, qValue, candidate)
255
- min = @global_min_values_per_aspect[index]
256
- max = @global_max_values_per_aspect[index]
257
-
258
- if qValue < min
259
-
260
- @global_min_values_per_aspect[index] = qValue
261
-
262
- reset_quality_scale candidate, index, :min
263
-
264
- end
265
- if qValue > max
266
-
267
- @global_max_values_per_aspect[index] = qValue
268
-
269
- reset_quality_scale candidate, index, :max
270
-
271
- end
272
- end
273
-
274
- # Reset the quality scale if the updated min or max value
275
- # was the best quality value seen for the goal with given _index_.
276
- def reset_quality_scale(candidate, index, typeOfReset)
277
-
278
- is_min = is_min_goal?(index)
279
-
280
- if (typeOfReset == :min && is_min) || (typeOfReset == :max && !is_min)
281
-
282
- @best_objects[index] = candidate
283
-
284
- #log_data :objective_better_object_for_goal, {
285
- # :better_candidate => candidate,
286
- # :type_of_improvement => typeOfReset
287
- # }, "Better object found for goal #{goal_methods[i]}"
288
-
289
- # Reset the best object since we have a new scale
290
- @best_candidate = nil
291
-
292
- end
293
-
294
- inc_version_number
295
-
296
- end
297
-
298
- # Check if a candidates quality value according to this objective is
299
- # up to date with the latest version of the objective.
300
- def quality_if_up_to_date?(candidate)
301
- qv = quality_in_object candidate
302
- (!qv.nil? && qv.version == current_version) ? qv : nil
303
- end
304
-
305
- # Get the hash of annotations that we have done to this object.
306
- def my_annotations(object)
307
- object._annotations[self] ||= Hash.new
308
- end
309
-
310
- def quality_in_object(object)
311
- my_annotations(object)[:quality]
312
- end
313
-
314
- def update_quality_value_in_object(object, qv)
315
- my_annotations(object)[:quality] = qv
316
- end
318
+ def update_quality_value_in_object(object, qv)
319
+ my_annotations(object)[:quality] = qv
320
+ end
317
321
  end
318
322
 
319
- # A QualityMapper maps a vector of sub-quality values (for each individual goal of
323
+ # A QualityAggregator converts a vector of sub-quality values (for each individual goal of
320
324
  # an objective) into a single number on which the candidates can be compared.
321
- class Objective::QualityMapper
322
- attr_reader :objective
323
-
324
- def objective=(objective)
325
- # Calculate the signs to be used in inverting the max methods later.
326
- @signs = objective.goal_methods.map {|gm| objective.is_min_goal_method?(gm) ? 1 : -1}
327
- @objective = objective
328
- end
329
-
330
- # Map an array of _sub_qualities_ to a single number given an array of weights.
331
- # This default class just sums the quality values regardless of the weights.
332
- def map_from_sub_qualities subQualityValues, weights
333
- subQualityValues.weighted_sum(@signs)
334
- end
325
+ # Not every comparator uses the aggregated value to compare candidates though,
326
+ # but the default one does.
327
+ # This default aggregator is just a sum of the individual qualities where
328
+ # max goals are negated.
329
+ class Objective::QualityAggregator
330
+ attr_reader :objective
331
+
332
+ # Set the objective to use.
333
+ def objective=(objective)
334
+ # Calculate the signs to be used in inverting the max methods later.
335
+ @signs = objective.goal_methods.map {|gm| objective.is_min_goal_method?(gm) ? 1 : -1}
336
+ @objective = objective
337
+ end
338
+
339
+ # Aggregate an array of _sub_qualities_ into a single number given an array of weights.
340
+ # This default class just sums the quality values regardless of the weights.
341
+ def aggregate_from_sub_qualities subQualityValues, weights
342
+ subQualityValues.weighted_sum(@signs)
343
+ end
335
344
  end
336
345
 
337
- # A WeightedSumMapper sums individual quality values, each multiplied with a
346
+ # A WeightedSumAggregator sums individual quality values, each multiplied with a
338
347
  # weight.
339
- class Objective::WeightedSumQualityMapper < Objective::QualityMapper
340
- def map_from_sub_qualities subQualityValues, weights
341
- sum = 0.0
342
- subQualityValues.each_with_index do |qv, i|
343
- sum += (qv * weights[i] * @signs[i])
344
- end
345
- sum
346
- end
348
+ class Objective::WeightedSumAggregator < Objective::QualityAggregator
349
+ def aggregate_from_sub_qualities subQualityValues, weights
350
+ sum = 0.0
351
+ subQualityValues.each_with_index do |qv, i|
352
+ sum += (qv * weights[i] * @signs[i])
353
+ end
354
+ sum
355
+ end
347
356
  end
348
357
 
349
- # A SumOfWeightedGlobalRatios implements Bentley's SWGR multi-objective
358
+ # A SumOfWeightedGlobalRatios is very similar to Bentley's SWGR multi-objective
350
359
  # fitness mapping scheme as described in the paper:
351
360
  # P. J. Bentley and J. P. Wakefield, "Finding Acceptable Solutions in the
352
361
  # Pareto-Optimal Range using Multiobjective Genetic Algorithms", 1997
353
362
  # http://eprints.hud.ac.uk/4052/1/PB_%26_JPW_1997_Finding_Acceptable_Solutions.htm
354
- # It is a weighted sum of the ratios to the best so far for each goal.
363
+ # with the difference that that lower values indicate better quality.
364
+ # It is the weighted sum of the ratios to the best so far for each goal.
355
365
  # One of its benefits is that one need not sort individuals in relation to
356
366
  # their peers; the aggregate fitness value is fully determined by the individual
357
367
  # and the global min and max values for each objective.
358
- class Objective::SumOfWeigthedGlobalRatiosMapper < Objective::WeightedSumQualityMapper
359
- def ratio(index, value, min, max)
360
- return 0.0 if value == nil
361
- if objective.is_min_aspect?(index)
362
- numerator = max - value
363
- else
364
- numerator = value - min
365
- end
366
- numerator.to_f.protected_division_with(max - min)
367
- end
368
-
369
- def map_from_sub_qualities subQualityValues, weights
370
- goal_mins = objective.global_min_values_per_goal
371
- goal_maxs = objective.global_max_values_per_goal
372
-
373
- ratios = subQualityValues.map_with_index do |v, i|
374
- ratio i, v, goal_mins[i], goal_maxs[i]
375
- end
376
-
377
- # We cannot reuse the superclass in calculating the weighted sum since
378
- # we have already taken the signs into account in the ratio method.
379
- sum = 0.0
380
- ratios.each_with_index do |r, i|
381
- sum += (qv * weights[i])
382
- end
383
-
384
- sum / weights.sum.to_f
385
- end
368
+ class Objective::SumOfWeigthedGlobalRatios < Objective::WeightedSumAggregator
369
+ def ratio(index, value, min, max)
370
+ return 1000.0 if value == nil # We heavily penalize if one sub-quality could not be calculated. Max is otherwise 1.0.
371
+ if objective.is_min_aspect?(index)
372
+ numerator = value - min
373
+ else
374
+ numerator = max - value
375
+ end
376
+ numerator.to_f.protected_division_with(max - min)
377
+ end
378
+
379
+ def aggregate_from_sub_qualities subQualityValues, weights
380
+ goal_mins = objective.global_min_values_per_goal
381
+ goal_maxs = objective.global_max_values_per_goal
382
+
383
+ ratios = subQualityValues.map_with_index do |v, i|
384
+ ratio i, v, goal_mins[i], goal_maxs[i]
385
+ end
386
+
387
+ # We cannot reuse the superclass in calculating the weighted sum since
388
+ # we have already taken the signs into account in the ratio method.
389
+ sum = 0.0
390
+ ratios.each_with_index do |r, i|
391
+ sum += (qv * weights[i])
392
+ end
393
+
394
+ sum / weights.sum.to_f
395
+ end
386
396
  end
387
397
 
388
398
  # A Comparator ranks a set of candidates based on their sub-qualities.
389
399
  # This default comparator just uses the quality value to sort the candidates, with
390
- # lower values indicating a better quality.
391
- class Objective::Comparator
392
- attr_accessor :objective
393
-
394
- # Return an array with the candidates ranked from best to worst.
395
- # Candidates that cannot be distinghuished from each other are randomly ranked.
396
- def rank_candidates candidates, weights
397
- candidates.sort_by {|c| objective.quality_of(c, weights).value}
398
- end
400
+ # lower values indicating better quality.
401
+ class Objective::LowerAggregateQualityIsBetterComparator
402
+ attr_accessor :objective
403
+
404
+ # Return an array with the candidates ranked from best to worst.
405
+ # Candidates that cannot be distinghuished from each other are randomly ranked.
406
+ def rank_candidates candidates, weights
407
+ candidates.sort_by {|c| objective.quality_of(c, weights).value}
408
+ end
409
+
410
+ def is_better_than?(c1, c2)
411
+ hat_compare(c1, c2) == 1
412
+ end
413
+
414
+ def is_better_than_for_goal?(index, c1, c2)
415
+ objective.quality_of(c1).sub_quality(index, true) < objective.quality_of(c2).sub_quality(index, true)
416
+ end
417
+
418
+ def hat_compare(c1, c2)
419
+ # We change the order since smaller values indicates higher quality
420
+ objective.quality_of(c2).value <=> objective.quality_of(c1).value
421
+ end
399
422
  end
400
423
 
401
424
  # Class for representing multi-objective _sub_qualitites_ and their summary
@@ -405,62 +428,66 @@ end
405
428
  # they reflect the quality of the candidate for the current version of
406
429
  # the objective.
407
430
  class QualityValue
408
- include Comparable
409
-
410
- attr_reader :value, :sub_qualities, :objective, :version, :candidate
411
-
412
- def initialize(qv, subQvs, candidate, objective)
413
- @value, @sub_qualities, @objective = qv, subQvs, objective
414
- @candidate = candidate
415
- @version = objective.current_version
416
- end
417
-
418
- def <=>(other)
419
- # This ensures they are ranked according to latest version of objective.
420
- ranked = objective.rank_candidates [self.candidate, other.candidate]
421
- if ranked.last == self.candidate
422
- return 1
423
- else
424
- return -1
425
- end
426
- end
427
-
428
- # Return the sub quality value with a given index. Can make sure maximization
429
- # goals are mapped as minimization goals if ensureMinimization is true.
430
- def sub_quality(index, ensureMinimization = false)
431
- return @sub_qualities[index] if !ensureMinimization || @objective.is_min_goal?(index)
432
- # Now we now this is a max goal that should be returned as a min goal => invert it.
433
- -(@sub_qualities[index])
434
- end
435
-
436
- def to_s
437
- subqs = sub_qualities.map {|f| f.to_significant_digits(4)}
438
- "%.3g (SubQs = #{subqs.inspect}, ver. #{version})" % value
439
- end
431
+ include Comparable
432
+
433
+ attr_reader :sub_qualities, :objective, :candidate
434
+
435
+ def initialize(subQvs, candidate, objective)
436
+ @sub_qualities, @objective = subQvs, objective
437
+ @candidate = candidate
438
+ end
439
+
440
+ # Return the aggregated quality value. Will always return an updated value
441
+ # since it will be recalculated if we have the wrong version.
442
+ def value
443
+ return @value if @version && @version == @objective.current_version
444
+ @version = @objective.current_version
445
+ @value = @objective.aggregated_quality(@sub_qualities)
446
+ end
447
+
448
+ def <=>(other)
449
+ return nil unless @objective == other.objective
450
+ @objective.hat_compare(@candidate, other.candidate)
451
+ end
452
+
453
+ # Return the sub quality value with a given index. Can make sure maximization
454
+ # goals are mapped as minimization goals if ensureMinimization is true.
455
+ def sub_quality(index, ensureMinimization = false)
456
+ return @sub_qualities[index] if !ensureMinimization || @objective.is_min_goal?(index)
457
+ # Now we now this is a max goal that should be returned as a min goal => invert it.
458
+ -(@sub_qualities[index])
459
+ end
460
+
461
+ def to_s
462
+ subqs = sub_qualities.map {|f| f.to_significant_digits(3)}
463
+ # Note! We ask for the value first which guarantees that we then have a version number.
464
+ qstr = "#{value.to_significant_digits(4)}"
465
+ "#{qstr} (SubQs = #{subqs.inspect}, ver. #{@version})"
466
+ end
440
467
  end
441
468
 
442
469
  # Short hand for when the objective function is given as a block that should be minimized.
443
470
  class ObjectiveMinimizeBlock < Objective
444
- def initialize(&objFunc)
445
- super()
446
- @objective_function = objFunc
447
- end
448
-
449
- def objective_min_cost_function(candidate)
450
- @objective_function.call(*candidate.to_a)
451
- end
471
+ def initialize(&objFunc)
472
+ super()
473
+ @objective_function = objFunc
474
+ end
475
+
476
+ def objective_min_cost_function(candidate)
477
+ @objective_function.call(*candidate.to_a)
478
+ end
452
479
  end
453
480
 
454
481
  # Short hand for when the objective function is given as a block that should be minimized.
455
482
  class ObjectiveMaximizeBlock < Objective
456
- def initialize(&objFunc)
457
- super()
458
- @objective_function = objFunc
459
- end
460
-
461
- def objective_max_cost_function(candidate)
462
- @objective_function.call(*candidate.to_a)
463
- end
483
+ def initialize(&objFunc)
484
+ super()
485
+ @objective_function = objFunc
486
+ end
487
+
488
+ def objective_max_cost_function(candidate)
489
+ @objective_function.call(*candidate.to_a)
490
+ end
464
491
  end
465
492
 
466
493
  end