neuroevolution_genetic (macula_neuroevolution v0.29.1)

View Source

Genetic operators for neuroevolution.

This module provides crossover and mutation operators for evolving neural networks. Supports two modes:

NEAT Topology Evolution

When individuals have genomes (connection_genes with innovation numbers), uses NEAT-style crossover and structural mutations via genome_factory. This enables topology-evolving networks.

Fixed Topology (Legacy)

For backward compatibility, when individuals don't have genomes, uses weight-only evolution with uniform crossover and perturbation mutation.

Reference: Stanley, K.O. and Miikkulainen, R. (2002). "Evolving Neural Networks through Augmenting Topologies." Evolutionary Computation, 10(2).

Summary

Functions

Create offspring from two parent individuals.

Create offspring using NEAT topology evolution.

Uniform crossover of two weight lists.

Mutate weights with given rate and strength.

Types

fitness/0

-type fitness() :: float() | undefined.

generation/0

-type generation() :: non_neg_integer().

genome/0

-type genome() ::
          #genome{connection_genes ::
                      [#connection_gene{innovation :: pos_integer() | undefined,
                                        from_id :: term(),
                                        to_id :: term(),
                                        weight :: float(),
                                        enabled :: boolean()}],
                  input_count :: non_neg_integer(),
                  hidden_count :: non_neg_integer(),
                  output_count :: non_neg_integer()}.

individual/0

-type individual() ::
          #individual{id :: individual_id(),
                      network :: network(),
                      genome :: genome() | undefined,
                      parent1_id :: individual_id() | undefined,
                      parent2_id :: individual_id() | undefined,
                      fitness :: fitness(),
                      metrics :: metrics(),
                      generation_born :: generation(),
                      birth_evaluation :: non_neg_integer(),
                      max_age :: pos_integer(),
                      is_survivor :: boolean(),
                      is_offspring :: boolean()}.

individual_id/0

-type individual_id() :: term().

metrics/0

-type metrics() :: map().

mutation_config/0

-type mutation_config() ::
          #mutation_config{weight_mutation_rate :: float(),
                           weight_perturb_rate :: float(),
                           weight_perturb_strength :: float(),
                           add_node_rate :: float(),
                           add_connection_rate :: float(),
                           toggle_connection_rate :: float(),
                           add_sensor_rate :: float(),
                           add_actuator_rate :: float(),
                           mutate_neuron_type_rate :: float(),
                           mutate_time_constant_rate :: float()}.

network/0

-type network() :: term().

neuro_config/0

-type neuro_config() ::
          #neuro_config{population_size :: pos_integer(),
                        evaluations_per_individual :: pos_integer(),
                        selection_ratio :: float(),
                        mutation_rate :: float(),
                        mutation_strength :: float(),
                        reservoir_mutation_rate :: float() | undefined,
                        reservoir_mutation_strength :: float() | undefined,
                        readout_mutation_rate :: float() | undefined,
                        readout_mutation_strength :: float() | undefined,
                        topology_mutation_config :: mutation_config() | undefined,
                        max_evaluations :: pos_integer() | infinity,
                        max_generations :: pos_integer() | infinity,
                        target_fitness :: float() | undefined,
                        network_topology :: {pos_integer(), [pos_integer()], pos_integer()},
                        evaluator_module :: module(),
                        evaluator_options :: map(),
                        event_handler :: {module(), term()} | undefined,
                        meta_controller_config :: term() | undefined,
                        speciation_config :: speciation_config() | undefined,
                        self_play_config :: self_play_config() | undefined,
                        realm :: binary(),
                        publish_events :: boolean(),
                        evaluation_mode :: direct | distributed | mesh,
                        mesh_config :: map() | undefined,
                        evaluation_timeout :: pos_integer(),
                        max_concurrent_evaluations :: pos_integer() | undefined,
                        strategy_config :: term() | undefined,
                        lc_chain_config :: term() | undefined,
                        checkpoint_interval :: pos_integer() | undefined,
                        checkpoint_config :: map() | undefined}.

self_play_config/0

-type self_play_config() ::
          #self_play_config{enabled :: boolean(),
                            archive_size :: pos_integer(),
                            archive_threshold :: float() | auto,
                            min_fitness_percentile :: float()}.

speciation_config/0

