Skip to content

Mutations

Executor

hgp_lib.mutations.mutation_executor.MutationExecutor

Coordinates literal and operator mutations across a collection of Rule trees.

The executor samples how many nodes of every rule should be mutated based on mutation_p, picks concrete nodes uniformly at random, and retries failed mutations up to num_tries times when a check_valid predicate is provided.

Parameters:

Name Type Description Default
literal_mutations Tuple[Mutation, ...]

Mutations that can be applied to literal nodes. The sequence must be non-empty and each entry must declare is_literal_mutation=True.

required
operator_mutations Tuple[Mutation, ...]

Mutations that can be applied to operator nodes. The sequence must be non-empty and each entry must declare is_operator_mutation=True.

required
mutation_p float

Probability of mutating each node inside a rule. Default: 0.1.

0.1
check_valid Callable[[Rule], bool] | None

Optional validator executed after every successful mutation. When supplied, the mutated rule is only kept if the predicate returns True. Default: None.

None
num_tries int

Maximum number of attempts per node in case mutations raise MutationError or fail validation. Must be 1 when no validator is provided. Default: 1.

1
operator_p float

Probability of selecting an operator node (vs. a literal) when choosing a mutation point in the rule tree. Must be in [0.0, 1.0]. Default: 0.9.

0.5

Examples:

>>> import random
>>> import numpy as np
>>> from hgp_lib.mutations import MutationExecutor, NegateMutation
>>> from hgp_lib.rules import Literal, And
>>> random.seed(1); np.random.seed(0)
>>> executor = MutationExecutor(
...     literal_mutations=[NegateMutation()],
...     operator_mutations=[NegateMutation()],
...     mutation_p=1.0,
...     operator_p=0.5,
... )
>>> rules = [Literal(value=0), And([Literal(value=0), Literal(value=1)])]
>>> executor.apply(rules)
>>> str(rules[0])
'~0'
>>> str(rules[1])
'~And(~0, ~1)'
Source code in hgp_lib\mutations\mutation_executor.py
 12
 13
 14
 15
 16
 17
 18
 19
 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
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
class MutationExecutor:
    """
    Coordinates literal and operator mutations across a collection of `Rule` trees.

    The executor samples how many nodes of every rule should be mutated based on `mutation_p`,
    picks concrete nodes uniformly at random, and retries failed mutations up to `num_tries`
    times when a `check_valid` predicate is provided.

    Args:
        literal_mutations (Tuple[Mutation, ...]):
            Mutations that can be applied to literal nodes. The sequence must be non-empty and
            each entry must declare `is_literal_mutation=True`.
        operator_mutations (Tuple[Mutation, ...]):
            Mutations that can be applied to operator nodes. The sequence must be non-empty and
            each entry must declare `is_operator_mutation=True`.
        mutation_p (float):
            Probability of mutating each node inside a rule. Default: `0.1`.
        check_valid (Callable[[Rule], bool] | None):
            Optional validator executed after every successful mutation. When supplied, the
            mutated rule is only kept if the predicate returns `True`. Default: `None`.
        num_tries (int):
            Maximum number of attempts per node in case mutations raise `MutationError` or fail
            validation. Must be `1` when no validator is provided. Default: `1`.
        operator_p (float):
            Probability of selecting an operator node (vs. a literal) when choosing a mutation
            point in the rule tree. Must be in [0.0, 1.0]. Default: `0.9`.

    Examples:
        >>> import random
        >>> import numpy as np
        >>> from hgp_lib.mutations import MutationExecutor, NegateMutation
        >>> from hgp_lib.rules import Literal, And
        >>> random.seed(1); np.random.seed(0)
        >>> executor = MutationExecutor(
        ...     literal_mutations=[NegateMutation()],
        ...     operator_mutations=[NegateMutation()],
        ...     mutation_p=1.0,
        ...     operator_p=0.5,
        ... )
        >>> rules = [Literal(value=0), And([Literal(value=0), Literal(value=1)])]
        >>> executor.apply(rules)
        >>> str(rules[0])
        '~0'
        >>> str(rules[1])
        '~And(~0, ~1)'
    """

    def __init__(
        self,
        literal_mutations: Tuple[Mutation, ...],
        operator_mutations: Tuple[Mutation, ...],
        mutation_p: float = 0.1,
        check_valid: Callable[[Rule], bool] | None = None,
        num_tries: int = 1,
        operator_p: float = 0.5,
    ):
        # mutation_p was checked
        # check_valid was checked
        # num_tries was checked
        # operator_p was checked
        # literal_mutations was checked
        # operator_mutations was checked
        self.mutation_p: float = mutation_p
        self.literal_mutations: Tuple[Mutation, ...] = literal_mutations
        self.operator_mutations: Tuple[Mutation, ...] = operator_mutations
        self.check_valid: Callable[[Rule], bool] | None = check_valid
        self.num_tries: int = num_tries
        self.operator_p: float = operator_p

    def apply(self, rules: List[Rule]):
        """
        Mutates the provided list of rules in place.

        In the event of a mutation failure, the original rule is kept in place.

        Args:
            rules (List[Rule]):
                The mutable collection of rules that will be potentially replaced by mutated
                versions depending on `mutation_p`.

        Examples:
            >>> import random
            >>> import numpy as np
            >>> from hgp_lib.mutations import MutationExecutor, NegateMutation
            >>> from hgp_lib.rules import Literal
            >>> random.seed(1); np.random.seed(1)
            >>> executor = MutationExecutor(
            ...     literal_mutations=[NegateMutation()],
            ...     operator_mutations=[NegateMutation()],
            ...     mutation_p=1.0,
            ... )
            >>> rules = [Literal(value=0)]
            >>> executor.apply(rules)
            >>> str(rules[0])
            '~0'
        """
        for i in range(len(rules)):
            rule = rules[i]
            n_mutations = np.random.binomial(len(rule), self.mutation_p)
            if n_mutations != 0:
                rules[i] = self._mutate(rule, n_mutations)

    def _mutate(self, rule: Rule, n_mutations: int) -> Rule:
        """
        Apply ``n_mutations`` point mutations to a copy of ``rule``.

        For each mutation attempt a random node is selected (biased by ``operator_p``),
        a random mutation from the appropriate set (literal or operator) is applied, and
        the result is validated if ``check_valid`` is set. Failed attempts (``MutationError``
        or validation failure) are retried up to ``num_tries`` times per mutation slot.
        If all retries fail for a given slot, the last valid state is kept.

        Args:
            rule (Rule):
                The rule to mutate. A deep copy is made internally; the original is not
                modified.
            n_mutations (int):
                Number of independent point mutations to attempt on the copy.

        Returns:
            Rule: The mutated copy, or the original ``rule`` if every mutation failed.

        Examples:
            >>> import random
            >>> import numpy as np
            >>> from hgp_lib.mutations import MutationExecutor, NegateMutation
            >>> from hgp_lib.rules import Literal
            >>> random.seed(0); np.random.seed(0)
            >>> executor = MutationExecutor(
            ...     literal_mutations=[NegateMutation()],
            ...     operator_mutations=[NegateMutation()],
            ...     mutation_p=1.0,
            ... )
            >>> rule = Literal(value=0)
            >>> result = executor._mutate(rule, 1)
            >>> str(result)
            '~0'
            >>> str(rule)
            '0'
        """
        new_rule = rule.copy()

        last_mutation = n_mutations - 1
        last_try = self.num_tries - 1

        for mutation_i in range(n_mutations):
            for tries in range(self.num_tries):
                selected = select_crossover_point(new_rule, operator_p=self.operator_p)
                # selected = random.choice(new_rule.flatten())

                try:
                    random.choice(
                        self.literal_mutations
                        if isinstance(selected, Literal)
                        else self.operator_mutations
                    ).apply(selected)
                except MutationError:
                    continue

                if self.check_valid is None or self.check_valid(new_rule):
                    rule = new_rule
                    break
                elif mutation_i != last_mutation or tries != last_try:
                    new_rule = rule.copy()
        return rule

