Skip to content

Commit

Permalink
Add an (almost) working xover
Browse files Browse the repository at this point in the history
  • Loading branch information
squillero committed Aug 24, 2023
1 parent d3e7e13 commit efdf811
Show file tree
Hide file tree
Showing 35 changed files with 663 additions and 150 deletions.
1 change: 1 addition & 0 deletions byron/classes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from .individual import *
from .macro import *
from .monitor import *
from .node import *
from .node_reference import *
from .node_view import *
from .parameter import *
Expand Down
8 changes: 0 additions & 8 deletions byron/classes/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@


class FrameABC(SElement, Paranoid):
_registered_names = set()

def __init__(self):
super().__init__()
self._checks = list()
Expand All @@ -63,12 +61,6 @@ def run_paranoia_checks(self) -> bool:
def name(cls):
return cls.__name__

@staticmethod
def register_name(name: str) -> bool:
assert name not in FrameABC._registered_names, f"{PARANOIA_VALUE_ERROR}: Frame name {name!r} already exists."
FrameABC._registered_names.add(name)
return True

@property
def shannon(self) -> list[int]:
return [hash(self.__class__)]
Expand Down
60 changes: 31 additions & 29 deletions byron/classes/individual.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,15 @@
from byron.classes.byron import Byron
from byron.classes.dump import *
from byron.classes.fitness import FitnessABC
from byron.classes.paranoid import Paranoid
from byron.classes.value_bag import ValueBag
from byron.classes.frame import FrameABC
from byron.classes.macro import Macro
from byron.classes.node import *
from byron.classes.node_reference import NodeReference
from byron.classes.node_view import NodeView
from byron.classes.macro import Macro
from byron.classes.frame import FrameABC
from byron.classes.parameter import ParameterABC, ParameterStructuralABC
from byron.classes.paranoid import Paranoid
from byron.classes.readymade_macros import MacroZero
from byron.classes.value_bag import ValueBag


@dataclass(frozen=True, slots=True)
Expand Down Expand Up @@ -107,7 +108,7 @@ class Individual(Paranoid):
Individuals are managed by a `Population` class.
"""

__COUNTER: int = 0
__LAST_BYRON_INDIVIDUAL: int = 0

_genome: nx.classes.MultiDiGraph
_fitness: FitnessABC | None
Expand All @@ -119,11 +120,14 @@ class Individual(Paranoid):

from ._individual_as import as_forest, as_lgp, _draw_forest, _draw_multipartite

def __init__(self, top_frame: type[FrameABC]) -> None:
Individual.__COUNTER += 1
self._id = Individual.__COUNTER
self._genome = nx.MultiDiGraph(node_count=NODE_ZERO + 1, top_frame=top_frame)
self._genome.add_node(NODE_ZERO, _selement=MacroZero(), _type=MACRO_NODE)
def __init__(self, top_frame: type[FrameABC], genome: nx.MultiDiGraph | None = None) -> None:
Individual.__LAST_BYRON_INDIVIDUAL += 1
self._id = Individual.__LAST_BYRON_INDIVIDUAL
if genome:
self._genome = genome
else:
self._genome = nx.MultiDiGraph(top_frame=top_frame)
self._genome.add_node(NODE_ZERO, _selement=MacroZero(), _type=MACRO_NODE)
self._fitness = None
self._str = ''
self._lineage = None
Expand Down Expand Up @@ -161,20 +165,20 @@ def is_finalized(self) -> bool:
@property
def valid(self) -> bool:
return all(
self.genome.nodes[n]["_selement"].is_valid(NodeView(NodeReference(self._genome, n))) for n in self._genome
self.genome.nodes[n]['_selement'].is_valid(NodeView(NodeReference(self._genome, n))) for n in self._genome
)

@property
def clone(self) -> "Individual":
def clone(self) -> 'Individual':
scratch = self._fitness, self._lineage
self._fitness, self._lineage = None, None
I = deepcopy(self)
I.__class__ = Individual
Individual.__COUNTER += 1
I._id = Individual.__COUNTER
Individual.__LAST_BYRON_INDIVIDUAL += 1 # TODO: [GX] Use a custom baseclass!
I._id = Individual.__LAST_BYRON_INDIVIDUAL
self._fitness, self._lineage = scratch
I._age = Age()
I._lineage = Lineage(None, (self,))
Node.reset_labels(I.genome)
return I

@property
Expand All @@ -198,6 +202,13 @@ def genome(self) -> nx.classes.MultiDiGraph:
# TODO: Add paranoia check?
return self._genome

@genome.setter
def genome(self, new_genome: nx.classes.MultiDiGraph):
"""Set the new Individual's genome (ie. the underlying NetworkX MultiDiGraph)."""
self._genome = new_genome
self._fitness = None
assert self.run_paranoia_checks()

