"""Provides the IsoLineEmitter."""
import numpy as np
from ribs._utils import check_batch_shape, check_shape
from ribs.emitters._emitter_base import EmitterBase
from ribs.emitters.operators import IsoLineOperator
[docs]class IsoLineEmitter(EmitterBase):
"""Emits solutions that are nudged towards other archive solutions.
If the archive is empty and ``self._initial_solutions`` is set, a call to
:meth:`ask` will return ``self._initial_solutions``. If
``self._initial_solutions`` is not set, we draw solutions from an isotropic
Gaussian distribution centered at ``self.x0`` with standard deviation
``self.iso_sigma``. Otherwise, to generate each new solution, the emitter
selects a pair of elites :math:`x_i` and :math:`x_j` and samples from
.. math::
x_i + \\sigma_{iso} \\mathcal{N}(0,\\mathcal{I}) +
\\sigma_{line}(x_j - x_i)\\mathcal{N}(0,1)
This emitter is based on the Iso+LineDD operator presented in `Vassiliades
2018 <https://arxiv.org/abs/1804.03906>`_.
Args:
archive (ribs.archives.ArchiveBase): An archive to use when creating and
inserting solutions. For instance, this can be
:class:`ribs.archives.GridArchive`.
iso_sigma (float): Scale factor for the isotropic distribution used to
generate solutions.
line_sigma (float): Scale factor for the line distribution used when
generating solutions.
x0 (array-like): Center of the Gaussian distribution from which to
sample solutions when the archive is empty. Must be 1-dimensional.
This argument is ignored if ``initial_solutions`` is set.
initial_solutions (array-like): An (n, solution_dim) array of solutions
to be used when the archive is empty. If this argument is None, then
solutions will be sampled from a Gaussian distribution centered at
``x0`` with standard deviation ``iso_sigma``.
bounds (None or array-like): Bounds of the solution space. Solutions are
clipped to these bounds. Pass None to indicate there are no bounds.
Alternatively, pass an array-like to specify the bounds for each
dim. Each element in this array-like can be None to indicate no
bound, or a tuple of ``(lower_bound, upper_bound)``, where
``lower_bound`` or ``upper_bound`` may be None to indicate no bound.
batch_size (int): Number of solutions to return in :meth:`ask`.
seed (int): Value to seed the random number generator. Set to None to
avoid a fixed seed.
Raises:
ValueError: There is an error in x0 or initial_solutions.
ValueError: There is an error in the bounds configuration.
"""
def __init__(self,
archive,
*,
iso_sigma=0.01,
line_sigma=0.2,
x0=None,
initial_solutions=None,
bounds=None,
batch_size=64,
seed=None):
self._rng = np.random.default_rng(seed)
self._batch_size = batch_size
self._iso_sigma = archive.dtype(iso_sigma)
self._line_sigma = archive.dtype(line_sigma)
self._x0 = None
self._initial_solutions = None
if x0 is None and initial_solutions is None:
raise ValueError("Either x0 or initial_solutions must be provided.")
if x0 is not None and initial_solutions is not None:
raise ValueError(
"x0 and initial_solutions cannot both be provided.")
if x0 is not None:
self._x0 = np.array(x0, dtype=archive.dtype)
check_shape(self._x0, "x0", archive.solution_dim,
"archive.solution_dim")
elif initial_solutions is not None:
self._initial_solutions = np.asarray(initial_solutions,
dtype=archive.dtype)
check_batch_shape(self._initial_solutions, "initial_solutions",
archive.solution_dim, "archive.solution_dim")
EmitterBase.__init__(
self,
archive,
solution_dim=archive.solution_dim,
bounds=bounds,
)
self._operator = IsoLineOperator(line_sigma=self._line_sigma,
iso_sigma=self._iso_sigma,
lower_bounds=self._lower_bounds,
upper_bounds=self._upper_bounds,
seed=seed)
@property
def x0(self):
"""numpy.ndarray: Center of the Gaussian distribution from which to
sample solutions when the archive is empty (if initial_solutions is not
set)."""
return self._x0
@property
def initial_solutions(self):
"""numpy.ndarray: The initial solutions which are returned when the
archive is empty (if x0 is not set)."""
return self._initial_solutions
@property
def iso_sigma(self):
"""float: Scale factor for the isotropic distribution used to
generate solutions when the archive is not empty."""
return self._iso_sigma
@property
def line_sigma(self):
"""float: Scale factor for the line distribution used when generating
solutions."""
return self._line_sigma
@property
def batch_size(self):
"""int: Number of solutions to return in :meth:`ask`."""
return self._batch_size
[docs] def ask(self):
"""Generates ``batch_size`` solutions.
If the archive is empty and ``self._initial_solutions`` is set, we
return ``self._initial_solutions``. If ``self._initial_solutions`` is
not set, we draw solutions from an isotropic Gaussian distribution
centered at ``self.x0`` with standard deviation ``self.iso_sigma``.
Otherwise, each solution is drawn from a distribution centered at
a randomly chosen elite with standard deviation ``self.iso_sigma``.
Returns:
If the archive is not empty, ``(batch_size, solution_dim)`` array
-- contains ``batch_size`` new solutions to evaluate. If the
archive is empty, we return ``self._initial_solutions``, which
might not have ``batch_size`` solutions.
"""
if self.archive.empty and self._initial_solutions is not None:
return np.clip(self._initial_solutions, self.lower_bounds,
self.upper_bounds)
if self.archive.empty:
parents = np.repeat(self.x0[None],
repeats=2 * self._batch_size,
axis=0)
else:
parents = self.archive.sample_elites(2 *
self._batch_size)["solution"]
return self._operator.ask(
parents=parents.reshape(2, self._batch_size, -1))