Current section

Files

Jump to
macula_tweann src mutation_helpers.erl
Raw

src/mutation_helpers.erl

%% @doc Helper functions for genome mutation operations.
%%
%% This module provides utility functions used by mutation operators
%% for linking network elements and weight management.
%%
%% @author Macula.io
%% @copyright 2025 Macula.io, Apache-2.0
-module(mutation_helpers).
-include("records.hrl").
-dialyzer({nowarn_function, [
update_source_output/3,
update_target_input/4,
link_neuron_to_target/3,
link_source_to_neuron/3
]}).
-export([
%% Neuron selection
select_random_neuron/1,
select_ltc_neuron/1,
%% Link operations
link_neuron_to_target/3,
link_source_to_neuron/3,
link_sensor_to_neuron/3,
link_neuron_to_actuator/3,
update_source_output/3,
update_target_input/4,
%% Weight utilities
create_random_weight/0,
get_link_weight/2,
find_splittable_link/1,
get_layer_coord/1,
%% LTC utilities
perturb_ltc_weight_list/2
]).
%% Delta multiplier for weight perturbation.
-define(DELTA_MULTIPLIER, math:pi() * 2).
%%==============================================================================
%% Neuron Selection
%%==============================================================================
%% @doc Select a random neuron from the agent's network.
%%
%% @param AgentId the agent
%% @returns NeuronId or {error, no_neurons}
-spec select_random_neuron(term()) -> term() | {error, no_neurons}.
select_random_neuron(AgentId) ->
Agent = genotype:dirty_read({agent, AgentId}),
Cortex = genotype:dirty_read({cortex, Agent#agent.cx_id}),
select_from_neuron_ids(Cortex#cortex.neuron_ids).
select_from_neuron_ids([]) -> {error, no_neurons};
select_from_neuron_ids(NeuronIds) -> selection_utils:random_select(NeuronIds).
%% @doc Select a random LTC or CfC neuron from the agent's network.
-spec select_ltc_neuron(term()) -> term() | {error, no_ltc_neurons | no_neurons}.
select_ltc_neuron(AgentId) ->
Agent = genotype:dirty_read({agent, AgentId}),
Cortex = genotype:dirty_read({cortex, Agent#agent.cx_id}),
filter_and_select_ltc(Cortex#cortex.neuron_ids).
filter_and_select_ltc([]) ->
{error, no_neurons};
filter_and_select_ltc(NeuronIds) ->
LtcNeurons = [NId || NId <- NeuronIds, is_ltc_neuron(NId)],
select_from_ltc_list(LtcNeurons).
select_from_ltc_list([]) -> {error, no_ltc_neurons};
select_from_ltc_list(LtcNeurons) -> selection_utils:random_select(LtcNeurons).
is_ltc_neuron(NeuronId) ->
N = genotype:dirty_read({neuron, NeuronId}),
N#neuron.neuron_type =:= ltc orelse N#neuron.neuron_type =:= cfc.
%%==============================================================================
%% Link Operations
%%==============================================================================
%% @doc Link a neuron to a target (neuron or actuator).
-spec link_neuron_to_target(term(), #neuron{}, term()) -> ok.
link_neuron_to_target(NeuronId, Neuron, TargetId) ->
%% Update source neuron's outputs
NewOutputIds = [TargetId | Neuron#neuron.output_ids],
UpdatedNeuron = Neuron#neuron{output_ids = NewOutputIds},
genotype:write(UpdatedNeuron),
%% Update target's inputs
update_target_with_new_input(NeuronId, TargetId),
ok.
update_target_with_new_input(NeuronId, TargetId) ->
case genotype:dirty_read({neuron, TargetId}) of
undefined ->
add_neuron_to_actuator_fanin(NeuronId, TargetId);
TargetNeuron ->
add_input_to_neuron(NeuronId, TargetNeuron)
end.
add_neuron_to_actuator_fanin(NeuronId, ActuatorId) ->
Actuator = genotype:dirty_read({actuator, ActuatorId}),
NewFaninIds = [NeuronId | Actuator#actuator.fanin_ids],
UpdatedActuator = Actuator#actuator{fanin_ids = NewFaninIds},
genotype:write(UpdatedActuator).
add_input_to_neuron(SourceId, TargetNeuron) ->
NewWeight = create_random_weight(),
NewInputIdps = [{SourceId, [NewWeight]} | TargetNeuron#neuron.input_idps],
UpdatedTarget = TargetNeuron#neuron{input_idps = NewInputIdps},
genotype:write(UpdatedTarget).
%% @doc Link a source (sensor or neuron) to a neuron.
-spec link_source_to_neuron(term(), term(), #neuron{}) -> ok.
link_source_to_neuron(SourceId, NeuronId, Neuron) ->
%% Update neuron's inputs
NewWeight = create_random_weight(),
NewInputIdps = [{SourceId, [NewWeight]} | Neuron#neuron.input_idps],
UpdatedNeuron = Neuron#neuron{input_idps = NewInputIdps},
genotype:write(UpdatedNeuron),
%% Update source's outputs
update_source_with_new_output(SourceId, NeuronId),
ok.
update_source_with_new_output(SourceId, NeuronId) ->
case genotype:dirty_read({neuron, SourceId}) of
undefined ->
add_neuron_to_sensor_fanout(SourceId, NeuronId);
SourceNeuron ->
add_output_to_neuron(SourceNeuron, NeuronId)
end.
add_neuron_to_sensor_fanout(SensorId, NeuronId) ->
Sensor = genotype:dirty_read({sensor, SensorId}),
NewFanoutIds = [NeuronId | Sensor#sensor.fanout_ids],
UpdatedSensor = Sensor#sensor{fanout_ids = NewFanoutIds},
genotype:write(UpdatedSensor).
add_output_to_neuron(SourceNeuron, NeuronId) ->
NewOutputIds = [NeuronId | SourceNeuron#neuron.output_ids],
UpdatedSource = SourceNeuron#neuron{output_ids = NewOutputIds},
genotype:write(UpdatedSource).
%% @doc Link a sensor to a neuron.
-spec link_sensor_to_neuron(term(), #sensor{}, term()) -> ok.
link_sensor_to_neuron(SensorId, Sensor, NeuronId) ->
%% Update sensor's fanout
NewFanoutIds = [NeuronId | Sensor#sensor.fanout_ids],
UpdatedSensor = Sensor#sensor{fanout_ids = NewFanoutIds},
genotype:write(UpdatedSensor),
%% Update neuron's inputs
Neuron = genotype:dirty_read({neuron, NeuronId}),
NewWeight = create_random_weight(),
NewInputIdps = [{SensorId, [NewWeight]} | Neuron#neuron.input_idps],
UpdatedNeuron = Neuron#neuron{input_idps = NewInputIdps},
genotype:write(UpdatedNeuron),
ok.
%% @doc Link a neuron to an actuator.
-spec link_neuron_to_actuator(term(), term(), #actuator{}) -> ok.
link_neuron_to_actuator(NeuronId, ActuatorId, Actuator) ->
%% Update actuator's fanin
NewFaninIds = [NeuronId | Actuator#actuator.fanin_ids],
UpdatedActuator = Actuator#actuator{fanin_ids = NewFaninIds},
genotype:write(UpdatedActuator),
%% Update neuron's outputs
Neuron = genotype:dirty_read({neuron, NeuronId}),
NewOutputIds = [ActuatorId | Neuron#neuron.output_ids],
UpdatedNeuron = Neuron#neuron{output_ids = NewOutputIds},
genotype:write(UpdatedNeuron),
ok.
%% @doc Update source element to output to new target.
-spec update_source_output(term(), term(), term()) -> ok.
update_source_output(FromId, OldToId, NewToId) ->
case genotype:dirty_read({neuron, FromId}) of
undefined ->
update_sensor_output(FromId, OldToId, NewToId);
Neuron ->
update_neuron_output(Neuron, OldToId, NewToId)
end,
ok.
update_sensor_output(SensorId, OldToId, NewToId) ->
Sensor = genotype:dirty_read({sensor, SensorId}),
NewFanoutIds = [NewToId | (Sensor#sensor.fanout_ids -- [OldToId])],
UpdatedSensor = Sensor#sensor{fanout_ids = NewFanoutIds},
genotype:write(UpdatedSensor).
update_neuron_output(Neuron, OldToId, NewToId) ->
NewOutputIds = [NewToId | (Neuron#neuron.output_ids -- [OldToId])],
UpdatedNeuron = Neuron#neuron{output_ids = NewOutputIds},
genotype:write(UpdatedNeuron).
%% @doc Update target element to receive from new source.
-spec update_target_input(term(), term(), term(), {float(), float(), float(), list()}) -> ok.
update_target_input(ToId, OldFromId, NewFromId, Weight) ->
case genotype:dirty_read({neuron, ToId}) of
undefined ->
update_actuator_input(ToId, OldFromId, NewFromId);
Neuron ->
update_neuron_input(Neuron, OldFromId, NewFromId, Weight)
end,
ok.
update_actuator_input(ActuatorId, OldFromId, NewFromId) ->
Actuator = genotype:dirty_read({actuator, ActuatorId}),
NewFaninIds = [NewFromId | (Actuator#actuator.fanin_ids -- [OldFromId])],
UpdatedActuator = Actuator#actuator{fanin_ids = NewFaninIds},
genotype:write(UpdatedActuator).
update_neuron_input(Neuron, OldFromId, NewFromId, Weight) ->
FilteredInputs = [{Id, W} || {Id, W} <- Neuron#neuron.input_idps, Id /= OldFromId],
NewInputIdps = [{NewFromId, [Weight]} | FilteredInputs],
UpdatedNeuron = Neuron#neuron{input_idps = NewInputIdps},
genotype:write(UpdatedNeuron).
%%==============================================================================
%% Weight Utilities
%%==============================================================================
%% @doc Create a random weight tuple.
-spec create_random_weight() -> {float(), float(), float(), []}.
create_random_weight() ->
{rand:uniform() - 0.5, 0.0, 0.1, []}.
%% @doc Get weight of a link between two elements.
-spec get_link_weight(term(), term()) -> {float(), float(), float(), list()}.
get_link_weight(FromId, ToId) ->
case genotype:dirty_read({neuron, ToId}) of
undefined ->
create_random_weight();
Neuron ->
find_weight_in_inputs(FromId, Neuron#neuron.input_idps)
end.
find_weight_in_inputs(FromId, InputIdps) ->
case lists:keyfind(FromId, 1, InputIdps) of
{FromId, [Weight | _]} -> Weight;
_ -> create_random_weight()
end.
%% @doc Find a link that can be split to insert a neuron.
-spec find_splittable_link(term()) -> {term(), term(), {float(), float(), float(), list()}} | {error, no_links}.
find_splittable_link(AgentId) ->
Agent = genotype:dirty_read({agent, AgentId}),
Cortex = genotype:dirty_read({cortex, Agent#agent.cx_id}),
Links = collect_all_links(Cortex#cortex.neuron_ids),
select_random_link(Links).
collect_all_links(NeuronIds) ->
lists:flatmap(fun collect_neuron_links/1, NeuronIds).
collect_neuron_links(NeuronId) ->
Neuron = genotype:dirty_read({neuron, NeuronId}),
[{NeuronId, OutputId} || OutputId <- Neuron#neuron.output_ids].
select_random_link([]) ->
{error, no_links};
select_random_link(Links) ->
{FromId, ToId} = selection_utils:random_select(Links),
Weight = get_link_weight(FromId, ToId),
{FromId, ToId, Weight}.
%% @doc Get layer coordinate from element ID.
-spec get_layer_coord(term()) -> float().
get_layer_coord({{Layer, _}, _Type}) -> Layer;
get_layer_coord(_) -> 0.5.
%%==============================================================================
%% LTC Utilities
%%==============================================================================
%% @doc Perturb a list of LTC weights.
-spec perturb_ltc_weight_list([float()], float()) -> [float()].
perturb_ltc_weight_list([], _PerturbRange) ->
%% Initialize with small random weights if empty
[rand:uniform() - 0.5, rand:uniform() - 0.5];
perturb_ltc_weight_list(Weights, PerturbRange) ->
[W + (rand:uniform() - 0.5) * PerturbRange || W <- Weights].