Current section
Files
Jump to
Current section
Files
src/sensor.erl
%% @doc Sensor process for TWEANN networks.
%%
%% Sensors are the input interface of a neural network. They read data
%% from the environment or problem domain and forward it to connected
%% neurons. Each sensor has a specific function that determines what
%% data it produces.
%%
%% == Sensor Lifecycle ==
%%
%% 1. Spawned by cortex with configuration
%% 2. Waits for sync signal from cortex
%% 3. Calls sensor function to get input data
%% 4. Forwards data to all connected neurons
%% 5. Repeats from step 2
%%
%% == Sensor Functions ==
%%
%% Sensor functions are atoms that map to actual functions in the
%% problem-specific module. Common examples:
%%
%% - `rng' - Random number generator (for testing)
%% - `xor_input' - XOR problem input
%% - `pole_input' - Pole balancing input
%%
%% @author Macula.io
%% @copyright 2025 Macula.io, Apache-2.0
-module(sensor).
-export([
start_link/1,
init/1
]).
-record(state, {
id :: term(),
cortex_pid :: pid(),
sensor_name :: atom(),
vector_length :: pos_integer(),
fanout_pids :: [pid()],
scape_pid :: pid() | undefined,
parameters :: list()
}).
%% @doc Start a sensor process.
%%
%% Options:
%% - `id' - Unique identifier for this sensor
%% - `cortex_pid' - PID of the controlling cortex
%% - `sensor_name' - Atom naming the sensor function
%% - `vector_length' - Length of output vector
%% - `fanout_pids' - List of PIDs to forward output to
%% - `scape_pid' - PID of the scape/environment (optional)
%% - `parameters' - Additional parameters for sensor function
-spec start_link(map()) -> {ok, pid()}.
start_link(Opts) ->
Pid = spawn_link(?MODULE, init, [Opts]),
{ok, Pid}.
%% @doc Initialize the sensor and enter the main loop.
-spec init(map()) -> no_return().
init(Opts) ->
Id = maps:get(id, Opts),
CortexPid = maps:get(cortex_pid, Opts),
SensorName = maps:get(sensor_name, Opts),
VectorLength = maps:get(vector_length, Opts, 1),
FanoutPids = maps:get(fanout_pids, Opts, []),
ScapePid = maps:get(scape_pid, Opts, undefined),
Parameters = maps:get(parameters, Opts, []),
State = #state{
id = Id,
cortex_pid = CortexPid,
sensor_name = SensorName,
vector_length = VectorLength,
fanout_pids = FanoutPids,
scape_pid = ScapePid,
parameters = Parameters
},
loop(State).
%% Internal functions
loop(State) ->
receive
{cortex, sync} ->
handle_sync(State),
loop(State);
{cortex, terminate} ->
ok;
{scape, Signal} ->
%% Scape provides the signal directly
handle_scape_signal(Signal, State),
loop(State);
{link, fanout_pids, FanoutPids} ->
%% Dynamic linking from constructor
loop(State#state{fanout_pids = FanoutPids})
end.
handle_sync(State) ->
#state{
sensor_name = SensorName,
vector_length = VectorLength,
fanout_pids = FanoutPids,
scape_pid = ScapePid,
parameters = Parameters
} = State,
%% Get sensor output
Signal = case ScapePid of
undefined ->
%% Use built-in sensor function
sense(SensorName, VectorLength, Parameters);
_ ->
%% Request from scape
ScapePid ! {self(), sense, SensorName, Parameters},
receive
{ScapePid, sensory_signal, SensorySignal} ->
SensorySignal
after 5000 ->
%% Timeout - return zeros
lists:duplicate(VectorLength, 0.0)
end
end,
%% Forward to all connected neurons
lists:foreach(
fun(NeuronPid) ->
NeuronPid ! {forward, self(), Signal}
end,
FanoutPids
).
handle_scape_signal(Signal, State) ->
#state{fanout_pids = FanoutPids} = State,
%% Forward scape signal to all connected neurons
lists:foreach(
fun(NeuronPid) ->
NeuronPid ! {forward, self(), Signal}
end,
FanoutPids
).
%% Built-in sensor functions
sense(rng, VectorLength, _Parameters) ->
%% Random number generator - useful for testing
[rand:uniform() * 2 - 1 || _ <- lists:seq(1, VectorLength)];
sense(ones, VectorLength, _Parameters) ->
%% All ones - useful for bias-like behavior
lists:duplicate(VectorLength, 1.0);
sense(zeros, VectorLength, _Parameters) ->
%% All zeros
lists:duplicate(VectorLength, 0.0);
sense(counter, VectorLength, _Parameters) ->
%% Counter - returns [1.0, 2.0, ..., N]
[float(I) / VectorLength || I <- lists:seq(1, VectorLength)];
sense(step, VectorLength, Parameters) ->
%% Step function with configurable step value
Step = proplists:get_value(step, Parameters, 0.1),
%% Get current value from process dictionary
Current = case get(step_value) of
undefined -> 0.0;
V -> V
end,
NewValue = Current + Step,
put(step_value, NewValue),
lists:duplicate(VectorLength, NewValue);
sense(_SensorName, VectorLength, _Parameters) ->
%% Default: return zeros for unknown sensor
lists:duplicate(VectorLength, 0.0).