Current section
Files
Jump to
Current section
Files
src/constructor.erl
%% @doc Phenotype constructor for TWEANN networks.
%%
%% This module converts genotypes (genetic descriptions) into phenotypes
%% (running neural network processes). It spawns sensor, neuron, actuator,
%% and cortex processes based on the genotype specification.
%%
%% Based on DXNN2 by Gene Sher ("Handbook of Neuroevolution through Erlang").
%%
%% == Construction Process ==
%%
%% 1. Read agent genotype from Mnesia database
%% 2. Spawn all sensors, neurons, and actuators
%% 3. Build PID mappings from genotype IDs to process PIDs
%% 4. Spawn cortex to coordinate the network
%% 5. Return phenotype record with all PIDs
%%
%% == Phenotype Record ==
%%
%% The phenotype is a map containing:
%% - agent_id: Original agent ID
%% - cortex_pid: PID of the cortex process
%% - sensor_pids: List of sensor PIDs
%% - neuron_pids: List of neuron PIDs
%% - actuator_pids: List of actuator PIDs
%% - id_to_pid: Map from genotype IDs to PIDs
%%
%% @author Macula.io
%% @copyright 2025 Macula.io, Apache-2.0
-module(constructor).
-include("records.hrl").
-export([
construct/1,
construct/2,
terminate/1
]).
%%==============================================================================
%% Public API
%%==============================================================================
%% @doc Construct a phenotype from an agent genotype.
%%
%% Spawns all neural network processes and returns a phenotype record.
%%
%% @param AgentId The ID of the agent genotype
%% @returns Phenotype map with all process PIDs
-spec construct(term()) -> #{
agent_id := term(),
cortex_pid := pid(),
sensor_pids := [pid()],
neuron_pids := [pid()],
actuator_pids := [pid()],
id_to_pid := map()
}.
construct(AgentId) ->
construct(AgentId, #{}).
%% @doc Construct a phenotype with options.
%%
%% Options:
%% - exoself_pid: PID to receive cortex messages (default: self())
%% - scape_pid: PID of the environment/scape (default: undefined)
%%
%% @param AgentId The ID of the agent genotype
%% @param Opts Construction options
%% @returns Phenotype map with all process PIDs
-spec construct(term(), map()) -> #{
agent_id := term(),
cortex_pid := pid(),
sensor_pids := [pid()],
neuron_pids := [pid()],
actuator_pids := [pid()],
id_to_pid := map()
}.
construct(AgentId, Opts) ->
ExoselfPid = maps:get(exoself_pid, Opts, self()),
ScapePid = maps:get(scape_pid, Opts, undefined),
%% Read agent and cortex from Mnesia (returns records)
Agent = genotype:dirty_read({agent, AgentId}),
CortexId = Agent#agent.cx_id,
Cortex = genotype:dirty_read({cortex, CortexId}),
%% Get component IDs from records
SensorIds = Cortex#cortex.sensor_ids,
NeuronIds = Cortex#cortex.neuron_ids,
ActuatorIds = Cortex#cortex.actuator_ids,
%% Spawn all components (without connections yet)
{SensorPids, SensorIdMap} = spawn_sensors(SensorIds, ScapePid),
{NeuronPids, NeuronIdMap} = spawn_neurons(NeuronIds),
{ActuatorPids, ActuatorIdMap} = spawn_actuators(ActuatorIds, ScapePid),
%% Build complete ID to PID mapping
IdToPid = maps:merge(maps:merge(SensorIdMap, NeuronIdMap), ActuatorIdMap),
%% Link components with actual PIDs
link_sensors(SensorIds, IdToPid),
link_neurons(NeuronIds, IdToPid),
link_actuators(ActuatorIds, IdToPid),
%% Spawn cortex
{ok, CortexPid} = cortex:start_link(#{
id => CortexId,
exoself_pid => ExoselfPid,
sensor_pids => SensorPids,
neuron_pids => NeuronPids,
actuator_pids => ActuatorPids
}),
%% Return phenotype
#{
agent_id => AgentId,
cortex_pid => CortexPid,
sensor_pids => SensorPids,
neuron_pids => NeuronPids,
actuator_pids => ActuatorPids,
id_to_pid => IdToPid
}.
%% @doc Terminate a phenotype (stop all processes).
%%
%% Sends terminate signal to the cortex, which terminates all components.
%%
%% @param Phenotype The phenotype map to terminate
-spec terminate(map()) -> ok.
terminate(Phenotype) ->
CortexPid = maps:get(cortex_pid, Phenotype),
CortexPid ! {exoself, stop},
ok.
%%==============================================================================
%% Internal Functions
%%==============================================================================
%% Spawn sensor processes (without fanout connections)
spawn_sensors(SensorIds, ScapePid) ->
{Pids, IdMap} = lists:foldl(
fun(SensorId, {AccPids, AccMap}) ->
Sensor = genotype:dirty_read({sensor, SensorId}),
{ok, Pid} = sensor:start_link(#{
id => SensorId,
cortex_pid => undefined, % Will not be used
sensor_name => Sensor#sensor.name,
vector_length => Sensor#sensor.vl,
fanout_pids => [], % Linked later
scape_pid => ScapePid,
parameters => case Sensor#sensor.parameters of
undefined -> [];
Params -> Params
end
}),
{AccPids ++ [Pid], AccMap#{SensorId => Pid}}
end,
{[], #{}},
SensorIds
),
{Pids, IdMap}.
%% Spawn neuron processes (without connections)
spawn_neurons(NeuronIds) ->
{Pids, IdMap} = lists:foldl(
fun(NeuronId, {AccPids, AccMap}) ->
Neuron = genotype:dirty_read({neuron, NeuronId}),
%% Get input_idps from record
InputIdps = Neuron#neuron.input_idps,
%% Get activation function with default
AF = case Neuron#neuron.af of
undefined -> tanh;
Val -> Val
end,
%% Get aggregation function with default
AggrF = case Neuron#neuron.aggr_f of
undefined -> dot_product;
Val2 -> Val2
end,
{ok, Pid} = neuron:start_link(#{
id => NeuronId,
cortex_pid => undefined, % Will be set by cortex
activation_function => AF,
aggregation_function => AggrF,
input_pids => [], % Linked later
output_pids => [], % Linked later
ro_pids => [], % Linked later
input_weights => #{}, % Linked later
bias => 0.0 % Bias is part of input_idps in DXNN2
}),
%% Store genotype input weights for later linking
put({neuron_idps, NeuronId}, InputIdps),
{AccPids ++ [Pid], AccMap#{NeuronId => Pid}}
end,
{[], #{}},
NeuronIds
),
{Pids, IdMap}.
%% Spawn actuator processes (without fanin connections)
spawn_actuators(ActuatorIds, ScapePid) ->
{Pids, IdMap} = lists:foldl(
fun(ActuatorId, {AccPids, AccMap}) ->
Actuator = genotype:dirty_read({actuator, ActuatorId}),
{ok, Pid} = actuator:start_link(#{
id => ActuatorId,
cortex_pid => undefined, % Will be set by cortex
actuator_name => Actuator#actuator.name,
vector_length => Actuator#actuator.vl,
fanin_pids => [], % Linked later
scape_pid => ScapePid,
parameters => case Actuator#actuator.parameters of
undefined -> [];
Params -> Params
end
}),
{AccPids ++ [Pid], AccMap#{ActuatorId => Pid}}
end,
{[], #{}},
ActuatorIds
),
{Pids, IdMap}.
%% Link sensors to their fanout neurons
link_sensors(SensorIds, IdToPid) ->
lists:foreach(
fun(SensorId) ->
Sensor = genotype:dirty_read({sensor, SensorId}),
SensorPid = maps:get(SensorId, IdToPid),
FanoutIds = Sensor#sensor.fanout_ids,
FanoutPids = [maps:get(Id, IdToPid) || Id <- FanoutIds],
%% Update sensor with actual fanout PIDs
SensorPid ! {link, fanout_pids, FanoutPids}
end,
SensorIds
).
%% Link neurons to their inputs and outputs
link_neurons(NeuronIds, IdToPid) ->
lists:foreach(
fun(NeuronId) ->
Neuron = genotype:dirty_read({neuron, NeuronId}),
NeuronPid = maps:get(NeuronId, IdToPid),
%% Get stored input_idps
InputIdps = get({neuron_idps, NeuronId}),
_ = erase({neuron_idps, NeuronId}),
%% Convert to PID-based weights map
InputWeights = lists:foldl(
fun({InputId, Weights}, Acc) ->
InputPid = maps:get(InputId, IdToPid),
Acc#{InputPid => Weights}
end,
#{},
InputIdps
),
InputPids = [maps:get(InputId, IdToPid) || {InputId, _} <- InputIdps],
%% Get output PIDs from record
OutputIds = Neuron#neuron.output_ids,
OutputPids = [maps:get(Id, IdToPid) || Id <- OutputIds],
%% Get recurrent output PIDs from record
RoIds = Neuron#neuron.ro_ids,
RoPids = [maps:get(Id, IdToPid) || Id <- RoIds],
%% Update neuron with actual PIDs
NeuronPid ! {link, input_pids, InputPids},
NeuronPid ! {link, output_pids, OutputPids},
NeuronPid ! {link, ro_pids, RoPids},
NeuronPid ! {link, input_weights, InputWeights}
end,
NeuronIds
).
%% Link actuators to their fanin neurons
link_actuators(ActuatorIds, IdToPid) ->
lists:foreach(
fun(ActuatorId) ->
Actuator = genotype:dirty_read({actuator, ActuatorId}),
ActuatorPid = maps:get(ActuatorId, IdToPid),
FaninIds = Actuator#actuator.fanin_ids,
FaninPids = [maps:get(Id, IdToPid) || Id <- FaninIds],
%% Update actuator with actual fanin PIDs
ActuatorPid ! {link, fanin_pids, FaninPids}
end,
ActuatorIds
).