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