./lablove
from the command line, the Lua file default.lua
, which must be in the same directory, is executed.
Here is a typical default.lua
:
default.lua:
experiment = "experiments/battle.lua"
expParam = getCommandLineParameter("exp");
if expParam ~= "" then experiment = expParam end
dofile(experiment)
The first line assigns the path to the experiment that we want to execute to variable experiment
. This way we can change the default experiment by just changing this definition. The next lines verify if an experiment path was provided through the command line. If it was, it overwrites experiment
. Finally, the experiment script is executed.
We encourage you not to alter any lines in this file except the first one. This way you keep the option of launching and experiment through command line parameters, for example:
./lablove -exp experiments/xpto.lua
will launch the experiment defined by the script xpto.lua
in directory experiments
. The LabLOVE distribution comes with several experiment definitions, contained in the experiments
directory. We suggest you to try them and observe the scripts.
experiments/battle.lua
file, which is included in the distribution.
In the battle experiment, the world is populated with two species of agents and food items. The two species are equal, except for their color. Agents are rewarded for their ability to shoot individuals from the other species. They can increase their chances of survival and have access to more energy for shooting by consuming food items.
The first part of the script consists of a sequence of value assignments to variables. This variables represent the several parameters of the experiment. By placing all these definitions at the beginning, we can easily tweak the experiment parameters latter:
-- Battle
-- Two species with antagonic goals co-evolve
--------------------------------------------------------------------------------
-- Experiment Parameters
--------------------------------------------------------------------------------
numberOfReds = 20
numberOfBlues = 20
numberOfPlants = 20
agentSize = 10.0
plantSize = 10.0
worldWidth = 1000
worldHeight = 1000
alphaComponents = {AND(), NOT(), SUM(), MUL(), INV(), NEG(), MOD(), AMP(), RAND(),
EQ(), GTZ(), ZERO(), MAX(), MIN(), AVG(), DMUL(), SEL(), MEM()}
betaComponents = {AND(), NOT(), SUM(), MUL(), INV(), NEG(), MOD(), AMP(), RAND(),
EQ(), GTZ(), ZERO(), CLK(), DMUL(), MEM(), TMEM()}
viewRange = 300.0
viewAngle = 350.0
maxAgeLow = 9500
maxAgeHigh = 10500
initialEnergy = 1.0
goCost = 0.001
rotateCost = 0.001
goForceScale = 0.3
rotateForceScale = 0.006
drag = 0.05
rotDrag = 0.05
fireInterval = 2000
laserLength = 25
laserSpeed = 100.0
laserRange = 300.0
laserStrengthFactor = 2.0
laserCostFactor = 0.1
laserHitDuration = 2
bufferSize = 100
fitnessAging = 0.5
addConnectionProb = 0.01
removeConnectionProb = 0.01
splitConnectionProb = 0.01
joinConnectionsProb = 0.01
changeInComponentProb = 0.2
recombineProb = 0.25
groupFactor = 0.5
timeLimit = 0
logTimeInterval = 100
logBrains = true
logOnlyLastBrain = true
evolutionStopTime = 0
agentBirthRadius = 0--100.0
keepBodyOnExpire = false
redRand = false
blueRand = false
humanAgent = false
In the next section of the code, we define command line parameters that can be used to override some of the values previously defined. This is useful for automating experimentation with several parameters, for example when using a cluster job submission system.
The first line executes a Lua script that defines auxiliary functions to parse command line parameters. For example, we have the getNumberParameter(name, currentValue, code)
function. This functions checks if there is a numeric parameter in the command line arguments with the name provided. If there is, it returns its value, otherwise it returns currentValue
. Notice that we pass the variables that we defined previously for the parameters. These function also append information to the parameterString
global variable.
In this section we also define the value for the logSuffix
variable, because it uses the parameterString
. The logSuffix
is used as a suffix for the names of log files generated by the experiment. This mechanism is useful to tag the log files with information about the parameter values used.
The line groupFactor = getNumberParameter("grp", groupFactor, "grp")
causes the grp
command line parameter to be valid, so if we execute the command: ./lablove -grp 0.8
, this causes the battle experiment to be launched with a group factor of 0.8. Also, the group factor value used is registered in the log file names with the string grp0.8
.
-- Command line, log file names, etc
--------------------------------------------------------------------------------
dofile("basic_command_line.lua")
groupFactor = getNumberParameter("grp", groupFactor, "grp")
redRand = getBoolParameter("redrand", redRand, "redrand")
blueRand = getBoolParameter("bluerand", blueRand, "bluerand")
logBaseName = "_battle_"
logSuffix = logBaseName
.. parameterString
.. "s"
.. seedIndex
The next step is to initialize the main simulation object. In this case we use Sim2D
, a 2D continuous simulation environment currently provided with LabLOVE.
Notice that the object:method()
notation is used to call methods on objects. We call methods to set the world dimensions, as well as the random number generator seed and the simulation time limit.
-- Simulation
--------------------------------------------------------------------------------
sim = Sim2D()
sim:setWorldDimensions(worldWidth, worldHeight, 250)
sim:setSeedIndex(seedIndex)
sim:setTimeLimit(timeLimit)
Next we define food network variables and symbols to represent the colors of the several species:
-- Food network
--------------------------------------------------------------------------------
blueFeed = 0
redFeed = 0
plantFood = 1
humanFeed = 0
-- Colors
--------------------------------------------------------------------------------
blueColor = SymbolRGB(0, 0, 255)
redColor = SymbolRGB(255, 0, 0)
plantColor = SymbolRGB(0, 255, 0)
Now it is time to initialize the base object of each species in the simulation. We start with plants, which are the food items. Plants are not considered agents, since they are passive objects without brains.
Again, initialization consists of crating an object of the appropriate type and calling its methods to parameterize it.
-- Plants
--------------------------------------------------------------------------------
plant = SimObj2D()
plant:setSize(plantSize)
plant:setInitialEnergy(1.0)
plant:setMaxAge(maxAgeHigh)
plant:setShape(SimObj2D.SHAPE_SQUARE)
plant:setLaserHitDuration(laserHitDuration)
plant:setColoringSymbolName("color")
plant:setTarget(false)
plantFoodSym = SymbolFloat(plantFood)
plantFoodTable = SymbolTable(plantFoodSym, foodTableCode)
foodTableCode = plantFoodTable:getID()
plant:addSymbolTable(plantFoodTable)
plant:setSymbolName("food", foodTableCode, plantFoodSym:getID())
plantSymTable = SymbolTable(plantColor)
plant:addSymbolTable(plantSymTable)
colorTableCode = plantSymTable:getID()
plantSymTable:setName("color")
plant:setSymbolName("color", colorTableCode, plantColor:getID())
Now we initialize the base object for the red species. The initialization of an agent base object is more complex, since it also requires the initialization of a brain, but the mechanism is the same.
After generic object parameterization, we create an instance of a gridbrain. The appropriate number of grids are created. A component set is assigned to each grid, including perception and action components. It is by assigning perception and action components to grids that we determine the sensory capabilities of an agent, and what it is capable of doing. Grids are assigned to the gridbrain, and the gridbrain to the agent.
-- Reds
--------------------------------------------------------------------------------
red = SimObj2D()
red:setBirthRadius(agentBirthRadius)
red:setSize(agentSize)
red:setDrag(drag)
red:setRotDrag(rotDrag)
red:setInitialEnergy(initialEnergy)
red:setMaxAge(maxAgeLow, maxAgeHigh)
red:setViewRange(viewRange)
red:setViewAngle(viewAngle)
red:setGoCost(goCost)
red:setRotateCost(rotateCost)
red:setGoForceScale(goForceScale)
red:setRotateForceScale(rotateForceScale)
red:setShape(SimObj2D.SHAPE_TRIANGLE)
red:setColoringSymbolName("color")
red:setSoundRange(500)
red:setFireInterval(fireInterval)
red:setLaserLength(laserLength)
red:setLaserSpeed(laserSpeed)
red:setLaserRange(laserRange)
red:setLaserStrengthFactor(laserStrengthFactor)
red:setLaserCostFactor(laserCostFactor)
red:setLaserHitDuration(laserHitDuration)
red:setKeepBodyOnExpirationDeath(keepBodyOnExpire)
red:setFeedCenter(0.5)
redFeedSym = SymbolFloat(redFeed)
redFeedTable = SymbolTable(redFeedSym)
feedTableCode = redFeedTable:getID()
red:addSymbolTable(redFeedTable)
red:setSymbolName("feed", feedTableCode, redFeedSym:getID())
redSymTable = SymbolTable(redColor, colorTableCode)
red:addSymbolTable(redSymTable)
redSymTable:setDynamic(true)
redSymTable:setName("color")
red:setSymbolName("color", colorTableCode, redColor:getID())
-- Symbols acquisition
red:addObjectSymbolAcquisition(colorTableCode, colorTableCode)
red:addMessageSymbolAcquisition(colorTableCode)
-- Agent Brain
brain = Gridbrain()
brain:setMutateAddConnectionProb(addConnectionProb)
brain:setMutateRemoveConnectionProb(removeConnectionProb)
brain:setMutateSplitConnectionProb(splitConnectionProb)
brain:setMutateJoinConnectionsProb(joinConnectionsProb)
brain:setMutateChangeInactiveComponentProb(changeInComponentProb)
-- Objects Grid
alphaSet = ComponentSet()
for i, comp in pairs(alphaComponents) do
alphaSet:addComponent(comp)
end
alphaSet:addComponent(PER(Sim2D.PERCEPTION_POSITION))
alphaSet:addComponent(PER(Sim2D.PERCEPTION_DISTANCE))
alphaSet:addComponent(PER(Sim2D.PERCEPTION_TARGET))
alphaSet:addComponent(PER(Sim2D.PERCEPTION_LTARGET))
alphaSet:addComponent(PER(Sim2D.PERCEPTION_LOF))
alphaSet:addComponent(PER(Sim2D.PERCEPTION_ORIENTATION))
alphaSet:addComponent(PER(Sim2D.PERCEPTION_SYMEQ, colorTableCode, redColor:getID(), colorTableCode, true))
alphaSet:addComponent(PER(Sim2D.PERCEPTION_SYMPRO, feedTableCode, redFeedSym:getID(), foodTableCode))
grid = Grid()
grid:init(Grid.ALPHA, 0, 0)
grid:setComponentSet(alphaSet)
brain:addGrid(grid, "objects");
soundSet = ComponentSet()
for i, comp in pairs(alphaComponents) do
soundSet:addComponent(comp)
end
soundSet:addComponent(PER(Sim2D.PERCEPTION_POSITION))
soundSet:addComponent(PER(Sim2D.PERCEPTION_DISTANCE))
soundSet:addComponent(PER(Sim2D.PERCEPTION_VALUE))
soundSet:addComponent(PER(Sim2D.PERCEPTION_SYMEQ, colorTableCode, redColor:getID(), colorTableCode, true))
soundGrid = Grid()
soundGrid:init(Grid.ALPHA, 0, 0)
soundGrid:setComponentSet(soundSet)
brain:addGrid(soundGrid, "sounds");
-- Beta Grid
betaSet = ComponentSet()
for i, comp in pairs(betaComponents) do
betaSet:addComponent(comp)
end
betaSet:addComponent(ACT(Sim2D.ACTION_GO))
betaSet:addComponent(ACT(Sim2D.ACTION_ROTATE))
betaSet:addComponent(ACT(Sim2D.ACTION_EATB))
betaSet:addComponent(ACT(Sim2D.ACTION_FIREB, colorTableCode, redColor:getID(), colorTableCode, false))
betaSet:addComponent(ACT(Sim2D.ACTION_SPEAK, colorTableCode, redColor:getID(), colorTableCode, true))
grid2 = Grid()
grid2:init(Grid.BETA, 0, 0)
grid2:setComponentSet(betaSet)
brain:addGrid(grid2, "beta")
red:setBrain(brain)
The same thing is now done for the blue species. In a scenario with many species, an interesting alternative would be to define a species initialization function, that takes as parameters the features of the species that can vary. In this case, the parameter used would be the color of the species.
-- Blues
--------------------------------------------------------------------------------
blue = SimObj2D()
blue:setBirthRadius(agentBirthRadius)
blue:setSize(agentSize)
blue:setDrag(drag)
blue:setRotDrag(rotDrag)
blue:setInitialEnergy(initialEnergy)
blue:setMaxAge(maxAgeLow, maxAgeHigh)
blue:setViewRange(viewRange)
blue:setViewAngle(viewAngle)
blue:setGoCost(goCost)
blue:setRotateCost(rotateCost)
blue:setGoForceScale(goForceScale)
blue:setRotateForceScale(rotateForceScale)
blue:setShape(SimObj2D.SHAPE_TRIANGLE)
blue:setColoringSymbolName("color")
blue:setSoundRange(500)
blue:setFireInterval(fireInterval)
blue:setLaserLength(laserLength)
blue:setLaserSpeed(laserSpeed)
blue:setLaserRange(laserRange)
blue:setLaserStrengthFactor(laserStrengthFactor)
blue:setLaserCostFactor(laserCostFactor)
blue:setLaserHitDuration(laserHitDuration)
blue:setKeepBodyOnExpirationDeath(keepBodyOnExpire)
blue:setFeedCenter(0.5)
--blueFoodSym = SymbolFloat(blueFood)
--blueFoodTable = SymbolTable(blueFoodSym, foodTableCode)
--blue:addSymbolTable(blueFoodTable)
--blue:setSymbolName("food", foodTableCode, blueFoodSym:getID())
blueFeedSym = SymbolFloat(blueFeed)
blueFeedTable = SymbolTable(blueFeedSym, feedTableCode)
blue:addSymbolTable(blueFeedTable)
blue:setSymbolName("feed", feedTableCode, blueFeedSym:getID())
blueSymTable = SymbolTable(blueColor, colorTableCode)
blue:addSymbolTable(blueSymTable)
blueSymTable:setDynamic(true)
blueSymTable:setName("color")
blue:setSymbolName("color", colorTableCode, blueColor:getID())
-- Symbols acquisition
blue:addObjectSymbolAcquisition(colorTableCode, colorTableCode)
blue:addMessageSymbolAcquisition(colorTableCode)
-- Agent Brain
brain = Gridbrain()
brain:setMutateAddConnectionProb(addConnectionProb)
brain:setMutateRemoveConnectionProb(removeConnectionProb)
brain:setMutateSplitConnectionProb(splitConnectionProb)
brain:setMutateJoinConnectionsProb(joinConnectionsProb)
brain:setMutateChangeInactiveComponentProb(changeInComponentProb)
-- Objects Grid
alphaSet = ComponentSet()
for i, comp in pairs(alphaComponents) do
alphaSet:addComponent(comp)
end
alphaSet:addComponent(PER(Sim2D.PERCEPTION_POSITION))
alphaSet:addComponent(PER(Sim2D.PERCEPTION_DISTANCE))
alphaSet:addComponent(PER(Sim2D.PERCEPTION_TARGET))
alphaSet:addComponent(PER(Sim2D.PERCEPTION_LTARGET))
alphaSet:addComponent(PER(Sim2D.PERCEPTION_LOF))
alphaSet:addComponent(PER(Sim2D.PERCEPTION_ORIENTATION))
alphaSet:addComponent(PER(Sim2D.PERCEPTION_SYMEQ, colorTableCode, blueColor:getID(), colorTableCode, true))
alphaSet:addComponent(PER(Sim2D.PERCEPTION_SYMPRO, feedTableCode, blueFeedSym:getID(), foodTableCode))
grid = Grid()
grid:init(Grid.ALPHA, 0, 0)
grid:setComponentSet(alphaSet)
brain:addGrid(grid, "objects");
soundSet = ComponentSet()
for i, comp in pairs(alphaComponents) do
soundSet:addComponent(comp)
end
soundSet:addComponent(PER(Sim2D.PERCEPTION_POSITION))
soundSet:addComponent(PER(Sim2D.PERCEPTION_DISTANCE))
soundSet:addComponent(PER(Sim2D.PERCEPTION_VALUE))
soundSet:addComponent(PER(Sim2D.PERCEPTION_SYMEQ, colorTableCode, blueColor:getID(), colorTableCode, true))
soundGrid = Grid()
soundGrid:init(Grid.ALPHA, 0, 0)
soundGrid:setComponentSet(soundSet)
brain:addGrid(soundGrid, "sounds");
-- Beta Grid
betaSet = ComponentSet()
for i, comp in pairs(betaComponents) do
betaSet:addComponent(comp)
end
betaSet:addComponent(ACT(Sim2D.ACTION_GO))
betaSet:addComponent(ACT(Sim2D.ACTION_ROTATE))
betaSet:addComponent(ACT(Sim2D.ACTION_EATB))
betaSet:addComponent(ACT(Sim2D.ACTION_FIREB, colorTableCode, blueColor:getID(), colorTableCode, false))
betaSet:addComponent(ACT(Sim2D.ACTION_SPEAK, colorTableCode, blueColor:getID(), colorTableCode, true))
grid2 = Grid()
grid2:init(Grid.BETA, 0, 0)
grid2:setComponentSet(betaSet)
brain:addGrid(grid2, "beta")
blue:setBrain(brain)
Next we initialize the population dynamics module, which takes care of object creation and removal. We create an instance of PopDynSEGA
, which implements the Simulation Embedded Genetic Algorithm (SEGA). The population dynamics object is then assigned to the simulation object.
Then we create a Species
object for each species: blue agents, red agents and plants. When creating a species object, we specify the base object for the species, the population size and the species genetic buffer size. The species objects are where we define he evolutionary goals of the species, as well as some evolutionary parameters like recombination probability, group factor and fitness ageing. Each species object is added to the population dynamics object.
-- Population Dynamics
--------------------------------------------------------------------------------
popDyn = PopDynSEGA()
sim:setPopulationDynamics(popDyn)
blueSpecies = Species(blue, numberOfBlues, bufferSize)
if blueRand then
blueSpecies:addGoal(SimObj2D.FITNESS_RANDOM)
else
blueSpecies:addGoal(SimObj2D.FITNESS_LASER_SCORE)
end
blueSpecies:setFitnessAging(fitnessAging)
blueSpecies:setRecombineProb(recombineProb)
blueSpecies:setGroupFactor(groupFactor)
popDyn:addSpecies(blueSpecies)
redSpecies = Species(red, numberOfReds, bufferSize)
if redRand then
redSpecies:addGoal(SimObj2D.FITNESS_RANDOM)
else
redSpecies:addGoal(SimObj2D.FITNESS_LASER_SCORE)
end
redSpecies:setFitnessAging(fitnessAging)
redSpecies:setRecombineProb(recombineProb)
redSpecies:setGroupFactor(groupFactor)
popDyn:addSpecies(redSpecies)
plantSpecies = Species(plant, numberOfPlants, 1)
plantSpecies:addGoal(SimObj2D.FITNESS_RANDOM)
popDyn:addSpecies(plantSpecies)
popDyn:setEvolutionStopTime(evolutionStopTime)
We now define a human agent, if the value of variable humanAgent
is true
. This is similar to defining a base agent object, but instead of adding it to a species object, we add it directly to the simulation. We then set its initial position in the world and set it as the human controlled object in the simulation object.
A human agent is controlled by a human through the keyboard. In this case, arrow keys cause the agent to move, the "e" key causes to eat to attempt to eat the nearest object and the space key causes it to fire a shot.
-- Human Agent
--------------------------------------------------------------------------------
if humanAgent then
human = SimObj2D()
human:setSize(agentSize)
human:setDrag(drag)
human:setRotDrag(rotDrag)
human:setInitialEnergy(1.0)
human:setMaxAge(maxAgeHigh)
human:setViewRange(viewRange)
human:setViewAngle(viewAngle)
human:setGoCost(goCost)
human:setRotateCost(rotateCost)
human:setGoForceScale(goForceScale)
human:setRotateForceScale(rotateForceScale)
human:setFeedCenter(0.5)
human:setShape(SimObj2D.SHAPE_TRIANGLE)
human:setSoundRange(500)
human:setFireInterval(fireInterval)
human:setLaserLength(laserLength)
human:setLaserSpeed(laserSpeed)
human:setLaserRange(laserRange)
human:setLaserStrengthFactor(laserStrengthFactor)
human:setLaserCostFactor(laserCostFactor)
human:setLaserHitDuration(laserHitDuration)
human:setColoringSymbolName("color")
humanColor = SymbolRGB(82, 228, 241)
symTable = SymbolTable(humanColor, colorTableCode)
human:addSymbolTable(symTable)
human:setSymbolName("color", colorTableCode, humanColor:getID())
humanFeed = SymbolFloat(humanFeed)
humanFeedTable = SymbolTable(humanFeed, feedTableCode)
human:addSymbolTable(humanFeedTable)
human:setSymbolName("feed", feedTableCode, humanFeed:getID())
-- Human Brain
brain = Gridbrain()
alphaSet = ComponentSet()
for i, comp in pairs(alphaComponents) do
alphaSet:addComponent(comp)
end
grid = Grid()
grid:init(Grid.ALPHA, 0, 0)
grid:setComponentSet(alphaSet)
brain:addGrid(grid, "objects");
betaSet = ComponentSet()
for i, comp in pairs(betaComponents) do
betaSet:addComponent(comp)
end
grid2 = Grid()
grid2:init(Grid.BETA, 0, 0)
grid2:setComponentSet(betaSet)
brain:addGrid(grid2, "beta")
human:setBrain(brain)
sim:addObject(human)
human:setPos(300, 300)
sim:setHuman(human)
end
In the following segment we initialize the logging modules. These modules record information about the history of the simulation in comma separated values (csv) files. One StatCommon
module is created for each agent species. This modules provides basic statistical information about the set of dead agents in a species for periods of the simulation. We set the file name associated with each module, using the previously defined logSuffix
. We then set the object fields for which we wish to gather data. A column in the log file will be generated for the average, median, maximum, minimum and standard deviation of each field. A row will be generated every p simulation cycles, where p is the log interval. We assign each StatCommon
object to their respective specie's object.
If the logBrains
variable has the value true
, we also create LogBestBrain
logging modules. These modules log a representation of the brain of the best agent found during each log interval. The brains are represented as graphics in scalable vector graphics (svg) format. The setBufferDump
method of species objects also allows us to dump the representation of the brain of each agent contained in the genetic buffer of the species at the end of each log interval.
The log time interval is generic to all logging modules and is set on the population dynamics object.
-- Logs and Statistics
--------------------------------------------------------------------------------
stats = StatCommon()
stats:setFile("log_blue" .. logSuffix .. ".csv")
stats:addField("energy_gained")
stats:addField("laser_score")
stats:addField("target_score")
stats:addField("gb_connections")
stats:addField("gb_active_connections")
stats:addField("gb_grid_width_objects")
stats:addField("gb_grid_height_objects")
stats:addField("gb_grid_width_beta")
stats:addField("gb_grid_height_beta")
stats:addField("symtable_used_color")
blueSpecies:addDeathLog(stats)
stats = StatCommon()
stats:setFile("log_red" .. logSuffix .. ".csv")
stats:addField("energy_gained")
stats:addField("laser_score")
stats:addField("target_score")
stats:addField("gb_connections")
stats:addField("gb_active_connections")
stats:addField("gb_grid_width_objects")
stats:addField("gb_grid_height_objects")
stats:addField("gb_grid_width_beta")
stats:addField("gb_grid_height_beta")
stats:addField("symtable_used_color")
redSpecies:addDeathLog(stats)
if logBrains then
logBrain = LogBestBrain()
logBrain:setFileNameSuffix(".svg")
if logOnlyLastBrain then
logBrain:setLogOnlyLast(true)
logBrain:setFileNamePrefix("brain_blue" .. logSuffix)
else
logBrain:setFileNamePrefix("brain_blue" .. logSuffix .. "t")
end
blueSpecies:addDeathLog(logBrain)
logBrain = LogBestBrain()
logBrain:setFileNameSuffix(".svg")
if logOnlyLastBrain then
logBrain:setLogOnlyLast(true)
logBrain:setFileNamePrefix("brain_red" .. logSuffix)
else
logBrain:setFileNamePrefix("brain_red" .. logSuffix .. "t")
end
redSpecies:addDeathLog(logBrain)
end
blueSpecies:setBufferDump("bdump_blue" .. logSuffix)
redSpecies:setBufferDump("bdump_red" .. logSuffix)
popDyn:setLogTimeInterval(logTimeInterval)
Finally we just initialize graphics and run the simulation. The parameters in the initGraphics
method allow us to set the window size, full screen mode, and no graphic mode. The ability to run the simulation with no graphical visualization is useful for machines with no display and faster experiment runs where visualization is not important.
-- Start Simulation
--------------------------------------------------------------------------------
sim:initGraphics(screenWidth, screenHeight, fullScreen, noGraphics)
sim:run()