#!/usr/local/bin/python

### prion.py - An extensible framework for LGP
### Version v01.02.01
### Copyright (C) 2018 Christopher J. Crowe
###
### This program is free software: you can redistribute it and/or modify
### it under the terms of the GNU General Public License as published by
### the Free Software Foundation, either version 3 of the License, or
### (at your option) any later version.
###
### This program is distributed in the hope that it will be useful,
### but WITHOUT ANY WARRANTY; without even the implied warranty of
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
### GNU General Public License for more details.
###
### You should have received a copy of the GNU General Public License
### along with this program. If not, see https://www.gnu.org/licenses/

import random

PRION_NOISY   = False
PRION_LIBTEST = False

# This is usually a minimal set of functions for the generated program to use.
# Can't imagine many reasons to not assume they will be included
# unless you imagine a good reason to do so, you need these for an actual system
# To really function at all.

# The "machine" (or processor), at its core is a simulated stack processor. In certain cases,
# stack-based machines demonstrate superior performance. In most of the remaineder, they
# seem to function no better or worse. Here is the naming scheme that will be utilized
# by the registers of this stack-based VM. The final implementation of the processor is uknown,
# so at this point in the design, we need to be careful about implementation decisions/constraints. 
# I want prion to almost boot-strap itself up so the priomordial functions are going to be
# instantiated as fully-realized and executable functions for the interpreter. I do want some
# flexibility at this point. But this is a very critical point in the base design so some
# aspects bear dwelling upon:
# -- At this point we can assume that we will have an array of stacks which will be utilized
#    as the registers of the processor
# -- These registers are capable of interpreting some intrinsic things or concepts. Such as the 
#    "push" and "pop" of the stack operations. 
# -- We also assume the inputs are a RO bus from which a single value is transmitted when
#    addressed by index

# Should you find any collisions in your local namespace for some odd reason, you can globally alter
# the naming conventions from here and the system can bootstrap itself out of you namespace
PRION_OUTPUT_NAME = "PRION_STACK"
PRION_INPUT_NAME  = "PRION_WIRE"

# These next two will "feel" a tad strange, but the are part of the bootstapping process for this system
# The code will bootstrap itself everytime it comes up so please pay attention if you're changing anything
PRION_TAG_DELIMITER = "&"
PRION_OUTPUT_LBL    = PRION_TAG_DELIMITER + "OUT"
PRION_INPUT_LBL     = PRION_TAG_DELIMITER + "WIRE"
PRION_FUNCTION_LBL  = PRION_TAG_DELIMITER + "FUNC"
PRION_LABEL_ARRAY   = [PRION_OUTPUT_LBL, PRION_INPUT_LBL, PRION_FUNCTION_LBL]

if PRION_NOISY:
	print("PRION_CORE::Library initialized with:")
	print("PRION_LABEL_ARRAY       = " + str(PRION_LABEL_ARRAY))
	print(" -- PRION_TAG_DELIMITER = " + PRION_TAG_DELIMITER)
	print(" -- PRION_OUTPUT_LBL    = " + PRION_OUTPUT_LBL)
	print(" -- PRION_INPUT_LBL     = " + PRION_INPUT_LBL)
	print(" -- PRION_FUNCTION_LBL  = " + PRION_FUNCTION_LBL)
	print("---------------------------------------------")

# Now we actually start writing the code for our primordial functions

PRION_OPCODE_INT = -1
PRION_OPCODE_NOP = 0

# prion_INP creation here
PRION_OPCODE_INP = PRION_OPCODE_NOP + 1
PRION_INP_FUNCTION_TEMP = """
def &FUNC(&OUT , &WIRE):
	&OUT.append(&WIRE)
	return
"""
PRION_INP_FUNCTION_DEF = PRION_INP_FUNCTION_TEMP.replace(PRION_FUNCTION_LBL, "prion_INP")
PRION_INP_FUNCTION_DEF = PRION_INP_FUNCTION_DEF.replace(PRION_OUTPUT_LBL, "stk")
PRION_INP_FUNCTION_DEF = PRION_INP_FUNCTION_DEF.replace(PRION_INPUT_LBL, "wire")