apply(rules)

Mutates the provided list of rules in place.

In the event of a mutation failure, the original rule is kept in place.

Parameters:

Name Type Description Default
rules List[Rule]

The mutable collection of rules that will be potentially replaced by mutated versions depending on mutation_p.

required

Examples:

>>> import random
>>> import numpy as np
>>> from hgp_lib.mutations import MutationExecutor, NegateMutation
>>> from hgp_lib.rules import Literal
>>> random.seed(1); np.random.seed(1)
>>> executor = MutationExecutor(
...     literal_mutations=[NegateMutation()],
...     operator_mutations=[NegateMutation()],
...     mutation_p=1.0,
... )
>>> rules = [Literal(value=0)]
>>> executor.apply(rules)
>>> str(rules[0])
'~0'
Source code in hgp_lib\mutations\mutation_executor.py
 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
def apply(self, rules: List[Rule]):
    """
    Mutates the provided list of rules in place.

    In the event of a mutation failure, the original rule is kept in place.

    Args:
        rules (List[Rule]):
            The mutable collection of rules that will be potentially replaced by mutated
            versions depending on `mutation_p`.

    Examples:
        >>> import random
        >>> import numpy as np
        >>> from hgp_lib.mutations import MutationExecutor, NegateMutation
        >>> from hgp_lib.rules import Literal
        >>> random.seed(1); np.random.seed(1)
        >>> executor = MutationExecutor(
        ...     literal_mutations=[NegateMutation()],
        ...     operator_mutations=[NegateMutation()],
        ...     mutation_p=1.0,
        ... )
        >>> rules = [Literal(value=0)]
        >>> executor.apply(rules)
        >>> str(rules[0])
        '~0'
    """
    for i in range(len(rules)):
        rule = rules[i]
        n_mutations = np.random.binomial(len(rule), self.mutation_p)
        if n_mutations != 0:
            rules[i] = self._mutate(rule, n_mutations)

Factory

hgp_lib.mutations.mutation_factory.MutationExecutorFactory

Factory for creating MutationExecutor instances.

Stores configuration-time parameters (mutation_p, num_tries) and defers data-dependent construction to create. Override create_literal_mutations and/or create_operator_mutations to customize which mutations are used.

Attributes:

Name Type Description
mutation_p float

Per-node mutation probability. Default: 0.1.

num_tries int

Maximum number of attempts per mutation node. Must be 1 when no check_valid is provided to create. Default: 1.

operator_p float

Probability of selecting an operator node (vs. a literal) when choosing a mutation point in the rule tree. Must be in [0.0, 1.0]. Default: 0.9.

operator_types Sequence[Type[Rule]]

Sequence of operator classes (e.g., (Or, And)) used by mutations. Default: (Or, And).

Examples:

>>> from hgp_lib.mutations import MutationExecutorFactory
>>> factory = MutationExecutorFactory(mutation_p=0.05)
>>> factory.mutation_p
0.05
>>> factory.num_tries
1

Subclass to use custom mutations:

>>> from hgp_lib.mutations import MutationExecutorFactory, NegateMutation
>>> class NegateOnlyFactory(MutationExecutorFactory):
...     def create_literal_mutations(self, num_literals):
...         return (NegateMutation(),)
...     def create_operator_mutations(self, num_literals):
...         return (NegateMutation(),)
>>> factory = NegateOnlyFactory(mutation_p=1.0)
>>> executor = factory.create(num_literals=4)
>>> len(executor.literal_mutations)
1
Source code in hgp_lib\mutations\mutation_factory.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
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
class MutationExecutorFactory:
    """
    Factory for creating `MutationExecutor` instances.

    Stores configuration-time parameters (`mutation_p`, `num_tries`) and defers
    data-dependent construction to `create`. Override `create_literal_mutations`
    and/or `create_operator_mutations` to customize which mutations are used.

    Attributes:
        mutation_p (float): Per-node mutation probability. Default: `0.1`.
        num_tries (int): Maximum number of attempts per mutation node. Must be `1`
            when no `check_valid` is provided to `create`. Default: `1`.
        operator_p (float): Probability of selecting an operator node (vs. a literal)
            when choosing a mutation point in the rule tree. Must be in [0.0, 1.0].
            Default: `0.9`.
        operator_types (Sequence[Type[Rule]]): Sequence of operator classes
            (e.g., `(Or, And)`) used by mutations. Default: `(Or, And)`.

    Examples:
        >>> from hgp_lib.mutations import MutationExecutorFactory
        >>> factory = MutationExecutorFactory(mutation_p=0.05)
        >>> factory.mutation_p
        0.05
        >>> factory.num_tries
        1

        Subclass to use custom mutations:

        >>> from hgp_lib.mutations import MutationExecutorFactory, NegateMutation
        >>> class NegateOnlyFactory(MutationExecutorFactory):
        ...     def create_literal_mutations(self, num_literals):
        ...         return (NegateMutation(),)
        ...     def create_operator_mutations(self, num_literals):
        ...         return (NegateMutation(),)
        >>> factory = NegateOnlyFactory(mutation_p=1.0)
        >>> executor = factory.create(num_literals=4)
        >>> len(executor.literal_mutations)
        1
    """

    def __init__(
        self,
        mutation_p: float = 0.1,
        num_tries: int = 1,
        operator_p: float = 0.5,
        operator_types: Sequence[Type[Rule]] = (Or, And),
    ):
        check_isinstance(mutation_p, float)
        if mutation_p < 0.0 or mutation_p > 1.0:
            raise ValueError(
                f"mutation_p must be between 0.0 and 1.0, got {mutation_p}"
            )
        check_isinstance(num_tries, int)
        if num_tries < 1:
            raise ValueError(f"num_tries must be at least 1, got {num_tries}")
        check_isinstance(operator_p, float)
        if operator_p < 0.0 or operator_p > 1.0:
            raise ValueError(
                f"operator_p must be between 0.0 and 1.0, got {operator_p}"
            )
        validate_operator_types(operator_types)

        self.mutation_p = mutation_p
        self.num_tries = num_tries
        self.operator_p = operator_p
        self.operator_types: Tuple[Type[Rule], ...] = tuple(operator_types)

    def create_literal_mutations(self, num_literals: int) -> Tuple[Mutation, ...]:
        """
        Create the set of standard literal-level mutations. Override this method to use custom literal mutations.

        Args:
            num_literals (int):
                Total number of available literal values. Must be greater than `1`.

        Returns:
            Tuple[Mutation, ...]: Literal mutations for the executor.

        Examples:
            >>> from hgp_lib.mutations import MutationExecutorFactory
            >>> from hgp_lib.rules import And, Or
            >>> mutations = MutationExecutorFactory().create_literal_mutations(num_literals=4)
            >>> [type(mutation).__name__ for mutation in mutations]
            ['DeleteMutation', 'NegateMutation', 'ReplaceLiteral', 'PromoteLiteral']
        """
        return (
            DeleteMutation(),
            NegateMutation(),
            ReplaceLiteral(num_literals),
            PromoteLiteral(num_literals, self.operator_types),
        )

    def create_operator_mutations(self, num_literals: int) -> Tuple[Mutation, ...]:
        """
        Create the set of standard operator-level mutations. Override this method to use custom operator mutations.

        Args:
            num_literals (int): Total number of available literal values.

        Returns:
            Tuple[Mutation, ...]: Operator mutations for the executor.

            Examples:
            >>> from hgp_lib.mutations import MutationExecutorFactory
            >>> from hgp_lib.rules import And, Or
            >>> mutations = MutationExecutorFactory().create_operator_mutations(num_literals=4)
            >>> [type(mutation).__name__ for mutation in mutations]
            ['DeleteMutation', 'NegateMutation', 'RemoveIntermediateOperator', 'ReplaceOperator', 'AddLiteral']
        """
        return (
            DeleteMutation(),
            NegateMutation(),
            RemoveIntermediateOperator(),
            ReplaceOperator(self.operator_types),
            AddLiteral(num_literals),
        )

    @staticmethod
    def _validate_mutations(
        literal_mutations: Tuple[Mutation, ...],
        operator_mutations: Tuple[Mutation, ...],
    ):
        check_isinstance(literal_mutations, Tuple)
        check_isinstance(operator_mutations, Tuple)

        if len(literal_mutations) == 0:
            raise ValueError("literal_mutations must be a non-empty Tuple")
        if len(operator_mutations) == 0:
            raise ValueError("operator_mutations must be a non-empty Tuple")

        for literal_mutation in literal_mutations:
            check_isinstance(literal_mutation, Mutation)
            if not literal_mutation.is_literal_mutation:
                raise TypeError(
                    f"Each literal_mutations must be a literal mutation, but '{type(literal_mutation)} is not'"
                )
        for operator_mutation in operator_mutations:
            check_isinstance(operator_mutation, Mutation)
            if not operator_mutation.is_operator_mutation:
                raise TypeError(
                    f"Each operator_mutations must be an operator mutation, but '{type(operator_mutation)} is not'"
                )

    def create(
        self,
        num_literals: int,
        check_valid: Callable[[Rule], bool] | None = None,
    ) -> MutationExecutor:
        """
        Create a `MutationExecutor` with the configured mutations and parameters.

        Args:
            num_literals (int): Total number of available literal values.
            check_valid (Callable[[Rule], bool] | None): Optional rule validator.
                Default: `None`.

        Returns:
            MutationExecutor: Configured mutation executor.
        """
        validate_num_literals(num_literals)

        if check_valid is None and self.num_tries > 1:
            raise ValueError("num_tries must be 1 if check_valid is None")

        literal_mutations = self.create_literal_mutations(num_literals)
        operator_mutations = self.create_operator_mutations(num_literals)

        self._validate_mutations(
            literal_mutations,
            operator_mutations,
        )

        return MutationExecutor(
            literal_mutations=literal_mutations,
            operator_mutations=operator_mutations,
            mutation_p=self.mutation_p,
            check_valid=check_valid,
            num_tries=self.num_tries,
            operator_p=self.operator_p,
        )

