Introduction

Hello dear Reader, on this page I will explain my complete and systemic implementation of the EvoGym library. When I first read the work of my predecessor Fabio Tanaka, who did an extraordinary work on Single Genome Soft Robots, I was really confused on the whole implementation of the different programs that co-existed in his workspace. To implement my own version of Hyper-Neat, I spent hours understanding what he has done and why. This is mainly because of this reason that I’ll explain how my code works and how I will work in the future on the EvoGym library. This article is made for people who want to learn more about Entity Component System, or the EvoGym Library and how to use it, or just how I manage my projects to reproduce my work.

To finish the introduction, you can find the example of such an implementation on a GitHub repository that I have made specially for this article here. The key idea of this small example is to evolve the the controller neural network of the robot in EvoGym with an evolutionary algorithm.

What is Entity Component System ?

First of all, this idea of using Entity Component System (ECS) has not emerged from me but from my supervisor (Claus Aranha, University of Tsukuba). I hope that this article will help others as much as it helped me organize my work, and for this particular reason, I wanted to thank Claus.

A brief example

Well, ECS is usually involved while creating video games, therefore the example I’m about to write will be about the creation of video games. I’m not a creator of video games myself, but it surely joins one big interest of mine : Simulations.

So, that being said, let’s go back to creating video games. Imagine yourself, two seconds trying to implements Non Playable Characters (NPC) in a fantasy world where the hero has to explore a vast land filled with lots of available races for these NPCs (Goblins, Elves…). In the mountains of the East you find Goblins, a cruel race that will steal every piece of gold that you carry. On the West part of this land resides dozens of Human towns with lots of market places. Well at some point a tiny and very kind goblin decided to quit his life of violence in the mountains and decided to reside on one of the many human town and to become a merchant. Normally in Object-Oriented Programming you would implement a Monster Class and then implement a Goblin Class that inherits from the Monster one, then the same goes for humans and their merchants. At the end of the day, it appears complicated to add other features to our poor little Goblin that just want to live a normal life as merchant.

Then, a simple way to implements all of this, and to manage every entity without going deeply into Class inheritance is to think about adding component to our entity/goblin. For example, in the case of our charming Goblin, we would add to the entity id number 7476572645 the following components/features :

  • Skin Color : green
  • Length : short (around 120cm probably)
  • Ears : long and pointed
  • Profession : merchant
  • Health : 100
  • Weapon : axe

This is a simple example but this way we can simply destroy/create/manage the desired components/features of every entity in the game.

How to describe such an architecture ?

What’s inside

As you’ve probably understood, ECS is a software architectural pattern. It contains 3 fundamental things :

  • Entity, it is a unique ID that we give to every entity in the simulation. This ID will allow us to go get the components of a given entity and to modify them.

  • Components, it is what will compose our entity, an entity can have as much as components as we want. But every entity must have one different component for a given type. For example, entity 793468682 can only have one component Health, not two. This will allow us to use dictionaries to store data and access them quickly. Here you might say : ‘Then it is completely useless if my entity can only have one weapon and not two’. Well, if you want any character to possess more than one weapon simply add another slot of weapon, but in each slot, only a single weapon will be stored.

  • Systems, it is what will handle the registry of components and will write what different entity possesses. It’s basically functions that takes every individuals possessing the same components and write/modify information directly in the component registries.

What does it look like ?

I have added here some other elements of the architecture that I will explain later.

project 
|
|-main.py
|-entity_manager.py
|-components.py
|-registry.py
|-world.py
|
|   systems
|---|
|   |-system1.py
|   |-system2.py
|
|   tools
|---|
|   |-tool1
|   |-tool2

entity_manager.py

The aim of this file is to create, destroy, know which entity is alive. Your entity manager should looks like this :


