Skip to content

Crossover

hgp_lib.crossover.crossover_executor.CrossoverExecutor

Coordinates subtree crossover operations across a collection of Rule trees.

The executor randomly selects pairs of rules based on crossover_p, then exchanges subtrees between the paired parents to produce offspring. An optional validator can reject invalid children, with configurable retry attempts.

Parameters:

Name Type Description Default
crossover_p float

Probability of selecting each rule for crossover. Default: 0.7.

0.7
crossover_strategy str

Strategy for pairing rules. Must be "best" or "random". Default: "random".

'random'
check_valid Callable[[Rule], bool] | None

Optional validator executed after crossover. When supplied, each child must pass validation or the crossover is retried up to num_tries times. All children that pass validation are kept until two children pass validation. Default: None. Note: The validator is called once during initialization to verify it returns a bool. Stateful validators should account for this extra call.

None
num_tries int

Maximum number of crossover attempts per pair when validation fails. 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 crossover point in the rule tree. Must be in [0.0, 1.0]. Default: 0.9.

0.9

Examples:

>>> import random
>>> import numpy as np
>>> from hgp_lib.crossover import CrossoverExecutor
>>> from hgp_lib.rules import And, Or, Literal
>>> random.seed(42); np.random.seed(42)
>>> executor = CrossoverExecutor(crossover_p=1.0)
>>> rules = [
...     And([Literal(value=0), Literal(value=1)]),
...     Or([Literal(value=2), Literal(value=3)])
... ]
>>> children, parent_indices = executor.apply(rules, [None, None])
>>> len(children)
2
Source code in hgp_lib\crossover\crossover_executor.py
  9
 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
 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
