@File : heuristics.py @Time : 2024/4/11 11:14:36 @Author : Alejandro Marrero @Version : 1.0 @Contact : amarrerd@ull.edu.es @License : (C)Copyright 2024, Alejandro Marrero @Desc : None

EA

Bases: Solver, SupportsSolve[P], RNG

Evolutionary Algorithm from DEAP for digneapy

Source code in digneapy/solvers/evolutionary.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
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
class EA(Solver, SupportsSolve[P], RNG):
    """Evolutionary Algorithm from DEAP for digneapy"""

    def __init__(
        self,
        direction: Direction,
        dim: int,
        min_g: int | float,
        max_g: int | float,
        cx=tools.cxUniform,
        mut=tools.mutUniformInt,
        pop_size: int = 10,
        cxpb: float = 0.6,
        mutpb: float = 0.3,
        generations: int = 500,
        n_cores: int = 1,
        seed: int = 42,
    ):
        """Creates a new EA instance with the given parameters.
        Args:
            dir (str): Direction of the evolution process. Min (minimisation) or Max (maximisation).
            dim (int): Number of variables of the problem to solve.
            min_g (int | float): Minimum value of the genome of the solutions.
            max_g (int | float): Maximum value of the genome of the solutions.
            pop_size (int, optional): Population size of the evolutionary algorithm. Defaults to 10.
            cxpb (float, optional): Crossover probability. Defaults to 0.6.
            mutpb (float, optional): Mutation probability. Defaults to 0.3.
            generations (int, optional): Number of generations to perform. Defaults to 500.

        Raises:
            TypeError: If direction is not in digneapy.solvers.DIRECTIONS
        """
        if not isinstance(direction, Direction):
            raise TypeError(
                f"Direction not allowed. Please use a value of the class Direction({Direction.values()})"
            )

        self.direction = direction
        self._cx = cx
        self._mut = mut
        self._pop_size = pop_size
        self._cxpb = cxpb
        self._mutpb = mutpb
        self._generations = generations
        self._n_cores = n_cores if n_cores > 1 else 1
        self._toolbox = base.Toolbox()
        self.initialize_rng(seed=seed)
        if direction == Direction.MINIMISE:
            self._toolbox.register(
                "individual",
                _gen_dignea_ind,
                creator.IndMin,
                self._rng,
                dim,
                min_g,
                max_g,
            )

        else:
            self._toolbox.register(
                "individual",
                _gen_dignea_ind,
                creator.IndMax,
                self._rng,
                dim,
                min_g,
                max_g,
            )

        self._toolbox.register(
            "population", tools.initRepeat, list, self._toolbox.individual
        )
        self._toolbox.register("mate", cx, indpb=0.5)
        self._toolbox.register("mutate", mut, low=min_g, up=max_g, indpb=(1.0 / dim))
        self._toolbox.register("select", tools.selTournament, tournsize=2)

        self._stats = tools.Statistics(key=lambda ind: ind.fitness.values)
        self._stats.register("avg", np.mean)
        self._stats.register("std", np.std)
        self._stats.register("min", np.min)
        self._stats.register("max", np.max)

        self._logbook = None
        self._best_found = Solution()

        self._name = f"EA_PS_{self._pop_size}_CXPB_{self._cxpb}_MUTPB_{self._mutpb}"
        self.__name__ = self._name

        if self._n_cores > 1:
            self._pool = ThreadPoolExecutor(max_workers=self._n_cores)
            self._toolbox.register("map", self._pool.map)

    def __call__(self, problem: P, *args, **kwargs) -> list[Solution]:
        """Call method of the EA solver. It runs the EA to solve the OptProblem given.

        Returns:
            Population (list[Solution]): Final population of the algorithm with the best individual found.
        """
        if problem is None:
            msg = "Problem is None in EA.__call__()"
            raise ValueError(msg)

        self._toolbox.register("evaluate", problem)
        # Reset the algorithm
        self._population = self._toolbox.population(n=self._pop_size)
        self._hof = tools.HallOfFame(1)
        self._logbook = None

        self._population, self._logbook = algorithms.eaSimple(
            self._population,
            self._toolbox,
            cxpb=self._cxpb,
            mutpb=self._mutpb,
            ngen=self._generations,
            halloffame=self._hof,
            stats=self._stats,
            verbose=False,
        )

        # Convert to Solution class
        cast_pop = [
            Solution(
                chromosome=i,
                objectives=(i.fitness.values[0],),
                fitness=i.fitness.values[0],
            )
            for i in self._population
        ]
        self._population = cast_pop
        self._best_found = Solution(
            chromosome=self._hof[0],
            objectives=(self._hof[0].fitness.values[0],),
            fitness=self._hof[0].fitness.values[0],
        )
        return [self._best_found, *cast_pop]

