%% @doc Generic bridge from Erlang neuroevolution_evaluator to Elixir evaluators. %% %% This module implements the neuroevolution_evaluator behaviour and %% delegates to any Elixir module that implements an `evaluate/2` function. %% %% == Usage == %% %% Configure the neuroevolution_server with this module as the evaluator: %% %% ``` %% Config = #neuro_config{ %% evaluator_module = elixir_evaluator_bridge, %% evaluator_options = #{ %% elixir_module => 'Elixir.MyApp.Evaluator', %% elixir_options => #{timeout => 5000} %% } %% }. %% ''' %% %% The Elixir module must export `evaluate/2`: %% %% ```elixir %% defmodule MyApp.Evaluator do %% def evaluate(individual, options) do %% # individual is a map: %{id: _, network: _, fitness: _, metrics: _, generation_born: _} %% # Return {:ok, metrics} or {:error, reason} %% {:ok, %{fitness: 100.0, score: 50}} %% end %% end %% ''' %% %% @author Macula.io %% @copyright 2025 Macula.io -module(elixir_evaluator_bridge). -behaviour(neuroevolution_evaluator). -include("neuroevolution.hrl"). -export([evaluate/2, calculate_fitness/1]). %% @doc Evaluate an individual using an Elixir evaluator module. %% %% Options must contain: %% - `elixir_module': The Elixir module atom (e.g., 'Elixir.MyApp.Evaluator') %% - `elixir_options': Optional map passed to the Elixir evaluator (default: #{}) %% %% The Elixir module must export `evaluate(individual_map, options) -> {:ok, metrics} | {:error, reason}' -spec evaluate(Individual, Options) -> Result when Individual :: individual(), Options :: map(), Result :: {ok, individual()} | {error, term()}. evaluate(Individual, Options) -> ElixirModule = maps:get(elixir_module, Options), ElixirOptions = maps:get(elixir_options, Options, #{}), %% Convert Erlang individual record to Elixir-friendly map IndividualMap = individual_to_map(Individual), %% Call the Elixir evaluator try ElixirModule:evaluate(IndividualMap, ElixirOptions) of {ok, Metrics} when is_map(Metrics) -> %% Update individual with metrics UpdatedIndividual = Individual#individual{ metrics = Metrics }, {ok, UpdatedIndividual}; {error, Reason} -> {error, Reason}; Other -> {error, {unexpected_return, Other}} catch Class:Reason:Stacktrace -> error_logger:error_msg( "[elixir_evaluator_bridge] Elixir evaluator ~p failed: ~p:~p~n~p~n", [ElixirModule, Class, Reason, Stacktrace] ), {error, {elixir_exception, Class, Reason}} end. %% @doc Calculate fitness from metrics. %% %% Uses the fitness value directly from metrics if present, %% otherwise falls back to default calculation. -spec calculate_fitness(Metrics) -> Fitness when Metrics :: map(), Fitness :: float(). calculate_fitness(Metrics) -> case maps:get(fitness, Metrics, undefined) of undefined -> %% Fallback to default fitness calculation neuroevolution_evaluator:default_fitness(Metrics); Fitness when is_float(Fitness) -> Fitness; Fitness when is_integer(Fitness) -> float(Fitness) end. %%% ============================================================================ %%% Internal Functions %%% ============================================================================ %% @private %% @doc Convert individual record to Elixir-friendly map. individual_to_map(#individual{id = Id, network = Network, fitness = Fitness, metrics = Metrics, generation_born = GenBorn}) -> #{ id => Id, network => Network, fitness => Fitness, metrics => Metrics, generation_born => GenBorn }.