Skip to content

pygx.algo.evolution.recombinators

Common recombinators for evolutionary algorithms.

recombinators

Common recombinators for evolutionary algorithms.

Types of recombinators

This file implements 3 types of recombinators: point-wise, segment-wise and permutation.

Point-wise recombinators A point-wise recombinator works for an arbitrary number of parents, it crossovers the parents' DNA into offspring's DNA point-by-point. For example, Uniform takes a random parent's decisions per decision point, Sample samples one of the parents' decisions with probabilities computed from a weighting function provided by the user, and Average works for float decision points by averaging parents' values into the child's. When dealing with categorical decision points (e.g. pg.oneof, pg.manyof), Sample can be very sample-efficient when the decision points are orthogonal to each other.

Segment-wise recombinators A segment-wise recombinator works on 2 parents, it cuts the parents' DNA into multiple segments, and chooses alternating segments from the parents. For example, if the i'th segment of one parent is taken as the i'th segment of a child, the (i + 1)'th segment of the child will be taken from another parent. Each recombination produces 2 children which start with a segment from different parents. For example:

parent 1:   1  2  | 3  4  5  6  | 7  8  9
parent 2:   10 20 | 30 40 50 60 | 70 80 90

child 1:    1  2  | 30 40 50 60 | 7  8  9
child 2:    10 20 | 3  4  5  6  | 70 80 90

Well-known segment-wise recombinators are single-point crossover (SPX), two-point crossover (TPX), K-point crossover and more generally, the segmented crossover. While each of the former three chooses a fixed number of cutting points randomly, the segmented crossover allows the user to specify a function to produce the cutting points, which can be done dynamically based on the global state and step. Also the customized cutting point can be effective when the user knows how the search space should be partitioned based on the application.

Permutations

A permutation recombinator works on 2 parents by permutating the order of the subchoices of multi-choice decision points, which can be useful in applications in which the order of choices matters (e.g. the traveling salesman problem). Well-known permutation crossovers are partially mapped crossover (PMX), order crossover (OX) and cycle crossover (CX).

Which recombinators to use?

Semantics on hyper primitives

+-------------+-----------+---------+--------+-------+-----+-----+-----+-----+
|             |           |    #    |        |       |         manyof        |
|             |           | parents | floatv | oneof |  sorted Y |  sorted N |
|             |           |         |        |       |  distinct |  distinct |
|             |           |         |        |       |  Y  |  N  |  Y  |  N  |
+=============+===========+=========+========+=======+=====+=====+=====+=====+
|Point-wise   | Uniform   |   > 0   |   X    |   X   |  X  |  X  |  X  |  X  |
+             +-----------+---------+--------+-------+-----+-----+-----+-----+
|             | Sample    |   > 0   |   X    |   X   |  X  |  X  |  X  |  X  |
+             +-----------+---------+--------+-------+-----+-----+-----+-----+
|             | Average   |   > 0   |   X    |       |     |     |     |     |
+             +-----------+---------+--------+-------+-----+-----+-----+-----+
|             | W-Average |   > 0   |   X    |       |     |     |     |     |
+-------------+-----------+---------+--------+-------+-----+-----+-----+-----+
|Segment-wise | KPoint    |    2    |   X    |   X   | X (treated as a |  X  |
|             |           |         |        |       | single decision |     |
+-------------+-----------+---------+--------+-------+-----+-----+-----+-----+
|             | Segmented |    2    |   X    |   X   | point)          |  X  |
+-------------+-----------+---------+--------+-------+-----+-----+-----+-----+
|Permutation  | PMX       |    2    |        |       |     |     |  X  |     |
+-------------+-----------+---------+--------+-------+-----+-----+-----+-----+
|             | Order     |    2    |        |       |     |     |  X  |     |
+-------------+-----------+---------+--------+-------+-----+-----+-----+-----+
|             | Cycle     |    2    |        |       |     |     |  X  |     |
+-------------+-----------+---------+--------+-------+-----+-----+-----+-----+

A blank cell means the recombinator works as a no-op on the hyper primitive.

Disruptiveness of recombinations

A recombinator is more disruptive if it produces children that have more differences from the parents. In that regard, Uniform is more disruptive than Sample when a fitness-based weighting function is used. KPoint with a larger K is more disruptive than with a smaller K.

