@File : init.py @Time : 2024/06/07 14:06:26 @Author : Alejandro Marrero @Version : 1.0 @Contact : amarrerd@ull.edu.es @License : (C)Copyright 2024, Alejandro Marrero @Desc : None

Domain

Bases: ABC, RNG

Domain is a class that defines the domain of the problem. The domain is defined by its dimension and the bounds of each variable.

Parameters:
  • RNG

    Subclass that implements the RNG protocol

Source code in digneapy/_core/_domain.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
class Domain(ABC, RNG):
    """Domain is a class that defines the domain of the problem.
    The domain is defined by its dimension and the bounds of each variable.

    Args:
        RNG: Subclass that implements the RNG protocol
    """

    def __init__(
        self,
        dimension: int,
        bounds: Sequence[tuple],
        dtype=np.float64,
        seed: int = 42,
        name: str = "Domain",
        feat_names: Optional[Sequence[str]] = None,
        *args,
        **kwargs,
    ):
        self.name = name
        self.__name__ = name
        self._dimension = dimension
        self._bounds = bounds
        self._dtype = dtype
        self.feat_names = feat_names if feat_names else list()
        self.initialize_rng(seed=seed)
        if len(self._bounds) != 0:
            ranges = list(zip(*bounds))
            self._lbs = np.array(ranges[0], dtype=dtype)
            self._ubs = np.array(ranges[1], dtype=dtype)

    @abstractmethod
    def generate_instance(self) -> Instance:
        """Generates a new instances for the domain

        Returns:
            Instance: New randomly generated instance
        """
        msg = "generate_instances is not implemented in Domain class."
        raise NotImplementedError(msg)

    @abstractmethod
    def extract_features(self, instances: Instance) -> tuple:
        """Extract the features of the instance based on the domain

        Args:
            instance (Instance): Instance to extract the features from

        Returns:
            Tuple: Values of each feature
        """
        msg = "extract_features is not implemented in Domain class."
        raise NotImplementedError(msg)

    @abstractmethod
    def extract_features_as_dict(self, instance: Instance) -> Mapping[str, float]:
        """Creates a dictionary with the features of the instance.
        The key are the names of each feature and the values are
        the values extracted from instance.

        Args:
            instance (Instance): Instance to extract the features from

        Returns:
            Mapping[str, float]: Dictionary with the names/values of each feature
        """
        msg = "extract_features_as_dict is not implemented in Domain class."
        raise NotImplementedError(msg)

    @abstractmethod
    def from_instance(self, instance: Instance) -> Problem:
        msg = "from_instance is not implemented in Domain class."
        raise NotImplementedError(msg)

    @property
    def bounds(self):
        return self._bounds

    def get_bounds_at(self, i: int) -> tuple:
        if i < 0 or i > len(self._bounds):
            raise ValueError(
                f"Index {i} out-of-range. The bounds are 0-{len(self._bounds)} "
            )
        return (self._lbs[i], self._ubs[i])

    @property
    def dimension(self):
        return self._dimension

    def __len__(self):
        return self._dimension

extract_features(instances) abstractmethod

Extract the features of the instance based on the domain

Parameters:
  • instance (Instance) –

    Instance to extract the features from

Returns:
  • Tuple( tuple ) –

    Values of each feature

Source code in digneapy/_core/_domain.py
66
67
68
69
70
71
72
73
74
75
76
77
@abstractmethod
def extract_features(self, instances: Instance) -> tuple:
    """Extract the features of the instance based on the domain

    Args:
        instance (Instance): Instance to extract the features from

    Returns:
        Tuple: Values of each feature
    """
    msg = "extract_features is not implemented in Domain class."
    raise NotImplementedError(msg)

extract_features_as_dict(instance) abstractmethod

Creates a dictionary with the features of the instance. The key are the names of each feature and the values are the values extracted from instance.

Parameters:
  • instance (Instance) –

    Instance to extract the features from

Returns:
  • Mapping[str, float]

    Mapping[str, float]: Dictionary with the names/values of each feature

Source code in digneapy/_core/_domain.py
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@abstractmethod
def extract_features_as_dict(self, instance: Instance) -> Mapping[str, float]:
    """Creates a dictionary with the features of the instance.
    The key are the names of each feature and the values are
    the values extracted from instance.

    Args:
        instance (Instance): Instance to extract the features from

    Returns:
        Mapping[str, float]: Dictionary with the names/values of each feature
    """
    msg = "extract_features_as_dict is not implemented in Domain class."
    raise NotImplementedError(msg)

generate_instance() abstractmethod

Generates a new instances for the domain

Returns:
  • Instance( Instance ) –

    New randomly generated instance

Source code in digneapy/_core/_domain.py
56
57
58
59
60
61
62
63
64
@abstractmethod
def generate_instance(self) -> Instance:
    """Generates a new instances for the domain

    Returns:
        Instance: New randomly generated instance
    """
    msg = "generate_instances is not implemented in Domain class."
    raise NotImplementedError(msg)

DominatedNS

Bases: NS

Dominated Novelty Search (DNS) Bahlous-Boldi, R., Faldor, M., Grillotti, L., Janmohamed, H., Coiffard, L., Spector, L., & Cully, A. (2025). Dominated Novelty Search: Rethinking Local Competition in Quality-Diversity. 1. https://arxiv.org/abs/2502.00593v1 Quality-Diversity algorithm that implements local competition through dynamic fitness transformations, eliminating the need for predefined bounds or parameters. The competition fitness, also known as the dominated novelty score, is calculated as the average distance to the k nearest neighbors with higher fitness. The value is set in the ``p'' attribute of the Instance class.

