Skip to content

Commit

Permalink
Experimental tuning
Browse files Browse the repository at this point in the history
and minor bugfixes
and added more typing documentation to functions
  • Loading branch information
portaloffreedom committed Apr 14, 2020
1 parent 81b3aef commit 7fc4a13
Show file tree
Hide file tree
Showing 16 changed files with 373 additions and 86 deletions.
138 changes: 138 additions & 0 deletions experiments/brain-speciation/MorphologyCompatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
from __future__ import annotations

from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Optional
from pyrevolve.evolution.individual import Individual


class MorphologyCompatibility:
def __init__(self,
total_threshold: float = 1.0,
branching_modules_count: float = 0.0,
branching: float = 0.0,
extremities: float = 0.0,
limbs: float = 0.0,
extensiveness: float = 0.0,
length_of_limbs: float = 0.0,
coverage: float = 0.0,
joints: float = 0.0,
active_hinges_count: float = 0.0,
proportion: float = 0.0,
width: float = 0.0,
height: float = 0.0,
z_depth: float = 0.0,
absolute_size: float = 0.0,
size: float = 0.0,
sensors: float = 0.0,
symmetry: float = 0.0,
hinge_count: float = 0.0,
brick_count: float = 0.0,
brick_sensor_count: float = 0.0,
touch_sensor_count: float = 0.0,
free_slots: float = 0.0,
height_base_ratio: float = 0.0,
max_permitted_modules: Optional[int] = None,
symmetry_vertical: float = 0.0,
base_density: float = 0.0,
bottom_layer: float = 0.0,
):
# Total threshold
self.total_threshold: float = total_threshold

# Absolute branching
self.branching_modules_count: float = branching_modules_count
# Relative branching
self.branching: float = branching
# Absolute number of limbs
self.extremities: float = extremities
# Relative number of limbs
self.limbs: float = limbs
# Absolute length of limbs
self.extensiveness: float = extensiveness
# Relative length of limbs
self.length_of_limbs: float = length_of_limbs
# Coverage
self.coverage: float = coverage
# Relative number of effective active joints
self.joints: float = joints
# Absolute number of effective active joints
self.active_hinges_count: float = active_hinges_count
# Proportion
self.proportion: float = proportion
# Width
self.width: float = width
# Height
self.height: float = height
# Z depth
self.z_depth: float = z_depth
# Absolute size
self.absolute_size: float = absolute_size
# Relative size in respect of the max body size `max_permitted_modules`
self.size: float = size
# Proportion of sensor vs empty slots
self.sensors: float = sensors
# Body symmetry in the xy plane
self.symmetry: float = symmetry
# Number of active joints
self.hinge_count: float = hinge_count
# Number of bricks
self.brick_count: float = brick_count
# Number of brick sensors
self.brick_sensor_count: float = brick_sensor_count
# Number of touch sensors
self.touch_sensor_count: float = touch_sensor_count
# Number of free slots
self.free_slots: float = free_slots
# Ratio of the height over the root of the area of the base
self.height_base_ratio: float = height_base_ratio
# Maximum number of modules allowed (sensors excluded)
self.max_permitted_modules: Optional[int] = max_permitted_modules
# Vertical symmetry
self.symmetry_vertical: float = symmetry_vertical
# Base model density
self.base_density: float = base_density
# Bottom layer of the robot
self.bottom_layer: float = bottom_layer

def compatible_individuals(self,
individual1: Individual,
individual2: Individual) -> bool:
morph_measure_1 = individual1.phenotype.measure_body()
morph_measure_2 = individual2.phenotype.measure_body()
_1 = morph_measure_1
_2 = morph_measure_2

# TODO consider normalization of some of these values, some are already normalized by definition