create_literal_mutations(num_literals)

Create the set of standard literal-level mutations. Override this method to use custom literal mutations.

Parameters:

Name Type Description Default
num_literals int

Total number of available literal values. Must be greater than 1.

required

Returns:

Type Description
Tuple[Mutation, ...]

Tuple[Mutation, ...]: Literal mutations for the executor.

Examples:

>>> from hgp_lib.mutations import MutationExecutorFactory
>>> from hgp_lib.rules import And, Or
>>> mutations = MutationExecutorFactory().create_literal_mutations(num_literals=4)
>>> [type(mutation).__name__ for mutation in mutations]
['DeleteMutation', 'NegateMutation', 'ReplaceLiteral', 'PromoteLiteral']
Source code in hgp_lib\mutations\mutation_factory.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def create_literal_mutations(self, num_literals: int) -> Tuple[Mutation, ...]:
    """
    Create the set of standard literal-level mutations. Override this method to use custom literal mutations.

    Args:
        num_literals (int):
            Total number of available literal values. Must be greater than `1`.

    Returns:
        Tuple[Mutation, ...]: Literal mutations for the executor.

    Examples:
        >>> from hgp_lib.mutations import MutationExecutorFactory
        >>> from hgp_lib.rules import And, Or
        >>> mutations = MutationExecutorFactory().create_literal_mutations(num_literals=4)
        >>> [type(mutation).__name__ for mutation in mutations]
        ['DeleteMutation', 'NegateMutation', 'ReplaceLiteral', 'PromoteLiteral']
    """
    return (
        DeleteMutation(),
        NegateMutation(),
        ReplaceLiteral(num_literals),
        PromoteLiteral(num_literals, self.operator_types),
    )

create_operator_mutations(num_literals)

Create the set of standard operator-level mutations. Override this method to use custom operator mutations.

Parameters:

Name Type Description Default
num_literals int

Total number of available literal values.

required

Returns:

Name Type Description
Mutation

Tuple[Mutation, ...]: Operator mutations for the executor.

Examples ...
Tuple[Mutation, ...]

from hgp_lib.mutations import MutationExecutorFactory

Tuple[Mutation, ...]

from hgp_lib.rules import And, Or

Tuple[Mutation, ...]

mutations = MutationExecutorFactory().create_operator_mutations(num_literals=4)

Tuple[Mutation, ...]

[type(mutation).name for mutation in mutations]

Tuple[Mutation, ...]

['DeleteMutation', 'NegateMutation', 'RemoveIntermediateOperator', 'ReplaceOperator', 'AddLiteral']

Source code in hgp_lib\mutations\mutation_factory.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def create_operator_mutations(self, num_literals: int) -> Tuple[Mutation, ...]:
    """
    Create the set of standard operator-level mutations. Override this method to use custom operator mutations.

    Args:
        num_literals (int): Total number of available literal values.

    Returns:
        Tuple[Mutation, ...]: Operator mutations for the executor.

        Examples:
        >>> from hgp_lib.mutations import MutationExecutorFactory
        >>> from hgp_lib.rules import And, Or
        >>> mutations = MutationExecutorFactory().create_operator_mutations(num_literals=4)
        >>> [type(mutation).__name__ for mutation in mutations]
        ['DeleteMutation', 'NegateMutation', 'RemoveIntermediateOperator', 'ReplaceOperator', 'AddLiteral']
    """
    return (
        DeleteMutation(),
        NegateMutation(),
        RemoveIntermediateOperator(),
        ReplaceOperator(self.operator_types),
        AddLiteral(num_literals),
    )

create(num_literals, check_valid=None)

Create a MutationExecutor with the configured mutations and parameters.

Parameters:

Name Type Description Default
num_literals int

Total number of available literal values.

required
check_valid Callable[[Rule], bool] | None

Optional rule validator. Default: None.

None

Returns:

Name Type Description
MutationExecutor MutationExecutor

Configured mutation executor.

Source code in hgp_lib\mutations\mutation_factory.py
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
def create(
    self,
    num_literals: int,
    check_valid: Callable[[Rule], bool] | None = None,
) -> MutationExecutor:
    """
    Create a `MutationExecutor` with the configured mutations and parameters.

    Args:
        num_literals (int): Total number of available literal values.
        check_valid (Callable[[Rule], bool] | None): Optional rule validator.
            Default: `None`.

    Returns:
        MutationExecutor: Configured mutation executor.
    """
    validate_num_literals(num_literals)

    if check_valid is None and self.num_tries > 1:
        raise ValueError("num_tries must be 1 if check_valid is None")

    literal_mutations = self.create_literal_mutations(num_literals)
    operator_mutations = self.create_operator_mutations(num_literals)

    self._validate_mutations(
        literal_mutations,
        operator_mutations,
    )

    return MutationExecutor(
        literal_mutations=literal_mutations,
        operator_mutations=operator_mutations,
        mutation_p=self.mutation_p,
        check_valid=check_valid,
        num_tries=self.num_tries,
        operator_p=self.operator_p,
    )

