def _remap(self):
        """Remaps the archive.

        The boundaries are relocated to the percentage marks of the distribution
        of solutions stored in the archive.

        Also re-adds all of the solutions to the archive.

        Returns:
            tuple: The result of calling :meth:`ArchiveBase.add` on the last
            item in the buffer.
        """
        # Sort all behavior values along the axis of each bc.
        sorted_bc = self._buffer.sorted_behavior_values

        # Calculate new boundaries.
        SlidingBoundariesArchive._remap_numba_helper(sorted_bc,
                                                     self._buffer.size,
                                                     self._boundaries,
                                                     self._behavior_dim,
                                                     self.dims)

        index_columns = tuple(map(list, zip(*self._occupied_indices)))
        old_sols = self._solutions[index_columns].copy()
        old_objs = self._objective_values[index_columns].copy()
        old_behs = self._behavior_values[index_columns].copy()

        self._reset_archive()
        for sol, obj, beh in zip(old_sols, old_objs, old_behs):
            # Add solutions from old archive.
            status, value = ArchiveBase.add(self, sol, obj, beh)
        for sol, obj, beh in self._buffer:
            # Add solutions from buffer.
            status, value = ArchiveBase.add(self, sol, obj, beh)
        return status, value
Example #2
0
    def __init__(self, dims, ranges, seed=None, dtype=np.float64):
        self._dims = np.array(dims)
        if len(self._dims) != len(ranges):
            raise ValueError(f"dims (length {len(self._dims)}) and ranges "
                             f"(length {len(ranges)}) must be the same length")

        ArchiveBase.__init__(
            self,
            storage_dims=tuple(self._dims),
            behavior_dim=len(self._dims),
            seed=seed,
            dtype=dtype,
        )

        ranges = list(zip(*ranges))
        self._lower_bounds = np.array(ranges[0], dtype=self.dtype)
        self._upper_bounds = np.array(ranges[1], dtype=self.dtype)
        self._interval_size = self._upper_bounds - self._lower_bounds

        self._boundaries = []
        for dim, lower_bound, upper_bound in zip(self._dims,
                                                 self._lower_bounds,
                                                 self._upper_bounds):
            self._boundaries.append(
                np.linspace(lower_bound, upper_bound, dim + 1))
    def add(self, solution, objective_value, behavior_values):
        """Attempts to insert a new solution into the archive.

        This method remaps the archive after every ``self.remap_frequency``
        solutions are added. Remapping involves changing the boundaries of the
        archive to the percentage marks of the behavior values stored in the
        buffer and re-adding all of the solutions stored in the buffer `and` the
        current archive.

        Args:
            solution (array-like): See :meth:`ArchiveBase.add`
            objective_value (float): See :meth:`ArchiveBase.add`
            behavior_values (array-like): See :meth:`ArchiveBase.add`
        Returns:
            See :meth:`ArchiveBase.add`
        """
        solution = np.asarray(solution)
        behavior_values = np.asarray(behavior_values)

        self._buffer.add(solution, objective_value, behavior_values)
        self._total_num_sol += 1

        if self._total_num_sol % self._remap_frequency == 0:
            status, value = self._remap()
            self._lower_bounds = np.array(
                [bound[0] for bound in self._boundaries])
            self._upper_bounds = np.array([
                bound[dim] for bound, dim in zip(self._boundaries, self._dims)
            ])
        else:
            status, value = ArchiveBase.add(self, solution, objective_value,
                                            behavior_values)
        return status, value
    def _remap(self):
        """Remaps the archive.

        The boundaries are relocated to the percentage marks of the distribution
        of solutions stored in the archive.

        Also re-adds all of the solutions to the archive.

        Returns:
            tuple: The result of calling :meth:`ArchiveBase.add` on the last
            item in the buffer.
        """
        # Sort all behavior values along the axis of each bc.
        sorted_bc = self._buffer.sorted_behavior_values

        # Calculate new boundaries.
        SlidingBoundariesArchive._remap_numba_helper(sorted_bc,
                                                     self._buffer.size,
                                                     self._boundaries,
                                                     self._behavior_dim,
                                                     self.dims)

        # TODO (btjanaka): Add an option that allows adding solutions from the
        # previous archive that are not in the buffer.

        # Add all solutions to the new empty archive.
        self._reset_archive()
        for solution, objective_value, behavior_value in self._buffer:
            status, value = ArchiveBase.add(self, solution, objective_value,
                                            behavior_value)
        return status, value
    def __init__(self,
                 dims,
                 ranges,
                 seed=None,
                 dtype=np.float64,
                 remap_frequency=100,
                 buffer_capacity=1000):
        self._dims = np.array(dims)
        if len(self._dims) != len(ranges):
            raise ValueError(f"dims (length {len(self._dims)}) and ranges "
                             f"(length {len(ranges)}) must be the same length")

        ArchiveBase.__init__(
            self,
            storage_dims=tuple(self._dims),
            behavior_dim=len(self._dims),
            seed=seed,
            dtype=dtype,
        )

        ranges = list(zip(*ranges))
        self._lower_bounds = np.array(ranges[0], dtype=self.dtype)
        self._upper_bounds = np.array(ranges[1], dtype=self.dtype)
        self._interval_size = self._upper_bounds - self._lower_bounds

        # Specifics for sliding boundaries.
        self._remap_frequency = remap_frequency

        # Allocate an extra entry in each row so we can put the upper bound at
        # the end.
        self._boundaries = np.full(
            (self._behavior_dim, np.max(self._dims) + 1),
            np.nan,
            dtype=self.dtype)

        # Initialize the boundaries.
        for i, (dim, lower_bound, upper_bound) in enumerate(
                zip(self._dims, self._lower_bounds, self._upper_bounds)):
            self._boundaries[i, :dim + 1] = np.linspace(
                lower_bound, upper_bound, dim + 1)

        # Create buffer.
        self._buffer = SolutionBuffer(buffer_capacity, self._behavior_dim)

        # Total number of solutions encountered.
        self._total_num_sol = 0