if PRION_NOISY:
	print("PRION_CORE::prion_INP::Defined as below")
	print(PRION_INP_FUNCTION_DEF)
	print("Assinged OPCODE: " + str(PRION_OPCODE_INP))
	print("---------------------------------------------")

###############################
# Indentaton matters here     #
# please be careful if        #
# this is moved around        #
###############################
exec(PRION_INP_FUNCTION_DEF)  #
###############################

if PRION_LIBTEST:
	print("PRION_CORE::prion_INP::Compiled...testing")
	x = []
	print(str(x))
	prion_INP(x, 1)
	print(str(x))
	prion_INP(x, 2)
	print(str(x))
	prion_INP(x, 3)
	print(str(x))
	print("---------------------------------------------")

# prion_PUSH creation here
PRION_OPCODE_PUSH        = PRION_OPCODE_INP + 1
PRION_PUSH_FUNCTION_TEMP = """
def &FUNC(&OUT , &WIRE):
	&OUT.append(&WIRE)
	return
"""
PRION_PUSH_FUNCTION_DEF = PRION_PUSH_FUNCTION_TEMP.replace(PRION_FUNCTION_LBL, "prion_PUSH")
PRION_PUSH_FUNCTION_DEF = PRION_PUSH_FUNCTION_DEF.replace(PRION_OUTPUT_LBL, "stk")
PRION_PUSH_FUNCTION_DEF = PRION_PUSH_FUNCTION_DEF.replace(PRION_INPUT_LBL, "erc")

if PRION_NOISY:
	print("PRION_CORE::prion_PUSH::Defined as below")
	print(PRION_PUSH_FUNCTION_DEF)
	print("Assinged OPCODE: " + str(PRION_OPCODE_PUSH))
	print("---------------------------------------------")

###############################
# Indentaton matters here     #
# please be careful if        #
# this is moved around        #
###############################
exec(PRION_PUSH_FUNCTION_DEF) #
###############################

if PRION_LIBTEST:
	print("PRION_CORE::prion_PUSH::Compiled...testing")
	x = []
	print(str(x))
	prion_PUSH(x, 1)
	print(str(x))
	prion_PUSH(x, 2)
	print(str(x))
	prion_PUSH(x, 3)
	print(str(x))
	print("---------------------------------------------")

# prion_POP creation here
PRION_OPCODE_POP        = PRION_OPCODE_PUSH + 1
PRION_POP_FUNCTION_TEMP = """
def &FUNC(&OUT):
	if len(&OUT) > 0:
		&OUT.pop()
	if len(&OUT) == 0:
		&OUT.append(0)
	return
"""
PRION_POP_FUNCTION_DEF = PRION_POP_FUNCTION_TEMP.replace(PRION_FUNCTION_LBL, "prion_POP")
PRION_POP_FUNCTION_DEF = PRION_POP_FUNCTION_DEF.replace(PRION_OUTPUT_LBL, "stk")

if PRION_NOISY:
	print("PRION_CORE::prion_POP::Defined as below")
	print(PRION_POP_FUNCTION_DEF)
	print("Assinged OPCODE: " + str(PRION_OPCODE_POP))
	print("---------------------------------------------")

###############################
# Indentaton matters here     #
# please be careful if        #
# this is moved around        #
###############################
exec(PRION_POP_FUNCTION_DEF)  #
###############################

if PRION_LIBTEST:
	print("PRION_CORE::prion_POP::Compiled...testing")
	x = []
	print(str(x))
	prion_PUSH(x, 1)
	print(str(x))
	prion_PUSH(x, 2)
	print(str(x))
	prion_PUSH(x, 3)
	print(str(x))
	prion_POP(x)
	print(str(x))
	prion_POP(x)
	print(str(x))
	prion_POP(x)
	print(str(x))
	print("---------------------------------------------")
	print("PRION_CORE::Explicit test of empty stack pops")
	prion_POP(x)
	print(str(x))
	prion_POP(x)
	print(str(x))
	prion_POP(x)
	print(str(x))
	print("---------------------------------------------")