@property
def lineage(self):
return self._lineage
Expand Down Expand Up @@ -301,27 +312,22 @@ def _parameters(self) -> list[ParameterABC]:
@property
def structure_tree(self) -> nx.classes.DiGraph:
"""A tree with the structure tree of the individual (ie. only edges of `kind=FRAMEWORK`)."""
if self.is_finalized:
return self._cached_structure_tree
return self._structure_tree()

@cached_property
def _cached_structure_tree(self) -> nx.classes.DiGraph:
# TODO: [GX] Understand why caching doesn't work with nx...
return self._structure_tree()

def _structure_tree(self) -> nx.classes.DiGraph:
tree = nx.DiGraph()
tree.add_nodes_from(self._genome.nodes)
tree.add_edges_from((u, v) for u, v, k in self._genome.edges(data="_type") if k == FRAMEWORK)
assert nx.is_branching(tree) and nx.is_weakly_connected(
tree
), f"{PARANOIA_VALUE_ERROR}: Structure of {self!r} is not a valid tree"
def _structure_tree(self) -> nx.DiGraph:
tree = get_structure_tree(self._genome)
assert tree, f"{PARANOIA_VALUE_ERROR}: Structure of {self!r} is not a valid tree"
return tree

#######################################################################
# PUBLIC METHODS

def run_paranoia_checks(self) -> bool:
logger.debug(f"run_paranoia_checks: Checking {self}")
assert self.valid, f"{PARANOIA_VALUE_ERROR}: Individual {self!r} is not valid"

# ==[check genome (structural)]======================================
Expand Down Expand Up @@ -364,10 +370,6 @@ def run_paranoia_checks(self) -> bool:
assert len(tree_edges) == len(set(tree_edges)), "ValueError (paranoia check): duplicated framework edge"

# ==[check nodes (semantic)]=========================================
assert all(
n < self._genome.graph["node_count"] for n in self._genome
), f"{PARANOIA_VALUE_ERROR}: Invalid 'node_count' attribute ({self._genome.graph['node_count']})"

assert all(
'_selement' in d for n, d in self._genome.nodes(data=True)
), f"{PARANOIA_VALUE_ERROR}: Missing '_selement'"
Expand Down
53 changes: 53 additions & 0 deletions byron/classes/node.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
#################################|###|#####################################
# __ | | #
# | |--.--.--.----.-----.-----. |===| This file is part of Byron v0.8 #
# | _ | | | _| _ | | |___| An evolutionary optimizer & fuzzer #
# |_____|___ |__| |_____|__|__| ).( https://github.com/squillero/byron #
# |_____| \|/ #
################################## ' ######################################

# Copyright 2023 Giovanni Squillero and Alberto Tonda
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
# See the License for the specific language governing permissions and
# limitations under the License.

# =[ HISTORY ]===============================================================
# v1 / August 2023 / Squillero (GX)

__all__ = ['Node']

import networkx as nx

from byron.tools.graph import fasten_subtree_parameters
from byron.global_symbols import NODE_ZERO
from byron.classes.node_reference import NodeReference


class Node(int):
r"""Simple helper to guarantee Node ids uniqueness"""
__slots__ = []
__LAST_BYRON_NODE = 0

def __new__(cls, node_id: int or None = None):
if node_id is not None:
return int.__new__(cls, node_id)
Node.__LAST_BYRON_NODE += 1
return int.__new__(cls, Node.__LAST_BYRON_NODE)

@staticmethod
def reset_labels(G: nx.Graph) -> None:
"""Set Graph node labels to unique numbers"""
new_labels = {k: Node() for k in G.nodes if k != NODE_ZERO}
nx.relabel_nodes(G, new_labels, copy=False)
fasten_subtree_parameters(NodeReference(G, NODE_ZERO))
2 changes: 1 addition & 1 deletion byron/classes/node_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def p(self) -> ValueBag:
)

