Skip to content

Commit

Permalink
EMO changes.
Browse files Browse the repository at this point in the history
- Added random integer mutation operator and tested it.
  • Loading branch information
gialmisi committed Dec 4, 2024
1 parent 3f6c4bc commit 2599892
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 1 deletion.
136 changes: 135 additions & 1 deletion desdeo/emo/operators/mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,141 @@ def do(self, offsprings: pl.DataFrame, parents: pl.DataFrame) -> pl.DataFrame:
return self.offspring

def update(self, *_, **__):
"""Do nothing. This is just the basic polynomial mutation operator."""
"""Do nothing."""

def state(self) -> Sequence[Message]:
"""Return the state of the mutation operator."""
if self.offspring_original is None or self.offspring is None:
return []
if self.verbosity == 0:
return []
if self.verbosity == 1:
return [
FloatMessage(
topic=MutationMessageTopics.MUTATION_PROBABILITY,
source=self.__class__.__name__,
value=self.mutation_probability,
),
]
# verbosity == 2
return [
PolarsDataFrameMessage(
topic=MutationMessageTopics.OFFSPRING_ORIGINAL,
source=self.__class__.__name__,
value=self.offspring_original,
),
PolarsDataFrameMessage(
topic=MutationMessageTopics.PARENTS,
source=self.__class__.__name__,
value=self.parents,
),
PolarsDataFrameMessage(
topic=MutationMessageTopics.OFFSPRINGS,
source=self.__class__.__name__,
value=self.offspring,
),
FloatMessage(
topic=MutationMessageTopics.MUTATION_PROBABILITY,
source=self.__class__.__name__,
value=self.mutation_probability,
),
]


class IntegerRandomMutation(BaseMutation):
"""Implements a random mutation operator for integer variables.
The mutation will mutate each binary integer variable,
by changing its value to a random value bounded by the
variable's bounds with a provided probability.
"""

@property
def provided_topics(self) -> dict[int, Sequence[MutationMessageTopics]]:
"""The message topics provided by the mutation operator."""
return {
0: [],
1: [
MutationMessageTopics.MUTATION_PROBABILITY,
],
2: [
MutationMessageTopics.MUTATION_PROBABILITY,
MutationMessageTopics.OFFSPRING_ORIGINAL,
MutationMessageTopics.OFFSPRINGS,
],
}

@property
def interested_topics(self):
"""The message topics that the mutation operator is interested in."""
return []

def __init__(
self,
*,
problem: Problem,
seed: int,
mutation_probability: float | None = None,
**kwargs,
):
"""Initialize a random integer mutation operator.
Args:
problem (Problem): The problem object.
seed (int): The seed for the random number generator.
mutation_probability (float | None, optional): The probability of mutation. If None,
the probability will be set to be 1/n, where n is the number of decision variables
in the problem. Defaults to None.
kwargs: Additional keyword arguments. These are passed to the Subscriber class. At the very least, the
publisher must be passed. See the Subscriber class for more information.
"""
super().__init__(problem, **kwargs)

if self.variable_combination != VariableDomainTypeEnum.integer:
raise ValueError("This mutation operator only works with integer variables.")
if mutation_probability is None:
self.mutation_probability = 1 / len(self.variable_symbols)
else:
self.mutation_probability = mutation_probability

self.rng = np.random.default_rng(seed)
self.seed = seed
self.offspring_original: pl.DataFrame
self.parents: pl.DataFrame
self.offspring: pl.DataFrame

def do(self, offsprings: pl.DataFrame, parents: pl.DataFrame) -> pl.DataFrame:
"""Perform the random integer mutation operation.
Args:
offsprings (pl.DataFrame): the offspring population to mutate.
parents (pl.DataFrame): the parent population from which the offspring
was generated (via crossover). Not used in the mutation operator.
Returns:
pl.DataFrame: the offspring resulting from the mutation.
"""
self.offspring_original = copy.copy(offsprings)
self.parents = parents # Not used, but kept for consistency

population = offsprings.to_numpy().astype(int)

# create a boolean mask based on the mutation probability
mutation_mask = self.rng.random(population.shape) < self.mutation_probability

mutated = np.where(
mutation_mask,
self.rng.integers(self.lower_bounds, self.upper_bounds, size=population.shape, dtype=int, endpoint=True),
population,
)

self.offspring = pl.from_numpy(mutated, schema=self.variable_symbols).select(pl.all()).cast(pl.Float64)
self.notify()

return self.offspring

def update(self, *_, **__):
"""Do nothing."""

def state(self) -> Sequence[Message]:
"""Return the state of the mutation operator."""
Expand Down
39 changes: 39 additions & 0 deletions tests/test_ea.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from desdeo.emo.operators.mutation import (
BinaryFlipMutation,
BoundedPolynomialMutation,
IntegerRandomMutation,
)
from desdeo.emo.operators.selection import (
ParameterAdaptationStrategy,
Expand Down Expand Up @@ -384,3 +385,41 @@ def test_uniform_integer_crossover():

with npt.assert_raises(AssertionError):
npt.assert_allclose(population, result)


@pytest.mark.ea
def test_integer_random_mutation():
"""Test whether the integer random mutation operator works as intended."""
publisher = Publisher()

problem = simple_integer_test_problem()

# default mutation probability
mutation = IntegerRandomMutation(problem=problem, publisher=publisher, seed=0)
num_vars = len(mutation.variable_symbols)

population = pl.DataFrame(
mutation.rng.integers(0, 10, size=(10, num_vars), endpoint=True),
schema=mutation.variable_symbols,
)

result = mutation.do(offsprings=population, parents=population)

assert result.shape == population.shape

with npt.assert_raises(AssertionError):
npt.assert_allclose(population, result)

# zero mutation probability
mutation = IntegerRandomMutation(problem=problem, publisher=publisher, seed=0, mutation_probability=0.0)

population = pl.DataFrame(
mutation.rng.integers(0, 10, size=(10, num_vars), endpoint=True),
schema=mutation.variable_symbols,
)

result = mutation.do(offsprings=population, parents=population)

assert result.shape == population.shape

npt.assert_allclose(population, result)

0 comments on commit 2599892

Please sign in to comment.