Current section
Files
Jump to
Current section
Files
src/neuron.erl
%% @doc Neural processing unit for TWEANN networks.
%%
%% The neuron is the fundamental processing element in a neural network.
%% It receives signals from sensors or other neurons, aggregates them,
%% applies an activation function, and forwards the result to connected
%% neurons or actuators.
%%
%% == Neuron Lifecycle ==
%%
%% 1. Spawned by cortex with initial state
%% 2. Waits for signals from input connections
%% 3. Aggregates all inputs when complete
%% 4. Applies activation function
%% 5. Forwards output to all output connections
%% 6. Repeats from step 2
%%
%% == State ==
%%
%% The neuron maintains:
%%
%% - Input connections with weights
%% - Output connections (PIDs)
%% - Accumulated input signals
%% - Activation function
%% - Aggregation function
%%
%% @author Macula.io
%% @copyright 2025 Macula.io, Apache-2.0
-module(neuron).
-export([
start_link/1,
init/1,
forward/3,
backup/1
]).
-record(state, {
id :: term(),
cortex_pid :: pid(),
activation_function :: atom(),
aggregation_function :: atom(),
input_pids :: [pid()],
output_pids :: [pid()],
ro_pids :: [pid()], % recurrent output PIDs
input_weights :: #{pid() => [{float(), float(), float(), list()}]},
bias :: float(),
acc_input :: #{pid() => [float()]},
expected_inputs :: non_neg_integer()
}).
%% @doc Start a neuron process.
%%
%% Options:
%% - `id' - Unique identifier for this neuron
%% - `cortex_pid' - PID of the controlling cortex
%% - `activation_function' - Atom naming the activation function (e.g., tanh)
%% - `aggregation_function' - Atom naming the aggregation function (e.g., dot_product)
%% - `input_pids' - List of PIDs that send input to this neuron
%% - `output_pids' - List of PIDs to forward output to
%% - `ro_pids' - List of recurrent output PIDs
%% - `input_weights' - Map of PID to list of weight tuples
%% - `bias' - Bias value for this neuron
-spec start_link(map()) -> {ok, pid()}.
start_link(Opts) ->
Pid = spawn_link(?MODULE, init, [Opts]),
{ok, Pid}.
%% @doc Initialize the neuron and enter the main loop.
-spec init(map()) -> no_return().
init(Opts) ->
Id = maps:get(id, Opts),
CortexPid = maps:get(cortex_pid, Opts),
ActivationFn = maps:get(activation_function, Opts, tanh),
AggregationFn = maps:get(aggregation_function, Opts, dot_product),
InputPids = maps:get(input_pids, Opts, []),
OutputPids = maps:get(output_pids, Opts, []),
RoPids = maps:get(ro_pids, Opts, []),
InputWeights = maps:get(input_weights, Opts, #{}),
Bias = maps:get(bias, Opts, 0.0),
State = #state{
id = Id,
cortex_pid = CortexPid,
activation_function = ActivationFn,
aggregation_function = AggregationFn,
input_pids = InputPids,
output_pids = OutputPids,
ro_pids = RoPids,
input_weights = InputWeights,
bias = Bias,
acc_input = #{},
expected_inputs = length(InputPids)
},
loop(State).
%% @doc Send a signal to a neuron.
%%
%% Called by sensors or other neurons to forward their output.
-spec forward(pid(), pid(), [float()]) -> ok.
forward(NeuronPid, FromPid, Signal) ->
NeuronPid ! {forward, FromPid, Signal},
ok.
%% @doc Request the neuron to backup its current weights.
%%
%% The neuron will send its weights to the cortex for storage.
-spec backup(pid()) -> ok.
backup(NeuronPid) ->
NeuronPid ! backup,
ok.
%% Internal functions
loop(State) ->
receive
{forward, FromPid, Signal} ->
NewState = handle_forward(FromPid, Signal, State),
loop(NewState);
backup ->
_ = handle_backup(State),
loop(State);
{cortex, terminate} ->
ok;
{update_weights, NewWeights, NewBias} ->
NewState = State#state{
input_weights = NewWeights,
bias = NewBias
},
loop(NewState);
%% Dynamic linking from constructor
{link, input_pids, InputPids} ->
NewState = State#state{
input_pids = InputPids,
expected_inputs = length(InputPids)
},
loop(NewState);
{link, output_pids, OutputPids} ->
loop(State#state{output_pids = OutputPids});
{link, ro_pids, RoPids} ->
loop(State#state{ro_pids = RoPids});
{link, input_weights, InputWeights} ->
loop(State#state{input_weights = InputWeights})
end.
handle_forward(FromPid, Signal, State) ->
#state{
acc_input = AccInput,
expected_inputs = ExpectedInputs
} = State,
%% Accumulate the signal
NewAccInput = maps:put(FromPid, Signal, AccInput),
ReceivedCount = maps:size(NewAccInput),
%% Check if we have all inputs
case ReceivedCount >= ExpectedInputs of
true ->
process_and_forward(State#state{acc_input = NewAccInput});
false ->
State#state{acc_input = NewAccInput}
end.
process_and_forward(State) ->
#state{
activation_function = ActivationFn,
aggregation_function = AggregationFn,
output_pids = OutputPids,
ro_pids = RoPids,
input_weights = InputWeights,
bias = Bias,
acc_input = AccInput,
input_pids = InputPids
} = State,
%% Build input list in correct order
Inputs = build_inputs(InputPids, AccInput),
%% Build weights list in correct order
Weights = build_weights(InputPids, InputWeights),
%% Aggregate inputs
Aggregated = aggregate(AggregationFn, Inputs, Weights),
%% Add bias and apply activation
Output = activate(ActivationFn, Aggregated + Bias),
%% Forward to all output connections
lists:foreach(
fun(OutputPid) ->
OutputPid ! {forward, self(), [Output]}
end,
OutputPids
),
%% Forward to recurrent outputs
lists:foreach(
fun(RoPid) ->
RoPid ! {forward, self(), [Output]}
end,
RoPids
),
%% Reset accumulated inputs
State#state{acc_input = #{}}.
build_inputs(InputPids, AccInput) ->
[{Pid, maps:get(Pid, AccInput, [0.0])} || Pid <- InputPids].
build_weights(InputPids, InputWeights) ->
[{Pid, maps:get(Pid, InputWeights, [{1.0, 0.0, 0.1, []}])} || Pid <- InputPids].
aggregate(dot_product, Inputs, Weights) ->
signal_aggregator:dot_product(Inputs, Weights);
aggregate(mult_product, Inputs, Weights) ->
signal_aggregator:mult_product(Inputs, Weights);
aggregate(diff_product, Inputs, Weights) ->
signal_aggregator:diff_product(Inputs, Weights);
aggregate(Function, Inputs, Weights) ->
signal_aggregator:Function(Inputs, Weights).
activate(tanh, X) -> functions:tanh(X);
activate(sigmoid, X) -> functions:sigmoid(X);
activate(sigmoid1, X) -> functions:sigmoid1(X);
activate(sin, X) -> functions:sin(X);
activate(cos, X) -> functions:cos(X);
activate(gaussian, X) -> functions:gaussian(X);
activate(linear, X) -> functions:linear(X);
activate(sgn, X) -> functions:sgn(X);
activate(bin, X) -> functions:bin(X);
activate(trinary, X) -> functions:trinary(X);
activate(multiquadric, X) -> functions:multiquadric(X);
activate(quadratic, X) -> functions:quadratic(X);
activate(cubic, X) -> functions:cubic(X);
activate(absolute, X) -> functions:absolute(X);
activate(sqrt, X) -> functions:sqrt(X);
activate(log, X) -> functions:log(X);
activate(relu, X) -> functions:relu(X);
activate(Function, X) -> functions:Function(X).
handle_backup(State) ->
#state{
id = Id,
cortex_pid = CortexPid,
input_weights = InputWeights,
bias = Bias
} = State,
CortexPid ! {backup, Id, InputWeights, Bias}.