diff --git a/pyshgp/gp/estimators.py b/pyshgp/gp/estimators.py index ae98dbd..9364ef6 100644 --- a/pyshgp/gp/estimators.py +++ b/pyshgp/gp/estimators.py @@ -8,7 +8,9 @@ import pyshgp.gp.variation as vr from pyshgp.gp.evaluation import DatasetEvaluator from pyshgp.gp.genome import GeneSpawner -from pyshgp.push.interpreter import PushInterpreter, DEFAULT_INTERPRETER, PushConfig, ProgramSignature +from pyshgp.push.interpreter import PushInterpreter, DEFAULT_INTERPRETER +from pyshgp.push.config import PushConfig +from pyshgp.push.program import ProgramSignature from pyshgp.utils import list_rindex from pyshgp.validation import check_is_fitted, check_X_y from pyshgp.monitoring import DEFAULT_VERBOSITY_LEVELS @@ -89,6 +91,12 @@ def __init__(self, self.verbose = verbose self.ext = kwargs + # Initialize attributes that will be set later. + self.evaluator = None + self.signature = None + self.search = None + self.solution = None + self.verbosity_config = DEFAULT_VERBOSITY_LEVELS[self.verbose] self.verbosity_config.update_log_level() @@ -108,8 +116,6 @@ def _build_search_algo(self): var_strat = vr.VariationStrategy() for op_name, prob in self.variation_strategy.items(): var_op = vr.get_variation_operator(op_name) - if not isinstance(var_op, vr.VariationOperator): - var_op = self._build_component(var_op) var_strat.add(var_op, prob) self.variation_strategy = var_strat @@ -129,6 +135,9 @@ def _build_search_algo(self): ) self.search = sr.get_search_algo(self._search_name, config=search_config, **self.ext) + def is_initialized(self) -> bool: + return self.search is not None + def fit(self, X, y): """Run the search algorithm to synthesize a push program. diff --git a/pyshgp/gp/evaluation.py b/pyshgp/gp/evaluation.py index d718596..87baaea 100644 --- a/pyshgp/gp/evaluation.py +++ b/pyshgp/gp/evaluation.py @@ -66,7 +66,7 @@ class Evaluator(ABC): When a program's output cannot be evaluated on a particular case, the penalty error is assigned. Default is 5e5. verbosity_config : Optional[VerbosityConfig] (default = None) - A VerbosityConfig controling what is logged during evaluation. + A VerbosityConfig controlling what is logged during evaluation. Default is no verbosity. """ @@ -83,7 +83,7 @@ def __init__(self, def default_error_function(self, actuals, expecteds) -> np.array: """Produce errors of actual program output given expected program output. - The default error function is intented to be a universal error function + The default error function is intended to be a universal error function for Push programs which only output a subset of the standard data types. Parameters diff --git a/pyshgp/gp/genome.py b/pyshgp/gp/genome.py index 20fdc88..7eace84 100644 --- a/pyshgp/gp/genome.py +++ b/pyshgp/gp/genome.py @@ -1,124 +1,84 @@ -"""The :mod:`genome` module defines classes related to Genomesself. +from __future__ import annotations -The ``Genome`` class defines Genomes as flat, linear representations of Push -programs. The ``GenomeSpawner`` class is a factory of random genes (``Atoms``) -and random ``Genomes``. - -""" -from collections import MutableSequence -from typing import Callable, Sequence, Union, Tuple, Optional, Any +from enum import Enum +from typing import Sequence, Union, Any, Callable, Optional, Tuple import numpy as np +from pyrsistent import PRecord, field, CheckedPVector -from pyshgp.push.interpreter import ProgramSignature, Program -from pyshgp.push.type_library import infer_literal -from pyshgp.push.atoms import Atom, Closer, Literal, Instruction, CodeBlock -from pyshgp.push.instruction_set import InstructionSet from pyshgp.gp.evaluation import Evaluator -from pyshgp.utils import DiscreteProbDistrib, Saveable, Copyable from pyshgp.monitoring import VerbosityConfig, DEFAULT_VERBOSITY_LEVELS, log +from pyshgp.push import InstructionSet, ProgramSignature, Program +from pyshgp.push.atoms import Atom, CodeBlock, Instruction, Closer, Literal +from pyshgp.push.type_library import infer_literal +from pyshgp.utils import DiscreteProbDistrib -class Opener: +class Opener(PRecord): """Marks the start of one or more CodeBlock.""" + count = field(type=int, mandatory=True) - __slots__ = ["count"] - - def __init__(self, count: int): - self.count = count - - def dec(self): - """Decrements the count by 1.""" - self.count -= 1 - - -def _has_opener(l: Sequence) -> bool: - return sum([isinstance(_, Opener) for _ in l]) > 0 - - -class Genome(MutableSequence, Saveable, Copyable): - """A flat sequence of Atoms where each Atom is a "gene" in the genome.""" - - def __init__(self, atoms: Sequence[Atom] = None): - self.list = [] - if atoms is not None: - for el in atoms: - self.append(el) - - def __getitem__(self, i: int) -> Any: - return self.list.__getitem__(i) + def dec(self) -> Opener: + return Opener(count=self.count - 1) - def __setitem__(self, i: int, o: Any) -> None: - self.list.__setitem__(i, Genome._conform_element(o)) - def __delitem__(self, i: int) -> None: - self.list.__delitem__(i) +def _has_opener(seq: Sequence) -> bool: + for el in seq: + if isinstance(el, Opener): + return True + return False - def __len__(self) -> int: - return self.list.__len__() - def __eq__(self, other): - return isinstance(other, Genome) and self.list == other.list +class Genome(CheckedPVector): + __type__ = Atom + __invariant__ = lambda a: (not isinstance(a, CodeBlock), 'CodeBlock') - def __repr__(self): - return "Genome" + self.list.__repr__() - def append(self, atom: Atom) -> None: - """Append a non-CodeBlock Atom to the end of the Genome.""" - self.list.append(Genome._conform_element(atom)) +def genome_to_code(genome: Genome) -> CodeBlock: + """Translate into nested CodeBlocks. - def insert(self, index: int, atom: Atom) -> None: - """Insert Atom before index.""" - self.list.insert(Genome._conform_element(atom)) + These CodeBlocks can be considered the Push program representation of + the Genome which can be executed by a PushInterpreter and evaluated + by an Evaluator. - @staticmethod - def _conform_element(el: Any) -> Atom: - if isinstance(el, CodeBlock): - raise ValueError("Cannot add CodeBlock to genomes. Genomes must be kept flat.") - return el - - def to_code_block(self) -> CodeBlock: - """Translate into nested CodeBlocks. - - These CodeBlocks can be considered the Push program representation of - the Genome which can be executed by a PushInterpreter and evaluated - by an Evaluator. - - """ - plushy_buffer = [] - for atom in self: - plushy_buffer.append(atom) - if isinstance(atom, Instruction) and atom.code_blocks > 0: - plushy_buffer.append(Opener(atom.code_blocks)) - - push_buffer = [] - while True: - # If done with plush but unclosed opens, recur with one more close. - if len(plushy_buffer) == 0 and _has_opener(push_buffer): - plushy_buffer.append(Closer()) - # If done with plush and all opens closed, return push. - elif len(plushy_buffer) == 0: - return CodeBlock(*push_buffer) - else: - atom = plushy_buffer[0] - # If next instruction is a close, and there is an open. - if isinstance(atom, Closer) and _has_opener(push_buffer): - ndx, opener = [(ndx, el) for ndx, el in enumerate(push_buffer) if isinstance(el, Opener)][-1] - post_open = push_buffer[ndx + 1:] - pre_open = push_buffer[:ndx] - if opener.count == 1: - push_buffer = pre_open + [post_open] - else: - opener.dec() - push_buffer = pre_open + [post_open, opener] - # If next instruction is a close, and there is no open. - elif not isinstance(atom, Closer): - push_buffer.append(atom) - del plushy_buffer[0] - - def make_str(self) -> str: - """Create one simple str representation of the Genome.""" - return " ".join([str(gene) for gene in self]) + """ + plushy_buffer = [] + for atom in genome: + plushy_buffer.append(atom) + if isinstance(atom, Instruction) and atom.code_blocks > 0: + plushy_buffer.append(Opener(count=atom.code_blocks)) + + push_buffer = [] + while True: + # If done with plush but unclosed opens, recur with one more close. + if len(plushy_buffer) == 0 and _has_opener(push_buffer): + plushy_buffer.append(Closer()) + # If done with plush and all opens closed, return push. + elif len(plushy_buffer) == 0: + return CodeBlock(*push_buffer) + else: + atom = plushy_buffer[0] + # If next instruction is a close, and there is an open. + if isinstance(atom, Closer) and _has_opener(push_buffer): + ndx, opener = [(ndx, el) for ndx, el in enumerate(push_buffer) if isinstance(el, Opener)][-1] + post_open = push_buffer[ndx + 1:] + pre_open = push_buffer[:ndx] + if opener.count == 1: + push_buffer = pre_open + [post_open] + else: + opener = opener.dec() + push_buffer = pre_open + [post_open, opener] + # If next instruction is a close, and there is no open. + elif not isinstance(atom, Closer): + push_buffer.append(atom) + del plushy_buffer[0] + + +class GeneTypes(Enum): + INSTRUCTION = 1 + CLOSE = 2 + LITERAL = 3 + ERC = 4 class GeneSpawner: @@ -173,7 +133,7 @@ class GeneSpawner: def __init__(self, instruction_set: InstructionSet, - literals: Sequence[Union[Literal, Any]], + literals: Sequence[Any], erc_generators: Sequence[Callable], distribution: DiscreteProbDistrib = "proportional"): self.instruction_set = instruction_set @@ -184,10 +144,10 @@ def __init__(self, if distribution == "proportional": self.distribution = ( DiscreteProbDistrib() - .add("instruction", len(instruction_set)) - .add("close", sum([i.code_blocks for i in instruction_set.values()])) - .add("literal", len(literals)) - .add("erc", len(erc_generators)) + .add(GeneTypes.INSTRUCTION, len(instruction_set)) + .add(GeneTypes.CLOSE, sum([i.code_blocks for i in instruction_set.values()])) + .add(GeneTypes.LITERAL, len(literals)) + .add(GeneTypes.ERC, len(erc_generators)) ) else: self.distribution = distribution @@ -231,7 +191,7 @@ def random_erc(self) -> Literal: erc_value = infer_literal(erc_value, self.type_library) return erc_value - def spawn_atom(self) -> Atom: + def random_gene(self) -> Atom: """Return a random Atom based on the GenomeSpawner's distribution. Returns @@ -241,13 +201,13 @@ def spawn_atom(self) -> Atom: """ atom_type = self.distribution.sample() - if atom_type == "instruction": + if atom_type is GeneTypes.INSTRUCTION: return self.random_instruction() - elif atom_type == "close": + elif atom_type is GeneTypes.CLOSE: return Closer() - elif atom_type == "literal": + elif atom_type is GeneTypes.LITERAL: return self.random_literal() - elif atom_type == "erc": + elif atom_type is GeneTypes.ERC: return self.random_erc() else: raise ValueError("GenomeSpawner distribution bad atom type {t}".format(t=str(atom_type))) @@ -275,11 +235,8 @@ def spawn_genome(self, size: Union[int, Sequence[int]]) -> Genome: if isinstance(size, Sequence): size = np.random.randint(size[0], size[1]) + 1 - gn = Genome() - for ndx in range(size): - gn.append(self.spawn_atom()) - - return gn + genes = [self.random_gene() for _ in range(size)] + return Genome.create(genes) class GenomeSimplifier: @@ -290,9 +247,9 @@ class GenomeSimplifier: introduce subtle errors or behaviors that is not covered by the training cases. Removing the superfluous code makes genomes (and thus programs) smaller and easier to understand. More importantly, simplification can - imporve the generalization of the given genome/program. + improve the generalization of the given genome/program. - The process of geneome simplification is iterative and closely resembles + The process of genome simplification is iterative and closely resembles simple hill climbing. For each iteration, the simplifier will randomly select a small number of random genes to remove. The Genome is re-evaluated and if its error gets worse, the change is reverted. After repeating this @@ -320,16 +277,16 @@ def __init__(self, self.verbosity_config = verbosity_config def _remove_rand_genes(self, genome: Genome) -> Genome: - gn = genome.copy(deep=True) + gn = genome n_genes_to_remove = min(np.random.randint(1, 4), len(genome) - 1) ndx_of_genes_to_remove = np.random.choice(np.arange(len(gn)), n_genes_to_remove, replace=False) ndx_of_genes_to_remove[::-1].sort() for ndx in ndx_of_genes_to_remove: - del gn[ndx] + gn = gn.delete(ndx) return gn def _errors_of_genome(self, genome: Genome) -> np.ndarray: - cb = genome.to_code_block() + cb = genome_to_code(genome) program = Program(cb, self.program_signature) return self.evaluator.evaluate(program) @@ -354,7 +311,7 @@ def simplify(self, Parameters ---------- genome - The Genome to simplifiy. + The Genome to simplify. original_errors Error vector of the genome to simplify. steps @@ -363,7 +320,7 @@ def simplify(self, Returns ------- pushgp.gp.genome.Genome - A Genome with random contents of a given size. + The shorter Genome that expresses the same computation. """ if self.verbosity_config is None: diff --git a/pyshgp/gp/individual.py b/pyshgp/gp/individual.py index 8ef6cb3..a96efb9 100644 --- a/pyshgp/gp/individual.py +++ b/pyshgp/gp/individual.py @@ -8,8 +8,8 @@ import numpy as np -from pyshgp.gp.genome import Genome -from pyshgp.push.interpreter import Program, ProgramSignature +from pyshgp.gp.genome import Genome, genome_to_code +from pyshgp.push.program import Program, ProgramSignature from pyshgp.utils import Saveable, Copyable @@ -30,7 +30,7 @@ class Individual(Saveable, Copyable): """ __slots__ = [ - "genome", "signature", "push_config", + "genome", "signature", "_program", "_error_vector", "_total_error", "_error_vector_bytes" ] @@ -45,7 +45,7 @@ def __init__(self, genome: Genome, signature: ProgramSignature): def get_program(self) -> Program: """Push program of individual. Taken from Plush genome.""" if self._program is None: - cb = self.genome.to_code_block() + cb = genome_to_code(self.genome) self._program = Program(cb, self.signature) return self._program diff --git a/pyshgp/gp/search.py b/pyshgp/gp/search.py index a27f03d..517deb8 100644 --- a/pyshgp/gp/search.py +++ b/pyshgp/gp/search.py @@ -7,7 +7,7 @@ from functools import partial from multiprocessing import Pool, Manager -from pyshgp.push.interpreter import ProgramSignature +from pyshgp.push.program import ProgramSignature from pyshgp.utils import DiscreteProbDistrib from pyshgp.gp.evaluation import Evaluator from pyshgp.gp.genome import GeneSpawner, GenomeSimplifier @@ -152,7 +152,7 @@ class SearchAlgorithm(ABC): Attributes ---------- config : SearchConfiguration - The configuation of the search algorithm. + The configuration of the search algorithm. generation : int The current generation, or iteration, of the search. best_seen : Individual diff --git a/pyshgp/gp/variation.py b/pyshgp/gp/variation.py index bafa4bb..354d759 100644 --- a/pyshgp/gp/variation.py +++ b/pyshgp/gp/variation.py @@ -7,7 +7,6 @@ from abc import ABC, abstractmethod from typing import Sequence, Union import math -from copy import copy from numpy.random import random, choice @@ -120,7 +119,7 @@ def produce(self, parents: Sequence[Genome], spawner: GeneSpawner) -> Genome: """ self.checknum_parents(parents) - child = parents[0].copy() + child = parents[0] for op in self.operators: child = op.produce([child] + parents[1:], spawner) return child @@ -157,14 +156,14 @@ class LiteralMutation(VariationOperator, ABC): push_type : pyshgp.push.types.PushType The PushType which the operator can mutate. rate : float - The probablility of applying the mutation to a given Literal. + The probability of applying the mutation to a given Literal. Attributes ---------- push_type : pyshgp.push.types.PushType The PushType which the operator can mutate. rate : float - The probablility of applying the mutation to a given Literal. + The probability of applying the mutation to a given Literal. num_parents : int Number of parent Genomes the operator needs to produce a child Individual. @@ -198,7 +197,7 @@ def produce(self, parents: Sequence[Genome], spawner: GeneSpawner = None) -> Gen new_atom = self._mutate_literal(atom) else: new_atom = atom - new_genome.append(new_atom) + new_genome = new_genome.append(new_atom) return new_genome @@ -208,13 +207,13 @@ class DeletionMutation(VariationOperator): Parameters ---------- rate : float - The probablility of removing any given Atom in the parent Genome. + The probability of removing any given Atom in the parent Genome. Default is 0.01. Attributes ---------- rate : float - The probablility of removing any given Atom in the parent Genome. + The probability of removing any given Atom in the parent Genome. Default is 0.01. num_parents : int Number of parent Genomes the operator needs to produce a child @@ -242,7 +241,7 @@ def produce(self, parents: Sequence[Genome], spawner: GeneSpawner) -> Genome: for gene in parents[0]: if random() < self.rate: continue - new_genome.append(gene) + new_genome = new_genome.append(gene) return new_genome @@ -252,13 +251,13 @@ class AdditionMutation(VariationOperator): Parameters ---------- rate : float - The probablility of adding a new Atom at any given point in the parent + The probability of adding a new Atom at any given point in the parent Genome. Default is 0.01. Attributes ---------- rate : float - The probablility of adding a new Atom at any given point in the parent + The probability of adding a new Atom at any given point in the parent Genome. Default is 0.01. num_parents : int Number of parent Genomes the operator needs to produce a child @@ -285,8 +284,8 @@ def produce(self, parents: Sequence[Genome], spawner: GeneSpawner) -> Genome: new_genome = Genome() for gene in parents[0]: if random() < self.rate: - new_genome.append(spawner.spawn_atom()) - new_genome.append(gene) + new_genome = new_genome.append(spawner.random_gene()) + new_genome = new_genome.append(gene) return new_genome @@ -298,7 +297,7 @@ class Alternation(VariationOperator): Parameters ---------- rate : float, optional (default=0.01) - The probablility of switching which parent program elements are being + The probability of switching which parent program elements are being copied from. Must be 0 <= rate <= 1. Defaults to 0.1. alignment_deviation : int, optional (default=10) The standard deviation of how far alternation may jump between indices @@ -307,7 +306,7 @@ class Alternation(VariationOperator): Attributes ---------- rate : float, optional (default=0.01) - The probablility of switching which parent program elements are being + The probability of switching which parent program elements are being copied from. Must be 0 <= rate <= 1. Defaults to 0.1. alignment_deviation : int, optional (default=10) The standard deviation of how far alternation may jump between indices @@ -335,8 +334,8 @@ def produce(self, parents: Sequence[Genome], spawner: GeneSpawner = None) -> Gen """ self.checknum_parents(parents) - gn1 = parents[0].copy() - gn2 = parents[1].copy() + gn1 = parents[0] + gn2 = parents[1] new_genome = Genome() # Random pick which parent to start from use_parent_1 = choice([True, False]) @@ -353,9 +352,9 @@ def produce(self, parents: Sequence[Genome], spawner: GeneSpawner = None) -> Gen else: # Pull gene from parent if use_parent_1: - new_genome.append(gn1[i]) + new_genome = new_genome.append(gn1[i]) else: - new_genome.append(gn2[i]) + new_genome = new_genome.append(gn2[i]) i = int(i + 1) # Change loop stop condition loop_times = len(gn1) @@ -431,7 +430,7 @@ def produce(self, parents: Sequence[Genome], spawner: GeneSpawner = None) -> Gen A GeneSpawner that can be used to produce new genes (aka Atoms). """ - return copy.copy(parents[0]) + return parents[0] def get_variation_operator(name: str, **kwargs) -> VariationOperator: diff --git a/pyshgp/monitoring.py b/pyshgp/monitoring.py index a68a9a9..a0800b6 100644 --- a/pyshgp/monitoring.py +++ b/pyshgp/monitoring.py @@ -3,7 +3,7 @@ import logging -# @TODO: Add logging of serach config to verbosity +# @TODO: Add logging of search config to verbosity # @TODO: Add logging of start time, end time, and runtime to verbosity diff --git a/pyshgp/push/__init__.py b/pyshgp/push/__init__.py index fc3d7bd..d8f7aed 100644 --- a/pyshgp/push/__init__.py +++ b/pyshgp/push/__init__.py @@ -1,7 +1,8 @@ """pyshgp.push""" from pyshgp.push.instruction_set import InstructionSet -from pyshgp.push.interpreter import ProgramSignature, Program, PushInterpreter +from pyshgp.push.program import Program, ProgramSignature +from pyshgp.push.interpreter import PushInterpreter from pyshgp.push.config import PushConfig __all__ = [ diff --git a/pyshgp/push/atoms.py b/pyshgp/push/atoms.py index 4271954..38c6a12 100644 --- a/pyshgp/push/atoms.py +++ b/pyshgp/push/atoms.py @@ -10,24 +10,20 @@ from typing import Any, Sequence from itertools import chain, count +from pyrsistent import PRecord + from pyshgp.push.types import PushType from pyshgp.utils import Saveable, Copyable class Atom: """Base class of all Atoms. The fundamental element of Push programs.""" - ... -class Closer(Atom): +class Closer(Atom, PRecord): """An Atom dedicated to denoting the close of a CodeBlock in its flat representsion.""" - - def __eq__(self, other): - return isinstance(other, Closer) - - def __repr__(self) -> str: - return "CLOSER" + ... class Literal(Atom): diff --git a/pyshgp/push/interpreter.py b/pyshgp/push/interpreter.py index 70f700b..a606a1c 100644 --- a/pyshgp/push/interpreter.py +++ b/pyshgp/push/interpreter.py @@ -9,40 +9,16 @@ class to determine limits. import time from enum import Enum +from pyshgp.push.program import Program from pyshgp.push.state import PushState from pyshgp.push.instruction_set import InstructionSet from pyshgp.push.atoms import Atom, Closer, Literal, Instruction, JitInstructionRef, CodeBlock from pyshgp.push.types import PushStr from pyshgp.push.config import PushConfig -from pyshgp.utils import Saveable from pyshgp.validation import PushError from pyshgp.monitoring import VerbosityConfig, DEFAULT_VERBOSITY_LEVELS, log_function -class ProgramSignature: - """A collection of values required to get consistent behavior from Push code.""" - - def __init__(self, arity: int, output_stacks: Sequence[str], push_config: PushConfig): - self.arity = arity - self.output_stacks = output_stacks - self.push_config = push_config - - -class Program(Saveable): - """A Push program composed of some Push code and a ProgramSignature.""" - - def __init__(self, code: CodeBlock, signature: ProgramSignature): - self.code = code - self.signature = signature - - def __repr__(self): - return "Program[{arity}][{outputs}]({code})".format( - arity=self.signature.arity, - outputs=self.signature.output_stacks, - code=self.code - ) - - class PushInterpreterStatus(Enum): """Enum class of all potential statuses of a PushInterpreter.""" @@ -99,6 +75,10 @@ def __init__(self, self.verbosity_config = verbosity_config # Initialize the PushState and status + self.state = None + self.status = None + self._verbose_trace = None + self._log_fn_for_trace = None self._validate() self.reset() diff --git a/pyshgp/push/program.py b/pyshgp/push/program.py new file mode 100644 index 0000000..5521420 --- /dev/null +++ b/pyshgp/push/program.py @@ -0,0 +1,29 @@ +from typing import Sequence + +from pyshgp.push.config import PushConfig +from pyshgp.push.atoms import CodeBlock +from pyshgp.utils import Saveable + + +class ProgramSignature: + """A collection of values required to get consistent behavior from Push code.""" + + def __init__(self, arity: int, output_stacks: Sequence[str], push_config: PushConfig): + self.arity = arity + self.output_stacks = output_stacks + self.push_config = push_config + + +class Program(Saveable): + """A Push program composed of some Push code and a ProgramSignature.""" + + def __init__(self, code: CodeBlock, signature: ProgramSignature): + self.code = code + self.signature = signature + + def __repr__(self): + return "Program[{arity}][{outputs}]({code})".format( + arity=self.signature.arity, + outputs=self.signature.output_stacks, + code=self.code + ) diff --git a/pyshgp/push/stack.py b/pyshgp/push/stack.py index 8919288..7e17305 100644 --- a/pyshgp/push/stack.py +++ b/pyshgp/push/stack.py @@ -3,13 +3,13 @@ A PushStack is used to hold values of a certain PushType in a PushState object. """ -from typing import Optional, Sequence +from typing import Optional, Sequence, List from pyshgp.push.types import PushType from pyshgp.utils import Token -class PushStack(list): +class PushStack(List): """Stack that holds elements of a sinlge PushType. Parameters @@ -31,6 +31,7 @@ class PushStack(list): __slots__ = ["push_type"] def __init__(self, push_type: PushType, values: Optional[Sequence] = None): + super().__init__() self.push_type = push_type if values is not None: for val in values: @@ -177,3 +178,4 @@ def __eq__(self, other): if not isinstance(other, PushStack): return False return self.push_type == other.push_type and list(self) == list(other) + diff --git a/pyshgp/validation.py b/pyshgp/validation.py index d3ae51f..a6d35af 100644 --- a/pyshgp/validation.py +++ b/pyshgp/validation.py @@ -1,5 +1,5 @@ """Module for validating data and raising informative errors.""" -from typing import Sequence, Tuple +from typing import Sequence, Tuple, Union import numpy as np import pandas as pd @@ -22,7 +22,7 @@ def check_1d(seq: Sequence) -> Sequence: return seq -def check_2d(seq: list) -> list: +def check_2d(seq: Union[list, np.ndarray]) -> list: """Check given seq is two-dimensional. Raise error if can't be easily transformed.""" for ndx, el in enumerate(seq): if not isinstance(el, (list, tuple, np.ndarray, pd.Series)): diff --git a/pyshgp_cli.py b/pyshgp_cli.py index 62fb5af..84daa4e 100644 --- a/pyshgp_cli.py +++ b/pyshgp_cli.py @@ -30,10 +30,9 @@ def _generate_instruction_rst(instr: Instruction) -> str: - lines = [] - lines.append(instr.name) - lines.append("=" * len(instr.name)) + lines = [instr.name, "=" * len(instr.name)] + signature_line = None signature_template = "*Takes: {i} - Produces: {o}*" if isinstance(instr, SimpleInstruction): signature_line = signature_line = signature_template.format( @@ -65,6 +64,7 @@ def _generate_instruction_markdown(instr: Instruction) -> str: lines = [] lines.append("### {n}".format(n=instr.name)) + signature_line = None signature_template = "_Takes: {i} - Produces: {o}_" if isinstance(instr, SimpleInstruction): signature_line = signature_line = signature_template.format( @@ -93,9 +93,9 @@ def _generate_instruction_markdown(instr: Instruction) -> str: def _generate_instruction_html(instr: Instruction) -> str: - lines = ["
  • "] - lines.append('

    {n}

    '.format(n=instr.name)) + lines = ["
  • ", '

    {n}

    '.format(n=instr.name)] + signature_line = None signature_template = "Takes: {i} - Produces: {o}" if isinstance(instr, SimpleInstruction): signature_line = signature_line = signature_template.format( diff --git a/requirements-with-dev.txt b/requirements-with-dev.txt index 4bb2c6d..a434ffa 100644 --- a/requirements-with-dev.txt +++ b/requirements-with-dev.txt @@ -1,6 +1,7 @@ numpy >= 1.12.0 scipy >= 0.18.0 pandas >= 0.23.4 +pyrsistent >= 0.16.0 pytest >= 5.0.1 pytest-watch >= 4.2.0 diff --git a/requirements.txt b/requirements.txt index 1ad6b91..e2a69af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ numpy >= 1.12.0 scipy >= 0.18.0 pandas >= 0.23.4 +pyrsistent >= 0.16.0 diff --git a/setup.py b/setup.py index f04f6ae..a96685b 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ def read(fname): "numpy>=1.12.0", "scipy>=0.18.0", "pandas>=0.23.4", + "pyrsistent>=0.16.0", ], setup_requires=[ "pytest-runner", diff --git a/tests/conftest.py b/tests/conftest.py index af4c3b3..0f874c9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,14 +4,16 @@ from pyshgp.gp.individual import Individual +from pyshgp.push.config import PushConfig from pyshgp.push.type_library import PushTypeLibrary from pyshgp.push.types import PushInt, PushBool, PushFloat, PushStr from pyshgp.push.instruction_set import InstructionSet from pyshgp.push.instruction import SimpleInstruction from pyshgp.push.atoms import Closer, Literal -from pyshgp.push.interpreter import PushInterpreter, PushConfig, ProgramSignature, Program +from pyshgp.push.interpreter import PushInterpreter +from pyshgp.push.program import ProgramSignature, Program from pyshgp.push.state import PushState -from pyshgp.gp.genome import Genome, GeneSpawner +from pyshgp.gp.genome import Genome, GeneSpawner, genome_to_code @pytest.fixture(scope="session") @@ -64,7 +66,7 @@ def simple_program_signature(push_config): @pytest.fixture(scope="function") def simple_program(simple_genome, simple_program_signature): - return Program(simple_genome.to_code_block(), simple_program_signature) + return Program(genome_to_code(simple_genome), simple_program_signature) @pytest.fixture(scope="function") diff --git a/tests/gp/test_evaluation.py b/tests/gp/test_evaluation.py index 60ab694..c41ed84 100644 --- a/tests/gp/test_evaluation.py +++ b/tests/gp/test_evaluation.py @@ -24,7 +24,7 @@ def test_default_error_function(self): evaluator.default_error_function( [Token.no_stack_item, True, 1, 2.3, "456", [7, 8]], ["a stack item", False, 3, 6.3, "abc", [5, 11]]), - np.array([np.inf, 1, 2, 4.0, 3, 2, 3]) + np.array([evaluator.penalty, 1, 2, 4.0, 3, 2, 3]) )) def test_dataset_evaluate_simple(self, simple_program): diff --git a/tests/gp/test_genome.py b/tests/gp/test_genome.py index 5cebd12..6641402 100644 --- a/tests/gp/test_genome.py +++ b/tests/gp/test_genome.py @@ -1,47 +1,48 @@ import pytest +from pyrsistent import InvariantException -from pyshgp.gp.genome import Opener, _has_opener, Genome, GenomeSimplifier +from pyshgp.gp.genome import Opener, _has_opener, genome_to_code, Genome, GenomeSimplifier from pyshgp.gp.evaluation import DatasetEvaluator from pyshgp.push.atoms import Atom, Literal, Instruction, CodeBlock from pyshgp.push.types import PushInt, PushBool def test_opener(): - o = Opener(2) + o = Opener(count=2) assert o.count == 2 - o.dec() + o = o.dec() assert o.count == 1 def test__has_opener(): lst = ["_" for x in range(5)] assert not _has_opener(lst) - lst[2] = Opener(1) + lst[2] = Opener(count=1) assert _has_opener(lst) class TestGenome: def test_genome_bad_init(self, atoms): - with pytest.raises(ValueError): + with pytest.raises(InvariantException): Genome(CodeBlock(*[atoms["5"], [atoms["5"], atoms["add"]]])) def test_missing_close_genome_to_codeblock(self, atoms): gn = Genome([atoms["true"], atoms["if"], atoms["1.2"], atoms["close"], atoms["5"]]) - cb = gn.to_code_block() + cb = genome_to_code(gn) assert cb[0] == Literal(True, PushBool) assert isinstance(cb[1], Instruction) assert isinstance(cb[2], CodeBlock) def test_extra_close_genome_to_codeblock(self, atoms): gn = Genome([atoms["close"], atoms["5"], atoms["close"], atoms["close"]]) - cb = gn.to_code_block() + cb = genome_to_code(gn) assert len(cb) == 1 assert cb[0] == Literal(5, PushInt) def test_empty_genome_to_codeblock(self, atoms): gn = Genome() - cb = gn.to_code_block() + cb = genome_to_code(gn) assert len(cb) == 0 @@ -56,8 +57,8 @@ def test_random_literal(self, simple_gene_spawner): def test_random_erc(self, simple_gene_spawner): assert isinstance(simple_gene_spawner.random_erc(), Literal) - def test_spawn_atom(self, simple_gene_spawner): - assert isinstance(simple_gene_spawner.spawn_atom(), Atom) + def test_random_gene(self, simple_gene_spawner): + assert isinstance(simple_gene_spawner.random_gene(), Atom) def test_spawn_genome(self, simple_gene_spawner): gn = simple_gene_spawner.spawn_genome(10) diff --git a/tests/gp/test_population.py b/tests/gp/test_population.py index 70290ea..988df48 100644 --- a/tests/gp/test_population.py +++ b/tests/gp/test_population.py @@ -5,7 +5,7 @@ from pyshgp.gp.individual import Individual from pyshgp.gp.genome import Genome from pyshgp.gp.evaluation import DatasetEvaluator -from pyshgp.push.interpreter import ProgramSignature +from pyshgp.push.program import ProgramSignature @pytest.fixture(scope="function") @@ -92,7 +92,7 @@ def test_evaluated_population(self, unevaluated_pop): [5, 0, 5], [5, 0, 5], [5, 0, 5], - [np.inf, np.inf, np.inf], + [evaluator.penalty, evaluator.penalty, evaluator.penalty], ]) assert np.all(np.equal(a, e)) diff --git a/tests/gp/test_search.py b/tests/gp/test_search.py index b807ee5..b6f86aa 100644 --- a/tests/gp/test_search.py +++ b/tests/gp/test_search.py @@ -2,7 +2,6 @@ from pyshgp.gp.search import SearchConfiguration from pyshgp.gp.evaluation import DatasetEvaluator -from pyshgp.push.interpreter import ProgramSignature @pytest.fixture(scope="session") @@ -25,5 +24,4 @@ def test_create_config_strs(self, empty_evaluator, simple_gene_spawner, simple_p assert config.get_selector().tournament_size == 14 assert config.get_variation_op().alignment_deviation == 5 - # @TODO: TEST - Test with custom PushTypeLibrary and custom instructions. diff --git a/tests/gp/test_selection.py b/tests/gp/test_selection.py index a75ffa2..9edf218 100644 --- a/tests/gp/test_selection.py +++ b/tests/gp/test_selection.py @@ -5,7 +5,7 @@ from pyshgp.gp.population import Population from pyshgp.gp.individual import Individual from pyshgp.gp.genome import Genome -from pyshgp.push.interpreter import ProgramSignature +from pyshgp.push.program import ProgramSignature @pytest.fixture(scope="function") diff --git a/tests/gp/test_translate_validation.py b/tests/gp/test_translate_validation.py index 73f7679..814837c 100644 --- a/tests/gp/test_translate_validation.py +++ b/tests/gp/test_translate_validation.py @@ -3,16 +3,16 @@ from pyshgp.push.atoms import CodeBlock, Closer, Literal, JitInstructionRef from pyshgp.push.interpreter import PushInterpreter from pyshgp.push.instruction_set import InstructionSet -from pyshgp.gp.genome import Genome +from pyshgp.gp.genome import Genome, genome_to_code -def _pysh_collection_from_list(lst, cls, instr_set: InstructionSet): +def _deserialize_atoms(lst, instr_set: InstructionSet): type_lib = instr_set.type_library - coll = cls() + atoms = [] for atom_spec in lst: atom = None if isinstance(atom_spec, list): - atom = _pysh_collection_from_list(atom_spec, cls, instr_set) + atom = CodeBlock(*_deserialize_atoms(atom_spec, instr_set)) else: atom_type = atom_spec["a"] if atom_type == "close": @@ -27,56 +27,48 @@ def _pysh_collection_from_list(lst, cls, instr_set: InstructionSet): atom = JitInstructionRef(atom_spec["n"]) else: raise ValueError("bad atom spec {s}".format(s=atom_spec)) - coll.append(atom) - return coll + atoms.append(atom) + return atoms def load_code(name, interpreter) -> CodeBlock: with open("tests/resources/programs/" + name + ".json") as f: - return _pysh_collection_from_list(json.load(f), CodeBlock, interpreter.instruction_set) + atoms = _deserialize_atoms(json.load(f), interpreter.instruction_set) + return CodeBlock(*atoms) def load_genome(name, interpreter) -> Genome: with open("tests/resources/genomes/" + name + ".json") as f: - return _pysh_collection_from_list(json.load(f), Genome, interpreter.instruction_set) + atoms = _deserialize_atoms(json.load(f), interpreter.instruction_set) + return Genome.create(atoms) -def test_genome_relu_1(): +def check_translation(program_name: str, interpreter: PushInterpreter): + genome = load_genome(program_name, interpreter) + prog = load_code(program_name, interpreter) + assert genome_to_code(genome) == prog + + +def check_unary_fn_translation(program_name: str): interpreter = PushInterpreter(InstructionSet(register_core=True).register_n_inputs(1)) - name = "relu_via_max" - genome = load_genome(name, interpreter) - prog = load_code(name, interpreter) - assert genome.to_code_block() == prog + check_translation(program_name, interpreter) + + +def test_genome_relu_1(): + check_unary_fn_translation("relu_via_max") def test_genome_relu_2(): - interpreter = PushInterpreter(InstructionSet(register_core=True).register_n_inputs(1)) - name = "relu_via_if" - genome = load_genome(name, interpreter) - prog = load_code(name, interpreter) - assert genome.to_code_block() == prog + check_unary_fn_translation("relu_via_if") def test_genome_fibonacci(): - interpreter = PushInterpreter(InstructionSet(register_core=True).register_n_inputs(1)) - name = "fibonacci" - genome = load_genome(name, interpreter) - prog = load_code(name, interpreter) - print(type(genome.to_code_block()), type(prog)) - assert genome.to_code_block() == prog + check_unary_fn_translation("fibonacci") def test_genome_rswn(): - interpreter = PushInterpreter(InstructionSet(register_core=True).register_n_inputs(1)) - name = "replace_space_with_newline" - genome = load_genome(name, interpreter) - prog = load_code(name, interpreter) - assert genome.to_code_block() == prog + check_unary_fn_translation("replace_space_with_newline") def test_genome_point_dist(point_instr_set): - interpreter = PushInterpreter(point_instr_set) - name = "point_distance" - genome = load_genome(name, interpreter) - prog = load_code(name, interpreter) - assert genome.to_code_block() == prog + check_translation("point_distance", PushInterpreter(point_instr_set)) diff --git a/tests/push/test_push_validation.py b/tests/push/test_push_validation.py index e294f96..b03fb96 100644 --- a/tests/push/test_push_validation.py +++ b/tests/push/test_push_validation.py @@ -1,7 +1,9 @@ import json from pyshgp.push.atoms import CodeBlock, Closer, Literal, JitInstructionRef -from pyshgp.push.interpreter import PushInterpreter, ProgramSignature, Program, PushConfig +from pyshgp.push.interpreter import PushInterpreter +from pyshgp.push.config import PushConfig +from pyshgp.push.program import ProgramSignature, Program from pyshgp.push.instruction_set import InstructionSet