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 = ["