The more disruptive an operation is, the more diversity the operation produces. Historical studies show that with small populations, more disruptive recombination such as Uniform or K-Point (k >> 2) may yield better results because they help overcome the limited information capacity of smaller populations and the tendency for more homogeneity. With larger populations, less disruptive recombinations like 2-point are more likely to work better. (Reference: Holland, John H. (1975). Adaptation in Natural and Artificial Systems, The University of Michigan Press.)

When we need faster convergence, Sample with a fitness-based weighting function will be very effective, which can be used to implement a point-wise greedy strategy.

On recombination for float values

Add an Average or WeightedAverage when you have float decision points in the search space (created by pg.floatv). They are no-ops when there are no float decision points in the space.

PointWise

Bases: Recombinator

Base class for point-wise recombinators.

A point-wise recombinator operates on decision points (abbr. points) from the search space one by one. It crossovers the values from the parents into the offspring's decision on each target point. For those points that are not targeted, the decisions from the parents will be copied to the offsprings. Therefore, N parents can result in N children when at least 1 decision point is not applicable for crossover; otherwise 1 child will be produced.

Example (parent DNAs must carry a DNASpec, e.g. via use_spec):

space = pg.Dict(x=pg.oneof(range(5)), y=pg.floatv(0.0, 1.0))
spec = pg.dna_spec(space)
parents = [pg.DNA([0, 0.25]), pg.DNA([2, 0.75])]
for p in parents:
  p.use_spec(spec)

# The float decision is averaged; the choice decision is not applicable
# for `Average`, so each parent contributes a child that keeps its own
# choice decision.
children = pg.algo.evolution.recombinators.Average()(parents)
assert sorted(d.to_numbers() for d in children) == [[0, 0.5], [2, 0.5]]

Targeted decision points are points that are applicable for current recombinator according to its semantics, as well as passing the where statement if it's specified. The PointWise base class depends on the applicable_decision_points method to select all applicable points from the search space. If not overridden by subclasses, the method returns all decision points in the search space, with multi-choice folded into a single decision point. When the where argument is provided by the user, it further filters out unwanted decision points, which is useful when we want to limit the range of points for crossover.

Example:

space = pg.Dict(
    x=pg.oneof(range(5)),
    y=pg.oneof(range(5), hints='excluded'))
spec = pg.dna_spec(space)
parents = [pg.DNA([0, 1]), pg.DNA([2, 3])]
for p in parents:
  p.use_spec(spec)

# Only 'x' takes part in the crossover: one of the parents' x-decisions
# is chosen for all children, while each child keeps its own y-decision.
# The children will be either [DNA([0, 1]), DNA([0, 3])]
# or [DNA([2, 1]), DNA([2, 3])].
children = pg.algo.evolution.recombinators.Uniform(
    where=lambda xs: [x for x in xs if x.hints != 'excluded'])(parents)

applicable_decision_points

applicable_decision_points(
    dna_spec: DNASpec, global_state: AttributeDict, step: int
) -> list[DecisionPoint]

Returns applicable decision points for this recombinator.

The default behavior is to return all decision points in the search space, with multi-choice subchoices folded into a single decision point. Subclasses can override this method to select applicable points according to their semantics.

Parameters:

Name Type Description Default
dna_spec DNASpec

The root DNASpec.

required
global_state AttributeDict

An optional keyword argument as the global state. Subclass can omit.

required
step int

An optional keyword argument as current step. Subclass can omit.

required

Returns:

Type Description
list[DecisionPoint]

A list of targeted decision points for point-wise recombination, which will be further filtered by the where statement later.

Source code in pygx/algo/evolution/_recombinators.py
def applicable_decision_points(
    self,
    dna_spec: pg.geno.DNASpec,
    global_state: pg.geno.AttributeDict,
    step: int,
) -> list[pg.geno.DecisionPoint]:
    """Returns applicable decision points for this recombinator.

    The default behavior is to return all decision points in the search space,
    with multi-choice subchoices folded into a single decision point. Subclasses
    can override this method to select applicable points according to their
    semantics.

    Args:
      dna_spec: The root DNASpec.
      global_state: An optional keyword argument as the global state. Subclass
        can omit.
      step: An optional keyword argument as current step. Subclass can omit.

    Returns:
      A list of targeted decision points for point-wise recombination, which
        will be further filtered by the `where` statement later.
    """
    applicable_points = []
    for dp in dna_spec.decision_points:
        # Fold multi-choice subchoices into a single decision point.
        if isinstance(dp, pg.geno.Choices) and dp.is_subchoice:
            if dp.subchoice_index == 0:
                applicable_points.append(dp.parent_spec)
        else:
            applicable_points.append(dp)
    return applicable_points

