Current section
Files
Jump to
Current section
Files
src/ltc_mutations.erl
%% @doc LTC (Liquid Time-Constant) mutation operators for neural network evolution.
%%
%% This module provides mutations specific to LTC/CfC neurons:
%% - mutate_neuron_type: Switch between standard/ltc/cfc
%% - mutate_time_constant: Perturb tau (response speed)
%% - mutate_state_bound: Perturb state bound A
%% - mutate_ltc_weights: Perturb backbone/head network weights
%%
%% LTC neurons have adaptive temporal dynamics that can be evolved.
%%
%% @author Macula.io
%% @copyright 2025 Macula.io, Apache-2.0
-module(ltc_mutations).
-include("records.hrl").
-dialyzer({nowarn_function, [
mutate_neuron_type/1,
mutate_time_constant/1,
mutate_state_bound/1,
mutate_ltc_weights/1
]}).
-export([
mutate_neuron_type/1,
mutate_time_constant/1,
mutate_state_bound/1,
mutate_ltc_weights/1
]).
%% Delta multiplier for weight perturbation.
-define(DELTA_MULTIPLIER, math:pi() * 2).
%%==============================================================================
%% LTC Mutations
%%==============================================================================
%% @doc Mutate neuron type of a random neuron.
%%
%% Changes neuron type between standard, ltc, and cfc.
%% This allows networks to evolve which neurons use temporal dynamics.
%%
%% @param AgentId the agent to mutate
%% @returns ok or {error, term()}
-spec mutate_neuron_type(term()) -> ok | {error, term()}.
mutate_neuron_type(AgentId) ->
case mutation_helpers:select_random_neuron(AgentId) of
{error, no_neurons} ->
{error, no_neurons};
NeuronId ->
change_neuron_type(NeuronId)
end.
change_neuron_type(NeuronId) ->
Neuron = genotype:dirty_read({neuron, NeuronId}),
CurrentType = Neuron#neuron.neuron_type,
AvailableTypes = [standard, ltc, cfc],
Alternatives = AvailableTypes -- [CurrentType],
NewType = selection_utils:random_select(Alternatives),
UpdatedNeuron = apply_type_change(Neuron, NewType),
genotype:write(UpdatedNeuron),
tweann_logger:debug("Mutated neuron type: ~p -> ~p", [CurrentType, NewType]),
ok.
apply_type_change(Neuron, standard) ->
Neuron#neuron{
neuron_type = standard,
internal_state = 0.0
};
apply_type_change(Neuron, NewType) ->
%% LTC or CfC: ensure tau and bound are reasonable
Tau = ensure_positive(Neuron#neuron.time_constant, 1.0),
Bound = ensure_positive(Neuron#neuron.state_bound, 1.0),
Neuron#neuron{
neuron_type = NewType,
time_constant = Tau,
state_bound = Bound
}.
ensure_positive(Value, _Default) when Value > 0 -> Value;
ensure_positive(_Value, Default) -> Default.
%% @doc Mutate time constant (tau) of a random LTC/CfC neuron.
%%
%% Perturbs the base time constant which controls response speed.
%% Smaller tau = faster response, larger tau = slower/smoother response.
%%
%% @param AgentId the agent to mutate
%% @returns ok or {error, term()}
-spec mutate_time_constant(term()) -> ok | {error, term()}.
mutate_time_constant(AgentId) ->
Agent = genotype:dirty_read({agent, AgentId}),
PerturbRange = Agent#agent.perturbation_range,
case mutation_helpers:select_ltc_neuron(AgentId) of
{error, Reason} ->
{error, Reason};
NeuronId ->
perturb_time_constant(NeuronId, PerturbRange)
end.
perturb_time_constant(NeuronId, PerturbRange) ->
Neuron = genotype:dirty_read({neuron, NeuronId}),
CurrentTau = Neuron#neuron.time_constant,
%% Perturb multiplicatively to keep positive
Delta = (rand:uniform() - 0.5) * PerturbRange,
NewTau = clamp(CurrentTau * (1.0 + Delta), 0.001, 100.0),
UpdatedNeuron = Neuron#neuron{time_constant = NewTau},
genotype:write(UpdatedNeuron),
tweann_logger:debug("Mutated time_constant: ~p -> ~p", [CurrentTau, NewTau]),
ok.
%% @doc Mutate state bound (A) of a random LTC/CfC neuron.
%%
%% Perturbs the state bound which limits internal state magnitude.
%% Larger bounds allow more dynamic range but may be less stable.
%%
%% @param AgentId the agent to mutate
%% @returns ok or {error, term()}
-spec mutate_state_bound(term()) -> ok | {error, term()}.
mutate_state_bound(AgentId) ->
Agent = genotype:dirty_read({agent, AgentId}),
PerturbRange = Agent#agent.perturbation_range,
case mutation_helpers:select_ltc_neuron(AgentId) of
{error, Reason} ->
{error, Reason};
NeuronId ->
perturb_state_bound(NeuronId, PerturbRange)
end.
perturb_state_bound(NeuronId, PerturbRange) ->
Neuron = genotype:dirty_read({neuron, NeuronId}),
CurrentBound = Neuron#neuron.state_bound,
%% Perturb multiplicatively to keep positive
Delta = (rand:uniform() - 0.5) * PerturbRange,
NewBound = clamp(CurrentBound * (1.0 + Delta), 0.1, 10.0),
UpdatedNeuron = Neuron#neuron{state_bound = NewBound},
genotype:write(UpdatedNeuron),
tweann_logger:debug("Mutated state_bound: ~p -> ~p", [CurrentBound, NewBound]),
ok.
%% @doc Mutate backbone/head weights of a random LTC/CfC neuron.
%%
%% Perturbs the learned weights in the LTC f() backbone and h() head networks.
%% These weights control the adaptive time constant and target state.
%%
%% @param AgentId the agent to mutate
%% @returns ok or {error, term()}
-spec mutate_ltc_weights(term()) -> ok | {error, term()}.
mutate_ltc_weights(AgentId) ->
Agent = genotype:dirty_read({agent, AgentId}),
PerturbRange = Agent#agent.perturbation_range * ?DELTA_MULTIPLIER,
case mutation_helpers:select_ltc_neuron(AgentId) of
{error, Reason} ->
{error, Reason};
NeuronId ->
perturb_ltc_weights(NeuronId, PerturbRange)
end.
perturb_ltc_weights(NeuronId, PerturbRange) ->
Neuron = genotype:dirty_read({neuron, NeuronId}),
NewBackboneWeights = mutation_helpers:perturb_ltc_weight_list(
Neuron#neuron.ltc_backbone_weights, PerturbRange
),
NewHeadWeights = mutation_helpers:perturb_ltc_weight_list(
Neuron#neuron.ltc_head_weights, PerturbRange
),
UpdatedNeuron = Neuron#neuron{
ltc_backbone_weights = NewBackboneWeights,
ltc_head_weights = NewHeadWeights
},
genotype:write(UpdatedNeuron),
ok.
%%==============================================================================
%% Private Helpers
%%==============================================================================
%% @private Clamp value between min and max.
-spec clamp(float(), float(), float()) -> float().
clamp(Value, Min, Max) ->
max(Min, min(Max, Value)).