%% @doc Topological mutation operators for neural network evolution. %% %% This module provides mutations that modify network structure: %% - add_neuron: Insert neuron into existing connection %% - add_outlink: Add output connection from neuron %% - add_inlink: Add input connection to neuron %% - add_sensorlink: Connect sensor to neuron %% - add_actuatorlink: Connect neuron to actuator %% - outsplice: Split output connection with new neuron %% - add_bias: Add bias connection to neuron %% %% @author Macula.io %% @copyright 2025 Macula.io, Apache-2.0 -module(topological_mutations). -include("records.hrl"). -dialyzer({nowarn_function, [ add_bias/1, add_outlink/1, add_inlink/1, add_neuron/1, outsplice/1, add_sensorlink/1, add_actuatorlink/1, add_sensor/1, add_actuator/1 ]}). -export([ add_bias/1, add_outlink/1, add_inlink/1, add_neuron/1, outsplice/1, add_sensorlink/1, add_actuatorlink/1, add_sensor/1, add_actuator/1 ]). %%============================================================================== %% Topological Mutations %%============================================================================== %% @doc Add bias input to a random neuron. %% %% Adds a bias connection (self-connection) to a neuron that %% doesn't already have one. %% %% @param AgentId the agent to mutate %% @returns ok or {error, term()} -spec add_bias(term()) -> ok | {error, term()}. add_bias(AgentId) -> case mutation_helpers:select_random_neuron(AgentId) of {error, no_neurons} -> {error, no_neurons}; NeuronId -> add_bias_to_neuron(NeuronId) end. add_bias_to_neuron(NeuronId) -> Neuron = genotype:dirty_read({neuron, NeuronId}), HasBias = lists:any(fun({InputId, _}) -> InputId == bias end, Neuron#neuron.input_idps), do_add_bias(HasBias, Neuron). do_add_bias(true, _Neuron) -> {error, already_has_bias}; do_add_bias(false, Neuron) -> BiasWeight = mutation_helpers:create_random_weight(), NewInputIdps = [{bias, [BiasWeight]} | Neuron#neuron.input_idps], UpdatedNeuron = Neuron#neuron{input_idps = NewInputIdps}, genotype:write(UpdatedNeuron), ok. %% @doc Add output link from a random neuron. %% %% Connects a neuron to another neuron or actuator that it's %% not currently connected to. %% %% @param AgentId the agent to mutate %% @returns ok or {error, term()} -spec add_outlink(term()) -> ok | {error, term()}. add_outlink(AgentId) -> Agent = genotype:dirty_read({agent, AgentId}), Cortex = genotype:dirty_read({cortex, Agent#agent.cx_id}), case mutation_helpers:select_random_neuron(AgentId) of {error, no_neurons} -> {error, no_neurons}; NeuronId -> add_outlink_from_neuron(NeuronId, Cortex) end. add_outlink_from_neuron(NeuronId, Cortex) -> Neuron = genotype:dirty_read({neuron, NeuronId}), AllTargets = Cortex#cortex.neuron_ids ++ Cortex#cortex.actuator_ids, CurrentOutputs = Neuron#neuron.output_ids, AvailableTargets = AllTargets -- CurrentOutputs -- [NeuronId], connect_to_target(NeuronId, Neuron, AvailableTargets). connect_to_target(_NeuronId, _Neuron, []) -> {error, no_available_targets}; connect_to_target(NeuronId, Neuron, Targets) -> TargetId = selection_utils:random_select(Targets), mutation_helpers:link_neuron_to_target(NeuronId, Neuron, TargetId), ok. %% @doc Add input link to a random neuron. %% %% Connects a sensor or another neuron to a neuron that it's %% not currently connected to. %% %% @param AgentId the agent to mutate %% @returns ok or {error, term()} -spec add_inlink(term()) -> ok | {error, term()}. add_inlink(AgentId) -> Agent = genotype:dirty_read({agent, AgentId}), Cortex = genotype:dirty_read({cortex, Agent#agent.cx_id}), case mutation_helpers:select_random_neuron(AgentId) of {error, no_neurons} -> {error, no_neurons}; NeuronId -> add_inlink_to_neuron(NeuronId, Cortex) end. add_inlink_to_neuron(NeuronId, Cortex) -> Neuron = genotype:dirty_read({neuron, NeuronId}), AllSources = Cortex#cortex.sensor_ids ++ Cortex#cortex.neuron_ids, CurrentInputIds = [InputId || {InputId, _} <- Neuron#neuron.input_idps], AvailableSources = AllSources -- CurrentInputIds -- [NeuronId], connect_from_source(NeuronId, Neuron, AvailableSources). connect_from_source(_NeuronId, _Neuron, []) -> {error, no_available_sources}; connect_from_source(NeuronId, Neuron, Sources) -> SourceId = selection_utils:random_select(Sources), mutation_helpers:link_source_to_neuron(SourceId, NeuronId, Neuron), ok. %% @doc Add a new neuron by splitting a connection. %% %% Selects a random connection, removes it, and inserts a new %% neuron in the middle. %% %% @param AgentId the agent to mutate %% @returns ok or {error, term()} -spec add_neuron(term()) -> ok | {error, term()}. add_neuron(AgentId) -> Agent = genotype:dirty_read({agent, AgentId}), Cortex = genotype:dirty_read({cortex, Agent#agent.cx_id}), case mutation_helpers:find_splittable_link(AgentId) of {error, no_links} -> {error, cannot_add_neuron}; {FromId, ToId, Weight} -> insert_neuron(Agent, Cortex, FromId, ToId, Weight) end. insert_neuron(Agent, Cortex, FromId, ToId, Weight) -> NewNeuronId = genotype:generate_id(neuron), Constraint = Agent#agent.constraint, AF = selection_utils:random_select(Constraint#constraint.neural_afs), AggrF = selection_utils:random_select(Constraint#constraint.neural_aggr_fs), NewNeuron = #neuron{ id = NewNeuronId, generation = Agent#agent.generation, cx_id = Agent#agent.cx_id, af = AF, aggr_f = AggrF, input_idps = [{FromId, [Weight]}], output_ids = [ToId], ro_ids = [] }, %% Update connections mutation_helpers:update_source_output(FromId, ToId, NewNeuronId), mutation_helpers:update_target_input(ToId, FromId, NewNeuronId, Weight), %% Write new neuron and update cortex genotype:write(NewNeuron), NewNeuronIds = [NewNeuronId | Cortex#cortex.neuron_ids], UpdatedCortex = Cortex#cortex{neuron_ids = NewNeuronIds}, genotype:write(UpdatedCortex), ok. %% @doc Add neuron by outsplicing (split output connection). %% %% Similar to add_neuron but specifically targets output connections. %% %% @param AgentId the agent to mutate %% @returns ok or {error, term()} -spec outsplice(term()) -> ok | {error, term()}. outsplice(AgentId) -> add_neuron(AgentId). %% @doc Add link from a sensor to a neuron. %% %% @param AgentId the agent to mutate %% @returns ok or {error, term()} -spec add_sensorlink(term()) -> ok | {error, term()}. add_sensorlink(AgentId) -> Agent = genotype:dirty_read({agent, AgentId}), Cortex = genotype:dirty_read({cortex, Agent#agent.cx_id}), select_sensor_and_link(Cortex). select_sensor_and_link(#cortex{sensor_ids = []}) -> {error, no_sensors}; select_sensor_and_link(Cortex) -> SensorId = selection_utils:random_select(Cortex#cortex.sensor_ids), Sensor = genotype:dirty_read({sensor, SensorId}), AvailableNeurons = Cortex#cortex.neuron_ids -- Sensor#sensor.fanout_ids, link_sensor_to_available_neuron(SensorId, Sensor, AvailableNeurons). link_sensor_to_available_neuron(_SensorId, _Sensor, []) -> {error, no_available_neurons}; link_sensor_to_available_neuron(SensorId, Sensor, Neurons) -> NeuronId = selection_utils:random_select(Neurons), mutation_helpers:link_sensor_to_neuron(SensorId, Sensor, NeuronId), ok. %% @doc Add link from a neuron to an actuator. %% %% @param AgentId the agent to mutate %% @returns ok or {error, term()} -spec add_actuatorlink(term()) -> ok | {error, term()}. add_actuatorlink(AgentId) -> Agent = genotype:dirty_read({agent, AgentId}), Cortex = genotype:dirty_read({cortex, Agent#agent.cx_id}), select_actuator_and_link(Cortex). select_actuator_and_link(#cortex{actuator_ids = []}) -> {error, no_actuators}; select_actuator_and_link(Cortex) -> ActuatorId = selection_utils:random_select(Cortex#cortex.actuator_ids), Actuator = genotype:dirty_read({actuator, ActuatorId}), AvailableNeurons = Cortex#cortex.neuron_ids -- Actuator#actuator.fanin_ids, link_neuron_to_available_actuator(ActuatorId, Actuator, AvailableNeurons). link_neuron_to_available_actuator(_ActuatorId, _Actuator, []) -> {error, no_available_neurons}; link_neuron_to_available_actuator(ActuatorId, Actuator, Neurons) -> NeuronId = selection_utils:random_select(Neurons), mutation_helpers:link_neuron_to_actuator(NeuronId, ActuatorId, Actuator), ok. %% @doc Add a new sensor to the network. %% %% @param AgentId the agent to mutate %% @returns ok or {error, term()} -spec add_sensor(term()) -> ok | {error, term()}. add_sensor(_AgentId) -> {error, not_implemented}. %% @doc Add a new actuator to the network. %% %% @param AgentId the agent to mutate %% @returns ok or {error, term()} -spec add_actuator(term()) -> ok | {error, term()}. add_actuator(_AgentId) -> {error, not_implemented}.