%% @doc Behaviour for domain-specific evaluators. %% %% This module defines the behaviour that domain-specific evaluators must %% implement. Evaluators are responsible for: %% %% %% %% == 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.