-type speciation_config() ::
          #speciation_config{enabled :: boolean(),
                             compatibility_threshold :: float(),
                             c1_excess :: float(),
                             c2_disjoint :: float(),
                             c3_weight_diff :: float(),
                             target_species :: pos_integer(),
                             threshold_adjustment_rate :: float(),
                             min_species_size :: pos_integer(),
                             max_stagnation :: non_neg_integer(),
                             species_elitism :: float(),
                             interspecies_mating_rate :: float()}.

Functions

create_offspring(Parent1, Parent2, Config, Generation)

-spec create_offspring(Parent1, Parent2, Config, Generation) -> Offspring
                          when
                              Parent1 :: individual(),
                              Parent2 :: individual(),
                              Config :: neuro_config(),
                              Generation :: generation(),
                              Offspring :: individual().

Create offspring from two parent individuals.

Automatically selects the appropriate mode: - NEAT mode: When both parents have genomes and topology_mutation_config is set - Legacy mode: Weight-only evolution with uniform crossover

For NEAT mode: 1. Determine fitter parent 2. Perform NEAT crossover (gene alignment by innovation number) 3. Apply structural and weight mutations 4. Convert genome to network for evaluation

For Legacy mode: 1. Extract weights from both parent networks 2. Perform uniform crossover 3. Apply mutation 4. Create new network with child weights

Returns a new individual record with lineage tracking.

create_offspring_neat(Parent1, Parent2, Config, Generation)

-spec create_offspring_neat(Parent1, Parent2, Config, Generation) -> Offspring
                               when
                                   Parent1 :: individual(),
                                   Parent2 :: individual(),
                                   Config :: neuro_config(),
                                   Generation :: generation(),
                                   Offspring :: individual().

Create offspring using NEAT topology evolution.

Uses genome_factory to perform NEAT-style crossover and mutation. Both parents must have genomes for this to work.

crossover_uniform(Weights1, Weights2)

-spec crossover_uniform(Weights1, Weights2) -> ChildWeights
                           when Weights1 :: [float()], Weights2 :: [float()], ChildWeights :: [float()].

Uniform crossover of two weight lists.

For each weight position, randomly selects from either parent with equal probability (50/50).

Both weight lists must be the same length.

Example: Parent1 = [1.0, 2.0, 3.0, 4.0], Parent2 = [5.0, 6.0, 7.0, 8.0], %% Might produce: [1.0, 6.0, 3.0, 8.0] Child = neuroevolution_genetic:crossover_uniform(Parent1, Parent2).

mutate_weights(Weights, MutationRate, MutationStrength)

-spec mutate_weights(Weights, MutationRate, MutationStrength) -> MutatedWeights
                        when
                            Weights :: [float()],
                            MutationRate :: float(),
                            MutationStrength :: float(),
                            MutatedWeights :: [float()].

Mutate weights with given rate and strength.

Each weight has MutationRate probability of being perturbed. When mutated, a random value in [-Strength, +Strength] is added.

Example: Weights = [1.0, 2.0, 3.0], Rate = 0.1, %% 10% of weights mutated Strength = 0.3, %% Changes up to +/- 0.3 Mutated = neuroevolution_genetic:mutate_weights(Weights, Rate, Strength).

mutate_weights_layered(Weights, Topology, ReservoirParams, ReadoutParams, FallbackParams)

-spec mutate_weights_layered(Weights, Topology, ReservoirParams, ReadoutParams, FallbackParams) ->
                                MutatedWeights
                                when
                                    Weights :: [float()],
                                    Topology :: {pos_integer(), [pos_integer()], pos_integer()},
                                    ReservoirParams :: {float(), float()} | undefined,
                                    ReadoutParams :: {float(), float()} | undefined,
                                    FallbackParams :: {float(), float()},
                                    MutatedWeights :: [float()].

Mutate weights with layer-specific rates.

Applies different mutation rates to reservoir (hidden) and readout (output) layers. The reservoir typically benefits from lower mutation rates for stability, while the readout can adapt faster with higher rates.

Reference: See guides/training-strategies.md for rationale.

Example: Weights = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6], %% 4 reservoir, 2 readout Topology = {2, [2], 2}, %% 2 inputs, 2 hidden, 2 outputs ReservoirParams = {0.05, 0.2}, %% Low rate for reservoir ReadoutParams = {0.20, 0.5}, %% High rate for readout Mutated = neuroevolution_genetic:mutate_weights_layered( Weights, Topology, ReservoirParams, ReadoutParams ).