diff --git a/HISTORY.md b/HISTORY.md index 57132a6bc..9634cc6de 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -8,7 +8,7 @@ - Support custom data fields in archive ({pr}`421`) - **Backwards-incompatible:** Remove `_batch` from parameter names ({pr}`422`, - {pr}`424`, {pr}`425`, {pr}`426`) + {pr}`424`, {pr}`425`, {pr}`426`, {pr}`428`) - Add Gaussian, IsoLine Operators and Refactor GaussianEmitter/IsoLineEmitter ({pr}`418`) - **Backwards-incompatible:** Remove metadata in favor of custom fields diff --git a/ribs/_docstrings.py b/ribs/_docstrings.py deleted file mode 100644 index c968eb4db..000000000 --- a/ribs/_docstrings.py +++ /dev/null @@ -1,109 +0,0 @@ -"""This provides the common docstrings that are used throughout the project.""" - -import re - - -class DocstringComponents: - """Adapted from https://github.com/mwaskom/seaborn/blob/9d8ce6ad4ab213994f0b - c84d0c46869df7be0b49/seaborn/_docstrings.py.""" - regexp = re.compile(r"\n((\n|.)+)\n\s*", re.MULTILINE) - - def __init__(self, comp_dict, strip_whitespace=True): - """Read entries from a dict, optionally stripping outer whitespace.""" - if strip_whitespace: - entries = {} - for key, val in comp_dict.items(): - m = re.match(self.regexp, val) - if m is None: - entries[key] = val - else: - entries[key] = m.group(1) - else: - entries = comp_dict.copy() - - self.entries = entries - - def __getattr__(self, attr): - """Provide dot access to entries for clean raw docstrings.""" - if attr in self.entries: - return self.entries[attr] - try: - return self.__getattribute__(attr) - except AttributeError as err: - # If Python is run with -OO, it will strip docstrings and our lookup - # from self.entries will fail. We check for __debug__, which is - # actually set to False by -O (it is True for normal execution). - # But we only want to see an error when building the docs; not - # something users should see, so this slight inconsistency is fine. - if __debug__: - raise err - else: - pass - - @classmethod - def from_nested_components(cls, **kwargs): - """Add multiple sub-sets of components.""" - return cls(kwargs, strip_whitespace=False) - - # NOTE Unclear how this will be useful, commenting out for now - # @classmethod - # def from_function_params(cls, func): - # """Use the numpydoc parser to extract components from existing func.""" - # params = NumpyDocString(pydoc.getdoc(func))["Parameters"] - # comp_dict = {} - # for p in params: - # name = p.name - # type = p.type - # desc = "\n ".join(p.desc) - # comp_dict[name] = f"{name} : {type}\n {desc}" - - # return cls(comp_dict) - - -core_args = { - "emitter": - """ - emitter (ribs.emitters.EmitterBase): Emitter to use for generating - solutions and updating the archive. - """, - "archive": - """ - archive (ribs.archives.ArchiveBase): Archive to use when creating - and inserting solutions. For instance, this can be - :class:`ribs.archives.GridArchive`. - """, - "solution_batch": - """ - solution_batch (numpy.ndarray): Batch of solutions generated by the - emitter's :meth:`ask()` method. - """, - "objective_batch": - """ - objective_batch (numpy.ndarray): Batch of objective values. - """, - "measures_batch": - """ - measures_batch (numpy.ndarray): ``(n, )`` - array with the measure space coordinates of each solution. - """, - "status_batch": - """ - status_batch (numpy.ndarray): An array of integer statuses - returned by a series of calls to archive's :meth:`add_single()` - method or by a single call to archive's :meth:`add()`. - """, - "value_batch": - """ - value_batch (numpy.ndarray): 1D array of floats returned by a series of - calls to archive's :meth:`add_single()` method or by a single call to - archive's :meth:`add()`. For what these floats represent, - refer to :meth:`ribs.archives.add()`. - """, - "seed": - """ - seed (int): Value to seed the random number generator. Set to None to - avoid a fixed seed. - """ -} - -core_docs = {"args": DocstringComponents(core_args)} diff --git a/ribs/emitters/_evolution_strategy_emitter.py b/ribs/emitters/_evolution_strategy_emitter.py index 6a1afc299..8bac4d940 100644 --- a/ribs/emitters/_evolution_strategy_emitter.py +++ b/ribs/emitters/_evolution_strategy_emitter.py @@ -227,9 +227,8 @@ def tell(self, solution, objective, measures, status_batch, value_batch): new_sols = status_batch.astype(bool).sum() # Sort the solutions using ranker. - indices, ranking_values = self._ranker.rank( - self, self.archive, self._rng, data["solution"], data["objective"], - data["measures"], add_info["status"], add_info["value"]) + indices, ranking_values = self._ranker.rank(self, self.archive, + self._rng, data, add_info) # Select the number of parents. num_parents = (new_sols if self._selection_rule == "filter" else diff --git a/ribs/emitters/_gradient_arborescence_emitter.py b/ribs/emitters/_gradient_arborescence_emitter.py index 7568c1c83..7eb89a10c 100644 --- a/ribs/emitters/_gradient_arborescence_emitter.py +++ b/ribs/emitters/_gradient_arborescence_emitter.py @@ -402,9 +402,8 @@ def tell(self, solution, objective, measures, status_batch, value_batch): new_sols = status_batch.astype(bool).sum() # Sort the solutions using ranker. - indices, ranking_values = self._ranker.rank( - self, self.archive, self._rng, data["solution"], data["objective"], - data["measures"], add_info["status"], add_info["value"]) + indices, ranking_values = self._ranker.rank(self, self.archive, + self._rng, data, add_info) # Select the number of parents. num_parents = (new_sols if self._selection_rule == "filter" else diff --git a/ribs/emitters/rankers.py b/ribs/emitters/rankers.py index b510340ea..6c31e9af5 100644 --- a/ribs/emitters/rankers.py +++ b/ribs/emitters/rankers.py @@ -34,8 +34,6 @@ import numpy as np -from ribs._docstrings import DocstringComponents, core_args - __all__ = [ "ImprovementRanker", "TwoStageImprovementRanker", @@ -46,21 +44,17 @@ "RankerBase", ] -# Define common docstrings -_ARGS = DocstringComponents(core_args) - -_RANK_ARGS = f""" +_RANK_ARGS = """ Args: emitter (ribs.emitters.EmitterBase): Emitter that this ``ranker`` object belongs to. archive (ribs.archives.ArchiveBase): Archive used by ``emitter`` when creating and inserting solutions. rng (numpy.random.Generator): A random number generator. -{_ARGS.solution_batch} -{_ARGS.objective_batch} -{_ARGS.measures_batch} -{_ARGS.status_batch} -{_ARGS.value_batch} + data (dict): Dict mapping from field names like ``solution`` and + ``objective`` to arrays with data for the solutions. Rankers at least + assume the presence of the ``solution`` key. + add_info (dict): Information returned by an archive's add() method. Returns: tuple(numpy.ndarray, numpy.ndarray): the first array (shape @@ -93,14 +87,13 @@ class RankerBase(ABC): """ @abstractmethod - def rank(self, emitter, archive, rng, solution_batch, objective_batch, - measures_batch, status_batch, value_batch): + def rank(self, emitter, archive, rng, data, add_info): # pylint: disable=missing-function-docstring pass # Generates the docstring for rank rank.__doc__ = f""" -Generates a batch of indices that represents an ordering of ``solution_batch``. +Generates a batch of indices that represents an ordering of ``data["solution"]``. {_RANK_ARGS} """ @@ -130,11 +123,10 @@ class ImprovementRanker(RankerBase): corresponding cell in the archive. """ - def rank(self, emitter, archive, rng, solution_batch, objective_batch, - measures_batch, status_batch, value_batch): + def rank(self, emitter, archive, rng, data, add_info): # Note that lexsort sorts the values in ascending order, # so we use np.flip to reverse the sorted array. - return np.flip(np.argsort(value_batch)), value_batch + return np.flip(np.argsort(add_info["value"])), add_info["value"] rank.__doc__ = f""" Generates a list of indices that represents an ordering of solutions. @@ -156,11 +148,11 @@ class TwoStageImprovementRanker(RankerBase): :meth:`ribs.archives.ArchiveBase.add` """ - def rank(self, emitter, archive, rng, solution_batch, objective_batch, - measures_batch, status_batch, value_batch): + def rank(self, emitter, archive, rng, data, add_info): # To avoid using an array of tuples, ranking_values is a 2D array # [[status_0, value_0], ..., [status_n, value_n]] - ranking_values = np.stack((status_batch, value_batch), axis=-1) + ranking_values = np.stack((add_info["status"], add_info["value"]), + axis=-1) # New solutions sort ahead of improved ones, which sort ahead of ones # that were not added. @@ -211,11 +203,10 @@ def target_measure_dir(self): def target_measure_dir(self, value): self._target_measure_dir = value - def rank(self, emitter, archive, rng, solution_batch, objective_batch, - measures_batch, status_batch, value_batch): + def rank(self, emitter, archive, rng, data, add_info): if self._target_measure_dir is None: raise RuntimeError("target measure direction not set") - projections = np.dot(measures_batch, self._target_measure_dir) + projections = np.dot(data["measures"], self._target_measure_dir) # Sort only by projection; use np.flip to reverse the order return np.flip(np.argsort(projections)), projections @@ -267,15 +258,14 @@ def target_measure_dir(self): def target_measure_dir(self, value): self._target_measure_dir = value - def rank(self, emitter, archive, rng, solution_batch, objective_batch, - measures_batch, status_batch, value_batch): + def rank(self, emitter, archive, rng, data, add_info): if self._target_measure_dir is None: raise RuntimeError("target measure direction not set") - projections = np.dot(measures_batch, self._target_measure_dir) + projections = np.dot(data["measures"], self._target_measure_dir) # To avoid using an array of tuples, ranking_values is a 2D array # [[status_0, projection_0], ..., [status_n, projection_n]] - ranking_values = np.stack((status_batch, projections), axis=-1) + ranking_values = np.stack((add_info["status"], projections), axis=-1) # Sort by whether the solution was added into the archive, # followed by projection. @@ -308,10 +298,9 @@ class ObjectiveRanker(RankerBase): emitter. """ - def rank(self, emitter, archive, rng, solution_batch, objective_batch, - measures_batch, status_batch, value_batch): + def rank(self, emitter, archive, rng, data, add_info): # Sort only by objective value. - return np.flip(np.argsort(objective_batch)), objective_batch + return np.flip(np.argsort(data["objective"])), data["objective"] rank.__doc__ = f""" Ranks the solutions based on their objective values. @@ -328,11 +317,11 @@ class TwoStageObjectiveRanker(RankerBase): `_ as OptimizingEmitter. """ - def rank(self, emitter, archive, rng, solution_batch, objective_batch, - measures_batch, status_batch, value_batch): + def rank(self, emitter, archive, rng, data, add_info): # To avoid using an array of tuples, ranking_values is a 2D array # [[status_0, objective_0], ..., [status_0, objective_n]] - ranking_values = np.stack((status_batch, objective_batch), axis=-1) + ranking_values = np.stack((add_info["status"], data["objective"]), + axis=-1) # Sort by whether the solution was added into the archive, followed # by the objective values. diff --git a/tests/emitters/rankers_test.py b/tests/emitters/rankers_test.py index eada0d213..4bdb7561c 100644 --- a/tests/emitters/rankers_test.py +++ b/tests/emitters/rankers_test.py @@ -44,9 +44,20 @@ def test_two_stage_improvement_ranker(archive_fixture, emitter, rng): value_batch = np.concatenate(([first_value], value_batch)) ranker = TwoStageImprovementRanker() - indices, ranking_values = ranker.rank(emitter, archive, rng, solution_batch, - objective_batch, measures_batch, - status_batch, value_batch) + indices, ranking_values = ranker.rank( + emitter, + archive, + rng, + { + "solution": solution_batch, + "objective": objective_batch, + "measures": measures_batch, + }, + { + "status": status_batch, + "value": value_batch, + }, + ) assert (indices == [0, 3, 2, 1]).all() assert (ranking_values == [ @@ -75,9 +86,20 @@ def test_random_direction_ranker(emitter, rng): ranker = RandomDirectionRanker() ranker.target_measure_dir = [0, 1, 0] # Set the random direction. - indices, ranking_values = ranker.rank(emitter, archive, rng, solution_batch, - objective_batch, measures_batch, - status_batch, value_batch) + indices, ranking_values = ranker.rank( + emitter, + archive, + rng, + { + "solution": solution_batch, + "objective": objective_batch, + "measures": measures_batch, + }, + { + "status": status_batch, + "value": value_batch, + }, + ) assert (indices == [1, 0, 3, 2]).all() assert (ranking_values == np.dot(measures_batch, [0, 1, 0])).all() @@ -110,9 +132,20 @@ def test_two_stage_random_direction(emitter, rng): ranker = TwoStageRandomDirectionRanker() ranker.target_measure_dir = [0, 1, 0] # Set the random direction. - indices, ranking_values = ranker.rank(emitter, archive, rng, solution_batch, - objective_batch, measures_batch, - status_batch, value_batch) + indices, ranking_values = ranker.rank( + emitter, + archive, + rng, + { + "solution": solution_batch, + "objective": objective_batch, + "measures": measures_batch, + }, + { + "status": status_batch, + "value": value_batch, + }, + ) assert (indices == [0, 3, 2, 1]).all() projections = np.dot(measures_batch, [0, 1, 0]) @@ -133,9 +166,20 @@ def test_objective_ranker(archive_fixture, emitter, rng): measures_batch) ranker = ObjectiveRanker() - indices, ranking_values = ranker.rank(emitter, archive, rng, solution_batch, - objective_batch, measures_batch, - status_batch, value_batch) + indices, ranking_values = ranker.rank( + emitter, + archive, + rng, + { + "solution": solution_batch, + "objective": objective_batch, + "measures": measures_batch, + }, + { + "status": status_batch, + "value": value_batch, + }, + ) assert (indices == [1, 2, 3, 0]).all() assert ranking_values == objective_batch @@ -159,9 +203,20 @@ def test_two_stage_objective_ranker(archive_fixture, emitter, rng): value_batch = np.concatenate((value_batch_1, value_batch_2)) ranker = TwoStageObjectiveRanker() - indices, ranking_values = ranker.rank(emitter, archive, rng, solution_batch, - objective_batch, measures_batch, - status_batch, value_batch) + indices, ranking_values = ranker.rank( + emitter, + archive, + rng, + { + "solution": solution_batch, + "objective": objective_batch, + "measures": measures_batch, + }, + { + "status": status_batch, + "value": value_batch, + }, + ) assert (indices == [1, 0, 2, 3]).all() assert (ranking_values == [