feldtruby 0.3.16 → 0.3.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile.lock +9 -2
  3. data/Rakefile +8 -0
  4. data/feldtruby.gemspec +6 -0
  5. data/lib/feldtruby/annotations.rb +10 -0
  6. data/lib/feldtruby/array/basic_stats.rb +3 -1
  7. data/lib/feldtruby/array/permutations_and_subsets.rb +17 -0
  8. data/lib/feldtruby/float.rb +23 -0
  9. data/lib/feldtruby/logger.rb +216 -30
  10. data/lib/feldtruby/minitest_extensions.rb +0 -1
  11. data/lib/feldtruby/mongodb.rb +16 -0
  12. data/lib/feldtruby/mongodb_logger.rb +245 -0
  13. data/lib/feldtruby/optimize/differential_evolution.rb +29 -5
  14. data/lib/feldtruby/optimize/elite_archive.rb +91 -0
  15. data/lib/feldtruby/optimize/max_steps_termination_criterion.rb +1 -1
  16. data/lib/feldtruby/optimize/objective.rb +343 -222
  17. data/lib/feldtruby/optimize/optimizer.rb +138 -60
  18. data/lib/feldtruby/optimize/search_space.rb +10 -0
  19. data/lib/feldtruby/optimize.rb +1 -26
  20. data/lib/feldtruby/statistics.rb +74 -3
  21. data/lib/feldtruby/time.rb +19 -0
  22. data/lib/feldtruby/version.rb +1 -1
  23. data/old/event_logger.rb +682 -0
  24. data/spikes/comparing_samplers_on_classic_optimization_functions/analyze_sampler_comparison_results.R +78 -0
  25. data/spikes/comparing_samplers_on_classic_optimization_functions/compare_samplers.rb +264 -0
  26. data/spikes/comparing_samplers_on_classic_optimization_functions/results_comparing_samplers_130405_175934.csv +561 -0
  27. data/spikes/comparing_samplers_on_classic_optimization_functions/results_comparing_samplers_levi13_beale_easom_eggholder.csv +11201 -0
  28. data/spikes/comparing_samplers_on_classic_optimization_functions/results_comparing_samplers_levi13_beale_easom_eggholder_all_radii_4_to_30.csv +44801 -0
  29. data/spikes/comparing_samplers_on_classic_optimization_functions/results_comparing_samplers_omnitest.csv +1401 -0
  30. data/spikes/mongodb_logger.rb +47 -0
  31. data/spikes/simple_de_run.rb +32 -0
  32. data/test/helper.rb +17 -1
  33. data/test/test_array_basic_stats.rb +5 -1
  34. data/test/test_array_permutations_and_subsets.rb +23 -0
  35. data/test/test_float.rb +15 -0
  36. data/test/test_html_doc_getter.rb +1 -1
  37. data/test/test_logger.rb +86 -48
  38. data/test/test_mongodb_logger.rb +116 -0
  39. data/test/test_object_annotations.rb +14 -0
  40. data/test/test_optimize.rb +7 -6
  41. data/test/test_optimize_differential_evolution.rb +21 -19
  42. data/test/test_optimize_elite_archive.rb +85 -0
  43. data/test/test_optimize_objective.rb +237 -74
  44. data/test/test_optimize_populationbasedoptimizer.rb +72 -6
  45. data/test/test_optimize_random_search.rb +0 -17
  46. data/test/test_optimize_search_space.rb +15 -0
  47. data/test/test_statistics.rb +30 -4
  48. data/test/test_time.rb +22 -0
  49. data/test/tmp_shorter.csv +200 -0
  50. 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 captures one or more (sub-)objectives into a single object
8
- # and supports a large number of ways to utilize basic objective
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
- # There can be multiple aspects (sub-objectives) for a single objective.
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 sub-objective.
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 sub-objective.
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
- # Current version of this objective. Is updated when the min or max values for a sub-objective
26
- # has been updated.
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
- # For logging changes to the objective.
30
- attr_accessor :logger
42
+ # A quality mapper maps the goal values of a candidate to a single number.
43
+ attr_accessor :quality_mapper
31
44
 