@cached_property
def structure_tree(self) -> nx.DiGraph:
def tree(self) -> nx.DiGraph:
tree = nx.DiGraph()
tree.add_nodes_from(self.ref.graph.nodes)
tree.add_edges_from((u, v) for u, v, k in self.ref.graph.edges(data="_type") if k == FRAMEWORK)
Expand Down
4 changes: 0 additions & 4 deletions byron/classes/population.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,6 @@ def __init__(self, top_frame: type[SElement], extra_parameters: dict | None = No
else:
self._memory = None

# def get_new_node(self) -> int:
# self._node_count += 1
# return self._node_count

@property
def top_frame(self):
return self._top_frame
Expand Down
6 changes: 3 additions & 3 deletions byron/classes/selement.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,11 @@ def is_valid(self, node: Optional['NodeReference'] = None) -> bool:
r"""Checks the validity of a `NodeReference` and internal attributes"""
return all(f(node) for f in self.__class__.NODE_CHECKS)

def _is_valid_debug(self, node: 'NodeReference') -> None:
def _is_valid_debug(self, node_ref: 'NodeReference') -> None:
check_result = True
for f in self.__class__.NODE_CHECKS:
if not f(node):
logger.info(f"NodeChecks: Failed check on genome 0x{id(node.genome):x}: {f.__qualname__}({node})")
if not f(node_ref):
logger.info(f"{PARANOIA_SYSTEM_ERROR}: Failed check {f.__qualname__}({node_ref})")
check_result = False
return check_result

Expand Down
46 changes: 40 additions & 6 deletions byron/framework/framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,26 @@

from collections import abc
from typing import Sequence
from functools import partial

from byron.global_symbols import *
from byron.user_messages import *
from byron.classes.selement import SElement
from byron.classes.frame import *
from byron.classes.macro import Macro
from byron.classes.node_reference import NodeReference
from byron.framework.macro import macro
from byron.framework.utilities import cook_selement_list
from byron.randy import rrandom


def alternative(
alternatives: abc.Collection[type[SElement]], *, name: str | None = None, extra_parameters: dict = None, **kwargs
alternatives: abc.Collection[type[SElement]],
*,
name: str | None = None,
max_instances: int | None = None,
extra_parameters: dict = None,
**kwargs,
) -> type[FrameABC]:
r"""Creates the class for a frame that can have alternative forms.
Expand All @@ -61,6 +68,8 @@ def alternative(
the possible alternatives.
name : str, optional
the name of the frame.
max_instances : int, optional
maximum number of instances of the given frame
extra_parameters : dict, optional
dictionary of parameters.
Expand Down Expand Up @@ -99,6 +108,7 @@ def alternative(
class T(FrameAlternative, FrameABC):
ALTERNATIVES = tuple(cooked_alternatives)
EXTRA_PARAMETERS = dict(extra_parameters) if extra_parameters else dict()
MAX_INSTANCES = max_instances

def __init__(self):
super().__init__()
Expand All @@ -107,6 +117,9 @@ def __init__(self):
def successors(self):
return [rrandom.choice(T.ALTERNATIVES)]

if max_instances:
T.add_node_check(partial(_check_instance_number, max_instances=max_instances))

if name:
T._patch_info(custom_class_id=name)
else:
Expand All @@ -116,13 +129,19 @@ def successors(self):


def sequence(
seq: abc.Sequence[type[SElement] | str], *, name: str | None = None, extra_parameters: dict = None, **kwargs
seq: abc.Sequence[type[SElement] | str],
*,
name: str | None = None,
max_instances: int | None = None,
extra_parameters: dict = None,
**kwargs,
) -> type[FrameABC]:
cooked_seq = cook_selement_list(seq)

class T(FrameSequence, FrameABC):
SEQUENCE = tuple(cooked_seq)
EXTRA_PARAMETERS = dict(extra_parameters) if extra_parameters else dict()
MAX_INSTANCES = max_instances

def __init__(self):
super().__init__()
Expand All @@ -131,6 +150,9 @@ def __init__(self):
def successors(self):
return T.SEQUENCE

if max_instances:
T.add_node_check(partial(_check_instance_number, max_instances=max_instances))

if name:
T._patch_info(custom_class_id=name)
elif len(cooked_seq) == 1:
Expand All @@ -146,6 +168,7 @@ def bunch(
size: tuple[int, int] | int = 1,
*,
name: str | None = None,
max_instances: int | None = None,
weights: Sequence[int] | None = None,
extra_parameters: dict = None,
**kwargs,
Expand Down Expand Up @@ -192,6 +215,7 @@ class T(FrameMacroBunch, FrameABC):
SIZE = size
POOL = tuple(sum(([m] * w for m, w in zip(pool, weights)), start=list()))
EXTRA_PARAMETERS = dict(extra_parameters) if extra_parameters else dict()
MAX_INSTANCES = max_instances

__slots__ = [] # Preventing the automatic creation of __dict__

Expand All @@ -203,10 +227,9 @@ def successors(self):
n_macros = rrandom.random_int(T.SIZE[0], T.SIZE[1])
return [rrandom.choice(T.POOL) for _ in range(n_macros)]

def check_out_degree(nr):
return nr.out_degree >= size[0] and nr.out_degree < size[1]

T.add_node_check(check_out_degree)
T.add_node_check(partial(_check_out_degree, min_=size[0], max_=size[1]))
if max_instances:
T.add_node_check(partial(_check_instance_number, max_instances=max_instances))

# White parentheses: ⦅ ⦆ (U+2985, U+2986)
if name:
Expand All @@ -219,3 +242,14 @@ def check_out_degree(nr):
T._patch_info(name='MacroBunch#')

return T


def _check_instance_number(node_ref: NodeReference, max_instances: int):
return (
len([se for n, se in node_ref.graph.nodes(data='_selement') if type(se) == type(node_ref.selement)])
<= max_instances
)


def _check_out_degree(node_ref: NodeReference, min_: int, max_: int):
return min_ <= node_ref.out_degree < max_
Loading

0 comments on commit efdf811

Please sign in to comment.