Source code in digneapy/_core/_novelty_search.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
class DominatedNS(NS):
    """
    Dominated Novelty Search (DNS)
    Bahlous-Boldi, R., Faldor, M., Grillotti, L., Janmohamed, H., Coiffard, L., Spector, L., & Cully, A. (2025).
    Dominated Novelty Search: Rethinking Local Competition in Quality-Diversity. 1.
    https://arxiv.org/abs/2502.00593v1
    Quality-Diversity algorithm that implements local competition through dynamic fitness transformations,
    eliminating the need for predefined bounds or parameters. The competition fitness, also known as the dominated novelty score,
    is calculated as the average distance to the k nearest neighbors with higher fitness.
    The value is set in the ``p'' attribute of the Instance class.
    """

    def __init__(self, k: Optional[int] = 15):
        super().__init__(k=k)
        self._archive = None

    def __call__(
        self, instances: Sequence[Instance]
    ) -> Tuple[list[Instance], list[float]]:
        """

        The method returns a descending sorted list of instances by their competition fitness value (p).
        For each instance ``i'' in the sequence, we calculate all the other instances that dominate it.
        Then, we compute the distances between their descriptors using the norm of the difference for each dimension of the descriptors.
        Novel instances will get a competition fitness of np.inf (assuring they will survive).
        Less novel instances will be selected by their competition fitness value. This competition mechanism creates two complementary evolutionary
        pressures: individuals must either improve their fitness or discover distinct behaviors that differ from better-performing
        solutions. Solutions that have no fitter neighbors (D𝑖 = ∅) receive an infinite competition fitness, ensuring their preservation in the
        population.

        Args:
            instances (Sequence[Instance]): Instances to calculate their competition

        Raises:
            ValueError: If len(d) where d is the descriptor of each instance i differs from another
            ValueError: If DNS.k >= len(instances)

        Returns:
            List[Instance]: Numpy array with the instances sorted by their competition fitness value (p). Descending order.
        """
        num_instances = len(instances)
        if num_instances <= self._k:
            msg = f"{self.__class__.__name__} trying to calculate sparseness with k({self._k}) > len(instances)({num_instances})"
            raise ValueError(msg)

        perf_values = np.array([instance.p for instance in instances])
        descriptors = np.array([instance.descriptor for instance in instances])
        mask = perf_values[:, None] > perf_values
        dominated_indices = [np.nonzero(row) for row in mask]
        fitness_values = np.full(num_instances, np.finfo(np.float32).max)
        for i in range(num_instances):
            if dominated_indices[i][0].size > 0:
                dist = np.linalg.norm(
                    descriptors[i] - descriptors[dominated_indices[i]], axis=1
                )
                if len(dist) > self._k:
                    dist = np.partition(dist, self._k)[: self._k]
                fitness_values[i] = np.sum(dist) / self._k
            instances[i].fitness = fitness_values[i]

        instances.sort(key=attrgetter("fitness"), reverse=True)
        return (instances, fitness_values)

__call__(instances)

The method returns a descending sorted list of instances by their competition fitness value (p). For each instance ``i'' in the sequence, we calculate all the other instances that dominate it. Then, we compute the distances between their descriptors using the norm of the difference for each dimension of the descriptors. Novel instances will get a competition fitness of np.inf (assuring they will survive). Less novel instances will be selected by their competition fitness value. This competition mechanism creates two complementary evolutionary pressures: individuals must either improve their fitness or discover distinct behaviors that differ from better-performing solutions. Solutions that have no fitter neighbors (D𝑖 = ∅) receive an infinite competition fitness, ensuring their preservation in the population.

Parameters:
  • instances (Sequence[Instance]) –

    Instances to calculate their competition

Raises:
  • ValueError

    If len(d) where d is the descriptor of each instance i differs from another

  • ValueError

    If DNS.k >= len(instances)

Returns:
  • Tuple[list[Instance], list[float]]

    List[Instance]: Numpy array with the instances sorted by their competition fitness value (p). Descending order.

Source code in digneapy/_core/_novelty_search.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def __call__(
    self, instances: Sequence[Instance]
) -> Tuple[list[Instance], list[float]]:
    """

    The method returns a descending sorted list of instances by their competition fitness value (p).
    For each instance ``i'' in the sequence, we calculate all the other instances that dominate it.
    Then, we compute the distances between their descriptors using the norm of the difference for each dimension of the descriptors.
    Novel instances will get a competition fitness of np.inf (assuring they will survive).
    Less novel instances will be selected by their competition fitness value. This competition mechanism creates two complementary evolutionary
    pressures: individuals must either improve their fitness or discover distinct behaviors that differ from better-performing
    solutions. Solutions that have no fitter neighbors (D𝑖 = ∅) receive an infinite competition fitness, ensuring their preservation in the
    population.

    Args:
        instances (Sequence[Instance]): Instances to calculate their competition

    Raises:
        ValueError: If len(d) where d is the descriptor of each instance i differs from another
        ValueError: If DNS.k >= len(instances)

    Returns:
        List[Instance]: Numpy array with the instances sorted by their competition fitness value (p). Descending order.
    """
    num_instances = len(instances)
    if num_instances <= self._k:
        msg = f"{self.__class__.__name__} trying to calculate sparseness with k({self._k}) > len(instances)({num_instances})"
        raise ValueError(msg)

    perf_values = np.array([instance.p for instance in instances])
    descriptors = np.array([instance.descriptor for instance in instances])
    mask = perf_values[:, None] > perf_values
    dominated_indices = [np.nonzero(row) for row in mask]
    fitness_values = np.full(num_instances, np.finfo(np.float32).max)
    for i in range(num_instances):
        if dominated_indices[i][0].size > 0:
            dist = np.linalg.norm(
                descriptors[i] - descriptors[dominated_indices[i]], axis=1
            )
            if len(dist) > self._k:
                dist = np.partition(dist, self._k)[: self._k]
            fitness_values[i] = np.sum(dist) / self._k
        instances[i].fitness = fitness_values[i]

    instances.sort(key=attrgetter("fitness"), reverse=True)
    return (instances, fitness_values)

Instance