class EntityManager : 

    def __init__(self) : 
        self.next_id = 0 
        self._alive = set()

    def create_entity(self) : 
        id = self.next_id 
        self.next_id += 1
        self._alive.add(id)
        return id 
    
    def destroy_entity(self, entity_id) : 
        self._alive.remove(entity_id)

    def is_alive(self, entity_id) : 
        return entity_id in self._alive

Here, entity_id is a unique ID associated to a single entity. Knowing which entity is alive will be useful for the simulation and not to run the programs on every entity created since the beginning of the simulation. Moreover, if your experience uses lots of RAM, you will be able to remove the old entity from their registry, therefore guaranteeing a lower memory usage and longer runs.

components.py

In this file, we only implements the components. Remember that a component is an object that does not possess any function, its unique purpose is to store data, not to process them (this will be the job of the systems).

Here is the example of a components file :


class GenomeComponent : 

    def __init__(self, connections, nodes) : 
        self.connections = connections 
        self.nodes = nodes 


class FitnessComponent : 

    def __init__(self, fitness, finished) : 
        self.fitness = fitness 
        self.finished = finished 

class ControllerComponent : 
    def __init__(self, node_evals, input_nodes, output_nodes) : 
        self.node_evals = node_evals  
        self.input_nodes = input_nodes 
        self.output_nodes = output_nodes 

See, we only store data and don’t try to process them.

registry.py

Well, here we begin to explore more deeply the architecture. The idea to access the components of a given entity is to use the registry. Basically, the registry stores, in dictionaries, the id of an individual and the object component that store the data.

The registry looks like this :


from components import * 

class ComponentRegistry : 

    def __init__(self) : 
        self.genome_registry = {}
        self.fitness_registry = {}
        self.controller_registry = {}


    # ADDER METHODS
    def add_genome(self, entity_id, connections, nodes) : 
        self.genome_registry[entity_id] = GenomeComponent(connections, nodes)
    
    def add_fitness(self, entity_id, fitness, finished) : 
        self.fitness_registry[entity_id] = FitnessComponent(fitness, finished)

    def add_controller(self, entity_id, node_evals, input_nodes, output_nodes) : 
        self.controller_registry[entity_id] = ControllerComponent(node_evals, input_nodes, output_nodes)
    
    # GETTER METHODS
    def get_genome(self, entity_id) : 
        return self.genome_registry[entity_id]

    def get_fitness(self, entity_id) : 
        return self.fitness_registry[entity_id]
    
    def get_controller(self, entity_id) : 
        return self.controller_registry[entity_id]
    
    # CHECKER METHODS 
    def has_genome(self, entity_id) : 
        return entity_id in self.genome_registry
    
    def has_fitness(self, entity_id) : 
        return entity_id in self.fitness_registry
    
    def has_controller(self, entity_id) :
        return entity_id in self.controller_registry 

    
    # ADVANCED GETTER METHODS 
    def get_all_id_with_genome(self) : 
        return self.genome_registry.keys()
    
    def get_all_id_with_fitness(self) : 
        return self.fitness_registry.keys()
    
    def get_all_with_controller(self) : 
        return self.controller_registry.keys()
    
    # MODIFIERS, please give an object 
    def modify_genome(self, entity_id, genome) : 
        self.genome_registry[entity_id] = genome
        
    def modify_fitness(self, entity_id, fitness) : 
        self.fitness_registry[entity_id] = fitness 
    
    def modify_controller(self, entity_id, controller) : 
        self.controller_registry[entity_id] = controller
    
    # CLEARER METHODS 
    def clear_all_except_genome(self) : 
        self.fitness_registry.clear()

    def clear_genome(self) : 
        self.genome_registry.clear()

    def clear_fitness(self) : 
        self.fitness_registry.clear()

    def clear_controller(self) : 
        self.controller_registry.clear()

So, here we have the registry that manages what component is associated to which entity. Moreover this registry is able to get a component given an ID, or to add a component to an entity, or to modify a component, or to remove a component, or to know if an entity has a component or not.

