Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions pymoo/operators/mutation/bitflip.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,62 @@


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
# 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 = min(0.5, 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)``.
For ``n_var >= 2`` this equals ``1 / n_var``, satisfying the theoretical expectation
: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)
Expand Down
23 changes: 23 additions & 0 deletions tests/operators/test_mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,26 @@ 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():
"""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

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))
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