Current section

Files

Jump to
macula_neuroevolution src silos lc_silo_behavior.erl
Raw

src/silos/lc_silo_behavior.erl

%% @doc Common behavior module for Liquid Conglomerate Silos.
%%
%% All 13 silos implement this common pattern derived from task_silo.erl:
%% gen_server behavior
%% L0/L1/L2 hierarchical control levels
%% Evaluation-centric tracking (total_evaluations as primary dimension)
%% Velocity-based stagnation detection
%% Cross-silo signal integration via lc_cross_silo
%% ETS tables for persistent collections (where needed)
%%
%% == Implementing a New Silo ==
%%
%% 1. Create module with -behaviour(lc_silo_behavior)
%% 2. Include lc_silos.hrl for common records
%% 3. Implement required callbacks:
%% init_silo/1: Initialize silo-specific state
%% collect_sensors/1: Gather L0 sensor values
%% apply_actuators/2: Apply L0 actuator outputs
%% compute_reward/1: Compute reward for LC learning
%%
%% @author Macula.io
%% @copyright 2025 Macula.io
-module(lc_silo_behavior).
%% Behavior callbacks
-callback init_silo(Config :: map()) ->
{ok, SiloState :: map()} | {error, Reason :: term()}.
-callback collect_sensors(SiloState :: map()) -> Sensors :: map().
-callback apply_actuators(Actuators :: map(), SiloState :: map()) ->
{ok, NewSiloState :: map()}.
-callback compute_reward(SiloState :: map()) -> Reward :: float().
%% Required: silo identity and time constant
-callback get_silo_type() -> atom().
-callback get_time_constant() -> float().
%% Optional: cross-silo communication
-callback handle_cross_silo_signals(Signals :: map(), SiloState :: map()) ->
{ok, NewSiloState :: map()}.
-callback emit_cross_silo_signals(SiloState :: map()) -> ok.
%% Optional: event persistence (call lc_event_emitter:emit/3)
-callback emit_silo_events(EventType :: atom(), SiloState :: map()) -> ok.
-optional_callbacks([
handle_cross_silo_signals/2,
emit_cross_silo_signals/1,
emit_silo_events/2
]).
%% API exports for helper functions
-export([
normalize/3,
clamp/3,
compute_velocity/4,
compute_stagnation_severity/2,
ema_smooth/3,
asymmetric_ema_smooth/5
]).
%%% ============================================================================
%%% Helper Functions for Silo Implementations
%%% ============================================================================
%% @doc Normalize a value to [0,1] range given min/max bounds.
-spec normalize(Value :: number(), Min :: number(), Max :: number()) -> float().
normalize(Value, Min, Max) when Max > Min ->
clamp((Value - Min) / (Max - Min), 0.0, 1.0);
normalize(_Value, _Min, _Max) ->
0.5.
%% @doc Clamp a value to a specified range.
-spec clamp(Value :: number(), Min :: number(), Max :: number()) -> number().
clamp(Value, Min, _Max) when Value < Min -> Min;
clamp(Value, _Min, Max) when Value > Max -> Max;
clamp(Value, _Min, _Max) -> Value.
%% @doc Compute improvement velocity from checkpoints.
%%
%% Velocity = (delta_fitness / delta_evaluations) * 1000
%%
%% Returns velocity in fitness improvement per 1000 evaluations.
-spec compute_velocity(
CurrentFitness :: float(),
CurrentEvals :: non_neg_integer(),
PrevFitness :: float(),
PrevEvals :: non_neg_integer()
) -> float().
compute_velocity(CurrentFitness, CurrentEvals, PrevFitness, PrevEvals)
when CurrentEvals > PrevEvals ->
DeltaFitness = CurrentFitness - PrevFitness,
DeltaEvals = CurrentEvals - PrevEvals,
(DeltaFitness / DeltaEvals) * 1000;
compute_velocity(_CurrentFitness, _CurrentEvals, _PrevFitness, _PrevEvals) ->
0.0.
%% @doc Compute stagnation severity from velocity.
%%
%% Severity = clamp((threshold - avg_velocity) / threshold, 0.0, 1.0)
%%
%% Returns 0.0 = healthy, 1.0 = critical stagnation.
-spec compute_stagnation_severity(
AvgVelocity :: float(),
VelocityThreshold :: float()
) -> float().
compute_stagnation_severity(AvgVelocity, VelocityThreshold) when VelocityThreshold > 0 ->
RawSeverity = (VelocityThreshold - AvgVelocity) / VelocityThreshold,
clamp(RawSeverity, 0.0, 1.0);
compute_stagnation_severity(_AvgVelocity, _VelocityThreshold) ->
0.0.
%% @doc Apply exponential moving average smoothing.
%%
%% SmoothedValue = Momentum * PreviousValue + (1 - Momentum) * NewValue
%%
%% Higher momentum = smoother but slower response.
-spec ema_smooth(
NewValue :: float(),
PreviousValue :: float(),
Momentum :: float()
) -> float().
ema_smooth(NewValue, PreviousValue, Momentum) when Momentum >= 0, Momentum =< 1 ->
Momentum * PreviousValue + (1.0 - Momentum) * NewValue;
ema_smooth(NewValue, _PreviousValue, _Momentum) ->
NewValue.
%% @doc Apply asymmetric EMA smoothing for fast escalation, slow de-escalation.
%%
%% When escalating (new value higher): use low momentum (fast response)
%% When de-escalating (new value lower): use high momentum (slow recovery)
%%
%% This prevents oscillation while ensuring responsive intervention.
-spec asymmetric_ema_smooth(
NewValue :: float(),
PreviousValue :: float(),
BaseMomentum :: float(),
EscalationFactor :: float(),
DeescalationOffset :: float()
) -> float().
asymmetric_ema_smooth(NewValue, PreviousValue, BaseMomentum, EscalationFactor, DeescalationOffset) ->
Momentum = compute_asymmetric_momentum(NewValue > PreviousValue, BaseMomentum, EscalationFactor, DeescalationOffset),
ema_smooth(NewValue, PreviousValue, Momentum).
%% @private Escalating: fast response (low momentum)
compute_asymmetric_momentum(true, BaseMomentum, EscalationFactor, _DeescalationOffset) ->
BaseMomentum * EscalationFactor;
%% @private De-escalating: slow recovery (high momentum, capped at 0.9)
compute_asymmetric_momentum(false, BaseMomentum, _EscalationFactor, DeescalationOffset) ->
min(0.9, BaseMomentum + DeescalationOffset).