merge abstractmethod

merge(
    decision_point: DecisionPoint,
    parent_decisions: list[int | list[int] | float | None],
    global_state: AttributeDict,
    step: int,
) -> int | list[int] | float

Implementation of point-wise decision making.

Parameters:

Name Type Description Default
decision_point DecisionPoint

Decision point for recombination.

required
parent_decisions list[int | list[int] | float | None]

A list of parent's decisions. Each item should be an int as an active single-choice decision, a list of int as active multi- choice decisions, a float as an active float decision, or None for inactive decision point (whose parent space is not chosen).

required
global_state AttributeDict

An optional keyword argument as the global state. Subclass can omit.

required
step int

An optional keyword argument as the current step. Subclass can omit.

required

Returns:

Type Description
int | list[int] | float

An int, list of int or float as the decision made for the decision point.

Source code in pygx/algo/evolution/_recombinators.py
@abc.abstractmethod
def merge(
    self,
    decision_point: pg.geno.DecisionPoint,
    parent_decisions: list[int | list[int] | float | None],
    global_state: pg.geno.AttributeDict,
    step: int,
) -> int | list[int] | float:
    """Implementation of point-wise decision making.

    Args:
      decision_point: Decision point for recombination.
      parent_decisions: A list of parent's decisions. Each item should be an
        int as an active single-choice decision, a list of int as active multi-
        choice decisions, a float as an active float decision, or None for
        inactive decision point (whose parent space is not chosen).
      global_state: An optional keyword argument as the global state. Subclass
        can omit.
      step: An optional keyword argument as the current step. Subclass can omit.

    Returns:
      An int, list of int or float as the decision made for the decision point.
    """

Uniform

Uniform(seed: int | None = None, **kwargs)

Bases: PointWise

Uniform crossover (UX) with equal probability.

The uniform recombinator picks a random parent's decision with equal probability at each applicable decision point. The merged decisions are applied to every parent's DNA to produce the children (duplicates removed): when every decision point takes part, a single child is produced; points excluded by where or inapplicable ones keep each parent's own decision, yielding up to one child per parent.

Reference:

G. Syswerda. 1989. Uniform crossover in genetic algorithms. In Proceedings of the 3rd International Conference on Genetic Algorithms. Morgan Kaufman, 2–9.

https://ci.nii.ac.jp/naid/10000012509/

Source code in pygx/algo/evolution/_recombinators.py
def __init__(self, seed: int | None = None, **kwargs):
    super().__init__(**dict(seed=seed), **kwargs)

Sample

Sample(weights: Operation | Callable[..., list[float]], **kwargs)

Bases: PointWise

Point-wise crossover that samples values from parents by weights.

The Sample recombinator works similarly as the Uniform recombinator, except that it takes a user function to compute the weights, based on which each parent's decision will be sampled. Uniform can be represented as Sample(lambda xs: [1] * len(xs)), whose weights function generates the sampling weights in uniform distribution.

Source code in pygx/algo/evolution/_recombinators.py
def __init__(
    self,
    weights: 'base.Operation | typing.Callable[..., list[float]]',
    **kwargs,
):
    super().__init__(**dict(weights=weights), **kwargs)

Numeric

Bases: PointWise

Base class for numerical recombinators.

A numeric recombinator operates on pg.geno.Float decision points, by recombining parents' values into child values.

Decisions of pg.geno.Choices will be copied over from N parents to N children on a one-to-one basis. If there are no choice decision points in the search space and the where clause does not filter any float points out, there will be a single child produced.

Example (parent DNAs must carry a DNASpec, e.g. via use_spec):

space = pg.Dict(x=pg.floatv(0.0, 1.0), y=pg.floatv(0.0, 1.0))
spec = pg.dna_spec(space)
parents = [pg.DNA([0.25, 0.5]), pg.DNA([0.75, 0.5])]
for p in parents:
  p.use_spec(spec)

# All decision points are floats, so a single child is produced.
children = pg.algo.evolution.recombinators.Average()(parents)
assert [d.to_numbers() for d in children] == [[0.5, 0.5]]

Average

Bases: Numeric

Average crossover.

Average crossover performs point-wise average of the parents' decisions on float decision points. We include this recombinator to help float decisions converge.

