Skip to content

Commit 7fc4a13

Browse files
Experimental tuning
and minor bugfixes and added more typing documentation to functions
1 parent 81b3aef commit 7fc4a13

File tree

16 files changed

+373
-86
lines changed

16 files changed

+373
-86
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
if TYPE_CHECKING:
5+
from typing import Optional
6+
from pyrevolve.evolution.individual import Individual
7+
8+
9+
class MorphologyCompatibility:
10+
def __init__(self,
11+
total_threshold: float = 1.0,
12+
branching_modules_count: float = 0.0,
13+
branching: float = 0.0,
14+
extremities: float = 0.0,
15+
limbs: float = 0.0,
16+
extensiveness: float = 0.0,
17+
length_of_limbs: float = 0.0,
18+
coverage: float = 0.0,
19+
joints: float = 0.0,
20+
active_hinges_count: float = 0.0,
21+
proportion: float = 0.0,
22+
width: float = 0.0,
23+
height: float = 0.0,
24+
z_depth: float = 0.0,
25+
absolute_size: float = 0.0,
26+
size: float = 0.0,
27+
sensors: float = 0.0,
28+
symmetry: float = 0.0,
29+
hinge_count: float = 0.0,
30+
brick_count: float = 0.0,
31+
brick_sensor_count: float = 0.0,
32+
touch_sensor_count: float = 0.0,
33+
free_slots: float = 0.0,
34+
height_base_ratio: float = 0.0,
35+
max_permitted_modules: Optional[int] = None,
36+
symmetry_vertical: float = 0.0,
37+
base_density: float = 0.0,
38+
bottom_layer: float = 0.0,
39+
):
40+
# Total threshold
41+
self.total_threshold: float = total_threshold
42+
43+
# Absolute branching
44+
self.branching_modules_count: float = branching_modules_count
45+
# Relative branching
46+
self.branching: float = branching
47+
# Absolute number of limbs
48+
self.extremities: float = extremities
49+
# Relative number of limbs
50+
self.limbs: float = limbs
51+
# Absolute length of limbs
52+
self.extensiveness: float = extensiveness
53+
# Relative length of limbs
54+
self.length_of_limbs: float = length_of_limbs
55+
# Coverage
56+
self.coverage: float = coverage
57+
# Relative number of effective active joints
58+
self.joints: float = joints
59+
# Absolute number of effective active joints
60+
self.active_hinges_count: float = active_hinges_count
61+
# Proportion
62+
self.proportion: float = proportion
63+
# Width
64+
self.width: float = width
65+
# Height
66+
self.height: float = height
67+
# Z depth
68+
self.z_depth: float = z_depth
69+
# Absolute size
70+
self.absolute_size: float = absolute_size
71+
# Relative size in respect of the max body size `max_permitted_modules`
72+
self.size: float = size
73+
# Proportion of sensor vs empty slots
74+
self.sensors: float = sensors
75+
# Body symmetry in the xy plane
76+
self.symmetry: float = symmetry
77+
# Number of active joints
78+
self.hinge_count: float = hinge_count
79+
# Number of bricks
80+
self.brick_count: float = brick_count
81+
# Number of brick sensors
82+
self.brick_sensor_count: float = brick_sensor_count
83+
# Number of touch sensors
84+
self.touch_sensor_count: float = touch_sensor_count
85+
# Number of free slots
86+
self.free_slots: float = free_slots
87+
# Ratio of the height over the root of the area of the base
88+
self.height_base_ratio: float = height_base_ratio
89+
# Maximum number of modules allowed (sensors excluded)
90+
self.max_permitted_modules: Optional[int] = max_permitted_modules
91+
# Vertical symmetry
92+
self.symmetry_vertical: float = symmetry_vertical
93+
# Base model density
94+
self.base_density: float = base_density
95+
# Bottom layer of the robot
96+
self.bottom_layer: float = bottom_layer
97+
98+
def compatible_individuals(self,
99+
individual1: Individual,
100+
individual2: Individual) -> bool:
101+
morph_measure_1 = individual1.phenotype.measure_body()
102+
morph_measure_2 = individual2.phenotype.measure_body()
103+
_1 = morph_measure_1
104+
_2 = morph_measure_2
105+
106+
# TODO consider normalization of some of these values, some are already normalized by definition
107+
108+
total_distance: float = 0.0
109+
total_distance += self.branching_modules_count * abs(_2.branching_modules_count - _1.branching_modules_count)
110+
total_distance += self.branching * abs(_2.branching - _1.branching)
111+
total_distance += self.extremities * abs(_2.extremities - _1.extremities)
112+
total_distance += self.limbs * abs(_2.limbs - _1.limbs)
113+
total_distance += self.extensiveness * abs(_2.extensiveness - _1.extensiveness)
114+
total_distance += self.length_of_limbs * abs(_2.length_of_limbs - _1.length_of_limbs)
115+
total_distance += self.coverage * abs(_2.coverage - _1.coverage)
116+
total_distance += self.joints * abs(_2.joints - _1.joints)
117+
total_distance += self.active_hinges_count * abs(_2.active_hinges_count - _1.active_hinges_count)
118+
total_distance += self.proportion * abs(_2.proportion - _1.proportion)
119+
total_distance += self.width * abs(_2.width - _1.width)
120+
total_distance += self.height * abs(_2.height - _1.height)
121+
total_distance += self.z_depth * abs(_2.z_depth - _1.z_depth)
122+
total_distance += self.absolute_size * abs(_2.absolute_size - _1.absolute_size)
123+
if self.max_permitted_modules is not None:
124+
total_distance += self.size * \
125+
abs(_2.absolute_size - _1.absolute_size) / self.max_permitted_modules
126+
total_distance += self.sensors * abs(_2.sensors - _1.sensors)
127+
total_distance += self.symmetry * abs(_2.symmetry - _1.symmetry)
128+
total_distance += self.hinge_count * abs(_2.hinge_count - _1.hinge_count)
129+
total_distance += self.brick_count * abs(_2.brick_count - _1.brick_count)
130+
total_distance += self.brick_sensor_count * abs(_2.brick_sensor_count - _1.brick_sensor_count)
131+
total_distance += self.touch_sensor_count * abs(_2.touch_sensor_count - _1.touch_sensor_count)
132+
total_distance += self.free_slots * abs(_2.free_slots - _1.free_slots)
133+
total_distance += self.height_base_ratio * abs(_2.height_base_ratio - _1.height_base_ratio)
134+
total_distance += self.symmetry_vertical * abs(_2.symmetry_vertical - _1.symmetry_vertical)
135+
total_distance += self.base_density * abs(_2.base_density - _1.base_density)
136+
total_distance += self.bottom_layer * abs(_2.bottom_layer - _1.bottom_layer)
137+
138+
return total_distance <= self.total_threshold