total_distance: float = 0.0
total_distance += self.branching_modules_count * abs(_2.branching_modules_count - _1.branching_modules_count)
total_distance += self.branching * abs(_2.branching - _1.branching)
total_distance += self.extremities * abs(_2.extremities - _1.extremities)
total_distance += self.limbs * abs(_2.limbs - _1.limbs)
total_distance += self.extensiveness * abs(_2.extensiveness - _1.extensiveness)
total_distance += self.length_of_limbs * abs(_2.length_of_limbs - _1.length_of_limbs)
total_distance += self.coverage * abs(_2.coverage - _1.coverage)
total_distance += self.joints * abs(_2.joints - _1.joints)
total_distance += self.active_hinges_count * abs(_2.active_hinges_count - _1.active_hinges_count)
total_distance += self.proportion * abs(_2.proportion - _1.proportion)
total_distance += self.width * abs(_2.width - _1.width)
total_distance += self.height * abs(_2.height - _1.height)
total_distance += self.z_depth * abs(_2.z_depth - _1.z_depth)
total_distance += self.absolute_size * abs(_2.absolute_size - _1.absolute_size)
if self.max_permitted_modules is not None:
total_distance += self.size * \
abs(_2.absolute_size - _1.absolute_size) / self.max_permitted_modules
total_distance += self.sensors * abs(_2.sensors - _1.sensors)
total_distance += self.symmetry * abs(_2.symmetry - _1.symmetry)
total_distance += self.hinge_count * abs(_2.hinge_count - _1.hinge_count)
total_distance += self.brick_count * abs(_2.brick_count - _1.brick_count)
total_distance += self.brick_sensor_count * abs(_2.brick_sensor_count - _1.brick_sensor_count)
total_distance += self.touch_sensor_count * abs(_2.touch_sensor_count - _1.touch_sensor_count)
total_distance += self.free_slots * abs(_2.free_slots - _1.free_slots)
total_distance += self.height_base_ratio * abs(_2.height_base_ratio - _1.height_base_ratio)
total_distance += self.symmetry_vertical * abs(_2.symmetry_vertical - _1.symmetry_vertical)
total_distance += self.base_density * abs(_2.base_density - _1.base_density)
total_distance += self.bottom_layer * abs(_2.bottom_layer - _1.bottom_layer)

return total_distance <= self.total_threshold
54 changes: 43 additions & 11 deletions experiments/brain-speciation/manager.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
from __future__ import annotations

from pyrevolve import parser
from pyrevolve.evolution import fitness
from pyrevolve.evolution.selection import multiple_selection, tournament_selection
from pyrevolve.evolution.selection import multiple_selection_with_duplicates, tournament_selection
from pyrevolve.evolution.speciation.population_speciated import PopulationSpeciated
from pyrevolve.evolution.speciation.population_speciated_config import PopulationSpeciatedConfig
from pyrevolve.evolution.speciation.population_speciated_management import steady_state_speciated_population_management
Expand All @@ -11,13 +13,17 @@
from pyrevolve.genotype.lsystem_neat.mutation import LSystemNeatMutationConf as lMutationConfig
from pyrevolve.genotype.plasticoding.mutation.mutation import MutationConfig as plasticMutationConfig
from pyrevolve.genotype.lsystem_neat.mutation import standard_mutation as lmutation

from pyrevolve.util.supervisor.analyzer_queue import AnalyzerQueue
from pyrevolve.util.supervisor.simulator_queue import SimulatorQueue
from pyrevolve.custom_logging.logger import logger
from pyrevolve.genotype.plasticoding import PlasticodingConfig
from pyrevolve.genotype.lsystem_neat.lsystem_neat_genotype import LSystemCPGHyperNEATGenotype, LSystemCPGHyperNEATGenotypeConfig
from pyrevolve.genotype.neat_brain_genome.neat_brain_genome import NeatBrainGenomeConfig
from .MorphologyCompatibility import MorphologyCompatibility

from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pyrevolve.evolution.individual import Individual


async def run():
Expand All @@ -31,7 +37,7 @@ async def run():
offspring_size = 50

