feldtruby 0.3.16 → 0.3.18
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.
- 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
|