class Opener(PRecord):
    """Marks the start of one or more CodeBlock."""

    count = field(type=int, mandatory=True)

    def dec(self) -> Opener:
        """Create an ``Opener`` with ``count`` decremented."""
        return Opener(count=self.count - 1) def _has_opener(seq: Sequence) -> bool:
    for el in seq:
        if isinstance(el, Opener):
            return True
    return False def genome_to_code(genome: Genome) -> 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 = l()
    for atom in genome[::-1]:
        if isinstance(atom, InstructionMeta) and atom.code_blocks > 0:
            plushy_buffer = plushy_buffer.cons(Opener(count=atom.code_blocks))
        plushy_buffer = plushy_buffer.cons(atom)

    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 = plushy_buffer.cons(Closer())
        # If done with plush and all opens closed, return push.
        elif len(plushy_buffer) == 0:
            return CodeBlock(push_buffer)
        else:
            atom = plushy_buffer.first
            plushy_buffer = plushy_buffer.rest
            # 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]
                push_buffer = pre_open + [CodeBlock(post_open)]
                if opener.count > 1:
                    opener = opener.dec()
                    push_buffer.append(opener)
            # If next instruction is a close, and there is no open, ignore it.
            # If next instruction is not a close.
            elif not isinstance(atom, Closer):
                push_buffer.append(atom) class Genome(CheckedPVector):
    """A linear sequence of genes (aka any atom that isn't a ``CodeBlock``).

    PyshGP uses the Plushy genome representation.

    See: http://gpbib.cs.ucl.ac.uk/gp-html/Spector_2019_GPTP.html

    """

    __type__ = Atom
    __invariant__ = lambda a: (not isinstance(a, CodeBlock), 'CodeBlock') class GenomeSimplifier:
    """Simplifies a genome while preserving, or improving, its error.

    Genomes, and Push programs, can contain superfluous Push code. This extra
    code often has no effect on the program behavior, but occasionally it can
    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
    improve the generalization of the given genome/program.

    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
    for some number of steps, the resulting genome will be the same size or
    smaller while containing the same (or better) error value.

    Reference:
    "Improving generalization of evolved programs through automatic simplification"
    Thomas Helmuth, Nicholas Freitag McPhee, Edward Pantridge, and Lee Spector. 2017.
    In Proceedings of the Genetic and Evolutionary Computation Conference (GECCO '17).
    ACM, New York, NY, USA, 937-944. DOI: https://doi.org/10.1145/3071178.3071330

    See: https://dl.acm.org/citation.cfm?id=3071178.3071330

    """

    # @TODO: Add noop swaps to simplification.

    def __init__(self,
                 evaluator: Evaluator,
                 program_signature: ProgramSignature):
        self.evaluator = evaluator
        self.program_signature = program_signature

    def _remove_rand_genes(self, genome: Genome) -> Genome:
        # @todo DRY with deletion variation operator.
        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:
            gn = gn.delete(ndx)
        return gn

    def _errors_of_genome(self, genome: Genome) -> np.ndarray:
        cb = genome_to_code(genome)
        program = Program(code=cb, signature=self.program_signature)
        return self.evaluator.evaluate(program)

    @tap
    def _step(self, genome: Genome, errors_to_beat: np.ndarray) -> Tuple[Genome, np.ndarray]:
        new_gn = self._remove_rand_genes(genome)
        new_errs = self._errors_of_genome(new_gn)
        if np.sum(new_errs) <= np.sum(errors_to_beat):
            return new_gn, new_errs
        return genome, errors_to_beat

    @tap
    def simplify(self,
                 genome: Genome,
                 original_errors: np.ndarray,
                 steps: int = 2000) -> Tuple[Genome, np.ndarray]:
        """Simplify the given genome while maintaining error.

        Parameters
        ----------
        genome
            The Genome to simplify.
        original_errors
            Error vector of the genome to simplify.
        steps
            Number of simplification iterations to perform. Default is 2000.

        Returns
        -------
        pyshgp.gp.genome.Genome
            The shorter Genome that expresses the same computation.

        """
        gn = genome
        errs = original_errors
        for step in range(steps):
            gn, errs = self._step(gn, errs)
            if len(gn) == 1:
                break
        return gn, errs class DatasetEvaluator(Evaluator):
    """Evaluator driven by a labeled dataset."""

    def __init__(self,
                 X, y,
                 interpreter: PushInterpreter = "default",
                 penalty: float = 1e6):
        """Create Evaluator based on a labeled dataset. Inspired by sklearn.

        Parameters
        ----------
        X : list, array-like, or pandas dataframe of shape = [n_samples, n_features]
            The inputs to evaluate each program on.

        y : list, array-like, or pandas dataframe.
            The target values. Shape = [n_samples] or [n_samples, n_outputs]

        interpreter : PushInterpreter or {"default"}
            The interpreter used to run the push programs.

        penalty : float
            If no response is given by the program on a given input, assign this
            error as the error.

        """
        super().__init__(interpreter, penalty)
        self.X = pd.DataFrame(X)
        self.y = pd.DataFrame(y)

    @tap
    def evaluate(self, program: Program) -> np.array:
        """Evaluate the program and return the error vector.

        Parameters
        ----------
        program
            Program (CodeBlock of Push code) to evaluate.

        Returns
        -------
        np.ndarray
            The error vector of the program.

        """
        super().evaluate(program)
        errors = []
        for ndx in range(self.X.shape[0]):
            inputs = self.X.iloc[ndx].to_list()
            expected = self.y.iloc[ndx].to_list()
            actual = self.interpreter.run(program, inputs)
            errors.append(self.default_error_function(actual, expected))
        return np.array(errors).flatten() class Atom:
    """Base class of all Atoms. The fundamental element of Push programs."""

    @abstractmethod
    def pretty_str(self) -> str:
        """Generate a simple string representation of the Atom."""
        raise NotImplementedError() class Literal(Atom, PClass):
    """An Atom which holds a constant value.

    Attributes
    ----------
    value : Any
        The value to be stored in Literal.
    push_type : PushType, optional
        The `PushType` of the `Literal`. Usually, the PushType's underlying native
        type is the same as the type of the Literal's value. The PushType is
        used to determine how to route the Literal onto the correct PushStack
        of the PushState during program execution.

    """

    __invariant__ = lambda l: (l.push_type.is_instance(l.value), "Value is not of PushType.")
    value = field(mandatory=True)
    push_type = field(type=PushType, mandatory=True)

    def pretty_str(self) -> str:
        """Generate a simple string representation of the Literal."""
        # @todo make smarter by using a method on the PushType.
        s = str(self.value)
        if isinstance(self.value, str):
            s = "\"" + s + "\""
        return s class InstructionMeta(Atom, PClass):
    """An identifier of a Push Instruction.

    The `InstructionMeta` is a placeholder atom. When a PushInterpreter evaluates
    an instruction ID, it searches for an instruction with the given
    name in its `InstructionSet`. If found, this instruction is evaluated by the `PushInterpreter`.

    Attributes
    ----------
    name: str
        The name of the Instruction being referenced.
    code_blocks: int
        The number of code blocks that are expected to follow the instruction.

    """

    name = field(type=str, mandatory=True)
    code_blocks = field(type=int, mandatory=True)

    def pretty_str(self) -> str:
        """Generate a simple string representation of the Instruction."""
        return self.name class CodeBlock(Atom, CheckedPVector):
    """An `Atom` which holds a sequence of other atoms in order to express a nested code block."""

    __type__ = Atom

    def size(self, depth: int = 1) -> int:
        """Return the size of the block and the size of all the nested blocks."""
        return sum([el.size(depth + 1) + 1 if isinstance(el, CodeBlock) else 1 for el in self])

    def depth(self) -> int:
        """Return the farthest depth of the CodeBlock."""
        i = iter(self)
        level = 0
        try:
            for level in count():
                i = chain([next(i)], i)
                i = chain.from_iterable(s for s in i if isinstance(s, Sequence) and not isinstance(s, str))
        except StopIteration:
            return level
        return 0

    def code_at_point(self, ndx: int) -> Optional[Atom]:
        """Return a nested element of the CodeBlock using depth first traversal."""
        if ndx == 0:
            return self
        i = ndx
        for el in self:
            i = i - 1
            if i == 0:
                return el
            if isinstance(el, CodeBlock):
                next_depth = el.code_at_point(i)
                if next_depth is not None:
                    return next_depth
                i = i - el.size()
        return None

    def with_code_inserted_at_point(self, code: Atom, index: int) -> CodeBlock:
        """Insert an element into the CodeBlock using depth first traversal."""
        if index > self.size():
            return self.append(code)
        _, new = self._attempt_code_insert(code, index)
        return new

    def _attempt_code_insert(self, code: Atom, index: int) -> Tuple[bool, CodeBlock]:
        i = index
        for idx, el in enumerate(self):
            if i == 0:
                new = CodeBlock(self[:idx] + [code] + self[idx:])
                return True, new
            if isinstance(el, CodeBlock):
                inserted, new = el._attempt_code_insert(code, i - 1)
                if inserted:
                    return True, self.set(idx, new)
                i = i - (el.size() + 1)
            i = i - 1
        if i == 0:
            return True, self.append(code)
        return False, self

    def pretty_str(self) -> str:
        """Generate a simple string representation of the CodeBlock."""
        return "(" + " ".join([el.pretty_str() for el in self]) + ")" class Char(str):