body_conf = PlasticodingConfig(
max_structural_modules=20, # TODO increase
max_structural_modules=20,
allow_vertical_brick=False,
use_movement_commands=True,
use_rotation_commands=False,
Expand Down Expand Up @@ -67,17 +73,32 @@ async def run():
crossover_conf = lCrossoverConfig(
crossover_prob=0.8,
)

compatibitity_tester = MorphologyCompatibility(
total_threshold=1.0,
size=1.0,
brick_count=1.0,
proportion=1.0,
coverage=1.0,
joints=1.5,
branching=1.0,
symmetry=0.0,
max_permitted_modules=body_conf.max_structural_modules,
)

# experiment params #

# Parse command line / file input arguments
args = parser.parse_args()
experiment_management = ExperimentManagement(args)
has_offspring = False
do_recovery = args.recovery_enabled and not experiment_management.experiment_is_new()

logger.info(f'Activated run {args.run} of experiment {args.experiment_name}')

if do_recovery:
gen_num, has_offspring, next_robot_id, next_species_id = experiment_management.read_recovery_state(population_size, offspring_size)
gen_num, has_offspring, next_robot_id, next_species_id = \
experiment_management.read_recovery_state(population_size, offspring_size, species=True)

if gen_num == num_generations-1:
logger.info('Experiment is already complete.')
Expand All @@ -97,9 +118,15 @@ async def run():
if next_species_id < 0:
next_species_id = 1

def are_genomes_compatible_fn(genotype1: LSystemCPGHyperNEATGenotype,
genotype2: LSystemCPGHyperNEATGenotype) -> bool:
return genotype1.is_brain_compatible(genotype2, genotype_conf)
def are_individuals_brains_compatible_fn(individual1: Individual,
individual2: Individual) -> bool:
assert isinstance(individual1.genotype, LSystemCPGHyperNEATGenotype)
assert isinstance(individual2.genotype, LSystemCPGHyperNEATGenotype)
return individual1.genotype.is_brain_compatible(individual2.genotype, genotype_conf)

def are_individuals_morphologies_compatible_fn(individual1: Individual,
individual2: Individual) -> bool:
return compatibitity_tester.compatible_individuals(individual1, individual2)

population_conf = PopulationSpeciatedConfig(
population_size=population_size,
Expand All @@ -111,18 +138,19 @@ def are_genomes_compatible_fn(genotype1: LSystemCPGHyperNEATGenotype,
crossover_operator=lcrossover,
crossover_conf=crossover_conf,
selection=lambda individuals: tournament_selection(individuals, 2),
parent_selection=lambda individuals: multiple_selection(individuals, 2, tournament_selection),
parent_selection=lambda individuals: multiple_selection_with_duplicates(individuals, 2, tournament_selection),
population_management=steady_state_speciated_population_management,
population_management_selector=tournament_selection,
evaluation_time=args.evaluation_time,
offspring_size=offspring_size,
experiment_name=args.experiment_name,
experiment_management=experiment_management,
# species stuff
are_genomes_compatible_fn=are_genomes_compatible_fn,
# are_individuals_compatible_fn=are_individuals_brains_compatible_fn,
are_individuals_compatible_fn=are_individuals_morphologies_compatible_fn,
young_age_threshold=5,
young_age_fitness_boost=2.0,
old_age_threshold=20,
old_age_threshold=35,
old_age_fitness_penalty=0.5,
species_max_stagnation=30,
)
Expand All @@ -142,12 +170,16 @@ def are_genomes_compatible_fn(genotype1: LSystemCPGHyperNEATGenotype,
next_species_id)

if do_recovery:
raise NotImplementedError('recovery not implemented')
# loading a previous state of the experiment
population.load_snapshot(gen_num)
if gen_num >= 0:
logger.info(f'Recovered snapshot {gen_num}, pop with {len(population.genus)} individuals')

# TODO partial recovery is not implemented, this is a substitute
has_offspring = False
next_robot_id = 1 + population.config.population_size + gen_num * population.config.offspring_size
population.next_robot_id = next_robot_id

if has_offspring:
raise NotImplementedError('partial recovery not implemented')
recovered_individuals = population.load_partially_completed_generation(gen_num, population_size, offspring_size, next_robot_id)
Expand Down
26 changes: 22 additions & 4 deletions pyrevolve/evolution/individual.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import Optional, List
from pyrevolve.revolve_bot import RevolveBot
from pyrevolve.genotype import Genotype
from pyrevolve.evolution.speciation.species import Species


class Individual:
Expand Down Expand Up @@ -38,24 +39,41 @@ def id(self) -> int:
_id = self.genotype.id
return _id

def export_genotype(self, folder) -> None:
def export_genotype(self, folder: str) -> None:
self.genotype.export_genotype(os.path.join(folder, f'genotype_{self.phenotype.id}.txt'))

def export_phenotype(self, folder) -> None:
def export_phenotype(self, folder: str) -> None:
if self.phenotype is None:
self.develop()
self.phenotype.save_file(os.path.join(folder, f'phenotype_{self.phenotype.id}.yaml'), conf_type='yaml')

def export_fitness(self, folder) -> None:
def export_phylogenetic_info(self, folder: str) -> None:
"""
Export phylogenetic information
(parents and other possibly other information to build a phylogenetic tree)
:param folder: folder where to save the information
"""
if self.parents is not None:
parents_ids: List[str] = [str(p.id) for p in self.parents]
parents_ids_str = ",".join(parents_ids)
else:
parents_ids_str = 'None'

filename = os.path.join(folder, f'parents_{self.id}.yaml')
with open(filename, 'w') as file:
file.write(f'parents:{parents_ids_str}')

def export_fitness_single_file(self, folder: str) -> None:
"""
It's saving the fitness into a file. The fitness can be a floating point number or None
:param folder: folder where to save the fitness
"""
with open(os.path.join(folder, f'fitness_{self.id}.txt'), 'w') as f:
f.write(str(self.fitness))

def export(self, folder) -> None:
def export(self, folder: str) -> None:
self.export_genotype(folder)
self.export_phylogenetic_info(folder)
self.export_phenotype(folder)
self.export_fitness(folder)

Expand Down
11 changes: 8 additions & 3 deletions pyrevolve/evolution/population/population.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
import os

from pyrevolve.evolution.individual import Individual
from pyrevolve.tol.manage import measures
from pyrevolve.custom_logging.logger import logger
from pyrevolve.evolution.population.population_config import PopulationConfig

from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import List, Optional, AnyStr
from typing import List, Optional
from pyrevolve.evolution.speciation.species import Species
from pyrevolve.tol.manage.measures import BehaviouralMeasurements
from pyrevolve.util.supervisor.analyzer_queue import AnalyzerQueue, SimulatorQueue

Expand Down Expand Up @@ -44,10 +44,15 @@ def __init__(self,
self.simulator_queue = simulator_queue
self.next_robot_id = next_robot_id

def _new_individual(self, genotype):
def _new_individual(self,
genotype,
parents: Optional[List[Individual]] = None):
individual = Individual(genotype)
individual.develop()
individual.phenotype.update_substrate()
if parents is not None:
individual.parents = parents

self.config.experiment_management.export_genotype(individual)
self.config.experiment_management.export_phenotype(individual)
self.config.experiment_management.export_phenotype_images(individual)
Expand Down
26 changes: 24 additions & 2 deletions pyrevolve/evolution/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ def tournament_selection(population: List[Individual], k=2) -> Individual:

def multiple_selection(population: List[Individual],
selection_size: int,
selection_function: Callable[[List[Individual]], Individual]) -> List[Individual]:
selection_function: Callable[[List[Individual]], Individual]
) -> List[Individual]:
"""
Perform selection on population of distinct group, can be used in the form parent selection or survival selection
Perform selection on population of distinct group, it can be used in the
form parent selection or survival selection.
It never selects the same individual more than once
:param population: list of individuals where to select from
:param selection_size: amount of individuals to select
:param selection_function:
Expand All @@ -49,3 +52,22 @@ def multiple_selection(population: List[Individual],
selected_individuals.append(selected_individual)
new_individual = True
return selected_individuals


def multiple_selection_with_duplicates(population: List[Individual],
selection_size: int,
selection_function: Callable[[List[Individual]], Individual]
) -> List[Individual]:
"""
Perform selection on population of distinct group, it can be used in the
form parent selection or survival selection.
It can select the same individual more than once
:param population: list of individuals where to select from
:param selection_size: amount of individuals to select
:param selection_function:
"""
selected_individuals = []
for _ in range(selection_size):
selected_individual = selection_function(population)
selected_individuals.append(selected_individual)
return selected_individuals
Loading

0 comments on commit 7fc4a13

Please sign in to comment.