Source code in digneapy/_core/_instance.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
class Instance:
    __slots__ = ("_vars", "_fit", "_p", "_s", "_features", "_desc", "_pscores")

    def __init__(
        self,
        variables: Optional[npt.ArrayLike] = None,
        fitness: float = 0.0,
        p: float = 0.0,
        s: float = 0.0,
        features: Optional[tuple[float]] = None,
        descriptor: Optional[tuple[float]] = None,
        portfolio_scores: Optional[tuple[float]] = None,
    ):
        """Creates an instance of a Instance (unstructured) for QD algorithms
        This class is used to represent a solution in a QD algorithm. It contains the
        variables, fitness, performance, novelty, features, descriptor and portfolio scores
        of the solution.
        The variables are stored as a numpy array, and the fitness, performance and novelty
        are stored as floats. The features, descriptor and portfolio scores are stored as
        numpy arrays.

        Args:
            variables (Optional[npt.ArrayLike], optional): Variables or genome of the instance. Defaults to None.
            fitness (float, optional): Fitness of the instance. Defaults to 0.0.
            p (float, optional): Performance score. Defaults to 0.0.
            s (float, optional): Novelty score. Defaults to 0.0.
            features (Optional[tuple[float]], optional): Tuple of features extracted from the domain. Defaults to None.
            descriptor (Optional[tuple[float]], optional): Tuple with the descriptor information of the instance. Defaults to None.
            portfolio_scores (Optional[tuple[float]], optional): Scores of the solvers in the portfolio. Defaults to None.

        Raises:
            ValueError: If fitness, p or s are not convertible to float.
        """
        try:
            fitness = float(fitness)
            p = float(p)
            s = float(s)
        except ValueError:
            raise ValueError(
                "The fitness, p and s parameters must be convertible to float"
            )

        self._vars = np.array(variables) if variables is not None else np.empty(0)
        self._fit = fitness
        self._p = p
        self._s = s
        self._features = np.array(features) if features is not None else np.empty(0)
        self._pscores = (
            np.array(portfolio_scores) if portfolio_scores is not None else np.empty(0)
        )
        self._desc = np.array(descriptor) if descriptor is not None else np.empty(0)

    def clone(self) -> Self:
        """Create a clone of the current instance. More efficient than using copy.deepcopy.

        Returns:
            Self: Instance object
        """
        return Instance(
            variables=list(self._vars),
            fitness=self._fit,
            p=self._p,
            s=self._s,
            features=tuple(self._features),
            portfolio_scores=tuple(self._pscores),
            descriptor=tuple(self._desc),
        )

    @property
    def variables(self):
        return self._vars

    @property
    def p(self) -> float:
        return self._p

    @p.setter
    def p(self, performance: float):
        try:
            performance = float(performance)
        except ValueError:
            # if performance != 0.0 and not float(performance):
            msg = f"The performance value {performance} is not a float in 'p' setter of class {self.__class__.__name__}"
            raise ValueError(msg)
        self._p = performance

    @property
    def s(self) -> float:
        return self._s

    @s.setter
    def s(self, novelty: float):
        try:
            novelty = float(novelty)
        except ValueError:
            # if novelty != 0.0 and not float(novelty):
            msg = f"The novelty value {novelty} is not a float in 's' setter of class {self.__class__.__name__}"
            raise ValueError(msg)
        self._s = novelty

    @property
    def fitness(self) -> float:
        return self._fit

    @fitness.setter
    def fitness(self, f: float):
        try:
            f = float(f)
        except ValueError:
            # if f != 0.0 and not float(f):
            msg = f"The fitness value {f} is not a float in fitness setter of class {self.__class__.__name__}"
            raise ValueError(msg)

        self._fit = f

    @property
    def features(self) -> np.ndarray:
        return self._features

    @features.setter
    def features(self, features: npt.ArrayLike):
        self._features = np.asarray(features)

    @property
    def descriptor(self) -> np.ndarray:
        return self._desc

    @descriptor.setter
    def descriptor(self, desc: npt.ArrayLike):
        self._desc = np.array(desc)

    @property
    def portfolio_scores(self):
        return self._pscores

    @portfolio_scores.setter
    def portfolio_scores(self, p: npt.ArrayLike):
        self._pscores = np.asarray(p)

    def __repr__(self):
        return f"Instance<f={self.fitness},p={self.p},s={self.s},vars={len(self._vars)},features={len(self.features)},descriptor={len(self.descriptor)},performance={len(self.portfolio_scores)}>"

    def __str__(self):
        import reprlib

        descriptor = reprlib.repr(self.descriptor)
        performance = reprlib.repr(self.portfolio_scores)
        performance = performance[performance.find("(") : performance.rfind(")") + 1]
        return f"Instance(f={self.fitness},p={self.p},s={self.s},features={len(self.features)},descriptor={descriptor},performance={performance})"

    def __iter__(self):
        return iter(self._vars)

    def __len__(self):
        return len(self._vars)

    def __getitem__(self, key):
        if isinstance(key, slice):
            cls = type(self)  # To facilitate subclassing
            return cls(self._vars[key])
        index = operator.index(key)
        return self._vars[index]

    def __setitem__(self, key, value):
        self._vars[key] = value

    def __eq__(self, other):
        if not isinstance(other, Instance):
            print(
                f"Other of type {other.__class__.__name__} can not be compared with with {self.__class__.__name__}"
            )
            return NotImplemented

        else:
            try:
                return all(a == b for a, b in zip(self, other, strict=True))
            except ValueError:
                return False

    def __gt__(self, other):
        if not isinstance(other, Instance):
            print(
                f"Other of type {other.__class__.__name__} can not be compared with with {self.__class__.__name__}"
            )
            return NotImplemented

        return self.fitness > other.fitness

    def __ge__(self, other):
        if not isinstance(other, Instance):
            print(
                f"Other of type {other.__class__.__name__} can not be compared with with {self.__class__.__name__}"
            )
            return NotImplemented

        return self.fitness >= other.fitness

    def __hash__(self):
        from functools import reduce

        hashes = (hash(x) for x in self)
        return reduce(operator.or_, hashes, 0)

    def __bool__(self):
        return self._vars.size != 0

    def __format__(self, fmt_spec=""):
        if fmt_spec.endswith("p"):
            # We are showing only the performances
            fmt_spec = fmt_spec[:-1]
            components = self.portfolio_scores
        else:
            fmt_spec = fmt_spec[:-1]
            components = self.descriptor

        components = (format(c, fmt_spec) for c in components)
        decriptor = "descriptor=({})".format(",".join(components))
        msg = f"Instance(f={self.fitness},p={format(self.p, fmt_spec)}, s={format(self.s, fmt_spec)}, {decriptor})"

        return msg

    def asdict(
        self,
        variables_names: Optional[Sequence[str]] = None,
        features_names: Optional[Sequence[str]] = None,
        score_names: Optional[Sequence[str]] = None,
    ) -> dict:
        """Convert the instance to a dictionary. The keys are the names of the attributes
        and the values are the values of the attributes.

        Args:
            variables_names (Optional[Sequence[str]], optional): Names of the variables in the dictionary, otherwise v_i. Defaults to None.
            features_names (Optional[Sequence[str]], optional): Name of the features in the dictionary, otherwise f_i. Defaults to None.
            score_names (Optional[Sequence[str]], optional): Name of the solvers, otherwise solver_i. Defaults to None.

        Returns:
            dict: Dictionary with the attributes of the instance as keys and the values of the attributes as values.
        """
        sckeys = (
            [f"solver_{i}" for i in range(len(self._pscores))]
            if score_names is None
            else score_names
        )
        _data = {
            "fitness": self._fit,
            "s": self._s,
            "p": self._p,
            "portfolio_scores": {sk: v for sk, v in zip(sckeys, self._pscores)},
        }

        if len(self._desc) not in (
            len(self._vars),
            len(self._features),
            len(self._pscores),
        ):  # Transformed descriptor
            _data["descriptor"] = {f"d{i}": v for i, v in enumerate(self._desc)}
        if len(self.features) != 0:
            f_keys = (
                [f"f{i}" for i in range(len(self._features))]
                if features_names is None or len(features_names) == 0
                else features_names
            )
            _data["features"] = {fk: v for fk, v in zip(f_keys, self._features)}

        if variables_names:
            if len(variables_names) != len(self._vars):
                print(
                    f"Error in asdict(). len(variables_names) = {len(variables_names)} != len(variables) ({len(self._vars)}). Fallback to v#"
                )
                _data["variables"] = {f"v{i}": v for i, v in enumerate(self._vars)}
            else:
                _data["variables"] = {
                    vk: v for vk, v in zip(variables_names, self._vars)
                }

        else:
            _data["variables"] = {f"v{i}": v for i, v in enumerate(self._vars)}

        return _data

    def to_json(self) -> str:
        """Convert the instance to a JSON string. The keys are the names of the attributes
        and the values are the values of the attributes.

        Returns:
            str: JSON string with the attributes of the instance as keys and the values of the attributes as values.
        """
        import json

        return json.dumps(self.asdict(), sort_keys=True, indent=4)

    def to_series(
        self,
        variables_names: Optional[Sequence[str]] = None,
        features_names: Optional[Sequence[str]] = None,
        score_names: Optional[Sequence[str]] = None,
    ) -> pd.Series:
        """Creates a pandas Series from the instance.

        Args:
            variables_names (Optional[Sequence[str]], optional): Names of the variables in the dictionary, otherwise v_i. Defaults to None.
            features_names (Optional[Sequence[str]], optional): Name of the features in the dictionary, otherwise f_i. Defaults to None.
            score_names (Optional[Sequence[str]], optional): Name of the solvers, otherwise solver_i. Defaults to None.

        Returns:
            pd.Series: Pandas Series with the attributes of the instance as keys and the values of the attributes as values.
        """
        _flatten_data = {}
        for key, value in self.asdict(
            variables_names=variables_names,
            features_names=features_names,
            score_names=score_names,
        ).items():
            if isinstance(value, dict):  # Flatten nested dicts
                for sub_key, sub_value in value.items():
                    _flatten_data[f"{sub_key}"] = sub_value
            else:
                _flatten_data[key] = value
        return pd.Series(_flatten_data)

