feldtruby 0.3.16 → 0.3.18
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile.lock +9 -2
- data/Rakefile +8 -0
- data/feldtruby.gemspec +6 -0
- data/lib/feldtruby/annotations.rb +10 -0
- data/lib/feldtruby/array/basic_stats.rb +3 -1
- data/lib/feldtruby/array/permutations_and_subsets.rb +17 -0
- data/lib/feldtruby/float.rb +23 -0
- data/lib/feldtruby/logger.rb +216 -30
- data/lib/feldtruby/minitest_extensions.rb +0 -1
- data/lib/feldtruby/mongodb.rb +16 -0
- data/lib/feldtruby/mongodb_logger.rb +245 -0
- data/lib/feldtruby/optimize/differential_evolution.rb +29 -5
- data/lib/feldtruby/optimize/elite_archive.rb +91 -0
- data/lib/feldtruby/optimize/max_steps_termination_criterion.rb +1 -1
- data/lib/feldtruby/optimize/objective.rb +343 -222
- data/lib/feldtruby/optimize/optimizer.rb +138 -60
- data/lib/feldtruby/optimize/search_space.rb +10 -0
- data/lib/feldtruby/optimize.rb +1 -26
- data/lib/feldtruby/statistics.rb +74 -3
- data/lib/feldtruby/time.rb +19 -0
- data/lib/feldtruby/version.rb +1 -1
- data/old/event_logger.rb +682 -0
- data/spikes/comparing_samplers_on_classic_optimization_functions/analyze_sampler_comparison_results.R +78 -0
- data/spikes/comparing_samplers_on_classic_optimization_functions/compare_samplers.rb +264 -0
- data/spikes/comparing_samplers_on_classic_optimization_functions/results_comparing_samplers_130405_175934.csv +561 -0
- data/spikes/comparing_samplers_on_classic_optimization_functions/results_comparing_samplers_levi13_beale_easom_eggholder.csv +11201 -0
- data/spikes/comparing_samplers_on_classic_optimization_functions/results_comparing_samplers_levi13_beale_easom_eggholder_all_radii_4_to_30.csv +44801 -0
- data/spikes/comparing_samplers_on_classic_optimization_functions/results_comparing_samplers_omnitest.csv +1401 -0
- data/spikes/mongodb_logger.rb +47 -0
- data/spikes/simple_de_run.rb +32 -0
- data/test/helper.rb +17 -1
- data/test/test_array_basic_stats.rb +5 -1
- data/test/test_array_permutations_and_subsets.rb +23 -0
- data/test/test_float.rb +15 -0
- data/test/test_html_doc_getter.rb +1 -1
- data/test/test_logger.rb +86 -48
- data/test/test_mongodb_logger.rb +116 -0
- data/test/test_object_annotations.rb +14 -0
- data/test/test_optimize.rb +7 -6
- data/test/test_optimize_differential_evolution.rb +21 -19
- data/test/test_optimize_elite_archive.rb +85 -0
- data/test/test_optimize_objective.rb +237 -74
- data/test/test_optimize_populationbasedoptimizer.rb +72 -6
- data/test/test_optimize_random_search.rb +0 -17
- data/test/test_optimize_search_space.rb +15 -0
- data/test/test_statistics.rb +30 -4
- data/test/test_time.rb +22 -0
- data/test/tmp_shorter.csv +200 -0
- metadata +62 -21
@@ -1,311 +1,442 @@
|
|
1
1
|
require 'feldtruby/optimize'
|
2
2
|
require 'feldtruby/float'
|
3
3
|
require 'feldtruby/optimize/sub_qualities_comparators'
|
4
|
+
require 'feldtruby/logger'
|
5
|
+
require 'feldtruby/float'
|
6
|
+
require 'feldtruby/annotations'
|
7
|
+
|
8
|
+
# Make all Ruby objects Annotateable so we can attach information to the
|
9
|
+
# individuals being optimized.
|
10
|
+
class Object
|
11
|
+
include FeldtRuby::Annotateable
|
12
|
+
end
|
4
13
|
|
5
14
|
module FeldtRuby::Optimize
|
6
15
|
|
7
|
-
# An Objective
|
8
|
-
#
|
9
|
-
# functions in a single framework. You subclass and add instance
|
10
|
-
# methods named as
|
11
|
-
# objective_min_qualityAspectName (for an objective/aspect to be minimized), or
|
12
|
-
# objective_max_qualityAspectName (for an objective/aspect to be minimized).
|
16
|
+
# An Objective maps candidate solutions to qualities so they can be compared and
|
17
|
+
# ranked.
|
13
18
|
#
|
14
|
-
#
|
19
|
+
# One objective can have one or more sub-objectives, called goals. Each goal
|
20
|
+
# is specified as a separate method and its name indicates if the returned
|
21
|
+
# Numeric should be minimized or maximized. To create your own objective
|
22
|
+
# you subclass and add instance methods named as
|
23
|
+
# goal_min_qualityAspectName (for a goal value to be minimized), or
|
24
|
+
# goal_max_qualityAspectName (for a goal value to be minimized).
|
15
25
|
#
|
16
26
|
# An objective keeps track of the min and max value that has been seen so far
|
17
|
-
# for each
|
18
|
-
|
19
|
-
# This base class uses mean-weighted-global-ratios (MWGR) as the default mechanism
|
20
|
-
# for handling multi-objectives i.e. with more than one sub-objective.
|
21
|
-
#
|
27
|
+
# for each goal.
|
22
28
|
# An objective has version numbers to indicate the number of times a new min or max
|
23
|
-
# value has been identified for a
|
29
|
+
# value has been identified for a goal.
|
30
|
+
#
|
31
|
+
# This base class uses weigthed sum as the quality mapper and number comparator
|
32
|
+
# as the comparator. But the mapper and comparator to be used can, of course,
|
33
|
+
# be changed.
|
24
34
|
class Objective
|
25
|
-
|
26
|
-
|
35
|
+
include FeldtRuby::Logging
|
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.
|
27
40
|
attr_accessor :current_version
|
28
41
|
|
29
|
-
#
|
30
|
-
attr_accessor :
|
42
|
+
# A quality mapper maps the goal values of a candidate to a single number.
|
43
|
+
attr_accessor :quality_mapper
|
31
44
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
45
|
+
# A comparator compares two or more candidates and ranks them based on their
|
46
|
+
# qualities.
|
47
|
+
attr_accessor :comparator
|
48
|
+
|
49
|
+
attr_reader :global_min_values_per_aspect, :global_max_values_per_aspect
|
50
|
+
|
51
|
+
def initialize(qualityMapper = nil, comparator = nil)
|
52
|
+
|
53
|
+
@quality_mapper = qualityMapper || WeightedSumQualityMapper.new
|
54
|
+
@comparator = comparator || Comparator.new
|
55
|
+
@quality_mapper.objective = self
|
56
|
+
@comparator.objective = self
|
57
|
+
|
58
|
+
self.current_version = 0
|
59
|
+
|
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
|
63
|
+
|
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
|
67
|
+
|
68
|
+
setup_logger_and_distribute_to_instance_variables()
|
69
|
+
|
70
|
+
# An array to keep the best object per goal.
|
71
|
+
@best_objects = Array.new
|
37
72
|
|
38
|
-
# Return the number of aspects/sub-objectives of this objective.
|
39
|
-
def num_aspects
|
40
|
-
@num_aspects ||= aspect_methods.length
|
41
73
|
end
|
42
74
|
|
43
|
-
|
44
|
-
|
75
|
+
# Return the number of goals of this objective.
|
76
|
+
def num_goals
|
77
|
+
@num_goals ||= goal_methods.length
|
45
78
|
end
|
46
79
|
|
47
|
-
|
48
|
-
|
80
|
+
# Return the names of the goal methods.
|
81
|
+
def goal_methods
|
82
|
+
@goal_methods ||= self.methods.select {|m| is_goal_method?(m)}
|
49
83
|
end
|
50
84
|
|
51
|
-
|
52
|
-
|
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]
|
53
88
|
end
|
54
89
|
|
55
|
-
|
56
|
-
|
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
|
57
93
|
end
|
58
94
|
|
59
|
-
|
60
|
-
|
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
|
61
98
|
end
|
62
99
|
|
63
|
-
# The
|
64
|
-
#
|
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
|
65
102
|
# can override this for more complex evaluation schemes.
|
66
|
-
def
|
67
|
-
|
103
|
+
def map_candidate_to_object_to_be_evaluated(candidate)
|
104
|
+
candidate
|
68
105
|
end
|
69
106
|
|
70
|
-
#
|
71
|
-
#
|
72
|
-
def
|
73
|
-
|
74
|
-
aspect_methods.map {|omethod| self.send(omethod, candidate)}
|
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)
|
75
111
|
end
|
76
112
|
|
77
|
-
#
|
78
|
-
#
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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)
|
116
|
+
|
117
|
+
raise "Must be same number of weights as there are goals (#{num_aspects}), but is #{weights.length}" unless weights.length == num_goals
|
118
|
+
|
119
|
+
weights = goal_methods.map {|gm| goalNameToNumberHash[gm]}
|
120
|
+
|
121
|
+
#log_value( :objective_weights_changed, goalNameToNumberHash,
|
122
|
+
# "Weights updated from #{@weights} to #{weights}" )
|
123
|
+
|
124
|
+
inc_version_number
|
125
|
+
|
126
|
+
@weights = weights
|
127
|
+
|
85
128
|
end
|
86
129
|
|
87
|
-
#
|
88
|
-
#
|
89
|
-
def
|
90
|
-
|
91
|
-
|
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
|
92
137
|
end
|
93
138
|
|
94
|
-
#
|
95
|
-
|
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)
|
143
|
+
|
144
|
+
q = quality_if_up_to_date?(candidate)
|
145
|
+
return q if q
|
146
|
+
|
147
|
+
sub_qualities = sub_qualities_of(candidate)
|
148
|
+
|
149
|
+
qv = update_quality_value_of candidate, sub_qualities, weights
|
150
|
+
|
151
|
+
update_best_candidate candidate, qv
|
152
|
+
|
153
|
+
qv
|
96
154
|
|
97
|
-
# Return the fitness of a candidate. It is the same as the quality value above.
|
98
|
-
def fitness_for(candidate, weights = nil)
|
99
|
-
quality_of(candidate, weights)
|
100
155
|
end
|
101
156
|
|
102
|
-
|
103
|
-
|
104
|
-
|
157
|
+
# Rank candidates from best to worst. Updates the quality value of each
|
158
|
+
# candidate.
|
159
|
+
def rank_candidates(candidates, weights = self.weights)
|
105
160
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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)}
|
164
|
+
|
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]
|
110
171
|
end
|
111
172
|
|
112
|
-
#
|
113
|
-
|
114
|
-
|
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
|
115
177
|
|
116
|
-
|
117
|
-
|
178
|
+
# Now use the comparator to rank the candidates.
|
179
|
+
comparator.rank_candidates candidates, weights
|
118
180
|
|
119
|
-
def update_best_candidate(candidate)
|
120
|
-
@best_candidate = candidate
|
121
|
-
@best_qv = candidate._quality_value
|
122
181
|
end
|
123
182
|
|
124
|
-
|
125
|
-
|
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)
|
126
187
|
end
|
127
188
|
|
128
|
-
|
129
|
-
|
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)
|
130
195
|
end
|
131
196
|
|
132
|
-
def
|
133
|
-
|
134
|
-
object._objective_version = current_version
|
135
|
-
object._quality_value_without_check = qv
|
197
|
+
def note_end_of_optimization(optimizer)
|
198
|
+
nil
|
136
199
|
end
|
137
200
|
|
138
|
-
|
139
|
-
|
140
|
-
|
201
|
+
attr_reader :best_candidate
|
202
|
+
|
203
|
+
private
|
204
|
+
|
205
|
+
def update_quality_value_of(candidate, subQualities, weights)
|
206
|
+
|
207
|
+
q = quality_mapper.map_from_sub_qualities(subQualities, weights)
|
208
|
+
|
209
|
+
qv = QualityValue.new q, subQualities, candidate, self
|
210
|
+
|
211
|
+
update_quality_value_in_object candidate, qv
|
212
|
+
|
141
213
|
end
|
142
214
|
|
143
|
-
def
|
144
|
-
|
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
|
145
219
|
end
|
146
220
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
end.sort_by {|a| -a[1]} # sort by the ratio values in descending order
|
221
|
+
def set_new_best_candidate candidate, qualityValue
|
222
|
+
|
223
|
+
@best_candidate = candidate
|
224
|
+
@best_quality_value = qualityValue
|
225
|
+
|
226
|
+
#log_data :objective_new_best_candidate, {
|
227
|
+
# :candidate => candidate,
|
228
|
+
# :quality_value => qualityValue
|
229
|
+
#}, "New best candidate found"
|
230
|
+
|
158
231
|
end
|
159
232
|
|
160
|
-
def
|
161
|
-
|
233
|
+
def inc_version_number
|
234
|
+
|
235
|
+
new_version = @current_version + 1
|
236
|
+
|
237
|
+
#log_value :objective_version_number, new_version,
|
238
|
+
# "New version of objective:\n#{self.to_s}"
|
239
|
+
|
240
|
+
@current_version = new_version
|
241
|
+
|
162
242
|
end
|
163
243
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
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
|
168
250
|
end
|
169
251
|
|
170
|
-
#
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
176
272
|
end
|
177
273
|
|
178
|
-
#
|
179
|
-
|
180
|
-
|
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
|
+
|
181
296
|
end
|
182
297
|
|
183
|
-
#
|
184
|
-
|
185
|
-
|
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
|
186
303
|
end
|
187
304
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
numerator = max - value
|
192
|
-
else
|
193
|
-
numerator = value - min
|
194
|
-
end
|
195
|
-
numerator.to_f.protected_division_with(max - min)
|
305
|
+
# Get the hash of annotations that we have done to this object.
|
306
|
+
def my_annotations(object)
|
307
|
+
object._annotations[self] ||= Hash.new
|
196
308
|
end
|
197
309
|
|
198
|
-
def
|
199
|
-
|
310
|
+
def quality_in_object(object)
|
311
|
+
my_annotations(object)[:quality]
|
200
312
|
end
|
201
313
|
|
202
|
-
def
|
203
|
-
|
204
|
-
if value < min
|
205
|
-
reset_quality_scale(candidate, aspectIndex, :min)
|
206
|
-
global_min_values_per_aspect[aspectIndex] = value
|
207
|
-
log_new_min_max(aspectIndex, value, min, "min")
|
208
|
-
end
|
209
|
-
max = global_max_values_per_aspect[aspectIndex]
|
210
|
-
if value > max
|
211
|
-
reset_quality_scale(candidate, aspectIndex, :max)
|
212
|
-
global_max_values_per_aspect[aspectIndex] = value
|
213
|
-
log_new_min_max(aspectIndex, value, max, "max")
|
214
|
-
end
|
314
|
+
def update_quality_value_in_object(object, qv)
|
315
|
+
my_annotations(object)[:quality] = qv
|
215
316
|
end
|
317
|
+
end
|
216
318
|
|
217
|
-
|
218
|
-
|
219
|
-
|
319
|
+
# A QualityMapper maps a vector of sub-quality values (for each individual goal of
|
320
|
+
# an objective) into a single number on which the candidates can be compared.
|
321
|
+
class Objective::QualityMapper
|
322
|
+
attr_reader :objective
|
220
323
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
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
|
225
329
|
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
335
|
+
end
|
229
336
|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
337
|
+
# A WeightedSumMapper sums individual quality values, each multiplied with a
|
338
|
+
# 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])
|
235
344
|
end
|
345
|
+
sum
|
346
|
+
end
|
347
|
+
end
|
236
348
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
349
|
+
# A SumOfWeightedGlobalRatios implements Bentley's SWGR multi-objective
|
350
|
+
# fitness mapping scheme as described in the paper:
|
351
|
+
# P. J. Bentley and J. P. Wakefield, "Finding Acceptable Solutions in the
|
352
|
+
# Pareto-Optimal Range using Multiobjective Genetic Algorithms", 1997
|
353
|
+
# 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.
|
355
|
+
# One of its benefits is that one need not sort individuals in relation to
|
356
|
+
# their peers; the aggregate fitness value is fully determined by the individual
|
357
|
+
# 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
|
245
365
|
end
|
366
|
+
numerator.to_f.protected_division_with(max - min)
|
367
|
+
end
|
246
368
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
num_same = subQvRatioDiffs.length - num_inc - num_dec
|
251
|
-
"#{num_inc} increased, #{num_dec} decreased, #{num_same} same"
|
252
|
-
end
|
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
|
253
372
|
|
254
|
-
|
255
|
-
|
256
|
-
sqs = subQvs.map do |sqv|
|
257
|
-
s = (Float === sqv ? sqv.round_to_decimals(4) : sqv).inspect
|
258
|
-
s += "%" if subQvsAreRatios
|
259
|
-
s
|
260
|
-
end.join(", ")
|
261
|
-
if qvIsRatio
|
262
|
-
qstr = ("%.4f" % (100.0 * qv)) + "%"
|
263
|
-
else
|
264
|
-
qstr = "%.4f" % qv
|
265
|
-
end
|
266
|
-
"#{qvDesc}: #{qstr}, #{subQvDesc}: [#{sqs}]"
|
373
|
+
ratios = subQualityValues.map_with_index do |v, i|
|
374
|
+
ratio i, v, goal_mins[i], goal_maxs[i]
|
267
375
|
end
|
268
376
|
|
269
|
-
|
270
|
-
|
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])
|
271
382
|
end
|
272
383
|
|
273
|
-
|
274
|
-
def method_missing(meth, *args, &block)
|
275
|
-
@qv.send(meth, *args, &block)
|
276
|
-
end
|
384
|
+
sum / weights.sum.to_f
|
277
385
|
end
|
386
|
+
end
|
387
|
+
|
388
|
+
# A Comparator ranks a set of candidates based on their sub-qualities.
|
389
|
+
# 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
|
278
393
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
"scale is now [#{global_min_values_per_aspect[index]}, #{global_max_values_per_aspect[index]}]",
|
284
|
-
"objective version = #{current_version}")
|
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}
|
285
398
|
end
|
399
|
+
end
|
400
|
+
|
401
|
+
# Class for representing multi-objective _sub_qualitites_ and their summary
|
402
|
+
# _value_. A quality has a version number which was the version of
|
403
|
+
# the objective when this quality was calculated. When a quality value
|
404
|
+
# is compared to another quality value they are first updated so that
|
405
|
+
# they reflect the quality of the candidate for the current version of
|
406
|
+
# the objective.
|
407
|
+
class QualityValue
|
408
|
+
include Comparable
|
286
409
|
|
287
|
-
|
288
|
-
|
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
|
289
416
|
end
|
290
417
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
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
|
296
426
|
end
|
297
427
|
|
298
|
-
#
|
299
|
-
#
|
300
|
-
|
301
|
-
|
302
|
-
|
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])
|
303
434
|
end
|
304
|
-
end
|
305
435
|
|
306
|
-
|
307
|
-
|
308
|
-
|
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
|
309
440
|
end
|
310
441
|
|
311
442
|
# Short hand for when the objective function is given as a block that should be minimized.
|
@@ -332,14 +463,4 @@ class ObjectiveMaximizeBlock < Objective
|
|
332
463
|
end
|
333
464
|
end
|
334
465
|
|
335
|
-
end
|
336
|
-
|
337
|
-
# We add strangely named accessor methods so we can attach the quality values to objects.
|
338
|
-
# We use strange names to minimize risk of method name conflicts.
|
339
|
-
class Object
|
340
|
-
attr_accessor :_quality_value_without_check, :_objective, :_objective_version
|
341
|
-
def _quality_value
|
342
|
-
@_objective.ensure_updated_quality_value(self) if defined?(@_objective) && @_objective
|
343
|
-
@_quality_value_without_check ||= nil # To avoid warning if unset
|
344
|
-
end
|
345
466
|
end
|