1 /* Copyright 2017-2021 PaGMO development team
2
3 This file is part of the PaGMO library.
4
5 The PaGMO library is free software; you can redistribute it and/or modify
6 it under the terms of either:
7
8 * the GNU Lesser General Public License as published by the Free
9 Software Foundation; either version 3 of the License, or (at your
10 option) any later version.
11
12 or
13
14 * the GNU General Public License as published by the Free Software
15 Foundation; either version 3 of the License, or (at your option) any
16 later version.
17
18 or both in parallel, as here.
19
20 The PaGMO library is distributed in the hope that it will be useful, but
21 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
22 or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
23 for more details.
24
25 You should have received copies of the GNU General Public License and the
26 GNU Lesser General Public License along with the PaGMO library. If not,
27 see https://www.gnu.org/licenses/. */
28
29 #include <algorithm>
30 #include <cmath>
31 #include <memory>
32 #include <random>
33 #include <stdexcept>
34 #include <string>
35 #include <vector>
36
37 #include <pagmo/exceptions.hpp>
38 #include <pagmo/types.hpp>
39 #include <pagmo/utils/hv_algos/hv_algorithm.hpp>
40 #include <pagmo/utils/hv_algos/hv_bf_approx.hpp>
41 #include <pagmo/utils/hypervolume.hpp>
42
43 #if defined(_MSC_VER)
44
45 #pragma warning(push)
46 #pragma warning(disable : 4646)
47
48 #endif
49
50 namespace pagmo
51 {
52
53 // Constructor
bf_approx(bool use_exact,unsigned trivial_subcase_size,double eps,double delta,double delta_multiplier,double alpha,double initial_delta_coeff,double gamma,unsigned seed)54 bf_approx::bf_approx(bool use_exact, unsigned trivial_subcase_size, double eps, double delta, double delta_multiplier,
55 double alpha, double initial_delta_coeff, double gamma, unsigned seed)
56 : m_use_exact(use_exact), m_trivial_subcase_size(trivial_subcase_size), m_eps(eps), m_delta(delta),
57 m_delta_multiplier(delta_multiplier), m_alpha(alpha), m_initial_delta_coeff(initial_delta_coeff), m_gamma(gamma),
58 m_e(seed)
59 {
60 if (eps < 0 || eps > 1) {
61 pagmo_throw(std::invalid_argument, "Epsilon needs to be a probability.");
62 }
63 if (delta < 0 || delta > 1) {
64 pagmo_throw(std::invalid_argument, "Delta needs to be a probability.");
65 }
66 }
67
68 /// Compute hypervolume
69 /**
70 * This method is overloaded to throw an exception in case a hypervolume indicator computation is requested.
71 *
72 * @return Nothing as it throws before.
73 * @throws std::invalid_argument whenever called
74 */
compute(std::vector<vector_double> &,const vector_double &) const75 double bf_approx::compute(std::vector<vector_double> &, const vector_double &) const
76 {
77 pagmo_throw(std::invalid_argument,
78 "This algorithm can just approximate extreme contributions but not the hypervolume itself.");
79 }
80
81 /// Least contributor method
82 /**
83 * This method establishes the individual that contributes the least to the hypervolume (approximately withing given
84 * epsilon and delta).
85 *
86 * @param points vector of fitness_vectors for which the hypervolume is computed
87 * @param r_point distinguished "reference point".
88 *
89 * @return index of the least contributing point
90 */
least_contributor(std::vector<vector_double> & points,const vector_double & r_point) const91 unsigned long long bf_approx::least_contributor(std::vector<vector_double> &points, const vector_double &r_point) const
92 {
93 return approx_extreme_contributor(
94 points, r_point, LEAST, [](double a, double b) { return a < b; }, lc_erase_condition, lc_end_condition);
95 }
96
97 /// Greatest contributor method
98 /**
99 * This method establishes the individual that contributes the most to the hypervolume (approximately withing given
100 * epsilon and delta).
101 *
102 * @param points vector of fitness_vectors for which the hypervolume is computed
103 * @param r_point distinguished "reference point".
104 *
105 * @return index of the greatest contributing point
106 */
greatest_contributor(std::vector<vector_double> & points,const vector_double & r_point) const107 unsigned long long bf_approx::greatest_contributor(std::vector<vector_double> &points,
108 const vector_double &r_point) const
109 {
110 return approx_extreme_contributor(
111 points, r_point, GREATEST, [](double a, double b) { return a > b; }, gc_erase_condition, gc_end_condition);
112 }
113
114 /// Verify before compute method
115 /**
116 * Verifies whether given algorithm suits the requested data.
117 *
118 * @param points vector of points containing the d dimensional points for which we compute the hypervolume
119 * @param r_point reference point for the vector of points
120 *
121 * @throws value_error when trying to compute the hypervolume for the non-maximal reference point
122 */
verify_before_compute(const std::vector<vector_double> & points,const vector_double & r_point) const123 void bf_approx::verify_before_compute(const std::vector<vector_double> &points, const vector_double &r_point) const
124 {
125 hv_algorithm::assert_minimisation(points, r_point);
126 }
127
128 /// Clone method.
129 /**
130 * @return a pointer to a new object cloning this
131 */
clone() const132 std::shared_ptr<hv_algorithm> bf_approx::clone() const
133 {
134 return std::shared_ptr<hv_algorithm>(new bf_approx(*this));
135 }
136
137 /// Algorithm name
138 /**
139 * @return The name of this particular algorithm
140 */
get_name() const141 std::string bf_approx::get_name() const
142 {
143 return "Bringmann-Friedrich approximation method";
144 }
145
146 /// Compute delta for given point
147 /**
148 * Uses chernoff inequality as it was proposed in the article by Bringmann and Friedrich
149 * The parameters of the method are taked from the Shark implementation of the algorithm.
150 */
compute_point_delta(unsigned round_no,vector_double::size_type idx,double log_factor) const151 double bf_approx::compute_point_delta(unsigned round_no, vector_double::size_type idx, double log_factor) const
152 {
153 return std::sqrt(0.5 * ((1. + m_gamma) * std::log(static_cast<double>(round_no)) + log_factor)
154 / (static_cast<double>(m_no_samples[idx])));
155 }
156
157 /// Compute bounding box method
158 /* Find the MINIMAL (in terms of volume) bounding box that contains all the exclusive hypervolume contributed by
159 * point at index 'p_idx'
160 * Thus obtained point 'z' and the original point 'points[p_idx]' form a box in which we perform the monte carlo
161 * sampling
162 *
163 * @param points pareto front points
164 * @param r_point reference point
165 * @param p_idx index of point for which we compute the bounding box
166 *
167 * @return fitness_vector describing the opposite corner of the bounding box
168 */
compute_bounding_box(const std::vector<vector_double> & points,const vector_double & r_point,vector_double::size_type p_idx) const169 vector_double bf_approx::compute_bounding_box(const std::vector<vector_double> &points, const vector_double &r_point,
170 vector_double::size_type p_idx) const
171 {
172 // z is the opposite corner of the bounding box (reference point as a 'safe' first candidate - this is the
173 // MAXIMAL bounding box as of yet)
174 vector_double z(r_point);
175
176 // Below we perform a reduction to the minimal bounding box.
177 // We check whether given point at 'idx' is DOMINATED (strong domination) by 'p_idx' in exactly one objective,
178 // and DOMINATING (weak domination) in the remaining ones.
179 const vector_double &p = points[p_idx];
180 vector_double::size_type worse_dim_idx = 0u;
181 auto f_dim = r_point.size();
182 for (decltype(points.size()) idx = 0u; idx < points.size(); ++idx) {
183 auto flag = false; // initiate the possible opposite point dimension by -1 (no candidate)
184
185 for (decltype(f_dim) f_idx = 0u; f_idx < f_dim; ++f_idx) {
186 if (points[idx][f_idx] >= p[f_idx]) { // if any point is worse by given dimension, it's the potential
187 // dimension in which we reduce the box
188 if (flag) { // if given point is already worse in any previous dimension, skip to
189 // next point as it's a bad candidate
190 flag = false; // set the result to "no candidate" and break
191 break;
192 }
193 worse_dim_idx = f_idx;
194 flag = true;
195 }
196 }
197 if (flag) { // if given point was worse only in one dimension it's the potential candidate
198 // for the bouding box reductor
199 z[worse_dim_idx] = std::min(z[worse_dim_idx], points[idx][worse_dim_idx]); // reduce the bounding box
200 }
201 }
202 return z;
203 }
204
205 /// Determine whether point 'p' influences the volume of box (a, b)
206 /**
207 * return 0 - box (p, R) has no overlapping volume with box (a, b)
208 * return 1 - box (p, R) overlaps some volume with the box (a, b)
209 * return 2 - point p dominates the point a (in which case, contribution by box (a, b) is guaranteed to be 0)
210 * return 3 - point p is equal to point a (box (a, b) also contributes 0 hypervolume)
211 */
point_in_box(const vector_double & p,const vector_double & a,const vector_double & b) const212 int bf_approx::point_in_box(const vector_double &p, const vector_double &a, const vector_double &b) const
213 {
214 int cmp_a_p = hv_algorithm::dom_cmp(a, p, 0);
215
216 // point a is equal to point p (duplicate)
217 if (cmp_a_p == 3) {
218 return 3;
219 // point a is dominated by p (a is the least contributor)
220 } else if (cmp_a_p == 1) {
221 return 2;
222 } else if (hv_algorithm::dom_cmp(b, p, 0) == 1) {
223 return 1;
224 } else {
225 return 0;
226 }
227 }
228
229 /// Performs a single round of sampling for given point at index 'idx'
sampling_round(const std::vector<vector_double> & points,double delta,unsigned round,vector_double::size_type idx,double log_factor) const230 void bf_approx::sampling_round(const std::vector<vector_double> &points, double delta, unsigned round,
231 vector_double::size_type idx, double log_factor) const
232 {
233 if (m_use_exact) {
234 // if the sampling for given point was already resolved using exact method
235 if (m_no_ops[idx] == 0) {
236 return;
237 }
238
239 // if the exact computation is trivial OR when the sampling takes too long in terms of elementary operations
240 if (m_box_points[idx].size() <= m_trivial_subcase_size
241 || static_cast<double>(m_no_ops[idx])
242 >= detail::expected_hv_operations(m_box_points[idx].size(), points[0].size())) {
243 const std::vector<vector_double::size_type> &bp = m_box_points[idx];
244 if (bp.size() == 0u) {
245 m_approx_volume[idx] = m_box_volume[idx];
246 } else {
247
248 auto f_dim = points[0].size();
249
250 const vector_double &p = points[idx];
251
252 std::vector<vector_double> sub_front(bp.size(), vector_double(f_dim, 0.0));
253
254 for (decltype(sub_front.size()) p_idx = 0u; p_idx < sub_front.size(); ++p_idx) {
255 for (decltype(sub_front[0].size()) d_idx = 0u; d_idx < sub_front[0].size(); ++d_idx) {
256 sub_front[p_idx][d_idx] = std::max(p[d_idx], points[bp[p_idx]][d_idx]);
257 }
258 }
259
260 const vector_double &refpoint = m_boxes[idx];
261 hypervolume hv_obj = hypervolume(sub_front, false);
262 hv_obj.set_copy_points(false);
263 double hv = hv_obj.compute(refpoint);
264 m_approx_volume[idx] = m_box_volume[idx] - hv;
265 }
266
267 m_point_delta[idx] = 0.0;
268 m_no_ops[idx] = 0;
269
270 return;
271 }
272 }
273
274 double tmp = m_box_volume[idx] / delta;
275 double required_no_samples = 0.5 * ((1. + m_gamma) * std::log(round) + log_factor) * tmp * tmp;
276
277 while (static_cast<double>(m_no_samples[idx]) < required_no_samples) {
278 ++m_no_samples[idx];
279 if (sample_successful(points, idx)) {
280 ++m_no_succ_samples[idx];
281 }
282 }
283
284 m_approx_volume[idx]
285 = static_cast<double>(m_no_succ_samples[idx]) / static_cast<double>(m_no_samples[idx]) * m_box_volume[idx];
286 m_point_delta[idx] = compute_point_delta(round, idx, log_factor) * m_box_volume[idx];
287 }
288
289 /// samples the bounding box and returns true if it fell into the exclusive hypervolume
sample_successful(const std::vector<vector_double> & points,vector_double::size_type idx) const290 bool bf_approx::sample_successful(const std::vector<vector_double> &points, vector_double::size_type idx) const
291 {
292 const vector_double &lb = points[idx];
293 const vector_double &ub = m_boxes[idx];
294 vector_double rnd_p(lb.size(), 0.0);
295
296 for (decltype(lb.size()) i = 0u; i < lb.size(); ++i) {
297 auto V_dist = std::uniform_real_distribution<double>(lb[i], ub[i]);
298 rnd_p[i] = V_dist(m_e);
299 }
300
301 for (decltype(m_box_points[idx].size()) i = 0u; i < m_box_points[idx].size(); ++i) {
302
303 // box_p is a point overlapping the bounding box volume
304 const vector_double &box_p = points[m_box_points[idx][i]];
305
306 // assume that box_p DOMINATES the random point and try to prove it otherwise below
307 bool dominates = true;
308
309 // increase the number of operations by the dimension size
310 m_no_ops[idx] += box_p.size() + 1;
311 for (decltype(box_p.size()) d_idx = 0u; d_idx < box_p.size(); ++d_idx) {
312 if (rnd_p[d_idx] < box_p[d_idx]) { // box_p does not dominate rnd_p
313 dominates = false;
314 break;
315 }
316 }
317 // if the box_p dominated the rnd_p return the sample as false
318 if (dominates) {
319 return false;
320 }
321 }
322 return true;
323 }
324
325 /// Approximated extreme contributor method
326 /**
327 * Compute the extreme contributor using the approximated algorithm.
328 * In order to make the original algorithm work for both the least and the greatest contributor, some portions
329 * of the code had to be removed to external methods.
330 * The following argument and functions are passed as arguments in the corresponding least/greatest contributor
331 * methods:
332 *
333 * - ec_type (argument):
334 * enum stating whether given execution aims to find the least or the greatest contributor.
335 * In either scenario, certain preprocessing steps are altered to determine it faster if possible.
336 *
337 * - cmp_func (function):
338 * Comparison function of two contributions, stating which of the contribution values fits our purpose (lesser or
339 * greater).
340 *
341 * - erase_condition (function):
342 * Determines whether current extreme contributor guarantees to exceed given candidate.
343 * In such case, the other point is removed from the racing.
344 *
345 * - end_condition (function):
346 * Determines whether given extreme contributor guarantees be accurate withing provided epsilon error.
347 * The return value of the function is the ratio, stating the estimated error.
348 */
approx_extreme_contributor(std::vector<vector_double> & points,const vector_double & r_point,extreme_contrib_type ec_type,bool (* cmp_func)(double,double),bool (* erase_condition)(vector_double::size_type,vector_double::size_type,vector_double &,vector_double &),double (* end_condition)(vector_double::size_type,vector_double::size_type,vector_double &,vector_double &)) const349 vector_double::size_type bf_approx::approx_extreme_contributor(
350 std::vector<vector_double> &points, const vector_double &r_point, extreme_contrib_type ec_type,
351 bool (*cmp_func)(double, double),
352 bool (*erase_condition)(vector_double::size_type, vector_double::size_type, vector_double &, vector_double &),
353 double (*end_condition)(vector_double::size_type, vector_double::size_type, vector_double &, vector_double &)) const
354 {
355 m_no_samples = std::vector<vector_double::size_type>(points.size(), 0);
356 m_no_succ_samples = std::vector<vector_double::size_type>(points.size(), 0);
357 m_no_ops = std::vector<vector_double::size_type>(points.size(), 1);
358 m_point_set = std::vector<vector_double::size_type>(points.size(), 0);
359 m_box_volume = vector_double(points.size(), 0.0);
360 m_approx_volume = vector_double(points.size(), 0.0);
361 m_point_delta = vector_double(points.size(), 0.0);
362 m_boxes = std::vector<vector_double>(points.size());
363 m_box_points = std::vector<std::vector<vector_double::size_type>>(points.size());
364
365 // precomputed log factor for the point delta computation
366 const double log_factor = std::log(2. * static_cast<double>(points.size()) * (1. + m_gamma) / (m_delta * m_gamma));
367
368 // round counter
369 unsigned round_no = 0u;
370
371 // round delta
372 double r_delta = 0.0;
373
374 bool stop_condition = false;
375
376 // index of extreme contributor
377 vector_double::size_type EC = 0u;
378
379 // put every point into the set
380 for (decltype(m_point_set.size()) i = 0u; i < m_point_set.size(); ++i) {
381 m_point_set[i] = i;
382 }
383
384 // Initial computation
385 // - compute bounding boxes and their hypervolume
386 // - set round Delta as max of hypervolumes
387 // - determine points overlapping with each bounding box
388 for (decltype(points.size()) idx = 0u; idx < points.size(); ++idx) {
389 m_boxes[idx] = compute_bounding_box(points, r_point, idx);
390 m_box_volume[idx] = hv_algorithm::volume_between(points[idx], m_boxes[idx]);
391 r_delta = std::max(r_delta, m_box_volume[idx]);
392
393 for (decltype(points.size()) idx2 = 0u; idx2 < points.size(); ++idx2) {
394 if (idx == idx2) {
395 continue;
396 }
397 int op = point_in_box(points[idx2], points[idx], m_boxes[idx]);
398 if (op == 1) {
399 m_box_points[idx].push_back(idx2);
400 } else if (ec_type == LEAST) {
401 // Execute extra checks specifically for the least contributor
402 switch (op) {
403 case 2:
404 // since contribution by idx is guaranteed to be 0.0 (as the point is dominated) we might as
405 // well return it as the least contributor right away
406 return idx;
407 case 3:
408 // points at idx and idx2 are equal, each of them will contribute 0.0 hypervolume, return
409 // idx as the least contributor
410 return idx;
411 default:
412 break;
413 }
414 }
415 }
416 }
417
418 // decrease the initial maximum volume by a constant factor
419 r_delta *= m_initial_delta_coeff;
420
421 // Main loop
422 do {
423 r_delta *= m_delta_multiplier;
424 ++round_no;
425
426 for (decltype(m_point_set.size()) _i = 0u; _i < m_point_set.size(); ++_i) {
427 auto idx = m_point_set[_i];
428 sampling_round(points, r_delta, round_no, idx, log_factor);
429 }
430
431 // sample the extreme contributor
432 sampling_round(points, m_alpha * r_delta, round_no, EC, log_factor);
433
434 // find the new extreme contributor
435 for (decltype(m_point_set.size()) _i = 0u; _i < m_point_set.size(); ++_i) {
436 auto idx = m_point_set[_i];
437 if (cmp_func(m_approx_volume[idx], m_approx_volume[EC])) {
438 EC = idx;
439 }
440 }
441
442 // erase known non-extreme contributors
443 // std::vector<unsigned>::iterator it = m_point_set.begin();
444 auto it = m_point_set.begin();
445 while (it != m_point_set.end()) {
446 auto idx = *it;
447 if ((idx != EC) && erase_condition(idx, EC, m_approx_volume, m_point_delta)) {
448 it = m_point_set.erase(it);
449 } else {
450 ++it;
451 }
452 }
453
454 // check termination condition
455 stop_condition = false;
456 if (m_point_set.size() <= 1) {
457 stop_condition = true;
458 } else {
459 stop_condition = true;
460 for (decltype(m_point_set.size()) _i = 0; _i < m_point_set.size(); ++_i) {
461 auto idx = m_point_set[_i];
462 if (idx == EC) {
463 continue;
464 }
465 double d = end_condition(idx, EC, m_approx_volume, m_point_delta);
466 if (d <= 0 || d > 1 + m_eps) {
467 stop_condition = false;
468 break;
469 }
470 }
471 }
472 } while (!stop_condition);
473
474 return EC;
475 }
476
lc_end_condition(vector_double::size_type idx,vector_double::size_type LC,vector_double & approx_volume,vector_double & point_delta)477 double bf_approx::lc_end_condition(vector_double::size_type idx, vector_double::size_type LC,
478 vector_double &approx_volume, vector_double &point_delta)
479 {
480 return (approx_volume[LC] + point_delta[LC]) / (approx_volume[idx] - point_delta[idx]);
481 }
482
gc_end_condition(vector_double::size_type idx,vector_double::size_type GC,vector_double & approx_volume,vector_double & point_delta)483 double bf_approx::gc_end_condition(vector_double::size_type idx, vector_double::size_type GC,
484 vector_double &approx_volume, vector_double &point_delta)
485 {
486 return (approx_volume[idx] + point_delta[idx]) / (approx_volume[GC] - point_delta[GC]);
487 }
488
lc_erase_condition(vector_double::size_type idx,vector_double::size_type LC,vector_double & approx_volume,vector_double & point_delta)489 bool bf_approx::lc_erase_condition(vector_double::size_type idx, vector_double::size_type LC,
490 vector_double &approx_volume, vector_double &point_delta)
491 {
492 return (approx_volume[idx] - point_delta[idx]) > (approx_volume[LC] + point_delta[LC]);
493 }
494
gc_erase_condition(vector_double::size_type idx,vector_double::size_type GC,vector_double & approx_volume,vector_double & point_delta)495 bool bf_approx::gc_erase_condition(vector_double::size_type idx, vector_double::size_type GC,
496 vector_double &approx_volume, vector_double &point_delta)
497 {
498 return (approx_volume[idx] + point_delta[idx]) < (approx_volume[GC] - point_delta[GC]);
499 }
500
501 } // namespace pagmo
502
503 #if defined(_MSC_VER)
504
505 #pragma warning(pop)
506
507 #endif
508