__init__(variables=None, fitness=0.0, p=0.0, s=0.0, features=None, descriptor=None, portfolio_scores=None)

Creates an instance of a Instance (unstructured) for QD algorithms This class is used to represent a solution in a QD algorithm. It contains the variables, fitness, performance, novelty, features, descriptor and portfolio scores of the solution. The variables are stored as a numpy array, and the fitness, performance and novelty are stored as floats. The features, descriptor and portfolio scores are stored as numpy arrays.

Parameters:
  • variables (Optional[ArrayLike], default: None ) –

    Variables or genome of the instance. Defaults to None.

  • fitness (float, default: 0.0 ) –

    Fitness of the instance. Defaults to 0.0.

  • p (float, default: 0.0 ) –

    Performance score. Defaults to 0.0.

  • s (float, default: 0.0 ) –

    Novelty score. Defaults to 0.0.

  • features (Optional[tuple[float]], default: None ) –

    Tuple of features extracted from the domain. Defaults to None.

  • descriptor (Optional[tuple[float]], default: None ) –

    Tuple with the descriptor information of the instance. Defaults to None.

  • portfolio_scores (Optional[tuple[float]], default: None ) –

    Scores of the solvers in the portfolio. Defaults to None.

Raises:
  • ValueError

    If fitness, p or s are not convertible to float.

Source code in digneapy/_core/_instance.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def __init__(
    self,
    variables: Optional[npt.ArrayLike] = None,
    fitness: float = 0.0,
    p: float = 0.0,
    s: float = 0.0,
    features: Optional[tuple[float]] = None,
    descriptor: Optional[tuple[float]] = None,
    portfolio_scores: Optional[tuple[float]] = None,
):
    """Creates an instance of a Instance (unstructured) for QD algorithms
    This class is used to represent a solution in a QD algorithm. It contains the
    variables, fitness, performance, novelty, features, descriptor and portfolio scores
    of the solution.
    The variables are stored as a numpy array, and the fitness, performance and novelty
    are stored as floats. The features, descriptor and portfolio scores are stored as
    numpy arrays.

    Args:
        variables (Optional[npt.ArrayLike], optional): Variables or genome of the instance. Defaults to None.
        fitness (float, optional): Fitness of the instance. Defaults to 0.0.
        p (float, optional): Performance score. Defaults to 0.0.
        s (float, optional): Novelty score. Defaults to 0.0.
        features (Optional[tuple[float]], optional): Tuple of features extracted from the domain. Defaults to None.
        descriptor (Optional[tuple[float]], optional): Tuple with the descriptor information of the instance. Defaults to None.
        portfolio_scores (Optional[tuple[float]], optional): Scores of the solvers in the portfolio. Defaults to None.

    Raises:
        ValueError: If fitness, p or s are not convertible to float.
    """
    try:
        fitness = float(fitness)
        p = float(p)
        s = float(s)
    except ValueError:
        raise ValueError(
            "The fitness, p and s parameters must be convertible to float"
        )

    self._vars = np.array(variables) if variables is not None else np.empty(0)
    self._fit = fitness
    self._p = p
    self._s = s
    self._features = np.array(features) if features is not None else np.empty(0)
    self._pscores = (
        np.array(portfolio_scores) if portfolio_scores is not None else np.empty(0)
    )
    self._desc = np.array(descriptor) if descriptor is not None else np.empty(0)

asdict(variables_names=None, features_names=None, score_names=None)

Convert the instance to a dictionary. The keys are the names of the attributes and the values are the values of the attributes.

Parameters:
  • variables_names (Optional[Sequence[str]], default: None ) –

    Names of the variables in the dictionary, otherwise v_i. Defaults to None.

  • features_names (Optional[Sequence[str]], default: None ) –

    Name of the features in the dictionary, otherwise f_i. Defaults to None.

  • score_names (Optional[Sequence[str]], default: None ) –

    Name of the solvers, otherwise solver_i. Defaults to None.

Returns:
  • dict( dict ) –

    Dictionary with the attributes of the instance as keys and the values of the attributes as values.

Source code in digneapy/_core/_instance.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
def asdict(
    self,
    variables_names: Optional[Sequence[str]] = None,
    features_names: Optional[Sequence[str]] = None,
    score_names: Optional[Sequence[str]] = None,
) -> dict:
    """Convert the instance to a dictionary. The keys are the names of the attributes
    and the values are the values of the attributes.

    Args:
        variables_names (Optional[Sequence[str]], optional): Names of the variables in the dictionary, otherwise v_i. Defaults to None.
        features_names (Optional[Sequence[str]], optional): Name of the features in the dictionary, otherwise f_i. Defaults to None.
        score_names (Optional[Sequence[str]], optional): Name of the solvers, otherwise solver_i. Defaults to None.

    Returns:
        dict: Dictionary with the attributes of the instance as keys and the values of the attributes as values.
    """
    sckeys = (
        [f"solver_{i}" for i in range(len(self._pscores))]
        if score_names is None
        else score_names
    )
    _data = {
        "fitness": self._fit,
        "s": self._s,
        "p": self._p,
        "portfolio_scores": {sk: v for sk, v in zip(sckeys, self._pscores)},
    }

    if len(self._desc) not in (
        len(self._vars),
        len(self._features),
        len(self._pscores),
    ):  # Transformed descriptor
        _data["descriptor"] = {f"d{i}": v for i, v in enumerate(self._desc)}
    if len(self.features) != 0:
        f_keys = (
            [f"f{i}" for i in range(len(self._features))]
            if features_names is None or len(features_names) == 0
            else features_names
        )
        _data["features"] = {fk: v for fk, v in zip(f_keys, self._features)}

    if variables_names:
        if len(variables_names) != len(self._vars):
            print(
                f"Error in asdict(). len(variables_names) = {len(variables_names)} != len(variables) ({len(self._vars)}). Fallback to v#"
            )
            _data["variables"] = {f"v{i}": v for i, v in enumerate(self._vars)}
        else:
            _data["variables"] = {
                vk: v for vk, v in zip(variables_names, self._vars)
            }

    else:
        _data["variables"] = {f"v{i}": v for i, v in enumerate(self._vars)}

    return _data