experiments/brain-speciation/manager.py

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#!/usr/bin/env python3
2+
from __future__ import annotations
3+
24
from pyrevolve import parser
35
from pyrevolve.evolution import fitness
4-
from pyrevolve.evolution.selection import multiple_selection, tournament_selection
6+
from pyrevolve.evolution.selection import multiple_selection_with_duplicates, tournament_selection
57
from pyrevolve.evolution.speciation.population_speciated import PopulationSpeciated
68
from pyrevolve.evolution.speciation.population_speciated_config import PopulationSpeciatedConfig
79
from pyrevolve.evolution.speciation.population_speciated_management import steady_state_speciated_population_management
@@ -11,13 +13,17 @@
1113
from pyrevolve.genotype.lsystem_neat.mutation import LSystemNeatMutationConf as lMutationConfig
1214
from pyrevolve.genotype.plasticoding.mutation.mutation import MutationConfig as plasticMutationConfig
1315
from pyrevolve.genotype.lsystem_neat.mutation import standard_mutation as lmutation
14-
1516
from pyrevolve.util.supervisor.analyzer_queue import AnalyzerQueue
1617
from pyrevolve.util.supervisor.simulator_queue import SimulatorQueue
1718
from pyrevolve.custom_logging.logger import logger
1819
from pyrevolve.genotype.plasticoding import PlasticodingConfig
1920
from pyrevolve.genotype.lsystem_neat.lsystem_neat_genotype import LSystemCPGHyperNEATGenotype, LSystemCPGHyperNEATGenotypeConfig
2021
from pyrevolve.genotype.neat_brain_genome.neat_brain_genome import NeatBrainGenomeConfig
22+
from .MorphologyCompatibility import MorphologyCompatibility
23+
24+
from typing import TYPE_CHECKING
25+
if TYPE_CHECKING:
26+
from pyrevolve.evolution.individual import Individual
2127

2228

2329
async def run():
@@ -31,7 +37,7 @@ async def run():
3137
offspring_size = 50
3238