The only bothering thing here is that every time you want to create a new component for your project, you have to add it to the registry and write all of its associated methods which can consume time that you don’t want to spend on this.

Tools

Before showing you what a typical system looks like, I want to talk a little bit about tools. Tools are meant to allow you to handle components data and to use them. For example here, in the components.py file above, I have a neural network component, but to create it, I need to extract the building information from the genome component. This is exactly the role of the tools. Here it would be extracting the building information from the genome component and create all of the data necessary to create a neural network component. To achieve this, I would simply write this function in a controller_operator.py file. Here is what the controller_operator.py file looks like :


import math 

class ControllerOperator : 
    activation_function = math.tanh
    output_activation_function = lambda x : x 
    agregation_function = sum
    response = 1 
    bias = 0 


    def __init__(self) : 
        pass 

    def generate_controller_from_genome(self, genome) : 
        node_evals = []
        for index_of_layer in range(len(genome.nodes.keys())) : 
            if index_of_layer == 0 : 
                input_nodes = []
                for input_node in genome.nodes[index_of_layer] : 
                    input_nodes.append(input_node)
                previous_layer = genome.nodes[index_of_layer]
                continue 

            if index_of_layer == len(genome.nodes.keys()) - 1 : 
                output_nodes = []
                for output_node in genome.nodes[index_of_layer] : 
                    output_nodes.append(output_node)

            for node in genome.nodes[index_of_layer] :
                inputs_of_node = []
                for previous_node in previous_layer : 
                    weight = genome.connections[(previous_node, node)]
                    inputs_of_node.append((previous_node, weight))
                node_evals.append((node, self.activation_function, self.agregation_function, self.bias, self.response, inputs_of_node))

            previous_layer = genome.nodes[index_of_layer]
        return node_evals, input_nodes, output_nodes
    
    def activate(self, controller, input_values) :
        values = {}
        for key, value in zip(controller.input_nodes, input_values) : 
            values[key] = value
        
        for node, activation_function, agregation_function, bias, response, inputs_of_node in controller.node_evals : 
            node_inputs = []
            for previous_node, weight in inputs_of_node : 
                node_inputs.append(values[previous_node] * weight)
            entering_node = agregation_function(node_inputs)
            values[node] = activation_function(bias + response * entering_node)
        return [values[node] for node in controller.output_nodes]

Then, these tools will be used in the systems. This will allow the reader to easily understand (I hope so) what is going on in the different system files and to easily modify the different tools and systems if it is required.

Systems

Systems are meant to to write/modify data from the registries.

I think that here an example is ten times more valuable than explanations. The following evaluation_system.py file will take all the individual alive and then add their controller to the registry before evaluate them and proceed to add their fitness to the registry. Here goes the file :


import numpy as np 

class EvaluationSystem : 
    def __init__(self, entity_manager, controller_operator, config, robot_simulator, reporter_tool, parallel_tool) :
        self.config = config
        self.entity_manager = entity_manager 
        self.controller_operator = controller_operator
        self.robot_simulator = robot_simulator
        self.reporter_tool = reporter_tool 
        self.parallel_tool = parallel_tool 
        self.generation = 1
    
    def __str__(self) : 
        return "EvaluationSystem, evaluate al individuals in the current population and add their fitness to registry"

    def process(self, registry) : 

        self.reporter_tool.start_generation(self.generation)
        self.generation += 1

        entity_ids = [id for id in registry.get_all_id_with_genome() if self.entity_manager.is_alive(id)]

        for entity_id in entity_ids : 
            genome = registry.get_genome(entity_id)
            node_evals, input_nodes, output_nodes = self.controller_operator.generate_controller_from_genome(genome)
            registry.add_controller(entity_id, node_evals, input_nodes, output_nodes)

        controllers = [registry.get_controller(entity_id) for entity_id in entity_ids]
        body = self.config.body 
        bodies = [np.array(body) for _ in range(len(entity_ids))] 

        function = self.robot_simulator.simulate
        chunk = list(zip(entity_ids, bodies, controllers))

        results = self.parallel_tool.run(function, chunk)
        fitnesses = []
        ids = []
        for entity_id, fitness, finished in results : 
            ids.append(entity_id)
            fitnesses.append(fitness)
            registry.add_fitness(entity_id, fitness, finished)

        fitnesses = np.array(fitnesses)
        arg_sorted_fitnesses = np.argsort(fitnesses)
        bests = []
        number_of_reported_individuals = self.config.number_of_reported_individuals
        for taken in range(number_of_reported_individuals) : 
            id = arg_sorted_fitnesses[len(entity_ids) - 1 - taken]
            bests.append((ids[id], fitnesses[id]))
            
        
        self.reporter_tool.bests(bests)

