
import copy,itertools
"""
research language fuckin python

Nand Neural Network Arithmetical Logical Unit

"""

def Nand(boolLst):
    """
    a combination of nand gate can emulate all other logic gate
    it is a universal gate like nor

    >>> Nand([True,True])
    False
    >>> Nand([True,False])
    True
    >>> Nand([False,True])
    True
    >>> Nand([False,False])
    True

    >>> Nand([True,True,True])
    False
    >>> Nand([True,False,True])
    True
    >>> Nand([False,True,True])
    True
    >>> Nand([False,False,True])
    True
    >>> Nand([True,True,False])
    True
    >>> Nand([True,False,False])
    True
    >>> Nand([False,True,False])
    True
    >>> Nand([False,False,False])
    True

    """
    result = True
    for b in boolLst:
        result = result and b
    return not result



def generateNetwork(config,num,maxConnexion=2,width=3):
    """
    starting with config
    for every combination of key in config
    grouped by maxConnexion
    repeat [num] time

    assume [config] to have input key created
    assume [num] as amount of layer
    assume [maxConnexion] as number of connection per nand
    assume [width] as key width the generator use

    >>> generateNetwork({'A':'','B':''},2,2)
    {'A': '', 'AAD': ['A', 'AAA'], 'AAE': ['A', 'AAC'], 'B': '', 'AAA': ['A'], 'AAC': ['B'], 'AAB': ['A', 'B'], 'AAM': ['AAC'], 'AAL': ['AAA', 'AAB'], 'AAO': ['AAB'], 'AAN': ['AAC', 'AAB'], 'AAI': ['B', 'AAB'], 'AAG': ['B', 'AAA'], 'AAK': ['AAA', 'AAC'], 'AAH': ['B', 'AAC'], 'AAF': ['A', 'AAB'], 'AAJ': ['AAA']}
    """
    cfg = copy.deepcopy(config)
    serie  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    keyLst = (   
        key 
        for key in itertools.imap(''.join, itertools.product(serie, repeat=width))  
    )
    nonRepeat = set()
    for n in range(num):
        refkeyLst = [
            list(set(keyset)) 
            for keyset in itertools.product(cfg.keys(),repeat=maxConnexion) 
        ]
        for refkey in refkeyLst:
            hash = ''.join(refkey)
            if hash in nonRepeat:
                continue
            nonRepeat.add(hash)
            newKey = next(keyLst)
            cfg[newKey] = refkey
    return cfg




nopeConfig = { 'C' : False }
yesConfig = { 'C' : True }

notConfig = { 
    'A' : 'input',
    'C' : ['A']
}
notTraining = [
    {
        'from':{ 'A':False },
        'expect':{ 'C':True }
    },
    {
        'from':{ 'A':True },
        'expect':{ 'C':False }
    }    
]


nandConfig = {
    'A' : 'first input overwrited',
    'B' : 'second input overwrited',
    'C' : ['A','B']
}
nandTraining = [
    {
        'from':{ 'A':False, 'B':False},
        'expect':{ 'C':True }
    },
    {
        'from':{ 'A':False, 'B':True },
        'expect':{ 'C':True }
    },
    {
        'from':{ 'A':True, 'B':False },
        'expect':{ 'C':True }
    },
    {
        'from':{ 'A':True, 'B':True },
        'expect':{ 'C':False }
    }
]

andConfig = {
    'A' : 'input',
    'B' : 'input',
    'AAA' : ['A','B'],
    'C' : ['AAA']
}
andTraining = [
    {
        'from': { 'A' : False, 'B' : False},
        'expect' : { 'C' : False }
    },
    {
        'from': { 'A' : True, 'B' : False},
        'expect' : { 'C' : False }
    },
    {
        'from': { 'A' : False, 'B' : True},
        'expect' : { 'C' : False }
    },
    {
        'from': { 'A' : True, 'B' : True},
        'expect' : { 'C' : True }
    },
]

xorConfig = {
    'A' : 'input',
    'B' : 'input',
    'C' : ['AAC', 'AAB'],
    'AAC' : ['AAA','A'],
    'AAB' : ['AAA','B'],
    'AAA' : ['A','B']
}
xorTraining = [
    {   
        'from' : { 'A' : True, 'B' : True },
        'expect' : { 'C' : False }
    },
    {   
        'from' : { 'A' : True, 'B' : False },
        'expect' : { 'C' : True }
    },
    {   
        'from' : { 'A' : False, 'B' : True },
        'expect' : { 'C' : True }
    },
    {   
        'from' : { 'A' : False, 'B' : False },
        'expect' : { 'C' : False }
    }
]



def resolve(config, input,operation=Nand):
    """
    neuron configuration is a dictionary
    key are text
    value can be array, string or bool
        string mean true
        bool mean either true or false
        array contain key
        all recursive key throw exception
        all unresolved key throw exception
    foreach key try to replace all value into bool
        if its already a bool, skip
        if its an array and contain all bool, replace with the nand of all these
        if its an array check every key and replace them who point to a bool with the said bool
    once all key become a bool, return the set

    >>> resolve(xorConfig,xorTraining[0]['from'])['C']
    False
    >>> resolve(xorConfig,xorTraining[1]['from'])['C']
    True
    >>> resolve(xorConfig,xorTraining[2]['from'])['C']
    True
    >>> resolve(xorConfig,xorTraining[3]['from'])['C']
    False

    """
    cfg = copy.deepcopy(config)
    cfg.update(input)
    for n in range(1000):
        if not [True for v in cfg.values() if isinstance(v,list)]:
            return cfg
        for k,v in cfg.items():
            if isinstance(v,list):
                nv = [ cfg[e] if isinstance(e,str) and isinstance(cfg[e],bool) else e for e in v ]
                if not [True for e in nv if isinstance(e,str)]:
                    nv = operation(nv)
                cfg[k] = nv
    return cfg 
 