__call__(problem, *args, **kwargs)

Call method of the EA solver. It runs the EA to solve the OptProblem given.

Returns:
  • Population( list[Solution] ) –

    Final population of the algorithm with the best individual found.

Source code in digneapy/solvers/evolutionary.py
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
def __call__(self, problem: P, *args, **kwargs) -> list[Solution]:
    """Call method of the EA solver. It runs the EA to solve the OptProblem given.

    Returns:
        Population (list[Solution]): Final population of the algorithm with the best individual found.
    """
    if problem is None:
        msg = "Problem is None in EA.__call__()"
        raise ValueError(msg)

    self._toolbox.register("evaluate", problem)
    # Reset the algorithm
    self._population = self._toolbox.population(n=self._pop_size)
    self._hof = tools.HallOfFame(1)
    self._logbook = None

    self._population, self._logbook = algorithms.eaSimple(
        self._population,
        self._toolbox,
        cxpb=self._cxpb,
        mutpb=self._mutpb,
        ngen=self._generations,
        halloffame=self._hof,
        stats=self._stats,
        verbose=False,
    )

    # Convert to Solution class
    cast_pop = [
        Solution(
            chromosome=i,
            objectives=(i.fitness.values[0],),
            fitness=i.fitness.values[0],
        )
        for i in self._population
    ]
    self._population = cast_pop
    self._best_found = Solution(
        chromosome=self._hof[0],
        objectives=(self._hof[0].fitness.values[0],),
        fitness=self._hof[0].fitness.values[0],
    )
    return [self._best_found, *cast_pop]

__init__(direction, dim, min_g, max_g, cx=tools.cxUniform, mut=tools.mutUniformInt, pop_size=10, cxpb=0.6, mutpb=0.3, generations=500, n_cores=1, seed=42)

Creates a new EA instance with the given parameters. Args: dir (str): Direction of the evolution process. Min (minimisation) or Max (maximisation). dim (int): Number of variables of the problem to solve. min_g (int | float): Minimum value of the genome of the solutions. max_g (int | float): Maximum value of the genome of the solutions. pop_size (int, optional): Population size of the evolutionary algorithm. Defaults to 10. cxpb (float, optional): Crossover probability. Defaults to 0.6. mutpb (float, optional): Mutation probability. Defaults to 0.3. generations (int, optional): Number of generations to perform. Defaults to 500.

Raises:
  • TypeError

    If direction is not in digneapy.solvers.DIRECTIONS

Source code in digneapy/solvers/evolutionary.py
 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
