Current section

Files

Jump to
macula_neuroevolution src silos developmental_silo developmental_silo.erl
Raw

src/silos/developmental_silo/developmental_silo.erl

%% @doc Developmental Silo - Ontogeny, plasticity, and critical periods.
%%
%% Part of the Liquid Conglomerate v2 architecture. The Developmental Silo manages:
%% Developmental stages and maturation
%% Plasticity levels and decay
%% Critical period timing
%% Metamorphosis triggers
%% Developmental noise
%%
%% == Time Constant ==
%%
%% Ï„ = 40 (slow adaptation for developmental dynamics)
%%
%% == Cross-Silo Signals ==
%%
%% Outgoing:
%% maturity_distribution to task: Population maturity level
%% plasticity_available to cultural: Learning capacity
%% metamorphosis_rate to ecological: Stage transition rate
%% expression_stage to regulatory: Developmental expression phase
%%
%% Incoming:
%% maturity_target from task: Target maturity level
%% plasticity_influence from cultural: Cultural effect on plasticity
%% stress_signal from ecological: Environmental stress level
%% critical_period_timing from temporal: Timing in critical period
%%
%% @author Macula.io
%% @copyright 2025 Macula.io
-module(developmental_silo).
-behaviour(gen_server).
-behaviour(lc_silo_behavior).
-include("lc_silos.hrl").
-include("lc_signals.hrl").
%% API
-export([
start_link/0,
start_link/1,
get_params/1,
update_developmental_state/4,
get_developmental_state/2,
trigger_metamorphosis/2,
open_critical_period/3,
close_critical_period/2,
get_developmental_stats/1,
get_state/1,
reset/1
]).
%% gen_server callbacks
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2
]).
%% lc_silo_behavior callbacks
-export([
init_silo/1,
collect_sensors/1,
apply_actuators/2,
compute_reward/1,
get_silo_type/0,
get_time_constant/0,
handle_cross_silo_signals/2,
emit_cross_silo_signals/1
]).
-define(SERVER, ?MODULE).
-define(TIME_CONSTANT, 40.0).
-define(HISTORY_SIZE, 100).
%% Default actuator values
-define(DEFAULT_PARAMS, #{
growth_rate => 0.05,
maturation_speed => 0.02,
critical_period_duration => 5,
plasticity_decay_rate => 0.05,
initial_plasticity => 0.9,
developmental_noise_level => 0.1,
metamorphosis_trigger => 0.8,
metamorphosis_severity => 0.5
}).
%% Actuator bounds
-define(ACTUATOR_BOUNDS, #{
growth_rate => {0.0, 0.2},
maturation_speed => {0.0, 0.1},
critical_period_duration => {1, 20},
plasticity_decay_rate => {0.0, 0.2},
initial_plasticity => {0.5, 1.0},
developmental_noise_level => {0.0, 0.3},
metamorphosis_trigger => {0.5, 0.95},
metamorphosis_severity => {0.0, 1.0}
}).
-record(state, {
%% Core silo state
realm :: binary(),
enabled_levels :: [l0 | l1 | l2],
l0_tweann_enabled :: boolean(),
l2_enabled :: boolean(),
%% Current parameters (actuator outputs)
current_params :: map(),
%% ETS tables
ets_tables :: #{atom() => ets:tid()},
%% Aggregate statistics
maturity_history :: [float()],
plasticity_history :: [float()],
metamorphosis_count :: non_neg_integer(),
%% Cross-silo signal cache
incoming_signals :: map(),
%% Previous values for smoothing
prev_maturity_distribution :: float(),
prev_plasticity_available :: float()
}).
%%% ============================================================================
%%% API Functions
%%% ============================================================================
-spec start_link() -> {ok, pid()} | ignore | {error, term()}.
start_link() ->
start_link(#{}).
-spec start_link(map()) -> {ok, pid()} | ignore | {error, term()}.
start_link(Config) ->
gen_server:start_link({local, ?SERVER}, ?MODULE, Config, []).
-spec get_params(pid()) -> map().
get_params(Pid) ->
gen_server:call(Pid, get_params).
-spec update_developmental_state(pid(), term(), float(), float()) -> ok.
update_developmental_state(Pid, IndividualId, Stage, Plasticity) ->
gen_server:cast(Pid, {update_developmental_state, IndividualId, Stage, Plasticity}).
-spec get_developmental_state(pid(), term()) -> {ok, map()} | not_found.
get_developmental_state(Pid, IndividualId) ->
gen_server:call(Pid, {get_developmental_state, IndividualId}).
-spec trigger_metamorphosis(pid(), term()) -> ok.
trigger_metamorphosis(Pid, IndividualId) ->
gen_server:cast(Pid, {trigger_metamorphosis, IndividualId}).
-spec open_critical_period(pid(), term(), atom()) -> ok.
open_critical_period(Pid, IndividualId, PeriodType) ->
gen_server:cast(Pid, {open_critical_period, IndividualId, PeriodType}).
-spec close_critical_period(pid(), term()) -> ok.
close_critical_period(Pid, IndividualId) ->
gen_server:cast(Pid, {close_critical_period, IndividualId}).
-spec get_developmental_stats(pid()) -> map().
get_developmental_stats(Pid) ->
gen_server:call(Pid, get_developmental_stats).
-spec get_state(pid()) -> map().
get_state(Pid) ->
gen_server:call(Pid, get_state).
-spec reset(pid()) -> ok.
reset(Pid) ->
gen_server:call(Pid, reset).
%%% ============================================================================
%%% lc_silo_behavior Callbacks
%%% ============================================================================
get_silo_type() -> developmental.
get_time_constant() -> ?TIME_CONSTANT.
init_silo(Config) ->
Realm = maps:get(realm, Config, <<"default">>),
EtsTables = lc_ets_utils:create_tables(developmental, Realm, [
{developmental_states, [{keypos, 1}]},
{critical_periods, [{keypos, 1}]},
{milestones, [{keypos, 1}]}
]),
{ok, #{
ets_tables => EtsTables,
realm => Realm
}}.
collect_sensors(State) ->
#state{
plasticity_history = PlasticityHistory,
ets_tables = EtsTables,
metamorphosis_count = MetaCount,
incoming_signals = InSignals
} = State,
%% Developmental state metrics
StatesTable = maps:get(developmental_states, EtsTables),
PeriodsTable = maps:get(critical_periods, EtsTables),
AllStates = lc_ets_utils:all(StatesTable),
{StageMean, MaturityMean, PlasticityMean} = compute_state_means(AllStates),
%% Plasticity trend
PlasticityTrend = compute_trend(PlasticityHistory),
%% Critical period metrics
AllPeriods = lc_ets_utils:all(PeriodsTable),
CriticalPeriodActiveRatio = compute_active_period_ratio(AllPeriods),
%% Canalization and heterochrony
Canalization = compute_canalization(AllStates),
Heterochrony = compute_heterochrony(AllStates),
%% Metamorphosis rate
MetamorphosisRate = lc_silo_behavior:normalize(MetaCount, 0, 50),
%% Developmental noise
DevelopmentalNoise = compute_developmental_noise(AllStates),
%% Cross-silo signals as sensors
MaturityTarget = maps:get(maturity_target, InSignals, 0.5),
StressLevel = maps:get(stress_signal, InSignals, 0.0),
#{
developmental_stage_mean => StageMean,
maturation_level_mean => MaturityMean,
plasticity_level_mean => PlasticityMean,
plasticity_trend => PlasticityTrend,
critical_period_active_ratio => CriticalPeriodActiveRatio,
canalization_strength => Canalization,
heterochrony_index => Heterochrony,
metamorphosis_rate => MetamorphosisRate,
developmental_noise => DevelopmentalNoise,
%% External signals
maturity_target => MaturityTarget,
stress_level => StressLevel
}.
apply_actuators(Actuators, State) ->
BoundedParams = apply_bounds(Actuators, ?ACTUATOR_BOUNDS),
NewState = State#state{current_params = BoundedParams},
emit_cross_silo_signals(NewState),
{ok, NewState}.
compute_reward(State) ->
Sensors = collect_sensors(State),
%% Reward components:
%% 1. Moderate plasticity (not too rigid, not too unstable)
Plasticity = maps:get(plasticity_level_mean, Sensors, 0.5),
PlasticityOptimality = 1.0 - abs(Plasticity - 0.5) * 2,
%% 2. Progress toward maturity target
MaturityMean = maps:get(maturation_level_mean, Sensors, 0.5),
MaturityTarget = maps:get(maturity_target, Sensors, 0.5),
MaturityProgress = 1.0 - abs(MaturityMean - MaturityTarget),
%% 3. Low developmental noise
Noise = maps:get(developmental_noise, Sensors, 0.5),
NoiseOptimality = 1.0 - Noise,
%% 4. High canalization (robustness)
Canalization = maps:get(canalization_strength, Sensors, 0.5),
%% 5. Moderate heterochrony
Heterochrony = maps:get(heterochrony_index, Sensors, 0.5),
HeterochronyOptimality = 1.0 - abs(Heterochrony - 0.3) * 2,
%% Combined reward
Reward = 0.20 * PlasticityOptimality +
0.25 * MaturityProgress +
0.20 * NoiseOptimality +
0.20 * Canalization +
0.15 * HeterochronyOptimality,
lc_silo_behavior:clamp(Reward, 0.0, 1.0).
handle_cross_silo_signals(Signals, State) ->
CurrentSignals = State#state.incoming_signals,
UpdatedSignals = maps:merge(CurrentSignals, Signals),
{ok, State#state{incoming_signals = UpdatedSignals}}.
emit_cross_silo_signals(State) ->
Sensors = collect_sensors(State),
%% Maturity distribution
MaturityMean = maps:get(maturation_level_mean, Sensors, 0.5),
MaturityDistribution = MaturityMean,
%% Plasticity available
PlasticityMean = maps:get(plasticity_level_mean, Sensors, 0.5),
PlasticityAvailable = PlasticityMean,
%% Metamorphosis rate
MetamorphosisRate = maps:get(metamorphosis_rate, Sensors, 0.0),
%% Expression stage: based on developmental stage
StageMean = maps:get(developmental_stage_mean, Sensors, 0.5),
ExpressionStage = StageMean,
%% Emit signals
emit_signal(task, maturity_distribution, MaturityDistribution),
emit_signal(cultural, plasticity_available, PlasticityAvailable),
emit_signal(ecological, metamorphosis_rate, MetamorphosisRate),
emit_signal(regulatory, expression_stage, ExpressionStage),
ok.
%%% ============================================================================
%%% gen_server Callbacks
%%% ============================================================================
init(Config) ->
Realm = maps:get(realm, Config, <<"default">>),
EnabledLevels = maps:get(enabled_levels, Config, [l0, l1]),
L0TweannEnabled = maps:get(l0_tweann_enabled, Config, false),
L2Enabled = maps:get(l2_enabled, Config, false),
%% Create ETS tables
EtsTables = lc_ets_utils:create_tables(developmental, Realm, [
{developmental_states, [{keypos, 1}]},
{critical_periods, [{keypos, 1}]},
{milestones, [{keypos, 1}]}
]),
State = #state{
realm = Realm,
enabled_levels = EnabledLevels,
l0_tweann_enabled = L0TweannEnabled,
l2_enabled = L2Enabled,
current_params = ?DEFAULT_PARAMS,
ets_tables = EtsTables,
maturity_history = [],
plasticity_history = [],
metamorphosis_count = 0,
incoming_signals = #{},
prev_maturity_distribution = 0.5,
prev_plasticity_available = 0.5
},
%% Schedule periodic cross-silo signal update
erlang:send_after(1000, self(), update_signals),
{ok, State}.
handle_call(get_params, _From, State) ->
{reply, State#state.current_params, State};
handle_call({get_developmental_state, IndividualId}, _From, State) ->
StatesTable = maps:get(developmental_states, State#state.ets_tables),
Result = lc_ets_utils:lookup(StatesTable, IndividualId),
{reply, Result, State};
handle_call(get_developmental_stats, _From, State) ->
StatesTable = maps:get(developmental_states, State#state.ets_tables),
PeriodsTable = maps:get(critical_periods, State#state.ets_tables),
Stats = #{
individual_count => lc_ets_utils:count(StatesTable),
active_critical_periods => lc_ets_utils:count(PeriodsTable),
metamorphosis_count => State#state.metamorphosis_count
},
{reply, Stats, State};
handle_call(get_state, _From, State) ->
StateMap = #{
realm => State#state.realm,
enabled_levels => State#state.enabled_levels,
current_params => State#state.current_params,
metamorphosis_count => State#state.metamorphosis_count,
sensors => collect_sensors(State)
},
{reply, StateMap, State};
handle_call(reset, _From, State) ->
%% Clear all ETS tables
maps:foreach(
fun(_Name, Table) -> ets:delete_all_objects(Table) end,
State#state.ets_tables
),
NewState = State#state{
current_params = ?DEFAULT_PARAMS,
maturity_history = [],
plasticity_history = [],
metamorphosis_count = 0,
incoming_signals = #{},
prev_maturity_distribution = 0.5,
prev_plasticity_available = 0.5
},
{reply, ok, NewState};
handle_call(_Request, _From, State) ->
{reply, {error, unknown_request}, State}.
handle_cast({update_developmental_state, IndividualId, Stage, Plasticity}, State) ->
StatesTable = maps:get(developmental_states, State#state.ets_tables),
Params = State#state.current_params,
%% Get or create state
Age = get_current_age(StatesTable, IndividualId),
Maturity = compute_maturity(Stage, Age, Params),
lc_ets_utils:insert(StatesTable, IndividualId, #{
stage => Stage,
plasticity => Plasticity,
age => Age + 1,
maturity => Maturity
}),
%% Update histories with numeric values
NewMaturityHistory = truncate_history(
[Maturity | State#state.maturity_history],
?HISTORY_SIZE
),
NewPlasticityHistory = truncate_history(
[Plasticity | State#state.plasticity_history],
?HISTORY_SIZE
),
NewState = State#state{
maturity_history = NewMaturityHistory,
plasticity_history = NewPlasticityHistory
},
{noreply, NewState};
handle_cast({trigger_metamorphosis, IndividualId}, State) ->
StatesTable = maps:get(developmental_states, State#state.ets_tables),
MilestonesTable = maps:get(milestones, State#state.ets_tables),
Params = State#state.current_params,
%% Record metamorphosis
case lc_ets_utils:lookup(StatesTable, IndividualId) of
{ok, CurrentState} ->
Severity = maps:get(metamorphosis_severity, Params, 0.5),
OldStage = maps:get(stage, CurrentState, juvenile),
OldStageNum = stage_to_number(OldStage),
NewStageNum = min(1.0, OldStageNum + Severity * 0.5),
NewPlasticity = maps:get(initial_plasticity, Params, 0.9),
lc_ets_utils:insert(StatesTable, IndividualId, #{
stage => NewStageNum,
plasticity => NewPlasticity,
age => maps:get(age, CurrentState, 0) + 1,
maturity => NewStageNum
}),
%% Record milestone
MilestoneId = erlang:unique_integer([positive]),
lc_ets_utils:insert(MilestonesTable, MilestoneId, #{
individual => IndividualId,
type => metamorphosis,
stage_before => OldStageNum,
stage_after => NewStageNum
});
not_found ->
ok
end,
NewState = State#state{
metamorphosis_count = State#state.metamorphosis_count + 1
},
{noreply, NewState};
handle_cast({open_critical_period, IndividualId, PeriodType}, State) ->
PeriodsTable = maps:get(critical_periods, State#state.ets_tables),
lc_ets_utils:insert(PeriodsTable, IndividualId, #{
period_type => PeriodType,
opened_at => erlang:system_time(millisecond),
closed_at => undefined,
active => true
}),
{noreply, State};
handle_cast({close_critical_period, IndividualId}, State) ->
PeriodsTable = maps:get(critical_periods, State#state.ets_tables),
case lc_ets_utils:lookup(PeriodsTable, IndividualId) of
{ok, PeriodData} ->
lc_ets_utils:insert(PeriodsTable, IndividualId, PeriodData#{
closed_at => erlang:system_time(millisecond),
active => false
});
not_found ->
ok
end,
{noreply, State};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(update_signals, State) ->
%% Fetch incoming signals from cross-silo coordinator
NewSignals = fetch_incoming_signals(),
UpdatedState = State#state{
incoming_signals = maps:merge(State#state.incoming_signals, NewSignals)
},
%% Emit outgoing signals
emit_cross_silo_signals(UpdatedState),
%% Reschedule
erlang:send_after(1000, self(), update_signals),
{noreply, UpdatedState};
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, State) ->
lc_ets_utils:delete_tables(State#state.ets_tables),
ok.
%%% ============================================================================
%%% Internal Functions - Developmental Metrics
%%% ============================================================================
get_current_age(StatesTable, IndividualId) ->
case lc_ets_utils:lookup(StatesTable, IndividualId) of
{ok, Data} -> maps:get(age, Data, 0);
not_found -> 0
end.
compute_maturity(Stage, Age, Params) ->
MaturationSpeed = maps:get(maturation_speed, Params, 0.02),
BaseMaturity = stage_to_number(Stage),
AgeContribution = min(1.0, Age * MaturationSpeed),
min(1.0, BaseMaturity * 0.7 + AgeContribution * 0.3).
%% Convert developmental stage atoms to numerical values
stage_to_number(embryonic) -> 0.1;
stage_to_number(larval) -> 0.2;
stage_to_number(juvenile) -> 0.5;
stage_to_number(adolescent) -> 0.7;
stage_to_number(adult) -> 0.9;
stage_to_number(mature) -> 1.0;
stage_to_number(N) when is_number(N) -> N;
stage_to_number(_) -> 0.5.
compute_state_means(AllStates) ->
compute_means_from_states(AllStates).
compute_means_from_states([]) -> {0.5, 0.5, 0.5};
compute_means_from_states(States) ->
Stages = [stage_to_number(maps:get(stage, Data, 0.5)) || {_Id, Data, _Ts} <- States],
Maturities = [maps:get(maturity, Data, 0.5) || {_Id, Data, _Ts} <- States],
Plasticities = [maps:get(plasticity, Data, 0.5) || {_Id, Data, _Ts} <- States],
{safe_mean(Stages), safe_mean(Maturities), safe_mean(Plasticities)}.
compute_active_period_ratio(AllPeriods) ->
compute_ratio_from_periods(AllPeriods).
compute_ratio_from_periods([]) -> 0.0;
compute_ratio_from_periods(Periods) ->
ActiveCount = length([P || {_Id, P, _Ts} <- Periods, maps:get(active, P, false)]),
ActiveCount / length(Periods).
compute_canalization(AllStates) ->
%% Canalization = resistance to perturbation (low variance in outcomes)
compute_canalization_from_states(AllStates).
compute_canalization_from_states([]) -> 0.5;
compute_canalization_from_states(States) ->
Stages = [maps:get(stage, Data, 0.5) || {_Id, Data, _Ts} <- States],
Variance = compute_variance(Stages),
1.0 - lc_silo_behavior:normalize(Variance, 0, 0.25).
compute_heterochrony(AllStates) ->
%% Heterochrony = variance in developmental timing
compute_heterochrony_from_states(AllStates).
compute_heterochrony_from_states([]) -> 0.0;
compute_heterochrony_from_states(States) ->
Ages = [maps:get(age, Data, 0) || {_Id, Data, _Ts} <- States],
Stages = [maps:get(stage, Data, 0.5) || {_Id, Data, _Ts} <- States],
%% Compute variance in stage/age ratios
Ratios = [safe_ratio(S, max(1, A)) || {S, A} <- lists:zip(Stages, Ages)],
Variance = compute_variance(Ratios),
lc_silo_behavior:normalize(Variance, 0, 0.5).
compute_developmental_noise(AllStates) ->
%% Noise = random variation in development
compute_noise_from_states(AllStates).
compute_noise_from_states([]) -> 0.0;
compute_noise_from_states(States) ->
Plasticities = [maps:get(plasticity, Data, 0.5) || {_Id, Data, _Ts} <- States],
Variance = compute_variance(Plasticities),
lc_silo_behavior:normalize(Variance, 0, 0.25).
safe_mean([]) -> 0.0;
safe_mean(Values) -> lists:sum(Values) / length(Values).
compute_variance([]) -> 0.0;
compute_variance([_]) -> 0.0;
compute_variance(Values) ->
Mean = safe_mean(Values),
SumSquares = lists:foldl(
fun(V, Acc) -> Acc + (V - Mean) * (V - Mean) end,
0.0,
Values
),
SumSquares / length(Values).
compute_trend([]) -> 0.5;
compute_trend([_]) -> 0.5;
compute_trend(Values) when length(Values) < 3 -> 0.5;
compute_trend(Values) ->
Recent = lists:sublist(Values, 5),
Older = lists:sublist(Values, 6, 5),
RecentMean = safe_mean(Recent),
OlderMean = safe_mean(Older),
Trend = safe_ratio(RecentMean - OlderMean, OlderMean + 0.001),
lc_silo_behavior:normalize(Trend, -0.5, 0.5).
safe_ratio(_Num, Denom) when Denom == 0.0; Denom == 0 -> 0.0;
safe_ratio(Num, Denom) -> Num / Denom.
%%% ============================================================================
%%% Internal Functions - History Management
%%% ============================================================================
truncate_history(List, MaxSize) when length(List) > MaxSize ->
lists:sublist(List, MaxSize);
truncate_history(List, _MaxSize) ->
List.
%%% ============================================================================
%%% Internal Functions - Cross-Silo
%%% ============================================================================
emit_signal(_ToSilo, SignalName, Value) ->
%% Event-driven: publish signal, lc_cross_silo routes to valid destinations
silo_events:publish_signal(developmental, SignalName, Value).
fetch_incoming_signals() ->
case whereis(lc_cross_silo) of
undefined -> #{};
_Pid -> lc_cross_silo:get_signals_for(developmental)
end.
%%% ============================================================================
%%% Internal Functions - Bounds
%%% ============================================================================
apply_bounds(Params, Bounds) ->
maps:fold(
fun(Key, Value, Acc) ->
BoundedValue = apply_single_bound(Key, Value, Bounds),
maps:put(Key, BoundedValue, Acc)
end,
#{},
Params
).
apply_single_bound(Key, Value, Bounds) ->
case maps:get(Key, Bounds, undefined) of
undefined -> Value;
{Min, Max} -> lc_silo_behavior:clamp(Value, Min, Max)
end.