class PushType:
class PushBoolType(PushType):
class PushIntType(PushType):
class PushFloatType(PushType):
class PushCharType(PushType):
class PushStrType(PushType):
class PushVectorType(PushType):
class BoolVector(CheckedPVector):
class IntVector(CheckedPVector):
class FloatVector(CheckedPVector):
class CharVector(CheckedPVector):
class StrVector(CheckedPVector):
    def __init__(self, char):
    def __eq__(self, other):
    def __init__(self,
                 name: str,
                 python_types: Tuple[type, ...],
                 is_collection: bool = False,
                 is_numeric: bool = False):
    def is_instance(self, value) -> bool:
    def coerce(self, value):
    def __eq__(self, other):
    def __hash__(self):
    def __init__(self):
    def __init__(self):
    def __init__(self):
    def __init__(self):
    def __init__(self):
    def __init__(self, name: str, p_vec_type):
    def coerce(self, value):
def make_vector_type(scalar_type: PushType):
CORE_SCALAR_PUSH_TYPES: List[PushType] = [
    PushBool,
    PushInt,
    PushChar,
    PushFloat,
    PushStr,
]
CORE_VECTOR_PUSH_TYPES: List[PushVectorType] = [
    PushIntVector,
    PushFloatVector,
    PushStrVector,
    PushBoolVector,
    PushCharVector,
]
CORE_PUSH_TYPES: List[PushType] = CORE_SCALAR_PUSH_TYPES + CORE_VECTOR_PUSH_TYPES
import pytest
from pyrsistent import InvariantException
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, InstructionMeta, CodeBlock
from pyshgp.push.types import PushInt, PushBool



def test_opener():
    o = Opener(count=2)
    assert o.count == 2
    o = o.dec()
    assert o.count == 1


def test__has_opener():
    lst = ["_" for x in range(5)]

    assert not _has_opener(lst)