Current section
Files
Jump to
Current section
Files
include/meta_controller.hrl
%% @doc Meta-Controller record definitions for LTC-based hyperparameter optimization
%%
%% This module implements a meta-learning system that uses Liquid Time-Constant
%% (LTC) networks to dynamically control neuroevolution hyperparameters.
%%
%% The meta-controller operates at a higher timescale than the task networks,
%% learning to "optimize the optimizer" by adapting parameters based on
%% training dynamics.
%%
%% @author Macula.io
%% @copyright 2025 Macula.io
-ifndef(META_CONTROLLER_HRL).
-define(META_CONTROLLER_HRL, true).
%%% ============================================================================
%%% Types
%%% ============================================================================
-type meta_param() :: mutation_rate | mutation_strength | selection_ratio |
%% Resource-aware parameters
evaluations_per_individual | max_concurrent_evaluations |
population_size |
%% Topology control parameters
add_node_rate | add_connection_rate | complexity_penalty.
-type reward_component() :: convergence_speed | final_fitness | efficiency_ratio |
diversity_aware | normative_structure.
%%% ============================================================================
%%% Configuration Record
%%% ============================================================================
%% @doc Configuration for the meta-controller.
%%
%% Controls the LTC network architecture and reward signal composition.
%%
%% Inputs (11 total):
%% 1-8: Evolution metrics (fitness, improvement, variance, stagnation, progress, trend, diversity, entropy)
%% 9-11: Resource metrics (memory_pressure, cpu_pressure, process_pressure)
%%
%% Outputs (5 base, more if topology/population control enabled):
%% 1-3: Base evolution params (mutation_rate, mutation_strength, selection_ratio)
%% 4-5: Resource-aware params (evaluations_per_individual, max_concurrent_evaluations)
%% 6-8: Topology params if control_topology=true (add_node_rate, add_connection_rate, complexity_penalty)
%% 9: Population size if control_population_size=true
-record(meta_config, {
%% Network topology: {Inputs, HiddenLayers, Outputs}
%% Default: {11, [24, 16, 8], 5} - 11 inputs (8 evolution + 3 resource), 5 outputs (3 base + 2 resource)
network_topology = {11, [24, 16, 8], 5} :: {pos_integer(), [pos_integer()], pos_integer()},
%% LTC neuron type: cfc (fast) or ltc (accurate ODE)
neuron_type = cfc :: ltc | cfc,
%% Base time constant for meta-controller neurons
%% Higher values = slower adaptation = more stable
time_constant = 50.0 :: float(),
%% State bound for LTC neurons
state_bound = 1.0 :: float(),
%% Reward component weights (must sum to ~1.0)
reward_weights = #{
convergence_speed => 0.25,
final_fitness => 0.25,
efficiency_ratio => 0.20,
diversity_aware => 0.15,
normative_structure => 0.15
} :: #{reward_component() => float()},
%% Learning rate for gradient-based meta-training
learning_rate = 0.001 :: float(),
%% Parameter bounds: {ParamName, {Min, Max}}
param_bounds = #{
%% Base evolution parameters
mutation_rate => {0.01, 0.5},
mutation_strength => {0.05, 1.0},
selection_ratio => {0.10, 0.50},
%% Resource-aware parameters (always controlled)
evaluations_per_individual => {1.0, 20.0}, %% 1 = emergency low-memory mode
max_concurrent_evaluations => {1.0, 50.0}, %% Limits parallelism under load
%% Population control (when control_population_size=true)
population_size => {10.0, 200.0},
%% Topology control bounds (NEAT mode)
add_node_rate => {0.0, 0.10},
add_connection_rate => {0.0, 0.20},
complexity_penalty => {0.0, 0.5}
} :: #{meta_param() => {float(), float()}},
%% Whether to include population_size as a controllable parameter
%% (disabled by default as it affects computation more than other params)
control_population_size = false :: boolean(),
%% Whether to control topology mutation rates (NEAT mode)
%% When true, adds 3 outputs: add_node_rate, add_connection_rate, complexity_penalty
%% Requires additional inputs for topology metrics (complexity, diversity)
control_topology = false :: boolean(),
%% History window size for computing reward signals
history_window = 10 :: pos_integer(),
%% Momentum for parameter updates (smooths changes)
momentum = 0.9 :: float()
}).
-type meta_config() :: #meta_config{}.
%%% ============================================================================
%%% State Record
%%% ============================================================================
%% @doc Internal state for the meta-controller gen_server.
-record(meta_state, {
%% Server identifier
id :: term(),
%% Configuration
config :: meta_config(),
%% Current hyperparameter values
current_params = #{
mutation_rate => 0.10,
mutation_strength => 0.30,
selection_ratio => 0.20
} :: #{meta_param() => float()},
%% LTC network internal states (one per hidden neuron)
%% Maps neuron_id => internal_state
ltc_states = #{} :: #{term() => float()},
%% LTC network weights
%% Structure: #{layer => #{neuron_id => [{input_id, weight}]}}
ltc_weights = #{} :: map(),
%% Training metrics history (circular buffer of last N generations)
metrics_history = [] :: [generation_metrics()],
%% Parameter change momentum (for smooth updates)
param_momentum = #{} :: #{meta_param() => float()},
%% Generation counter
generation = 0 :: non_neg_integer(),
%% Cumulative reward for current training run
cumulative_reward = 0.0 :: float(),
%% Best fitness ever seen (for normalization)
best_fitness_ever = 0.0 :: float(),
%% Stagnation counter (generations without improvement)
stagnation_count = 0 :: non_neg_integer(),
%% Running state
running = false :: boolean()
}).
-type meta_state() :: #meta_state{}.
%%% ============================================================================
%%% Generation Metrics Record
%%% ============================================================================
%% @doc Metrics captured from a single generation for meta-learning.
-record(generation_metrics, {
%% Generation number
generation :: pos_integer(),
%% Fitness metrics
best_fitness :: float(),
avg_fitness :: float(),
worst_fitness :: float(),
fitness_std_dev :: float(),
%% Improvement metrics
fitness_delta :: float(), %% Change from previous generation
relative_improvement :: float(), %% Percentage improvement
%% Diversity metrics
population_diversity :: float(), %% Variance in fitness
strategy_entropy :: float(), %% Information content of strategies
%% Efficiency metrics
evaluations_used :: pos_integer(),
fitness_per_evaluation :: float(),
%% Normative structure metrics
diversity_corridors :: float(), %% Cluster distinctness
adaptation_readiness :: float(), %% Variance in adjacent traits
%% Current parameters used
params_used :: #{meta_param() => float()},
%% Timestamp
timestamp :: erlang:timestamp()
}).
-type generation_metrics() :: #generation_metrics{}.
%%% ============================================================================
%%% Reward Signal Record
%%% ============================================================================
%% @doc Computed reward signal for meta-controller training.
-record(meta_reward, {
%% Individual reward components
convergence_speed :: float(),
final_fitness :: float(),
efficiency_ratio :: float(),
diversity_aware :: float(),
normative_structure :: float(),
%% Composite reward (weighted sum)
total :: float(),
%% Generation this reward is for
generation :: pos_integer()
}).
-type meta_reward() :: #meta_reward{}.
%%% ============================================================================
%%% Network Structure Records
%%% ============================================================================
%% @doc LTC neuron state for the meta-controller network.
-record(meta_neuron, {
%% Neuron identifier: {layer, index}
id :: {pos_integer(), pos_integer()},
%% Internal state x(t)
internal_state = 0.0 :: float(),
%% Base time constant
time_constant :: float(),
%% State bound
state_bound :: float(),
%% Input weights: [{source_id, weight}]
input_weights = [] :: [{term(), float()}],
%% Bias
bias = 0.0 :: float(),
%% Backbone weights for CfC f() function
backbone_weights = [] :: [float()],
%% Head weights for CfC h() function
head_weights = [] :: [float()]
}).
-type meta_neuron() :: #meta_neuron{}.
%% @doc Output mapping for meta-controller.
%% Maps network outputs to parameter adjustments.
-record(output_mapping, {
%% Output index -> parameter name
index_to_param :: #{non_neg_integer() => meta_param()},
%% Parameter name -> output index
param_to_index :: #{meta_param() => non_neg_integer()},
%% Activation functions for each output (e.g., sigmoid for bounded params)
output_activations :: #{non_neg_integer() => atom()}
}).
-type output_mapping() :: #output_mapping{}.
%%% ============================================================================
%%% Training Event Record
%%% ============================================================================
%% @doc Training event for gradient-based meta-learning.
-record(meta_training_event, {
%% Generation
generation :: pos_integer(),
%% Input features (normalized metrics)
inputs :: [float()],
%% Output parameters (raw network outputs before scaling)
outputs :: [float()],
%% Reward received
reward :: float(),
%% Gradient estimate for this step
gradients :: #{term() => float()}
}).
-type meta_training_event() :: #meta_training_event{}.
%%% ============================================================================
%%% Structure Metrics Record (Normative Awareness)
%%% ============================================================================
%% @doc Structure metrics for normative awareness reward component.
%%
%% These metrics capture whether the population maintains the capacity
%% for future adaptation, not just current fitness.
-record(structure_metrics, {
%% Number of distinct strategy clusters in population
diversity_corridors = 0.0 :: float(),
%% Variance in traits that are adjacent to fitness (exploration potential)
adaptation_readiness = 0.0 :: float(),
%% Estimated distance to unexplored fitness regions
breakthrough_potential = 0.0 :: float(),
%% Shannon entropy of strategy distribution
strategy_entropy = 0.0 :: float()
}).
-type structure_metrics() :: #structure_metrics{}.
%%% ============================================================================
%%% L2 Guidance Record (L2→L1 Interface)
%%% ============================================================================
%% @doc L2 guidance output that controls L1 (task_silo) behavior.
%%
%% Instead of L2 directly outputting hyperparameters (mutation_rate, etc.),
%% it outputs meta-parameters that control HOW L1 adjusts hyperparameters.
%% This creates a proper hierarchical control system:
%%
%% L2 (Strategic) → L1 Guidance → L1 (Tactical) → Hyperparams → L0 (Bounds)
%%
%% Example:
%% - L2 outputs aggression_factor = 1.5
%% - L1 uses this: AdjustedMR = BaseMR * (1 + NetFactor * 1.5)
%% - Result: mutation_rate can increase by up to 150% instead of 50%
-record(l2_guidance, {
%% =========================================================================
%% Task Silo L1 Hyperparameters
%% =========================================================================
%% How aggressive L1 adjustments should be
%% Higher = stronger response to stagnation
%% Range: [0.0, 2.0], Default: 0.5
aggression_factor = 0.5 :: float(),
%% How fast exploration_boost increases per stagnating generation
%% Higher = faster escalation when stuck
%% Range: [0.05, 0.5], Default: 0.1
exploration_step = 0.1 :: float(),
%% Threshold for what counts as "improvement" (below = stagnating)
%% Lower = more sensitive to small gains
%% Range: [0.0001, 0.01], Default: 0.001
stagnation_sensitivity = 0.001 :: float(),
%% How much to boost topology mutations when heavily stagnating
%% Higher = more aggressive structural changes
%% Range: [1.0, 3.0], Default: 1.5
topology_aggression = 1.5 :: float(),
%% Weight for exploitation vs exploration in NetFactor calculation
%% Higher = more exploitation when improving
%% Range: [0.2, 0.8], Default: 0.5
exploitation_weight = 0.5 :: float(),
%% Momentum for smoothing exploration/exploitation boost updates
%% Higher = smoother transitions, less oscillation
%% Range: [0.0, 0.95], Default: 0.7
adaptation_momentum = 0.7 :: float(),
%% Intervention severity thresholds (stagnation severity -> action level)
%% warning_threshold: When to emit warning events
%% Range: [0.2, 0.5], Default: 0.3
warning_threshold = 0.3 :: float(),
%% intervention_threshold: When to actively intervene
%% Range: [0.4, 0.8], Default: 0.6
intervention_threshold = 0.6 :: float(),
%% critical_threshold: When to apply maximum intervention
%% Range: [0.7, 0.99], Default: 0.9
critical_threshold = 0.9 :: float(),
%% Velocity window size for stagnation detection
%% Higher = more stable but slower reaction
%% Range: [5, 30], Default: 10
velocity_window_size = 10 :: pos_integer(),
%% =========================================================================
%% Resource Silo L1 Hyperparameters
%% =========================================================================
%% Memory pressure threshold for throttling
%% Range: [0.5, 0.85], Default: 0.7
memory_high_threshold = 0.7 :: float(),
%% Memory pressure threshold for pausing evolution
%% Range: [0.8, 0.98], Default: 0.9
memory_critical_threshold = 0.9 :: float(),
%% CPU pressure threshold for throttling
%% Range: [0.6, 0.95], Default: 0.9
cpu_high_threshold = 0.9 :: float(),
%% Pressure scale factor for concurrency reduction
%% Range: [0.5, 0.99], Default: 0.9
pressure_scale_factor = 0.9 :: float(),
%% Minimum concurrency scale (floor)
%% Range: [0.05, 0.3], Default: 0.1
min_scale_factor = 0.1 :: float(),
%% Threshold for detecting significant pressure changes
%% Range: [0.01, 0.2], Default: 0.05
pressure_change_threshold = 0.05 :: float(),
%% =========================================================================
%% Metadata
%% =========================================================================
%% Generation this guidance was computed for
generation = 0 :: non_neg_integer()
}).
-type l2_guidance() :: #l2_guidance{}.
%% @doc Parameter bounds for L2 guidance outputs.
-define(L2_GUIDANCE_BOUNDS, #{
%% Task Silo L1 hyperparameters
aggression_factor => {0.0, 2.0},
exploration_step => {0.05, 0.5},
stagnation_sensitivity => {0.0001, 0.01},
topology_aggression => {1.0, 3.0},
exploitation_weight => {0.2, 0.8},
adaptation_momentum => {0.0, 0.95},
warning_threshold => {0.2, 0.5},
intervention_threshold => {0.4, 0.8},
critical_threshold => {0.7, 0.99},
velocity_window_size => {5, 30},
%% Resource Silo L1 hyperparameters
memory_high_threshold => {0.5, 0.85},
memory_critical_threshold => {0.8, 0.98},
cpu_high_threshold => {0.6, 0.95},
pressure_scale_factor => {0.5, 0.99},
min_scale_factor => {0.05, 0.3},
pressure_change_threshold => {0.01, 0.2}
}).
%% @doc Default L2 guidance (used when L2 is disabled).
%% NOTE: These defaults are AGGRESSIVE to ensure responsiveness when L2 learning
%% is not yet active. Once L0 TWEANNs are wired in, L2 can learn optimal values.
-define(L2_GUIDANCE_DEFAULTS, #l2_guidance{
%% Task Silo L1 hyperparameters
%% AGGRESSIVE defaults for rule-based L1 (before TWEANN evolution is wired in)
aggression_factor = 1.5, %% Was 0.5: now 3x stronger response to stagnation
exploration_step = 0.5, %% Was 0.1: now 5x faster exploration boost ramp
stagnation_sensitivity = 0.001, %% Keep sensitive to stagnation
topology_aggression = 2.5, %% Was 1.5: more aggressive structural mutations
exploitation_weight = 0.3, %% Was 0.5: less exploitation, more exploration
adaptation_momentum = 0.3, %% Was 0.7: faster response (30% old, 70% new)
warning_threshold = 0.2, %% Was 0.3: warn earlier
intervention_threshold = 0.4, %% Was 0.6: intervene earlier
critical_threshold = 0.7, %% Was 0.9: critical earlier
velocity_window_size = 5, %% Was 10: shorter window = faster reaction
%% Resource Silo L1 hyperparameters
memory_high_threshold = 0.7,
memory_critical_threshold = 0.9,
cpu_high_threshold = 0.9,
pressure_scale_factor = 0.9,
min_scale_factor = 0.1,
pressure_change_threshold = 0.05,
%% Metadata
generation = 0
}).
%%% ============================================================================
%%% Velocity-Based Stagnation State
%%% ============================================================================
%% @doc State for velocity-based stagnation detection.
%%
%% Tracks improvement velocity (fitness gain per 1000 evaluations) instead of
%% counting generations without improvement. This enables:
%%
%% 1. Reaction to slowdown BEFORE full plateau
%% 2. Continuous severity scale (0.0-1.0) instead of binary trigger
%% 3. Compatibility with variable batch sizes and steady-state evolution
%%
%% Algorithm:
%% velocity = (current_fitness - prev_fitness) / (current_evals - prev_evals) * 1000
%% avg_velocity = rolling_average(improvement_window)
%% stagnation_severity = clamp((threshold - avg_velocity) / threshold, 0.0, 1.0)
%%
%% Reference: Evaluation-centric evolution architecture plan.
-record(velocity_state, {
%% Rolling window of improvement velocities
improvement_window = [] :: [float()],
%% Number of checkpoints to track in window
window_size = 10 :: pos_integer(),
%% Current improvement velocity (fitness gain per 1000 evaluations)
current_velocity = 0.0 :: float(),
%% Velocity threshold: below this = stagnating
%% Default: 0.001 = 0.1% fitness improvement per 1000 evaluations
velocity_threshold = 0.001 :: float(),
%% Fitness checkpoints: [{total_evaluations, best_fitness}]
%% Used to compute velocity between checkpoints
fitness_checkpoints = [] :: [{non_neg_integer(), float()}],
%% Stagnation severity: 0.0 = healthy, 1.0 = critical
%% Continuous scale enables proportional response
stagnation_severity = 0.0 :: float(),
%% Total evaluations at last update
last_total_evaluations = 0 :: non_neg_integer()
}).
-type velocity_state() :: #velocity_state{}.
%% @doc Default velocity state initializer.
-define(VELOCITY_STATE_DEFAULTS, #velocity_state{
improvement_window = [],
window_size = 10,
current_velocity = 0.0,
velocity_threshold = 0.001,
fitness_checkpoints = [],
stagnation_severity = 0.0,
last_total_evaluations = 0
}).
-endif. %% META_CONTROLLER_HRL