Literal Mutations

hgp_lib.mutations.literal_mutations.DeleteMutation

Bases: Mutation

Remove a Rule node from its parent operator in-place.

Applicable to both literal and operator nodes. The mutation has two modes depending on the parent's subrule count:

  • Parent has 3+ subrules: the target node is simply removed from the parent's subrule list.
  • Parent has exactly 2 subrules (collapse): the remaining sibling is appended to the grandparent's subrule list and the now-redundant parent operator is removed from the grandparent. This requires a grandparent to exist; if there is none, a MutationError is raised.

A MutationError is also raised when the target node is the root of the tree (has no parent).

Attributes:

Name Type Description
is_literal_mutation bool

True.

is_operator_mutation bool

True.

Examples:

Simple removal (parent has 3 subrules):

>>> from hgp_lib.mutations import DeleteMutation
>>> from hgp_lib.rules import And, Literal
>>> parent = And([Literal(value=0), Literal(value=1), Literal(value=2)])
>>> mutation = DeleteMutation()
>>> mutation.apply(parent.subrules[1])
>>> parent
And(0, 2)

Collapse (parent has 2 subrules — sibling is appended to grandparent):

>>> from hgp_lib.rules import Or
>>> root = Or([
...     Literal(value=0),
...     And([Literal(value=1), Literal(value=2)]),
... ])
>>> mutation.apply(root.subrules[1].subrules[0])
>>> root
Or(0, 2)
Source code in hgp_lib\mutations\literal_mutations.py
 11
 12
 13
 14
 15
 16
 17
 18
 19
 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
class DeleteMutation(Mutation):
    """
    Remove a ``Rule`` node from its parent operator in-place.

    Applicable to both literal and operator nodes. The mutation has two modes
    depending on the parent's subrule count:

    - Parent has **3+ subrules**: the target node is simply removed from the
      parent's subrule list.
    - Parent has **exactly 2 subrules** (collapse): the remaining sibling is
      appended to the grandparent's subrule list and the now-redundant parent
      operator is removed from the grandparent. This requires a grandparent to
      exist; if there is none, a ``MutationError`` is raised.

    A ``MutationError`` is also raised when the target node is the root of the
    tree (has no parent).

    Attributes:
        is_literal_mutation (bool): ``True``.
        is_operator_mutation (bool): ``True``.

    Examples:
        Simple removal (parent has 3 subrules):

        >>> from hgp_lib.mutations import DeleteMutation
        >>> from hgp_lib.rules import And, Literal
        >>> parent = And([Literal(value=0), Literal(value=1), Literal(value=2)])
        >>> mutation = DeleteMutation()
        >>> mutation.apply(parent.subrules[1])
        >>> parent
        And(0, 2)

        Collapse (parent has 2 subrules — sibling is appended to grandparent):

        >>> from hgp_lib.rules import Or
        >>> root = Or([
        ...     Literal(value=0),
        ...     And([Literal(value=1), Literal(value=2)]),
        ... ])
        >>> mutation.apply(root.subrules[1].subrules[0])
        >>> root
        Or(0, 2)
    """

    def __init__(self):
        super().__init__(is_literal_mutation=True, is_operator_mutation=True)

    def apply(self, rule: Rule):
        """
        Delete ``rule`` from its parent's subrule list in-place.

        When the parent has more than two subrules, the target is simply removed.
        When the parent has exactly two subrules, the remaining sibling is appended
        to the grandparent's subrule list and the parent operator is removed from
        the grandparent (collapse).

        Args:
            rule (Rule):
                The rule node to delete.

        Raises:
            MutationError:
                If ``rule`` has no parent (root node), or if the parent has exactly
                two subrules and no grandparent exists.

        Examples:
            Simple removal:

            >>> from hgp_lib.mutations import DeleteMutation
            >>> from hgp_lib.rules import Or, Literal
            >>> parent = Or([Literal(value=1), Literal(value=2), Literal(value=3)])
            >>> DeleteMutation().apply(parent.subrules[2])
            >>> parent
            Or(1, 2)

            Collapse — the inner And is removed and its surviving child is
            appended to the root:

            >>> from hgp_lib.rules import And
            >>> root = Or([
            ...     Literal(value=0),
            ...     And([Literal(value=1), Literal(value=2)]),
            ...     Literal(value=3),
            ... ])
            >>> DeleteMutation().apply(root.subrules[1].subrules[1])
            >>> root
            Or(0, 3, 1)
        """
        parent = rule.parent
        if parent is None:
            raise MutationError()
        subrules = parent.subrules
        if len(subrules) == 2:
            # Special case, we might need to collapse the operator along with the literal
            grandparent = parent.parent
            if grandparent is None:
                raise MutationError()
            other_rule_index = 0
            if subrules[0] is rule:
                other_rule_index = 1
            subrules[other_rule_index].parent = grandparent
            grandparent.subrules.append(subrules[other_rule_index])
            del subrules[1 - other_rule_index]
            subrules = grandparent.subrules
            rule = parent
        for i in range(len(subrules)):
            if subrules[i] is rule:
                del subrules[i]
                return
        raise RuntimeError("Unreachable code")

apply(rule)

Delete rule from its parent's subrule list in-place.

When the parent has more than two subrules, the target is simply removed. When the parent has exactly two subrules, the remaining sibling is appended to the grandparent's subrule list and the parent operator is removed from the grandparent (collapse).

Parameters:

Name Type Description Default
rule Rule

The rule node to delete.

required

Raises:

Type Description
MutationError

If rule has no parent (root node), or if the parent has exactly two subrules and no grandparent exists.

Examples:

Simple removal:

>>> from hgp_lib.mutations import DeleteMutation
>>> from hgp_lib.rules import Or, Literal
>>> parent = Or([Literal(value=1), Literal(value=2), Literal(value=3)])
>>> DeleteMutation().apply(parent.subrules[2])
>>> parent
Or(1, 2)

Collapse — the inner And is removed and its surviving child is appended to the root:

>>> from hgp_lib.rules import And
>>> root = Or([
...     Literal(value=0),
...     And([Literal(value=1), Literal(value=2)]),
...     Literal(value=3),
... ])
>>> DeleteMutation().apply(root.subrules[1].subrules[1])
>>> root
Or(0, 3, 1)
Source code in hgp_lib\mutations\literal_mutations.py
 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
def apply(self, rule: Rule):
    """
    Delete ``rule`` from its parent's subrule list in-place.

    When the parent has more than two subrules, the target is simply removed.
    When the parent has exactly two subrules, the remaining sibling is appended
    to the grandparent's subrule list and the parent operator is removed from
    the grandparent (collapse).

    Args:
        rule (Rule):
            The rule node to delete.

    Raises:
        MutationError:
            If ``rule`` has no parent (root node), or if the parent has exactly
            two subrules and no grandparent exists.

    Examples:
        Simple removal:

        >>> from hgp_lib.mutations import DeleteMutation
        >>> from hgp_lib.rules import Or, Literal
        >>> parent = Or([Literal(value=1), Literal(value=2), Literal(value=3)])
        >>> DeleteMutation().apply(parent.subrules[2])
        >>> parent
        Or(1, 2)

        Collapse — the inner And is removed and its surviving child is
        appended to the root:

        >>> from hgp_lib.rules import And
        >>> root = Or([
        ...     Literal(value=0),
        ...     And([Literal(value=1), Literal(value=2)]),
        ...     Literal(value=3),
        ... ])
        >>> DeleteMutation().apply(root.subrules[1].subrules[1])
        >>> root
        Or(0, 3, 1)
    """
    parent = rule.parent
    if parent is None:
        raise MutationError()
    subrules = parent.subrules
    if len(subrules) == 2:
        # Special case, we might need to collapse the operator along with the literal
        grandparent = parent.parent
        if grandparent is None:
            raise MutationError()
        other_rule_index = 0
        if subrules[0] is rule:
            other_rule_index = 1
        subrules[other_rule_index].parent = grandparent
        grandparent.subrules.append(subrules[other_rule_index])
        del subrules[1 - other_rule_index]
        subrules = grandparent.subrules
        rule = parent
    for i in range(len(subrules)):
        if subrules[i] is rule:
            del subrules[i]
            return
    raise RuntimeError("Unreachable code")