class CrossoverExecutor:
    """
    Coordinates subtree crossover operations across a collection of `Rule` trees.

    The executor randomly selects pairs of rules based on `crossover_p`, then exchanges
    subtrees between the paired parents to produce offspring. An optional validator can
    reject invalid children, with configurable retry attempts.

    Args:
        crossover_p (float, optional):
            Probability of selecting each rule for crossover. Default: `0.7`.
        crossover_strategy (str, optional):
            Strategy for pairing rules. Must be `"best"` or `"random"`. Default: `"random"`.
        check_valid (Callable[[Rule], bool] | None, optional):
            Optional validator executed after crossover. When supplied, each child must
            pass validation or the crossover is retried up to `num_tries` times. All children that
            pass validation are kept until two children pass validation. Default: `None`.
            Note: The validator is called once during initialization to verify it returns a bool.
            Stateful validators should account for this extra call.
        num_tries (int, optional):
            Maximum number of crossover attempts per pair when validation fails.
            Must be `1` when no validator is provided. Default: `1`.
        operator_p (float, optional):
            Probability of selecting an operator node (vs. a literal) when choosing
            a crossover 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.crossover import CrossoverExecutor
        >>> from hgp_lib.rules import And, Or, Literal
        >>> random.seed(42); np.random.seed(42)
        >>> executor = CrossoverExecutor(crossover_p=1.0)
        >>> rules = [
        ...     And([Literal(value=0), Literal(value=1)]),
        ...     Or([Literal(value=2), Literal(value=3)])
        ... ]
        >>> children, parent_indices = executor.apply(rules, [None, None])
        >>> len(children)
        2
    """

    def __init__(
        self,
        crossover_p: float = 0.7,
        crossover_strategy: str = "random",
        check_valid: Callable[[Rule], bool] | None = None,
        num_tries: int = 1,
        operator_p: float = 0.9,
    ):
        # crossover_p was checked
        # crossover_strategy was checked
        # check_valid was checked
        # num_tries was checked, but not with check_valid
        # operator_p was checked

        self.crossover_p: float = crossover_p
        self.crossover_strategy: str = crossover_strategy
        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], feature_mappings: List[dict | None] | None = None
    ) -> Tuple[List[Rule], List[int]]:
        """
        Applies crossover to the provided list of rules and returns children with parent tracking.

        Rules are randomly selected for crossover based on `crossover_p`, paired
        consecutively, and their subtrees are exchanged. Before crossover, feature mappings
        are applied to translate rules from child populations (which may use different
        feature indices) into the parent's feature space.

        This method supports hierarchical GP by tracking which parent rules contributed
        to each child, enabling score propagation back to child populations.

        Args:
            rules (List[Rule]):
                The collection of parent rules that will undergo crossover. May include
                rules from both the current population and child populations.
            feature_mappings (List[dict | None] | None):
                A list of feature mapping dictionaries, one per rule. Each mapping translates
                feature indices from a child population's space to the parent's space.
                Use None for rules that don't need remapping (i.e., from the current population).
                Default: `None`.
        Returns:
            Tuple[List[Rule], List[int]]: A tuple containing:
                - List[Rule]: The children produced by crossover operations.
                - List[int]: The indices of parent rules that contributed to each child.
                  For each child, both parent indices are recorded (so the list length
                  is 2 * number of children).

        Examples:
            >>> import random
            >>> import numpy as np
            >>> from hgp_lib.crossover import CrossoverExecutor
            >>> from hgp_lib.rules import And, Or, Literal
            >>> random.seed(42); np.random.seed(42)
            >>> executor = CrossoverExecutor(crossover_p=1.0)
            >>> rules = [
            ...     And([Literal(value=0), Literal(value=1)]),
            ...     Or([Literal(value=2), Literal(value=3)])
            ... ]
            >>> children, parent_indices = executor.apply(rules, [None, None])
            >>> len(children)
            2
        """
        n = len(rules)
        if n == 0:
            return [], []

        if feature_mappings is None:
            feature_mappings = [None] * n

        if self.crossover_strategy == "random":
            k = np.random.binomial(n, self.crossover_p)
            k -= k % 2
            eligible_indices = np.random.permutation(n)[:k]
        else:
            # self.crossover_strategy == "best"
            raise NotImplementedError()

        children = []
        parent_indices = []
        for i1, i2 in eligible_indices.reshape(-1, 2):
            parent_a = apply_feature_mapping(rules[i1], feature_mappings[i1])
            parent_b = apply_feature_mapping(rules[i2], feature_mappings[i2])
            ch = self.crossover(parent_a, parent_b)
            children.extend(ch)
            parent_indices.extend((i1, i2) * len(ch))

        return children, parent_indices

    def crossover(self, parent_a: Rule, parent_b: Rule) -> Sequence[Rule]:
        """
        Performs subtree crossover between two parent rules.

        A random node is selected from each parent, and the subtrees rooted at those
        nodes are exchanged using `deep_swap`. When a validator is provided, each child
        is validated individually and accepted children are collected until at least two pass
        validation or `num_tries` attempts are exhausted.

        Args:
            parent_a (Rule): First parent rule.
            parent_b (Rule): Second parent rule.

        Returns:
            Sequence[Rule]: Children with exchanged subtrees. Returns two children when
                no validator is provided, or up to two valid children when a validator
                is used.

        Examples:
            >>> import random
            >>> from hgp_lib.crossover import CrossoverExecutor
            >>> from hgp_lib.rules import And, Or, Literal
            >>> random.seed(0)
            >>> executor = CrossoverExecutor()
            >>> parent_a = And([Literal(value=0), Literal(value=1)])
            >>> parent_b = Or([Literal(value=2), Literal(value=3)])
            >>> child_a, child_b = executor.crossover(parent_a, parent_b)
            >>> parent_a is child_a
            False
        """
        accepted = []
        for _ in range(self.num_tries):
            child_a, child_b = parent_a.copy(), parent_b.copy()
            # node_a = random.choice(child_a.flatten())
            # node_b = random.choice(child_b.flatten())
            deep_swap(
                select_crossover_point(child_a, operator_p=self.operator_p),
                select_crossover_point(child_b, operator_p=self.operator_p),
            )

            if self.check_valid is None:
                return child_a, child_b

            if self.check_valid(child_a):
                accepted.append(child_a)
            if self.check_valid(child_b):
                accepted.append(child_b)
            if len(accepted) >= 2:
                break
        return accepted

apply(rules, feature_mappings=None)

Applies crossover to the provided list of rules and returns children with parent tracking.

Rules are randomly selected for crossover based on crossover_p, paired consecutively, and their subtrees are exchanged. Before crossover, feature mappings are applied to translate rules from child populations (which may use different feature indices) into the parent's feature space.

This method supports hierarchical GP by tracking which parent rules contributed to each child, enabling score propagation back to child populations.

Parameters:

Name Type Description Default
rules List[Rule]

The collection of parent rules that will undergo crossover. May include rules from both the current population and child populations.

required
feature_mappings List[dict | None] | None

A list of feature mapping dictionaries, one per rule. Each mapping translates feature indices from a child population's space to the parent's space. Use None for rules that don't need remapping (i.e., from the current population). Default: None.

None