clone()

Create a clone of the current instance. More efficient than using copy.deepcopy.

Returns:
  • Self( Self ) –

    Instance object

Source code in digneapy/_core/_instance.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def clone(self) -> Self:
    """Create a clone of the current instance. More efficient than using copy.deepcopy.

    Returns:
        Self: Instance object
    """
    return Instance(
        variables=list(self._vars),
        fitness=self._fit,
        p=self._p,
        s=self._s,
        features=tuple(self._features),
        portfolio_scores=tuple(self._pscores),
        descriptor=tuple(self._desc),
    )

to_json()

Convert the instance to a JSON string. The keys are the names of the attributes and the values are the values of the attributes.

Returns:
  • str( str ) –

    JSON string with the attributes of the instance as keys and the values of the attributes as values.

Source code in digneapy/_core/_instance.py
301
302
303
304
305
306
307
308
309
310
def to_json(self) -> str:
    """Convert the instance to a JSON string. The keys are the names of the attributes
    and the values are the values of the attributes.

    Returns:
        str: JSON string with the attributes of the instance as keys and the values of the attributes as values.
    """
    import json

    return json.dumps(self.asdict(), sort_keys=True, indent=4)

to_series(variables_names=None, features_names=None, score_names=None)

Creates a pandas Series from the instance.

Parameters:
  • variables_names (Optional[Sequence[str]], default: None ) –

    Names of the variables in the dictionary, otherwise v_i. Defaults to None.

  • features_names (Optional[Sequence[str]], default: None ) –

    Name of the features in the dictionary, otherwise f_i. Defaults to None.

  • score_names (Optional[Sequence[str]], default: None ) –

    Name of the solvers, otherwise solver_i. Defaults to None.

Returns:
  • Series

    pd.Series: Pandas Series with the attributes of the instance as keys and the values of the attributes as values.

Source code in digneapy/_core/_instance.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
def to_series(
    self,
    variables_names: Optional[Sequence[str]] = None,
    features_names: Optional[Sequence[str]] = None,
    score_names: Optional[Sequence[str]] = None,
) -> pd.Series:
    """Creates a pandas Series from the instance.

    Args:
        variables_names (Optional[Sequence[str]], optional): Names of the variables in the dictionary, otherwise v_i. Defaults to None.
        features_names (Optional[Sequence[str]], optional): Name of the features in the dictionary, otherwise f_i. Defaults to None.
        score_names (Optional[Sequence[str]], optional): Name of the solvers, otherwise solver_i. Defaults to None.

    Returns:
        pd.Series: Pandas Series with the attributes of the instance as keys and the values of the attributes as values.
    """
    _flatten_data = {}
    for key, value in self.asdict(
        variables_names=variables_names,
        features_names=features_names,
        score_names=score_names,
    ).items():
        if isinstance(value, dict):  # Flatten nested dicts
            for sub_key, sub_value in value.items():
                _flatten_data[f"{sub_key}"] = sub_value
        else:
            _flatten_data[key] = value
    return pd.Series(_flatten_data)

NS

Descriptor strategies for the Novelty Search algorithm. The current version supports Features, Performance and Instance variations.

Source code in digneapy/_core/_novelty_search.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
class NS:
    """Descriptor strategies for the Novelty Search algorithm.
    The current version supports Features, Performance and Instance variations.
    """

    _EXPECTED_METRICS = "euclidean"

    def __init__(
        self,
        archive: Optional[Archive] = None,
        k: Optional[int] = 15,
        dist_metric: Optional[str] = "euclidean",
    ):
        """Creates an instance of the NoveltySearch Algorithm
        Args:
            archive (Archive, optional): Archive to store the instances to guide the evolution. Defaults to Archive(threshold=0.001).
            k (int, optional): Number of neighbours to calculate the sparseness. Defaults to 15.
            dist_metric (str, optional): Defines the distance metric used by NearestNeighbor in the archives. Defaults to euclidean.
        """
        if k < 0:
            raise ValueError(
                f"{__name__} k must be a positive integer and less than the number of instances."
            )
        self._k = k
        self._archive = archive if archive is not None else Archive(threshold=0.001)
        self._dist_metric = (
            dist_metric if dist_metric in NS._EXPECTED_METRICS else "euclidean"
        )

    @property
    def archive(self):
        return self._archive

    @property
    def k(self):
        return self._k

    def __str__(self):
        return f"NS(k={self._k},A={self._archive})"

    def __repr__(self) -> str:
        return f"NS<k={self._k},A={self._archive}>"

    def __call__(
        self, instances: Sequence[Instance]
    ) -> Tuple[list[Instance], list[float]]:
        """Calculates the sparseness of the given instances against the individuals
        in the Archive.

        Args:
            instances (Sequence[Instance]): Instances to calculate their sparseness
            verbose (bool, optional): Flag to show the progress. Defaults to False.

        Raises:
            ValueError: If len(d) where d is the descriptor of each instance i differs from another
            ValueError: If NoveltySearch.k >= len(instances)

        Returns:
            Tuple(list[Instance], list[float]): Tuple with the instances and the list of sparseness values, one for each instance
        """
        num_instances = len(instances)
        if num_instances <= self._k:
            msg = f"{self.__class__.__name__} trying to calculate sparseness with k({self._k}) > len(instances)({num_instances})"
            raise ValueError(msg)

        results = sparseness(instances, self._archive, k=self._k)
        return instances, results

__call__(instances)

Calculates the sparseness of the given instances against the individuals in the Archive.

Parameters:
  • instances (Sequence[Instance]) –

    Instances to calculate their sparseness

  • verbose (bool) –

    Flag to show the progress. Defaults to False.

Raises:
  • ValueError

    If len(d) where d is the descriptor of each instance i differs from another

  • ValueError

    If NoveltySearch.k >= len(instances)

Returns:
  • Tuple( (list[Instance], list[float]) ) –

    Tuple with the instances and the list of sparseness values, one for each instance

Source code in digneapy/_core/_novelty_search.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def __call__(
    self, instances: Sequence[Instance]
) -> Tuple[list[Instance], list[float]]:
    """Calculates the sparseness of the given instances against the individuals
    in the Archive.

    Args:
        instances (Sequence[Instance]): Instances to calculate their sparseness
        verbose (bool, optional): Flag to show the progress. Defaults to False.

    Raises:
        ValueError: If len(d) where d is the descriptor of each instance i differs from another
        ValueError: If NoveltySearch.k >= len(instances)

    Returns:
        Tuple(list[Instance], list[float]): Tuple with the instances and the list of sparseness values, one for each instance
    """
    num_instances = len(instances)
    if num_instances <= self._k:
        msg = f"{self.__class__.__name__} trying to calculate sparseness with k({self._k}) > len(instances)({num_instances})"
        raise ValueError(msg)

    results = sparseness(instances, self._archive, k=self._k)
    return instances, results