hgp_lib.mutations.literal_mutations.NegateMutation

Bases: Mutation

The NegateMutation inverts inplace the logical negation flag of a given Rule. It is applicable to both literals and operator nodes.

Attributes:

Name Type Description
is_literal_mutation bool

True.

is_operator_mutation bool

True.

Examples:

>>> from hgp_lib.mutations import NegateMutation
>>> from hgp_lib.rules import Literal, And
>>> mutation = NegateMutation()
>>> rule = Literal(value=1)
>>> mutation.apply(rule)
>>> rule
~1
>>> mutation.apply(rule)
>>> rule
1
>>> rule = And([Literal(value=0), Literal(value=1)])
>>> mutation.apply(rule)
>>> rule
~And(0, 1)
Source code in hgp_lib\mutations\literal_mutations.py
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
class NegateMutation(Mutation):
    """
    The `NegateMutation` inverts inplace the logical negation flag of a given `Rule`. It is applicable to both literals
    and operator nodes.

    Attributes:
        is_literal_mutation (bool): `True`.
        is_operator_mutation (bool): `True`.

    Examples:
        >>> from hgp_lib.mutations import NegateMutation
        >>> from hgp_lib.rules import Literal, And
        >>> mutation = NegateMutation()
        >>> rule = Literal(value=1)
        >>> mutation.apply(rule)
        >>> rule
        ~1
        >>> mutation.apply(rule)
        >>> rule
        1
        >>> rule = And([Literal(value=0), Literal(value=1)])
        >>> mutation.apply(rule)
        >>> rule
        ~And(0, 1)
    """

    def __init__(self):
        super().__init__(is_literal_mutation=True, is_operator_mutation=True)

    def apply(self, rule: Rule):
        """
        Applies an inplace negation mutation to the given `Rule`. Toggles the rule's `negated` flag.

        Args:
            rule (Rule):
                The rule node whose negation flag will be flipped.

        Examples:
            >>> from hgp_lib.mutations import NegateMutation
            >>> from hgp_lib.rules import Literal
            >>> rule = Literal(value=0)
            >>> mutation = NegateMutation()
            >>> mutation.apply(rule)
            >>> rule
            ~0
            >>> mutation.apply(rule)
            >>> rule
            0
        """
        rule.negated = not rule.negated

apply(rule)

Applies an inplace negation mutation to the given Rule. Toggles the rule's negated flag.

Parameters:

Name Type Description Default
rule Rule

The rule node whose negation flag will be flipped.

required

Examples:

>>> from hgp_lib.mutations import NegateMutation
>>> from hgp_lib.rules import Literal
>>> rule = Literal(value=0)
>>> mutation = NegateMutation()
>>> mutation.apply(rule)
>>> rule
~0
>>> mutation.apply(rule)
>>> rule
0
Source code in hgp_lib\mutations\literal_mutations.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
def apply(self, rule: Rule):
    """
    Applies an inplace negation mutation to the given `Rule`. Toggles the rule's `negated` flag.

    Args:
        rule (Rule):
            The rule node whose negation flag will be flipped.

    Examples:
        >>> from hgp_lib.mutations import NegateMutation
        >>> from hgp_lib.rules import Literal
        >>> rule = Literal(value=0)
        >>> mutation = NegateMutation()
        >>> mutation.apply(rule)
        >>> rule
        ~0
        >>> mutation.apply(rule)
        >>> rule
        0
    """
    rule.negated = not rule.negated

hgp_lib.mutations.literal_mutations.ReplaceLiteral

Bases: Mutation

The ReplaceLiteral mutation replaces the value of a literal Rule with a different random literal index. It ensures that the new literal value differs from the current one. This mutation is only applicable to literal nodes and never to operator nodes.

Attributes:

Name Type Description
is_literal_mutation bool

True.

is_operator_mutation bool

False.

num_literals int

The total number of possible literal values. Must be greater than 1.

Notes
  • The new literal value is chosen uniformly at random from the range [0, num_literals).
  • If the randomly chosen value equals the current literal's value, it is incremented modulo num_literals to guarantee a change.
  • The mutation operates inplace and modifies only the literal's value.

Examples:

>>> from hgp_lib.mutations import ReplaceLiteral
>>> from hgp_lib.rules import Literal
>>> mutation = ReplaceLiteral(num_literals=2)
>>> rule = Literal(value=0)
>>> mutation.apply(rule)
>>> rule
1
Source code in hgp_lib\mutations\literal_mutations.py
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
class ReplaceLiteral(Mutation):
    """
    The `ReplaceLiteral` mutation replaces the value of a literal `Rule` with a different random literal index.
    It ensures that the new literal value differs from the current one. This mutation is only applicable to literal
    nodes and never to operator nodes.

    Attributes:
        is_literal_mutation (bool): `True`.
        is_operator_mutation (bool): `False`.
        num_literals (int): The total number of possible literal values. Must be greater than `1`.

    Notes:
        - The new literal value is chosen uniformly at random from the range `[0, num_literals)`.
        - If the randomly chosen value equals the current literal's value, it is incremented modulo `num_literals`
          to guarantee a change.
        - The mutation operates inplace and modifies only the literal's `value`.

    Examples:
        >>> from hgp_lib.mutations import ReplaceLiteral
        >>> from hgp_lib.rules import Literal
        >>> mutation = ReplaceLiteral(num_literals=2)
        >>> rule = Literal(value=0)
        >>> mutation.apply(rule)
        >>> rule
        1
    """

    def __init__(self, num_literals: int):
        super().__init__(is_literal_mutation=True, is_operator_mutation=False)

        # num_literals was validated

        self.num_literals = num_literals

    def apply(self, rule: Rule):
        """
        Applies an inplace literal replacement mutation to the given `Rule`. Randomly assigns a new literal value
        different from the current one.

        Args:
            rule (Rule):
                The literal rule whose value will be replaced.

        Examples:
            >>> from hgp_lib.mutations import ReplaceLiteral
            >>> from hgp_lib.rules import Literal
            >>> mutation = ReplaceLiteral(num_literals=4)
            >>> rule = Literal(value=1)
            >>> mutation.apply(rule)
            >>> rule.value != 1
            True
        """
        new_value = np.random.randint(self.num_literals)
        if new_value == rule.value:
            new_value = (new_value + 1) % self.num_literals
        rule.value = new_value

apply(rule)

Applies an inplace literal replacement mutation to the given Rule. Randomly assigns a new literal value different from the current one.

Parameters:

Name Type Description Default
rule Rule

The literal rule whose value will be replaced.

required

Examples:

>>> from hgp_lib.mutations import ReplaceLiteral
>>> from hgp_lib.rules import Literal
>>> mutation = ReplaceLiteral(num_literals=4)
>>> rule = Literal(value=1)
>>> mutation.apply(rule)
>>> rule.value != 1
True
Source code in hgp_lib\mutations\literal_mutations.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def apply(self, rule: Rule):
    """
    Applies an inplace literal replacement mutation to the given `Rule`. Randomly assigns a new literal value
    different from the current one.

    Args:
        rule (Rule):
            The literal rule whose value will be replaced.

    Examples:
        >>> from hgp_lib.mutations import ReplaceLiteral
        >>> from hgp_lib.rules import Literal
        >>> mutation = ReplaceLiteral(num_literals=4)
        >>> rule = Literal(value=1)
        >>> mutation.apply(rule)
        >>> rule.value != 1
        True
    """
    new_value = np.random.randint(self.num_literals)
    if new_value == rule.value:
        new_value = (new_value + 1) % self.num_literals
    rule.value = new_value

hgp_lib.mutations.literal_mutations.PromoteLiteral

Bases: Mutation

The PromoteLiteral mutation converts a literal Rule into an operator node (i.e. And, Or) by promoting it and attaching two literal children: one representing the old literal and another newly generated literal. This mutation increases the structural complexity of the rule tree.

Attributes:

Name Type Description
is_literal_mutation bool

True.

is_operator_mutation bool

False.

num_literals int

The total number of possible literal values. Must be greater than 1.

operator_types Tuple[Type[Rule]]

Tuple of operator classes (e.g., (Or, And)) that can replace the literal.

Notes
  • The literal is transformed inplace into an operator node by changing its class (__class__).
  • Two new subrules are attached:
    1. The original literal (same value and negation),
    2. A new literal with a randomly chosen value different from the original.
  • Random negation flags are assigned independently to the new operator and the new literal.
  • The value attribute of the promoted node is cleared (None) since it becomes an operator.

Examples:

>>> from hgp_lib.mutations import PromoteLiteral
>>> from hgp_lib.rules import Literal, And, Or
>>> mutation = PromoteLiteral(num_literals=4)
>>> rule = Literal(value=1, negated=False)
>>> mutation.apply(rule)
>>> len(rule)
3
>>> isinstance(rule, Or) or isinstance(rule, And)
True
>>> rule.subrules[0]
1
Source code in hgp_lib\mutations\literal_mutations.py
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
class PromoteLiteral(Mutation):
    """
    The `PromoteLiteral` mutation converts a literal `Rule` into an operator node (i.e. `And`, `Or`) by promoting it
    and attaching two literal children: one representing the old literal and another newly generated literal.
    This mutation increases the structural complexity of the rule tree.

    Attributes:
        is_literal_mutation (bool): `True`.
        is_operator_mutation (bool): `False`.
        num_literals (int): The total number of possible literal values. Must be greater than `1`.
        operator_types (Tuple[Type[Rule]]): Tuple of operator classes (e.g., `(Or, And)`) that can replace the literal.

    Notes:
        - The literal is transformed inplace into an operator node by changing its class (`__class__`).
        - Two new subrules are attached:
            1. The original literal (same value and negation),
            2. A new literal with a randomly chosen value different from the original.
        - Random negation flags are assigned independently to the new operator and the new literal.
        - The `value` attribute of the promoted node is cleared (`None`) since it becomes an operator.

    Examples:
        >>> from hgp_lib.mutations import PromoteLiteral
        >>> from hgp_lib.rules import Literal, And, Or
        >>> mutation = PromoteLiteral(num_literals=4)
        >>> rule = Literal(value=1, negated=False)
        >>> mutation.apply(rule)
        >>> len(rule)
        3
        >>> isinstance(rule, Or) or isinstance(rule, And)
        True
        >>> rule.subrules[0]
        1
    """

    def __init__(
        self, num_literals: int, operator_types: Tuple[Type[Rule], ...] = (Or, And)
    ):
        super().__init__(is_literal_mutation=True, is_operator_mutation=False)

        # num_literals was validated
        # operator_types was validated

        self.num_literals = num_literals
        self.operator_types = operator_types

    def apply(self, rule: Rule):
        """
        Promotes a literal node into a randomly chosen operator (`And` or `Or`), creating two new literal subrules:
        one representing the old literal and another newly generated literal with a different value.

        Args:
            rule (Rule):
                The literal rule to promote.

        Notes:
            - The transformation occurs inplace by reassigning `rule.__class__`.
            - The mutation randomizes negation for both the operator and the new literal.

        Examples:
            >>> from hgp_lib.mutations import PromoteLiteral
            >>> from hgp_lib.rules import Literal
            >>> mutation = PromoteLiteral(num_literals=3)
            >>> rule = Literal(value=0, negated=True)
            >>> mutation.apply(rule)
            >>> len(rule)
            3
            >>> isinstance(rule, Or) or isinstance(rule, And)
            True
            >>> rule.subrules[0]
            ~0
        """
        rule.__class__ = random.choice(self.operator_types)  # Efficient class change
        new_value = random.randint(0, self.num_literals - 1)
        if new_value == rule.value:
            new_value = (new_value + 1) % self.num_literals
        rule.subrules = [
            Literal(None, rule, rule.value, rule.negated),  # Old literal
            Literal(None, rule, new_value, random.random() < 0.5),  # New literal
        ]
        rule.negated = random.random() < 0.5
        rule.value = None  # Removing the value from the new operator

apply(rule)

Promotes a literal node into a randomly chosen operator (And or Or), creating two new literal subrules: one representing the old literal and another newly generated literal with a different value.

Parameters:

Name Type Description Default
rule Rule

The literal rule to promote.

required
Notes
  • The transformation occurs inplace by reassigning rule.__class__.
  • The mutation randomizes negation for both the operator and the new literal.

Examples:

>>> from hgp_lib.mutations import PromoteLiteral
>>> from hgp_lib.rules import Literal
>>> mutation = PromoteLiteral(num_literals=3)
>>> rule = Literal(value=0, negated=True)
>>> mutation.apply(rule)
>>> len(rule)
3
>>> isinstance(rule, Or) or isinstance(rule, And)
True
>>> rule.subrules[0]
~0
Source code in hgp_lib\mutations\literal_mutations.py
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
def apply(self, rule: Rule):
    """
    Promotes a literal node into a randomly chosen operator (`And` or `Or`), creating two new literal subrules:
    one representing the old literal and another newly generated literal with a different value.

    Args:
        rule (Rule):
            The literal rule to promote.

    Notes:
        - The transformation occurs inplace by reassigning `rule.__class__`.
        - The mutation randomizes negation for both the operator and the new literal.

    Examples:
        >>> from hgp_lib.mutations import PromoteLiteral
        >>> from hgp_lib.rules import Literal
        >>> mutation = PromoteLiteral(num_literals=3)
        >>> rule = Literal(value=0, negated=True)
        >>> mutation.apply(rule)
        >>> len(rule)
        3
        >>> isinstance(rule, Or) or isinstance(rule, And)
        True
        >>> rule.subrules[0]
        ~0
    """
    rule.__class__ = random.choice(self.operator_types)  # Efficient class change
    new_value = random.randint(0, self.num_literals - 1)
    if new_value == rule.value:
        new_value = (new_value + 1) % self.num_literals
    rule.subrules = [
        Literal(None, rule, rule.value, rule.negated),  # Old literal
        Literal(None, rule, new_value, random.random() < 0.5),  # New literal
    ]
    rule.negated = random.random() < 0.5
    rule.value = None  # Removing the value from the new operator

Operator Mutations

hgp_lib.mutations.operator_mutations.RemoveIntermediateOperator

Bases: Mutation

The RemoveIntermediateOperator mutation removes an intermediate operator node (e.g., And, Or) and promotes its subrules to the operator's parent, flattening the rule tree structure.

Attributes:

Name Type Description
is_literal_mutation bool

False.

is_operator_mutation bool