Returns: Tuple[List[Rule], List[int]]: A tuple containing: - List[Rule]: The children produced by crossover operations. - List[int]: The indices of parent rules that contributed to each child. For each child, both parent indices are recorded (so the list length is 2 * number of children).

Examples:

>>> import random
>>> import numpy as np
>>> from hgp_lib.crossover import CrossoverExecutor
>>> from hgp_lib.rules import And, Or, Literal
>>> random.seed(42); np.random.seed(42)
>>> executor = CrossoverExecutor(crossover_p=1.0)
>>> rules = [
...     And([Literal(value=0), Literal(value=1)]),
...     Or([Literal(value=2), Literal(value=3)])
... ]
>>> children, parent_indices = executor.apply(rules, [None, None])
>>> len(children)
2
Source code in hgp_lib\crossover\crossover_executor.py
 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
def apply(
    self, rules: List[Rule], feature_mappings: List[dict | None] | None = None
) -> Tuple[List[Rule], List[int]]:
    """
    Applies crossover to the provided list of rules and returns children with parent tracking.

    Rules are randomly selected for crossover based on `crossover_p`, paired
    consecutively, and their subtrees are exchanged. Before crossover, feature mappings
    are applied to translate rules from child populations (which may use different
    feature indices) into the parent's feature space.

    This method supports hierarchical GP by tracking which parent rules contributed
    to each child, enabling score propagation back to child populations.

    Args:
        rules (List[Rule]):
            The collection of parent rules that will undergo crossover. May include
            rules from both the current population and child populations.
        feature_mappings (List[dict | None] | None):
            A list of feature mapping dictionaries, one per rule. Each mapping translates
            feature indices from a child population's space to the parent's space.
            Use None for rules that don't need remapping (i.e., from the current population).
            Default: `None`.
    Returns:
        Tuple[List[Rule], List[int]]: A tuple containing:
            - List[Rule]: The children produced by crossover operations.
            - List[int]: The indices of parent rules that contributed to each child.
              For each child, both parent indices are recorded (so the list length
              is 2 * number of children).

    Examples:
        >>> import random
        >>> import numpy as np
        >>> from hgp_lib.crossover import CrossoverExecutor
        >>> from hgp_lib.rules import And, Or, Literal
        >>> random.seed(42); np.random.seed(42)
        >>> executor = CrossoverExecutor(crossover_p=1.0)
        >>> rules = [
        ...     And([Literal(value=0), Literal(value=1)]),
        ...     Or([Literal(value=2), Literal(value=3)])
        ... ]
        >>> children, parent_indices = executor.apply(rules, [None, None])
        >>> len(children)
        2
    """
    n = len(rules)
    if n == 0:
        return [], []

    if feature_mappings is None:
        feature_mappings = [None] * n

    if self.crossover_strategy == "random":
        k = np.random.binomial(n, self.crossover_p)
        k -= k % 2
        eligible_indices = np.random.permutation(n)[:k]
    else:
        # self.crossover_strategy == "best"
        raise NotImplementedError()

    children = []
    parent_indices = []
    for i1, i2 in eligible_indices.reshape(-1, 2):
        parent_a = apply_feature_mapping(rules[i1], feature_mappings[i1])
        parent_b = apply_feature_mapping(rules[i2], feature_mappings[i2])
        ch = self.crossover(parent_a, parent_b)
        children.extend(ch)
        parent_indices.extend((i1, i2) * len(ch))

    return children, parent_indices

crossover(parent_a, parent_b)

Performs subtree crossover between two parent rules.

A random node is selected from each parent, and the subtrees rooted at those nodes are exchanged using deep_swap. When a validator is provided, each child is validated individually and accepted children are collected until at least two pass validation or num_tries attempts are exhausted.

Parameters:

Name Type Description Default
parent_a Rule

First parent rule.

required
parent_b Rule

Second parent rule.

required

Returns:

Type Description
Sequence[Rule]

Sequence[Rule]: Children with exchanged subtrees. Returns two children when no validator is provided, or up to two valid children when a validator is used.

Examples:

>>> import random
>>> from hgp_lib.crossover import CrossoverExecutor
>>> from hgp_lib.rules import And, Or, Literal
>>> random.seed(0)
>>> executor = CrossoverExecutor()
>>> parent_a = And([Literal(value=0), Literal(value=1)])
>>> parent_b = Or([Literal(value=2), Literal(value=3)])
>>> child_a, child_b = executor.crossover(parent_a, parent_b)
>>> parent_a is child_a
False
Source code in hgp_lib\crossover\crossover_executor.py
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
def crossover(self, parent_a: Rule, parent_b: Rule) -> Sequence[Rule]:
    """
    Performs subtree crossover between two parent rules.

    A random node is selected from each parent, and the subtrees rooted at those
    nodes are exchanged using `deep_swap`. When a validator is provided, each child
    is validated individually and accepted children are collected until at least two pass
    validation or `num_tries` attempts are exhausted.

    Args:
        parent_a (Rule): First parent rule.
        parent_b (Rule): Second parent rule.

    Returns:
        Sequence[Rule]: Children with exchanged subtrees. Returns two children when
            no validator is provided, or up to two valid children when a validator
            is used.

    Examples:
        >>> import random
        >>> from hgp_lib.crossover import CrossoverExecutor
        >>> from hgp_lib.rules import And, Or, Literal
        >>> random.seed(0)
        >>> executor = CrossoverExecutor()
        >>> parent_a = And([Literal(value=0), Literal(value=1)])
        >>> parent_b = Or([Literal(value=2), Literal(value=3)])
        >>> child_a, child_b = executor.crossover(parent_a, parent_b)
        >>> parent_a is child_a
        False
    """
    accepted = []
    for _ in range(self.num_tries):
        child_a, child_b = parent_a.copy(), parent_b.copy()
        # node_a = random.choice(child_a.flatten())
        # node_b = random.choice(child_b.flatten())
        deep_swap(
            select_crossover_point(child_a, operator_p=self.operator_p),
            select_crossover_point(child_b, operator_p=self.operator_p),
        )

        if self.check_valid is None:
            return child_a, child_b

        if self.check_valid(child_a):
            accepted.append(child_a)
        if self.check_valid(child_b):
            accepted.append(child_b)
        if len(accepted) >= 2:
            break
    return accepted

hgp_lib.crossover.crossover_factory.CrossoverExecutorFactory

Factory for creating configured CrossoverExecutor instances.

This factory encapsulates all crossover configuration parameters except the optional check_valid callable, which is supplied at creation time. This is useful when the same crossover configuration is reused with different validation strategies.

Parameters:

Name Type Description Default
crossover_p float

Probability of selecting each rule for crossover. Default: 0.7.

0.7
crossover_strategy str

Strategy for pairing rules. Must be "best" or "random". Default: "random".

'random'
num_tries int

Maximum number of crossover attempts per pair when validation fails. Must be 1 when no validator is provided at creation time. Default: 1.

1
operator_p float

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

0.9

Examples:

>>> import random
>>> import numpy as np
>>> from hgp_lib.crossover import CrossoverExecutorFactory
>>> from hgp_lib.rules import And, Or, Literal
>>> factory = CrossoverExecutorFactory(crossover_p=1.0)
>>> def validator(rule):
...     return True
>>> executor = factory.create(validator)
>>> rules = [
...     And([Literal(value=0), Literal(value=1)]),
...     Or([Literal(value=2), Literal(value=3)])
... ]
>>> children, parent_indices = executor.apply(rules, [None, None])
>>> len(children)
2
>>> # Without validator
>>> factory = CrossoverExecutorFactory(crossover_p=1.0, num_tries=1)
>>> executor = factory.create(None)
>>> len(executor.apply(rules, [None, None])[0])
2
Source code in hgp_lib\crossover\crossover_factory.py
  8
  9
 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
 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
