feldtruby 0.3.18 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +11 -1
- data/lib/feldtruby/array/basic_stats.rb +5 -0
- data/lib/feldtruby/logger.rb +22 -10
- data/lib/feldtruby/optimize/differential_evolution.rb +8 -8
- data/lib/feldtruby/optimize/objective.rb +361 -334
- data/lib/feldtruby/optimize/optimizer.rb +7 -8
- data/lib/feldtruby/version.rb +1 -1
- data/spikes/comparing_samplers_on_classic_optimization_functions/compare_samplers.rb +0 -76
- data/test/helper.rb +7 -2
- data/test/long_running/multi_objective_problems.rb +58 -0
- data/test/long_running/single_objective_problems.rb +163 -0
- data/test/long_running/test_single_objective_optimization.rb +112 -0
- data/test/test_array_basic_stats.rb +20 -12
- data/test/test_logger.rb +4 -4
- data/test/test_optimize_objective.rb +175 -119
- metadata +8 -2
@@ -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
|
-
|
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
|
-
|
35
|
+
include FeldtRuby::Logging
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
43
|
-
attr_accessor :quality_mapper
|
42
|
+
attr_reader :global_min_values_per_aspect, :global_max_values_per_aspect
|
44
43
|
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
56
|
+
self.current_version = 0
|
50
57
|
|
51
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
66
|
+
setup_logger_and_distribute_to_instance_variables()
|
59
67
|
|
60
|
-
|
61
|
-
|
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
|
-
|
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
|
-
|
73
|
+
# Return the number of goals of this objective.
|
74
|
+
def num_goals
|
75
|
+
@num_goals ||= goal_methods.length
|
76
|
+
end
|
69
77
|
|
70
|
-
|
71
|
-
|
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
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
96
|
-
|
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
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
122
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
212
|
+
attr_reader :best_candidate
|
129
213
|
|
130
|
-
|
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
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
-
|
145
|
-
|
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
|
-
|
227
|
+
def set_new_best_candidate candidate, qualityValue
|
148
228
|
|
149
|
-
|
229
|
+
@best_candidate = candidate
|
230
|
+
@best_quality_value = qualityValue
|
150
231
|
|
151
|
-
|
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
|
-
|
237
|
+
end
|
154
238
|
|
155
|
-
|
239
|
+
def inc_version_number
|
156
240
|
|
157
|
-
|
158
|
-
# candidate.
|
159
|
-
def rank_candidates(candidates, weights = self.weights)
|
241
|
+
new_version = self.current_version + 1
|
160
242
|
|
161
|
-
|
162
|
-
|
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
|
-
|
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
|
-
|
174
|
-
sqvss.each_with_index do |sqvs, i|
|
175
|
-
update_quality_value_of candidates[i], sqvs, weights
|
176
|
-
end
|
248
|
+
end
|
177
249
|
|
178
|
-
|
179
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
198
|
-
nil
|
199
|
-
end
|
268
|
+
reset_quality_scale candidate, index, :min
|
200
269
|
|
201
|
-
|
270
|
+
end
|
271
|
+
if qValue > max
|
202
272
|
|
203
|
-
|
273
|
+
@global_max_values_per_aspect[index] = qValue
|
204
274
|
|
205
|
-
|
275
|
+
reset_quality_scale candidate, index, :max
|
206
276
|
|
207
|
-
|
277
|
+
end
|
278
|
+
end
|
208
279
|
|
209
|
-
|
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
|
-
|
284
|
+
is_min = is_min_goal?(index)
|
212
285
|
|
213
|
-
|
286
|
+
if (typeOfReset == :min && is_min) || (typeOfReset == :max && !is_min)
|
214
287
|
|
215
|
-
|
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
|
-
|
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
|
-
|
224
|
-
|
295
|
+
# Reset the best object since we have a new scale
|
296
|
+
@best_candidate = nil
|
225
297
|
|
226
|
-
|
227
|
-
# :candidate => candidate,
|
228
|
-
# :quality_value => qualityValue
|
229
|
-
#}, "New best candidate found"
|
298
|
+
end
|
230
299
|
|
231
|
-
|
300
|
+
inc_version_number
|
232
301
|
|
233
|
-
|
302
|
+
end
|
234
303
|
|
235
|
-
|
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
|
-
|
238
|
-
|
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
|
-
|
314
|
+
def quality_in_object(object)
|
315
|
+
my_annotations(object)[:quality]
|
316
|
+
end
|
241
317
|
|
242
|
-
|
243
|
-
|
244
|
-
|
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
|
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
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
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
|
346
|
+
# A WeightedSumAggregator sums individual quality values, each multiplied with a
|
338
347
|
# weight.
|
339
|
-
class Objective::
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
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
|
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
|
-
#
|
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::
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
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
|
391
|
-
class Objective::
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
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
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
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
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
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
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
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
|