True.

Notes
  • The mutation raises a MutationError if the operator has no parent (i.e., it is the root of the tree).
  • All child subrules of the removed operator are reattached directly to the parent.
  • The parent's subrule list is updated inplace, preserving the relative order of existing subrules.

Examples:

>>> from hgp_lib.mutations import RemoveIntermediateOperator
>>> from hgp_lib.rules import And, Or, Literal
>>> rule = And([
...     Literal(value=0),
...     Or([
...         Literal(value=1),
...         Literal(value=2),
...     ]),
...     Literal(value=3)
... ])
>>> mutation = RemoveIntermediateOperator()
>>> mutation.apply(rule.subrules[1])
>>> rule
And(0, 3, 1, 2)
Source code in hgp_lib\mutations\operator_mutations.py
10
11
12
13
14
15
16
17
18
19
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
class RemoveIntermediateOperator(Mutation):
    """
    The `RemoveIntermediateOperator` mutation removes an intermediate operator node (e.g., `And`, `Or`) and promotes its
    subrules to the operator's parent, flattening the rule tree structure.

    Attributes:
        is_literal_mutation (bool): `False`.
        is_operator_mutation (bool): `True`.

    Notes:
        - The mutation raises a `MutationError` if the operator has no parent (i.e., it is the root of the tree).
        - All child subrules of the removed operator are reattached directly to the parent.
        - The parent's subrule list is updated inplace, preserving the relative order of existing subrules.

    Examples:
        >>> from hgp_lib.mutations import RemoveIntermediateOperator
        >>> from hgp_lib.rules import And, Or, Literal
        >>> rule = And([
        ...     Literal(value=0),
        ...     Or([
        ...         Literal(value=1),
        ...         Literal(value=2),
        ...     ]),
        ...     Literal(value=3)
        ... ])
        >>> mutation = RemoveIntermediateOperator()
        >>> mutation.apply(rule.subrules[1])
        >>> rule
        And(0, 3, 1, 2)
    """

    def __init__(self):
        super().__init__(is_literal_mutation=False, is_operator_mutation=True)

    def apply(self, rule: Rule):
        """
        Applies an inplace structural mutation that removes the specified operator and attaches its subrules directly
        to the parent operator.

        Args:
            rule (Rule):
                The operator rule to remove.

        Raises:
            MutationError:
                If the operator has no parent (i.e., it is the root node).
            RuntimeError:
                If the target operator is not found within its parent's subrule list, which should never occur during
                normal operation.

        Examples:
            >>> from hgp_lib.mutations import RemoveIntermediateOperator
            >>> from hgp_lib.rules import Or, And, Literal
            >>> rule = Or([
            ...     Literal(value=1),
            ...     And([
            ...         Literal(value=2),
            ...         Literal(value=3),
            ...     ]),
            ...     Literal(value=4)
            ... ])
            >>> mutation = RemoveIntermediateOperator()
            >>> mutation.apply(rule.subrules[1])
            >>> rule
            Or(1, 4, 2, 3)
        """
        parent = rule.parent
        if parent is None:
            raise MutationError()
        for s in rule.subrules:
            s.parent = parent
        parent.subrules += rule.subrules
        for i in range(len(parent.subrules)):
            if parent.subrules[i] is rule:
                del parent.subrules[i]
                return
        raise RuntimeError("Unreachable code")

apply(rule)

Applies an inplace structural mutation that removes the specified operator and attaches its subrules directly to the parent operator.

Parameters:

Name Type Description Default
rule Rule

The operator rule to remove.

required

Raises:

Type Description
MutationError

If the operator has no parent (i.e., it is the root node).

RuntimeError

If the target operator is not found within its parent's subrule list, which should never occur during normal operation.

Examples:

>>> from hgp_lib.mutations import RemoveIntermediateOperator
>>> from hgp_lib.rules import Or, And, Literal
>>> rule = Or([
...     Literal(value=1),
...     And([
...         Literal(value=2),
...         Literal(value=3),
...     ]),
...     Literal(value=4)
... ])
>>> mutation = RemoveIntermediateOperator()
>>> mutation.apply(rule.subrules[1])
>>> rule
Or(1, 4, 2, 3)
Source code in hgp_lib\mutations\operator_mutations.py
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
def apply(self, rule: Rule):
    """
    Applies an inplace structural mutation that removes the specified operator and attaches its subrules directly
    to the parent operator.

    Args:
        rule (Rule):
            The operator rule to remove.

    Raises:
        MutationError:
            If the operator has no parent (i.e., it is the root node).
        RuntimeError:
            If the target operator is not found within its parent's subrule list, which should never occur during
            normal operation.

    Examples:
        >>> from hgp_lib.mutations import RemoveIntermediateOperator
        >>> from hgp_lib.rules import Or, And, Literal
        >>> rule = Or([
        ...     Literal(value=1),
        ...     And([
        ...         Literal(value=2),
        ...         Literal(value=3),
        ...     ]),
        ...     Literal(value=4)
        ... ])
        >>> mutation = RemoveIntermediateOperator()
        >>> mutation.apply(rule.subrules[1])
        >>> rule
        Or(1, 4, 2, 3)
    """
    parent = rule.parent
    if parent is None:
        raise MutationError()
    for s in rule.subrules:
        s.parent = parent
    parent.subrules += rule.subrules
    for i in range(len(parent.subrules)):
        if parent.subrules[i] is rule:
            del parent.subrules[i]
            return
    raise RuntimeError("Unreachable code")

hgp_lib.mutations.operator_mutations.ReplaceOperator

Bases: Mutation

The ReplaceOperator mutation replaces an operator node (e.g., And, Or) with another operator type chosen randomly from the provided set of operator classes. The transformation occurs inplace, preserving the subrules and other attributes of the node.

Attributes:

Name Type Description
is_literal_mutation bool

False.

is_operator_mutation bool

True.

operator_types Tuple[Type[Rule]]

Tuple of operator classes that can replace one another (e.g., (Or, And)).

Notes
  • The mutation has no effect on literal nodes.
  • The operator type is switched inplace by directly reassigning the node's __class__ attribute.
  • The replacement operator is always different from the current operator type.

Examples:

>>> from hgp_lib.mutations import ReplaceOperator
>>> from hgp_lib.rules import And, Or, Literal
>>> rule = And([Literal(value=0), Literal(value=1)])
>>> mutation = ReplaceOperator(operator_types=(Or, And))
>>> mutation.apply(rule)
>>> rule
Or(0, 1)
Source code in hgp_lib\mutations\operator_mutations.py
 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
class ReplaceOperator(Mutation):
    """
    The `ReplaceOperator` mutation replaces an operator node (e.g., `And`, `Or`) with another operator type chosen
    randomly from the provided set of operator classes. The transformation occurs inplace, preserving the subrules
    and other attributes of the node.

    Attributes:
        is_literal_mutation (bool): `False`.
        is_operator_mutation (bool): `True`.
        operator_types (Tuple[Type[Rule]]): Tuple of operator classes that can replace one another (e.g., `(Or, And)`).

    Notes:
        - The mutation has no effect on literal nodes.
        - The operator type is switched inplace by directly reassigning the node's `__class__` attribute.
        - The replacement operator is always different from the current operator type.

    Examples:
        >>> from hgp_lib.mutations import ReplaceOperator
        >>> from hgp_lib.rules import And, Or, Literal
        >>> rule = And([Literal(value=0), Literal(value=1)])
        >>> mutation = ReplaceOperator(operator_types=(Or, And))
        >>> mutation.apply(rule)
        >>> rule
        Or(0, 1)
    """

    def __init__(self, operator_types: Tuple[Type[Rule], ...] = (Or, And)):
        super().__init__(is_literal_mutation=False, is_operator_mutation=True)

        # operator_types was validated

        self.operator_types = operator_types

    def apply(self, rule: Rule):
        """
        Applies an inplace operator replacement mutation to the given `Rule`, changing its type to a different operator
        from the available set.

        Args:
            rule (Rule):
                The operator rule to replace. Must be an instance of one of the `operator_types`.

        Examples:
            >>> from hgp_lib.mutations import ReplaceOperator
            >>> from hgp_lib.rules import And, Or, Literal
            >>> rule = Or([Literal(value=2), Literal(value=3)])
            >>> mutation = ReplaceOperator()
            >>> mutation.apply(rule)
            >>> rule
            And(2, 3)
        """
        rule.__class__ = random.choice(
            [
                operator_type
                for operator_type in self.operator_types
                if not isinstance(rule, operator_type)
            ]
        )