def __init__(
    self,
    direction: Direction,
    dim: int,
    min_g: int | float,
    max_g: int | float,
    cx=tools.cxUniform,
    mut=tools.mutUniformInt,
    pop_size: int = 10,
    cxpb: float = 0.6,
    mutpb: float = 0.3,
    generations: int = 500,
    n_cores: int = 1,
    seed: int = 42,
):
    """Creates a new EA instance with the given parameters.
    Args:
        dir (str): Direction of the evolution process. Min (minimisation) or Max (maximisation).
        dim (int): Number of variables of the problem to solve.
        min_g (int | float): Minimum value of the genome of the solutions.
        max_g (int | float): Maximum value of the genome of the solutions.
        pop_size (int, optional): Population size of the evolutionary algorithm. Defaults to 10.
        cxpb (float, optional): Crossover probability. Defaults to 0.6.
        mutpb (float, optional): Mutation probability. Defaults to 0.3.
        generations (int, optional): Number of generations to perform. Defaults to 500.

    Raises:
        TypeError: If direction is not in digneapy.solvers.DIRECTIONS
    """
    if not isinstance(direction, Direction):
        raise TypeError(
            f"Direction not allowed. Please use a value of the class Direction({Direction.values()})"
        )

    self.direction = direction
    self._cx = cx
    self._mut = mut
    self._pop_size = pop_size
    self._cxpb = cxpb
    self._mutpb = mutpb
    self._generations = generations
    self._n_cores = n_cores if n_cores > 1 else 1
    self._toolbox = base.Toolbox()
    self.initialize_rng(seed=seed)
    if direction == Direction.MINIMISE:
        self._toolbox.register(
            "individual",
            _gen_dignea_ind,
            creator.IndMin,
            self._rng,
            dim,
            min_g,
            max_g,
        )

    else:
        self._toolbox.register(
            "individual",
            _gen_dignea_ind,
            creator.IndMax,
            self._rng,
            dim,
            min_g,
            max_g,
        )

    self._toolbox.register(
        "population", tools.initRepeat, list, self._toolbox.individual
    )
    self._toolbox.register("mate", cx, indpb=0.5)
    self._toolbox.register("mutate", mut, low=min_g, up=max_g, indpb=(1.0 / dim))
    self._toolbox.register("select", tools.selTournament, tournsize=2)

    self._stats = tools.Statistics(key=lambda ind: ind.fitness.values)
    self._stats.register("avg", np.mean)
    self._stats.register("std", np.std)
    self._stats.register("min", np.min)
    self._stats.register("max", np.max)

    self._logbook = None
    self._best_found = Solution()

    self._name = f"EA_PS_{self._pop_size}_CXPB_{self._cxpb}_MUTPB_{self._mutpb}"
    self.__name__ = self._name

    if self._n_cores > 1:
        self._pool = ThreadPoolExecutor(max_workers=self._n_cores)
        self._toolbox.register("map", self._pool.map)

ParEAKP

Parallel Evolutionary Algorithm for Knapsack Problems It uses Uniform One Mutation and Uniform Mutation as mating operators. The replacement is based on a Greedy strategy. The parent and offspring populations are evaluated pairwise and at each position i, the best individual between parent_i and offspring_i survives for the next_population_i.

Source code in digneapy/solvers/evolutionary.py
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
class ParEAKP:
    """Parallel Evolutionary Algorithm for Knapsack Problems
    It uses Uniform One Mutation and Uniform Mutation as mating operators.
    The replacement is based on a Greedy strategy. The parent and offspring
    populations are evaluated pairwise and at each position i, the best individual
    between parent_i and offspring_i survives for the next_population_i.
    """

    def __init__(
        self,
        pop_size: int = 32,
        generations: int = 1000,
        mutpb: float = 0.2,
        cxpb: float = 0.7,
        cores: int = 1,
    ):
        """Creates an instance of the ParEAKP solver

        Args:
            pop_size (int, optional): Population size. Defaults to 32.
            generations (int, optional): Number of generations to perform. Defaults to 1000.
            mutpb (float, optional): Probability of mutation. Defaults to 0.2.
            cxpb (float, optional): Probability of crossover between two individuals. Defaults to 0.7.
            cores (int, optional): Number of cores to use. Defaults to 1.
        """
        from parallel_ea import _ParEACpp

        self._pop_size = pop_size
        self._generations = generations
        self._mutpb = mutpb
        self._cxpb = cxpb
        self._n_cores = cores
        self.__name__ = (
            f"ParEAKP_PS_{self._pop_size}_CXPB_{self._cxpb}_MUTPB_{self._mutpb}"
        )
        self._runner = _ParEACpp(
            self._pop_size, self._generations, self._mutpb, self._cxpb, self._n_cores
        )

    def __call__(self, problem: Knapsack, *args, **kwargs) -> list[Solution]:
        """Runs the algorithm to solve the KP problem

        Args:
            kp (Knapsack, optional): Instance of a KP. Defaults to None.

        Raises:
            ValueError: If no instance is given

        Returns:
            List[Solution]: Best solution found by the algorithm
        """
        if problem is None:
            msg = "Knapsack Problem is None in ParEAKP.__call__(). Expected a Knapsack instance."
            raise ValueError(msg)

        x, fitness = self._runner.run(
            len(problem), problem.weights, problem.profits, problem.capacity
        )
        return [Solution(chromosome=x, objectives=(fitness,), fitness=fitness)]