32
- def initialize
33
- @logger = nil # To avoid getting warnings that logger has not been initialized
34
- @current_version = 0
35
- @pareto_front = Array.new(num_aspects) # The pareto front is misguided since it only has one best value per sub-objective, not the whole front!
36
- end
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
- def num_sub_objectives
44
- num_aspects
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
- def aspect_methods
48
- @aspect_methods ||= self.methods.select {|m| is_aspect_method?(m)}
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
- def is_min_aspect?(aspectIndex)
52
- (@is_min_aspect ||= (aspect_methods.map {|m| is_min_aspect_method?(m)}))[aspectIndex]
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
- def is_aspect_method?(methodNameAsSymbolOrString)
56
- methodNameAsSymbolOrString.to_s =~ /^objective_(min|max)_([\w_]+)$/
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
- def is_min_aspect_method?(methodNameAsSymbolOrString)
60
- methodNameAsSymbolOrString.to_s =~ /^objective_min_([\w_]+)$/
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 vectors can be mapped to a more complex candidate object before we call
64
- # the sub objectives to calc their quality values. Default is no mapping but subclasses
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 map_candidate_vector_to_candidate_to_be_evaluated(vector)
67
- vector
103
+ def map_candidate_to_object_to_be_evaluated(candidate)
104
+ candidate
68
105
  end
69
106
 
70
- # Return a vector of the "raw" sub-quality values, i.e. the fitness value for each sub-objective.
71
- # The candidate vector is assumed to be a vector of values.
72
- def sub_qualities_of(candidateVector)
73
- candidate = map_candidate_vector_to_candidate_to_be_evaluated(candidateVector)
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
- # Return a single quality value for the whole objective for a given candidate.
78
- # By default this uses a variant of Bentley and Wakefield's sum-of-weighted-global-ratios (SWGR)
79
- # called mean-of-weighted-global-ratios (MWGR) which always returns a fitness value
80
- # in the range (0.0, 1.0) with 1.0 signaling the best fitness seen so far. The scale is adaptive
81
- # though so that the best candidate so far always has a fitness value of 1.0.
82
- def quality_of(candidate, weights = self.default_weights)
83
- return candidate._quality_value_without_check if quality_value_is_up_to_date?(candidate)
84
- num_aspects == 1 ? qv_single(candidate) : qv_mwgr(candidate, weights)
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
- # Set the default weights to use when calculating a single quality values from
88
- # the vector of sub-qualities.
89
- def default_weights=(weights)
90
- raise "Must be same number of weights as there are sub-objectives (#{num_aspects}), but is #{weights.length}" unless weights.length == num_aspects
91
- @default_weights = weights
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
- # Current default weights among the sub-objectives (nil if none have been set)
95
- attr_reader :default_weights
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
- # Sane above here!
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
- def reset_quality_scale(candidate, aspectIndex, typeOfReset)
107
- if (typeOfReset == :min && is_min_aspect?(aspectIndex)) ||
108
- (typeOfReset == :max && !is_min_aspect?(aspectIndex))
109
- @pareto_front[aspectIndex] = candidate
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
- # Reset the best object since we have a new scale
113
- @best_candidate = nil
114
- @best_qv = nil
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
- inc_version_number
117
- end
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
- def inc_version_number
125
- @current_version += 1
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
- def quality_value_is_up_to_date?(candidate)
129
- candidate._objective == self && candidate._objective_version == current_version
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 update_quality_value_in_object(object, qv)
133
- object._objective = self
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
- def ensure_updated_quality_value(candidate)
139
- return if quality_value_is_up_to_date?(candidate)
140
- quality_value(candidate)
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 rank_candidates(candidates, weights = nil)
144
- mwgr_rank_candidates(candidates, weights)
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
- # Rand candidates from best to worst. NOTE! We do the steps of MWGR separately since we must
148
- # update the global mins and maxs before calculating the SWG ratios.
149
- def mwgr_rank_candidates(candidates, weights = nil)
150
- sub_qvss = candidates.map {|c| sub_qualities_of(c)}
151
- sub_qvss.zip(candidates).each {|sub_qvs, c| update_global_mins_and_maxs(sub_qvs, c)}
152
- sub_qvss.each_with_index.map do |sub_qvs, i|
153
- qv = mwgr_ratios(sub_qvs).weighted_mean(weights)
154
- qv = QualityValue.new(qv, sub_qvs, self)
155
- update_quality_value_in_object(candidates[i], qv)
156
- [candidates[i], qv, sub_qvs]
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 note_end_of_optimization(optimizer)
161
- log("Objective reporting the Pareto front", info_pareto_front())
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
- def info_pareto_front
165
- @pareto_front.each_with_index.map do |c, i|
166
- "Pareto front candidate for objective #{aspect_methods[i]}: #{map_candidate_vector_to_candidate_to_be_evaluated(c).inspect}"
167
- end.join("\n")
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
- # Return the quality value assuming this is a single objective.
171
- def qv_single(candidate)
172
- qv = self.send(aspect_methods.first,
173
- map_candidate_vector_to_candidate_to_be_evaluated(candidate))
174
- update_quality_value_in_object(candidate, qv)
175
- qv
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
- # Mean-of-weigthed-global-ratios (MWGR) quality value
179
- def qv_mwgr(candidate, weights = nil)
180
- mwgr_rank_candidates([candidate], weights).first[1]
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
- # Calculate the SWGR ratios
184
- def mwgr_ratios(subObjectiveValues)
185
- subObjectiveValues.each_with_index.map {|v,i| ratio_for_aspect(i, v)}
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
- def ratio_for_aspect(aspectIndex, value)
189
- min, max = global_min_values_per_aspect[aspectIndex], global_max_values_per_aspect[aspectIndex]
190
- if is_min_aspect?(aspectIndex)
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 update_global_mins_and_maxs(aspectValues, candidate = nil)
199
- aspectValues.each_with_index {|v, i| update_global_min_and_max(i, v, candidate)}
310
+ def quality_in_object(object)
311
+ my_annotations(object)[:quality]
200
312
  end
