Current section

Files

Jump to
macula_neuroevolution src agent_sdk bridge_evaluator.erl
Raw

src/agent_sdk/bridge_evaluator.erl

%% @doc Bridge Evaluator - Adapts Agent SDK to neuroevolution_evaluator behaviour.
%%
%% This module bridges the Agent SDK's agent_bridge with the neuroevolution
%% system's evaluator interface. It allows agent_trainer to use agent_bridge
%% configurations with neuroevolution_server.
%%
%% == Usage ==
%%
%% This module is used internally by agent_trainer. You don't need to
%% use it directly. Configure your training via agent_trainer:train/2.
%%
%% == How It Works ==
%%
%% 1. agent_trainer stores the validated bridge in evaluator_options
%% 2. This module extracts the bridge and env_config from options
%% 3. For each individual, it extracts the network and runs an episode
%% 4. Returns the individual with populated metrics and fitness
%%
%% @author Macula.io
%% @copyright 2025 Macula.io
%% @see agent_trainer
%% @see neuroevolution_evaluator
-module(bridge_evaluator).
-behaviour(neuroevolution_evaluator).
-include("neuroevolution.hrl").
%%% ============================================================================
%%% Behaviour Callbacks
%%% ============================================================================
-export([evaluate/2, calculate_fitness/1]).
%% @doc Evaluates an individual using the agent bridge.
%%
%% Extracts the network from the individual, runs it through the bridge's
%% sense-think-act cycle, and returns the individual with metrics populated.
-spec evaluate(Individual, Options) -> {ok, Individual} | {error, term()} when
Individual :: individual(),
Options :: map().
evaluate(Individual, Options) ->
Bridge = maps:get(bridge, Options),
EnvConfig = maps:get(env_config, Options, #{}),
EpisodesPerEval = maps:get(episodes_per_eval, Options, 1),
Network = Individual#individual.network,
%% Run evaluation
Result = run_evaluation(Bridge, Network, EnvConfig, EpisodesPerEval),
case Result of
{ok, Fitness, Metrics} ->
UpdatedIndividual = Individual#individual{
fitness = Fitness,
metrics = Metrics
},
{ok, UpdatedIndividual};
{ok, Metrics} when is_map(Metrics) ->
%% Bridge without evaluator - shouldn't happen in agent_trainer
%% but handle gracefully
Fitness = default_fitness(Metrics),
UpdatedIndividual = Individual#individual{
fitness = Fitness,
metrics = Metrics
},
{ok, UpdatedIndividual};
{error, Reason} ->
{error, Reason}
end.
%% @doc Calculates fitness from metrics (not used - fitness comes from bridge).
%%
%% This is a fallback in case the neuroevolution server calls it directly.
%% Normally, fitness is calculated by agent_evaluator via the bridge.
-spec calculate_fitness(Metrics) -> float() when
Metrics :: map().
calculate_fitness(Metrics) ->
default_fitness(Metrics).
%%% ============================================================================
%%% Internal Functions
%%% ============================================================================
%% @private
%% Run single or multi-episode evaluation
run_evaluation(Bridge, Network, EnvConfig, 1) ->
agent_bridge:run_episode(Bridge, Network, EnvConfig);
run_evaluation(Bridge, Network, EnvConfig, Episodes) when Episodes > 1 ->
Results = [agent_bridge:run_episode(Bridge, Network, EnvConfig)
|| _ <- lists:seq(1, Episodes)],
aggregate_results(Results).
%% @private
%% Aggregate multiple episode results
aggregate_results(Results) ->
{Fitnesses, MetricsList} = lists:foldl(
fun({ok, Fitness, Metrics}, {FAcc, MAcc}) when is_float(Fitness) ->
{[Fitness | FAcc], [Metrics | MAcc]};
({ok, Metrics}, {FAcc, MAcc}) when is_map(Metrics) ->
{FAcc, [Metrics | MAcc]};
({error, _}, {FAcc, MAcc}) ->
{FAcc, MAcc}
end,
{[], []},
Results
),
case {Fitnesses, MetricsList} of
{[], []} ->
{error, all_episodes_failed};
{[], [M | _]} ->
%% No fitness (no evaluator), return first metrics
{ok, M};
{Fs, [M | _]} ->
%% Average fitness, return first metrics (or could merge)
AvgFitness = lists:sum(Fs) / length(Fs),
{ok, AvgFitness, M}
end.
%% @private
%% Default fitness calculation when none provided
default_fitness(Metrics) when is_map(Metrics) ->
%% Sum all numeric values as a basic heuristic
maps:fold(
fun(_K, V, Acc) when is_number(V) -> Acc + V;
(_K, _V, Acc) -> Acc
end,
0.0,
Metrics
).