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.
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