world.py

One of the last thing that we must do is to assemble all the different systems together in a world. This world is also an object, it contains all the different systems and the registry. Here goes the example of the world.py file :


from registry import ComponentRegistry

class World : 
    def __init__(self) : 
        self.registry = ComponentRegistry()
        self._builder_systems = []
        self._step_systems = []
        self.all_systems = []

    def add_builder_system(self, system) : 
        self._builder_systems.append(system)
        self.all_systems.append(system)
    
    def add_step_system(self, system) : 
        self._step_systems.append(system)
        self.all_systems.append(system)

    def reset(self) : 
        self.registry.clear_all_except_genome()

    def build(self) : 
        for system in self._builder_systems : 
            system.process(self.registry)

    def step(self) : 
        for system in self._step_systems : 
            system.process(self.registry)

main.py

The last thing to do is to put all of this together in a main file to run the simulation. Here goes the example of the main.py file :


import os
import json  

from config import Config  
from entity_manager import EntityManager 
from world import World
 
from systems.build_system import BuildSystem
from systems.evaluation_system import EvaluationSystem
from systems.tournament_system import TournamentSystem

from tools.controller_operator import ControllerOperator
from tools.genome_operator import GenomeOperator
from tools.robot_simulator import RobotSimulator
from tools.parallel_tool import ParallelTool
from tools.reporter_tool import ReporterTool

from results_manager.results_saver import ResultsSaver


def main() : 
    entity_manager = EntityManager()
    world = World()


    config_path = input("\nEnter the path to the config file from the configs folder (can be just config.json) : ")
    local_dir = os.path.dirname(os.path.abspath(__file__))
    config_path_final = os.path.join(local_dir, "configs", config_path)
    with open(config_path_final, 'r') as f : 
        config = json.load(f)

    config = Config(config)

    results_saver = ResultsSaver()
    results_saver.add_results_path()

    controller_operator = ControllerOperator()
    robot_simulator = RobotSimulator(config, controller_operator)
    genome_operator = GenomeOperator(config, robot_simulator)
    parallel_tool = ParallelTool(config)
    reporter_tool = ReporterTool(config)


    build_system = BuildSystem(config, entity_manager, genome_operator, reporter_tool)
    evaluation_system = EvaluationSystem(entity_manager, controller_operator, config, robot_simulator, reporter_tool, parallel_tool)
    tournament_system = TournamentSystem(entity_manager, config, genome_operator, reporter_tool)

    world.add_builder_system(build_system)
    world.add_step_system(evaluation_system)
    world.add_step_system(tournament_system)

    world.build()

    for generation in range(config.generations) : 
        world.step()

    results_saver.save_results(world.registry, config, config_path)

if __name__ == "__main__" : 
    main()

As you can see, this file is just there to initialize every object and run the different systems in a loop and in the right order.

Conclusion

Now we have all the necessary information to implement a simulation using ECS. This was the first part of this series of article, next I’ll explain how to install the different necessary libraries and use Docker to run all of your EvoGym simulations in a container. Moreover, I will shortly explain how I use the Evogym library to reproduce my work.