@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
116
117
118
119
120
121
122
123
124
125
126
127
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,
        name: str = "Domain",
        feat_names: Optional[Sequence[str]] = None,
        seed: Optional[int] = 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_instances(self, n: int = 1) -> List[Instance]:
        """Generates N instances for the domain.

        Args:
            n (int, optional): Number of instances to generate. Defaults to 1.

        Returns:
            List[Instance]: A list of Instance objects created from the raw numpy generation
        """
        raise NotImplementedError(
            "generate_n_instances is not implemented in Domain class."
        )

    @abstractmethod
    def generate_problems_from_instances(
        self, instances: Sequence[Instance] | np.ndarray
    ) -> List[Problem]:
        msg = "generate_problems_from_instances is not implemented in Domain class."
        raise NotImplementedError(msg)

    @abstractmethod
    def extract_features(
        self, instances: Sequence[Instance] | np.ndarray
    ) -> np.ndarray:
        """Extract the features of the instances 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, instances: Sequence[Instance] | np.ndarray
    ) -> List[Dict[str, np.float32]]:
        """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)

    @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 instances based on the domain

Parameters:
  • instance (Instance) –

    Instance to extract the features from

Returns:
  • Tuple( ndarray ) –

    Values of each feature

Source code in digneapy/_core/_domain.py
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@abstractmethod
def extract_features(
    self, instances: Sequence[Instance] | np.ndarray
) -> np.ndarray:
    """Extract the features of the instances 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(instances) 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:
  • List[Dict[str, float32]]

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

Source code in digneapy/_core/_domain.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
@abstractmethod
def extract_features_as_dict(
    self, instances: Sequence[Instance] | np.ndarray
) -> List[Dict[str, np.float32]]:
    """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_instances(n=1) abstractmethod

Generates N instances for the domain.

Parameters:
  • n (int, default: 1 ) –

    Number of instances to generate. Defaults to 1.

Returns:
  • List[Instance]

    List[Instance]: A list of Instance objects created from the raw numpy generation