# Arithmetic Functions
# Now we build our more complex arithmetic  prion functions
# -- Addition # -- Subtraction # -- Multiplication # -- Division # These functions need to validate the stack and store and in some cases # temporary data. Some, like division, will require additional care # it must be protected from div/0 errors. Also we must ensure that # all functions leave the stack they operated on in a consistent # and meaningful state for the next operation (and they will not # know which operation is theoretically coming next in their raw form) # prion_ADD creation here PRION_OPCODE_ADD = PRION_OPCODE_POP + 1 PRION_ADD_FUNCTION_TEMP = """ def &FUNC(&OUT): if len(&OUT) > 1: &OUT.append(&OUT.pop() + &OUT.pop()) return if len(&OUT) == 0: &OUT.append(0) return return """ PRION_ADD_FUNCTION_DEF = PRION_ADD_FUNCTION_TEMP.replace(PRION_FUNCTION_LBL, "prion_ADD") PRION_ADD_FUNCTION_DEF = PRION_ADD_FUNCTION_DEF.replace(PRION_OUTPUT_LBL, "stk") if PRION_NOISY: print("PRION_CORE::prion_ADD::Defined as below") print(PRION_ADD_FUNCTION_DEF) print("Assinged OPCODE: " + str(PRION_OPCODE_ADD)) print("---------------------------------------------") ############################### # Indentaton matters here # # please be careful if # # this is moved around # ############################### exec(PRION_ADD_FUNCTION_DEF) # ############################### if PRION_LIBTEST: print("PRION_CORE::prion_ADD::Compiled...testing") x = [] print(str(x)) prion_PUSH(x, 1) print(str(x)) prion_PUSH(x, 2) print(str(x)) prion_PUSH(x, 3) print(str(x)) prion_ADD(x) print(str(x)) prion_ADD(x) print(str(x)) prion_ADD(x) print(str(x)) print("---------------------------------------------") # prion_SUB creation here PRION_OPCODE_SUB = PRION_OPCODE_ADD + 1 PRION_SUB_FUNCTION_TEMP = """ def &FUNC(&OUT): if len(&OUT) > 1: &OUT.append(&OUT.pop() - &OUT.pop()) return if len(&OUT) == 0: &OUT.append(0) return return """ PRION_SUB_FUNCTION_DEF = PRION_SUB_FUNCTION_TEMP.replace(PRION_FUNCTION_LBL, "prion_SUB") PRION_SUB_FUNCTION_DEF = PRION_SUB_FUNCTION_DEF.replace(PRION_OUTPUT_LBL, "stk") if PRION_NOISY: print("PRION_CORE::prion_SUB::Defined as below") print(PRION_SUB_FUNCTION_DEF) print("Assinged OPCODE: " + str(PRION_OPCODE_SUB)) print("---------------------------------------------") ############################### # Indentaton matters here # # please be careful if # # this is moved around # ############################### exec(PRION_SUB_FUNCTION_DEF) # ############################### if PRION_LIBTEST: print("PRION_CORE::prion_SUB::Compiled...testing") x = [] print(str(x)) prion_PUSH(x, 1) print(str(x)) prion_PUSH(x, 2) print(str(x)) prion_PUSH(x, 3) print(str(x)) prion_SUB(x) print(str(x)) prion_SUB(x) print(str(x)) prion_SUB(x) print(str(x)) print("---------------------------------------------") # prion_MULT creation here PRION_OPCODE_MULT = PRION_OPCODE_SUB + 1 PRION_MULT_FUNCTION_TEMP = """ def &FUNC(&OUT): if len(&OUT) > 1: &OUT.append(&OUT.pop() * &OUT.pop()) return if len(&OUT) == 0: &OUT.append(0) return return """ PRION_MULT_FUNCTION_DEF = PRION_MULT_FUNCTION_TEMP.replace(PRION_FUNCTION_LBL, "prion_MULT") PRION_MULT_FUNCTION_DEF = PRION_MULT_FUNCTION_DEF.replace(PRION_OUTPUT_LBL, "stk") if PRION_NOISY: print("PRION_CORE::prion_MULT::Defined as below") print(PRION_MULT_FUNCTION_DEF) print("Assinged OPCODE: " + str(PRION_OPCODE_MULT)) print("---------------------------------------------") ############################### # Indentaton matters here # # please be careful if # # this is moved around # ############################### exec(PRION_MULT_FUNCTION_DEF) # ############################### if PRION_LIBTEST: print("PRION_CORE::prion_MULT::Compiled...testing") x = [] print(str(x)) prion_PUSH(x, 1) print(str(x)) prion_PUSH(x, 2) print(str(x)) prion_PUSH(x, 3) print(str(x)) prion_MULT(x) print(str(x)) prion_MULT(x) print(str(x)) prion_MULT(x) print(str(x)) print("---------------------------------------------") # prion_DIV creation here PRION_OPCODE_DIV = PRION_OPCODE_MULT + 1 PRION_DIV_FUNCTION_TEMP = """ def &FUNC(&OUT): if len(&OUT) > 1: x1 = &OUT.pop() * 1.0 x2 = &OUT.pop() * 1.0 if x2 == 0.0: &OUT.append(x1) &OUT.append(x2) return &OUT.append(x1 / x2) return if len(&OUT) == 0: &OUT.append(0) return return """ PRION_DIV_FUNCTION_DEF = PRION_DIV_FUNCTION_TEMP.replace(PRION_FUNCTION_LBL, "prion_DIV") PRION_DIV_FUNCTION_DEF = PRION_DIV_FUNCTION_DEF.replace(PRION_OUTPUT_LBL, "stk") if PRION_NOISY: print("PRION_CORE::prion_DIV::Defined as below") print(PRION_DIV_FUNCTION_DEF) print("Assinged OPCODE: " + str(PRION_OPCODE_DIV)) print("---------------------------------------------") ############################### # Indentaton matters here # # please be careful if # # this is moved around # ############################### exec(PRION_DIV_FUNCTION_DEF) # ############################### if PRION_LIBTEST: print("PRION_CORE::prion_DIV::Compiled...testing") x = [] print(str(x)) prion_PUSH(x, 1) print(str(x)) prion_PUSH(x, 2) print(str(x)) prion_PUSH(x, 3) print(str(x)) prion_DIV(x) print(str(x)) prion_DIV(x) print(str(x)) prion_DIV(x) print(str(x)) print("---------------------------------------------") print("PRION_CORE::prion_DIV::Explicit Div/0 check") x = [] print(str(x)) prion_PUSH(x, 0) print(str(x)) prion_PUSH(x, 5) print(str(x)) prion_DIV(x) print(str(x)) prion_DIV(x) print(str(x)) print("---------------------------------------------") # prion_IF creation here PRION_OPCODE_IF = PRION_OPCODE_DIV + 1 PRION_IF_FUNCTION_TEMP = """ def &FUNC(&OUT): if len(&OUT) > 2: x1 = &OUT.pop() x2 = &OUT.pop() x3 = &OUT.pop() if x1 > x2: &OUT.append(x1) return &OUT.append(x3) return if len(&OUT) == 0: &OUT.append(0) return return """ PRION_IF_FUNCTION_DEF = PRION_IF_FUNCTION_TEMP.replace(PRION_FUNCTION_LBL, "prion_IF") PRION_IF_FUNCTION_DEF = PRION_IF_FUNCTION_DEF.replace(PRION_OUTPUT_LBL, "stk") if PRION_NOISY: print("PRION_CORE::prion_IF::Defined as below") print(PRION_IF_FUNCTION_DEF) print("Assinged OPCODE: " + str(PRION_OPCODE_IF)) print("---------------------------------------------") ############################### # Indentaton matters here # # please be careful if # # this is moved around # ############################### exec(PRION_IF_FUNCTION_DEF) # ############################### if PRION_LIBTEST: print("PRION_CORE::prion_IF::Compiled...testing") x = [] print(str(x)) prion_PUSH(x, 1) print(str(x)) prion_PUSH(x, 2) print(str(x)) prion_PUSH(x, 3) print(str(x)) prion_IF(x) print(str(x)) prion_IF(x) print(str(x)) prion_IF(x) print(str(x)) print("---------------------------------------------") ######################################################################## # You can define your own base functions in the area below ######################################################################## ######################################################################## # Here is where we package up our functional primitives nicely for # the consumption of classes that would have no interest in the above ######################################################################## PRION_OXF = { PRION_OPCODE_INP:prion_INP, PRION_OPCODE_PUSH:prion_PUSH, PRION_OPCODE_POP:prion_POP, PRION_OPCODE_ADD:prion_ADD, PRION_OPCODE_SUB:prion_SUB, PRION_OPCODE_MULT:prion_MULT, PRION_OPCODE_DIV:prion_DIV, PRION_OPCODE_IF:prion_IF } PRION_OXK = { PRION_OPCODE_INP:"INP", PRION_OPCODE_PUSH:"PUSH", PRION_OPCODE_POP:"POP", PRION_OPCODE_ADD:"ADD", PRION_OPCODE_SUB:"SUB", PRION_OPCODE_MULT:"MULT", PRION_OPCODE_DIV:"DIV", PRION_OPCODE_IF:"IF" } PRION_SXO = { "pop":PRION_OPCODE_POP, "+":PRION_OPCODE_ADD, "-":PRION_OPCODE_SUB, "*":PRION_OPCODE_MULT, "/":PRION_OPCODE_DIV, "?":PRION_OPCODE_IF } PRION_FSET = [ PRION_SXO[key] for key in PRION_SXO ] PRION_ERC_INT = 0 PRION_ERC_DBL = 1 PRION_MUTDEF = 0.05 PRION_O_IDX = 0 PRION_S_IDX = 1 PRION_I_IDX = 2 PRION_E_IDX = 2 PRION_F_SLCT = 1 PRION_I_SLCT = 2 PRION_E_SLCT = 3 ######################################################################## # The genome class is going to be what you will ultimately be using # pretty much constanly by every other object in the system. I am keeping # it in herently simple: # -- It has a type of ERC (Ephemoral Random Constant) # -- It has an intrisic idea of bouns for whatever the ERC requested is # -- It is capable of generating a single line of LGP code for our stack # processor (a codon) and you have at least three available types: # [INPUT, ERC, FUNCTION] # You can ask for a specific class of codon, or simply call the basic # codon() to get one of them randomly (with equal weight) # # A valid codon follows the three template types. each of the *codon() # functions will give the folowing types of returns # -- i_codon() = input_codon = [OPCODE, STACK_IDX, INPUT_IDX] # -- e_codon() = erc_codon = [OPCODE, STACK_IDX, ERC_VAL] # -- f_codon() = func_codon = [OPCODE, STACK_IDX] # # I append IDX to the stack and input identifiers since what we could be # looking at an array of inputs but also, if solving for multiple outputs, # we can have an arrsy of stack registers (the value left of the top of # the stack register once the sequence terminates will denote the "answer"). ######################################################################## class Genome: def __init__(self, wcount, scount, erc_low=0, erc_high=1, fset=PRION_FSET, erc_type=PRION_ERC_INT): self.wcount = wcount # Number of wires (Inputs) self.scount = scount # Number of registes (Outputs) self.fset = fset # Functions that this genome can offer (Defaults to PRION_FSET) self.erc_type = erc_type # Data type of ERC (Defaults to Integer) self.erc_low = erc_low # Lower bound of ERC (Defaults to 0) self.erc_high = erc_high # Higher bound of ERC (Defaults to 1) # -- e_codon() = erc_codon = [OPCODE, STACK_IDX, ERC_VAL] def e_codon(self): if self.erc_type == PRION_ERC_INT: return [PRION_OPCODE_PUSH, random.randint(0, self.scount - 1), random.randint(self.erc_low, self.erc_high)] if self.erc_type == PRION_ERC_FLOAT: return [PRION_OPCODE_PUSH, random.randint(0, self.scount - 1), random.uniform(self.erc_low, self.erc_high)] return [PRION_OPCODE_NOP] # -- i_codon() = input_codon = [OPCODE, STACK_IDX, INPUT_IDX] def i_codon(self): return [PRION_OPCODE_INP, random.randint(0, self.scount - 1), random.randint(0, self.wcount - 1)] # -- f_codon() = func_codon = [OPCODE, STACK_IDX] def f_codon(self): if len(self.fset) > 0: return [random.choice(self.fset), random.randint(0, self.scount - 1)] return [PRION_OPCODE_NOP] # return one of the three base codons with equal probability def codon(self): x = random.choice([PRION_F_SLCT, PRION_I_SLCT, PRION_E_SLCT]) if x == PRION_E_SLCT: return self.e_codon() if x == PRION_I_SLCT: return self.i_codon() if x == PRION_F_SLCT: return self.f_codon() return [PRION_OPCODE_NOP] ######################################################################## # The sequencer is where you would put all of your enhanced filters for # the genrated programs. This one is faily strightforward. Keep # requesting codons at randome until the disred length is reached # You can inherit and overload the sequence() function in derived classes # to suit your problem domain as necessary ######################################################################## class Sequencer: def __init__(self, genome): self.genome = genome def sequence(self, length): return [self.genome.codon() for x in range(length)] ######################################################################## # The decoder is another simple base class provided mainly as a # convenience to the user / researcher. It enables a sequence of valid # codons to be slightly more readable. This of it as a reverse assembler # for the virtual stack processor. ######################################################################## class Decoder: def __init__(self, genome): self.genome = genome def decode(self, prog): readable = "" for x in prog: readable += str(PRION_OXK[x[PRION_O_IDX]]) + "\t" + str(x[PRION_O_IDX:]) + "\n" return readable ######################################################################## # The processor is the base implementation of the virtual stack # processor on which the generated sequences will be run. It is assumed # the processor can tak an array of inputs (the size of which is contained # in the genome) and an array of stack registers (also specified in the # genome. The base processor takes a prion program and runs the sequence # against stacks which start with the initial state [0]. # This means that a stack register in this virtual processor will always # have an initial value ######################################################################## class Processor: def __init__(self, genome): self.genome = genome def evaluate(self, prog, inputs): stacks = [[0] for x in range(self.genome.scount)] for line in prog: if line[PRION_O_IDX] == PRION_OPCODE_INT: pass elif line[PRION_O_IDX] == PRION_OPCODE_NOP: pass elif line[PRION_O_IDX] == PRION_OPCODE_INP: stacks[line[PRION_S_IDX]].append(inputs[line[PRION_I_IDX]]) elif line[PRION_O_IDX] == PRION_OPCODE_PUSH: stacks[line[PRION_S_IDX]].append(line[PRION_E_IDX]) else: PRION_OXF[line[PRION_O_IDX]](stacks[line[PRION_S_IDX]]) return [x[0] for x in stacks] ######################################################################## # The splicer class is where we start seeing more of the biological # aspect of the prion object model. The base splicer performs a simple # crossover operation. It makes the base assumption that two sequences # can be of different lengths. To avoid sizing problems it will ensure # that it picks a cutpoint that will not exceed the bounds of either # sequence. ######################################################################## class Splicer: def __init__(self, genome): self.genome = genome def splice(self, prog1, prog2): s1 = len(prog1) s2 = len(prog2) if s1 == s2: cutpoint = random.randint(0, s1 - 1) return prog1[0:cutpoint] + prog2[cutpoint:] return prog1 ######################################################################## # The mutator also shows some of its roots in biological/evolutionary # processes. It is given a genome and a percentage p. If the # random.uniform(0.0, 100.0) returns less than or equal to p, the current # codon is replaced by a random one from the available genome ######################################################################## class Mutator: def __init__(self, genome): self.genome = genome def mutate(self, seq, p=PRION_MUTDEF): idx = 0 seqsz = len(seq) while seqsz != 0: if random.uniform(0.0, 100.0)