class CrossoverExecutorFactory:
    """
    Factory for creating configured `CrossoverExecutor` instances.

    This factory encapsulates all crossover configuration parameters except
    the optional `check_valid` callable, which is supplied at creation time.
    This is useful when the same crossover configuration is reused with
    different validation strategies.

    Args:
        crossover_p (float, optional):
            Probability of selecting each rule for crossover. Default: `0.7`.
        crossover_strategy (str, optional):
            Strategy for pairing rules. Must be `"best"` or `"random"`. Default: `"random"`.
        num_tries (int, optional):
            Maximum number of crossover attempts per pair when validation fails.
            Must be `1` when no validator is provided at creation time. Default: `1`.
        operator_p (float, optional):
            Probability of selecting an operator node (vs. a literal) when choosing
            a crossover 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.crossover import CrossoverExecutorFactory
        >>> from hgp_lib.rules import And, Or, Literal
        >>> factory = CrossoverExecutorFactory(crossover_p=1.0)
        >>> def validator(rule):
        ...     return True
        >>> executor = factory.create(validator)
        >>> rules = [
        ...     And([Literal(value=0), Literal(value=1)]),
        ...     Or([Literal(value=2), Literal(value=3)])
        ... ]
        >>> children, parent_indices = executor.apply(rules, [None, None])
        >>> len(children)
        2

        >>> # Without validator
        >>> factory = CrossoverExecutorFactory(crossover_p=1.0, num_tries=1)
        >>> executor = factory.create(None)
        >>> len(executor.apply(rules, [None, None])[0])
        2
    """

    def __init__(
        self,
        crossover_p: float = 0.7,
        crossover_strategy: str = "random",
        num_tries: int = 1,
        operator_p: float = 0.9,
    ):
        check_isinstance(crossover_p, float)
        check_isinstance(crossover_strategy, str)
        check_isinstance(num_tries, int)
        check_isinstance(operator_p, float)

        if crossover_p < 0.0 or crossover_p > 1.0:
            raise ValueError(
                f"crossover_p must be a float between 0.0 and 1.0, is '{crossover_p}'"
            )

        if operator_p < 0.0 or operator_p > 1.0:
            raise ValueError(
                f"operator_p must be a float between 0.0 and 1.0, is '{operator_p}'"
            )

        accepted_strategies = ("best", "random")
        if crossover_strategy not in accepted_strategies:
            raise ValueError(
                f"crossover_strategy must be one of {accepted_strategies}, is '{crossover_strategy}'"
            )

        if num_tries < 1:
            raise ValueError(f"num_tries must be greater than 0, is '{num_tries}'")

        self.crossover_p: float = crossover_p
        self.crossover_strategy: str = crossover_strategy
        self.num_tries: int = num_tries
        self.operator_p: float = operator_p

    def create(
        self, check_valid: Callable[[Rule], bool] | None = None
    ) -> CrossoverExecutor:
        """
        Create a new `CrossoverExecutor` using the factory configuration.

        Args:
            check_valid (Callable[[Rule], bool] | None, optional):
                Optional validator executed after crossover. When supplied, each
                child must pass validation or the crossover is retried up to
                `num_tries` times. Default: `None`.

        Returns:
            CrossoverExecutor:
                A configured crossover executor instance.

        Raises:
            ValueError:
                If `num_tries > 1` and `check_valid` is `None`.

        Examples:
            >>> factory = CrossoverExecutorFactory(crossover_p=1.0)
            >>> executor = factory.create(lambda r: True)
            >>> isinstance(executor, CrossoverExecutor)
            True
        """
        if check_valid is None and self.num_tries > 1:
            raise ValueError("num_tries must be 1 if check_valid is None")

        return CrossoverExecutor(
            crossover_p=self.crossover_p,
            crossover_strategy=self.crossover_strategy,
            check_valid=check_valid,
            num_tries=self.num_tries,
            operator_p=self.operator_p,
        )

create(check_valid=None)

Create a new CrossoverExecutor using the factory configuration.

Parameters:

Name Type Description Default
check_valid Callable[[Rule], bool] | None

Optional validator executed after crossover. When supplied, each child must pass validation or the crossover is retried up to num_tries times. Default: None.

None

Returns:

Name Type Description
CrossoverExecutor CrossoverExecutor

A configured crossover executor instance.

Raises:

Type Description
ValueError

If num_tries > 1 and check_valid is None.

Examples:

>>> factory = CrossoverExecutorFactory(crossover_p=1.0)
>>> executor = factory.create(lambda r: True)
>>> isinstance(executor, CrossoverExecutor)
True
Source code in hgp_lib\crossover\crossover_factory.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
def create(
    self, check_valid: Callable[[Rule], bool] | None = None
) -> CrossoverExecutor:
    """
    Create a new `CrossoverExecutor` using the factory configuration.

    Args:
        check_valid (Callable[[Rule], bool] | None, optional):
            Optional validator executed after crossover. When supplied, each
            child must pass validation or the crossover is retried up to
            `num_tries` times. Default: `None`.

    Returns:
        CrossoverExecutor:
            A configured crossover executor instance.

    Raises:
        ValueError:
            If `num_tries > 1` and `check_valid` is `None`.

    Examples:
        >>> factory = CrossoverExecutorFactory(crossover_p=1.0)
        >>> executor = factory.create(lambda r: True)
        >>> isinstance(executor, CrossoverExecutor)
        True
    """
    if check_valid is None and self.num_tries > 1:
        raise ValueError("num_tries must be 1 if check_valid is None")

    return CrossoverExecutor(
        crossover_p=self.crossover_p,
        crossover_strategy=self.crossover_strategy,
        check_valid=check_valid,
        num_tries=self.num_tries,
        operator_p=self.operator_p,
    )