diff --git a/experiments/brain-speciation/manager.py b/experiments/brain-speciation/manager.py index 83aeeff1e5..6cddfc6b91 100644 --- a/experiments/brain-speciation/manager.py +++ b/experiments/brain-speciation/manager.py @@ -19,7 +19,7 @@ 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 pyrevolve.revolve_bot.morphology_compatibility import MorphologyCompatibility from typing import TYPE_CHECKING if TYPE_CHECKING: diff --git a/experiments/species_exploration/manager.py b/experiments/species_exploration/manager.py new file mode 100644 index 0000000000..db90354cff --- /dev/null +++ b/experiments/species_exploration/manager.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +from pyrevolve import parser +from pyrevolve.evolution import fitness +from pyrevolve.evolution.population.population_management import steady_state_population_management +from pyrevolve.evolution.selection import multiple_selection_with_duplicates, multiple_selection, 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 +from pyrevolve.experiment_management import ExperimentManagement +from pyrevolve.genotype.lsystem_neat.crossover import CrossoverConfig as lCrossoverConfig +from pyrevolve.genotype.lsystem_neat.crossover import standard_crossover as lcrossover +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 pyrevolve.revolve_bot.morphology_compatibility import MorphologyCompatibility + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from typing import Union + from pyrevolve.evolution.population.population import Population + from pyrevolve.evolution.individual import Individual + + +async def run(): + """ + The main coroutine, which is started below. + """ + + # experiment params # + num_generations = 200 + population_size = 100 + offspring_size = 50 + remove_species_gen_n = 100 + + body_conf = PlasticodingConfig( + max_structural_modules=20, + allow_vertical_brick=False, + use_movement_commands=True, + use_rotation_commands=False, + use_movement_stack=False, + allow_joint_joint_attachment=False, + ) + brain_conf = NeatBrainGenomeConfig() + brain_conf.multineat_params.DisjointCoeff = 0.3 + brain_conf.multineat_params.ExcessCoeff = 0.3 + brain_conf.multineat_params.WeightDiffCoeff = 0.3 + brain_conf.multineat_params.ActivationADiffCoeff = 0.3 + brain_conf.multineat_params.ActivationBDiffCoeff = 0.3 + brain_conf.multineat_params.TimeConstantDiffCoeff = 0.3 + brain_conf.multineat_params.BiasDiffCoeff = 0.3 + brain_conf.multineat_params.ActivationFunctionDiffCoeff = 0.3 + brain_conf.multineat_params.CompatTreshold = 3.0 + brain_conf.multineat_params.MinCompatTreshold = 3.0 + brain_conf.multineat_params.CompatTresholdModifier = 0.1 + brain_conf.multineat_params.CompatTreshChangeInterval_Generations = 1 + brain_conf.multineat_params.CompatTreshChangeInterval_Evaluations = 1 + genotype_conf = LSystemCPGHyperNEATGenotypeConfig(body_conf, brain_conf) + + plasticMutation_conf = plasticMutationConfig( + mutation_prob=0.8, + genotype_conf=body_conf + ) + + lmutation_conf = lMutationConfig( + plasticoding_mutation_conf=plasticMutation_conf, + neat_conf=brain_conf, + ) + + 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}') + + species_done = False + if do_recovery: + #TODO actually, if gen_num > remove_species_gen_n, we should read the recovery state with species=False + gen_num, has_offspring, next_robot_id, next_species_id = \ + experiment_management.read_recovery_state(population_size, offspring_size, species=True) + # Partial recovery for speciated populations is not implemented + has_offspring = False + if gen_num == remove_species_gen_n: + new_gen_num, new_has_offspring, new_next_robot_id, _ = \ + experiment_management.read_recovery_state(population_size, offspring_size, species=False) + if new_gen_num > gen_num: + species_done = True + gen_num = new_gen_num + has_offspring = new_has_offspring + next_robot_id = new_next_robot_id + + if gen_num == num_generations-1: + logger.info('Experiment is already complete.') + return + else: + gen_num = 0 + next_robot_id = 1 + next_species_id = 1 + + if gen_num < 0: + logger.info('Experiment continuing from first generation') + gen_num = 0 + + if next_robot_id < 0: + next_robot_id = 1 + + if next_species_id < 0: + next_species_id = 1 + + 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, + genotype_constructor=LSystemCPGHyperNEATGenotype, + genotype_conf=genotype_conf, + fitness_function=fitness.displacement_velocity, + mutation_operator=lmutation, + mutation_conf=lmutation_conf, + crossover_operator=lcrossover, + crossover_conf=crossover_conf, + selection=lambda individuals: tournament_selection(individuals, 2), + 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_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=35, + old_age_fitness_penalty=0.5, + species_max_stagnation=30, + ) + + def adapt_population_config(config): + config.population_management = steady_state_population_management + config.parent_selection = \ + lambda individuals: multiple_selection(individuals, 2, tournament_selection) + + n_cores = args.n_cores + + simulator_queue = SimulatorQueue(n_cores, args, args.port_start) + await simulator_queue.start() + + analyzer_queue = AnalyzerQueue(1, args, args.port_start+n_cores) + await analyzer_queue.start() + + population: Union[PopulationSpeciated, Population] + if not species_done: + population = PopulationSpeciated(population_conf, + simulator_queue, + analyzer_queue, + next_robot_id, + next_species_id) + else: + population = \ + Population(population_conf, + simulator_queue, + analyzer_queue, + next_robot_id) + adapt_population_config(population.config) + + if do_recovery: + # 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 + next_robot_id = 1 + population.config.population_size + gen_num * population.config.offspring_size + population.next_robot_id = next_robot_id + + if has_offspring: + if not species_done: + raise NotImplementedError('partial recovery not implemented') + recovered_individuals = population.load_partially_completed_generation(gen_num, population_size, offspring_size, next_robot_id) + gen_num += 1 + logger.info(f'Recovered unfinished offspring for generation {gen_num}') + + if gen_num == 0: + await population.initialize(recovered_individuals) + else: + population = await population.next_generation(gen_num, recovered_individuals) + + experiment_management.export_snapshots_species(population.genus, gen_num) + else: + # starting a new experiment + experiment_management.create_exp_folders() + await population.initialize() + experiment_management.export_snapshots_species(population.genus, gen_num) + + while gen_num < num_generations-1: + gen_num += 1 + population = await population.next_generation(gen_num) + if isinstance(population, PopulationSpeciated): + assert not species_done + experiment_management.export_snapshots_species(population.genus, gen_num) + else: + assert species_done + # WARNING: This export_snapshots may need fixing + experiment_management.export_snapshots(population.individuals, gen_num) + + if gen_num == remove_species_gen_n: + species_done = True + population = population.into_population() + # Adjust the configuration + adapt_population_config(population.config) + # save the converted population for debugging (may create unpredictable behaviours in recovery) + experiment_management.export_snapshots(population.individuals, f"{gen_num}_flattened") diff --git a/pyrevolve/evolution/population/population.py b/pyrevolve/evolution/population/population.py index 2f14eb6d1c..30cadd53b3 100644 --- a/pyrevolve/evolution/population/population.py +++ b/pyrevolve/evolution/population/population.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio import os +import re from pyrevolve.evolution.individual import Individual from pyrevolve.custom_logging.logger import logger @@ -8,10 +9,11 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import List, Optional + from typing import List, Optional, Callable from pyrevolve.evolution.speciation.species import Species from pyrevolve.tol.manage.measures import BehaviouralMeasurements from pyrevolve.util.supervisor.analyzer_queue import AnalyzerQueue, SimulatorQueue + from pyrevolve.evolution.speciation.population_speciated import PopulationSpeciated, PopulationSpeciatedConfig class Population: @@ -63,14 +65,16 @@ def _new_individual(self, def load_snapshot(self, gen_num: int) -> None: """ - Recovers all genotypes and fitnesses of robots in the lastest selected population + Recovers all genotypes and fitnesses of robots in the selected generation :param gen_num: number of the generation snapshot to recover """ - data_path = self.config.experiment_management.experiment_folder - for r, d, f in os.walk(os.path.join(data_path, f'selectedpop_{gen_num}')): - for file in f: - if 'body' in file: - _id = file.split('.')[0].split('_')[-2]+'_'+file.split('.')[0].split('_')[-1] + extract_id = re.compile(r'^body_(\d+)\.png$') + generation_folder = self.config.experiment_management.generation_folder(gen_num) + for _, _, files in os.walk(generation_folder): + for file in files: + test = extract_id.search(file) + if test is not None: + _id = test.group(1) self.individuals.append( self.config.experiment_management.load_individual(_id, self.config)) @@ -95,9 +99,8 @@ def load_offspring(self, n_robots = population_size + last_snapshot * offspring_size for robot_id in range(n_robots+1, next_robot_id): - #TODO refactor filename individuals.append( - self.config.experiment_management.load_individual(str(robot_id), self.config) + self.config.experiment_management.load_individual(robot_id, self.config) ) self.next_robot_id = next_robot_id @@ -172,6 +175,49 @@ async def next_generation(self, return new_population + def into_speciated_population(self, + are_individuals_compatible_fn: Optional[Callable[[Individual, Individual], bool]] = None, + young_age_threshold: int = None, + young_age_fitness_boost: float = None, + old_age_threshold: int = None, + old_age_fitness_penalty: float = None, + species_max_stagnation: int = None) -> PopulationSpeciated: + """ + Creates species based on the current population. + + You have to populate the missing parameter for the PopulationSpeciatedConfig in case the + `self.config` is not already of type PopulationSpeciatedConfig + + :param are_individuals_compatible_fn: see PopulationSpeciatedConfig + :param young_age_threshold: see PopulationSpeciatedConfig + :param young_age_fitness_boost: see PopulationSpeciatedConfig + :param old_age_threshold: see PopulationSpeciatedConfig + :param old_age_fitness_penalty: see PopulationSpeciatedConfig + :param species_max_stagnation: see PopulationSpeciatedConfig + :return: A new version of the current population, but divided in species. + """ + from pyrevolve.evolution.speciation.population_speciated import PopulationSpeciated, PopulationSpeciatedConfig + young_age_threshold = PopulationSpeciatedConfig.DEFAULT_YOUNG_AGE_THRESHOLD if young_age_threshold is None else young_age_threshold + young_age_fitness_boost = PopulationSpeciatedConfig.DEFAULT_YOUNG_AGE_FITNESS_BOOST if young_age_fitness_boost is None else young_age_fitness_boost + old_age_threshold = PopulationSpeciatedConfig.DEFAULT_OLD_AGE_THRESHOLD if old_age_threshold is None else old_age_threshold + old_age_fitness_penalty = PopulationSpeciatedConfig.DEFAULT_OLD_AGE_FITNESS_PENALTY if old_age_fitness_penalty is None else old_age_fitness_penalty + species_max_stagnation = PopulationSpeciatedConfig.DEFAULT_SPECIES_MAX_STAGNATION if species_max_stagnation is None else species_max_stagnation + + new_population_config = PopulationSpeciatedConfig \ + .from_population_config(self.config, + are_individuals_compatible_fn=are_individuals_compatible_fn, + young_age_threshold=young_age_threshold, + young_age_fitness_boost=young_age_fitness_boost, + old_age_threshold=old_age_threshold, + old_age_fitness_penalty=old_age_fitness_penalty, + species_max_stagnation=species_max_stagnation,) + new_population = PopulationSpeciated(new_population_config, + self.simulator_queue, + self.analyzer_queue, + self.next_robot_id) + new_population.genus.speciate(self.individuals) + return new_population + async def evaluate(self, new_individuals: List[Individual], gen_num: int, diff --git a/pyrevolve/evolution/speciation/population_speciated.py b/pyrevolve/evolution/speciation/population_speciated.py index cfafd8ea85..3f7554bc75 100644 --- a/pyrevolve/evolution/speciation/population_speciated.py +++ b/pyrevolve/evolution/speciation/population_speciated.py @@ -25,6 +25,7 @@ def __init__(self, next_species_id: int = 1): # TODO analyzer super().__init__(config, simulator_queue, analyzer_queue, next_robot_id) + self.config: PopulationSpeciatedConfig = self.config # this is only for correct type hinting self.individuals = None # TODO Crash when we should use it # Genus contains the collection of different species. @@ -82,6 +83,11 @@ async def next_generation(self, return new_population + def into_population(self) -> Population: + new_population = Population(self.config, self.simulator_queue, self.analyzer_queue, self.next_robot_id) + new_population.individuals = [individual for individual in self.genus.iter_individuals()] + return new_population + def _generate_individual(self, individuals: List[Individual]) -> Individual: # Selection operator (based on fitness) # Crossover diff --git a/pyrevolve/evolution/speciation/population_speciated_config.py b/pyrevolve/evolution/speciation/population_speciated_config.py index 6c47105a4b..9fb4230394 100644 --- a/pyrevolve/evolution/speciation/population_speciated_config.py +++ b/pyrevolve/evolution/speciation/population_speciated_config.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Callable, Optional, List + from typing import Callable, Optional, List, Union from pyrevolve.evolution.individual import Individual from pyrevolve.genotype import Genotype from pyrevolve.revolve_bot import RevolveBot @@ -11,6 +11,12 @@ class PopulationSpeciatedConfig(PopulationConfig): + DEFAULT_YOUNG_AGE_THRESHOLD: int = 5 + DEFAULT_YOUNG_AGE_FITNESS_BOOST: float = 1.1 + DEFAULT_OLD_AGE_THRESHOLD: int = 30 + DEFAULT_OLD_AGE_FITNESS_PENALTY: float = 0.5 + DEFAULT_SPECIES_MAX_STAGNATION: int = 50 + DEFAULT_OFFSPRING_SIZE: Optional[int] = None # TODO reorder arguments def __init__(self, @@ -33,12 +39,12 @@ def __init__(self, experiment_name: str, experiment_management, are_individuals_compatible_fn: Callable[[Individual, Individual], bool], - young_age_threshold: int = 5, - young_age_fitness_boost: float = 1.1, - old_age_threshold: int = 30, - old_age_fitness_penalty: float = 0.5, - species_max_stagnation: int = 50, - offspring_size: Optional[int] = None): + young_age_threshold: int = DEFAULT_YOUNG_AGE_THRESHOLD, + young_age_fitness_boost: float = DEFAULT_YOUNG_AGE_FITNESS_BOOST, + old_age_threshold: int = DEFAULT_OLD_AGE_THRESHOLD, + old_age_fitness_penalty: float = DEFAULT_OLD_AGE_FITNESS_PENALTY, + species_max_stagnation: int = DEFAULT_SPECIES_MAX_STAGNATION, + offspring_size: Optional[int] = DEFAULT_OFFSPRING_SIZE): """ Creates a PopulationSpeciatedConfig object that sets the particular configuration for the population with species @@ -95,3 +101,66 @@ def __init__(self, self.old_age_fitness_penalty = old_age_fitness_penalty self.species_max_stagnation = species_max_stagnation + @staticmethod + def from_population_config( + population_config: Union[PopulationConfig, PopulationSpeciatedConfig], + are_individuals_compatible_fn: Optional[Callable[[Individual, Individual], bool]] = None, + young_age_threshold: int = DEFAULT_YOUNG_AGE_THRESHOLD, + young_age_fitness_boost: float = DEFAULT_YOUNG_AGE_FITNESS_BOOST, + old_age_threshold: int = DEFAULT_OLD_AGE_THRESHOLD, + old_age_fitness_penalty: float = DEFAULT_OLD_AGE_FITNESS_PENALTY, + species_max_stagnation: int = DEFAULT_SPECIES_MAX_STAGNATION) -> PopulationSpeciatedConfig: + """ + Transforms the population config into a population_speciated_config. + If the argument `population_config` passed in is already a `PopulationSpeciatedConfig`, + nothing happens and the argument is returned as it is. + + :param population_config: PopulationConfig to test and possibly convert + :param are_individuals_compatible_fn: parameter for PopulationSpeciatedConfig constructor + in case of conversion [refer to constructor for meaning] + :param young_age_threshold: parameter for PopulationSpeciatedConfig constructor + in case of conversion [refer to constructor for meaning] + :param young_age_fitness_boost: parameter for PopulationSpeciatedConfig constructor + in case of conversion [refer to constructor for meaning] + :param old_age_threshold: parameter for PopulationSpeciatedConfig constructor + in case of conversion [refer to constructor for meaning] + :param old_age_fitness_penalty: parameter for PopulationSpeciatedConfig constructor + in case of conversion [refer to constructor for meaning] + :param species_max_stagnation: parameter for PopulationSpeciatedConfig constructor + in case of conversion [refer to constructor for meaning] + :return: the population config ensured to be of type `PopulationSpeciatedConfig` + :raises AttributeError: if the `population_config` param cannot be converted to `PopulationSpeciatedConfig` + """ + if isinstance(population_config, PopulationSpeciatedConfig): + return population_config + elif isinstance(population_config, PopulationConfig): + if are_individuals_compatible_fn is None: + raise AttributeError("`PopulationSpeciated` is upgrading to `PopulationSpeciatedConfig` " + "but the parameter `are_individuals_compatible_fn` is not specified") + return PopulationSpeciatedConfig( + population_size=population_config.population_size, + genotype_constructor=population_config.genotype_constructor, + genotype_conf=population_config.genotype_conf, + fitness_function=population_config.fitness_function, + mutation_operator=population_config.mutation_operator, + mutation_conf=population_config.mutation_conf, + crossover_operator=population_config.crossover_operator, + crossover_conf=population_config.crossover_conf, + selection=population_config.selection, + parent_selection=population_config.parent_selection, + population_management=population_config.population_management, + population_management_selector=population_config.population_management_selector, + evaluation_time=population_config.evaluation_time, + experiment_name=population_config.experiment_name, + experiment_management=population_config.experiment_management, + offspring_size=population_config.offspring_size, + are_individuals_compatible_fn=are_individuals_compatible_fn, + young_age_threshold=young_age_threshold, + young_age_fitness_boost=young_age_fitness_boost, + old_age_threshold=old_age_threshold, + old_age_fitness_penalty=old_age_fitness_penalty, + species_max_stagnation=species_max_stagnation, + ) + else: + raise AttributeError(f"PopulationSpeciatedConfig config cannot be created from {type(population_config)}") + diff --git a/pyrevolve/evolution/speciation/population_speciated_management.py b/pyrevolve/evolution/speciation/population_speciated_management.py index a440506f89..cf68fd6a7a 100644 --- a/pyrevolve/evolution/speciation/population_speciated_management.py +++ b/pyrevolve/evolution/speciation/population_speciated_management.py @@ -10,8 +10,7 @@ def steady_state_speciated_population_management(old_individuals, new_individual return multiple_selection_with_duplicates(selection_pool, number_of_individuals, selector) -def generational_population_speciated_management(old_individuals, new_individuals, number_of_individuals, selector): +def generational_speciated_population_management(old_individuals, new_individuals, number_of_individuals, selector): # Note (old_individuals, number_of_individuals, and selector) are not used, # but for the interface to be similar to steady state speciated. return new_individuals - diff --git a/pyrevolve/experiment_management.py b/pyrevolve/experiment_management.py index 8f9f84ce41..45c962cb0f 100644 --- a/pyrevolve/experiment_management.py +++ b/pyrevolve/experiment_management.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import List, AnyStr, Optional + from typing import List, AnyStr, Optional, Union from pyrevolve.tol.manage.measures import BehaviouralMeasurements from pyrevolve.evolution.speciation.genus import Genus from pyrevolve.evolution.speciation.species import Species @@ -76,7 +76,7 @@ def data_folder(self) -> str: """ return self._data_folder - def generation_folder(self, gen_num: int): + def generation_folder(self, gen_num: Union[int, AnyStr]): return os.path.join(self._generations_folder, f'generation_{gen_num}') def export_genotype(self, individual: Individual) -> None: @@ -197,26 +197,36 @@ def export_failed_eval_robot(self, individual: Individual) -> None: individual.phenotype.save_file(os.path.join(self._failed_robots_folder, f'phenotype_{individual.phenotype.id}.yaml')) individual.phenotype.save_file(os.path.join(self._failed_robots_folder, f'phenotype_{individual.phenotype.id}.sdf'), conf_type='sdf') - #TODO refactoring - def export_snapshots(self, individuals: List[Individual], gen_num: int) -> None: + def export_snapshots(self, individuals: List[Individual], gen_num: Union[int, AnyStr]) -> None: + """ + Export snapshot of the population + :param individuals: list of individuals to save + :param gen_num: number of generation to save (this will be in the folder name) + """ if self.settings.recovery_enabled: - path = os.path.join(self._generations_folder, f'generation_{gen_num}') - if os.path.exists(path): - shutil.rmtree(path) - os.mkdir(path) + generation_folder = self.generation_folder(gen_num) + if os.path.exists(generation_folder): + shutil.rmtree(generation_folder) + os.mkdir(generation_folder) + for ind in individuals: - self.export_phenotype_images(ind, os.path.join(self.experiment_folder, f'selectedpop_{gen_num}')) + self.export_phenotype_images(ind, generation_folder) logger.info(f'Exported snapshot {gen_num} with {len(individuals)} individuals') def export_snapshots_species(self, genus: Genus, gen_num: int) -> None: + """ + Export snapshot of the population, expecting a Genus as input (for populations with species) + :param genus: collection of species to save (it includes references to all the individuals) + :param gen_num: number of generation to save (this will be in the folder name) + """ if self.settings.recovery_enabled: - path = os.path.join(self._generations_folder, f'generation_{gen_num}') - if os.path.exists(path): - shutil.rmtree(path) - os.mkdir(path) + generation_folder = self.generation_folder(gen_num) + if os.path.exists(generation_folder): + shutil.rmtree(generation_folder) + os.mkdir(generation_folder) for species in genus.species_collection: - species_on_disk = os.path.join(path, f'species_{species.id}.yaml') - species_folder = os.path.join(path, f'species_{species.id}') + species_on_disk = os.path.join(generation_folder, f'species_{species.id}.yaml') + species_folder = os.path.join(generation_folder, f'species_{species.id}') os.mkdir(species_folder) species.serialize(species_on_disk) for individual, _ in species.iter_individuals(): @@ -250,7 +260,7 @@ def read_recovery_state(self, population_size: int, offspring_size: int, species :param population_size: how many individuals should each generation have :param offspring_size: how many offspring to expect for each generation :param species: if the data we are about to read is from a speciated population - :return: (last complete generation), (the next generation already has some data), (next robot id) + :return: (last complete generation), (the next generation already has some data), (next robot id), (next species id) """ # the latest complete snapshot @@ -277,12 +287,17 @@ def read_recovery_state(self, population_size: int, offspring_size: int, species n_exported_genomes = 0 for species_on_disk in os.scandir(folder.path): species_on_disk: os.DirEntry - if not species_on_disk.is_file(): + if not species_on_disk.is_file() \ + or not species_on_disk.path.endswith('.yaml'): continue with open(species_on_disk.path) as file: - species = yaml.load(file, Loader=yaml.SafeLoader) - n_exported_genomes += len(species['individuals_ids']) - species_id = species['id'] + try: + species = yaml.load(file, Loader=yaml.SafeLoader) + n_exported_genomes += len(species['individuals_ids']) + species_id = species['id'] + except Exception as e: + logger.exception(f'Failed to load "{species_on_disk.path}"', exc_info=e) + continue if species_id > last_species_id: last_species_id = species_id @@ -317,12 +332,11 @@ def read_recovery_state(self, population_size: int, offspring_size: int, species else: has_offspring = False - # last complete generation, the next generation already has some data, next robot id - #TODO return also last species ID + # last complete generation, the next generation already has some data, next robot id, next species id return last_complete_generation, has_offspring, last_id_with_fitness+1, last_species_id+1, def load_individual(self, - _id: AnyStr, + _id: int, config: PopulationConfig, fitness: Optional[str] = None) -> Individual: """ diff --git a/experiments/brain-speciation/MorphologyCompatibility.py b/pyrevolve/revolve_bot/morphology_compatibility.py similarity index 93% rename from experiments/brain-speciation/MorphologyCompatibility.py rename to pyrevolve/revolve_bot/morphology_compatibility.py index a2fff685ee..46b69908ec 100644 --- a/experiments/brain-speciation/MorphologyCompatibility.py +++ b/pyrevolve/revolve_bot/morphology_compatibility.py @@ -28,6 +28,7 @@ def __init__(self, symmetry: float = 0.0, hinge_count: float = 0.0, brick_count: float = 0.0, + brick_count_proportion: float = 0.0, brick_sensor_count: float = 0.0, touch_sensor_count: float = 0.0, free_slots: float = 0.0, @@ -78,6 +79,8 @@ def __init__(self, self.hinge_count: float = hinge_count # Number of bricks self.brick_count: float = brick_count + # Number of bricks proportionate to max size + self.brick_count_proportion: float = brick_count_proportion # Number of brick sensors self.brick_sensor_count: float = brick_sensor_count # Number of touch sensors @@ -120,13 +123,16 @@ def compatible_individuals(self, 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: + if self.max_permitted_modules is not None and self.size != 0.0: 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) + if self.max_permitted_modules is not None and self.brick_count_proportion != 0.0: + total_distance += self.brick_count_proportion * \ + abs(_2.brick_count - _1.brick_count) / self.max_permitted_modules 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)