apply(rule)

Applies an inplace operator replacement mutation to the given Rule, changing its type to a different operator from the available set.

Parameters:

Name Type Description Default
rule Rule

The operator rule to replace. Must be an instance of one of the operator_types.

required

Examples:

>>> from hgp_lib.mutations import ReplaceOperator
>>> from hgp_lib.rules import And, Or, Literal
>>> rule = Or([Literal(value=2), Literal(value=3)])
>>> mutation = ReplaceOperator()
>>> mutation.apply(rule)
>>> rule
And(2, 3)
Source code in hgp_lib\mutations\operator_mutations.py
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
def apply(self, rule: Rule):
    """
    Applies an inplace operator replacement mutation to the given `Rule`, changing its type to a different operator
    from the available set.

    Args:
        rule (Rule):
            The operator rule to replace. Must be an instance of one of the `operator_types`.

    Examples:
        >>> from hgp_lib.mutations import ReplaceOperator
        >>> from hgp_lib.rules import And, Or, Literal
        >>> rule = Or([Literal(value=2), Literal(value=3)])
        >>> mutation = ReplaceOperator()
        >>> mutation.apply(rule)
        >>> rule
        And(2, 3)
    """
    rule.__class__ = random.choice(
        [
            operator_type
            for operator_type in self.operator_types
            if not isinstance(rule, operator_type)
        ]
    )

hgp_lib.mutations.operator_mutations.AddLiteral

Bases: Mutation

The AddLiteral mutation adds a new literal subrule to an operator node (e.g., And, Or). The new literal is chosen randomly from the available literal space, ensuring it does not duplicate an existing literal value already present in the operator.

Attributes:

Name Type Description
is_literal_mutation bool

False.

is_operator_mutation bool

True.

available_literals Set[int]

Set of all possible literal indices from which new literals are sampled.

Notes
  • The mutation raises a MutationError if all possible literals are already present under the operator.
  • The added literal's negation flag is determined randomly with equal probability.
  • The mutation operates inplace, modifying the operator's list of subrules directly.

Examples:

>>> from hgp_lib.mutations import AddLiteral
>>> from hgp_lib.rules import And, Literal
>>> rule = And([Literal(value=0), Literal(value=1)])
>>> mutation = AddLiteral(num_literals=3)
>>> mutation.apply(rule)
>>> rule.subrules[2].negated=True  # Setting negated to have deterministic output
>>> rule
And(0, 1, ~2)
Source code in hgp_lib\mutations\operator_mutations.py
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
class AddLiteral(Mutation):
    """
    The `AddLiteral` mutation adds a new literal subrule to an operator node (e.g., `And`, `Or`).
    The new literal is chosen randomly from the available literal space, ensuring it does not duplicate
    an existing literal value already present in the operator.

    Attributes:
        is_literal_mutation (bool): `False`.
        is_operator_mutation (bool): `True`.
        available_literals (Set[int]): Set of all possible literal indices from which new literals are sampled.

    Notes:
        - The mutation raises a `MutationError` if all possible literals are already present under the operator.
        - The added literal's negation flag is determined randomly with equal probability.
        - The mutation operates inplace, modifying the operator's list of subrules directly.

    Examples:
        >>> from hgp_lib.mutations import AddLiteral
        >>> from hgp_lib.rules import And, Literal
        >>> rule = And([Literal(value=0), Literal(value=1)])
        >>> mutation = AddLiteral(num_literals=3)
        >>> mutation.apply(rule)
        >>> rule.subrules[2].negated=True  # Setting negated to have deterministic output
        >>> rule
        And(0, 1, ~2)
    """

    def __init__(self, num_literals: int):
        super().__init__(is_literal_mutation=False, is_operator_mutation=True)

        # num_literals was validated

        self.num_literals = num_literals
        self.available_literals = set(range(num_literals))

    def apply(self, rule: Rule):
        """
        Adds a new literal subrule to the given operator node. The new literal's value is selected randomly from the
        remaining available literals not already present in the operator.

        Args:
            rule (Rule):
                The operator rule to which the new literal will be added.

        Raises:
            MutationError:
                If no new literal values are available for addition.

        Examples:
            >>> from hgp_lib.mutations import AddLiteral
            >>> from hgp_lib.rules import Or, Literal
            >>> mutation = AddLiteral(num_literals=3)
            >>> rule = Or([Literal(value=0), Literal(value=1)])
            >>> mutation.apply(rule)
            >>> rule.subrules[2].negated=False  # Setting negated to have deterministic output
            >>> rule.subrules[2]
            2
            >>> rule
            Or(0, 1, 2)
        """
        existing_rules = {s.value for s in rule.subrules if s.value is not None}

        if len(existing_rules) == self.num_literals:
            raise MutationError()

        random_shot = random.choice(tuple(self.available_literals - existing_rules))
        rule.subrules.append(Literal(None, rule, random_shot, random.random() < 0.5))

apply(rule)

Adds a new literal subrule to the given operator node. The new literal's value is selected randomly from the remaining available literals not already present in the operator.

Parameters:

Name Type Description Default
rule Rule

The operator rule to which the new literal will be added.

required

Raises:

Type Description
MutationError

If no new literal values are available for addition.

Examples:

>>> from hgp_lib.mutations import AddLiteral
>>> from hgp_lib.rules import Or, Literal
>>> mutation = AddLiteral(num_literals=3)
>>> rule = Or([Literal(value=0), Literal(value=1)])
>>> mutation.apply(rule)
>>> rule.subrules[2].negated=False  # Setting negated to have deterministic output
>>> rule.subrules[2]
2
>>> rule
Or(0, 1, 2)
Source code in hgp_lib\mutations\operator_mutations.py
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
def apply(self, rule: Rule):
    """
    Adds a new literal subrule to the given operator node. The new literal's value is selected randomly from the
    remaining available literals not already present in the operator.

    Args:
        rule (Rule):
            The operator rule to which the new literal will be added.

    Raises:
        MutationError:
            If no new literal values are available for addition.

    Examples:
        >>> from hgp_lib.mutations import AddLiteral
        >>> from hgp_lib.rules import Or, Literal
        >>> mutation = AddLiteral(num_literals=3)
        >>> rule = Or([Literal(value=0), Literal(value=1)])
        >>> mutation.apply(rule)
        >>> rule.subrules[2].negated=False  # Setting negated to have deterministic output
        >>> rule.subrules[2]
        2
        >>> rule
        Or(0, 1, 2)
    """
    existing_rules = {s.value for s in rule.subrules if s.value is not None}

    if len(existing_rules) == self.num_literals:
        raise MutationError()

    random_shot = random.choice(tuple(self.available_literals - existing_rules))
    rule.subrules.append(Literal(None, rule, random_shot, random.random() < 0.5))