From 0a11918711d1c1c6b689457c2cd9cea3c265b409 Mon Sep 17 00:00:00 2001 From: cofer Date: Tue, 13 Oct 2026 13:10:28 +0200 Subject: [PATCH 1/2] docs(mutation): document BitflipMutation parameter hierarchy and add regression test --- pymoo/operators/mutation/bitflip.py | 53 +++++++++++++++++++++++++++++ tests/operators/test_mutation.py | 16 +++++++++ 2 files changed, 69 insertions(+) diff --git a/pymoo/operators/mutation/bitflip.py b/pymoo/operators/mutation/bitflip.py index ad687e9d2..810dbb1e4 100644 --- a/pymoo/operators/mutation/bitflip.py +++ b/pymoo/operators/mutation/bitflip.py @@ -6,6 +6,59 @@ class BitflipMutation(Mutation): + """Bitflip mutation operator for binary chromosomes. + + Flips bits in binary genotypes with an independent probability per locus. + + In evolutionary computation literature, mutation probability (:math:`p_m`) + almost universally refers to the per-variable (per-bit) flip rate (typically + :math:`1 / n_{\\text{var}}`). In PyMoo, mutation operates on two hierarchical levels: + + 1. **Chromosome level (``prob``)**: The probability that an offspring individual + undergoes the mutation operator at all. Defaults to ``1.0`` (all offspring are evaluated). + 2. **Gene level (``prob_var``)**: The probability that each individual bit is flipped. + Defaults to ``min(0.5, 1 / n_var)``. + + .. warning:: + **Compound Probability Trap:** + Do not pass your per-locus mutation rate (e.g. :math:`1/n`) as the first positional + argument ``prob`` without specifying ``prob_var``: + + .. code-block:: python + + # WRONG: Effective per-bit rate degrades to (1/n) * (1/n) = 1/n^2! + mutation = BitflipMutation(1.0 / n_var) + + # CORRECT: Evaluates every offspring with an independent 1/n per-bit rate: + mutation = BitflipMutation(prob=1.0, prob_var=1.0 / n_var) + + # ALSO CORRECT (relies on PyMoo's default prob_var = 1/n_var): + mutation = BitflipMutation(prob=1.0) + + Parameters + ---------- + prob : float, optional (default=1.0) + Probability that an individual undergoes the bitflip mutation routine. + In canonical genetic algorithms, this is set to ``1.0`` so that every offspring + is subject to per-locus flipping. + prob_var : float, optional (default=None) + Probability of flipping each individual bit. If ``None``, defaults to + ``min(0.5, 1.0 / problem.n_var)``, which satisfies the theoretical expectation of + :math:`\\mathbb{E}[\\text{flips}] = 1.0` bit per individual. + + Examples + -------- + >>> from pymoo.operators.mutation.bitflip import BitflipMutation + >>> # Standard GA configuration (E[flips] = 1.0 bit per chromosome): + >>> mutation = BitflipMutation(prob=1.0) + >>> + >>> # Explicit per-locus rate (e.g. 5% flip probability per bit): + >>> mutation = BitflipMutation(prob=1.0, prob_var=0.05) + """ + + def __init__(self, prob=1.0, prob_var=None, **kwargs): + super().__init__(prob=prob, prob_var=prob_var, **kwargs) + def _do(self, problem, X, random_state=None, **kwargs): prob_var = self.get_prob_var(problem, size=(len(X), 1)) Xp = np.copy(X) diff --git a/tests/operators/test_mutation.py b/tests/operators/test_mutation.py index 79bb3c3da..8f0c70bb8 100644 --- a/tests/operators/test_mutation.py +++ b/tests/operators/test_mutation.py @@ -41,3 +41,19 @@ def test_inversion_mutation(): end = 3 mut = inversion_mutation(y, seq=(start, end)) np.testing.assert_allclose(mut, np.array([1, 4, 3, 2, 5])) + + +def test_bitflip_mutation_expectation(): + from pymoo.core.problem import Problem + from pymoo.core.population import Population + + class DummyBinaryProblem(Problem): + def __init__(self, n_var=100): + super().__init__(n_var=n_var, n_obj=1, xl=0, xu=1, vtype=bool) + + problem = DummyBinaryProblem(n_var=100) + mut = BitflipMutation(prob=1.0) + pop = Population.new("X", np.zeros((400, 100), dtype=bool)) + mutated = mut.do(problem, pop).get("X") + mean_flips = np.mean(np.sum(mutated, axis=1)) + assert 0.75 <= mean_flips <= 1.25 From d6dc74d3e7e0cb82f312eecbdd8e08ab013ae15e Mon Sep 17 00:00:00 2001 From: cofer Date: Tue, 13 Oct 2026 13:33:04 +0200 Subject: [PATCH 2/2] fix(review): account for 0.5 prob_var cap in docstring; seed statistical test for CI determinism --- pymoo/operators/mutation/bitflip.py | 9 ++++++--- tests/operators/test_mutation.py | 9 ++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/pymoo/operators/mutation/bitflip.py b/pymoo/operators/mutation/bitflip.py index 810dbb1e4..7683aee5b 100644 --- a/pymoo/operators/mutation/bitflip.py +++ b/pymoo/operators/mutation/bitflip.py @@ -26,13 +26,15 @@ class BitflipMutation(Mutation): .. code-block:: python - # WRONG: Effective per-bit rate degrades to (1/n) * (1/n) = 1/n^2! + # WRONG: Effective per-bit rate degrades to + # prob * min(0.5, 1/n_var) = (1/n_var) * min(0.5, 1/n_var) + # For n_var >= 2 this equals 1/n_var^2, which is near-zero. mutation = BitflipMutation(1.0 / n_var) # CORRECT: Evaluates every offspring with an independent 1/n per-bit rate: mutation = BitflipMutation(prob=1.0, prob_var=1.0 / n_var) - # ALSO CORRECT (relies on PyMoo's default prob_var = 1/n_var): + # ALSO CORRECT (relies on PyMoo's default prob_var = min(0.5, 1/n_var)): mutation = BitflipMutation(prob=1.0) Parameters @@ -43,7 +45,8 @@ class BitflipMutation(Mutation): is subject to per-locus flipping. prob_var : float, optional (default=None) Probability of flipping each individual bit. If ``None``, defaults to - ``min(0.5, 1.0 / problem.n_var)``, which satisfies the theoretical expectation of + ``min(0.5, 1.0 / problem.n_var)``. + For ``n_var >= 2`` this equals ``1 / n_var``, satisfying the theoretical expectation :math:`\\mathbb{E}[\\text{flips}] = 1.0` bit per individual. Examples diff --git a/tests/operators/test_mutation.py b/tests/operators/test_mutation.py index 8f0c70bb8..978a38cc0 100644 --- a/tests/operators/test_mutation.py +++ b/tests/operators/test_mutation.py @@ -44,6 +44,12 @@ def test_inversion_mutation(): def test_bitflip_mutation_expectation(): + """Regression test: default BitflipMutation(prob=1.0) flips ~1 bit per chromosome. + + Uses a fixed random state to ensure the test is deterministic in CI. + For n_var=100 and 400 individuals, the CLT gives a 99.9% confidence + interval well within [0.75, 1.25]. + """ from pymoo.core.problem import Problem from pymoo.core.population import Population @@ -54,6 +60,7 @@ def __init__(self, n_var=100): problem = DummyBinaryProblem(n_var=100) mut = BitflipMutation(prob=1.0) pop = Population.new("X", np.zeros((400, 100), dtype=bool)) - mutated = mut.do(problem, pop).get("X") + rng = np.random.default_rng(seed=42) + mutated = mut.do(problem, pop, random_state=rng).get("X") mean_flips = np.mean(np.sum(mutated, axis=1)) assert 0.75 <= mean_flips <= 1.25