References: https://link.springer.com/content/pdf/10.1007/s00500-006-0049-7.pdf

WeightedAverage

WeightedAverage(weights: Operation | Callable[..., list[float]], **kwargs)

Bases: Numeric

Weighted-average crossover.

Similar as the Average crossover, a weighted-average crossover operates on all float decision points and carries over other parts of the chromosome from each parent to a child. Thus it produces the same number of children as the parents.

It uses formula cv[i] = sum(pw[j] * pv[j][i]) / sum(pw[j]) to compute the values for all or selected float points. cv[i] is the value of the i-th float point of the child DNA. pw[j] is the weight computed from the j-th parent, and pv[j][i] is the j-th parent's value on the i-th float point.

References: https://link.springer.com/content/pdf/10.1007/s00500-006-0049-7.pdf

Source code in pygx/algo/evolution/_recombinators.py
def __init__(
    self,
    weights: 'base.Operation | typing.Callable[..., list[float]]',
    **kwargs,
):
    super().__init__(**dict(weights=weights), **kwargs)

SegmentWise

Bases: Recombinator

Base for recombinators that interleavingly take segments from parents' DNA.

A segment-wise recombinator operates on 2 parents. It cuts both parents' DNA at K positions, which forms K + 1 segments. Then it takes the K + 1 segments from both parents in an interleaving manner. For example, a two-point crossover that takes places at cutting points 2 and 5 will result in 2 children as follows:

Parent 1:   [1,  2,  | 3,  4,  5,  | 6,  7]
Parent 2:   [10, 20, | 30, 40, 50, | 60, 70]
                     |             |
Child 1:    [1,  2,  | 30, 40, 50, | 6,  7]
Child 2:    [10, 20, | 3,  4,  5,  | 60, 70]

One important aspect of recombination is that the children produced from recombination should be valid. Decisions can be moved around when and only when they are independent from other decisions. In PyGX, there are multiple interdependent DNA sequences: 1) conditional space, whose DNA is represented by tuples (e.g. (1, 2) means inner-choice 2 is made under outer-choice 1). 2) subchoices of a multi-choice which has sorted and/or distinct constraint. Each interdependent DNA group, like a sub-tree for conditional space, or a list of subchoices for constrained multi-choice will be treated as a single position when the DNA is being cut into segments. For example, DNA([0, 1, [1, 2, 0, 3]]) has length 3 if the multi-choice is sorted or distinct, otherwise its length is 6.

The variations of segment-wise recombination differ from each other in how they choose the cutting strategies. KPoint randomly chooses K cutting points in the DNA sequence; a KPoint whose K reaches the sequence length cuts the DNA at every position, forming a list of length-1 segments. Segmented lets the user customize cutting strategies based on a list of applicable decision points.

cutting_indices abstractmethod

cutting_indices(
    independent_decision_points: list[DecisionPoint],
    global_state: AttributeDict,
    step: int,
) -> list[int]

Implementation of getting the indices of the cutting points.

Parameters:

Name Type Description Default
independent_decision_points list[DecisionPoint]

A list of independent decision points.

required
global_state AttributeDict

An optional keyword argument as the global state. Subclass can omit.

required
step int

An optional keyword argument as the curent step. Subclass can omit.

required

Returns:

Type Description
list[int]

A list of integers as the cutting points.

Source code in pygx/algo/evolution/_recombinators.py
@abc.abstractmethod
def cutting_indices(
    self,
    independent_decision_points: list[pg.geno.DecisionPoint],
    global_state: pg.geno.AttributeDict,
    step: int,
) -> list[int]:
    """Implementation of getting the indices of the cutting points.

    Args:
      independent_decision_points: A list of independent decision points.
      global_state: An optional keyword argument as the global state. Subclass
        can omit.
      step: An optional keyword argument as the curent step. Subclass can omit.

    Returns:
      A list of integers as the cutting points.
    """

KPoint

KPoint(k: int | Callable[[int], int], **kwargs)

Bases: SegmentWise

K-point crossover.

K-point crossover is one of the basic crossovers in evolutionary algorithms. It cuts both parents' DNA at K positions, which forms K + 1 segments. Then it takes the K + 1 segments from both parents in an interleaving manner. For example, a two-point crossover that takes places at cutting points 2 and 5 will result in 2 children as follows:

Parent 1:   [1,  2,  | 3,  4,  5,  | 6,  7]
Parent 2:   [10, 20, | 30, 40, 50, | 60, 70]
                     |             |
Child 1:    [1,  2,  | 30, 40, 50, | 6,  7]
Child 2:    [10, 20, | 3,  4,  5,  | 60, 70]

When K=1, we get a single-point crossover. Similarly, when K=2, we get a two-point crossover.

When K equals or is greater than the length of DNA sequence, we get an alternating-position (APX) crossover.

Reference: https://mitpress.mit.edu/books/introduction-genetic-algorithms https://dl.acm.org/doi/abs/10.5555/93126.93134 https://www.intechopen.com/chapters/335

Source code in pygx/algo/evolution/_recombinators.py
def __init__(self, k: int | typing.Callable[[int], int], **kwargs):
    super().__init__(**dict(k=k), **kwargs)

cutting_indices

cutting_indices(
    independent_decision_points: list[DecisionPoint],
    global_state: AttributeDict,
    step: int,
) -> list[int]

Returns the indices of cutting points for a list decision points.

Source code in pygx/algo/evolution/_recombinators.py
def cutting_indices(
    self,
    independent_decision_points: list[pg.geno.DecisionPoint],
    global_state: pg.geno.AttributeDict,
    step: int,
) -> list[int]:
    """Returns the indices of cutting points for a list decision points."""
    del global_state
    k = scalars.scalar_value(self.k, step)
    if len(independent_decision_points) > k + 1:
        indices = sorted(
            self._random.sample(
                list(range(1, len(independent_decision_points))), k=k
            )
        )
    else:
        indices = list(range(1, len(independent_decision_points)))
    return indices

Segmented

Segmented(cutting_points: Callable[[list[DecisionPoint]], list[int]], **kwargs)

Bases: SegmentWise

Segmented crossover.

Instead of using a predefined cutting strategy, the segmented recombinator allows the user to customize how the cutting points should be chosen, which can be used to implement fixed cutting point strategies (e.g nodes + edges) as well as decision points' based cutting point strategies.

Example:

# A fixed single-point crossover at the middle of the chromosome:
pg.algo.evolution.recombinators.Segmented(lambda xs: [len(xs) // 2])

# A multi-point crossover that cuts after decision points carrying a
# 'block_end' hint.
pg.algo.evolution.recombinators.Segmented(
    lambda xs: [i + 1 for i, x in enumerate(xs)
                if x.hints == 'block_end'])
Source code in pygx/algo/evolution/_recombinators.py
def __init__(
    self,
    cutting_points: typing.Callable[
        [list[pg.geno.DecisionPoint]], list[int]
    ],
    **kwargs,
):
    super().__init__(**dict(cutting_points=cutting_points), **kwargs)

cutting_indices

cutting_indices(
    independent_decision_points: list[DecisionPoint],
    global_state: AttributeDict,
    step: int,
) -> list[int]

Returns the indices of cutting points for a list decision points.

Source code in pygx/algo/evolution/_recombinators.py
def cutting_indices(
    self,
    independent_decision_points: list[pg.geno.DecisionPoint],
    global_state: pg.geno.AttributeDict,
    step: int,
) -> list[int]:
    """Returns the indices of cutting points for a list decision points."""
    return self.cutting_points(independent_decision_points)

Permutation

Permutation(
    where: DecisionPointFilterLike = ANY, seed: int | None = None, **kwargs
)

Bases: Recombinator

Base for recombinators that permutate the multi-choice subchoices.

A permutation recombinator operates on target permutation decision points. A permutation point is a multi-choice decision point that has distinct but unsorted subchoices, also with a num_choices equals to the number of its candidates. A permutation decision point can be created via pg.permutate or pg.manyof(len(candidates), candidates).

Example:

pg.Dict(x=pg.manyof(2, range(5)), y=pg.permutate(range(3)))

contains 1 permutation decision point, as the first pg.manyof has only 2 subchoices while the number of candidates is 5.

A permutation point is targeted if it's included in the return value of the where function when it's specified. By default, the where filter returns 1 random point among all the permutation points in the search space. When users specify the where argument to select more than 1 permutation points, the original DNA from each parent will be merged with each permutation proposal to generate a child. There will be a multiply effect between the number of parents and the number of proposals for each permutation, but the proposals generated from different permutation points will not be multiplied. That being said, if there are N parents, M proposals per crossover and K crossovers (K=1 by default), the max number of children will be N * M * K. Most permutation recombinators (PMX, OX, CX, etc.) operate on 2 parents and produce 2 proposals in a single crossover, resulting in N * M * K = 2 * 2 * 1 = 4 children.

For example, if DNA([0, 1, [1, 2, 3, 0]]) and DNA([2, 3, [0, 1, 2, 3]]) are recombinated on [1, 2, 3, 0] and [0, 1, 2, 3], which outputs [0, 2, 3, 1] and [3, 1, 2, 0] as recombined results. Then there will be 4 DNA in the output:

DNA([0, 1, [0, 2, 3, 1]])
DNA([0, 1, [3, 1, 2, 0]])
DNA([2, 3, [0, 2, 3, 1]])
DNA([2, 3, [3, 1, 2, 0]])

It's worth noting that though common permutation operations take 2 parents, the Permutation base class is designed to support an arbitrary number of parents. Use the NUM_PARENTS class attribute to specify the intended parent number if a subclass needs a fixed parent number.

Source code in pygx/algo/evolution/_recombinators.py
def __init__(
    self,
    where: where_lib.DecisionPointFilterLike = where_lib.ANY,
    seed: int | None = None,
    **kwargs,
):
    # `where_lib.ANY` is a module-level singleton shared by every
    # recombinator that does not override it, so holding it directly
    # would be a second hold (rule 2). It is immutable in practice, so
    # reference it rather than copying — `maybe_ref` leaves a
    # caller-supplied, unheld filter to be adopted as usual.
    super().__init__(**dict(where=pg.maybe_ref(where), seed=seed), **kwargs)

permutate abstractmethod

permutate(
    multi_choice_spec: Choices, parents: list[list[int]]
) -> list[list[int]]

Permutate decisions for a multi_choice_spec.

Source code in pygx/algo/evolution/_recombinators.py
@abc.abstractmethod
def permutate(
    self, multi_choice_spec: pg.geno.Choices, parents: list[list[int]]
) -> list[list[int]]:
    """Permutate decisions for a multi_choice_spec."""

PartiallyMapped

PartiallyMapped(
    where: DecisionPointFilterLike = ANY, seed: int | None = None, **kwargs
)

Bases: Permutation

Partially mapped crossover (PMX).

The partially mapped crossover (PMX) was proposed by D. Goldberg and R. Lingle, “Alleles, Loci and the Traveling Salesman Problem,” in Proceedings of the 1st International Conference on Genetic Algorithms and Their Applications, vol. 1985, pp. 154–159, Los Angeles, USA.

Reference: https://dl.acm.org/doi/10.5555/645511.657095

Source code in pygx/algo/evolution/_recombinators.py
def __init__(
    self,
    where: where_lib.DecisionPointFilterLike = where_lib.ANY,
    seed: int | None = None,
    **kwargs,
):
    # `where_lib.ANY` is a module-level singleton shared by every
    # recombinator that does not override it, so holding it directly
    # would be a second hold (rule 2). It is immutable in practice, so
    # reference it rather than copying — `maybe_ref` leaves a
    # caller-supplied, unheld filter to be adopted as usual.
    super().__init__(**dict(where=pg.maybe_ref(where), seed=seed), **kwargs)

partially_mapped_crossover

partially_mapped_crossover(
    parents: list[list[int]], start: int, end: int
) -> list[list[int]]

Cross over and remap the rest elements at given cutting points.

Source code in pygx/algo/evolution/_recombinators.py
def partially_mapped_crossover(
    self, parents: list[list[int]], start: int, end: int
) -> list[list[int]]:
    """Cross over and remap the rest elements at given cutting points."""
    assert len(parents) == 2
    size = len(parents[0])

    children, assigned, indices = [], [], []
    for i in range(2):
        child: list[int | None] = [None] * size
        child[start:end] = parents[(i + 1) % 2][start:end]
        children.append(child)

        assigned.append({c for c in children[i] if c is not None})
        indices.append({v: j for j, v in enumerate(parents[i])})

    positions = list(range(start)) + list(range(end, size))
    for i in range(2):
        for j in positions:
            v = parents[i][j]
            k = (i + 1) % 2
            while v in assigned[i]:
                v = parents[(k + 1) % 2][indices[k][v]]
            children[i][j] = v
            assigned[i].add(v)
    return children

Order

Order(where: DecisionPointFilterLike = ANY, seed: int | None = None, **kwargs)

Bases: Permutation

Order crossover (OX).

The order crossover (OX) was proposed by L. Davis, “Applying adaptive algorithms to epistatic domains,” IJCAI, vol. 85, pp. 162–164, 1985.

It builds offspring by choosing a subtour of a parent and preserving the relative order of bits of the other parent.

Reference: https://dl.acm.org/doi/10.5555/1625135.1625164

Source code in pygx/algo/evolution/_recombinators.py
def __init__(
    self,
    where: where_lib.DecisionPointFilterLike = where_lib.ANY,
    seed: int | None = None,
    **kwargs,
):
    # `where_lib.ANY` is a module-level singleton shared by every
    # recombinator that does not override it, so holding it directly
    # would be a second hold (rule 2). It is immutable in practice, so
    # reference it rather than copying — `maybe_ref` leaves a
    # caller-supplied, unheld filter to be adopted as usual.
    super().__init__(**dict(where=pg.maybe_ref(where), seed=seed), **kwargs)

order_crossover

order_crossover(
    parents: list[list[int]], start: int, end: int
) -> list[list[int]]

Cross over and remap the rest elements at given cutting points.

Source code in pygx/algo/evolution/_recombinators.py
def order_crossover(
    self, parents: list[list[int]], start: int, end: int
) -> list[list[int]]:
    """Cross over and remap the rest elements at given cutting points."""
    assert len(parents) == 2
    size = len(parents[0])

    children, crossovered, indices = [], [], []
    for i in range(2):
        child: list[int | None] = [None] * size
        child[start:end] = parents[(i + 1) % 2][start:end]
        children.append(child)

        crossovered.append({c for c in children[i] if c is not None})
        indices.append({v: j for j, v in enumerate(parents[i])})

    positions = list(range(end, size)) + list(range(start))
    for i in range(2):
        parent_pos = end % size
        for j in positions:
            v = parents[i][parent_pos]
            while v in crossovered[i]:
                parent_pos = (parent_pos + 1) % size
                v = parents[i][parent_pos]
            children[i][j] = v
            parent_pos = (parent_pos + 1) % size
    return children

Cycle

Cycle(where: DecisionPointFilterLike = ANY, seed: int | None = None, **kwargs)

Bases: Permutation

Cycle crossover (CX).

The cycle crossover (CX) operator was first proposed by I. M. Oliver, D. J. d. Smith, and R. C. J. Holland, “Study of permutation crossover operators on the traveling salesman problem,” in Genetic algorithms and their applications: proceedings of the second International Conference on Genetic Algorithms: July 28-31, 1987 at the Massachusetts Institute of Technology, Cambridge, MA, USA, 1987.

Reference: https://dl.acm.org/doi/10.5555/42512.42542.

Source code in pygx/algo/evolution/_recombinators.py
def __init__(
    self,
    where: where_lib.DecisionPointFilterLike = where_lib.ANY,
    seed: int | None = None,
    **kwargs,
):
    # `where_lib.ANY` is a module-level singleton shared by every
    # recombinator that does not override it, so holding it directly
    # would be a second hold (rule 2). It is immutable in practice, so
    # reference it rather than copying — `maybe_ref` leaves a
    # caller-supplied, unheld filter to be adopted as usual.
    super().__init__(**dict(where=pg.maybe_ref(where), seed=seed), **kwargs)

cycle_crossover

cycle_crossover(parents: list[list[int]]) -> list[list[int]]

Cycle crossover.

Source code in pygx/algo/evolution/_recombinators.py
def cycle_crossover(self, parents: list[list[int]]) -> list[list[int]]:
    """Cycle crossover."""
    size = len(parents[0])
    children, indices, selected = [], [], []
    for i in range(2):
        children.append([None] * size)
        indices.append({v: i for i, v in enumerate(parents[i])})
        selected.append(set())

    def pick(child_id, parent_id, index):
        if children[child_id][index] is None:
            x = parents[parent_id][index]
            y = parents[(parent_id + 1) % 2][index]

            children[child_id][index] = x
            selected[child_id].add(x)

            pick(child_id, parent_id, indices[parent_id][y])
            pick((child_id + 1) % 2, (parent_id + 1) % 2, index)

    for i in range(size):
        if children[0][i] is None:
            child_id = self._random.choice([0, 1])
            pick(child_id, 0, i)
    return typing.cast(list[list[int]], children)

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