Current section

Files

Jump to
macula_neuroevolution src elixir_evaluator_bridge.erl
Raw

src/elixir_evaluator_bridge.erl

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