def test(config,training):
    """

    >>> test(notConfig,notTraining)
    True
    >>> test(nandConfig,nandTraining)
    True
    >>> test(andConfig,andTraining)
    True
    >>> test(xorConfig,xorTraining)
    True

    """
    for rule in training:
        ret = resolve(config,rule['from'])
        for key,expect in rule['expect'].items():
            if expect != ret[key]:
                return False
    return True

def train(training,config):
    """
    neuron training is an array
    each element contain a dictionary
        where from contain a dictionary
            of key,bool value to overide
        where expect contain another dictionary
            of key,bool value expected after process
        more key may help explain the rule


    >>> network = generateNetwork({'A':'','B':''},2,2)
    >>> print network
    {'A': '', 'AAD': ['A', 'AAA'], 'AAE': ['A', 'AAC'], 'B': '', 'AAA': ['A'], 'AAC': ['B'], 'AAB': ['A', 'B'], 'AAM': ['AAC'], 'AAL': ['AAA', 'AAB'], 'AAO': ['AAB'], 'AAN': ['AAC', 'AAB'], 'AAI': ['B', 'AAB'], 'AAG': ['B', 'AAA'], 'AAK': ['AAA', 'AAC'], 'AAH': ['B', 'AAC'], 'AAF': ['A', 'AAB'], 'AAJ': ['AAA']}

    >>> train(notTraining,network)
    {'A': '', 'C': ['A']}
    
    >>> train(andTraining,network)
    {'A': '', 'C': ['AAB'], 'B': '', 'AAB': ['A', 'B']}

    >>> network = generateNetwork({'A':'','B':''},3,2)
    >>> train(xorTraining,network)
    {'A': '', 'C': ['AAF', 'AAI'], 'AAF': ['A', 'AAB'], 'AAB': ['A', 'B'], 'AAI': ['B', 'AAB'], 'B': ''}
    """
    answerIs = {}
    for rule in training:
        ret = resolve(config,rule['from'])
        for key,expect in rule['expect'].items():
            potentialLst = []
            for potential,result in ret.items():
                if result == expect:
                    potentialLst.append(potential)
            if not key in answerIs:
                answerIs[key] = set(potentialLst)
            else:
                answerIs[key] = answerIs[key].intersection(potentialLst)
    cfg = copy.deepcopy(config)
    for key,setKey in answerIs.items():
        if not setKey:
            raise Exception("""not solved with training """ + str(training) + " and network  " + str(cfg))
        keyLst = list(setKey)[:1]
        cfg[key] = cfg[keyLst[0]]
        answerIs[key] = cfg[keyLst[0]]
    #clean out unnecessary neurons
    missingkey = [ key for keyLst in answerIs.values() for key in keyLst ]
    while missingkey:
        for key in missingkey:
            answerIs[key] = config[key]
        missingkey = [  refkey
                        for key,keyLst in answerIs.items()
                        if isinstance(keyLst,list)
                        for refkey in keyLst
                        if not refkey in answerIs   ]
    return answerIs

def train2(training,config):
    """
    better coded version under construction

    >>> network = generateNetwork({'A':'','B':''},3,2)
    >>> training = [
    ... {
    ...    'from' : {'A':True,'B':True},
    ...    'expect' : {'OR':True, 'AND':True, 'XOR':False}
    ... },
    ... {
    ...    'from' : {'A':False,'B':True},
    ...    'expect' : {'OR':True, 'AND':False, 'XOR':True}
    ... },
    ... {
    ...    'from' : {'A':True,'B':False},
    ...    'expect' : {'OR':True, 'AND':False, 'XOR':True}
    ... },
    ... {
    ...    'from' : {'A':False,'B':False},
    ...    'expect' : {'OR':False, 'AND':False, 'XOR':False}
    ... }
    ... ]
    >>> train2(training,network)

    """
    allInputKey = [
        key
        for rule in training
        for key in rule['from']
    ]
    allOutputKey = list(set([
        key
        for rule in training
        for key in rule['expect']
    ]))
    allOutputRedirectionProduct = ( 
        zip(allOutputKey,keytpl)
        for keytpl in itertools.product(config.keys(),repeat=len(allOutputKey))
    )

    return [next(allOutputRedirectionProduct)]

if __name__ == '__main__':
    print """
#######################################
#                                     #
#       nand neural network           #
#                                     #
#######################################
    """
    import doctest
    doctest.testmod()
    print 'tested'

    nlevel = 3
    nlink = 2
    baseConfig = {'A':'first input','B':'second config'}
    trainingRule = copy.deepcopy(xorTraining)
    for rule in trainingRule:
        rule['expect']['D'] = not rule['expect']['C']


    print "generate ",str(nlevel)," level and ",nlink," link config from ", baseConfig
    networkConfig = generateNetwork(baseConfig,3,2)
    print 'network size :', len(networkConfig)

    print "train for C = XOR A B and D = NXOR A B with", trainingRule
    trainedConfig = train(trainingRule,networkConfig)
    print "done"

    print trainedConfig