Source code in digneapy/_core/_domain.py
57
58
59
60
61
62
63
64
65
66
67
68
69
@abstractmethod
def generate_instances(self, n: int = 1) -> List[Instance]:
    """Generates N instances for the domain.

    Args:
        n (int, optional): Number of instances to generate. Defaults to 1.

    Returns:
        List[Instance]: A list of Instance objects created from the raw numpy generation
    """
    raise NotImplementedError(
        "generate_n_instances is not implemented in Domain class."
    )

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
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
class Instance:
    __slots__ = (
        "_vars",
        "_fit",
        "_p",
        "_s",
        "_features",
        "_desc",
        "_pscores",
        "_otype",
        "_dtype",
    )

    def __init__(
        self,
        variables: Optional[npt.ArrayLike] = None,
        fitness: np.float64 = np.float64(0.0),
        p: np.float64 = np.float64(0.0),
        s: np.float64 = np.float64(0.0),
        features: Optional[tuple[np.float32]] = None,
        descriptor: Optional[tuple[np.float32]] = None,
        portfolio_scores: Optional[tuple[np.float64]] = None,
        otype=np.float64,
        dtype=np.uint32,
    ):
        """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.
        """
        self._otype = otype
        self._dtype = dtype
        try:
            fitness = self._otype(fitness)
            p = self._otype(p)
            s = self._otype(s)
        except ValueError:
            raise ValueError(
                "The fitness, p and s parameters must be convertible to float"
            )

        self._vars = (
            np.array(variables, dtype=self._dtype)
            if variables is not None
            else np.empty(0, dtype=self._dtype)
        )
        self._fit = fitness
        self._p = p
        self._s = s
        self._features = (
            np.array(features, dtype=np.float32)
            if features is not None
            else np.empty(0, dtype=np.float32)
        )
        self._pscores = (
            np.array(portfolio_scores, dtype=self._otype)
            if portfolio_scores is not None
            else np.empty(0, dtype=self._otype)
        )

        self._desc = (
            np.array(descriptor, dtype=np.float32)
            if descriptor is not None
            else np.empty(0, dtype=np.float32)
        )

    @property
    def dtype(self):
        return self._dtype

    @property
    def otype(self):
        return self._otype

    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),
        )

    def clone_with(self, **overrides):
        """Clones an Instance with overriden attributes

        Returns:
            Instance
        """
        new_object = self.clone()
        for key, value in overrides.items():
            setattr(new_object, key, value)
        return new_object

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

    @variables.setter
    def variables(self, new_variables: npt.ArrayLike):
        if len(new_variables) != len(self._vars):
            raise ValueError(
                "Updating the variables of an Instance object with a different number of values."
                f"Instance have {len(self._vars)}"
                f"variables and the new_variables sequence have {len(new_variables)}"
            )
        self._vars = np.asarray(new_variables)

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

    @p.setter
    def p(self, performance: np.float64):
        try:
            performance = np.float64(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) -> np.float64:
        return self._s

    @s.setter
    def s(self, novelty: np.float64):
        try:
            novelty = np.float64(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) -> np.float64:
        return self._fit

    @fitness.setter
    def fitness(self, f: np.float64):
        try:
            f = np.float64(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,
        only_genotype: bool = False,
        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:
            only_genotype (bool, Default True): Whether to return the Instance as a dictionary containing only the variables.
            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.
        """
        _data = {}
        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)}
        if only_genotype:
            return _data

        else:
            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)},
                **_data,
            }

            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)}

        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,
        only_genotype: bool = False,
        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:
            only_genotype (bool, Default True): Whether to return the Instance as a pd.Series containing only the variables.
            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(
            only_genotype=only_genotype,
            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=np.float64(0.0), p=np.float64(0.0), s=np.float64(0.0), features=None, descriptor=None, portfolio_scores=None, otype=np.float64, dtype=np.uint32)

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: float64(0.0) ) –

    Fitness of the instance. Defaults to 0.0.

  • p (float, default: float64(0.0) ) –

    Performance score. Defaults to 0.0.

  • s (float, default: float64(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
 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
def __init__(
    self,
    variables: Optional[npt.ArrayLike] = None,
    fitness: np.float64 = np.float64(0.0),
    p: np.float64 = np.float64(0.0),
    s: np.float64 = np.float64(0.0),
    features: Optional[tuple[np.float32]] = None,
    descriptor: Optional[tuple[np.float32]] = None,
    portfolio_scores: Optional[tuple[np.float64]] = None,
    otype=np.float64,
    dtype=np.uint32,
):
    """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.
    """
    self._otype = otype
    self._dtype = dtype
    try:
        fitness = self._otype(fitness)
        p = self._otype(p)
        s = self._otype(s)
    except ValueError:
        raise ValueError(
            "The fitness, p and s parameters must be convertible to float"
        )

    self._vars = (
        np.array(variables, dtype=self._dtype)
        if variables is not None
        else np.empty(0, dtype=self._dtype)
    )
    self._fit = fitness
    self._p = p
    self._s = s
    self._features = (
        np.array(features, dtype=np.float32)
        if features is not None
        else np.empty(0, dtype=np.float32)
    )
    self._pscores = (
        np.array(portfolio_scores, dtype=self._otype)
        if portfolio_scores is not None
        else np.empty(0, dtype=self._otype)
    )

    self._desc = (
        np.array(descriptor, dtype=np.float32)
        if descriptor is not None
        else np.empty(0, dtype=np.float32)
    )

asdict(only_genotype=False, 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:
  • only_genotype (bool, Default True, default: False ) –

    Whether to return the Instance as a dictionary containing only the variables.

  • 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
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
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
def asdict(
    self,
    only_genotype: bool = False,
    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:
        only_genotype (bool, Default True): Whether to return the Instance as a dictionary containing only the variables.
        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.
    """
    _data = {}
    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)}
    if only_genotype:
        return _data

    else:
        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)},
            **_data,
        }

        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)}

    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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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),
    )

clone_with(**overrides)

Clones an Instance with overriden attributes

Returns:
  • Instance

Source code in digneapy/_core/_instance.py
126
127
128
129
130
131
132
133
134
135
def clone_with(self, **overrides):
    """Clones an Instance with overriden attributes

    Returns:
        Instance
    """
    new_object = self.clone()
    for key, value in overrides.items():
        setattr(new_object, key, value)
    return new_object

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
366
367
368
369
370
371
372
373
374
375
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(only_genotype=False, variables_names=None, features_names=None, score_names=None)

Creates a pandas Series from the instance.