__init__(archive=None, k=15, dist_metric='euclidean')

Creates an instance of the NoveltySearch Algorithm Args: archive (Archive, optional): Archive to store the instances to guide the evolution. Defaults to Archive(threshold=0.001). k (int, optional): Number of neighbours to calculate the sparseness. Defaults to 15. dist_metric (str, optional): Defines the distance metric used by NearestNeighbor in the archives. Defaults to euclidean.

Source code in digneapy/_core/_novelty_search.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def __init__(
    self,
    archive: Optional[Archive] = None,
    k: Optional[int] = 15,
    dist_metric: Optional[str] = "euclidean",
):
    """Creates an instance of the NoveltySearch Algorithm
    Args:
        archive (Archive, optional): Archive to store the instances to guide the evolution. Defaults to Archive(threshold=0.001).
        k (int, optional): Number of neighbours to calculate the sparseness. Defaults to 15.
        dist_metric (str, optional): Defines the distance metric used by NearestNeighbor in the archives. Defaults to euclidean.
    """
    if k < 0:
        raise ValueError(
            f"{__name__} k must be a positive integer and less than the number of instances."
        )
    self._k = k
    self._archive = archive if archive is not None else Archive(threshold=0.001)
    self._dist_metric = (
        dist_metric if dist_metric in NS._EXPECTED_METRICS else "euclidean"
    )

Problem

Bases: ABC, RNG

Source code in digneapy/_core/_problem.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
class Problem(ABC, RNG):
    def __init__(
        self,
        dimension: int,
        bounds: Sequence[tuple],
        name: str = "DefaultProblem",
        dtype=np.float64,
        seed: int = 42,
        *args,
        **kwargs,
    ):
        """Creates a new problem instance.
        The problem is defined by its dimension and the bounds of each variable.

        Args:
            dimension (int): Number of variables in the problem
            bounds (Sequence[tuple]): Bounds of each variable in the problem
            name (str, optional): Name of the problem for printing and logging purposes. Defaults to "DefaultProblem".
            dtype (_type_, optional): Type of the variables. Defaults to np.float64.
            seed (int, optional): Seed for the RNG. Defaults to 42.
        """
        self._name = name
        self.__name__ = name
        self._dimension = dimension
        self._bounds = bounds
        self._dtype = dtype
        self.initialize_rng(seed=seed)
        if len(self._bounds) != 0:
            ranges = list(zip(*bounds))
            self._lbs = np.array(ranges[0], dtype=dtype)
            self._ubs = np.array(ranges[1], dtype=dtype)

    @property
    def dimension(self):
        return self._dimension

    @property
    def bounds(self):
        return self._bounds

    def get_bounds_at(self, i: int) -> tuple:
        if i < 0 or i > len(self._bounds):
            raise ValueError(
                f"Index {i} out-of-range. The bounds are 0-{len(self._bounds)} "
            )
        return (self._lbs[i], self._ubs[i])

    @abstractmethod
    def create_solution(self) -> Solution:
        """Creates a random solution to the problem.
        This method can be used to initialise the solutions
        for any algorithm
        """
        msg = "create_solution method not implemented in Problem"
        raise NotImplementedError(msg)

    @abstractmethod
    def evaluate(self, individual: Sequence | Solution) -> Tuple[float]:
        """Evaluates the candidate individual with the information of the Knapsack

        Args:
            individual (Sequence | Solution): Individual to evaluate

        Raises:
            ValueError: Raises an error if the len(individual) != len(instance) / 2

        Returns:
            Tuple[float]: Profit
        """
        msg = "evaluate method not implemented in Problem"
        raise NotImplementedError(msg)

    @abstractmethod
    def __call__(self, individual: Sequence | Solution) -> Tuple[float]:
        msg = "__call__ method not implemented in Problem"
        raise NotImplementedError(msg)

    @abstractmethod
    def to_instance(self) -> Instance:
        """Creates an instance from the information of the problem.
        This method is used in the generators to create instances to evolve
        """
        msg = "to_instance method not implemented in Problem"
        raise NotImplementedError(msg)

    @abstractmethod
    def to_file(self, filename: str):
        msg = "to_file method not implemented in Problem"
        raise NotImplementedError(msg)

    @classmethod
    def from_file(cls, filename: str):
        msg = "from_file method not implemented in Problem"
        raise NotImplementedError(msg)

__init__(dimension, bounds, name='DefaultProblem', dtype=np.float64, seed=42, *args, **kwargs)

Creates a new problem instance. The problem is defined by its dimension and the bounds of each variable.

Parameters:
  • dimension (int) –

    Number of variables in the problem

  • bounds (Sequence[tuple]) –

    Bounds of each variable in the problem

  • name (str, default: 'DefaultProblem' ) –

    Name of the problem for printing and logging purposes. Defaults to "DefaultProblem".

  • dtype (_type_, default: float64 ) –

    Type of the variables. Defaults to np.float64.

  • seed (int, default: 42 ) –

    Seed for the RNG. Defaults to 42.

Source code in digneapy/_core/_problem.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def __init__(
    self,
    dimension: int,
    bounds: Sequence[tuple],
    name: str = "DefaultProblem",
    dtype=np.float64,
    seed: int = 42,
    *args,
    **kwargs,
):
    """Creates a new problem instance.
    The problem is defined by its dimension and the bounds of each variable.

    Args:
        dimension (int): Number of variables in the problem
        bounds (Sequence[tuple]): Bounds of each variable in the problem
        name (str, optional): Name of the problem for printing and logging purposes. Defaults to "DefaultProblem".
        dtype (_type_, optional): Type of the variables. Defaults to np.float64.
        seed (int, optional): Seed for the RNG. Defaults to 42.
    """
    self._name = name
    self.__name__ = name
    self._dimension = dimension
    self._bounds = bounds
    self._dtype = dtype
    self.initialize_rng(seed=seed)
    if len(self._bounds) != 0:
        ranges = list(zip(*bounds))
        self._lbs = np.array(ranges[0], dtype=dtype)
        self._ubs = np.array(ranges[1], dtype=dtype)

create_solution() abstractmethod

Creates a random solution to the problem. This method can be used to initialise the solutions for any algorithm

Source code in digneapy/_core/_problem.py
71
72
73
74
75
76
77
78
@abstractmethod
def create_solution(self) -> Solution:
    """Creates a random solution to the problem.
    This method can be used to initialise the solutions
    for any algorithm
    """
    msg = "create_solution method not implemented in Problem"
    raise NotImplementedError(msg)

evaluate(individual) abstractmethod

Evaluates the candidate individual with the information of the Knapsack

Parameters:
  • individual (Sequence | Solution) –

    Individual to evaluate

Raises:
  • ValueError

    Raises an error if the len(individual) != len(instance) / 2

Returns:
  • Tuple[float]

    Tuple[float]: Profit