__call__(problem, *args, **kwargs)

Runs the algorithm to solve the KP problem

Parameters:
  • kp (Knapsack) –

    Instance of a KP. Defaults to None.

Raises:
  • ValueError

    If no instance is given

Returns:
  • list[Solution]

    List[Solution]: Best solution found by the algorithm

Source code in digneapy/solvers/evolutionary.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
def __call__(self, problem: Knapsack, *args, **kwargs) -> list[Solution]:
    """Runs the algorithm to solve the KP problem

    Args:
        kp (Knapsack, optional): Instance of a KP. Defaults to None.

    Raises:
        ValueError: If no instance is given

    Returns:
        List[Solution]: Best solution found by the algorithm
    """
    if problem is None:
        msg = "Knapsack Problem is None in ParEAKP.__call__(). Expected a Knapsack instance."
        raise ValueError(msg)

    x, fitness = self._runner.run(
        len(problem), problem.weights, problem.profits, problem.capacity
    )
    return [Solution(chromosome=x, objectives=(fitness,), fitness=fitness)]

__init__(pop_size=32, generations=1000, mutpb=0.2, cxpb=0.7, cores=1)

Creates an instance of the ParEAKP solver

Parameters:
  • pop_size (int, default: 32 ) –

    Population size. Defaults to 32.

  • generations (int, default: 1000 ) –

    Number of generations to perform. Defaults to 1000.

  • mutpb (float, default: 0.2 ) –

    Probability of mutation. Defaults to 0.2.

  • cxpb (float, default: 0.7 ) –

    Probability of crossover between two individuals. Defaults to 0.7.

  • cores (int, default: 1 ) –

    Number of cores to use. Defaults to 1.

Source code in digneapy/solvers/evolutionary.py
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
def __init__(
    self,
    pop_size: int = 32,
    generations: int = 1000,
    mutpb: float = 0.2,
    cxpb: float = 0.7,
    cores: int = 1,
):
    """Creates an instance of the ParEAKP solver

    Args:
        pop_size (int, optional): Population size. Defaults to 32.
        generations (int, optional): Number of generations to perform. Defaults to 1000.
        mutpb (float, optional): Probability of mutation. Defaults to 0.2.
        cxpb (float, optional): Probability of crossover between two individuals. Defaults to 0.7.
        cores (int, optional): Number of cores to use. Defaults to 1.
    """
    from parallel_ea import _ParEACpp

    self._pop_size = pop_size
    self._generations = generations
    self._mutpb = mutpb
    self._cxpb = cxpb
    self._n_cores = cores
    self.__name__ = (
        f"ParEAKP_PS_{self._pop_size}_CXPB_{self._cxpb}_MUTPB_{self._mutpb}"
    )
    self._runner = _ParEACpp(
        self._pop_size, self._generations, self._mutpb, self._cxpb, self._n_cores
    )