From d6f244d8d17010954f1b8e7898c455c9f0b99588 Mon Sep 17 00:00:00 2001 From: MLopez-Ibanez <2620021+MLopez-Ibanez@users.noreply.github.com> Date: Thu, 22 May 2025 11:33:46 +0100 Subject: [PATCH] Implementation of HV4D+ algorithm. * c/Makefile: Bump version to 0.16.4. * c/hv4d.c: New. * c/hv_priv.h: Move common hv code here. * python/: Update whatsnew, compilation and hv4d benchmark. * r/NEWS.md: Update. --- bibkeys.txt | 1 + c/Makefile | 59 +++- c/common.h | 2 +- c/hv.c | 2 + c/hv3dplus.c | 168 +---------- c/hv4d.c | 261 ++++++++++++++++++ c/hv_priv.h | 221 +++++++++++++++ c/libhv.mk | 4 +- c/sort.h | 30 +- python/doc/source/REFERENCES.bib | 15 + .../hv_bench-DTLZLinearShape.4d-time.png | Bin 47001 -> 48704 bytes python/doc/source/index.rst | 14 +- python/doc/source/whatsnew/index.rst | 2 +- python/src/moocore/_ffi_build.py | 1 + python/src/moocore/_moocore.py | 37 ++- r/NEWS.md | 2 +- r/R/hv.R | 39 ++- r/inst/REFERENCES.bib | 15 + r/man/hypervolume.Rd | 36 ++- r/src/Makevars | 2 +- 20 files changed, 689 insertions(+), 222 deletions(-) create mode 100644 c/hv4d.c create mode 100644 c/hv_priv.h diff --git a/bibkeys.txt b/bibkeys.txt index afcdeb2c1..6468bb314 100644 --- a/bibkeys.txt +++ b/bibkeys.txt @@ -14,6 +14,7 @@ FonGueLopPaq2011emo FonPaqLop06:hypervolume GruFon2009:emaa Grunert01 +GueFon2017hv4d HerWer1987tabucol IshMasTanNoj2015igd Jen03 diff --git a/c/Makefile b/c/Makefile index 0a5c3a148..32906f415 100644 --- a/c/Makefile +++ b/c/Makefile @@ -2,7 +2,7 @@ # Makefile for moocore -VERSION = 0.16.3$(REVISION) +VERSION = 0.16.4$(REVISION) #----------------------------------------------------------------------- @@ -82,12 +82,55 @@ BINDIR?=../bin BINDIR:=$(abspath $(BINDIR)) ## Define source files -SRCS = igd.c epsilon.c dominatedsets.c nondominated.c io.c ndsort.c hv_contrib.c hv.c hv3dplus.c pareto.c whv.c whv_hype.c \ - eaf.c eafdiff.c eaf_main.c eaf3d.c avl.c cmdline.c libutil.c main-hv.c timer.c \ - rng.c mt19937/mt19937.c -HEADERS = io.h io_priv.h common.h gcc_attribs.h igd.h epsilon.h nondominated.h hv.h cmdline.h whv.h whv_hype.h \ - eaf.h cvector.h avl.h bit_array.h \ - rng.h ziggurat_constants.h mt19937/mt19937.h +SRCS = avl.c \ + cmdline.c \ + dominatedsets.c \ + eaf3d.c \ + eaf.c \ + eafdiff.c \ + eaf_main.c \ + epsilon.c \ + hv3dplus.c \ + hv4d.c \ + hv.c \ + hv_contrib.c \ + igd.c \ + io.c \ + libutil.c \ + main-hv.c \ + mt19937/mt19937.c \ + ndsort.c \ + nondominated.c \ + pareto.c \ + rng.c \ + timer.c \ + whv.c \ + whv_hype.c + +HDRS = avl.h \ + avl_tiny.h \ + bit_array.h \ + cmdline.h \ + common.h \ + config.h \ + cvector.h \ + eaf.h \ + epsilon.h \ + gcc_attribs.h \ + hv.h \ + hv_priv.h \ + igd.h \ + io.h \ + io_priv.h \ + mt19937/mt19937.h \ + nondominated.h \ + pow_int.h \ + rng.h \ + sort.h \ + timer.h \ + whv.h \ + whv_hype.h \ + ziggurat_constants.h # Relative to root folder. DIST_OTHER_FILES = git_version *.mk @@ -211,7 +254,7 @@ mex: Hypervolume_MEX.c $(LIBHV_OBJS) $(call ECHO,---> You can use 'make mex MEXFLAGS="-O -std=c99"' to pass flags to mex. Code compiled by mex is less optimized (slower) than the default 'hv' command-line program <---) $(MEX) -v $(MEXFLAGS) -std=c99 $^ -DIST_SRC_FILES = $(DIST_OTHER_FILES) $(OBJS:.o=.c) $(HEADERS) +DIST_SRC_FILES = $(DIST_OTHER_FILES) $(OBJS:.o=.c) $(HDRS) DIST_SRC:= moocore-$(VERSION)-src dist: DEBUG=0 dist: CDEBUG= diff --git a/c/common.h b/c/common.h index 5f3ca4419..412f217c6 100644 --- a/c/common.h +++ b/c/common.h @@ -28,7 +28,7 @@ void warnprintf(const char *format,...) ATTRIBUTE_FORMAT_PRINTF(1, 2); perror(buffer); \ exit(EXIT_FAILURE); \ } while(0) -#endif +#endif // R_PACKAGE #include static inline void * diff --git a/c/hv.c b/c/hv.c index 5de7a4f70..f9cd4d91d 100644 --- a/c/hv.c +++ b/c/hv.c @@ -1078,6 +1078,7 @@ hv_recursive_ref(avl_tree_t * restrict tree, dlnode_t * restrict list, double hv3d_plus(const double * restrict data, size_t n, const double * restrict ref); +double hv4d(const double * restrict data, size_t n, const double * restrict ref); /* Returns 0 if no point strictly dominates ref. @@ -1089,6 +1090,7 @@ double fpli_hv(const double * restrict data, int d, int n, if (unlikely(n == 0)) return 0.0; ASSUME(d < 256); ASSUME(d > 1); + if (d == 4) return hv4d(data, n, ref); if (d == 3) return hv3d_plus(data, n, ref); if (d == 2) return hv2d(data, n, ref); dimension_t dim = (dimension_t) d; diff --git a/c/hv3dplus.c b/c/hv3dplus.c index a6e281daa..1c3c50fa7 100644 --- a/c/hv3dplus.c +++ b/c/hv3dplus.c @@ -41,26 +41,11 @@ ******************************************************************************/ - -#include -#include #include #include #include "common.h" -#include "sort.h" - -/* - Writing 'struct dlnode * next[1]' instead of 'struct dlnode * next' allows us - to share more code with the 4D case. The compiler should be able to remove - the extra indirection. -*/ -typedef struct dlnode { - const double *x; // point coordinates (objective vector). - struct dlnode * closest[2]; // closest[0] == cx, closest[1] == cy - struct dlnode * cnext[2]; // current next - struct dlnode * next[1]; // keeps the points sorted according to coordinate 3. - struct dlnode * prev[1]; // keeps the points sorted according to coordinate 3 (except the sentinel 3). -} dlnode_t; +#define HV_DIMENSION 3 +#include "hv_priv.h" typedef const double avl_item_t; typedef struct avl_node_t { @@ -78,60 +63,6 @@ typedef struct avl_node_t { /* ---------------- Data Structures Functions --------------------------------*/ -static void -init_sentinels(dlnode_t * list, const double * ref) -{ - const dimension_t dim = 3; - /* The list that keeps the points sorted according to the 3rd-coordinate - does not really need the 3 sentinels, just one to represent (-inf, -inf, - ref[2]). But we need the other two to maintain a list of nondominated - projections in the (x,y)-plane of points that is kept sorted according - to the 1st and 2nd coordinates, and for that list we need two sentinels - to represent (-inf, ref[1]) and (ref[0], -inf). */ - - // Allocate the 3 sentinels of dimension dim. - const double z3[] = { - -DBL_MAX, ref[1], -DBL_MAX, // Sentinel 1 - ref[0], -DBL_MAX, -DBL_MAX, // Sentinel 2 - -DBL_MAX, -DBL_MAX, ref[2] // Sentinel 2 - }; - double * x = malloc(sizeof(z3)); - memcpy(x, z3, sizeof(z3)); - - dlnode_t * s1 = list; - dlnode_t * s2 = list + 1; - dlnode_t * s3 = list + 2; - - // Sentinel 1 - s1->x = x; - s1->closest[0] = s2; - s1->closest[1] = s1; - s1->cnext[0] = NULL; - s1->cnext[1] = NULL; - s1->next[0] = s2; - s1->prev[0] = s3; - - x += dim; - // Sentinel 2 - s2->x = x; - s2->closest[0] = s2; - s2->closest[1] = s1; - s2->cnext[0] = NULL; - s2->cnext[1] = NULL; - s2->next[0] = s3; - s2->prev[0] = s1; - - x += dim; - // Sentinel 3 - s3->x = x; - s3->closest[0] = s2; - s3->closest[1] = s1; - s3->cnext[0] = NULL; - s3->cnext[1] = NULL; - s3->next[0] = s1; - s3->prev[0] = s2; -} - static inline void print_x(dlnode_t * p) { @@ -154,15 +85,6 @@ new_avl_node(dlnode_t * p, avl_node_t * node) return node; } -/* ------------ Update data structure ---------------------------------------*/ - -static inline void -remove_from_z(dlnode_t * old) -{ - old->prev[0]->next[0] = old->next[0]; - old->next[0]->prev[0] = old->prev[0]; -} - /* -------------------- Preprocessing ---------------------------------------*/ @@ -243,102 +165,22 @@ preprocessing(dlnode_t * list, size_t n) free(tnodes); } -/* - * Setup circular double-linked list in each dimension. - */ -static dlnode_t * -setup_cdllist(const double * restrict data, size_t n, const double * restrict ref) -{ - ASSUME(n >= 1); - const dimension_t d = 3; - const double **scratch = malloc(n * sizeof(*scratch)); - size_t i, j; - for (i = 0, j = 0; j < n; j++) { - /* Filters those points that do not strictly dominate the reference - point. This is needed to ensure that the points left are only those - that are needed to calculate the hypervolume. */ - if (unlikely(strongly_dominates(data + j * d, ref, d))) { - scratch[i] = data + j * d; - i++; - } - } - n = i; // Update number of points. - if (n > 1) - qsort(scratch, n, sizeof(*scratch), cmp_double_asc_rev_3d); - - dlnode_t * list = (dlnode_t *) malloc((n + 3) * sizeof(*list)); - init_sentinels(list, ref); - if (unlikely(n == 0)) { - free(scratch); - return list; - } - - dlnode_t * q = list+1; - dlnode_t * list3 = list+3; - assert(list->next[0] == list + 1); - assert(q->next[0] == list + 2); - for (size_t i = 0; i < n; i++) { - dlnode_t * p = list3 + i; - p->x = scratch[i]; - // Initialize it when debugging so it will crash if uninitialized. - DEBUG1( - p->closest[0] = NULL; - p->closest[1] = NULL; - p->cnext[0] = NULL; - p->cnext[1] = NULL;); - // Link the list in order. - q->next[0] = p; - p->prev[0] = q; - q = p; - } - free(scratch); - assert(q == (list3 + n - 1)); - assert(list+2 == list->prev[0]); - // q = last point, q->next = s3, s3->prev = last point - q->next[0] = list + 2; - (list+2)->prev[0] = q; - preprocessing(list, n); - return list; -} - - - -static void -free_cdllist(dlnode_t * list) -{ - free((void*) list->x); // Free sentinels. - free(list); -} - static inline double compute_area3d_simple(const double * px, const dlnode_t * q) { - const dlnode_t * u = q->cnext[1]; - double area = (q->x[0] - px[0]) * (u->x[1] - px[1]); - assert(area > 0); - while (px[0] < u->x[0]) { - q = u; - u = u->cnext[1]; - // With repeated coordinates, they can be zero. - assert((q->x[0] - px[0] >= 0) && (u->x[1] - q->x[1] >= 0)); - area += (q->x[0] - px[0]) * (u->x[1] - q->x[1]); - } - return area; + return compute_area_simple(px, q, 1); } static double hv3dplus(dlnode_t * list) { - // restart_list_y: - assert(list + 1 == list->next[0]); - list->cnext[0] = list+1; - (list+1)->cnext[1] = list; // Link sentinels (-inf ref[1] -inf) and (ref[0] -inf -inf). + restart_list_y(list); + assert(list+2 == list->prev[0]); double area = 0; double volume = 0; dlnode_t * p = (list+1)->next[0]; const dlnode_t * stop = list+2; - assert(stop == list->prev[0]); while (p != stop) { p->cnext[0] = p->closest[0]; p->cnext[1] = p->closest[1]; diff --git a/c/hv4d.c b/c/hv4d.c new file mode 100644 index 000000000..cba1f3810 --- /dev/null +++ b/c/hv4d.c @@ -0,0 +1,261 @@ +/****************************************************************************** + HV4D+ algorithm. + ------------------------------------------------------------------------------ + + Copyright (c) 2013, 2016, 2017 + Andreia P. Guerreiro + + This program is free software (software libre); you can redistribute + it and/or modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 3 of the + License. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, you can obtain a copy of the GNU + General Public License at: + http://www.gnu.org/copyleft/gpl.html + or by writing to: + Free Software Foundation, Inc., 59 Temple Place, + Suite 330, Boston, MA 02111-1307 USA + + ------------------------------------------------------------------------------ + + Reference: + + [1] Andreia P. Guerreiro and Carlos M. Fonseca. Computing and Updating + Hypervolume Contributions in Up to Four Dimensions. IEEE Transactions on + Evolutionary Computation, 22(3):449–463, June 2018. + + ------------------------------------------------------------------------------ + + Modified by Manuel López-Ibáñez (2025): + - Integration within moocore. + - Correct handling of weakly dominated points and repeated coordinates during + preprocessing(). + - More efficient setup_cdllist() and preprocessing() in terms of time and memory. + +******************************************************************************/ + +#include +#include +#include "common.h" +#define HV_DIMENSION 4 +#include "hv_priv.h" + +/* ---------- Data Structures Functions -------------------------------------*/ + +static inline void _attr_maybe_unused +print_point(const char *s, const double * x) +{ + fprintf(stderr, "%s: %g %g %g %g\n", s, x[0], x[1], x[2], x[4]); +} + + + + +/* ---------------------------------- Update data structure ---------------------------------------*/ + +static inline void +add_to_z(dlnode_t * new) +{ + new->next[0] = new->prev[0]->next[0]; //in case new->next[0] was removed for being dominated + new->next[0]->prev[0] = new; + new->prev[0]->next[0] = new; +} + +static inline bool +lex_cmp_3d_102(const double * a, const double *b) +{ + return a[1] < b[1] || (a[1] == b[1] && (a[0] < b[0] || (a[0] == b[0] && a[2] < b[2]))); +} + +static inline bool +lex_cmp_3d_012(const double * a, const double *b) +{ + return a[0] < b[0] || (a[0] == b[0] && (a[1] < b[1] || (a[1] == b[1] && a[2] < b[2]))); +} + +/* + Go through the points in the order of z and either remove points that are + dominated by new with respect to x,y,z or update the cx and cy lists by + adding new. +*/ +static unsigned int +update_links(dlnode_t * list, dlnode_t * new) +{ + assert(list+2 == list->prev[0]); + unsigned int ndom = 0; + dlnode_t * p = new->next[0]; + const double * px = p->x; + const double * newx = new->x; + dlnode_t * stop = list+2; + while (p != stop) { + if (px[0] <= newx[0] && px[1] <= newx[1] && (px[0] < newx[0] || px[1] < newx[1])) + break; + + if (newx[0] <= px[0]){ + //new <= p + if (newx[1] <= px[1]){ + assert(weakly_dominates(newx, px, 3)); + p->ndomr++; + // p->domr = new; + ndom++; + remove_from_z(p); //HV-ONLY (does not need dominated to compute HV) + } else if (newx[0] < px[0] && lex_cmp_3d_102(newx, p->closest[1]->x)) { // newx[1] > px[1] + p->closest[1] = new; + } + } else if (newx[1] < px[1] && lex_cmp_3d_012(newx, p->closest[0]->x)) {//newx[0] > px[0] + p->closest[0] = new; + } + p = p->next[0]; + px = p->x; + } + return ndom; +} + + + +// This does what setupZandClosest does while reconstructing L at z = new->x[2]. +static void +restart_base_setup_z_and_closest(dlnode_t * list, dlnode_t * new) +{ + assert(list+1 == list->next[0]); + dlnode_t * closest1 = list; + dlnode_t * closest0 = list+1; + const double * closest1x = closest1->x; + const double * closest0x = closest0->x; + dlnode_t * p = closest0->next[0]; + assert(p == list->next[0]->next[0]); + const double * px = p->x; + const double * newx = new->x; + restart_list_y(list); + while (lexicographic_less_3d(px, newx)) { + // reconstruct + p->cnext[0] = p->closest[0]; + p->cnext[1] = p->closest[1]; + + p->cnext[0]->cnext[1] = p; + p->cnext[1]->cnext[0] = p; + + // setup_z_and_closest + if (px[0] <= newx[0] && px[1] <= newx[1]) { + new->ndomr++; + assert(weakly_dominates(px, newx, 4)); + // FIXME: If it is dominated why update new->closest[0] and + // new->closest[1] and the rest? Why not return here? + return; + } else if (px[1] < newx[1] && (px[0] < closest0x[0] || (px[0] == closest0x[0] && px[1] < closest0x[1]))) { + closest0 = p; + closest0x = closest0->x; + } else if (px[0] < newx[0] && (px[1] < closest1x[1] || (px[1] == closest1x[1] && px[0] < closest1x[0]))) { + closest1 = p; + closest1x = closest1->x; + } + p = p->next[0]; + px = p->x; + } + + new->closest[0] = closest0; + new->closest[1] = closest1; + + new->prev[0] = p->prev[0]; + new->next[0] = p; +} + +static double +one_contribution_3d(dlnode_t * list, dlnode_t * new) +{ + restart_base_setup_z_and_closest(list, new); + if (new->ndomr > 0) + return 0; + + new->cnext[0] = new->closest[0]; + new->cnext[1] = new->closest[1]; + + const double * newx = new->x; + double area = compute_area_simple(newx, new->cnext[0], 1); + dlnode_t * p = new->next[0]; + const double * px = p->x; + assert(!weakly_dominates(px, newx, 4)); + + double lastz = newx[2]; + double volume = area * (px[2] - lastz); + while (px[0] > newx[0] || px[1] > newx[1]) { + p->cnext[0] = p->closest[0]; + p->cnext[1] = p->closest[1]; + + if (px[0] >= newx[0] && px[1] >= newx[1]) { + area -= compute_area_simple(px, p->cnext[0], 1); + p->cnext[1]->cnext[0] = p; + p->cnext[0]->cnext[1] = p; + + } else if (px[0] >= newx[0]) { + if (px[0] <= new->cnext[0]->x[0]) { + const double tmpx[] = { px[0], newx[1] }; + // if px[0] == new->cnext[0]->x[0] then area starts at 0. + area -= compute_area_simple(tmpx, new->cnext[0], 1); + p->cnext[0] = new->cnext[0]; + p->cnext[1]->cnext[0] = p; + new->cnext[0] = p; + } + } else if (px[1] <= new->cnext[1]->x[1]) { + const double tmpx[] = { newx[0], px[1] }; + area -= compute_area_simple(tmpx, new->cnext[1], 0); + p->cnext[1] = new->cnext[1]; + p->cnext[0]->cnext[1] = p; + new->cnext[1] = p; + } + assert(!weakly_dominates(px, p->next[0]->x, 4)); + lastz = px[2]; + p = p->next[0]; + px = p->x; + volume += area * (px[2] - lastz); + } + return volume; +} + +/* Compute the hypervolume indicator in d=4 by iteratively computing the one + contribution problem in d=3. */ +static double +hv4dplusU(dlnode_t * list) +{ + assert(list+2 == list->prev[0]); + assert(list+2 == list->prev[1]); + assert(list+1 == list->next[1]); + + double volume = 0, hv = 0; + dlnode_t * new = (list+1)->next[1]; + dlnode_t * last = list+2; + while (new != last) { + double new_v = one_contribution_3d(list, new); + assert(new_v > 0 || (new_v == 0 && new->ndomr)); + // if new_v == 0, then new was dominated by something else. + if (new_v > 0) { + add_to_z(new); + // FIXME update_links return ndom but that value is not used by the algorithm? + update_links(list, new); + volume += new_v; + } + assert(!weakly_dominates(new->x, new->next[1]->x, 4)); + double height = new->next[1]->x[3] - new->x[3]; + hv += volume * height; + new = new->next[1]; + } + return hv; +} + + + + +double hv4d(const double * restrict data, size_t n, const double * restrict ref) +{ + dlnode_t * list = setup_cdllist(data, n, ref); + double hv = hv4dplusU(list); + free_cdllist(list); + return hv; +} diff --git a/c/hv_priv.h b/c/hv_priv.h new file mode 100644 index 000000000..c29b7654d --- /dev/null +++ b/c/hv_priv.h @@ -0,0 +1,221 @@ +#include "sort.h" + +#if !defined(HV_DIMENSION) || (HV_DIMENSION != 3 && HV_DIMENSION != 4) +#error "HV_DIMENSION must be 3 or 4" +#endif + +/* ----------------------- Data Structure -----------------------------------*/ + +/* + With HV_DIMENSION==3, we have 'struct dlnode * next[1]' instead of 'struct dlnode * next'. + The compiler should be able to remove the extra indirection. +*/ + +typedef struct dlnode { + const double *x; // point coordinates (objective vector). + struct dlnode * closest[2]; // closest[0] == cx, closest[1] == cy + struct dlnode * cnext[2]; //current next + struct dlnode * next[HV_DIMENSION - 2]; /* keeps the points sorted according to coordinates 2,3 and 4 + (in the case of 2 and 3, only the points swept by 4 are kept) */ + struct dlnode * prev[HV_DIMENSION - 2]; //keeps the points sorted according to coordinates 2 and 3 (except the sentinel 3) +#if HV_DIMENSION == 4 + unsigned int ndomr; // number of dominators. +#endif +} dlnode_t; + +static void +init_sentinels(dlnode_t * list, const double * ref) +{ + // Allocate the 3 sentinels of dimension dim. + const double z[] = { +#if HV_DIMENSION == 3 + -DBL_MAX, ref[1], -DBL_MAX, // Sentinel 1 + ref[0], -DBL_MAX, -DBL_MAX, // Sentinel 2 + -DBL_MAX, -DBL_MAX, ref[2] // Sentinel 2 +#else + -DBL_MAX, ref[1], -DBL_MAX, -DBL_MAX, // Sentinel 1 + ref[0], -DBL_MAX, -DBL_MAX, -DBL_MAX, // Sentinel 2 + -DBL_MAX, -DBL_MAX, ref[2], ref[3] // Sentinel 2 +#endif + }; + + double * x = malloc(sizeof(z)); + memcpy(x, z, sizeof(z)); + + /* The list that keeps the points sorted according to the 3rd-coordinate + does not really need the 3 sentinels, just one to represent (-inf, -inf, + ref[2]). But we need the other two to maintain a list of nondominated + projections in the (x,y)-plane of points that is kept sorted according + to the 1st and 2nd coordinates, and for that list we need two sentinels + to represent (-inf, ref[1]) and (ref[0], -inf). */ + dlnode_t * s1 = list; + dlnode_t * s2 = list + 1; + dlnode_t * s3 = list + 2; + + // Sentinel 1 + s1->x = x; + s1->closest[0] = s2; + s1->closest[1] = s1; + s1->cnext[1] = NULL; + s1->cnext[0] = NULL; + s1->next[0] = s2; + s1->prev[0] = s3; +#if HV_DIMENSION == 4 + s1->next[1] = s2; + s1->prev[1] = s3; + s1->ndomr = 0; +#endif + + x += HV_DIMENSION; + // Sentinel 2 + s2->x = x; + s2->closest[0] = s2; + s2->closest[1] = s1; + s2->cnext[1] = NULL; + s2->cnext[0] = NULL; + s2->next[0] = s3; + s2->prev[0] = s1; +#if HV_DIMENSION == 4 + s2->next[1] = s3; + s2->prev[1] = s1; + s2->ndomr = 0; +#endif + + x += HV_DIMENSION; + // Sentinel 3 + s3->x = x; + s3->closest[0] = s2; + s3->closest[1] = s1; + s3->cnext[1] = NULL; + s3->cnext[0] = NULL; + s3->next[0] = s1; + s3->prev[0] = s2; +#if HV_DIMENSION == 4 + s3->next[1] = s1; + s3->prev[1] = s2; + s3->ndomr = 0; +#endif +} + +#if HV_DIMENSION == 3 // Defined in hv3dplus.c +static inline void preprocessing(dlnode_t * list, size_t n); +#endif + +/* + * Setup circular double-linked list in each dimension + */ +static dlnode_t * +setup_cdllist(const double * restrict data, size_t n, const double * restrict ref) +{ + ASSUME(n >= 1); + const dimension_t dim = HV_DIMENSION; + const double **scratch = malloc(n * sizeof(*scratch)); + size_t i, j; + for (i = 0, j = 0; j < n; j++) { + /* Filters those points that do not strictly dominate the reference + point. This is needed to ensure that the points left are only those + that are needed to calculate the hypervolume. */ + if (unlikely(strongly_dominates(data + j * dim, ref, dim))) { + scratch[i] = data + j * dim; + i++; + } + } + n = i; // Update number of points. + if (likely(n > 1)) + qsort(scratch, n, sizeof(*scratch), + (HV_DIMENSION == 3) ? cmp_double_asc_rev_3d : cmp_double_asc_rev_4d); + + dlnode_t * list = (dlnode_t *) malloc((n + 3) * sizeof(*list)); + init_sentinels(list, ref); + if (unlikely(n == 0)) { + free(scratch); + return list; + } + + const dimension_t d = HV_DIMENSION - 3; // index within the list. + dlnode_t * q = list+1; + dlnode_t * list3 = list+3; + assert(list->next[d] == list + 1); + assert(q->next[d] == list + 2); + for (i = 0, j = 0; j < n; j++) { + if (weakly_dominates(q->x, scratch[j], dim)) { + /* print_point("q", q->x); */ + /* print_point("i", scratch[j]); */ + continue; + } + dlnode_t * p = list3 + i; + p->x = scratch[j]; + // Initialize it when debugging so it will crash if uninitialized. + DEBUG1( + p->closest[0] = NULL; + p->closest[1] = NULL; + p->cnext[0] = NULL; + p->cnext[1] = NULL;); +#if HV_DIMENSION == 4 + p->ndomr = 0; +#endif + // Link the list in order. + q->next[d] = p; + p->prev[d] = q; + q = p; + i++; + } + n = i; + free(scratch); + assert((list3 + n - 1) == q); + assert(list+2 == list->prev[d]); + // q = last point, q->next = s3, s3->prev = last point + q->next[d] = list+2; + (list+2)->prev[d] = q; +#if HV_DIMENSION == 3 + preprocessing(list, n); +#endif + return list; +} + +static inline void +free_cdllist(dlnode_t * list) +{ + free((void*) list->x); // Free sentinels. + free(list); +} + +/* ------------ Update data structure ---------------------------------------*/ + +// Link sentinels (-inf ref[1] -inf) and (ref[0] -inf -inf). +static inline void +restart_list_y(dlnode_t * list) +{ + assert(list+1 == list->next[0]); + list->cnext[0] = list+1; + (list+1)->cnext[1] = list; +} + +static inline void +remove_from_z(dlnode_t * old) +{ + old->prev[0]->next[0] = old->next[0]; + old->next[0]->prev[0] = old->prev[0]; +} + + +static inline double +compute_area_simple(const double * px, const dlnode_t * q, int i) +{ + const int j = 1 - i; + const dlnode_t * u = q->cnext[i]; + double area = (q->x[j] - px[j]) * (u->x[i] - px[i]); +#if HV_DIMENSION == 3 + assert(area > 0); +#endif + while (px[j] < u->x[j]) { + q = u; + u = u->cnext[i]; + // With repeated coordinates, they can be zero. +#if HV_DIMENSION == 3 + assert((q->x[0] - px[0] >= 0) && (u->x[1] - q->x[1] >= 0)); +#endif + area += (q->x[j] - px[j]) * (u->x[i] - q->x[i]); + } + return area; +} diff --git a/c/libhv.mk b/c/libhv.mk index e78b6393f..f22386986 100644 --- a/c/libhv.mk +++ b/c/libhv.mk @@ -1,6 +1,6 @@ # -*- Makefile-gmake -*- -LIBHV_SRCS = hv.c hv3dplus.c -LIBHV_HDRS = hv.h +LIBHV_SRCS = hv.c hv3dplus.c hv4d.c +LIBHV_HDRS = hv.h hv_priv.h LIBHV_OBJS = $(LIBHV_SRCS:.c=.o) HV_LIB = fpli_hv.a diff --git a/c/sort.h b/c/sort.h index 76d9fdf2b..9872b1473 100644 --- a/c/sort.h +++ b/c/sort.h @@ -3,6 +3,9 @@ #include "common.h" +// General type for comparison functions used in qsort(). +typedef int (*cmp_fun_t)(const void *, const void *); + /* x < y, i.e., x is strictly lower than y in all dimensions. Assumes minimization. */ @@ -11,12 +14,23 @@ static inline bool strongly_dominates(const double * restrict x, const double * restrict y, dimension_t dim) { ASSUME(dim >= 2); - for (dimension_t i = 0; i < dim; i++) - if (x[i] >= y[i]) + for (dimension_t d = 0; d < dim; d++) + if (x[d] >= y[d]) return false; return true; } +static inline bool +weakly_dominates(const double * restrict x, const double * restrict y, dimension_t dim) +{ + ASSUME(dim >= 2); + for (dimension_t d = 0; d < dim; d++) + if (x[d] > y[d]) + return false; + return true; +} + + static inline int cmp_double_asc_rev(const void * restrict p1, const void * restrict p2, dimension_t dim) { @@ -37,6 +51,18 @@ cmp_double_asc_rev_3d(const void * restrict p1, const void * restrict p2) return cmp_double_asc_rev(p1, p2, 3); } +static inline int +cmp_double_asc_rev_4d(const void * restrict p1, const void * restrict p2) +{ + return cmp_double_asc_rev(p1, p2, 4); +} + +static inline bool +lexicographic_less_3d(const double * a, const double * b) +{ + return (a[2] < b[2] || (a[2] == b[2] && (a[1] < b[1] || (a[1] == b[1] && a[0] <= b[0])))); +} + static inline int cmp_double_asc_y_des_x(const void * restrict p1, const void * restrict p2) diff --git a/python/doc/source/REFERENCES.bib b/python/doc/source/REFERENCES.bib index 21c136daf..f024e9085 100644 --- a/python/doc/source/REFERENCES.bib +++ b/python/doc/source/REFERENCES.bib @@ -210,6 +210,21 @@ @article{DubLopStu2011amai doi = {10.1007/s10472-011-9235-0} } +@article{GueFon2017hv4d, + author = { Andreia P. Guerreiro and Carlos M. Fonseca }, + title = {Computing and Updating Hypervolume Contributions in Up to + Four Dimensions}, + journal = {IEEE Transactions on Evolutionary Computation}, + year = 2018, + volume = 22, + number = 3, + pages = {449--463}, + month = jun, + annote = {Proposed HV3D$^{+}$ with $O(n\log n)$ complexity and + HV4D$^{+}$ with $O(n^2)$ complexity}, + doi = {10.1109/tevc.2017.2729550} +} + @article{HerWer1987tabucol, author = {A. Hertz and de Werra, D.}, title = {Using Tabu Search Techniques for Graph Coloring}, diff --git a/python/doc/source/_static/hv_bench-DTLZLinearShape.4d-time.png b/python/doc/source/_static/hv_bench-DTLZLinearShape.4d-time.png index b064e89641b0b5b4e87028e78c773750777dbab8..99c922edd4ac01f8e3f8cbf2e14c7613e1cc9722 100644 GIT binary patch literal 48704 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 literal 47001 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 diff --git a/python/doc/source/index.rst b/python/doc/source/index.rst index 73177a0b2..e6822f974 100644 --- a/python/doc/source/index.rst +++ b/python/doc/source/index.rst @@ -65,20 +65,19 @@ performance measures, performance assessment Benchmarks ---------- -The following plots compare the performance of `moocore`_, `pymoo`_, -`BoTorch`_, `jMetalPy`_, and `DEAP-er`_. Other optimization packages are not included in the comparison because they are based on these packages so they are *at least* as slow as them. For example `Xopt`_ uses `BoTorch`_, `pysamoo`_ is an extension of `pymoo`_. +The following plots compare the performance of `moocore`_, `pymoo`_, `BoTorch`_, `jMetalPy`_, and `DEAP-er`_. Other optimization packages are not included in the comparison because they are based on these packages so they are **at least as slow** as them. For example `Xopt`_ uses `BoTorch`_, `pysamoo`_ is an extension of `pymoo`_, and `DESDEO`_ uses `pymoo`_ internally. -Not all packages provided the same functionality. For example, `pymoo`_ does not provide the :ref:`epsilon indicator ` whereas `jMetalPy`_ does not provide the :ref:`IGD+ indicator `. `BoTorch`_ provides neither of them. +Not all packages provide the same functionality. For example, `pymoo`_ does not provide the :ref:`epsilon indicator ` whereas `jMetalPy`_ does not provide the :ref:`IGD+ indicator `. `BoTorch`_ provides neither of them. The following plots compare the speed of computing the :ref:`hypervolume indicator ` in 3D and 4D. As the plots show, `moocore`_ is nearly 100 times faster than the other packages and 1000 times faster than `BoTorch`_ and, by extension, `Xopt`_, which is an order of magnitude slower than the second slowest. |pic1| |pic2| .. |pic1| image:: _static/hv_bench-DTLZLinearShape.3d-time.png - :width: 48% + :width: 49% .. |pic2| image:: _static/hv_bench-DTLZLinearShape.4d-time.png - :width: 48% + :width: 49% The following plots compare the speed of finding nondominated solutions, equivalent to :func:`moocore.is_nondominated`, in 2D and 3D. As the plots show, `moocore`_ is nearly 100 times faster in 2D and 1000 times faster in 3D than the other packages. Here, `pymoo`_ quickly becomes even slower than `BoTorch`_. @@ -86,10 +85,10 @@ The following plots compare the speed of finding nondominated solutions, equival |pic3| |pic4| .. |pic3| image:: _static/ndom_bench-test2D-200k-time.png - :width: 48% + :width: 49% .. |pic4| image:: _static/ndom_bench-ran3d-10k-time.png - :width: 48% + :width: 49% The following plot compares the speed of computing the :ref:`epsilon indicator ` metric and :ref:`IGD+ indicator `. Despite the algorithms for computing these metrics are relatively simple and easy to vectorize in Python, the `moocore`_ implementation is still 10 to 100 times faster. @@ -113,6 +112,7 @@ The source code for the benchmarks above can be found at https://github.com/mult .. _DEAP-er: https://deap-er.readthedocs.io/en/latest/ .. _Xopt: https://xopt.xopt.org/index.html .. _pysamoo: https://anyoptimization.com/projects/pysamoo/ +.. _DESDEO: https://desdeo.readthedocs.io/en/latest/ .. This is not really the index page, that is found in _templates/indexcontent.html The toctree content here will be added to the diff --git a/python/doc/source/whatsnew/index.rst b/python/doc/source/whatsnew/index.rst index 1f97126ad..a620e30b6 100644 --- a/python/doc/source/whatsnew/index.rst +++ b/python/doc/source/whatsnew/index.rst @@ -7,7 +7,7 @@ What's new Version 0.1.7 (dev) ------------------- -- :func:`~moocore.hypervolume` now uses the HV3D+ algorithm for the 3D case. +- :func:`~moocore.hypervolume` now uses the HV3D\ :sup:`+` algorithm for the 3D case and the HV4D\ :sup:`+` algorithm for the 4D case. - :func:`~moocore.read_datasets` is significantly faster for large files. - :func:`~moocore.is_nondominated` and :func:`~moocore.filter_dominated` are faster for 3D inputs. diff --git a/python/src/moocore/_ffi_build.py b/python/src/moocore/_ffi_build.py index a7a54d6e7..ccaa27ca6 100644 --- a/python/src/moocore/_ffi_build.py +++ b/python/src/moocore/_ffi_build.py @@ -31,6 +31,7 @@ "eafdiff.c", "hv.c", "hv3dplus.c", + "hv4d.c", "hv_contrib.c", "io.c", "libutil.c", # For fatal_error() diff --git a/python/src/moocore/_moocore.py b/python/src/moocore/_moocore.py index 909bf746e..91efdfa65 100644 --- a/python/src/moocore/_moocore.py +++ b/python/src/moocore/_moocore.py @@ -390,18 +390,7 @@ def hypervolume( r"""Hypervolume indicator. Compute the hypervolume metric with respect to a given reference point - assuming minimization of all objectives. For 2D and 3D, the algorithm used - :footcite:p:`FonPaqLop06:hypervolume,BeuFonLopPaqVah09:tec` has :math:`O(n - \log n)` complexity, where :math:`n` is the number of input points. For 4D - or higher, it uses a recursive algorithm that has the 3D algorithm as a - base case :footcite:p:`FonPaqLop06:hypervolume`, which has :math:`O(n^{m-2} - \log n)` time and linear space complexity in the worst-case, where - :math:`m` is the dimension of the points, but experimental results show - that the pruning techniques used may reduce the time complexity even - further. Andreia P. Guerreiro improved the integration of the 3D case with - the recursive algorithm, which leads to significant reduction of - computation time. She has also enhanced the numerical stability of the - algorithm by avoiding floating-point comparisons of partial hypervolumes. + assuming minimization of all objectives. .. seealso:: For details about the hypervolume, see :ref:`hypervolume_metric`. @@ -427,6 +416,30 @@ def hypervolume( RelativeHypervolume : Compute hypervolume relative to a reference set. + Notes + ----- + For 2D and 3D, the algorithms used + :footcite:p:`FonPaqLop06:hypervolume,BeuFonLopPaqVah09:tec` have :math:`O(n + \log n)` complexity, where :math:`n` is the number of input points. The 3D case uses the HV3D\ :sup:`+` algorithm :footcite:p:`GueFon2017hv4d`, which has the same complexity as the HV3D algorithm :footcite:p:`FonPaqLop06:hypervolume,BeuFonLopPaqVah09:tec`, but it is faster in practice. + + For 4D, the algorithm used is HV4D\ :sup:`+` :footcite:p:`GueFon2017hv4d`, + which has :math:`O(n^2)` complexity. Compared to the `original + implementation `_, this implementation + correctly handles weakly dominated points and has been further optimized + for speed. + + For 5D or higher, it uses a recursive algorithm that has the 3D algorithm + as a base case :footcite:p:`FonPaqLop06:hypervolume`, which has + :math:`O(n^{m-2} \log n)` time and linear space complexity in the + worst-case, where :math:`m` is the dimension of the points, but + experimental results show that the pruning techniques used may reduce the + time complexity even further. Andreia P. Guerreiro improved the + integration of the 3D case with the recursive algorithm, which leads to + significant reduction of computation time. She has also enhanced the + numerical stability of the algorithm by avoiding floating-point comparisons + of partial hypervolumes. + + References ---------- .. footbibliography:: diff --git a/r/NEWS.md b/r/NEWS.md index 0c5fb6938..e6164fda6 100644 --- a/r/NEWS.md +++ b/r/NEWS.md @@ -1,6 +1,6 @@ # moocore 0.1.6.900 - * `hypervolume()` now uses the HV3D+ algorithm for the 3D case. + * `hypervolume()` now uses the HV3D+ algorithm for the 3D case and the HV4D+ algorithm for the 4D case. * `read_datasets()` is significantly faster for large files. diff --git a/r/R/hv.R b/r/R/hv.R index d7a0e0459..2af399eb5 100644 --- a/r/R/hv.R +++ b/r/R/hv.R @@ -1,18 +1,8 @@ #' Hypervolume metric #' #' Compute the hypervolume metric with respect to a given reference point -#' assuming minimization of all objectives. For 2D and 3D, the algorithm used -#' \citep{FonPaqLop06:hypervolume,BeuFonLopPaqVah09:tec} has \eqn{O(n \log n)} -#' complexity, where \eqn{n} is the number of input points. For 4D or higher, -#' it uses a recursive algorithm that has the 3D algorithm as a base case -#' algorithm \citep{FonPaqLop06:hypervolume}, which has \eqn{O(n^{m-2} \log n)} -#' time and linear space complexity in the worst-case, where \eqn{m} is the -#' dimension of the points, but experimental results show that the pruning -#' techniques used may reduce the time complexity even further. Andreia -#' P. Guerreiro improved the integration of the 3D case with the recursive -#' algorithm, which leads to significant reduction of computation time. She has -#' also enhanced the numerical stability of the algorithm by avoiding -#' floating-point comparisons of partial hypervolumes. +#' assuming minimization of all objectives. +#' #' #' @inherit epsilon params return #' @@ -49,6 +39,31 @@ #' another, then we know for sure than the latter set cannot be better than the #' former in terms of Pareto-optimality. #' +#' For 2D and 3D, the algorithms used +#' \citep{FonPaqLop06:hypervolume,BeuFonLopPaqVah09:tec} have \eqn{O(n \log n)} +#' complexity, where \eqn{n} is the number of input points. The 3D case uses +#' the \eqn{\text{HV3D}^{+}} algorithm \citep{GueFon2017hv4d}, which has the +#' sample complexity as the HV3D algorithm +#' \citep{FonPaqLop06:hypervolume,BeuFonLopPaqVah09:tec}, but it is faster in +#' practice. +#' +#' For 4D, the algorithm used is \eqn{\text{HV4D}^{+}} \citep{GueFon2017hv4d}, +#' which has \eqn{O(n^2)} complexity. Compared to the [original +#' implementation](https://github.com/apguerreiro/HVC/), this implementation +#' correctly handles weakly dominated points and has been further optimized for +#' speed. +#' +#' For 5D or higher, it uses a recursive algorithm that has the 3D algorithm as +#' a base case algorithm \citep{FonPaqLop06:hypervolume}, which has +#' \eqn{O(n^{m-2} \log n)} time and linear space complexity in the worst-case, +#' where \eqn{m} is the dimension of the points, but experimental results show +#' that the pruning techniques used may reduce the time complexity even +#' further. Andreia P. Guerreiro improved the integration of the 3D case with +#' the recursive algorithm, which leads to significant reduction of computation +#' time. She has also enhanced the numerical stability of the algorithm by +#' avoiding floating-point comparisons of partial hypervolumes. +#' +#' #' @references #' #' \insertAllCited{} diff --git a/r/inst/REFERENCES.bib b/r/inst/REFERENCES.bib index 21c136daf..f024e9085 100644 --- a/r/inst/REFERENCES.bib +++ b/r/inst/REFERENCES.bib @@ -210,6 +210,21 @@ @article{DubLopStu2011amai doi = {10.1007/s10472-011-9235-0} } +@article{GueFon2017hv4d, + author = { Andreia P. Guerreiro and Carlos M. Fonseca }, + title = {Computing and Updating Hypervolume Contributions in Up to + Four Dimensions}, + journal = {IEEE Transactions on Evolutionary Computation}, + year = 2018, + volume = 22, + number = 3, + pages = {449--463}, + month = jun, + annote = {Proposed HV3D$^{+}$ with $O(n\log n)$ complexity and + HV4D$^{+}$ with $O(n^2)$ complexity}, + doi = {10.1109/tevc.2017.2729550} +} + @article{HerWer1987tabucol, author = {A. Hertz and de Werra, D.}, title = {Using Tabu Search Techniques for Graph Coloring}, diff --git a/r/man/hypervolume.Rd b/r/man/hypervolume.Rd index 0e1402ccf..0f6c7d6e3 100644 --- a/r/man/hypervolume.Rd +++ b/r/man/hypervolume.Rd @@ -22,18 +22,7 @@ objectives or a vector of logical values, with one value per objective.} } \description{ Compute the hypervolume metric with respect to a given reference point -assuming minimization of all objectives. For 2D and 3D, the algorithm used -\citep{FonPaqLop06:hypervolume,BeuFonLopPaqVah09:tec} has \eqn{O(n \log n)} -complexity, where \eqn{n} is the number of input points. For 4D or higher, -it uses a recursive algorithm that has the 3D algorithm as a base case -algorithm \citep{FonPaqLop06:hypervolume}, which has \eqn{O(n^{m-2} \log n)} -time and linear space complexity in the worst-case, where \eqn{m} is the -dimension of the points, but experimental results show that the pruning -techniques used may reduce the time complexity even further. Andreia -P. Guerreiro improved the integration of the 3D case with the recursive -algorithm, which leads to significant reduction of computation time. She has -also enhanced the numerical stability of the algorithm by avoiding -floating-point comparisons of partial hypervolumes. +assuming minimization of all objectives. } \details{ The hypervolume of a set of multidimensional points \eqn{A \subset @@ -61,6 +50,29 @@ of the former must be strictly larger than the hypervolume of the latter. Conversely, if the hypervolume of a set is larger than the hypervolume of another, then we know for sure than the latter set cannot be better than the former in terms of Pareto-optimality. + +For 2D and 3D, the algorithms used +\citep{FonPaqLop06:hypervolume,BeuFonLopPaqVah09:tec} have \eqn{O(n \log n)} +complexity, where \eqn{n} is the number of input points. The 3D case uses +the \eqn{\text{HV3D}^{+}} algorithm \citep{GueFon2017hv4d}, which has the +sample complexity as the HV3D algorithm +\citep{FonPaqLop06:hypervolume,BeuFonLopPaqVah09:tec}, but it is faster in +practice. + +For 4D, the algorithm used is \eqn{\text{HV4D}^{+}} \citep{GueFon2017hv4d}, +which has \eqn{O(n^2)} complexity. Compared to the \href{https://github.com/apguerreiro/HVC/}{original implementation}, this implementation +correctly handles weakly dominated points and has been further optimized for +speed. + +For 5D or higher, it uses a recursive algorithm that has the 3D algorithm as +a base case algorithm \citep{FonPaqLop06:hypervolume}, which has +\eqn{O(n^{m-2} \log n)} time and linear space complexity in the worst-case, +where \eqn{m} is the dimension of the points, but experimental results show +that the pruning techniques used may reduce the time complexity even +further. Andreia P. Guerreiro improved the integration of the 3D case with +the recursive algorithm, which leads to significant reduction of computation +time. She has also enhanced the numerical stability of the algorithm by +avoiding floating-point comparisons of partial hypervolumes. } \examples{ diff --git a/r/src/Makevars b/r/src/Makevars index 09d7d3231..cfff692e0 100644 --- a/r/src/Makevars +++ b/r/src/Makevars @@ -5,7 +5,7 @@ LTO = $(LTO_OPT) DEBUG=0 PKG_CPPFLAGS = -DR_PACKAGE -DDEBUG=$(DEBUG) -I./libmoocore/ $(LTO) -MOOCORE_SRC_FILES = hv3dplus.c hv_contrib.c hv.c pareto.c whv.c whv_hype.c avl.c eaf3d.c eaf.c io.c rng.c mt19937/mt19937.c +MOOCORE_SRC_FILES = hv3dplus.c hv4d.c hv_contrib.c hv.c pareto.c whv.c whv_hype.c avl.c eaf3d.c eaf.c io.c rng.c mt19937/mt19937.c SOURCES = $(MOOCORE_SRC_FILES:%=libmoocore/%) init.c Rmoocore.c OBJECTS = $(SOURCES:.c=.o)