--- primary_layer: layer3 page_type: concept --- # Mutation `GA` (`saealib.GA`) delegates adding perturbation to the offspring generated by crossover to `Mutation`, a swappable operator. To change the mutation scheme, you only need to swap out this `Mutation`, not `GA` itself. ## Mutation's role `Mutation` requires only one method, `mutate_batch(candidates_batch, mutate_range, rng=...)`, to be implemented. It receives candidate individuals with shape `(n, dim)` and returns mutated individuals with the same shape. `mutate_range` receives the design variables' lower and upper bounds as a `(lb, ub)` tuple. The base class derives the convenience method `mutate(p, mutate_range, rng=...)` from a single-row call to `mutate_batch()`. `GA` uses this convenience method under `variation_execution="sequential"`. Overriding only `mutate()` has no effect in the default batch mode; see [Algorithm](algorithm.md) and the `GA` API reference for `variation_execution`. The individual-level mutation probability is held by the class attribute `prob`. Whereas for `Crossover` the decision of whether to perform crossover is `GA`'s responsibility, each `Mutation` implementation makes its own mutation decision. Every `mutate_batch()` implementation must draw one gate per row and leave ungated rows unchanged. This also supplies the self-gate for the inherited single-row `mutate()`. `GA` therefore calls the selected mutation method unconditionally and doesn't pre-filter candidates; a custom `Mutation` must implement this `prob` gate itself. The other class attribute, `prob_var`, is the per-variable mutation probability. When `None`, built-in mutations resolve it to `min(0.5, 1/dim)` when `mutate_batch()` is called. ## Built-in Mutations | Class | Parameters | Characteristics | |---|---|---| | `MutationUniform` | `prob, *, prob_var=None` | Replaces a chosen dimension with a uniform random value from `[lb, ub]` | | `MutationGaussian` | `prob, *, sigma, prob_var=None` | Adds `N(0, sigma)` Gaussian perturbation to a chosen dimension {cite}`rechenberg1973es` | | `MutationPolynomial` | `prob, *, eta, prob_var=None` | Polynomial mutation {cite}`deb2001mooea`. Larger `eta` gives a smaller perturbation | | `MutationIntegerUniform` | `prob, *, prob_var=None` | Replaces with a uniform random integer in `[lb[i], ub[i]]` (inclusive). For integer variables | | `MutationCategorical` | `prob, *, prob_var=None` | Replaces the category index with a uniform random integer from `{0, ..., n_categories-1}`. For categorical variables | For problems with only continuous variables, pick one of these and pass it to `GA(mutation=..., ...)`. The basic decision is: choose `MutationUniform` if you want the search's coarseness matched to the variable's own value range, or `MutationGaussian` or `MutationPolynomial` if you want to perturb only the neighborhood of the existing value. `MutationGaussian` specifies the perturbation magnitude directly via `sigma`, whereas `MutationPolynomial` specifies the concentration of the perturbation via `eta` (the distribution index), with the perturbation width relative to the value range determined automatically. `MutationIntegerUniform` and `MutationCategorical` merely share a common private implementation for how the random number is generated (replacement with a uniform random integer within the value range), but are treated as separate classes in the public API. For problems where design variables mix integer and categorical variables, `GA` uses a different `Mutation` instance per variable type. If the `GA` constructor's `integer_mutation`/`categorical_mutation` arguments are omitted, `MutationIntegerUniform`/`MutationCategorical` are supplied automatically (with `prob_var` inherited from the continuous-variable `mutation`). `GA.ask()` splits individuals into columns by variable type, applies each `Mutation` only to its corresponding columns, and then reassembles the results. The default batch mode calls each type's `mutate_batch()` on its columns; sequential mode calls each type's `mutate()` and preserves the earlier per-individual routing and random-number sequence. The correspondence between variable types and `Mutation` is determined by [Problem](../problem_and_ranking/problem.md)'s `variables` argument. ```{note} Only `MutationUniform` is currently `@register()`ed; the other 4 classes are not yet registered with the Registry. Keep this difference in mind if you resolve classes from strings via the Registry. ``` ### External library adapters `PymooMutation(operator, *, prob=1.0)` wraps an already-constructed [pymoo](https://pymoo.org/) mutation operator (e.g. `PM()`) so existing pymoo-based research code can be reused unchanged inside `GA`. Unlike `PymooCrossover`, `prob` is applied by `PymooMutation` itself, matching every built-in `Mutation`'s convention that the individual-level probability check lives inside `mutate_batch()`. `prob_var` is deliberately not mirrored from the wrapped pymoo operator — it stays `None` here, so mixed-variable routing in `GA` falls back to its own default rather than reading a foreign, non-`float` value. In `GA`'s default batch mode, `PymooMutation.mutate_batch()` calls the wrapped operator's `_do()` at most once, on the gated subset. Under `variation_execution="sequential"`, the inherited `mutate()` passes one individual to that batch implementation, so `_do()` is called once per gated individual, exactly as with a direct `mutate()` call. `rng` is forwarded via pymoo's `random_state` parameter. See [Installation](../../getting_started/installation.md) for the `pymoo` extra. ## Extension hooks If you just want to add post-processing, such as rounding values that fall outside the bounds, you can add it to an existing `Mutation` instance with `with_post(fn)` instead of creating a new subclass. `with_post` doesn't modify the original instance — it returns a copy with `fn` added. ```python import numpy as np from saealib import MutationUniform def clip_offspring(offspring, mutate_range, rng, ctx=None): lb, ub = mutate_range return np.clip(offspring, lb, ub) base = MutationUniform(prob=1.0) clipped = base.with_post(clip_offspring) ``` `fn`'s signature is `fn(offspring, mutate_range, rng, ctx) -> np.ndarray`, receiving the result of the existing hook (by default an identity function that does nothing) and returning an additional transformation. Calling `with_post` multiple times chains the hooks in the order called. In batch mode, `GA` calls `post_mutation_batch(offspring_batch, mutate_range, rng, ctx)`. Its default implementation calls `post_mutation()` once per individual, so hooks installed by `with_post()` continue to work. Override `post_mutation_batch()` when the post-processing itself should be genuinely vectorized; such an override is responsible for composing any `with_post()` hook it needs to retain. ## Implementing a custom Mutation If you need a custom mutation scheme, subclass `Mutation` and implement `mutate_batch()`. The following example replaces selected dimensions with the midpoint of their value range. ```python import numpy as np from saealib import Mutation class MidpointMutation(Mutation): def __init__(self, prob: float = 1.0, *, prob_var: float | None = None): super().__init__() self.prob = prob self.prob_var = prob_var def mutate_batch(self, candidates_batch, mutate_range, rng=np.random.default_rng()): candidates_batch = np.asarray(candidates_batch, dtype=float) n, dim = candidates_batch.shape p_var = self.prob_var if self.prob_var is not None else min(0.5, 1.0 / dim) gate = rng.random(n) < self.prob result = candidates_batch.copy() if not np.any(gate): return result selected = result[gate] var_gate = rng.random(selected.shape) < p_var lb, ub = mutate_range midpoint = (np.asarray(lb) + np.asarray(ub)) / 2.0 selected[var_gate] = np.broadcast_to(midpoint, selected.shape)[var_gate] result[gate] = selected return result ``` The individual-level gate must contain one draw per row and preserve every ungated row. The variable-level `prob_var` gate is operator-specific but follows the same convention as the built-in classes here. Omitting these checks results in an implementation where every dimension always mutates. ## Related components - [Algorithm](algorithm.md): How `GA` combines `Mutation` - [Crossover](crossover.md): The paired operator called before mutation - [Problem](../problem_and_ranking/problem.md): Defining integer and categorical variables, and their correspondence with mixed-variable Mutations - [Extension guidelines](../extension_guidelines.md): The general design philosophy behind `with_post`-style hooks ## References - {py:class}`saealib.Mutation` - {py:class}`saealib.MutationUniform` - {py:class}`saealib.MutationGaussian` - {py:class}`saealib.MutationPolynomial` - {py:class}`saealib.MutationIntegerUniform` - {py:class}`saealib.MutationCategorical` - {py:class}`saealib.PymooMutation`