diff --git a/digneapy/archives/_grid_archive.py b/digneapy/archives/_grid_archive.py index ee25fcb..32fbd6d 100644 --- a/digneapy/archives/_grid_archive.py +++ b/digneapy/archives/_grid_archive.py @@ -18,6 +18,7 @@ import numpy as np from digneapy.core import Instance +from digneapy.qd._desc_strategies import descriptor_strategies from ._base_archive import Archive @@ -37,6 +38,7 @@ def __init__( self, dimensions: Sequence[int], ranges: Sequence[Tuple[float, float]], + attribute: str = "features", instances: Optional[Iterable[Instance]] = None, eps: float = 1e-6, dtype=np.float64, @@ -55,6 +57,7 @@ def __init__( :math:`[-2,2]` (inclusive). ``ranges`` should be the same length as ``dims``. instances (Optional[Iterable[Instance]], optional): Instances to pre-initialise the archive. Defaults to None. + attribute: str = Attribute of the Instances to compute the diversity. eps (float, optional): Due to floating point precision errors, we add a small epsilon when computing the archive indices in the :meth:`index_of` method -- refer to the implementation `here. Defaults to 1e-6. @@ -73,6 +76,16 @@ def __init__( ) self._dimensions = np.asarray(dimensions) + self._inst_attrs = attribute + if attribute not in descriptor_strategies: + msg = f"describe_by {attribute} not available in {self.__class__.__name__}.__init__. Set to features by default" + print(msg) + self._inst_attrs = "features" + self._descriptor_strategy = descriptor_strategies["features"] + else: + self._inst_attrs = attribute + self._descriptor_strategy = descriptor_strategies[attribute] + ranges = list(zip(*ranges)) self._lower_bounds = np.array(ranges[0], dtype=dtype) self._upper_bounds = np.array(ranges[1], dtype=dtype) @@ -221,7 +234,7 @@ def append(self, instance: Instance): TypeError: ``instance`` is not a instance of the class Instance. """ if isinstance(instance, Instance): - index = self.index_of(np.asarray(instance.descriptor)) + index = self.index_of(self._descriptor_strategy([instance])) if index not in self._grid or instance > self._grid[index]: self._grid[index] = copy.deepcopy(instance) @@ -239,7 +252,7 @@ def extend(self, iterable: Iterable[Instance], *args, **kwargs): msg = "Only objects of type Instance can be inserted into a GridArchive" raise TypeError(msg) - indeces = self.index_of([inst.descriptor for inst in iterable]) + indeces = self.index_of(self._descriptor_strategy(iterable)) for idx, instance in zip(indeces, iterable, strict=True): if idx not in self._grid or instance.fitness > self._grid[idx].fitness: self._grid[idx] = copy.deepcopy(instance) diff --git a/digneapy/qd/__init__.py b/digneapy/qd/__init__.py index 01d9d63..8560e3d 100644 --- a/digneapy/qd/__init__.py +++ b/digneapy/qd/__init__.py @@ -14,7 +14,6 @@ from digneapy.qd._desc_strategies import ( DescStrategy, descriptor_strategies, - features_strategy, instance_strategy, performance_strategy, rdstrat, @@ -24,7 +23,6 @@ __all__ = [ "NS", "CMA_ME", - "features_strategy", "performance_strategy", "instance_strategy", "descriptor_strategies", diff --git a/digneapy/qd/_desc_strategies.py b/digneapy/qd/_desc_strategies.py index 5a0957b..19e644e 100644 --- a/digneapy/qd/_desc_strategies.py +++ b/digneapy/qd/_desc_strategies.py @@ -44,8 +44,8 @@ def decorate(func: DescStrategy): return decorate -def features_strategy(iterable: Iterable[Instance]) -> np.ndarray: - """It generates the feature descriptor of an instance +def __property_strategy(attr: str): + """Returns a np.ndarray with the information required of the instances Args: iterable (Iterable[Instance]): Instances to describe @@ -53,7 +53,18 @@ def features_strategy(iterable: Iterable[Instance]) -> np.ndarray: Returns: np.ndarray: Array of the feature descriptors of each instance """ - return np.asarray([i.features for i in iterable]) + try: + if attr not in ("features", "descriptor"): + raise AttributeError() + except AttributeError: + raise ValueError( + f"Object of class Instance does not have a property named {attr}" + ) + + def strategy(iterable: Iterable[Instance]) -> np.ndarray: + return np.asarray([getattr(i, attr) for i in iterable]) + + return strategy def performance_strategy(iterable: Iterable[Instance]) -> np.ndarray: @@ -85,9 +96,11 @@ def instance_strategy(iterable: Iterable[Instance]) -> np.ndarray: - features --> Creates a np.ndarray with all the features of the instances. - performance --> Creates a np.ndarray with the mean performance score of each solver over the instances. - instance --> Creates a np.ndarray with the whole instance as its self descriptor. + - descriptor --> Creates a np.ndarray with all the transformed descriptors of the instances. Only when using a Transformer. """ descriptor_strategies: MutableMapping[str, DescStrategy] = { - "features": features_strategy, + "features": __property_strategy(attr="features"), "performance": performance_strategy, "instance": instance_strategy, + "descriptor": __property_strategy(attr="descriptor"), } diff --git a/digneapy/qd/_novelty_search.py b/digneapy/qd/_novelty_search.py index a184824..77f1aad 100644 --- a/digneapy/qd/_novelty_search.py +++ b/digneapy/qd/_novelty_search.py @@ -19,8 +19,7 @@ from digneapy.archives import Archive from digneapy.core import Instance -from digneapy.qd._desc_strategies import (descriptor_strategies, - features_strategy) +from digneapy.qd._desc_strategies import descriptor_strategies from digneapy.transformers import SupportsTransform @@ -54,7 +53,7 @@ def __init__( msg = f"describe_by {descriptor} not available in {self.__class__.__name__}.__init__. Set to features by default" print(msg) self._describe_by = "features" - self._descriptor_strategy = features_strategy + self._descriptor_strategy = descriptor_strategies["features"] else: self._describe_by = descriptor self._descriptor_strategy = descriptor_strategies[descriptor] diff --git a/examples/evolve_nn_pytorch.py b/examples/evolve_nn_pytorch.py index 636930c..22f475f 100644 --- a/examples/evolve_nn_pytorch.py +++ b/examples/evolve_nn_pytorch.py @@ -10,14 +10,14 @@ @Desc : None """ +import copy from collections import deque from typing import Optional -import numpy as np import pandas as pd from digneapy import Direction -from digneapy.archives import Archive +from digneapy.archives import Archive, GridArchive from digneapy.domains.knapsack import KPDomain from digneapy.generators import EIG from digneapy.operators.replacement import first_improve_replacement @@ -50,38 +50,10 @@ class NSEval: def __init__(self, features_info, resolution: int = 20): self.resolution = resolution self.features_info = features_info - self.hypercube = [ - np.linspace(start, stop, self.resolution) for start, stop in features_info - ] self.kp_domain = KPDomain(dimension=50, capacity_approach="percentage") self.portfolio = deque([default_kp, map_kp, miw_kp, mpw_kp]) - def __save_instances(self, filename, generated_instances): - """Writes the generated instances into a CSV file - - Args: - filename (str): Filename - generated_instances (iterable): Iterable of instances - """ - features = [ - "target", - "capacity", - "max_p", - "max_w", - "min_p", - "min_w", - "avg_eff", - "mean", - "std", - ] - with open(filename, "w") as file: - file.write(",".join(features) + "\n") - for solver, descriptors in generated_instances.items(): - for desc in descriptors: - content = solver + "," + ",".join(str(f) for f in desc) + "\n" - file.write(content) - - def __call__(self, transformer: TorchNN, filename: Optional[str] = None): + def __call__(self, transformer: TorchNN): """This method runs the Novelty Search using a NN as a transformer for searching novelty. It generates KP instances for each of the solvers in the portfolio [Default, MaP, MiW, MPW] and calculates how many bins of the @@ -89,15 +61,20 @@ def __call__(self, transformer: TorchNN, filename: Optional[str] = None): Args: transformer (TorchNN): Transformer to reduce a 8D feature vector into a 2D vector. - filename (str, optional): Filename to store the instances. Defaults to None. Returns: int: Number of bins occupied. The maximum value if 8 x R. """ - gen_instances = {s.__name__: [] for s in self.portfolio} + gen_instances = { + s.__name__: GridArchive( + dimensions=(self.resolution,) * 8, + ranges=self.features_info, + attribute="features", + ) + for s in self.portfolio + } for i in range(len(self.portfolio)): self.portfolio.rotate(i) # This allow us to change the target on the fly - eig = EIG( pop_size=10, generations=1000, @@ -111,24 +88,22 @@ def __call__(self, transformer: TorchNN, filename: Optional[str] = None): replacement=first_improve_replacement, transformer=transformer, ) - archive, solution_set = eig() - descriptors = [list(i.descriptor) for i in solution_set] - gen_instances[self.portfolio[0].__name__].extend(descriptors) - if any(len(sequence) != 0 for sequence in gen_instances.values()): - self.__save_instances(filename, gen_instances) - - # Here we gather all the instances together to calculate the metric - coverage = {k: set() for k in range(8)} - for ( - solver, - descriptors, - ) in gen_instances.items(): # For each set of instances - for desc in descriptors: # For each descriptor in the set - for i, f in enumerate(desc): # Location of the ith feature - coverage[i].add(np.digitize(f, self.hypercube[i])) - - f = sum(len(s) for s in coverage.values()) - return f + _, solution_set = eig() + print(solution_set) + if len(solution_set) != 0: + gen_instances[self.portfolio[0].__name__].extend( + copy.deepcopy(solution_set) + ) + + for solver, hypercube in gen_instances.items(): + print(f"Solver {solver} -> {hypercube.coverage}") + + combined_coverage = [ + list(hypercube.filled_cells) for hypercube in gen_instances.values() + ] + + print(combined_coverage) + return 0 def main():