saealib.operators.pymoo_mutation のソースコード

"""Adapter exposing a constructed pymoo mutation operator as saealib's Mutation."""

from __future__ import annotations

from typing import Protocol

import numpy as np

from saealib.operators.mutation import Mutation


class _PymooMutationLike(Protocol):
    """Structural interface of a ``pymoo.core.mutation.Mutation`` instance."""

    def _do(
        self,
        problem: object,
        x: np.ndarray,
        *args: object,
        random_state: object = None,
        **kwargs: object,
    ) -> np.ndarray:
        """Apply the mutation to a batch of individuals, (n, dim) -> (n, dim)."""
        ...


[ドキュメント] class PymooMutation(Mutation): """ Adapter wrapping a pymoo mutation operator as a saealib ``Mutation``. Lets researchers who already have a pymoo ``Mutation`` (e.g. ``PM()``) reuse it unchanged inside saealib's ``GA``. Parameters ---------- operator : pymoo.core.mutation.Mutation An already-constructed pymoo mutation operator instance. prob : float, optional Individual-level mutation probability. Unlike ``PymooCrossover``, this class applies the gate itself inside ``mutate_batch()``. Defaults to 1.0. Notes ----- ``prob_var`` (pymoo's per-variable mutation probability) is deliberately **not** mirrored from the wrapped operator: pymoo stores it as a ``pymoo.core.variable.Real`` or ``None``, and the wrapped operator's ``_do()`` resolves its own per-variable rate internally regardless of what saealib's ``prob_var`` attribute says. ``self.prob_var`` stays ``None`` here so that any saealib code reading it (e.g. mixed-variable routing in ``GA``) falls back to its own default rather than seeing a foreign, potentially non-``float`` value. ``mutate_batch()`` calls the wrapped operator's ``_do()`` at most once, on the gated subset, and forwards ``rng`` via pymoo's ``random_state`` parameter for reproducibility. The inherited ``mutate()`` derives a single-row call from that batch implementation. A minimal cached pymoo ``Problem`` shim is synthesized the same way. """
[ドキュメント] def __init__(self, operator: _PymooMutationLike, *, prob: float = 1.0) -> None: super().__init__() self.operator = operator self.prob = prob self.prob_var = None self._problem: object | None = None self._problem_key: tuple[int, bytes, bytes] | None = None
def _pymoo_problem(self, dim: int, mutate_range: tuple) -> object: """Return a cached pymoo ``Problem`` shim, rebuilt when dim/bounds change.""" from pymoo.core.problem import Problem as PymooProblem lb = np.asarray(mutate_range[0], dtype=float) ub = np.asarray(mutate_range[1], dtype=float) key = (dim, lb.tobytes(), ub.tobytes()) if key != self._problem_key: self._problem = PymooProblem(n_var=dim, n_obj=1, xl=lb, xu=ub) self._problem_key = key return self._problem
[ドキュメント] def mutate_batch( self, candidates_batch: np.ndarray, mutate_range: tuple, rng: np.random.Generator = np.random.default_rng(), ) -> np.ndarray: """ Execute mutation on a batch of candidates via a single pymoo call. Unlike ``Crossover.crossover_batch``, this method owns its own ``prob`` gate: it draws one gate value per row (``rng.random(n) < self.prob``, mirroring ``mutate()``'s per-call ``rng.random() >= self.prob`` check) and only mutates the gated rows; ungated rows are returned unchanged. The wrapped operator's ``_do()`` is called at most once, on only the gated subset. If no row is gated (``gate.sum() == 0``), ``_do()`` is not called at all, avoiding a zero-length-batch call into the wrapped pymoo operator. Parameters ---------- candidates_batch : np.ndarray Batch of candidate individuals. shape = (n, dim) mutate_range : tuple Tuple of (lower_bound, upper_bound) for mutation. rng : np.random.Generator, optional Forwarded to the wrapped operator as ``random_state``. Returns ------- np.ndarray Mutated individuals. shape = (n, dim) Notes ----- For more than one gated row, a loop of separate single-row ``mutate_batch`` calls (equivalently, separate :meth:`mutate` calls) does not reproduce the same per-row results as one batched call with the same seeded ``rng``. Pymoo operators such as PM draw each random phase across the whole gated batch, whereas single-row calls interleave phases per row. """ candidates_batch = np.asarray(candidates_batch, dtype=float) n = candidates_batch.shape[0] gate = rng.random(n) < self.prob result = candidates_batch.copy() if not np.any(gate): return result problem = self._pymoo_problem(candidates_batch.shape[-1], mutate_range) mutated = self.operator._do(problem, candidates_batch[gate], random_state=rng) result[gate] = np.asarray(mutated, dtype=float) return result