Source code in digneapy/_core/_problem.py
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@abstractmethod
def evaluate(self, individual: Sequence | Solution) -> Tuple[float]:
    """Evaluates the candidate individual with the information of the Knapsack

    Args:
        individual (Sequence | Solution): Individual to evaluate

    Raises:
        ValueError: Raises an error if the len(individual) != len(instance) / 2

    Returns:
        Tuple[float]: Profit
    """
    msg = "evaluate method not implemented in Problem"
    raise NotImplementedError(msg)

to_instance() abstractmethod

Creates an instance from the information of the problem. This method is used in the generators to create instances to evolve

Source code in digneapy/_core/_problem.py
101
102
103
104
105
106
107
@abstractmethod
def to_instance(self) -> Instance:
    """Creates an instance from the information of the problem.
    This method is used in the generators to create instances to evolve
    """
    msg = "to_instance method not implemented in Problem"
    raise NotImplementedError(msg)

RNG

Bases: Protocol

Protocol to type check all operators have _rng of instances types in digneapy

Source code in digneapy/_core/types.py
18
19
20
21
22
23
24
25
26
class RNG(Protocol):
    """Protocol to type check all operators have _rng of instances types in digneapy"""

    _rng: np.random.Generator
    _seed: int

    def initialize_rng(self, seed: Optional[int] = None):
        self._seed = seed if seed else 42
        self._rng = np.random.default_rng(self._seed)

Solution

Class representing a solution in a genetic algorithm. It contains the chromosome, objectives, constraints, and fitness of the solution.

Source code in digneapy/_core/_solution.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
class Solution:
    """
    Class representing a solution in a genetic algorithm.
    It contains the chromosome, objectives, constraints, and fitness of the solution.
    """

    def __init__(
        self,
        chromosome: Optional[Iterable] = None,
        objectives: Optional[Iterable] = None,
        constraints: Optional[Iterable] = None,
        fitness: float = 0.0,
    ):
        """Creates a new solution object.
        The chromosome is a numpy array of the solution's genes.
        The objectives and constraints are numpy arrays of the solution's objectives and constraints.
        The fitness is a float representing the solution's fitness value.

        Args:
            chromosome (Optional[Iterable], optional): Tuple or any other iterable with the chromosome/variables. Defaults to None.
            objectives (Optional[Iterable], optional): Tuple or any other iterable with the objectives values. Defaults to None.
            constraints (Optional[Iterable], optional): Tuple or any other iterable with the constraint values. Defaults to None.
            fitness (float, optional): Fitness of the solution. Defaults to 0.0.
        """
        if chromosome is not None:
            self.chromosome = np.asarray(chromosome)
        else:
            self.chromosome = np.empty(0)
        self.objectives = np.array(objectives) if objectives else np.empty(0)
        self.constraints = np.array(constraints) if constraints else np.empty(0)
        self.fitness = fitness

    def clone(self) -> Self:
        """Returns a deep copy of the solution. It is more efficient than using the copy module.

        Returns:
            Self: Solution object
        """
        return Solution(
            chromosome=list(self.chromosome),
            objectives=list(self.objectives),
            constraints=list(self.constraints),
            fitness=self.fitness,
        )

    def __str__(self) -> str:
        return f"Solution(dim={len(self.chromosome)},f={self.fitness},objs={self.objectives},const={self.constraints})"

    def __repr__(self) -> str:
        return f"Solution<dim={len(self.chromosome)},f={self.fitness},objs={self.objectives},const={self.constraints}>"

    def __len__(self) -> int:
        return len(self.chromosome)

    def __iter__(self):
        return iter(self.chromosome)

    def __bool__(self):
        return len(self) != 0

    def __eq__(self, other) -> bool:
        if isinstance(other, Solution):
            try:
                return all(a == b for a, b in zip(self, other, strict=True))
            except ValueError:
                return False
        else:
            return NotImplemented

    def __gt__(self, other):
        if not isinstance(other, Solution):
            msg = f"Other of type {other.__class__.__name__} can not be compared with with {self.__class__.__name__}"
            print(msg)
            return NotImplemented
        return self.fitness > other.fitness

    def __getitem__(self, key):
        if isinstance(key, slice):
            cls = type(self)  # To facilitate subclassing
            return cls(self.chromosome[key])
        index = operator.index(key)
        return self.chromosome[index]

    def __setitem__(self, key, value):
        self.chromosome[key] = value

__init__(chromosome=None, objectives=None, constraints=None, fitness=0.0)

Creates a new solution object. The chromosome is a numpy array of the solution's genes. The objectives and constraints are numpy arrays of the solution's objectives and constraints. The fitness is a float representing the solution's fitness value.

Parameters:
  • chromosome (Optional[Iterable], default: None ) –

    Tuple or any other iterable with the chromosome/variables. Defaults to None.

  • objectives (Optional[Iterable], default: None ) –

    Tuple or any other iterable with the objectives values. Defaults to None.

  • constraints (Optional[Iterable], default: None ) –

    Tuple or any other iterable with the constraint values. Defaults to None.

  • fitness (float, default: 0.0 ) –

    Fitness of the solution. Defaults to 0.0.

Source code in digneapy/_core/_solution.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def __init__(
    self,
    chromosome: Optional[Iterable] = None,
    objectives: Optional[Iterable] = None,
    constraints: Optional[Iterable] = None,
    fitness: float = 0.0,
):
    """Creates a new solution object.
    The chromosome is a numpy array of the solution's genes.
    The objectives and constraints are numpy arrays of the solution's objectives and constraints.
    The fitness is a float representing the solution's fitness value.

    Args:
        chromosome (Optional[Iterable], optional): Tuple or any other iterable with the chromosome/variables. Defaults to None.
        objectives (Optional[Iterable], optional): Tuple or any other iterable with the objectives values. Defaults to None.
        constraints (Optional[Iterable], optional): Tuple or any other iterable with the constraint values. Defaults to None.
        fitness (float, optional): Fitness of the solution. Defaults to 0.0.
    """
    if chromosome is not None:
        self.chromosome = np.asarray(chromosome)
    else:
        self.chromosome = np.empty(0)
    self.objectives = np.array(objectives) if objectives else np.empty(0)
    self.constraints = np.array(constraints) if constraints else np.empty(0)
    self.fitness = fitness

clone()

Returns a deep copy of the solution. It is more efficient than using the copy module.

Returns:
  • Self( Self ) –

    Solution object

Source code in digneapy/_core/_solution.py
52
53
54
55
56
57
58
59
60
61
62
63
def clone(self) -> Self:
    """Returns a deep copy of the solution. It is more efficient than using the copy module.

    Returns:
        Self: Solution object
    """
    return Solution(
        chromosome=list(self.chromosome),
        objectives=list(self.objectives),
        constraints=list(self.constraints),
        fitness=self.fitness,
    )

Solver

Bases: ABC, SupportsSolve[P]

Solver is any callable type that receives a OptProblem as its argument and returns a tuple with the solution found