3339
body_conf = PlasticodingConfig(
34-
max_structural_modules=20, # TODO increase
40+
max_structural_modules=20,
3541
allow_vertical_brick=False,
3642
use_movement_commands=True,
3743
use_rotation_commands=False,
@@ -67,17 +73,32 @@ async def run():
6773
crossover_conf = lCrossoverConfig(
6874
crossover_prob=0.8,
6975
)
76+
77+
compatibitity_tester = MorphologyCompatibility(
78+
total_threshold=1.0,
79+
size=1.0,
80+
brick_count=1.0,
81+
proportion=1.0,
82+
coverage=1.0,
83+
joints=1.5,
84+
branching=1.0,
85+
symmetry=0.0,
86+
max_permitted_modules=body_conf.max_structural_modules,
87+
)
88+
7089
# experiment params #
7190

7291
# Parse command line / file input arguments
7392
args = parser.parse_args()
7493
experiment_management = ExperimentManagement(args)
94+
has_offspring = False
7595
do_recovery = args.recovery_enabled and not experiment_management.experiment_is_new()
7696

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

7999
if do_recovery:
80-
gen_num, has_offspring, next_robot_id, next_species_id = experiment_management.read_recovery_state(population_size, offspring_size)
100+
gen_num, has_offspring, next_robot_id, next_species_id = \
101+
experiment_management.read_recovery_state(population_size, offspring_size, species=True)
81102

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

100-
def are_genomes_compatible_fn(genotype1: LSystemCPGHyperNEATGenotype,
101-
genotype2: LSystemCPGHyperNEATGenotype) -> bool:
102-
return genotype1.is_brain_compatible(genotype2, genotype_conf)
121+
def are_individuals_brains_compatible_fn(individual1: Individual,
122+
individual2: Individual) -> bool:
123+
assert isinstance(individual1.genotype, LSystemCPGHyperNEATGenotype)
124+
assert isinstance(individual2.genotype, LSystemCPGHyperNEATGenotype)
125+
return individual1.genotype.is_brain_compatible(individual2.genotype, genotype_conf)
126+
127+
def are_individuals_morphologies_compatible_fn(individual1: Individual,
128+
individual2: Individual) -> bool:
129+
return compatibitity_tester.compatible_individuals(individual1, individual2)
103130

104131
population_conf = PopulationSpeciatedConfig(
105132
population_size=population_size,
@@ -111,18 +138,19 @@ def are_genomes_compatible_fn(genotype1: LSystemCPGHyperNEATGenotype,
111138
crossover_operator=lcrossover,
112139
crossover_conf=crossover_conf,
113140
selection=lambda individuals: tournament_selection(individuals, 2),
114-
parent_selection=lambda individuals: multiple_selection(individuals, 2, tournament_selection),
141+
parent_selection=lambda individuals: multiple_selection_with_duplicates(individuals, 2, tournament_selection),
115142
population_management=steady_state_speciated_population_management,
116143
population_management_selector=tournament_selection,
117144
evaluation_time=args.evaluation_time,
118145
offspring_size=offspring_size,
119146
experiment_name=args.experiment_name,
120147
experiment_management=experiment_management,
121148
# species stuff
122-
are_genomes_compatible_fn=are_genomes_compatible_fn,
149+
# are_individuals_compatible_fn=are_individuals_brains_compatible_fn,
150+
are_individuals_compatible_fn=are_individuals_morphologies_compatible_fn,
123151
young_age_threshold=5,
124152
young_age_fitness_boost=2.0,
125-
old_age_threshold=20,
153+
old_age_threshold=35,
126154
old_age_fitness_penalty=0.5,
127155
species_max_stagnation=30,
128156
)
@@ -142,12 +170,16 @@ def are_genomes_compatible_fn(genotype1: LSystemCPGHyperNEATGenotype,
142170
next_species_id)
143171

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

178+
# TODO partial recovery is not implemented, this is a substitute
179+
has_offspring = False
180+
next_robot_id = 1 + population.config.population_size + gen_num * population.config.offspring_size
181+
population.next_robot_id = next_robot_id
182+
151183
if has_offspring:
152184
raise NotImplementedError('partial recovery not implemented')
153185
recovered_individuals = population.load_partially_completed_generation(gen_num, population_size, offspring_size, next_robot_id)

pyrevolve/evolution/individual.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import Optional, List
77
from pyrevolve.revolve_bot import RevolveBot
88
from pyrevolve.genotype import Genotype
9+
from pyrevolve.evolution.speciation.species import Species
910

1011

1112
class Individual:
@@ -38,24 +39,41 @@ def id(self) -> int:
3839
_id = self.genotype.id
3940
return _id
4041

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

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

49-
def export_fitness(self, folder) -> None:
50+
def export_phylogenetic_info(self, folder: str) -> None:
51+
"""
52+
Export phylogenetic information
53+
(parents and other possibly other information to build a phylogenetic tree)
54+
:param folder: folder where to save the information
55+
"""
56+
if self.parents is not None:
57+
parents_ids: List[str] = [str(p.id) for p in self.parents]
58+
parents_ids_str = ",".join(parents_ids)
59+
else:
60+
parents_ids_str = 'None'
61+
62+
filename = os.path.join(folder, f'parents_{self.id}.yaml')
63+
with open(filename, 'w') as file:
64+
file.write(f'parents:{parents_ids_str}')
65+
66+
def export_fitness_single_file(self, folder: str) -> None:
5067
"""
5168
It's saving the fitness into a file. The fitness can be a floating point number or None
5269
:param folder: folder where to save the fitness
5370
"""
5471
with open(os.path.join(folder, f'fitness_{self.id}.txt'), 'w') as f:
5572
f.write(str(self.fitness))
5673

57-
def export(self, folder) -> None:
74+
def export(self, folder: str) -> None:
5875
self.export_genotype(folder)
76+
self.export_phylogenetic_info(folder)
5977
self.export_phenotype(folder)
6078
self.export_fitness(folder)
6179

pyrevolve/evolution/population/population.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
import os
44

55
from pyrevolve.evolution.individual import Individual
6-
from pyrevolve.tol.manage import measures
76
from pyrevolve.custom_logging.logger import logger
87
from pyrevolve.evolution.population.population_config import PopulationConfig
98

109
from typing import TYPE_CHECKING
1110
if TYPE_CHECKING:
12-
from typing import List, Optional, AnyStr
11+
from typing import List, Optional
12+
from pyrevolve.evolution.speciation.species import Species
1313
from pyrevolve.tol.manage.measures import BehaviouralMeasurements
1414
from pyrevolve.util.supervisor.analyzer_queue import AnalyzerQueue, SimulatorQueue
1515

@@ -44,10 +44,15 @@ def __init__(self,
4444
self.simulator_queue = simulator_queue
4545
self.next_robot_id = next_robot_id
4646

47-
def _new_individual(self, genotype):
47+
def _new_individual(self,
48+
genotype,
49+
parents: Optional[List[Individual]] = None):
4850
individual = Individual(genotype)
4951
individual.develop()
5052
individual.phenotype.update_substrate()
53+
if parents is not None:
54+
individual.parents = parents
55+
5156
self.config.experiment_management.export_genotype(individual)
5257
self.config.experiment_management.export_phenotype(individual)
5358
self.config.experiment_management.export_phenotype_images(individual)

pyrevolve/evolution/selection.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ def tournament_selection(population: List[Individual], k=2) -> Individual:
3232

3333
def multiple_selection(population: List[Individual],
3434
selection_size: int,
35-
selection_function: Callable[[List[Individual]], Individual]) -> List[Individual]:
35+
selection_function: Callable[[List[Individual]], Individual]
36+
) -> List[Individual]:
3637
"""
37-
Perform selection on population of distinct group, can be used in the form parent selection or survival selection
38+
Perform selection on population of distinct group, it can be used in the
39+
form parent selection or survival selection.
40+
It never selects the same individual more than once
3841
:param population: list of individuals where to select from
3942
:param selection_size: amount of individuals to select
4043
:param selection_function:
@@ -49,3 +52,22 @@ def multiple_selection(population: List[Individual],
4952
selected_individuals.append(selected_individual)
5053
new_individual = True
5154
return selected_individuals
55+
56+
57+
def multiple_selection_with_duplicates(population: List[Individual],
58+
selection_size: int,
59+
selection_function: Callable[[List[Individual]], Individual]
60+
) -> List[Individual]:
61+
"""
62+
Perform selection on population of distinct group, it can be used in the
63+
form parent selection or survival selection.
64+
It can select the same individual more than once
65+
:param population: list of individuals where to select from
66+
:param selection_size: amount of individuals to select
67+
:param selection_function:
68+
"""
69+
selected_individuals = []
70+
for _ in range(selection_size):
71+
selected_individual = selection_function(population)
72+
selected_individuals.append(selected_individual)
73+
return selected_individuals

0 commit comments

Comments
 (0)