# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---
# # Linear Separability
#  To check for linear separability of a Boolean function, write your own computer program to train a simple perceptron with four input terminals, and one output $O^{(\mu)}={\text{tanh}}[\frac{1}{2}(-\theta+\sum_{i=1}^4 w_i x_i^{(\mu)})] \quad$ where $w_i$ are the weights, $\theta$ is the threshold, and $x_i^{(\mu)}$ is the i-th component of the $\mu$-th input pattern. Train the perceptron by implementing stochastic gradient descent (sequential training) on the energy function $H = \frac{1}{2}\sum_\mu \left (t^{(\mu)} - O^{(\mu)} \right )^2$. Set the learning rate $\eta$ to 0.02. Initialise the weights to random numbers uniformly distributed in the range [-0.2,0.2]. Initialise the threshold to a random number uniformly distributed in the range [−1,1].
#
# For each function perform a maximum of $10^5$ updates. If you find that the network classifies all inputs of a function correctly, i.e. if $\text{sgn}(O^{(\mu)})=t^{(\mu)}$ for $\mu=1,2,\ldots,16$, the function is linearly separable and the learning should be stopped. If correct classification is not achieved within the learning time, this may mean that the function is either not linearly separable, or that your algorithm is stuck in a local mimimum. In such cases, you need to repeat the learning at least 10 times using the same procedure. Conclude that the function is not linearly separable if you do not achieve correct classification for any of the repetitions.
#
# Based on the results from your stochastic gradient-descent algorithm, answer which of the functions given below are linearly separable by ticking the corresponding box(es). 
# +
import numpy as np
import numpy.random as rnd
import csv
def calculate_output(threshold, weight_matrix, x, my):
    inner_sum = 0
    for i in range(len(x[0])):
        inner_sum+= weight_matrix[i]*x[my][i]
    return np.tanh(0.5*(inner_sum-threshold))
def calculate_energy_function(t,O):
    inner_sum = 0
    for my in len(t):
        inner_sum+= np.power(t[my] - O[my],2)
    return 0.5*inner_sum

def calculate_gradient(t, weight_matrix, x, threshold, learning_rate = 0.02, my_list = range(16)):
    #Let's do it without matrix multiplication first and later think of a solution where we dont have 3 for loops
    dw = np.zeros(w_ij.shape)
    for i in range(len(w_ij)):
        inner_sum = 0
        for my in my_list:
            O_my = calculate_output(threshold, weight_matrix,x, my)
            #Here the  order of x[my][i] is important!
            inner_sum += (t[my] - O_my)*x[my][i]
        dw[i] = learning_rate*inner_sum
    return dw    
def esign(i):
    if i == 0:
        return 1
    return np.sign(i)
#Targe vectors
A =  [-1, -1, 1, 1, -1, 1, 1, 1, -1, -1, -1, -1, 1, 1, 1, 1] 
B = [1, 1, 1, 1, 1, -1, -1, -1, 1, -1, 1, -1, -1, -1, -1, -1] 
C = [-1, -1, -1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1, 1, 1] 
D = [1, 1, 1, 1, -1, -1, -1, -1, -1, 1, -1, -1, 1, -1, -1, -1] 
E = [-1, -1, 1, 1, -1, 1, 1, -1, 1, -1, -1, 1, -1, -1, 1, -1] 
F = [-1, -1, -1, 1, -1, 1, 1, 1, -1, 1, 1, -1, 1, 1, 1, -1] 
G = [1, 1, -1, 1, -1, -1, 1, -1, 1, -1, 1, 1, -1, 1, 1, -1]
#x vector
with open("../data/input_data_numeric.csv","r") as f:
    x = list(csv.reader(f, delimiter=","))
    for idx, el in enumerate(x):
        x[idx] = list(map(int,el))
    x = np.array(x)[:,1:]
#Initialize variables with random inputs
for t in (A, B,C,D,E,F):
    for j in range(10):
        w_ij = (rnd.rand(4,)*0.4)-np.ones(4,)*0.2
        threshold = rnd.random_sample()*2 -1
        learning_rate = 0.02
        for i in range(100000):
            dw = calculate_gradient(t,w_ij,x, threshold)
            w_ij += dw
            #We need to test!
            finished = True
            for el in range(16):
                finished = finished and (t[el] == esign(calculate_output(threshold, w_ij, x, el)))
            if finished:
                break
        if finished:
            break
    print(finished)