Source code in digneapy/_core/_solver.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Solver(ABC, SupportsSolve[P]):
    """Solver is any callable type that receives a OptProblem
    as its argument and returns a tuple with the solution found
    """

    @abstractmethod
    def __call__(self, problem: P, *args, **kwargs) -> list[Solution]:
        """Solves a optimisation problem

        Args:
            problem (OptProblem): Any optimisation problem or callablle that receives a Sequence and returns a Tuple[float]

        Raises:
            NotImplementedError: Must be implemented by subclasses

        Returns:
            List[Solution]: Returns a sequence of olutions
        """
        msg = "__call__ method not implemented in Solver"
        raise NotImplementedError(msg)

__call__(problem, *args, **kwargs) abstractmethod

Solves a optimisation problem

Parameters:
  • problem (OptProblem) –

    Any optimisation problem or callablle that receives a Sequence and returns a Tuple[float]

Raises:
  • NotImplementedError

    Must be implemented by subclasses

Returns:
  • list[Solution]

    List[Solution]: Returns a sequence of olutions

Source code in digneapy/_core/_solver.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@abstractmethod
def __call__(self, problem: P, *args, **kwargs) -> list[Solution]:
    """Solves a optimisation problem

    Args:
        problem (OptProblem): Any optimisation problem or callablle that receives a Sequence and returns a Tuple[float]

    Raises:
        NotImplementedError: Must be implemented by subclasses

    Returns:
        List[Solution]: Returns a sequence of olutions
    """
    msg = "__call__ method not implemented in Solver"
    raise NotImplementedError(msg)

Statistics

Source code in digneapy/_core/_metrics.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class Statistics:
    def __init__(self):
        self._stats_s = tools.Statistics(key=attrgetter("s"))
        self._stats_p = tools.Statistics(key=attrgetter("p"))
        self._stats_f = tools.Statistics(key=attrgetter("fitness"))

        self._stats = tools.MultiStatistics(
            s=self._stats_s, p=self._stats_p, fitness=self._stats_f
        )
        self._stats.register("mean", np.mean)
        self._stats.register("std", np.std)
        self._stats.register("min", np.min)
        self._stats.register("max", np.max)
        self._stats.register("qd_score", np.sum)

    def __call__(
        self, population: Sequence[Instance], as_series: bool = False
    ) -> dict | pd.Series:
        """Calculates the statistics of the population.
        Args:
            population (Sequence[Instance]): List of instances to calculate the statistics.
        Returns:
            dict: Dictionary with the statistics of the population.
        """
        if len(population) == 0:
            raise ValueError(
                "Error: Trying to calculate the metrics with an empty population"
            )
        if not all(isinstance(ind, Instance) for ind in population):
            raise TypeError("Error: Population must be a sequence of Instance objects")

        record = self._stats.compile(population)
        if as_series:
            _flatten_record = {}
            for key, value in record.items():
                if isinstance(value, dict):  # Flatten nested dicts
                    for sub_key, sub_value in value.items():
                        _flatten_record[f"{key}_{sub_key}"] = sub_value
                else:
                    _flatten_record[key] = value
            return pd.Series(_flatten_record)
        else:
            return record

__call__(population, as_series=False)

Calculates the statistics of the population. Args: population (Sequence[Instance]): List of instances to calculate the statistics. Returns: dict: Dictionary with the statistics of the population.

Source code in digneapy/_core/_metrics.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def __call__(
    self, population: Sequence[Instance], as_series: bool = False
) -> dict | pd.Series:
    """Calculates the statistics of the population.
    Args:
        population (Sequence[Instance]): List of instances to calculate the statistics.
    Returns:
        dict: Dictionary with the statistics of the population.
    """
    if len(population) == 0:
        raise ValueError(
            "Error: Trying to calculate the metrics with an empty population"
        )
    if not all(isinstance(ind, Instance) for ind in population):
        raise TypeError("Error: Population must be a sequence of Instance objects")

    record = self._stats.compile(population)
    if as_series:
        _flatten_record = {}
        for key, value in record.items():
            if isinstance(value, dict):  # Flatten nested dicts
                for sub_key, sub_value in value.items():
                    _flatten_record[f"{key}_{sub_key}"] = sub_value
            else:
                _flatten_record[key] = value
        return pd.Series(_flatten_record)
    else:
        return record

SupportsSolve

Bases: Protocol[P]

Protocol to type check all the solver types in digneapy. A solver is any callable type that receives at least a problem (Problem) and returns a list of object of the Solution class.

Source code in digneapy/_core/_solver.py
20
21
22
23
24
25
26
class SupportsSolve(Protocol[P]):
    """Protocol to type check all the solver types in digneapy.
    A solver is any callable type that receives at least a problem (Problem) and
    returns a list of object of the Solution  class.
    """

    def __call__(self, problem: P, *args, **kwargs) -> list[Solution]: ...

qd_score(instances_fitness)

Calculates the Quality Diversity score of a set of instances fitness.

Parameters:
  • instances (Sequence[float]) –

    List with the fitness of several instances to calculate the QD score.

Returns:
  • float( float64 ) –

    Sum of the fitness of all instances.

Source code in digneapy/_core/_metrics.py
23
24
25
26
27
28
29
30
31
32
def qd_score(instances_fitness: Sequence[float]) -> np.float64:
    """Calculates the Quality Diversity score of a set of instances fitness.

    Args:
        instances (Sequence[float]): List with the fitness of several instances to calculate the QD score.

    Returns:
        float: Sum of the fitness of all instances.
    """
    return np.sum(instances_fitness)

qd_score_auc(qd_scores, batch_size)

Calculates the Quantifying Efficiency in Quality Diversity Optimization In quality diversity (QD) optimization, the QD score is a holistic metric which sums the objective values of all cells in the archive. Since the QD score only measures the performance of a QD algorithm at a single point in time, it fails to reflect algorithm efficiency. Two algorithms may have the same QD score even though one algorithm achieved that score with fewer evaluations. We propose a metric called “QD score AUC” which quantifies this efficiency.

Parameters:
  • qd_scores (Sequence[float]) –

    Sequence of QD scores.

  • batch_size (int) –

    Number of instances evaluated in each generation.

Returns:
  • float64

    np.float64: QD score AUC metric.

Source code in digneapy/_core/_metrics.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def qd_score_auc(qd_scores: Sequence[float], batch_size: int) -> np.float64:
    """Calculates the Quantifying Efficiency in Quality Diversity Optimization
    In quality diversity (QD) optimization, the QD score is a holistic
    metric which sums the objective values of all cells in the archive.
    Since the QD score only measures the performance of a QD algorithm at a single point in time, it fails to reflect algorithm efficiency.
    Two algorithms may have the same QD score even though one
    algorithm achieved that score with fewer evaluations. We propose
    a metric called “QD score AUC” which quantifies this efficiency.

    Args:
        qd_scores (Sequence[float]): Sequence of QD scores.
        batch_size (int): Number of instances evaluated in each generation.

    Returns:
        np.float64: QD score AUC metric.
    """
    return np.sum(qd_scores) * batch_size