Current section

Files

Jump to
macula_tweann src actuator.erl
Raw

src/actuator.erl

%% @doc Actuator process for TWEANN networks.
%%
%% Actuators are the output interface of a neural network. They receive
%% signals from neurons and produce actions or outputs that affect the
%% environment or are used for fitness evaluation.
%%
%% == Actuator Lifecycle ==
%%
%% 1. Spawned by cortex with configuration
%% 2. Waits for signals from input neurons
%% 3. Accumulates all inputs
%% 4. Calls actuator function to produce output
%% 5. Reports output to cortex
%% 6. Repeats from step 2
%%
%% == Actuator Functions ==
%%
%% Actuator functions determine how the neural output is used.
%% Common examples:
%%
%% - `pts' - Pass-through sum (for testing)
%% - `xor_output' - XOR problem output
%% - `pole_output' - Pole balancing action
%%
%% @author Macula.io
%% @copyright 2025 Macula.io, Apache-2.0
-module(actuator).
-export([
start_link/1,
init/1
]).
-record(state, {
id :: term(),
cortex_pid :: pid(),
actuator_name :: atom(),
vector_length :: pos_integer(),
fanin_pids :: [pid()],
scape_pid :: pid() | undefined,
parameters :: list(),
acc_input :: #{pid() => [float()]},
expected_inputs :: non_neg_integer()
}).
%% @doc Start an actuator process.
%%
%% Options:
%% - `id' - Unique identifier for this actuator
%% - `cortex_pid' - PID of the controlling cortex
%% - `actuator_name' - Atom naming the actuator function
%% - `vector_length' - Expected length of input vector
%% - `fanin_pids' - List of PIDs that send input to this actuator
%% - `scape_pid' - PID of the scape/environment (optional)
%% - `parameters' - Additional parameters for actuator function
-spec start_link(map()) -> {ok, pid()}.
start_link(Opts) ->
Pid = spawn_link(?MODULE, init, [Opts]),
{ok, Pid}.
%% @doc Initialize the actuator and enter the main loop.
-spec init(map()) -> no_return().
init(Opts) ->
Id = maps:get(id, Opts),
CortexPid = maps:get(cortex_pid, Opts),
ActuatorName = maps:get(actuator_name, Opts),
VectorLength = maps:get(vector_length, Opts, 1),
FaninPids = maps:get(fanin_pids, Opts, []),
ScapePid = maps:get(scape_pid, Opts, undefined),
Parameters = maps:get(parameters, Opts, []),
State = #state{
id = Id,
cortex_pid = CortexPid,
actuator_name = ActuatorName,
vector_length = VectorLength,
fanin_pids = FaninPids,
scape_pid = ScapePid,
parameters = Parameters,
acc_input = #{},
expected_inputs = length(FaninPids)
},
loop(State).
%% Internal functions
loop(State) ->
receive
{forward, FromPid, Signal} ->
NewState = handle_forward(FromPid, Signal, State),
loop(NewState);
{cortex, terminate} ->
ok;
{link, fanin_pids, FaninPids} ->
%% Dynamic linking from constructor
NewState = State#state{
fanin_pids = FaninPids,
expected_inputs = length(FaninPids)
},
loop(NewState)
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_report(State#state{acc_input = NewAccInput});
false ->
State#state{acc_input = NewAccInput}
end.
process_and_report(State) ->
#state{
cortex_pid = CortexPid,
actuator_name = ActuatorName,
fanin_pids = FaninPids,
scape_pid = ScapePid,
parameters = Parameters,
acc_input = AccInput
} = State,
%% Build input vector in correct order
Input = build_input(FaninPids, AccInput),
%% Process through actuator function
Output = actuate(ActuatorName, Input, ScapePid, Parameters),
%% Report to cortex
CortexPid ! {actuator_output, self(), Output},
%% Reset accumulated inputs
State#state{acc_input = #{}}.
build_input(FaninPids, AccInput) ->
lists:flatten([maps:get(Pid, AccInput, [0.0]) || Pid <- FaninPids]).
%% Built-in actuator functions
actuate(pts, Input, _ScapePid, _Parameters) ->
%% Pass-through sum - just return the input as-is
Input;
actuate(identity, Input, _ScapePid, _Parameters) ->
%% Identity - same as pts
Input;
actuate(threshold, Input, _ScapePid, Parameters) ->
%% Binary threshold - convert to 0/1 based on threshold
Threshold = proplists:get_value(threshold, Parameters, 0.0),
[if V > Threshold -> 1.0; true -> 0.0 end || V <- Input];
actuate(softmax, Input, _ScapePid, _Parameters) ->
%% Softmax - convert to probability distribution
Max = lists:max(Input),
Exps = [math:exp(V - Max) || V <- Input],
Sum = lists:sum(Exps),
[E / Sum || E <- Exps];
actuate(argmax, Input, _ScapePid, _Parameters) ->
%% Argmax - return index of maximum value (0-indexed)
{_Max, Index, _} = lists:foldl(
fun(V, {MaxV, MaxI, I}) ->
case V > MaxV of
true -> {V, I, I + 1};
false -> {MaxV, MaxI, I + 1}
end
end,
{hd(Input), 0, 0},
Input
),
[float(Index)];
actuate(scape, Input, ScapePid, Parameters) ->
%% Send to scape for evaluation
case ScapePid of
undefined ->
Input;
_ ->
ScapePid ! {self(), actuate, Input, Parameters},
receive
{ScapePid, result, Result} ->
Result
after 5000 ->
Input
end
end;
actuate(_ActuatorName, Input, _ScapePid, _Parameters) ->
%% Default: pass through
Input.