Parameters:
  • only_genotype (bool, Default True, default: False ) –

    Whether to return the Instance as a pd.Series containing only the variables.

  • 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
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
def to_series(
    self,
    only_genotype: bool = False,
    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:
        only_genotype (bool, Default True): Whether to return the Instance as a pd.Series containing only the variables.
        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(
        only_genotype=only_genotype,
        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

Source code in digneapy/_core/_novelty_search.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
class NS:
    def __init__(
        self,
        archive: Optional[Archive] = None,
        k: int = 15,
    ):
        """Creates an instance of the Novelty Search Algorithm
        Args:
            archive (Archive): 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.
        """
        if k < 0:
            raise ValueError(
                f"{__name__} k must be a positive integer and less than the number of instances."
            )

        if archive is not None and not isinstance(archive, Archive):
            raise ValueError("You must provide a valid Archive object")
        self._k = k
        self._archive = archive if archive is not None else Archive(threshold=0.001)

    @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_descriptors: np.ndarray) -> np.ndarray:
        """Computes the Novelty Search of the instance descriptors with respect to the archive.
           It uses the Euclidean distance to compute the sparseness.

        Args:
            instance_descriptors (np.ndarray): Numpy array with the descriptors of the instances
            archive (Archive): Archive which stores the novelty instances found so far
            k (int, optional): Number of neighbors to consider in the computation of the sparseness. Defaults to 15.

        Raises:
            ValueError: If len(instance_descriptors) <= k

        Returns:
            np.ndarray: novelty scores (s) of the instances descriptors
        """
        if len(instances_descriptors) == 0:
            raise ValueError(
                f"NS was given an empty population to compute the sparseness. Shape is: {instances_descriptors.shape}"
            )
        num_instances = len(instances_descriptors)
        num_archive = len(self.archive)
        result = np.zeros(num_instances, dtype=np.float64)
        if num_archive == 0 and num_instances <= self._k:
            # Initially, the archive is empty and we may not have enough instances to evaluate
            print(
                f"NS has an empty archive at this moment and the given population is not large enough to compute the sparseness. {num_instances} < k ({self._k}). Returning zeros.",
                file=sys.stderr,
            )
            return result

        if num_instances + num_archive <= self._k:
            msg = f"Trying to calculate novelty search with k({self._k}) >= {num_instances} (instances) + {num_archive} (archive)."
            raise ValueError(msg)

        combined = (
            instances_descriptors
            if num_archive == 0
            else np.vstack([instances_descriptors, self._archive.descriptors])
        )
        for i in range(num_instances):
            mask = np.ones(num_instances, bool)
            mask[i] = False
            differences = combined[i] - combined[np.nonzero(mask)]
            distances = np.linalg.norm(differences, axis=1)
            _neighbors = np.partition(distances, self._k + 1)[1 : self._k + 1]
            result[i] = np.sum(_neighbors) / self._k

        return result

__call__(instances_descriptors)

Computes the Novelty Search of the instance descriptors with respect to the archive. It uses the Euclidean distance to compute the sparseness.

Parameters:
  • instance_descriptors (ndarray) –

    Numpy array with the descriptors of the instances

  • archive (Archive) –

    Archive which stores the novelty instances found so far

  • k (int) –

    Number of neighbors to consider in the computation of the sparseness. Defaults to 15.

Raises:
  • ValueError

    If len(instance_descriptors) <= k

Returns:
  • ndarray

    np.ndarray: novelty scores (s) of the instances descriptors

Source code in digneapy/_core/_novelty_search.py
 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
def __call__(self, instances_descriptors: np.ndarray) -> np.ndarray:
    """Computes the Novelty Search of the instance descriptors with respect to the archive.
       It uses the Euclidean distance to compute the sparseness.

    Args:
        instance_descriptors (np.ndarray): Numpy array with the descriptors of the instances
        archive (Archive): Archive which stores the novelty instances found so far
        k (int, optional): Number of neighbors to consider in the computation of the sparseness. Defaults to 15.

    Raises:
        ValueError: If len(instance_descriptors) <= k

    Returns:
        np.ndarray: novelty scores (s) of the instances descriptors
    """
    if len(instances_descriptors) == 0:
        raise ValueError(
            f"NS was given an empty population to compute the sparseness. Shape is: {instances_descriptors.shape}"
        )
    num_instances = len(instances_descriptors)
    num_archive = len(self.archive)
    result = np.zeros(num_instances, dtype=np.float64)
    if num_archive == 0 and num_instances <= self._k:
        # Initially, the archive is empty and we may not have enough instances to evaluate
        print(
            f"NS has an empty archive at this moment and the given population is not large enough to compute the sparseness. {num_instances} < k ({self._k}). Returning zeros.",
            file=sys.stderr,
        )
        return result

    if num_instances + num_archive <= self._k:
        msg = f"Trying to calculate novelty search with k({self._k}) >= {num_instances} (instances) + {num_archive} (archive)."
        raise ValueError(msg)

    combined = (
        instances_descriptors
        if num_archive == 0
        else np.vstack([instances_descriptors, self._archive.descriptors])
    )
    for i in range(num_instances):
        mask = np.ones(num_instances, bool)
        mask[i] = False
        differences = combined[i] - combined[np.nonzero(mask)]
        distances = np.linalg.norm(differences, axis=1)
        _neighbors = np.partition(distances, self._k + 1)[1 : self._k + 1]
        result[i] = np.sum(_neighbors) / self._k

    return result

__init__(archive=None, k=15)

Creates an instance of the Novelty Search Algorithm Args: archive (Archive): 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.

Source code in digneapy/_core/_novelty_search.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def __init__(
    self,
    archive: Optional[Archive] = None,
    k: int = 15,
):
    """Creates an instance of the Novelty Search Algorithm
    Args:
        archive (Archive): 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.
    """
    if k < 0:
        raise ValueError(
            f"{__name__} k must be a positive integer and less than the number of instances."
        )

    if archive is not None and not isinstance(archive, Archive):
        raise ValueError("You must provide a valid Archive object")
    self._k = k
    self._archive = archive if archive is not None else Archive(threshold=0.001)

Problem

Bases: ABC, RNG

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
 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
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 | np.ndarray:
        """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 __array__(
        self, dtype: Any = None, copy: Optional[bool] = None
    ) -> npt.ArrayLike:
        msg = "__array__ method not implemented in Problem"
        raise NotImplementedError(msg)

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

        Args:
            individual (Sequence | Solution | np.ndarray): Individual to evaluate

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

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

    @abstractmethod
    def __call__(self, individual: Sequence | Solution | np.ndarray) -> 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
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
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
72
73
74
75
76
77
78
79
@abstractmethod
def create_solution(self) -> Solution | np.ndarray:
    """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 | ndarray) –

    Individual to evaluate

Raises:
  • ValueError

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

Returns:
  • Tuple[float]

    Tuple[float]: fitness

Source code in digneapy/_core/_problem.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
@abstractmethod
def evaluate(self, individual: Sequence | Solution | np.ndarray) -> Tuple[float]:
    """Evaluates the candidate individual with the information of the Knapsack

    Args:
        individual (Sequence | Solution | np.ndarray): Individual to evaluate

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

    Returns:
        Tuple[float]: fitness
    """
    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
109
110
111
112
113
114
115
@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: Generator
    _seed: int | None

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

Solution

Class representing a solution in a genetic algorithm. It contains the variables, 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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
class Solution:
    """
    Class representing a solution in a genetic algorithm.
    It contains the variables, objectives, constraints, and fitness of the solution.
    """

    def __init__(
        self,
        variables: Optional[Iterable] = [],
        objectives: Optional[Iterable] = [],
        constraints: Optional[Iterable] = [],
        fitness: np.float64 = np.float64(0.0),
        dtype=np.uint32,
        otype=np.float64,
    ):
        """Creates a new solution object.
        The variables 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:
            variables (Optional[Iterable], optional): Tuple or any other iterable with the variables/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.
        """

        self._otype = otype
        self._dtype = dtype
        self.variables = np.asarray(variables, dtype=self.dtype)
        self.objectives = np.array(objectives, dtype=self.otype)
        self.constraints = np.array(constraints, dtype=self.otype)
        self.fitness = otype(fitness)

    @property
    def dtype(self):
        return self._dtype

    @property
    def otype(self):
        return self._otype

    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(
            variables=list(self.variables),
            objectives=list(self.objectives),
            constraints=list(self.constraints),
            fitness=self.fitness,
            otype=self.otype,
        )

    def clone_with(self, **overrides):
        """Clones an Instance with overriden attributes

        Returns:
            Instance
        """
        new_object = self.clone()
        for key, value in overrides.items():
            setattr(new_object, key, value)
        return new_object

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

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

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

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

    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.variables[key])
        index = operator.index(key)
        return self.variables[index]

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

__init__(variables=[], objectives=[], constraints=[], fitness=np.float64(0.0), dtype=np.uint32, otype=np.float64)

