Skip to content

Evolutionary computation

Evolutionary computation: genetic algorithms and bit strings on fitness landscapes.

AgedStrategyElement(label, visual=VisualSpec('agents', {'color_by': 'score'}), *, actions=0.0, score=0.0, age=0)

Bases: StrategyElement

Strategy of Robby with an age, which dies of old age or starves in the evolution by circumstance.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'score'})
actions Annotated[object, StateSpec()]

The initial action per situation, an array of 243 integers from 0 to 6.

0.0
score Annotated[float, StateSpec()]

The initial score.

0.0
age Annotated[float, StateSpec(domain=NN)]

The initial age in generations.

0

Examples:

>>> from skcomplex.systems.computer_science.evolutionary_computation import AgedStrategyElement
>>> AgedStrategyElement('strategy', age=3).age
3
Source code in src/skcomplex/systems/computer_science/evolutionary_computation/_genetic_algorithms.py
268
269
270
271
272
273
274
275
276
277
278
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'score'}),
    *,
    actions: Annotated[object, StateSpec()] = 0.0,
    score: Annotated[float, StateSpec()] = 0.0,
    age: Annotated[float, StateSpec(domain=NN)] = 0,
) -> None:
    super().__init__(label, visual=visual, actions=actions, score=score)
    self.age = age

AnnealedCandidateElement(label, x=0.0, y=0.0)

Bases: Element

Candidate solution of the plane, whose coordinates are sampled one state at a time.

Parameters:

Name Type Description Default
label str

The label of the candidate or the group.

required
x Annotated[float, StateSpec()]

The first coordinate.

0.0
y Annotated[float, StateSpec()]

The second coordinate.

0.0

Examples:

>>> from skcomplex.systems.computer_science.evolutionary_computation import AnnealedCandidateElement
>>> AnnealedCandidateElement('candidate', x=1.0).x
1.0
Source code in src/skcomplex/systems/computer_science/evolutionary_computation/_optimisers.py
54
55
56
57
58
59
60
61
62
def __init__(
    self,
    label: str,
    x: Annotated[float, StateSpec()] = 0.0,
    y: Annotated[float, StateSpec()] = 0.0,
) -> None:
    super().__init__(label)
    self.x = x
    self.y = y

BitStringElement(label, visual=VisualSpec('agents', {'color_by': 'fitness'}), *, genome=0.0, fitness=0.0)

Bases: Element

Agent whose genome is a string of bits, stored as the integer it spells, with the fitness of that genome.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'fitness'})
genome Annotated[float, StateSpec(domain=NN)]

The initial genome, an integer whose binary digits are the bits.

0.0
fitness Annotated[float, StateSpec(domain=closed_open(0, oo))]

The initial fitness.

0.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.computer_science.evolutionary_computation import BitStringElement
>>> group = GroupElement('genomes', element=BitStringElement, size=4, values={'genome': [0, 1, 2, 3]})
>>> group.label, group.element.__name__, group.size
('genomes', 'BitStringElement', 4)
Source code in src/skcomplex/systems/computer_science/evolutionary_computation/_landscapes.py
54
55
56
57
58
59
60
61
62
63
64
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'fitness'}),
    *,
    genome: Annotated[float, StateSpec(domain=NN)] = 0.0,
    fitness: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.genome = genome
    self.fitness = fitness

CircumstanceInteraction(label, *, target, lifespan=7, starvation=0.3, mutation=0.005, switch=None)

Bases: Interaction

Evolution by circumstance: strategies die of old age or starve, and random survivors breed in their places.

Every generation the strategies age by one, the ones that reach lifespan die of old age, and the fraction starvation of the population with the lowest scores starves. Random pairs of survivors, whatever their score, exchange the tails of their strategies after a random cut, mutate every action to another one with probability mutation, and place their child, of age 0, in the place of a dead strategy, until the population is whole again. With a switch period, the starvation is on for switch generations and off for the next switch, in turn.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of aged strategies.

required
lifespan int

The age at which a strategy dies.

7
starvation float

The fraction of the population, of the lowest scores, that starves every generation.

0.3
mutation float

The probability that an action of a child is replaced by another one.

0.005
switch int | None

The number of generations after which the starvation switches on or off, or None to keep it on.

None

Examples:

>>> from skcomplex.systems.computer_science.evolutionary_computation import CircumstanceInteraction
>>> CircumstanceInteraction('circumstance', target='strategies').lifespan
7
Source code in src/skcomplex/systems/computer_science/evolutionary_computation/_genetic_algorithms.py
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    lifespan: int = 7,
    starvation: float = 0.3,
    mutation: float = 0.005,
    switch: int | None = None,
) -> None:
    super().__init__(label)
    self.target = target
    self.lifespan = lifespan
    self.starvation = starvation
    self.mutation = mutation
    self.switch = switch

ReproductionInteraction(label, *, target, death=0.1, mutation=0.05, zero_values=(), one_values=())

Bases: EvolutionInteraction

Death at a constant rate and replacement by mutated copies of parents chosen in proportion to their fitness.

It is the death-birth replacement of EvolutionInteraction, with parents drawn among all agents, on genomes stored as the integers their bits spell. A newborn has one random bit flipped with probability mutation. The fitness of a genome is the mean over its loci of a value given to each allele of each locus, an additive landscape with one value for a 0 and one for a 1 at every locus.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of bit strings.

required
death Annotated[float, ParameterSpec(domain=closed(0, 1))]

The probability that an agent dies in a step.

0.1
mutation Annotated[float, ParameterSpec(domain=closed(0, 1))]

The probability that a newborn has one random bit flipped.

0.05
zero_values tuple[float, ...]

The value of a 0 at every locus.

()
one_values tuple[float, ...]

The value of a 1 at every locus.

()

Examples:

>>> from skcomplex.systems.computer_science.evolutionary_computation import ReproductionInteraction
>>> ReproductionInteraction(
...     'reproduction', target='genomes', zero_values=(0.1, 0.2), one_values=(0.9, 0.3)
... ).mutation
0.05
Source code in src/skcomplex/systems/computer_science/evolutionary_computation/_landscapes.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    death: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 0.1,
    mutation: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 0.05,
    zero_values: tuple[float, ...] = (),
    one_values: tuple[float, ...] = (),
) -> None:
    super().__init__(
        label,
        target=target,
        strategy='genome',
        selection='proportional',
        replacement='death-birth',
        mutation=mutation,
        death=death,
        parents='all',
    )
    self.zero_values = zero_values
    self.one_values = one_values

RoomCleaningInteraction(label, *, target, sessions=10, n_actions=100, side=10, density=0.5, phase=None)

Bases: Interaction

Score of every strategy of Robby, the mean of its scores in sessions of cleaning random rooms.

Every strategy cleans sessions random rooms of side side, with a can in each cell with probability density, for n_actions actions, and its state score takes the mean score. A picked can scores 10, picking up nothing scores -1, and bumping into a wall scores -5.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of strategies.

required
sessions int

The number of rooms a strategy cleans to be scored.

10
n_actions int

The number of actions in a session.

100
side int

The side of a room.

10
density float

The probability that a cell holds a can.

0.5
phase str | None

The phase of the step the scoring runs in, or None for the default phase.

None

Examples:

>>> from skcomplex.systems.computer_science.evolutionary_computation import RoomCleaningInteraction
>>> RoomCleaningInteraction('scoring', target='strategies').sessions
10
Source code in src/skcomplex/systems/computer_science/evolutionary_computation/_genetic_algorithms.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    sessions: int = 10,
    n_actions: int = 100,
    side: int = 10,
    density: float = 0.5,
    phase: str | None = None,
) -> None:
    super().__init__(label)
    self.target = target
    self.sessions = sessions
    self.n_actions = n_actions
    self.side = side
    self.density = density
    self.phase = phase

score(strategies, rng)

Return the mean score of strategies in random rooms.

Parameters:

Name Type Description Default
strategies IntArray

The actions of every strategy, one row per strategy.

required
rng Generator

The random number generator of the rooms and the random moves.

required

Returns:

Name Type Description
scores Array

The mean score of every strategy.

Source code in src/skcomplex/systems/computer_science/evolutionary_computation/_genetic_algorithms.py
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def score(self, strategies: IntArray, rng: np.random.Generator) -> Array:
    """Return the mean score of strategies in random rooms.

    Args:
        strategies:
            The actions of every strategy, one row per strategy.

        rng:
            The random number generator of the rooms and the random moves.

    Returns:
        scores:
            The mean score of every strategy.
    """
    return RoomCleaningInteraction._evaluate(
        strategies,
        rng,
        sessions=self.sessions,
        actions=self.n_actions,
        side=self.side,
        density=self.density,
    )

