Creating Custom Morphologies
View SourceThis guide explains how to create custom morphologies for macula-tweann v0.9.0+.
What is a Morphology?
A morphology defines the sensory inputs and motor outputs for a neural network problem domain. It specifies:
- What sensors the network can use to perceive its environment
- What actuators the network can use to affect its environment
- The vector length (number of values) each sensor/actuator provides
Quick Start
1. Create a Module
Create a new Erlang module that implements the morphology_behaviour:
-module(my_morphology).
-behaviour(morphology_behaviour).
-include_lib("macula_tweann/include/records.hrl").
-export([get_sensors/1, get_actuators/1]).
%% Required callbacks
get_sensors(my_problem) ->
[#sensor{
name = my_sensor,
type = standard,
scape = {private, my_scape},
vl = 3, % 3 input values
parameters = [],
fanout_ids = [],
generation = 0
}];
get_sensors(_) ->
error(invalid_morphology).
get_actuators(my_problem) ->
[#actuator{
name = my_actuator,
type = standard,
scape = {private, my_scape},
vl = 2, % 2 output values
parameters = [],
fanin_ids = [],
generation = 0
}];
get_actuators(_) ->
error(invalid_morphology).2. Register Your Morphology
Before using your morphology, register it with the morphology registry:
ok = morphology_registry:register(my_problem, my_morphology).3. Use It
Now you can create agents with your custom morphology:
Constraint = #constraint{
morphology = my_problem,
neural_afs = [tanh],
neural_pfns = [none],
neural_aggr_fs = [dot_product]
},
AgentId = genotype:construct_Agent(SpecieId, AgentId, Constraint).Morphology Behaviour Callbacks
get_sensors/1
Returns a list of #sensor{} records for the given morphology name.
Sensor Record Fields:
name(atom) - Unique identifier for this sensortype(standard | ...) - Sensor typescape({private|public, atom()}) - Scape this sensor reads fromvl(integer) - Vector length (number of input values)parameters(list) - Configuration parametersfanout_ids([id()]) - Leave empty, set by genotypegeneration(integer) - Set to 0cx_id(id()) - Leave undefined, set by genotypeid(id()) - Leave undefined, set by genotype
get_actuators/1
Returns a list of #actuator{} records for the given morphology name.
Actuator Record Fields:
name(atom) - Unique identifier for this actuatortype(standard | ...) - Actuator typescape({private|public, atom()}) - Scape this actuator writes tovl(integer) - Vector length (number of output values)parameters(list) - Configuration parametersfanin_ids([id()]) - Leave empty, set by genotypegeneration(integer) - Set to 0cx_id(id()) - Leave undefined, set by genotypeid(id()) - Leave undefined, set by genotype
Examples
Example 1: Simple Classification
A morphology for binary classification with 10 features:
-module(morphology_classifier).
-behaviour(morphology_behaviour).
-include_lib("macula_tweann/include/records.hrl").
-export([get_sensors/1, get_actuators/1]).
get_sensors(binary_classifier) ->
[#sensor{
name = classifier_input,
type = standard,
scape = {private, classifier_sim},
vl = 10, % 10 feature inputs
parameters = [],
fanout_ids = [],
generation = 0
}];
get_sensors(_) -> error(invalid_morphology).
get_actuators(binary_classifier) ->
[#actuator{
name = classifier_output,
type = standard,
scape = {private, classifier_sim},
vl = 1, % Single binary output
parameters = [],
fanin_ids = [],
generation = 0
}];
get_actuators(_) -> error(invalid_morphology).Example 2: Multi-Sensor Morphology
A morphology with multiple sensor types:
-module(morphology_robot).
-behaviour(morphology_behaviour).
-include_lib("macula_tweann/include/records.hrl").
-export([get_sensors/1, get_actuators/1]).
get_sensors(robot) ->
[
% Distance sensors
#sensor{
name = distance_sensors,
type = standard,
scape = {private, robot_sim},
vl = 5,
parameters = [],
fanout_ids = [],
generation = 0
},
% Camera input
#sensor{
name = camera,
type = standard,
scape = {private, robot_sim},
vl = 100, % 10x10 pixel grid
parameters = [10, 10],
fanout_ids = [],
generation = 0
},
% Battery level
#sensor{
name = battery,
type = standard,
scape = {private, robot_sim},
vl = 1,
parameters = [],
fanout_ids = [],
generation = 0
}
];
get_sensors(_) -> error(invalid_morphology).
get_actuators(robot) ->
[
% Motor control
#actuator{
name = motors,
type = standard,
scape = {private, robot_sim},
vl = 2, % Left and right motor speeds
parameters = [],
fanin_ids = [],
generation = 0
}
];
get_actuators(_) -> error(invalid_morphology).Example 3: Supporting Multiple Morphologies
One module can support multiple related morphologies:
-module(morphology_game).
-behaviour(morphology_behaviour).
-include_lib("macula_tweann/include/records.hrl").
-export([get_sensors/1, get_actuators/1]).
%% Player morphology
get_sensors(player) ->
[#sensor{
name = game_state,
type = standard,
scape = {private, game_sim},
vl = 20,
parameters = [],
fanout_ids = [],
generation = 0
}];
%% Enemy morphology
get_sensors(enemy) ->
[#sensor{
name = game_state,
type = standard,
scape = {private, game_sim},
vl = 15, % Different perception
parameters = [],
fanout_ids = [],
generation = 0
}];
get_sensors(_) -> error(invalid_morphology).
%% Same actuators for both
get_actuators(Morphology) when Morphology =:= player; Morphology =:= enemy ->
[#actuator{
name = game_action,
type = standard,
scape = {private, game_sim},
vl = 4, % Up, down, left, right
parameters = [],
fanin_ids = [],
generation = 0
}];
get_actuators(_) -> error(invalid_morphology).Then register both morphologies:
ok = morphology_registry:register(player, morphology_game),
ok = morphology_registry:register(enemy, morphology_game).Registry API
morphology_registry:register/2
Register a morphology implementation:
ok = morphology_registry:register(MorphologyName, ModuleName).Returns:
okon success{error, {module_not_loaded, Module}}if module doesn't exist{error, {missing_callback, Callback, Arity}}if behaviour not implemented
morphology_registry:unregister/1
Remove a morphology registration:
ok = morphology_registry:unregister(MorphologyName).morphology_registry:get/1
Get the implementing module for a morphology:
{ok, Module} = morphology_registry:get(MorphologyName).
{error, not_found} = morphology_registry:get(unknown).morphology_registry:list_all/0
List all registered morphologies:
Names = morphology_registry:list_all(). % [atom()]morphology_registry:is_registered/1
Check if a morphology is registered:
true = morphology_registry:is_registered(my_problem).
false = morphology_registry:is_registered(unknown).Scapes
Sensors and actuators connect to "scapes" - the environments they interact with.
Scape Format
{Visibility, ScapeName}Where:
VisibilityisprivateorpublicScapeNameis an atom identifying the scape module/process
Private Scapes
Private scapes are used for problem-specific simulations:
scape = {private, my_sim}The scape name should match your simulation module name.
Public Scapes
Public scapes are shared across multiple agents:
scape = {public, shared_environment}Application Lifecycle
In Your Application
% In your application start/2
start(_StartType, _StartArgs) ->
% Ensure macula_tweann is started
{ok, _} = application:ensure_all_started(macula_tweann),
% Register your morphologies
ok = morphology_registry:register(my_problem, my_morphology),
ok = morphology_registry:register(another_problem, another_morphology),
% Start your supervisor
my_sup:start_link().In Tests
setup() ->
application:ensure_all_started(macula_tweann),
morphology_registry:register(test_morph, test_morphology),
genotype:init_db().
teardown(_) ->
morphology_registry:unregister(test_morph),
genotype:reset_db().Example Morphologies
See examples/ directory for complete working examples:
examples/xor/- XOR function learningexamples/pole_balancing/- Cart-pole balancingexamples/forex/- Forex trading agentexamples/flatland/- Prey/predator simulation
To use example morphologies in your project:
% Register the examples you need
ok = morphology_registry:register(xor_mimic, morphology_xor),
ok = morphology_registry:register(pole_balancing, morphology_pole_balancing),
ok = morphology_registry:register(forex_trader, morphology_forex),
ok = morphology_registry:register(prey, morphology_flatland),
ok = morphology_registry:register(predator, morphology_flatland).Best Practices
- One Module Per Domain - Group related morphologies in one module
- Meaningful Names - Use descriptive sensor/actuator names
- Document VL - Comment why you chose specific vector lengths
- Error Handling - Always handle invalid morphology names
- Registration - Register morphologies at application startup
- Testing - Test your morphology with actual agent construction
Troubleshooting
Error: morphology_not_registered
You forgot to register your morphology:
ok = morphology_registry:register(my_problem, my_morphology).Error: missing_callback
Your module doesn't implement the behaviour correctly. Ensure:
-behaviour(morphology_behaviour).
-export([get_sensors/1, get_actuators/1]).Error: module_not_loaded
The module doesn't exist or isn't compiled. Check:
- Module file exists
- Module is in code path
- Module compiles without errors
Sensors/Actuators Not Found
Make sure your morphology name matches exactly:
% Registration
morphology_registry:register(my_problem, my_morphology)
% Usage - MUST MATCH
#constraint{morphology = my_problem, ...}
% Implementation - MUST MATCH
get_sensors(my_problem) -> ...Migration from Pre-v0.9.0
If you were using the old hardcoded morphology system:
Before (< v0.9.0):
% Morphologies were hardcoded in morphology.erl
Constraint = #constraint{morphology = xor_mimic, ...}After (>= v0.9.0):
% Register first
ok = morphology_registry:register(xor_mimic, morphology_xor),
% Then use
Constraint = #constraint{morphology = xor_mimic, ...}Version History
- v0.9.0 - Introduced morphology behaviour and registry system
- v0.8.x and earlier - Hardcoded morphologies in morphology.erl