diff --git a/README.md b/README.md index c16a793..f4d7024 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![License: Apache 2.0](https://img.shields.io/badge/license-apache--2.0-green.svg)](https://opensource.org/licenses/Apache-2.0) [![Status: Actrive](https://img.shields.io/badge/status-active-brightgreen.svg)](https://github.com/cad-polito-it/byron) ![Language: Python](https://img.shields.io/badge/language-python-blue.svg) -![Version: 0.1.dev2](https://img.shields.io/badge/version-0.8a1.dev15-orange.svg) +![Version: 0.1.dev2](https://img.shields.io/badge/version-0.8a1.dev16-orange.svg) ![Codename: Don Juan](https://img.shields.io/badge/codename-Don_Juan-pink.svg) [![Documentation Status](https://readthedocs.org/projects/byron/badge/?version=pre-alpha)](https://byron.readthedocs.io/en/pre-alpha/?badge=pre-alpha) diff --git a/byron/classes/individual.py b/byron/classes/individual.py index e6a4d44..079dd0d 100644 --- a/byron/classes/individual.py +++ b/byron/classes/individual.py @@ -248,7 +248,7 @@ def fitness(self, value: FitnessABC) -> None: value << i.fitness or not value.is_distinguishable(i.fitness) for i in self.lineage.parents ): self._lineage.operator.stats.failures += 1 - logger.debug(f"Individual: Fitness of {self} is {value}") + logger.debug(f"Individual: Fitness of {self}/{self.lineage} is {value}") @property def as_message(self) -> list[int]: diff --git a/byron/classes/selement.py b/byron/classes/selement.py index 5f38be9..693d10a 100644 --- a/byron/classes/selement.py +++ b/byron/classes/selement.py @@ -177,7 +177,7 @@ def _is_valid_debug(self, node_ref: 'NodeReference') -> None: check_result = True for f in self.__class__.NODE_CHECKS: if not f(node_ref): - logger.info(f"{PARANOIA_SYSTEM_ERROR}: Failed check on {node_ref} ({f})") + logger.info(f"is_valid: Failed check on {node_ref}: {f}") check_result = False return check_result diff --git a/byron/functions.py b/byron/functions.py index 160854a..ec7b1dc 100644 --- a/byron/functions.py +++ b/byron/functions.py @@ -58,7 +58,7 @@ def mutate(parameter: ParameterABC, /, strength: float) -> None: or counter < 100 or strength < 0.01 or performance_warning( - f"Failed to mutate {parameter.__class__.__name__} with strength {strength} ({counter:,} failed attempts)", + f"Failed to mutate {parameter!r} with strength {strength} ({counter:,} failed attempts)", stacklevel_offset=2, ) ) diff --git a/byron/global_symbols.py b/byron/global_symbols.py index 1b811a7..3319166 100644 --- a/byron/global_symbols.py +++ b/byron/global_symbols.py @@ -64,7 +64,7 @@ from collections import defaultdict import multiprocessing -__version__ = "0.8a1.dev15" +__version__ = "0.8a1.dev16" __date__ = "25-08-2023" __codename__ = "Don Juan" __author__ = "Giovanni Squillero and Alberto Tonda" diff --git a/byron/operators/single_node_crossovers.py b/byron/operators/single_node_crossovers.py index a415ecb..713b100 100644 --- a/byron/operators/single_node_crossovers.py +++ b/byron/operators/single_node_crossovers.py @@ -27,6 +27,7 @@ # v1 / August 2023 / Squillero (GX) from copy import deepcopy +from collections import defaultdict from .ea_tools import * from byron.classes import * @@ -35,31 +36,68 @@ from byron.tools.graph import * from byron.user_messages import * -# unfussy vs. chosy +def _generic_node_crossover( + parent1: Individual, parent2: Individual, *, choosy: bool = False, only_targets: bool = False, link_type: str +): + # assert parent1.run_paranoia_checks() + # assert parent2.run_paranoia_checks() + + common_selements_raw = group_selements([parent1, parent2], only_targets=only_targets, choosy=choosy) + common_selements = defaultdict(lambda: defaultdict(list)) + for path, elements in common_selements_raw.items(): + if len(elements) < 2: + continue + for ind, nodes in elements.items(): + plausible_nodes = [ + n for n in nodes if link_type in set(t for u, v, t in ind.genome.in_edges(n, data='_type')) + ] + if not plausible_nodes: + continue + common_selements[path][ind] = plausible_nodes + if len(common_selements[path]) != 2: + del common_selements[path] -def _generic_node_crossover(parent1: Individual, parent2: Individual, link_type: str): - assert parent1.run_paranoia_checks() - assert parent2.run_paranoia_checks() - common_selements = {k: v for k, v in group_selements([parent1, parent2], only_targets=True).items() if len(v) == 2} if not common_selements: raise ByronOperatorFailure + target = rrandom.choice(list(common_selements.keys())) node1 = rrandom.choice(common_selements[target][parent1]) node2 = rrandom.choice(common_selements[target][parent2]) new_genome = nx.compose(deepcopy(parent1.genome), deepcopy(parent2.genome)) - node1_fanin = new_genome.in_edges(node1, data='_type', keys=True) - node2_fanin = new_genome.in_edges(node2, data='_type', keys=True) + node1_fanin = new_genome.in_edges(node1, data=True, keys=True) + node2_fanin = new_genome.in_edges(node2, data=True, keys=True) try: - node1_parent_link = rrandom.choice([(u, v, k, t) for u, v, k, t in node1_fanin if t == link_type]) - node2_parent_link = rrandom.choice([(u, v, k, t) for u, v, k, t in node2_fanin if t == node1_parent_link[3]]) + node1_parent_link = rrandom.choice([(u, v, k, d) for u, v, k, d in node1_fanin if d['_type'] == link_type]) + node2_parent_link = rrandom.choice( + [(u, v, k, d) for u, v, k, d in node2_fanin if d['_type'] == node1_parent_link[3]['_type']] + ) except: raise ByronOperatorFailure - new_genome.remove_edge(node1_parent_link[0], node1_parent_link[1], node1_parent_link[2]) - new_genome.remove_edge(node2_parent_link[0], node2_parent_link[1], node2_parent_link[2]) - new_genome.add_edge(node1_parent_link[0], node2_parent_link[1], node1_parent_link[2], _type=node1_parent_link[3]) - new_genome.add_edge(node2_parent_link[0], node1_parent_link[1], node2_parent_link[2], _type=node1_parent_link[3]) + # NOTE[GX]: replace link in node1_parent -> node1 with node1_parent -> node2 preserving links order + parent1_complete_fanout = list(new_genome.edges(node1_parent_link[0], data=True, keys=True)) + for edge in parent1_complete_fanout: + new_genome.remove_edge(edge[0], edge[1], key=edge[2]) + for edge in parent1_complete_fanout: + if edge == node1_parent_link: + new_genome.add_edge( + node1_parent_link[0], node2_parent_link[1], node1_parent_link[2], **node1_parent_link[3] + ) + else: + new_genome.add_edge(edge[0], edge[1], key=edge[2], **edge[3]) + # NOTE[GX]: replace link in node2_parent -> node2 with node2_parent -> node1 preserving links order + parent2_complete_fanout = list(new_genome.edges(node1_parent_link[0], data=True, keys=True)) + for edge in parent2_complete_fanout: + new_genome.remove_edge(edge[0], edge[1], key=edge[2]) + for edge in parent2_complete_fanout: + if edge == node2_parent_link: + new_genome.add_edge( + node2_parent_link[0], node1_parent_link[1], node2_parent_link[2], **node2_parent_link[3] + ) + else: + new_genome.add_edge(edge[0], edge[1], key=edge[2], **edge[3]) + discard_useless_components(new_genome) if not get_structure_tree(new_genome): @@ -67,18 +105,23 @@ def _generic_node_crossover(parent1: Individual, parent2: Individual, link_type: Node.reset_labels(new_genome) new_individual = Individual(parent1.top_frame, new_genome) - assert new_individual.run_paranoia_checks() + # assert new_individual.run_paranoia_checks() - assert parent1.run_paranoia_checks() - assert parent2.run_paranoia_checks() + # assert parent1.run_paranoia_checks() + # assert parent2.run_paranoia_checks() return [new_individual] -@genetic_operator(num_parents=2) +# @genetic_operator(num_parents=2) def linked_node_crossover(parent1: Individual, parent2: Individual): - return _generic_node_crossover(parent1, parent2, LINK) + return _generic_node_crossover(parent1, parent2, only_targets=True, link_type=LINK) + + +@genetic_operator(num_parents=2) +def leaf_crossover_unfussy(parent1: Individual, parent2: Individual): + return _generic_node_crossover(parent1, parent2, choosy=False, only_targets=False, link_type=FRAMEWORK) @genetic_operator(num_parents=2) -def leaf_crossover(parent1: Individual, parent2: Individual): - return _generic_node_crossover(parent1, parent2, FRAMEWORK) +def leaf_crossover_choosy(parent1: Individual, parent2: Individual): + return _generic_node_crossover(parent1, parent2, choosy=True, only_targets=False, link_type=FRAMEWORK) diff --git a/byron/tools/graph.py b/byron/tools/graph.py index 7530603..33074d5 100644 --- a/byron/tools/graph.py +++ b/byron/tools/graph.py @@ -44,6 +44,7 @@ import networkx as nx from byron.global_symbols import * +from byron.user_messages import * from byron.classes.node_reference import NodeReference from byron.classes.parameter import ParameterABC, ParameterStructuralABC from byron.classes.selement import * diff --git a/examples/onemax/onemax-go/golang.py b/examples/onemax/onemax-go/golang.py index 090a2fd..a633663 100755 --- a/examples/onemax/onemax-go/golang.py +++ b/examples/onemax/onemax-go/golang.py @@ -24,7 +24,7 @@ def framework(): int64 = byron.f.integer_parameter(0, 2**64) math_op = byron.f.choice_parameter(['+', '-', '*', '/', '&', '^', '|']) variable = byron.f.macro('var {_node} uint64', _label='') - variable.force_parent('prologue') + # variable.force_parent('prologue') # standard main_prologue = byron.f.macro('package main\nfunc evolved_function() uint64 {{') @@ -50,7 +50,7 @@ def framework(): subs_prologue = byron.f.macro('func {_node}(foo uint64) uint64 {{', _label='') subs_math = byron.f.macro('foo {op}= {num}', op=math_op, num=int64) subs_epilogue = byron.f.macro('return foo\n}}') - subs = byron.f.sequence([subs_prologue, byron.f.bunch([subs_math], size=2), subs_epilogue], name='function') + subs = byron.f.sequence([subs_prologue, byron.f.bunch([subs_math], size=(2, 10)), subs_epilogue], name='function') call = byron.f.macro( '{var1} = {sub}({var2})', @@ -59,5 +59,8 @@ def framework(): var2=byron.f.global_reference(variable), ) - code = byron.f.bunch([imath, vmath, call], size=5, name='body') - return byron.f.sequence((main_prologue, code, main_epilogue), max_instances=1) + few_default_vars = byron.f.bunch([variable], size=2) + # NOTE[GX]: Force all newly created variables to appear after the few default ones + variable.force_parent(few_default_vars) + code = byron.f.bunch([imath, vmath, call], weights=(3, 3, 1), size=(2, 20), name='body') + return byron.f.sequence((main_prologue, few_default_vars, code, main_epilogue), max_instances=1) diff --git a/examples/onemax/onemax-go/onemax-go.py b/examples/onemax/onemax-go/onemax-go.py index 834a595..92ca68b 100755 --- a/examples/onemax/onemax-go/onemax-go.py +++ b/examples/onemax/onemax-go/onemax-go.py @@ -20,23 +20,23 @@ @byron.fitness_function def dummy_fitness(text): - return len(text) + return 1 / len(text) def main(): top_frame = golang.framework() + # evaluator = byron.evaluator.ScriptEvaluator('./evaluate-all.sh', filename_format="individual{i:06}.go") + evaluator = byron.evaluator.ParallelScriptEvaluator( + 'go', 'onemax.go', other_required_files=('main.go',), flags=('run',), timeout=10, default_result='-1' + ) # evaluator = byron.evaluator.PythonEvaluator(dummy_fitness) - evaluator = byron.evaluator.ScriptEvaluator('./evaluate-all.sh', filename_format="individual{i:06}.go") - # evaluator = byron.evaluator.ParallelScriptEvaluator( - # 'go', 'onemax.go', other_required_files=('main.go',), flags=('run',), timeout=10, default_result='-1' - # ) byron.f.set_option('$dump_node_info', True) final_population = byron.ea.vanilla_ea( top_frame, evaluator, - max_generation=100, + max_generation=1_000, mu=50, lambda_=20, max_fitness=64.0, diff --git a/pyproject.toml b/pyproject.toml index 592e10b..67c060e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ [tool.poetry] name = "byron" -version = "0.8a1.dev15" +version = "0.8a1.dev16" description = "Multi-purpose extensible self-adaptive optimizer and fuzzer" authors = [ "Giovanni Squillero ", @@ -96,7 +96,7 @@ max-line-length = 120 source-roots = ['src'] [bumpver] -current_version = "0.8a1.dev15" +current_version = "0.8a1.dev16" version_pattern = "MAJOR.MINOR[PYTAGNUM].devINC0" commit_message = "Bump version to {new_version}" commit = false