RunningBestElement(label, best=1e+300)

Bases: Element

Lowest objective value of a search so far.

Parameters:

Name Type Description Default
label str

The label of the element.

required
best Annotated[float, StateSpec()]

The value before the search starts.

1e+300

Examples:

>>> from skcomplex.systems.computer_science.evolutionary_computation import RunningBestElement
>>> RunningBestElement('record', best=5.0).best
5.0
Source code in src/skcomplex/systems/computer_science/evolutionary_computation/_optimisers.py
81
82
83
def __init__(self, label: str, best: Annotated[float, StateSpec()] = 1e300) -> None:
    super().__init__(label)
    self.best = best

RunningBestInteraction(label, *, target, source)

Bases: Interaction

Keep the lowest value of an observable, best <- value if value < best else best.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the element that holds best.

required
source Annotated[str, RoleSpec()]

The path of the observable.

required

Examples:

>>> from skcomplex.systems.computer_science.evolutionary_computation import RunningBestInteraction
>>> RunningBestInteraction('update', target='record', source='search/lowest').source
'search/lowest'
Source code in src/skcomplex/systems/computer_science/evolutionary_computation/_optimisers.py
126
127
128
129
130
131
132
133
134
135
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    source: Annotated[str, RoleSpec()],
) -> None:
    super().__init__(label)
    self.target = target
    self.source = source

SearchCandidateElement(label, x=(0.0, 0.0))

Bases: Element

Candidate solution that holds a vector.

Parameters:

Name Type Description Default
label str

The label of the candidate or the group.

required
x Annotated[ArrayLike, StateSpec(vector=True)]

The vector of the candidate.

(0.0, 0.0)

Examples:

>>> from skcomplex.systems.computer_science.evolutionary_computation import SearchCandidateElement
>>> SearchCandidateElement('candidate', x=(0.0, 1.0)).x
(0.0, 1.0)
Source code in src/skcomplex/systems/computer_science/evolutionary_computation/_optimisers.py
102
103
104
def __init__(self, label: str, x: Annotated[npt.ArrayLike, StateSpec(vector=True)] = (0.0, 0.0)) -> None:
    super().__init__(label)
    self.x = x

StrategyElement(label, visual=VisualSpec('agents', {'color_by': 'score'}), *, actions=0.0, score=0.0)

Bases: Element

Strategy of Robby, the action for each of the 243 situations he can see, with its last measured score.

The actions are 0 to 3 for moving north, south, east, and west, 4 to stay, 5 to pick up a can, and 6 to move in a random direction.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'score'})
actions Annotated[object, StateSpec()]

The initial action per situation, an array of 243 integers from 0 to 6.

0.0
score Annotated[float, StateSpec()]

The initial score.

0.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.computer_science.evolutionary_computation import StrategyElement
>>> GroupElement('strategies', element=StrategyElement, size=3).get_params()['size']
3
Source code in src/skcomplex/systems/computer_science/evolutionary_computation/_genetic_algorithms.py
64
65
66
67
68
69
70
71
72
73
74
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'score'}),
    *,
    actions: Annotated[object, StateSpec()] = 0.0,
    score: Annotated[float, StateSpec()] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.actions = actions
    self.score = score

bit_string_evolution(n_agents=100, *, n_bits=8, death=0.1, mutation=0.05, random_state=0)

Return a population of bit strings that evolves on a random additive fitness landscape.

Every step each agent dies with a constant probability, and the dead are replaced by copies of parents chosen in proportion to their fitness, with one bit flipped with a small probability, so the mean fitness climbs.

Parameters:

Name Type Description Default
n_agents int

The size of the population, which stays constant.

100
n_bits int

The number of bits of a genome.

8
death float

The probability that an agent dies in a step.

0.1
mutation float

The probability that a newborn has one random bit flipped.

0.05
random_state int | Generator | None

The seed or generator of the landscape and of the random initial genomes.

0

Returns:

Name Type Description
system System

The system, with the group genomes of states genome and fitness.

Examples:

>>> from skcomplex.systems.computer_science.evolutionary_computation import bit_string_evolution
>>> bit_string_evolution(10, n_bits=4).interactions[0].target
'genomes'
Source code in src/skcomplex/systems/computer_science/evolutionary_computation/_landscapes.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
@register_system(
    field='computer_science/evolutionary_computation',
    tags=('evolution', 'agents', 'fitness landscape', 'genetic algorithm'),
    references=('Downey 2018',),
    defaults={'until': 100},
)
def bit_string_evolution(
    n_agents: int = 100,
    *,
    n_bits: int = 8,
    death: float = 0.1,
    mutation: float = 0.05,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return a population of bit strings that evolves on a random additive fitness landscape.

    Every step each agent dies with a constant probability, and the dead are replaced by copies of parents chosen in
    proportion to their fitness, with one bit flipped with a small probability, so the mean fitness climbs.

    Args:
        n_agents:
            The size of the population, which stays constant.

        n_bits:
            The number of bits of a genome.

        death:
            The probability that an agent dies in a step.

        mutation:
            The probability that a newborn has one random bit flipped.

        random_state:
            The seed or generator of the landscape and of the random initial genomes.

    Returns:
        system:
            The system, with the group `genomes` of states `genome` and `fitness`.

    Examples:
        >>> from skcomplex.systems.computer_science.evolutionary_computation import bit_string_evolution
        >>> bit_string_evolution(10, n_bits=4).interactions[0].target
        'genomes'
    """
    rng = np.random.default_rng(random_state)
    zero_values, one_values = rng.random(n_bits), rng.random(n_bits)
    genomes = rng.integers(0, 2**n_bits, size=n_agents).astype(float)
    fitness = ReproductionInteraction._fitness_of(genomes, zero_values, one_values)
    reproduction = ReproductionInteraction(
        'reproduction',
        target='genomes',
        death=death,
        mutation=mutation,
        zero_values=tuple(zero_values.tolist()),
        one_values=tuple(one_values.tolist()),
    )
    return System(
        elements=[
            GroupElement(
                'genomes',
                element=BitStringElement,
                size=n_agents,
                values={'genome': genomes, 'fitness': fitness},
            ),
        ],
        interactions=[reproduction],
    )

evolutionary_optimisers(n_candidates=40, *, algorithm='differential-evolution', landscape='rastrigin', temperature=5.0, width=0.5, generations=100.0, random_state=0)

Return a population of candidates that searches a two-dimensional landscape, one generation per step.

A population optimiser moves the candidates through OptimizationInteraction, and reports search.best. Simulated annealing samples every coordinate through SamplingInteraction at a temperature that falls geometrically to 1e-3 of its start over generations steps, and tracker/record.best holds the lowest energy it has reached, from the observable search/lowest that the catalog lists, so a run passes it in observables (Raman 2021 §6.3.3.3, §6.3.4).

Parameters:

Name Type Description Default
n_candidates int

The number of candidates.

40
algorithm str

'differential-evolution', 'particle-swarm', 'evolution-strategy', 'covariance-adaptation', or 'annealing'.

'differential-evolution'
landscape str

'rastrigin' or 'rosenbrock'.

'rastrigin'
temperature float

The initial temperature of annealing.

5.0
width float

The standard deviation of a move of annealing.

0.5
generations float

The number of steps over which annealing cools.

100.0
random_state int | Generator | None

The seed or generator of the initial candidates.

0

Returns:

Name Type Description
system System

The system, with the group candidates.

Raises:

Type Description
ValidationError

The algorithm or the landscape is unknown.

Examples:

>>> from skcomplex.systems.computer_science.evolutionary_computation import evolutionary_optimisers
>>> evolutionary_optimisers(10).interactions[0].label
'search'
Source code in src/skcomplex/systems/computer_science/evolutionary_computation/_optimisers.py
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
@register_system(
    field='computer_science/evolutionary_computation',
    tags=('optimisation', 'evolutionary algorithm', 'annealing', 'fitness landscape'),
    references=('Raman 2021', 'Storn and Price 1997', 'Kirkpatrick, Gelatt, and Vecchi 1983'),
    defaults={'until': 100},
    observables=_search_observables,
)
def evolutionary_optimisers(
    n_candidates: int = 40,
    *,
    algorithm: str = 'differential-evolution',
    landscape: str = 'rastrigin',
    temperature: float = 5.0,
    width: float = 0.5,
    generations: float = 100.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return a population of candidates that searches a two-dimensional landscape, one generation per step.

    A population optimiser moves the candidates through `OptimizationInteraction`, and reports `search.best`. Simulated
    annealing samples every coordinate through `SamplingInteraction` at a temperature that falls geometrically to 1e-3
    of its start over `generations` steps, and `tracker/record.best` holds the lowest energy it has reached, from the
    observable `search/lowest` that the catalog lists, so a run passes it in `observables` (Raman 2021 §6.3.3.3,
    §6.3.4).

    Args:
        n_candidates:
            The number of candidates.

        algorithm:
            `'differential-evolution'`, `'particle-swarm'`, `'evolution-strategy'`, `'covariance-adaptation'`, or
            `'annealing'`.

        landscape:
            `'rastrigin'` or `'rosenbrock'`.

        temperature:
            The initial temperature of annealing.

        width:
            The standard deviation of a move of annealing.

        generations:
            The number of steps over which annealing cools.

        random_state:
            The seed or generator of the initial candidates.

    Returns:
        system:
            The system, with the group `candidates`.

    Raises:
        ValidationError:
            The algorithm or the landscape is unknown.

    Examples:
        >>> from skcomplex.systems.computer_science.evolutionary_computation import evolutionary_optimisers
        >>> evolutionary_optimisers(10).interactions[0].label
        'search'
    """
    if (algorithm not in ALGORITHMS and algorithm != ANNEALING) or landscape not in LANDSCAPES:
        names = ', '.join((*ALGORITHMS, ANNEALING))
        error_msg = (
            f'evolutionary_optimisers has algorithm={algorithm!r} and landscape={landscape!r}. Use one of {names}, '
            f'and one of {", ".join(LANDSCAPES)}.'
        )
        raise ValidationError(error_msg)
    energy, objective, bound = LANDSCAPES[landscape]
    start = np.random.default_rng(random_state).uniform(-bound, bound, (n_candidates, N_DIM))
    if algorithm == ANNEALING:
        return _annealing(start, energy, temperature, width, generations)
    candidates = GroupElement('candidates', element=SearchCandidateElement, size=n_candidates, values={'x': start})
    search = OptimizationInteraction(
        'search',
        target='candidates',
        state='x',
        objective=objective,
        bounds=(-bound, bound),
        algorithm=ALGORITHMS[algorithm],
    )
    return System(elements=[candidates], interactions=[search], time=DiscreteTime())

robby_ga(n_strategies=50, *, mutation=0.005, sessions=10, n_actions=100, regime='generational', lifespan=7, starvation=0.3, switch=None, random_state=0)

Return Mitchell's genetic algorithm that evolves strategies for Robby, a robot that collects cans in a room.

Each step is one generation. Parents are selected with probabilities proportional to their rank, every pair of parents exchanges the tails of their strategies after a random cut, and every action of a child mutates to another action with probability mutation. Then every child is scored in random rooms. Random strategies score far below zero because they bump into walls, and evolution finds strategies that avoid walls and collect cans.

Gooding's evolution by circumstance, the regime 'circumstance', replaces the selection with deaths: every strategy starts at a random age, dies at the age lifespan, and the fraction starvation with the lowest scores starves every generation. Random pairs of survivors, whatever their scores, breed children in the places of the dead. The starvation is the only selection, so with it the scores rise as under the genetic algorithm, and without it they drift. With a switch period the starvation switches on and off every switch generations, as in Gooding's runs that switch it every 3000 iterations.

Parameters:

Name Type Description Default
n_strategies int

The size of the population.

50
mutation float

The probability that an action of a child is replaced by another one.

0.005
sessions int

The number of rooms a strategy cleans to be scored.

10
n_actions int

The number of actions in a session.

100
regime str

The evolution, 'generational' for Mitchell's genetic algorithm or 'circumstance' for Gooding's evolution by circumstance.

'generational'
lifespan int

The age at which a strategy dies, in the evolution by circumstance.

7
starvation float

The fraction of the lowest scores that starves every generation, in the evolution by circumstance.

0.3
switch int | None

The number of generations after which the starvation switches on or off, or None to keep it on, in the evolution by circumstance.

None
random_state int | Generator | None

The seed or generator of the random first generation and of its scores.

0

Returns:

Name Type Description
system System

The system, with the group strategies of states actions and score, and age in the evolution by circumstance.

Raises:

Type Description
ParameterError

The regime is unknown.

Examples:

>>> from skcomplex.systems.computer_science.evolutionary_computation import robby_ga
>>> robby_ga(5).elements[0].values['actions'].shape
(5, 243)
Source code in src/skcomplex/systems/computer_science/evolutionary_computation/_genetic_algorithms.py
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
@register_system(
    field='computer_science/evolutionary_computation',
    tags=('genetic algorithm', 'agents', 'evolution', 'robot'),
    references=('Mitchell 2009', 'Gooding 2019'),
    defaults={'until': 20},
)
def robby_ga(
    n_strategies: int = 50,
    *,
    mutation: float = 0.005,
    sessions: int = 10,
    n_actions: int = 100,
    regime: str = 'generational',
    lifespan: int = 7,
    starvation: float = 0.3,
    switch: int | None = None,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return Mitchell's genetic algorithm that evolves strategies for Robby, a robot that collects cans in a room.

    Each step is one generation. Parents are selected with probabilities proportional to their rank, every pair of
    parents exchanges the tails of their strategies after a random cut, and every action of a child mutates to another
    action with probability `mutation`. Then every child is scored in random rooms. Random strategies score far below
    zero because they bump into walls, and evolution finds strategies that avoid walls and collect cans.

    Gooding's evolution by circumstance, the regime `'circumstance'`, replaces the selection with deaths: every
    strategy starts at a random age, dies at the age `lifespan`, and the fraction `starvation` with the lowest scores
    starves every generation. Random pairs of survivors, whatever their scores, breed children in the places of the
    dead. The starvation is the only selection, so with it the scores rise as under the genetic algorithm, and without
    it they drift. With a `switch` period the starvation switches on and off every `switch` generations, as in
    Gooding's runs that switch it every 3000 iterations.

    Args:
        n_strategies:
            The size of the population.

        mutation:
            The probability that an action of a child is replaced by another one.

        sessions:
            The number of rooms a strategy cleans to be scored.

        n_actions:
            The number of actions in a session.

        regime:
            The evolution, `'generational'` for Mitchell's genetic algorithm or `'circumstance'` for Gooding's
            evolution by circumstance.

        lifespan:
            The age at which a strategy dies, in the evolution by circumstance.

        starvation:
            The fraction of the lowest scores that starves every generation, in the evolution by circumstance.

        switch:
            The number of generations after which the starvation switches on or off, or `None` to keep it on, in the
            evolution by circumstance.

        random_state:
            The seed or generator of the random first generation and of its scores.

    Returns:
        system:
            The system, with the group `strategies` of states `actions` and `score`, and `age` in the evolution by
            circumstance.

    Raises:
        ParameterError:
            The regime is unknown.

    Examples:
        >>> from skcomplex.systems.computer_science.evolutionary_computation import robby_ga
        >>> robby_ga(5).elements[0].values['actions'].shape
        (5, 243)
    """
    if regime not in REGIMES:
        error_msg = f'Unknown regime {regime!r}. Use one of {", ".join(REGIMES)}.'
        raise ParameterError(error_msg)
    rng = np.random.default_rng(random_state)
    evolution = EvolutionInteraction(
        'evolution',
        target='strategies',
        strategy='actions',
        fitness='score',
        selection='rank',
        crossover=1.0,
        mutation=mutation,
        alphabet=N_ACTIONS,
    )
    scoring = RoomCleaningInteraction('scoring', target='strategies', sessions=sessions, n_actions=n_actions)
    actions = rng.integers(0, N_ACTIONS, size=(n_strategies, N_SITUATIONS))
    scores = scoring.score(actions, rng)
    if regime == 'circumstance':
        circumstance = CircumstanceInteraction(
            'circumstance',
            target='strategies',
            lifespan=lifespan,
            starvation=starvation,
            mutation=mutation,
            switch=switch,
        )
        ages = rng.integers(0, lifespan, size=n_strategies).astype(float)
        strategies = GroupElement(
            'strategies',
            element=AgedStrategyElement,
            size=n_strategies,
            values={'actions': actions, 'score': scores, 'age': ages},
        )
        return System(elements=[strategies], interactions=[circumstance, scoring])
    return System(
        elements=[
            GroupElement(
                'strategies',
                element=StrategyElement,
                size=n_strategies,
                values={'actions': actions, 'score': scores},
            ),
        ],
        interactions=[evolution, scoring],
    )