%% @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).