Skip to content

pygx.algo.evolution.mutators

Mutators for evolutionary algorithms.

mutators

Mutators for evolutionary algorithms.

Uniform

Uniform(
    where: Callable[[DNA], bool] | None = None,
    seed: int | None = None,
    **kwargs
)

Bases: Mutator

Mutates a DNA by randomizing a branch of the DNA.

This is a minimal mutator. It acts as follows. PyGX represents a DNA as a tree, with information at each node, where child nodes are conditional on the value of parent nodes. This mutator will pick a node uniformly at random and mutate the subtree rooted at that node (inclusive), respecting dependencies specified in the DNASpec.

However, in general, we recommend that you write your own Mutator subclass so you can tailor it to your search space. This would allow you, for example: i) to modify a value drawing from a custom distribution: e.g. a gaussian-distributed additive change may be more appropriate in many cases. ii) to choose a node in the tree with a non-uniform distribution. E.g. you may want to modify some nodes more frequently if they encode areas of the space that should be explored more thoroughly. iii) perform mutations that implement a different type of locality than that represented by the tree structure. E.g. if two nodes at the same level need to be modified in a coordinated way.

Source code in pygx/algo/evolution/_mutators.py
def __init__(
    self,
    where: Callable[[pg.DNA], bool] | None = None,
    seed: int | None = None,
    **kwargs,
):
    super().__init__(**dict(where=where, seed=seed), **kwargs)

mutate

mutate(dna: DNA, step: int = 0) -> DNA

Mutates the DNA at a given step.

Source code in pygx/algo/evolution/_mutators.py
@override
def mutate(self, dna: pg.DNA, step: int = 0) -> pg.DNA:  # pyright: ignore[reportIncompatibleMethodOverride]
    """Mutates the DNA at a given step."""
    del step
    dna = dna.sym_clone(deep=True)  # Prevent overwriting argument.
    child_nodes, parent_nodes, child_indexes = self._get_relationships(dna)
    if not child_nodes:
        raise RuntimeError(f'Immutable DNA: {dna!r}')
    child_node, parent_node, child_index = self._random.choice(
        list(zip(child_nodes, parent_nodes, child_indexes))
    )
    child_spec = child_node.spec
    assert child_spec is not None, child_node
    if parent_node is None:
        # The node mutated ("child") is the root of the DNA tree.
        return pg.random_dna(
            child_spec,
            self._random,
            previous_dna=child_node,
        )
    else:
        # The node mutated is not the root of the DNA tree.
        if _node_needs_distinct(child_spec):
            # The approach taken here is inefficient in the special case when there
            # are many choices. If a random choice is likely to succeed, that
            # scenario can be sped up by redrawing random choices until success.
            # Consider adding a branch to handle that case, depending on need.
            assert isinstance(child_spec, pg.geno.Choices)
            # Compute mutated node value, enforcing distinct constraint.
            candidates = child_spec.candidates
            distinct_candidates = set(range(len(candidates))) - {
                c.value for c in parent_node.children
            }
            if distinct_candidates:
                new_child_value = self._random.choice(
                    list(distinct_candidates)
                )
                # Create a new sub-tree.
                new_child_node = pg.DNA(
                    new_child_value,
                    children=[
                        pg.random_dna(
                            candidates[new_child_value],
                            # Choice has changed for the new node,
                            # thus previous_dna does not apply.
                            self._random,
                            previous_dna=None,
                        )
                    ],
                )
                new_child_node.use_spec(child_spec)
            else:
                new_child_node = None
        else:
            new_child_node = pg.random_dna(
                child_spec,
                self._random,
                previous_dna=child_node,
            )
        if new_child_node is not None:
            # NOTE(daiyip): we update the children without invalidating the internal
            # states of the DNA for better performance.
            parent_node.children.sym_rebind(
                {child_index: new_child_node}, skip_notification=True
            )
            if _node_needs_sorting(child_spec):
                assert isinstance(child_spec, pg.geno.Choices)
                parent_spec = child_spec.parent_spec
                assert isinstance(parent_spec, pg.geno.Choices), child_spec
                # Reordering `parent_node`'s OWN children. `sym_rebind`
                # builds a fresh `pg.List` for the field, so each child
                # reads as a second hold (its holder is still the old
                # list) even though one holder owns it throughout and
                # only the index changes. `DNA.children` declares
                # `assign='adopt'`, which resolves that.
                children = sorted(
                    parent_node.children, key=lambda c: c.value
                )
                # When child choices are reordered, their DNASpec need to be
                # realigned.
                assert len(children) == parent_spec.num_choices
                for i, child in enumerate(children):
                    child.use_spec(parent_spec.subchoice(i))
                parent_node.sym_rebind(
                    children=children, skip_notification=True
                )
        return dna

Swap

Swap(
    where: Callable[[DNA], bool] | None = None,
    seed: int | None = None,
    **kwargs
)

Bases: Mutator

Specialized mutator that swaps DNA branches rooted at sibling nodes.

Source code in pygx/algo/evolution/_mutators.py
def __init__(
    self,
    where: Callable[[pg.DNA], bool] | None = None,
    seed: int | None = None,
    **kwargs,
):
    super().__init__(**dict(where=where, seed=seed), **kwargs)

mutate

mutate(dna: DNA, step: int = 0) -> DNA

Mutates the DNA. If impossible, returns a clone.

Source code in pygx/algo/evolution/_mutators.py
@override
def mutate(self, dna: pg.DNA, step: int = 0) -> pg.DNA:  # pyright: ignore[reportIncompatibleMethodOverride]
    """Mutates the DNA. If impossible, returns a clone."""
    dna = dna.sym_clone(deep=True)  # Prevent overwriting argument.
    parent_node_candidates = self._get_candidate_nodes(dna)
    self._random.shuffle(parent_node_candidates)
    parent_node: pg.DNA | None = None
    child_indexes = []
    for parent_node in parent_node_candidates:
        parent_spec = parent_node.spec
        assert isinstance(parent_spec, pg.geno.Choices), parent_node
        if not parent_spec.sorted:
            # If no sorting is required, any swap is valid.
            child_indexes = self._random.sample(
                range(len(parent_node.children)), 2
            )
            break  # Found a pair to swap.

    if child_indexes:
        # Swap the two indexes.
        assert parent_node is not None
        assert len(child_indexes) == 2
        child0 = parent_node.children[child_indexes[0]]
        child1 = parent_node.children[child_indexes[1]]
        parent_node.children.sym_rebind({child_indexes[0]: child1})
        parent_node.children.sym_rebind({child_indexes[1]: child0})
    return dna

where_fn_spec

where_fn_spec()

Returns ValueSpec for 'where' function.

Source code in pygx/algo/evolution/_mutators.py
def where_fn_spec():
    """Returns ValueSpec for 'where' function."""
    return pg.typing.Callable(
        [pg.typing.Object(pg.DNA)], returns=pg.typing.Bool()
    ).noneable()

options: show_root_heading: false show_root_toc_entry: false members_order: source heading_level: 2