%% @doc Genetic operators for neuroevolution. %% %% This module provides crossover and mutation operators for evolving %% neural network weights. These are the core genetic algorithms used %% by the neuroevolution server to create offspring from parent individuals. %% %% == Crossover == %% %% Crossover combines weights from two parent networks to create offspring. %% Currently implements uniform crossover where each weight has 50% chance %% of coming from either parent. %% %% == Mutation == %% %% Mutation perturbs weights by adding small random values. The mutation %% rate controls what fraction of weights are modified, and mutation %% strength controls the magnitude of changes. %% %% @author Macula.io %% @copyright 2025 Macula.io -module(neuroevolution_genetic). -include("neuroevolution.hrl"). %% API -export([ crossover_uniform/2, mutate_weights/3, create_offspring/4 ]). %%% ============================================================================ %%% API Functions %%% ============================================================================ %% @doc 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). %% ''' -spec crossover_uniform(Weights1, Weights2) -> ChildWeights when Weights1 :: [float()], Weights2 :: [float()], ChildWeights :: [float()]. crossover_uniform(Weights1, Weights2) -> lists:zipwith( fun(W1, W2) -> case rand:uniform() < 0.5 of true -> W1; false -> W2 end end, Weights1, Weights2 ). %% @doc 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). %% ''' -spec mutate_weights(Weights, MutationRate, MutationStrength) -> MutatedWeights when Weights :: [float()], MutationRate :: float(), MutationStrength :: float(), MutatedWeights :: [float()]. mutate_weights(Weights, MutationRate, MutationStrength) -> lists:map( fun(W) -> case rand:uniform() < MutationRate of true -> %% Perturb: add random value in [-Strength, +Strength] Delta = (rand:uniform() - 0.5) * 2 * MutationStrength, W + Delta; false -> W end end, Weights ). %% @doc Create offspring from two parent individuals. %% %% Combines crossover and mutation in a single operation: %% 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. -spec create_offspring(Parent1, Parent2, Config, Generation) -> Offspring when Parent1 :: individual(), Parent2 :: individual(), Config :: neuro_config(), Generation :: generation(), Offspring :: individual(). create_offspring(Parent1, Parent2, Config, Generation) -> %% Extract weights from parent networks Weights1 = network_evaluator:get_weights(Parent1#individual.network), Weights2 = network_evaluator:get_weights(Parent2#individual.network), %% Crossover ChildWeights = crossover_uniform(Weights1, Weights2), %% Mutation MutatedWeights = mutate_weights( ChildWeights, Config#neuro_config.mutation_rate, Config#neuro_config.mutation_strength ), %% Create new network with child weights {InputSize, HiddenLayers, OutputSize} = Config#neuro_config.network_topology, Network = network_evaluator:create_feedforward(InputSize, HiddenLayers, OutputSize), ChildNetwork = network_evaluator:set_weights(Network, MutatedWeights), %% Create offspring individual with lineage ChildId = make_ref(), #individual{ id = ChildId, network = ChildNetwork, parent1_id = Parent1#individual.id, parent2_id = Parent2#individual.id, generation_born = Generation, is_offspring = true }.