Example #6
0
    def initialize(self, solution_dim):
        """Initializes the archive storage space and centroids.

        This method may take a while to run. In addition to allocating storage
        space, it runs :func:`~sklearn.cluster.k_means` to create an approximate
        CVT, and it constructs a :class:`~scipy.spatial.cKDTree` object
        containing the centroids found by k-means. k-means is not run if
        ``custom_centroids`` was passed in during construction.

        Args:
            solution_dim (int): The dimension of the solution space.
        Raises:
            RuntimeError: The archive is already initialized.
            RuntimeError: The number of centroids returned by k-means clustering
                was fewer than the number of bins specified during construction.
                This is most likely caused by having too few samples and too
                many bins.
        """
        ArchiveBase.initialize(self, solution_dim)

        if self._centroids is None:
            self._samples = self._rng.uniform(
                self._lower_bounds,
                self._upper_bounds,
                size=(self._samples, self._behavior_dim),
            ).astype(self.dtype) if isinstance(self._samples,
                                               int) else self._samples

            self._centroids = k_means(self._samples, self._bins,
                                      **self._k_means_kwargs)[0]

            if self._centroids.shape[0] < self._bins:
                raise RuntimeError(
                    "While generating the CVT, k-means clustering found "
                    f"{self._centroids.shape[0]} centroids, but this archive "
                    f"needs {self._bins} bins. This most likely happened "
                    "because there are too few samples and/or too many bins.")

        if self._use_kd_tree:
            self._centroid_kd_tree = cKDTree(self._centroids,
                                             **self._ckdtree_kwargs)
Example #7
0
    def as_pandas(self, include_solutions=True):
        """Converts the archive into a Pandas dataframe.

        Args:
            include_solutions (bool): Whether to include solution columns.
        Returns:
            pandas.DataFrame: A dataframe where each row is an elite in the
            archive. The dataframe has ``behavior_dim`` columns called
            ``index_{i}`` for the archive index, ``behavior_dim`` columns called
            ``behavior_{i}`` for the behavior values, 1 column for the objective
            function value called ``objective``, and ``solution_dim`` columns
            called ``solution_{i}`` for the solution values.
        """
        return ArchiveBase.as_pandas(self, include_solutions)
Example #8
0
    def as_pandas(self, include_solutions=True):
        """Converts the archive into a Pandas dataframe.

        Args:
            include_solutions (bool): Whether to include solution columns.
        Returns:
            pandas.DataFrame: A dataframe where each row is an elite in the
            archive. The dataframe consists of 1 ``index`` column indicating the
            index of the centroid in ``self._centroids``, ``behavior_dim``
            columns called ``behavior_{i}`` for the behavior values, 1 column
            for the objective function value called ``objective``, and
            ``solution_dim`` columns called ``solution_{i}`` for the solution
            values.
        """
        df = ArchiveBase.as_pandas(self, include_solutions)
        df.rename(columns={"index_0": "index"}, inplace=True)
        return df
Example #9
0
            the wrong shape.
    """
    def __init__(self,
                 bins,
                 ranges,
                 seed=None,
                 dtype=np.float64,
                 samples=100_000,
                 custom_centroids=None,
                 k_means_kwargs=None,
                 use_kd_tree=False,
                 ckdtree_kwargs=None):
        ArchiveBase.__init__(
            self,
            storage_dims=(bins, ),
            behavior_dim=len(ranges),
            seed=seed,
            dtype=dtype,
        )

        ranges = list(zip(*ranges))
        self._lower_bounds = np.array(ranges[0], dtype=self.dtype)
        self._upper_bounds = np.array(ranges[1], dtype=self.dtype)

        self._bins = bins

        # Apply default args for k-means. Users can easily override these,
        # particularly if they want higher quality clusters.
        self._k_means_kwargs = ({} if k_means_kwargs is None else
                                k_means_kwargs.copy())
        if "n_init" not in self._k_means_kwargs: