Skip to content

Commit

Permalink
🧪 Includes bunch of tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
amarrerod committed Jun 19, 2024
1 parent 6c3cbc6 commit 6f6223b
Show file tree
Hide file tree
Showing 13 changed files with 379 additions and 112 deletions.
10 changes: 7 additions & 3 deletions digneapy/archives/_base_archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ def threshold(self):
def threshold(self, t: float):
try:
t_f = float(t)
except ValueError:
except Exception:
msg = f"The threshold value {t} is not a float in 'threshold' setter of class {self.__class__.__name__}"
raise AttributeError(msg)
raise TypeError(msg)
self._threshold = t_f

def __iter__(self):
Expand Down Expand Up @@ -126,7 +126,7 @@ def append(self, i: Instance):
self.instances.append(i)
else:
msg = f"Only objects of type {Instance.__class__.__name__} can be inserted into an archive"
raise AttributeError(msg)
raise TypeError(msg)

def __default_filter(self, instance: Instance):
return instance.s >= self._threshold
Expand All @@ -142,6 +142,10 @@ def extend(
filter_fn (Callable, optional): A function that takes an instance and returns a boolean.
Defaults to filtering by sparseness.
"""
if not all(isinstance(i, Instance) for i in iterable):
msg = f"Only objects of type {Instance.__class__.__name__} can be inserted into an archive"
raise TypeError(msg)

default_filter = self.__default_filter
actual_filter = filter_fn if filter_fn is not None else default_filter

Expand Down
84 changes: 63 additions & 21 deletions digneapy/archives/_grid_archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,16 @@ def __init__(
epsilon when computing the archive indices in the :meth:`index_of`
method -- refer to the implementation `here. Defaults to 1e-6.
dtype(str or data-type): Data type of the solutions, objectives,
and measures. We only support ``"f"`` / ``np.float32`` and ``"d"`` /
``np.float64``.
and measures.
Raises:
AttributeError: ``dimensions`` and ``ranges`` are not the same length
ValueError: ``dimensions`` and ``ranges`` are not the same length
"""
Archive.__init__(self, threshold=np.inf)
if len(ranges) == 0 or len(dimensions) == 0:
raise AttributeError("dimensions or ranges must have length >= 1")
raise ValueError("dimensions or ranges must have length >= 1")
if len(ranges) != len(dimensions):
raise AttributeError(
"len(dimensions) != len(ranges) in GridArchive.__init__()"
)
raise ValueError("len(dimensions) != len(ranges) in GridArchive.__init__()")

self._dimensions = np.asarray(dimensions)

Expand All @@ -89,7 +86,7 @@ def __init__(
):
self._boundaries.append(np.linspace(l_b, u_b, dimension + 1))

if instances:
if instances is not None:
self.extend(instances)

@property
Expand All @@ -105,7 +102,7 @@ def bounds(self):
entries laid out like this::
Archive cells: | 0 | 1 | ... | self.dims[i] |
boundaries[i]: 0 1 2 self.dims[i] - 1 self.dims[i]
boundaries[i]: 0 1 2 self.dims[i] - 1 self.dims[i]
Thus, ``boundaries[i][j]`` and ``boundaries[i][j + 1]`` are the lower
and upper bounds of cell ``j`` in dimension ``i``. To access the lower
Expand Down Expand Up @@ -148,25 +145,68 @@ def __repr__(self):
def __len__(self):
return len(self._grid)

def __getitem__(self, key):
"""Returns a dictionary with the descriptors as the keys. The values are the instances found.
Note that some of the given keys may not be in the archive.
Args:
key (array-like or descriptor): Descriptors of the instances that want to retrieve.
Valid examples are:
- archive[[0,11], [0,5]] --> Get the instances with the descriptors (0,11) and (0, 5)
- archive[0,11] --> Get the instance with the descriptor (0,11)
Raises:
TypeError: If the key is an slice. Not allowed.
ValueError: If the shape of the keys are not valid.
Returns:
dict: Returns a dict with the found instances.
"""
if isinstance(key, slice):
raise TypeError(
"Slicing is not available in GridArchive. Use 1D index or descriptor-type indeces"
)
descriptors = np.asarray(key)
if descriptors.ndim == 1 and descriptors.shape[0] != len(self._dimensions):
raise ValueError(
f"Expected descriptors to be an array with shape "
f"(batch_size, 1) or (batch_size, dimensions) (i.e. shape "
f"(batch_size, {len(self._dimensions)})) but it had shape "
f"{descriptors.shape}"
)

indeces = self.index_of(descriptors).tolist()
if isinstance(indeces, int):
indeces = [indeces]
descriptors = [descriptors]

instances = {}
for idx, desc in zip(indeces, descriptors):
if idx not in self._grid:
print(f"There is not any instance in the cell {desc}.")
else:
instances[tuple(desc)] = copy.copy(self._grid[idx])
return instances

def __iter__(self):
"""Iterates over the dictionary of instances
Returns:
Iterator: Yields position in the hypercube and instance located in such position
"""
return iter(self._grid)
return iter(self._grid.values())

def lower_i(self, i):
if i < 0 or i > len(self._boundaries):
if i < 0 or i > len(self._lower_bounds):
msg = f"index {i} is out of bounds. Valid values are [0-{len(self._boundaries)}]"
raise AttributeError(msg)
return self._boundaries[i][0]
raise ValueError(msg)
return self._lower_bounds[i]

def upper_i(self, i):
if i < 0 or i > len(self._boundaries):
if i < 0 or i > len(self._upper_bounds):
msg = f"index {i} is out of bounds. Valid values are [0-{len(self._boundaries)}]"
raise AttributeError(msg)
return self._boundaries[i][1]
raise ValueError(msg)
return self._upper_bounds[i]

def append(self, i: Instance):
"""Inserts an Instance into the Grid
Expand All @@ -184,14 +224,18 @@ def append(self, i: Instance):

else:
msg = "Only objects of type Instance can be inserted into a GridArchive"
raise AttributeError(msg)
raise TypeError(msg)

def extend(self, iterable: Iterable[Instance], *args, **kwargs):
"""Includes all the instances in iterable into the Grid
Args:
iterable (Iterable[Instance]): Iterable of instances
"""
if not all(isinstance(i, Instance) for i in iterable):
msg = "Only objects of type Instance can be inserted into a GridArchive"
raise TypeError(msg)

indeces = self.index_of([inst.descriptor for inst in iterable])
for idx, instance in zip(indeces, iterable, strict=True):
if idx not in self._grid or instance.fitness > self._grid[idx].fitness:
Expand Down Expand Up @@ -248,9 +292,7 @@ def to_json(self):
data = {
"dimensions": self._dimensions.tolist(),
"lbs": self._lower_bounds.tolist(),
"ubs": self._upper_bounds,
"n_cells": self._cells,
"boundaries": self._boundaries.tolist(),
"grid": self._grid,
"ubs": self._upper_bounds.tolist(),
"n_cells": self._cells.astype(int),
}
return json.dumps(data, indent=4)
48 changes: 30 additions & 18 deletions digneapy/core/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@

from abc import ABC, abstractmethod
from collections.abc import Sequence
from typing import Mapping, Optional, Tuple
from typing import Mapping

import numpy as np

from digneapy.core.instance import Instance
from digneapy.core.problem import Problem
Expand All @@ -21,15 +23,22 @@
class Domain(ABC):
def __init__(
self,
dimension: int,
bounds: Sequence[tuple],
dtype=np.float64,
name: str = "Domain",
dimension: int = 0,
bounds: Optional[Sequence[Tuple]] = None,
*args,
**kwargs,
):
self.name = name
self.dimension = dimension
self.bounds = bounds if bounds else [(0.0, 0.0)]
self.__name__ = name
self._dimension = dimension
self._bounds = bounds
self._dtype = dtype
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:
Expand Down Expand Up @@ -74,17 +83,20 @@ 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

def lower_i(self, i):
if i < 0 or i > len(self.bounds):
msg = f"index {i} is out of bounds. Valid values are [0-{len(self.bounds)}]"
raise AttributeError(msg)
return self.bounds[i][0]

def upper_i(self, i):
if i < 0 or i > len(self.bounds):
msg = f"index {i} is out of bounds. Valid values are [0-{len(self.bounds)}]"
raise AttributeError(msg)
return self.bounds[i][1]
return self._dimension
2 changes: 1 addition & 1 deletion digneapy/core/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import reprlib
from collections.abc import Iterable
from functools import reduce
from typing import Optional, Sequence, Tuple
from typing import Optional, Tuple

import numpy as np

Expand Down
19 changes: 6 additions & 13 deletions digneapy/core/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ def __init__(
**kwargs,
):
self._name = name
self.__name__ = name
self._dimension = dimension
self._bounds = bounds
self._dtype = dtype
if len(self._bounds) != 0:
ranges = list(zip(*bounds))
self._lbs = np.array(ranges[0], dtype=dtype)
self._ubs = np.array(ranges[0], dtype=dtype)
self._ubs = np.array(ranges[1], dtype=dtype)

@property
def dimension(self):
Expand All @@ -46,17 +47,9 @@ def dimension(self):
def bounds(self):
return self._bounds

@property
def l_bounds(self):
return self._lbs

@property
def u_bounds(self):
return self.u_bounds

def get_bounds_at(self, i: int) -> tuple:
if i < 0 or i > len(self._bounds):
raise RuntimeError(
raise ValueError(
f"Index {i} out-of-range. The bounds are 0-{len(self._bounds)} "
)
return (self._lbs[i], self._ubs[i])
Expand All @@ -71,11 +64,11 @@ def create_solution(self) -> Solution:
raise NotImplementedError(msg)

@abstractmethod
def evaluate(self, individual: Sequence) -> Tuple[float]:
def evaluate(self, individual: Sequence | Solution) -> Tuple[float]:
"""Evaluates the candidate individual with the information of the Knapsack
Args:
individual (Sequence): Individual to evaluate
individual (Sequence | Solution): Individual to evaluate
Raises:
AttributeError: Raises an error if the len(individual) != len(instance) / 2
Expand All @@ -87,7 +80,7 @@ def evaluate(self, individual: Sequence) -> Tuple[float]:
raise NotImplementedError(msg)

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

Expand Down
29 changes: 14 additions & 15 deletions digneapy/domains/bin_packing.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __init__(
bounds = list((0, dim - 1) for _ in range(dim))
super().__init__(dimension=dim, bounds=bounds, name="BPP")

def evaluate(self, individual: Sequence) -> tuple[float]:
def evaluate(self, individual: Sequence | Solution) -> tuple[float]:
"""Evaluates the candidate individual with the information of the Bin Packing.
The fitness of the solution is the amount of unused space, as well as the
number of bins for a specific solution. Falkenauer (1998) performance metric
Expand All @@ -48,14 +48,17 @@ def evaluate(self, individual: Sequence) -> tuple[float]:
Returns:
Tuple[float]: Falkenauer Fitness
"""
if len(individual) != self._dimension:
chromosome = (
individual.chromosome if isinstance(individual, Solution) else individual
)
if len(chromosome) != self._dimension:
msg = f"Mismatch between individual variables and instance variables in {self.__class__.__name__}"
raise AttributeError(msg)
raise ValueError(msg)

used_bins = np.max(individual).astype(int) + 1
used_bins = np.max(chromosome).astype(int) + 1
fill_i = np.zeros(used_bins)

for item_idx, bin in enumerate(individual):
for item_idx, bin in enumerate(chromosome):
fill_i[bin] += self._items[item_idx]

fitness = (
Expand All @@ -65,7 +68,7 @@ def evaluate(self, individual: Sequence) -> tuple[float]:

return (fitness,)

def __call__(self, individual: Sequence) -> tuple[float]:
def __call__(self, individual: Sequence | Solution) -> tuple[float]:
return self.evaluate(individual)

def __repr__(self):
Expand Down Expand Up @@ -113,13 +116,13 @@ def __init__(
capacity_ratio: float = 0.8,
):
if dimension < 0:
raise RuntimeError(f"Expected dimension > 0 got {dimension}")
raise ValueError(f"Expected dimension > 0 got {dimension}")
if min_i < 0:
raise RuntimeError(f"Expected min_i > 0 got {min_i}")
raise ValueError(f"Expected min_i > 0 got {min_i}")
if max_i < 0:
raise RuntimeError(f"Expected max_i > 0 got {max_i}")
raise ValueError(f"Expected max_i > 0 got {max_i}")
if min_i > max_i:
raise RuntimeError(
raise ValueError(
f"Expected min_i to be less than max_i got ({min_i}, {max_i})"
)

Expand All @@ -143,11 +146,7 @@ def __init__(
self._capacity_approach = capacity_approach

bounds = [(self._min_i, self._max_i) for _ in range(self._dimension)]
super().__init__(
"BPP",
dimension=dimension,
bounds=bounds,
)
super().__init__(dimension=dimension, bounds=bounds, name="BPP")

@property
def capacity_approach(self):
Expand Down
Loading

0 comments on commit 6f6223b

Please sign in to comment.