Creates a new solution object. The variables 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:
  • variables (Optional[Iterable], default: [] ) –

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

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

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

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

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

  • fitness (float, default: float64(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
51
52
def __init__(
    self,
    variables: Optional[Iterable] = [],
    objectives: Optional[Iterable] = [],
    constraints: Optional[Iterable] = [],
    fitness: np.float64 = np.float64(0.0),
    dtype=np.uint32,
    otype=np.float64,
):
    """Creates a new solution object.
    The variables 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:
        variables (Optional[Iterable], optional): Tuple or any other iterable with the variables/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.
    """

    self._otype = otype
    self._dtype = dtype
    self.variables = np.asarray(variables, dtype=self.dtype)
    self.objectives = np.array(objectives, dtype=self.otype)
    self.constraints = np.array(constraints, dtype=self.otype)
    self.fitness = otype(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
62
63
64
65
66
67
68
69
70
71
72
73
74
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(
        variables=list(self.variables),
        objectives=list(self.objectives),
        constraints=list(self.constraints),
        fitness=self.fitness,
        otype=self.otype,
    )

clone_with(**overrides)

Clones an Instance with overriden attributes

Returns:
  • Instance

Source code in digneapy/_core/_solution.py
76
77
78
79
80
81
82
83
84
85
def clone_with(self, **overrides):
    """Clones an Instance with overriden attributes

    Returns:
        Instance
    """
    new_object = self.clone()
    for key, value in overrides.items():
        setattr(new_object, key, value)
    return new_object

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]: ...

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 method returns a descending sorted list of instances by their competition fitness value. 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:
  • descriptors (ndarray) –

    Numpy array with the descriptors of the instances

  • performances (ndarray) –

    Numpy array with the performance values of the instances

  • k (int, default: 15 ) –

    Number of nearest neighbours to calculate the competition fitness. Default to 15.

  • force_feasible_only (bool, default: True ) –

    Allow only instances with performance >= 0 to be considered. Default True.

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

Returns:
  • Tuple[ndarray, ndarray, ndarray, ndarray]

    Tuple[np.ndarray]: Tuple with the descriptors, performances and competition fitness values sorted, plus the sorted indexing (descending order).

Source code in digneapy/_core/_novelty_search.py
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
def dominated_novelty_search(
    descriptors: np.ndarray,
    performances: np.ndarray,
    k: int = 15,
    force_feasible_only: bool = True,
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
    """
    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 method returns a descending sorted list of instances by their competition fitness value.
    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:
        descriptors (np.ndarray): Numpy array with the descriptors of the instances
        performances (np.ndarray): Numpy array with the performance values of the instances
        k (int): Number of nearest neighbours to calculate the competition fitness. Default to 15.
        force_feasible_only (bool): Allow only instances with performance >= 0 to be considered. Default True.
    Raises:
        ValueError: If len(d) where d is the descriptor of each instance i differs from another
        ValueError: If k >= len(instances)

    Returns:
        Tuple[np.ndarray]: Tuple with the descriptors, performances and competition fitness values sorted, plus the sorted indexing (descending order).
    """
    num_instances = len(descriptors)
    if num_instances <= k:
        msg = f"Trying to calculate the dominated novelty search with k({k}) > len(instances) = {num_instances}"
        raise ValueError(msg)

    if len(performances) != len(descriptors):
        raise ValueError(
            f"Array mismatch between peformances and descriptors. len(performance) = {len(performances)} != {len(descriptors)} len(descriptors)"
        )
    # Try to force only feasible performances to get proper biased instances
    is_unfeasible = (
        performances < 0.0 if force_feasible_only else (performances == -np.inf)
    )
    fitter = performances[:, None] <= performances[None, :]
    fitter = np.where(is_unfeasible[None, :], False, fitter)
    np.fill_diagonal(fitter, False)
    distance = np.linalg.norm(
        descriptors[:, None, :] - descriptors[None, :, :], axis=-1
    )
    distance = np.where(fitter, distance, np.inf)
    neg_dist = -distance
    indices = np.argpartition(neg_dist, -k, axis=-1)[..., -k:]
    values = np.take_along_axis(neg_dist, indices, axis=-1)
    indices = np.argsort(values, axis=-1)[..., ::-1]
    values = np.take_along_axis(values, indices, axis=-1)
    indices = np.take_along_axis(indices, indices, axis=-1)
    distance = np.mean(
        -values, where=np.take_along_axis(fitter, indices, axis=1), axis=-1
    )
    distance = np.where(np.isnan(distance), np.inf, distance)
    distance = np.where(is_unfeasible, -np.inf, distance)
    sorted_indices = np.argsort(-distance)
    return (
        descriptors[sorted_indices],
        performances[sorted_indices],
        distance[sorted_indices],
        sorted_indices,
    )

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: np.ndarray) -> 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: np.ndarray, 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