neuroevolution_server (faber_neuroevolution v1.2.4)

View Source

Population-based evolutionary training server.

This gen_server manages a population of neural network individuals, running them through domain-specific evaluation and evolving them across generations.

Pluggable Evolution Strategies

The server delegates all evolution logic to a configurable strategy module that implements the evolution_strategy behaviour. Built-in strategies:

- generational_strategy - Traditional (mu,lambda) batch evolution (default) - steady_state_strategy - Continuous replacement, no generations - island_strategy - Parallel populations with migration - novelty_strategy - Behavioral novelty search - map_elites_strategy - Quality-diversity with niche grid

Generation Lifecycle

Each generation follows this cycle:

  1. Evaluate all individuals in parallel using the configured evaluator
  2. Strategy receives evaluation results and manages selection/breeding
  3. Strategy emits lifecycle events (individual_born, individual_died, etc.)
  4. Server orchestrates next evaluation round
  5. Repeat

Configuration

The server is configured via a #neuro_config{} record that specifies:

  • Population size and selection ratio
  • Mutation rate and strength
  • Network topology (inputs, hidden layers, outputs)
  • Evaluator module (implements neuroevolution_evaluator behaviour)
  • Strategy module (implements evolution_strategy behaviour)
  • Optional event handler for notifications

Summary

Functions

Get the last fully-evaluated population, sorted by fitness descending. This is captured before the strategy replaces/resets individuals for the next generation. All individuals retain their fitness values. Returns empty list if no evaluation has completed yet.

Get the current population (raw individuals). NOTE: After training_complete, this returns the post-breeding population where non-elite individuals have fitness reset to 0. For the fully evaluated population with all fitness values, use get_last_evaluated_population/1.

Get population snapshot from the strategy.

Get current training statistics.

Start the neuroevolution server with given configuration.

Start the neuroevolution server with configuration and options.

Start the evolutionary training process.

Stop the evolutionary training process.

Update configuration dynamically (used by external meta-controllers).

Types

breeding_event/0

-type breeding_event() ::
          #breeding_event{parent1_id :: individual_id(),
                          parent2_id :: individual_id(),
                          child_id :: individual_id(),
                          generation :: generation()}.

competitive_entry/0

-type competitive_entry() ::
          #{generation := generation(),
            best_fitness := fitness(),
            avg_fitness := fitness(),
            worst_fitness := fitness(),
            top_10_avg := fitness(),
            bottom_10_avg := fitness(),
            fitness_variance := float(),
            competitive_pressure := float(),
            archive_size := non_neg_integer(),
            archive_avg := fitness()}.

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

individual_summary/0

-type individual_summary() ::
          #{id := individual_id(),
            fitness := fitness(),
            is_survivor => boolean(),
            is_offspring => boolean(),
            species_id => species_id(),
            age => non_neg_integer()}.

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,
                        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,
                        seed_networks :: [term()]}.

neuro_state/0

-type neuro_state() ::
          #neuro_state{id :: term(),
                       config :: neuro_config(),
                       population :: [individual()],
                       total_evaluations :: non_neg_integer(),
                       generation :: generation(),
                       running :: boolean(),
                       evaluating :: boolean(),
                       games_completed :: non_neg_integer(),
                       total_games :: non_neg_integer(),
                       best_fitness_ever :: fitness(),
                       last_gen_best :: fitness(),
                       last_gen_avg :: fitness(),
                       generation_history :: [{generation(), fitness(), fitness()}],
                       last_gen_results :: map() | undefined,
                       breeding_events :: [breeding_event()],
                       competitive_history :: [competitive_entry()],
                       eval_task :: reference() | undefined,
                       meta_controller :: pid() | undefined,
                       lc_chain :: pid() | undefined,
                       strategy_module :: module() | undefined,
                       strategy_state :: term() | undefined,
                       species :: [species()],
                       species_events :: [species_event()],
                       next_species_id :: species_id(),
                       pending_evaluations :: #{reference() => {individual_id(), individual()}},
                       eval_timeout_ref :: reference() | undefined,
                       last_checkpoint_time :: non_neg_integer() | undefined,
                       last_checkpoint_evals :: non_neg_integer(),
                       cached_resource_recommendations :: map(),
                       cached_task_recommendations :: map(),
                       last_evaluated_population :: [individual()]}.

population_snapshot/0

-type population_snapshot() ::
          #{size := non_neg_integer(),
            individuals := [individual_summary()],
            best_fitness := fitness(),
            avg_fitness := fitness(),
            worst_fitness := fitness(),
            species_count => non_neg_integer(),
            generation => pos_integer(),
            extra => map()}.

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()}.

species/0

-type species() ::
          #species{id :: species_id(),
                   representative :: individual(),
                   members :: [individual_id()],
                   best_fitness :: fitness(),
                   best_fitness_ever :: fitness(),
                   generation_created :: generation(),
                   age :: non_neg_integer(),
                   stagnant_generations :: non_neg_integer(),
                   offspring_quota :: non_neg_integer()}.

species_event/0

-type species_event() ::
          #species_event{generation :: generation(),
                         species_id :: species_id(),
                         event_type ::
                             species_created | species_extinct | species_stagnant | champion_emerged,
                         details :: map()}.

species_id/0

-type species_id() :: pos_integer().

Functions

get_last_evaluated_population(ServerRef)

-spec get_last_evaluated_population(ServerRef) -> {ok, [individual()]} when ServerRef :: pid() | atom().

Get the last fully-evaluated population, sorted by fitness descending. This is captured before the strategy replaces/resets individuals for the next generation. All individuals retain their fitness values. Returns empty list if no evaluation has completed yet.

get_population(ServerRef)

-spec get_population(ServerRef) -> {ok, [individual()]} when ServerRef :: pid() | atom().

Get the current population (raw individuals). NOTE: After training_complete, this returns the post-breeding population where non-elite individuals have fitness reset to 0. For the fully evaluated population with all fitness values, use get_last_evaluated_population/1.

get_population_snapshot(ServerRef)

-spec get_population_snapshot(ServerRef) -> {ok, population_snapshot()} when ServerRef :: pid() | atom().

Get population snapshot from the strategy.

get_stats(ServerRef)

-spec get_stats(ServerRef) -> {ok, Stats} when ServerRef :: pid() | atom(), Stats :: map().

Get current training statistics.

start_link(Config)

-spec start_link(Config) -> {ok, pid()} | {error, term()} when Config :: neuro_config().

Start the neuroevolution server with given configuration.

start_link(Config, Options)

-spec start_link(Config, Options) -> {ok, pid()} | {error, term()}
                    when Config :: neuro_config(), Options :: proplists:proplist().

Start the neuroevolution server with configuration and options.

Options: - {id, Id} - Server identifier (default: make_ref()) - {name, Name} - Register with given name

start_training(ServerRef)

-spec start_training(ServerRef) -> {ok, started | already_running} when ServerRef :: pid() | atom().

Start the evolutionary training process.

stop_training(ServerRef)

-spec stop_training(ServerRef) -> ok when ServerRef :: pid() | atom().

Stop the evolutionary training process.

update_config(ServerRef, Params)

-spec update_config(ServerRef, Params) -> {ok, map()}
                       when ServerRef :: pid() | atom(), Params :: #{atom() => number()}.

Update configuration dynamically (used by external meta-controllers).

Allows updating hyperparameters like mutation_rate, mutation_strength, and selection_ratio between generations. This is used by the Elixir Liquid Conglomerate meta-controller to feed its recommendations into the Erlang neuroevolution server.