201
313
 
202
- def update_global_min_and_max(aspectIndex, value, candidate)
203
- min = global_min_values_per_aspect[aspectIndex]
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
- # Class for representing multi-objective qualitites...
218
- class QualityValue
219
- attr_reader :qv, :sub_qvs, :objective
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
- def initialize(qv, subQvs, objective)
222
- @qv, @sub_qvs, @objective = qv, subQvs, objective
223
- @version = objective.current_version
224
- end
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
- def <=>(other)
227
- @qv <=> other.qv
228
- end
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
- # Two quality values are the same if they have the same qv, regardless of their
231
- # sub qualities.
232
- def ==(other)
233
- other = other.qv if QualityValue === other
234
- @qv == other
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
- def improvement_in_relation_to(other)
238
- if QualityValue === other
239
- pdiff = @qv.ratio_diff_vs(other.qv)
240
- subpdiffs = @sub_qvs.zip(other.sub_qvs).map {|s, os| s.ratio_diff_vs(os)}
241
- qinspect(pdiff, subpdiffs, "Difference", "SubQ. differences", true) + ", #{report_on_num_differences(subpdiffs)}"
242
- else
243
- @qv.improvement_in_relation_to(other)
244
- end
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
- def report_on_num_differences(subQvRatioDiffs)
248
- num_inc = subQvRatioDiffs.select {|v| v > 0}.length
249
- num_dec = subQvRatioDiffs.select {|v| v < 0}.length
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
- def qinspect(qv, subQvs, qvDesc = "Quality", subQvDesc = "SubQualities", subQvsAreRatios = false, qvIsRatio = true)
255
- subQvs = subQvs.map {|v| v*100.0} if subQvsAreRatios
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
- def inspect
270
- qinspect(@qv, @sub_qvs) + ", Obj. version: #{@version}"
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
- # Refer all other methods to the main quality value
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
- def log_new_min_max(index, newValue, oldValue, description)
280
- log("New global #{description} for sub-objective #{aspect_methods[index]}",
281
- ("a %.3f" % (100.0 * (newValue - oldValue).protected_division_with(oldValue))) + "% difference",
282
- "new = #{newValue}, old = #{oldValue}",
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
- def log(msg, *values)
288
- @logger.anote(msg, *values) if @logger
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
- # Global min values for each aspect. Needed for SWGR. Updated every time we see a new
292
- # quality value for an aspect.
293
- # All are minus infinity when we have not seen any values yet.
294
- def global_min_values_per_aspect
295
- @global_min_values_per_aspect ||= Array.new(num_aspects).map {Float::INFINITY}
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
- # Global max values for each aspect. Needed for SWGR. Updated every time we see a new
299
- # quality value for an aspect.
300
- # All are minus infinity when we have not seen any values yet.
301
- def global_max_values_per_aspect
302
- @global_max_values_per_aspect ||= Array.new(num_aspects).map {-Float::INFINITY}
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
- # The MWGR is a simple way to weigh the fitness values of multiple sub-objectives into a single
307
- # fitness value.
308
- module MeanWeigthedGlobalRatios
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