Current section
Files
Jump to
Current section
Files
src/neuroevolution_evaluator.erl
%% @doc Behaviour for domain-specific evaluators.
%%
%% This module defines the behaviour that domain-specific evaluators must
%% implement. Evaluators are responsible for:
%%
%% <ul>
%% <li>Running an individual through domain-specific tests/games/simulations</li>
%% <li>Calculating metrics (score, ticks survived, wins, etc.)</li>
%% <li>Optionally calculating fitness from metrics</li>
%% </ul>
%%
%% == Implementing an Evaluator ==
%%
%% To create a custom evaluator, implement the `evaluate/2' callback:
%%
%% -module(my_evaluator).
%% -behaviour(neuroevolution_evaluator).
%%
%% -export([evaluate/2]).
%%
%% evaluate(Individual, Options) ->
%% Network = Individual#individual.network,
%% %% Run your evaluation logic
%% Score = run_game(Network),
%% Ticks = get_survival_time(),
%% %% Return updated individual with metrics
%% UpdatedIndividual = Individual#individual{
%% metrics = #{score => Score, ticks => Ticks}
%% },
%% {ok, UpdatedIndividual}.
%%
%% == Optional Fitness Calculation ==
%%
%% By default, fitness is calculated by the neuroevolution server using
%% a standard formula. You can override this by implementing `calculate_fitness/1':
%%
%% -export([calculate_fitness/1]).
%%
%% calculate_fitness(Metrics) ->
%% Score = maps:get(score, Metrics, 0),
%% Ticks = maps:get(ticks, Metrics, 0),
%% Score * 50.0 + Ticks / 50.0.
%%
%% @author Macula.io
%% @copyright 2025 Macula.io
-module(neuroevolution_evaluator).
-include("neuroevolution.hrl").
%%% ============================================================================
%%% Behaviour Callbacks
%%% ============================================================================
%% Callback: evaluate/2
%% Invoked for each individual during generation evaluation.
%% The evaluator should:
%% 1. Run the individual's network through domain-specific tests
%% 2. Collect performance metrics
%% 3. Return the individual with updated metrics field
%%
%% Options map contains evaluator_options from config, plus:
%% - games - number of evaluations to run
%% - notify_pid - optional PID to send progress notifications
%%
%% If evaluation fails, return {error, Reason}.
-callback evaluate(Individual, Options) -> Result when
Individual :: individual(),
Options :: map(),
Result :: {ok, EvaluatedIndividual :: individual()} | {error, term()}.
%% Callback: calculate_fitness/1 (optional)
%% If not implemented, the server uses a default fitness calculation.
%% Implement this to customize how metrics are converted to fitness.
-callback calculate_fitness(Metrics) -> Fitness when
Metrics :: map(),
Fitness :: float().
-optional_callbacks([calculate_fitness/1]).
%%% ============================================================================
%%% API Functions
%%% ============================================================================
-export([
evaluate_individual/3,
default_fitness/1
]).
%% @doc Evaluate an individual using the specified evaluator module.
%%
%% Delegates to the evaluator module's evaluate/2 callback.
%% Returns the evaluated individual with metrics populated.
%% Catches and logs any exceptions from the evaluator.
-spec evaluate_individual(Individual, EvaluatorModule, Options) -> Result when
Individual :: individual(),
EvaluatorModule :: module(),
Options :: map(),
Result :: {ok, individual()} | {error, term()}.
evaluate_individual(Individual, EvaluatorModule, Options) ->
try
EvaluatorModule:evaluate(Individual, Options)
catch
Class:Reason:Stacktrace ->
error_logger:error_msg(
"[neuroevolution_evaluator] Evaluation failed: ~p:~p~n~p~n",
[Class, Reason, Stacktrace]
),
{error, {evaluation_failed, Class, Reason}}
end.
%% @doc Default fitness calculation from standard metrics.
%%
%% Uses a formula that balances:
%% - Score (food eaten, points, etc.) - primary goal
%% - Survival (ticks lived) - secondary goal
%% - Wins - bonus
%%
%% Formula: Score * 50 + Ticks / 50 + Wins * 2
-spec default_fitness(Metrics) -> Fitness when
Metrics :: map(),
Fitness :: float().
default_fitness(Metrics) ->
Score = maps:get(total_score, Metrics, 0),
Ticks = maps:get(total_ticks, Metrics, 0),
Wins = maps:get(wins, Metrics, 0),
ScoreComponent = Score * 50.0,
SurvivalComponent = Ticks / 50.0,
WinComponent = Wins * 2.0,
ScoreComponent + SurvivalComponent + WinComponent.