From caf2059df7cc0110f1a476b6f0620fb03afd0093 Mon Sep 17 00:00:00 2001 From: Corwinpro Date: Wed, 12 Feb 2020 09:54:24 +0000 Subject: [PATCH] Maint: Hypervolume (pyhv.py) module rewrite (#484) --- README.md | 1 - .../functions/multiobjective/LGPL_LICENSE | 165 ---------- nevergrad/functions/multiobjective/core.py | 5 +- .../functions/multiobjective/hypervolume.py | 309 +++++++++++++++++ nevergrad/functions/multiobjective/pyhv.py | 295 ----------------- .../multiobjective/test_structures.py | 310 ++++++++++++++++++ 6 files changed, 621 insertions(+), 464 deletions(-) delete mode 100644 nevergrad/functions/multiobjective/LGPL_LICENSE create mode 100644 nevergrad/functions/multiobjective/hypervolume.py delete mode 100644 nevergrad/functions/multiobjective/pyhv.py create mode 100644 nevergrad/functions/multiobjective/test_structures.py diff --git a/README.md b/README.md index 50aaa8932..772b9d965 100644 --- a/README.md +++ b/README.md @@ -112,4 +112,3 @@ as well as pieces of advice on how to choose the proper optimizer for your probl ## License `nevergrad` is released under the MIT license. See [LICENSE](LICENSE) for additional details about it. -LGPL code is however also included in the multiobjective subpackage. diff --git a/nevergrad/functions/multiobjective/LGPL_LICENSE b/nevergrad/functions/multiobjective/LGPL_LICENSE deleted file mode 100644 index 755013bb2..000000000 --- a/nevergrad/functions/multiobjective/LGPL_LICENSE +++ /dev/null @@ -1,165 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. diff --git a/nevergrad/functions/multiobjective/core.py b/nevergrad/functions/multiobjective/core.py index db51385a3..6eae1bc4b 100644 --- a/nevergrad/functions/multiobjective/core.py +++ b/nevergrad/functions/multiobjective/core.py @@ -6,8 +6,7 @@ from typing import Tuple, Any, Callable, List, Dict import numpy as np from nevergrad.common.typetools import ArrayLike -from .pyhv import _HyperVolume - +from .hypervolume import HypervolumeIndicator ArgsKwargs = Tuple[Tuple[Any, ...], Dict[str, Any]] @@ -37,7 +36,7 @@ class MultiobjectiveFunction: def __init__(self, multiobjective_function: Callable[..., ArrayLike], upper_bounds: ArrayLike) -> None: self.multiobjective_function = multiobjective_function self._upper_bounds = np.array(upper_bounds, copy=False) - self._hypervolume: Any = _HyperVolume(self._upper_bounds) # type: ignore + self._hypervolume: Any = HypervolumeIndicator(self._upper_bounds) # type: ignore self._points: List[Tuple[ArgsKwargs, np.ndarray]] = [] self._best_volume = -float("Inf") diff --git a/nevergrad/functions/multiobjective/hypervolume.py b/nevergrad/functions/multiobjective/hypervolume.py new file mode 100644 index 000000000..36c1c93d6 --- /dev/null +++ b/nevergrad/functions/multiobjective/hypervolume.py @@ -0,0 +1,309 @@ +# (C) Copyright 2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +import typing as tp +import numpy as np + + +class VectorNode: + """ A node object of the VectorLinkedList. + A VectorNode is a point in a space with dim = `dimension`, and an optional + `coordinate` (which can be assigned after the VectorNode initialization). + The VectorNode object points to two arrays with self.next and self.prev attributes. + The `self.next` contains a list of VectorNode (aka geometric points), such that + `self.next[i]` is a VectorNode immediately after the `self` on the i-th coordinate, + `self.prev[j]` is a VectorNode immediately before the `self` on the j-th coordinate. + The `area` is a vector, with its i-th element equal the area of the projection + of the `coordinate` on the (i-1) subspace. + The `volume` is the product of the `area` by the difference between the i-th + coordinate of the self and self.prev[i]. + The `dominated_flag` is used to skip dominated points (see section III.C). + The VectorNode data structure is introduced in section III.A of the original paper.. + """ + def __init__(self, dimension: int, coordinates: tp.Optional[tp.Union[np.ndarray, tp.List[float]]] = None) -> None: + self.dimension = dimension + self.coordinates = np.array(coordinates, copy=False) + self._next: tp.List["VectorNode"] = [self for _ in range(self.dimension)] + self._prev: tp.List["VectorNode"] = [self for _ in range(self.dimension)] + self.dominated_flag = 0 + self.area = np.zeros(self.dimension) + self.volume = np.zeros(self.dimension) + + def __str__(self) -> str: + return str(self.coordinates) + + def __lt__(self, other: tp.Any) -> bool: + assert isinstance(other, VectorNode) + return bool(np.all(self.coordinates < other.coordinates)) + + def configure_area(self, dimension: int) -> None: + self.area[0] = 1.0 + self.area[1: dimension + 1] = [ + -self.area[i] * self.coordinates[i] for i in range(dimension) + ] + + @property + def next(self) -> tp.List["VectorNode"]: + return self._next + + @property + def prev(self) -> tp.List["VectorNode"]: + return self._prev + + def pop(self, index: int) -> None: + """ Assigns the references of the self predecessor and successor at + `index` index to each other, removes the links to the `self` node. + """ + predecessor = self.prev[index] + successor = self.next[index] + assert predecessor is not None and successor is not None + predecessor.next[index] = successor + successor.prev[index] = predecessor + + +class VectorLinkedList: + """ Linked list structure with list of VectorNodes as elements.""" + def __init__(self, dimension: int) -> None: + self.dimension = dimension + self.sentinel = VectorNode(dimension) + + @classmethod + def create_sorted(cls, dimension: int, points: tp.Any) -> "VectorLinkedList": + """ Instantiate a VectorLinkedList of dimension `dimension`. The list is + populated by nodes::VectorNode created from `points`. The nodes are sorted + by i-th coordinate attribute in i-th row.""" + linked_list = cls(dimension) + nodes = [VectorNode(dimension, coordinates=point) for point in points] + for i in range(dimension): + sorted_node = cls.sort_by_index(nodes, i) + linked_list.extend(sorted_node, i) + return linked_list + + @staticmethod + def sort_by_index(node_list: tp.List[VectorNode], dimension_index: int) -> tp.List[VectorNode]: + """ Returns a sorted list of `VectorNode`, with the sorting key defined by the + `dimension_index`-th coordinates of the nodes in the `node_list`.""" + return sorted(node_list, key=lambda node: node.coordinates[dimension_index]) + + def __str__(self) -> str: + string = [ + str([str(node) for node in self.iterate(dimension)]) + for dimension in range(self.dimension) + ] + return "\n".join(string) + + def __len__(self) -> int: + return self.dimension + + def chain_length(self, index: int) -> int: + length = sum(1 for _ in self.iterate(index)) + return length + + def append(self, node: VectorNode, index: int) -> None: + """ Append a node to the `index`-th position.""" + current_last = self.sentinel.prev[index] + assert current_last is not None + node.next[index] = self.sentinel + node.prev[index] = current_last + self.sentinel.prev[index] = node + current_last.next[index] = node + + def extend(self, nodes: tp.List[VectorNode], index: int) -> None: + """ Extends the VectorLinkedList with a list of nodes + at `index` position""" + for node in nodes: + self.append(node, index) + + @staticmethod + def update_coordinate_bounds( + bounds: np.ndarray, + node: VectorNode, + index: int + ) -> np.ndarray: + for i in range(index): + if bounds[i] > node.coordinates[i]: + bounds[i] = node.coordinates[i] + return bounds + + def pop(self, node: VectorNode, index: int) -> VectorNode: + """ Removes and returns 'node' from all lists at the + positions from 0 in index (exclusively).""" + for i in range(index): + node.pop(i) + + return node + + def reinsert(self, node: VectorNode, index: int) -> None: + """ + Inserts 'node' at the position it had before it was removed + in all lists at the positions from 0 in index (exclusively). + This method assumes that the next and previous nodes of the + node that is reinserted are in the list. + """ + for i in range(index): + node.prev[i].next[i] = node + node.next[i].prev[i] = node + + def iterate(self, index: int, start: tp.Optional[VectorNode] = None) -> tp.Iterator[VectorNode]: + if start is None: + node = self.sentinel.next[index] + else: + node = start + while node is not self.sentinel: + assert node is not None + yield node + node = node.next[index] + + def reverse_iterate(self, index: int, start: tp.Optional[VectorNode] = None) -> tp.Iterator[VectorNode]: + if start is None: + node = self.sentinel.prev[index] + else: + node = start + while node is not self.sentinel: + assert node is not None + yield node + node = node.prev[index] + + +class HypervolumeIndicator: + """ Core class to calculate the hypervolme value of a set of points. + As introduced in the original paper, "the indicator is a measure of + the region which is simultaneously dominated by a set of points P, + and bounded by a reference point r = `self.reference_bounds`. It is + a union of axis-aligned hyper-rectangles with one common vertex, r." + + To calculate the hypervolume indicator, initialize an instance of the + HypervolumeIndicator; the hypervolume of a set of points P is calculated + by HypervolumeIndicator.compute(points = P) method. + + For the algorithm, refer to the section III and Algorithms 2, 3 of the + paper `An Improved Dimension-Sweep Algorithm for the Hypervolume Indicator` + by C.M. Fonseca et all, IEEE Congress on Evolutionary Computation, 2006. + """ + def __init__(self, reference_point: np.ndarray) -> None: + self.reference_point = np.array(reference_point, copy=False) + self.dimension = self.reference_point.size + self.reference_bounds = np.full(self.dimension, -np.inf) + self._multilist: tp.Optional[VectorLinkedList] = None + + @property + def multilist(self) -> VectorLinkedList: + assert self._multilist is not None + return self._multilist + + def compute(self, points: tp.Union[tp.List[np.ndarray], np.ndarray]) -> float: + points = points - self.reference_point + self.reference_bounds = np.full(self.dimension, -np.inf) + self._multilist = VectorLinkedList.create_sorted(self.dimension, points) + hypervolume = self.recursive_hypervolume(self.dimension - 1) + return hypervolume + + def plane_hypervolume(self) -> float: + """ Calculates the hypervolume on a two dimensional plane. The algorithm + is described in Section III-A of the original paper. """ + dimension = 1 + hypervolume = 0.0 + h = self.multilist.sentinel.next[dimension].coordinates[dimension - 1] + for node in self.multilist.iterate(dimension): + next_node = node.next[dimension] + if next_node is self.multilist.sentinel: + break + hypervolume += h * (node.coordinates[dimension] - next_node.coordinates[dimension]) + h = min(h, next_node.coordinates[dimension - 1]) + last_node = self.multilist.sentinel.prev[dimension] + hypervolume += h * last_node.coordinates[dimension] + return hypervolume + + def recursive_hypervolume(self, dimension: int) -> float: + """ Recursive hypervolume computation. The algorithm is provided by Algorithm 3. + of the original paper.""" + if self.multilist.chain_length(dimension-1) == 0: + return 0 + assert self.multilist is not None + if dimension == 0: + return -float(self.multilist.sentinel.next[0].coordinates[0]) + + if dimension == 1: + return self.plane_hypervolume() + + # Line 4 + for node in self.multilist.reverse_iterate(dimension): + assert node is not None + if node.dominated_flag < dimension: + node.dominated_flag = 0 + # Line 5 + hypervolume = 0.0 + # Line 6 + current_node = self.multilist.sentinel.prev[dimension] + # Lines 7 to 12 + for node in self.multilist.reverse_iterate(dimension, start=current_node): + assert node is not None + current_node = node + if self.multilist.chain_length(dimension-1) > 1 and ( + node.coordinates[dimension] > self.reference_bounds[dimension] + or node.prev[dimension].coordinates[dimension] >= self.reference_bounds[dimension] + ): + # Line 9 + self.reference_bounds = self.multilist.update_coordinate_bounds( + self.reference_bounds, node, dimension + ) # type: ignore + # Line 10 + self.multilist.pop(node, dimension) + # Line 11 + # front_size -= 1 + else: + break + + # Line 13 + if self.multilist.chain_length(dimension-1) > 1: + # Line 14 + hypervolume = current_node.prev[dimension].volume[dimension] + hypervolume += current_node.prev[dimension].area[dimension] * ( + current_node.coordinates[dimension] - current_node.prev[dimension].coordinates[dimension] + ) + else: + current_node.configure_area(dimension) + + # Line 15 + current_node.volume[dimension] = hypervolume + # Line 16 + self.skip_dominated_points(current_node, dimension) + + # Line 17 + for node in self.multilist.iterate(dimension, start=current_node.next[dimension]): + assert node is not None + # Line 18 + hypervolume += node.prev[dimension].area[dimension] * ( + node.coordinates[dimension] - node.prev[dimension].coordinates[dimension] + ) + # Line 19 + self.reference_bounds[dimension] = node.coordinates[dimension] + # Line 20 + self.reference_bounds = self.multilist.update_coordinate_bounds( + self.reference_bounds, node, dimension + ) # type: ignore + # Line 21 + self.multilist.reinsert(node, dimension) + # Line 22 + # front_size += 1 + # Line 25 + node.volume[dimension] = hypervolume + # Line 26 + self.skip_dominated_points(node, dimension) + + # Line 27 + last_node = self.multilist.sentinel.prev[dimension] + hypervolume -= last_node.area[dimension] * last_node.coordinates[dimension] + return hypervolume + + def skip_dominated_points(self, node: VectorNode, dimension: int) -> None: + """ Implements Algorithm 2, _skipdom_, for skipping dominated points.""" + if node.dominated_flag >= dimension: + node.area[dimension] = node.prev[dimension].area[dimension] + else: + node.area[dimension] = self.recursive_hypervolume(dimension - 1) + if node.area[dimension] <= node.prev[dimension].area[dimension]: + node.dominated_flag = dimension diff --git a/nevergrad/functions/multiobjective/pyhv.py b/nevergrad/functions/multiobjective/pyhv.py deleted file mode 100644 index cae3bb83c..000000000 --- a/nevergrad/functions/multiobjective/pyhv.py +++ /dev/null @@ -1,295 +0,0 @@ -# This file is part of DEAP. -# -# Copyright (C) 2010 Simon Wessing -# TU Dortmund University -# -# In personal communication, the original authors authorized DEAP team -# to use this file under the Lesser General Public License. -# -# You can find the original library here : -# http://ls11-www.cs.uni-dortmund.de/_media/rudolph/hypervolume/hv_python.zip -# -# DEAP is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as -# published by the Free Software Foundation, either version 3 of -# the License, or (at your option) any later version. -# -# DEAP 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with DEAP. If not, see . -# -# Modified by O. Teytaud as needed for Nevergrad. - -from math import log, floor -import random -import warnings - - -class _HyperVolume: - """ - Hypervolume computation based on variant 3 of the algorithm in the paper: - C. M. Fonseca, L. Paquete, and M. Lopez-Ibanez. An improved dimension-sweep - algorithm for the hypervolume indicator. In IEEE Congress on Evolutionary - Computation, pages 1157-1163, Vancouver, Canada, July 2006. - - Minimization is implicitly assumed here! - - """ - - def __init__(self, referencePoint): - """Constructor.""" - self.referencePoint = referencePoint - self.list = [] - - def compute(self, front): - """Returns the hypervolume that is dominated by a non-dominated front. - - Before the HV computation, front and reference point are translated, so - that the reference point is [0, ..., 0]. - - """ - - def weaklyDominates(point, other): - for i in range(len(point)): - if point[i] > other[i]: - return False - return True - - relevantPoints = [] - referencePoint = self.referencePoint - dimensions = len(referencePoint) - ####### - # fmder: Here it is assumed that every point dominates the reference point - # for point in front: - # # only consider points that dominate the reference point - # if weaklyDominates(point, referencePoint): - # relevantPoints.append(point) - relevantPoints = front - # fmder - ####### - if any(referencePoint): - # shift points so that referencePoint == [0, ..., 0] - # this way the reference point doesn't have to be explicitly used - # in the HV computation - - ####### - # fmder: Assume relevantPoints are numpy array - # for j in xrange(len(relevantPoints)): - # relevantPoints[j] = [relevantPoints[j][i] - referencePoint[i] for i in xrange(dimensions)] - relevantPoints -= referencePoint - # fmder - ####### - - self.preProcess(relevantPoints) - bounds = [-1.0e308] * dimensions - hyperVolume = self.hvRecursive(dimensions - 1, len(relevantPoints), bounds) - return hyperVolume - - def hvRecursive(self, dimIndex, length, bounds): - """Recursive call to hypervolume calculation. - - In contrast to the paper, the code assumes that the reference point - is [0, ..., 0]. This allows the avoidance of a few operations. - - """ - hvol = 0.0 - sentinel = self.list.sentinel - if length == 0: - return hvol - elif dimIndex == 0: - # special case: only one dimension - # why using hypervolume at all? - return -sentinel.next[0].cargo[0] - elif dimIndex == 1: - # special case: two dimensions, end recursion - q = sentinel.next[1] - h = q.cargo[0] - p = q.next[1] - while p is not sentinel: - pCargo = p.cargo - hvol += h * (q.cargo[1] - pCargo[1]) - if pCargo[0] < h: - h = pCargo[0] - q = p - p = q.next[1] - hvol += h * q.cargo[1] - return hvol - else: - remove = self.list.remove - reinsert = self.list.reinsert - hvRecursive = self.hvRecursive - p = sentinel - q = p.prev[dimIndex] - while q.cargo is not None: # Risky change. https://github.com/facebookresearch/nevergrad/issues/346 - if q.ignore < dimIndex: - q.ignore = 0 - q = q.prev[dimIndex] - q = p.prev[dimIndex] - while length > 1 and (q.cargo[dimIndex] > bounds[dimIndex] or q.prev[dimIndex].cargo[dimIndex] >= bounds[dimIndex]): - p = q - remove(p, dimIndex, bounds) - q = p.prev[dimIndex] - length -= 1 - qArea = q.area - qCargo = q.cargo - qPrevDimIndex = q.prev[dimIndex] - if length > 1: - hvol = qPrevDimIndex.volume[dimIndex] + qPrevDimIndex.area[dimIndex] * (qCargo[dimIndex] - qPrevDimIndex.cargo[dimIndex]) - else: - qArea[0] = 1 - qArea[1:dimIndex + 1] = [qArea[i] * -qCargo[i] for i in range(dimIndex)] - q.volume[dimIndex] = hvol - if q.ignore >= dimIndex: - qArea[dimIndex] = qPrevDimIndex.area[dimIndex] - else: - qArea[dimIndex] = hvRecursive(dimIndex - 1, length, bounds) - if qArea[dimIndex] <= qPrevDimIndex.area[dimIndex]: - q.ignore = dimIndex - while p is not sentinel: - pCargoDimIndex = p.cargo[dimIndex] - hvol += q.area[dimIndex] * (pCargoDimIndex - q.cargo[dimIndex]) - bounds[dimIndex] = pCargoDimIndex - reinsert(p, dimIndex, bounds) - length += 1 - q = p - p = p.next[dimIndex] - q.volume[dimIndex] = hvol - if q.ignore >= dimIndex: - q.area[dimIndex] = q.prev[dimIndex].area[dimIndex] - else: - q.area[dimIndex] = hvRecursive(dimIndex - 1, length, bounds) - if q.area[dimIndex] <= q.prev[dimIndex].area[dimIndex]: - q.ignore = dimIndex - hvol -= q.area[dimIndex] * q.cargo[dimIndex] - return hvol - - def preProcess(self, front): - """Sets up the list data structure needed for calculation.""" - dimensions = len(self.referencePoint) - nodeList = _MultiList(dimensions) - nodes = [_MultiList.Node(dimensions, point) for point in front] - for i in range(dimensions): - self.sortByDimension(nodes, i) - nodeList.extend(nodes, i) - self.list = nodeList - - def sortByDimension(self, nodes, i): - """Sorts the list of nodes by the i-th value of the contained points.""" - # build a list of tuples of (point[i], node) - decorated = [(node.cargo[i], node) for node in nodes] - # sort by this value - decorated.sort() - # write back to original list - nodes[:] = [node for (_, node) in decorated] - - -class _MultiList: - """A special data structure needed by FonsecaHyperVolume. - - It consists of several doubly linked lists that share common nodes. So, - every node has multiple predecessors and successors, one in every list. - - """ - - class Node: - - def __init__(self, numberLists, cargo=None): - self.cargo = cargo - self.next = [None] * numberLists - self.prev = [None] * numberLists - self.ignore = 0 - self.area = [0.0] * numberLists - self.volume = [0.0] * numberLists - - def __str__(self): - return str(self.cargo) - - def __lt__(self, other): - return all(self.cargo < other.cargo) - - def __init__(self, numberLists): - """Constructor. - - Builds 'numberLists' doubly linked lists. - - """ - self.numberLists = numberLists - self.sentinel = _MultiList.Node(numberLists) - self.sentinel.next = [self.sentinel] * numberLists - self.sentinel.prev = [self.sentinel] * numberLists - - def __str__(self): - strings = [] - for i in range(self.numberLists): - currentList = [] - node = self.sentinel.next[i] - while node != self.sentinel: - currentList.append(str(node)) - node = node.next[i] - strings.append(str(currentList)) - stringRepr = "" - for string in strings: - stringRepr += string + "\n" - return stringRepr - - def __len__(self): - """Returns the number of lists that are included in this _MultiList.""" - return self.numberLists - - def getLength(self, i): - """Returns the length of the i-th list.""" - length = 0 - sentinel = self.sentinel - node = sentinel.next[i] - while node != sentinel: - length += 1 - node = node.next[i] - return length - - def append(self, node, index): - """Appends a node to the end of the list at the given index.""" - lastButOne = self.sentinel.prev[index] - node.next[index] = self.sentinel - node.prev[index] = lastButOne - # set the last element as the new one - self.sentinel.prev[index] = node - lastButOne.next[index] = node - - def extend(self, nodes, index): - """Extends the list at the given index with the nodes.""" - sentinel = self.sentinel - for node in nodes: - lastButOne = sentinel.prev[index] - node.next[index] = sentinel - node.prev[index] = lastButOne - # set the last element as the new one - sentinel.prev[index] = node - lastButOne.next[index] = node - - def remove(self, node, index, bounds): - """Removes and returns 'node' from all lists in [0, 'index'[.""" - for i in range(index): - predecessor = node.prev[i] - successor = node.next[i] - predecessor.next[i] = successor - successor.prev[i] = predecessor - if bounds[i] > node.cargo[i]: - bounds[i] = node.cargo[i] - return node - - def reinsert(self, node, index, bounds): - """ - Inserts 'node' at the position it had in all lists in [0, 'index'[ - before it was removed. This method assumes that the next and previous - nodes of the node that is reinserted are in the list. - - """ - for i in range(index): - node.prev[i].next[i] = node - node.next[i].prev[i] = node - if bounds[i] > node.cargo[i]: - bounds[i] = node.cargo[i] diff --git a/nevergrad/functions/multiobjective/test_structures.py b/nevergrad/functions/multiobjective/test_structures.py new file mode 100644 index 000000000..806ff0809 --- /dev/null +++ b/nevergrad/functions/multiobjective/test_structures.py @@ -0,0 +1,310 @@ +# (C) Copyright 2020 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +import numpy as np + +from nevergrad.functions.multiobjective.hypervolume import ( + VectorNode, + VectorLinkedList, + HypervolumeIndicator, +) + + +def test_initialize_empty_node() -> None: + dim = 4 + node = VectorNode(dim) + + assert isinstance(node.coordinates, np.ndarray) + for entry in node.next: + assert entry is node + for entry in node.prev: + assert entry is node + + assert list(node.area) == [0.0] * dim + assert list(node.volume) == [0.0] * dim + assert str(node) == "None" + + +def test_initialize_node() -> None: + dim = 4 + coordinates = [1.0, 2.0, 3.0] + node = VectorNode(dim, coordinates=coordinates) + + assert isinstance(node.coordinates, np.ndarray) + assert list(node.coordinates) == coordinates + for entry in node.next: + assert entry is node + for entry in node.prev: + assert entry is node + + assert list(node.area) == [0.0] * dim + assert list(node.volume) == [0.0] * dim + assert str(node) == "[1. 2. 3.]" + + node.configure_area(0) + assert node.area[0] == 1.0 + assert node.area[1] == 0.0 + assert node.area[2] == 0.0 + + node.configure_area(1) + assert node.area[0] == 1.0 + assert node.area[1] == -1.0 + assert node.area[2] == 0.0 + + node.configure_area(2) + assert node.area[0] == 1.0 + assert node.area[1] == -1.0 + assert node.area[2] == 2.0 + + +def test_initialize_linked_list() -> None: + dim = 4 + multilist = VectorLinkedList(dimension=dim) + + assert dim == multilist.dimension + assert isinstance(multilist.sentinel, VectorNode) + assert len(multilist.sentinel.prev) == 4 + assert len(multilist.sentinel.next) == 4 + assert len(multilist) == 4 + + for d in range(dim): + assert multilist.sentinel is multilist.sentinel.next[d] + assert multilist.sentinel is multilist.sentinel.prev[d] + + assert len(multilist.sentinel.next) == len(multilist.sentinel.prev) + assert len(multilist.sentinel.next) == len(multilist.sentinel.next[0].next) + + assert str(multilist) == "\n".join([str([])] * dim) + + +def test_append() -> None: + dim = 4 + multilist = VectorLinkedList(dimension=dim) + + new_node = VectorNode(dim) + multilist.append(new_node, 0) + + for i in range(1, dim): + assert new_node.next[i] is new_node + assert new_node.prev[i] is new_node + assert multilist.sentinel.next[i] is multilist.sentinel + assert multilist.sentinel.prev[i] is multilist.sentinel + + assert new_node.next[0] is multilist.sentinel + assert new_node.prev[0] is multilist.sentinel + assert multilist.sentinel.next[0] is new_node + assert multilist.sentinel.prev[0] is new_node + + another_node = VectorNode(dim) + multilist.append(another_node, 0) + for i in range(1, dim): + assert new_node.next[i] is new_node + assert new_node.prev[i] is new_node + assert multilist.sentinel.next[i] is multilist.sentinel + assert multilist.sentinel.prev[i] is multilist.sentinel + + assert new_node.next[0] is another_node + assert new_node.prev[0] is multilist.sentinel + assert multilist.sentinel.next[0] is new_node + assert multilist.sentinel.prev[0] is another_node + + +def test_extend() -> None: + dim = 1 + multilist = VectorLinkedList(dimension=dim) + another_multilist = VectorLinkedList(dimension=dim) + + new_node = VectorNode(dim) + another_node = VectorNode(dim) + + multilist.append(new_node, 0) + multilist.append(another_node, 0) + + another_multilist.extend([new_node, another_node], 0) + assert another_multilist.chain_length(0) == 2 + assert another_multilist.sentinel.next[0] is multilist.sentinel.next[0] + assert another_multilist.sentinel.next[0].next[0] is multilist.sentinel.next[0].next[0] + + +def test_chain_length() -> None: + dim = 3 + multilist = VectorLinkedList(dimension=dim) + + new_node = VectorNode(dim) + multilist.append(new_node, 0) + assert multilist.chain_length(0) == 1 + assert multilist.chain_length(1) == 0 + assert multilist.chain_length(2) == 0 + + another_node = VectorNode(dim) + multilist.append(another_node, 0) + assert multilist.chain_length(0) == 2 + assert multilist.chain_length(1) == 0 + assert multilist.chain_length(2) == 0 + + multilist.append(another_node, 1) + assert multilist.chain_length(0) == 2 + assert multilist.chain_length(1) == 1 + assert multilist.chain_length(2) == 0 + + multilist.append(new_node, 2) + assert multilist.chain_length(0) == 2 + assert multilist.chain_length(1) == 1 + assert multilist.chain_length(2) == 1 + + +def test_pop() -> None: + dim = 4 + multilist = VectorLinkedList(dimension=dim) + + new_node = VectorNode(dim) + multilist.append(new_node, 0) + + popped_node = multilist.pop(new_node, 0 + 1) + assert popped_node is new_node + assert new_node.next[0] is multilist.sentinel + assert new_node.prev[0] is multilist.sentinel + for i in range(dim): + assert multilist.sentinel.next[i] is multilist.sentinel + assert multilist.sentinel.prev[i] is multilist.sentinel + + +def test_reinsert() -> None: + dim = 2 + multilist = VectorLinkedList(dimension=dim) + + new_node = VectorNode(dim) + another_node = VectorNode(dim) + + multilist.append(new_node, 0) + multilist.append(another_node, 0) + + multilist.append(another_node, 1) + multilist.append(new_node, 1) + + popped_node = multilist.pop(new_node, 1 + 1) + + multilist.reinsert(new_node, 0 + 1) + assert multilist.chain_length(0) == 2 + assert multilist.chain_length(1) == 1 + assert new_node.next[0] is another_node + assert new_node.prev[0] is multilist.sentinel + assert another_node.prev[0] is new_node + assert another_node.next[0] is multilist.sentinel + assert another_node.prev[1] is multilist.sentinel + assert another_node.next[1] is multilist.sentinel + + multilist.reinsert(popped_node, 1 + 1) + assert multilist.chain_length(0) == 2 + assert multilist.chain_length(1) == 2 + assert another_node.prev[1] is multilist.sentinel + assert another_node.next[1] is new_node + assert new_node.prev[1] is another_node + assert new_node.next[1] is multilist.sentinel + + +def test_iterate() -> None: + dim = 1 + multilist = VectorLinkedList(dimension=dim) + + new_node = VectorNode(dim) + another_node = VectorNode(dim) + + multilist.append(new_node, 0) + multilist.append(another_node, 0) + gen = multilist.iterate(0) + assert next(gen) is new_node + assert next(gen) is another_node + + yet_another_node = VectorNode(dim) + multilist.append(yet_another_node, 0) + gen = multilist.iterate(0, start=another_node) + assert next(gen) is another_node + assert next(gen) is yet_another_node + + +def test_reverse_iterate() -> None: + dim = 1 + multilist = VectorLinkedList(dimension=dim) + + new_node = VectorNode(dim) + another_node = VectorNode(dim) + yet_another_node = VectorNode(dim) + + multilist.append(new_node, 0) + multilist.append(another_node, 0) + multilist.append(yet_another_node, 0) + + gen = multilist.reverse_iterate(0) + assert next(gen) is yet_another_node + assert next(gen) is another_node + assert next(gen) is new_node + + gen = multilist.reverse_iterate(0, start=another_node) + assert next(gen) is another_node + assert next(gen) is new_node + + +def test_update_coordinate_bounds() -> None: + bounds = np.array([-1.0, -1.0, -1.0]) + node = VectorNode(3, coordinates=[1.0, -2.0, -1.0]) + bounds = VectorLinkedList.update_coordinate_bounds(bounds, node, 0 + 1) + assert list(bounds) == [-1, -1, -1] + bounds = VectorLinkedList.update_coordinate_bounds(bounds, node, 1 + 1) + assert list(bounds) == [-1, -2, -1] + bounds = VectorLinkedList.update_coordinate_bounds(bounds, node, 2 + 1) + assert list(bounds) == [-1, -2, -1] + + +def test_sort_by_index() -> None: + nodes = [ + VectorNode(3, [1, 2, 3]), + VectorNode(3, [2, 3, 1]), + VectorNode(3, [3, 1, 2]) + ] + new_nodes = VectorLinkedList.sort_by_index(nodes, 0) + assert new_nodes == nodes + + new_nodes = VectorLinkedList.sort_by_index(nodes, 1) + assert new_nodes == [nodes[2], nodes[0], nodes[1]] + + new_nodes = VectorLinkedList.sort_by_index(nodes, 2) + assert new_nodes == [nodes[1], nodes[2], nodes[0]] + + +def test_create_sorted() -> None: + dimension = 3 + coordinates = [ + [1, 2, 3], + [2, 3, 1], + [3, 1, 2] + ] + linked_list = VectorLinkedList.create_sorted(dimension, coordinates) + assert isinstance(linked_list, VectorLinkedList) + assert list(linked_list.sentinel.next[0].coordinates) == [1, 2, 3] + assert list(linked_list.sentinel.next[1].coordinates) == [3, 1, 2] + assert list(linked_list.sentinel.next[2].coordinates) == [2, 3, 1] + + assert list(linked_list.sentinel.next[0].next[0].coordinates) == [2, 3, 1] + assert list(linked_list.sentinel.next[1].next[1].coordinates) == [1, 2, 3] + assert list(linked_list.sentinel.next[2].next[2].coordinates) == [3, 1, 2] + + +def test_version_consistency() -> None: + reference = np.array([79, 89, 99]) + hv = HypervolumeIndicator(reference) + front = np.array( + [ + (110, 110, 100), + (110, 90, 87), + (80, 80, 36), + (50, 50, 55), + (105, 30, 43), + (110, 110, 100) + ] + ) + volume = hv.compute(front) + assert volume == 11113.0