import numpy as np
import time
import h5py
import matplotlib.pyplot as plt
import scipy
from sklearn.model_selection import train_test_split
from parse_coverages import get_tests_matrix
import argparse

# from PIL import Image
 
# %matplotlib inline
# plt.rcParams['figure.figsize'] = (5.0, 4.0) # set default size of plots
# plt.rcParams['image.interpolation'] = 'nearest'
# plt.rcParams['image.cmap'] = 'gray'
 
# %load_ext autoreload
# %autoreload 2
 
np.random.seed(1)

def sigmoid(Z):
    """
    Implements the sigmoid activation in numpy
    
    Arguments:
    Z -- numpy array of any shape
    
    Returns:
    A -- output of sigmoid(z), same shape as Z
    cache -- returns Z as well, useful during backpropagation
    """
    
    A = 1/(1+np.exp(-Z))
    cache = Z
    
    return A, cache

def sigmoid_backward(dA, cache):
    """
    Implement the backward propagation for a single SIGMOID unit.
 
    Arguments:
    dA -- post-activation gradient, of any shape
    cache -- 'Z' where we store for computing backward propagation efficiently
 
    Returns:
    dZ -- Gradient of the cost with respect to Z
    """
    Z = cache
    
    s = 1/(1+np.exp(-Z))
    dA = np.squeeze(np.asarray(dA))
    s = np.squeeze(np.asarray(s))
    dZ = dA * s * (1-s)

    if (Z.shape[0] == 1):
        dZ = dZ.reshape((1, dZ.shape[0]))
    
    assert (dZ.shape == Z.shape)
    
    return dZ

def relu(Z):
    """
    Implement the RELU function.
 
    Arguments:
    Z -- Output of the linear layer, of any shape
 
    Returns:
    A -- Post-activation parameter, of the same shape as Z
    cache -- a python dictionary containing "A" ; stored for computing the backward pass efficiently
    """
    
    A = np.maximum(0,Z)
    
    assert(A.shape == Z.shape)
    
    cache = Z 
    return A, cache

def relu_backward(dA, cache):
    """
    Implement the backward propagation for a single RELU unit.
 
    Arguments:
    dA -- post-activation gradient, of any shape
    cache -- 'Z' where we store for computing backward propagation efficiently
 
    Returns:
    dZ -- Gradient of the cost with respect to Z
    """
    
    Z = cache
    dZ = np.array(dA, copy=True) # just converting dz to a correct object.
    
    # When z <= 0, you should set dz to 0 as well. 
    dZ[Z <= 0] = 0
    
    assert (dZ.shape == Z.shape)
    
    return dZ

def initialize_parameters_deep(layer_dims):
    """
    Arguments:
    layer_dims -- python array (list) containing the dimensions of each layer in our network
    
    Returns:
    parameters -- python dictionary containing your parameters "W1", "b1", ..., "WL", "bL":
                    Wl -- weight matrix of shape (layer_dims[l], layer_dims[l-1])
                    bl -- bias vector of shape (layer_dims[l], 1)
    """
    
    np.random.seed(5)
    parameters = {}
    L = len(layer_dims)            # number of layers in the network
 
    for l in range(1, L):
        parameters['W' + str(l)] = np.random.randn(layer_dims[l], layer_dims[l-1]) * 0.01
        parameters['b' + str(l)] = np.zeros((layer_dims[l], 1))
        assert(parameters['W' + str(l)].shape == (layer_dims[l], layer_dims[l-1]))
        assert(parameters['b' + str(l)].shape == (layer_dims[l], 1))
    # print(parameters)
    return parameters

def linear_forward(A, W, b):
    """
    Implement the linear part of a layer's forward propagation.
 
    Arguments:
    A -- activations from previous layer (or input data): (size of previous layer, number of examples)
    W -- weights matrix: numpy array of shape (size of current layer, size of previous layer)
    b -- bias vector, numpy array of shape (size of the current layer, 1)
 
    Returns:
    Z -- the input of the activation function, also called pre-activation parameter 
    cache -- a python dictionary containing "A", "W" and "b" ; stored for computing the backward pass efficiently
    """
    # print ('linear_forward')
    # print ('W',W)
    # print ('A',A)
    # print ('b',b)
    Z = np.dot(W, A) + b
    # print ('Z',Z)

    assert(Z.shape == (W.shape[0], A.shape[1]))
    cache = (A, W, b)
    
    return Z, cache

def linear_activation_forward(A_prev, W, b, activation):
    """
    Implement the forward propagation for the LINEAR->ACTIVATION layer
 
    Arguments:
    A_prev -- activations from previous layer (or input data): (size of previous layer, number of examples)
    W -- weights matrix: numpy array of shape (size of current layer, size of previous layer)
    b -- bias vector, numpy array of shape (size of the current layer, 1)
    activation -- the activation to be used in this layer, stored as a text string: "sigmoid" or "relu"
 
    Returns:
    A -- the output of the activation function, also called the post-activation value 
    cache -- a python dictionary containing "linear_cache" and "activation_cache";
             stored for computing the backward pass efficiently
    """
    if activation == "sigmoid":
        # Inputs: "A_prev, W, b". Outputs: "A, activation_cache".
        Z, linear_cache = linear_forward(A_prev, W, b)
        A, activation_cache = sigmoid(Z)
    
    elif activation == "relu":
        # Inputs: "A_prev, W, b". Outputs: "A, activation_cache".
        Z, linear_cache = linear_forward(A_prev, W, b)
        A, activation_cache = relu(Z)
    
    assert (A.shape == (W.shape[0], A_prev.shape[1]))
    cache = (linear_cache, activation_cache)

    return A, cache

def L_model_forward(X, parameters):
    """
    Implement forward propagation for the [LINEAR->RELU]*(L-1)->LINEAR->SIGMOID computation
    
    Arguments:
    X -- data, numpy array of shape (input size, number of examples)
    parameters -- output of initialize_parameters_deep()
    
    Returns:
    AL -- last post-activation value
    caches -- list of caches containing:
                every cache of linear_relu_forward() (there are L-1 of them, indexed from 0 to L-2)
                the cache of linear_sigmoid_forward() (there is one, indexed L-1)
    """
    caches = []
    A = X
    L = len(parameters) // 2                  # number of layers in the neural network
    
    # Implement [LINEAR -> RELU]*(L-1). Add "cache" to the "caches" list.
    for l in range(1, L):
        A_prev = A 
        A, cache = linear_activation_forward(A_prev, parameters['W' + str(l)], parameters['b' + str(l)], "sigmoid")
        caches.append(cache)
    
    # Implement LINEAR -> SIGMOID. Add "cache" to the "caches" list.
    AL, cache = linear_activation_forward(A, parameters['W' + str(L)], parameters['b' + str(L)], "sigmoid")
    caches.append(cache)
    assert(AL.shape == (1,X.shape[1]))
    return AL, caches

def compute_cost(AL, Y):
    """
    Implement the cost function defined by equation (7).
 
    Arguments:
    AL -- probability vector corresponding to your label predictions, shape (1, number of examples)
    Y -- true "label" vector (for example: containing 0 if non-cat, 1 if cat), shape (1, number of examples)
 
    Returns:
    cost -- cross-entropy cost
    """
    
    m = Y.shape[1]
 
    # Compute loss from aL and y.
    cost = -np.sum(np.multiply(np.log(AL),Y) + np.multiply(np.log(1 - AL), 1 - Y)) / m
    
    cost = np.squeeze(cost)
    assert(cost.shape == ())
    
    return cost

def linear_backward(dZ, cache):
    """
    Implement the linear portion of backward propagation for a single layer (layer l)
 
    Arguments:
    dZ -- Gradient of the cost with respect to the linear output (of current layer l)
    cache -- tuple of values (A_prev, W, b) coming from the forward propagation in the current layer
 
    Returns:
    dA_prev -- Gradient of the cost with respect to the activation (of the previous layer l-1), same shape as A_prev
    dW -- Gradient of the cost with respect to W (current layer l), same shape as W
    db -- Gradient of the cost with respect to b (current layer l), same shape as b
    """
    A_prev, W, b = cache
    m = A_prev.shape[1]
 
    dW = np.dot(dZ, A_prev.T) / m
    db = np.sum(dZ, axis=1, keepdims=True) / m
    dA_prev = np.dot(W.T, dZ)
    
    assert (dA_prev.shape == A_prev.shape)
    assert (dW.shape == W.shape)
    assert (db.shape == b.shape)
    
    return dA_prev, dW, db

def linear_activation_backward(dA, cache, activation):
    """
    Implement the backward propagation for the LINEAR->ACTIVATION layer.
    
    Arguments:
    dA -- post-activation gradient for current layer l 
    cache -- tuple of values (linear_cache, activation_cache) we store for computing backward propagation efficiently
    activation -- the activation to be used in this layer, stored as a text string: "sigmoid" or "relu"
    
    Returns:
    dA_prev -- Gradient of the cost with respect to the activation (of the previous layer l-1), same shape as A_prev
    dW -- Gradient of the cost with respect to W (current layer l), same shape as W
    db -- Gradient of the cost with respect to b (current layer l), same shape as b
    """
    linear_cache, activation_cache = cache
    
    if activation == "relu":
        dZ = relu_backward(dA, activation_cache)
        dA_prev, dW, db = linear_backward(dZ, linear_cache)
        
    elif activation == "sigmoid":
        dZ = sigmoid_backward(dA, activation_cache)
        dA_prev, dW, db = linear_backward(dZ, linear_cache)
    
    return dA_prev, dW, db

def L_model_backward(AL, Y, caches):
    """
    Implement the backward propagation for the [LINEAR->RELU] * (L-1) -> LINEAR -> SIGMOID group
    
    Arguments:
    AL -- probability vector, output of the forward propagation (L_model_forward())
    Y -- true "label" vector (containing 0 if non-cat, 1 if cat)
    caches -- list of caches containing:
                every cache of linear_activation_forward() with "relu" (it's caches[l], for l in range(L-1) i.e l = 0...L-2)
                the cache of linear_activation_forward() with "sigmoid" (it's caches[L-1])
    
    Returns:
    grads -- A dictionary with the gradients
             grads["dA" + str(l)] = ... 
             grads["dW" + str(l)] = ...
             grads["db" + str(l)] = ... 
    """
    grads = {}
    L = len(caches) # the number of layers
    m = AL.shape[1]
    Y = Y.reshape(AL.shape) # after this line, Y is the same shape as AL
    
    # Initializing the backpropagation
    dAL = - (np.divide(Y, AL) - np.divide(1 - Y, 1 - AL))
    
    # Lth layer (SIGMOID -> LINEAR) gradients. Inputs: "AL, Y, caches". Outputs: "grads["dAL"], grads["dWL"], grads["dbL"]
    current_cache = caches[L-1]
    grads["dA" + str(L)], grads["dW" + str(L)], grads["db" + str(L)] = linear_activation_backward(dAL, current_cache, "sigmoid")
    
    for l in reversed(range(L-1)):
        # lth layer: (RELU -> LINEAR) gradients.
        # Inputs: "grads["dA" + str(l + 2)], caches". Outputs: "grads["dA" + str(l + 1)] , grads["dW" + str(l + 1)] , grads["db" + str(l + 1)] 
        current_cache = caches[l]
        dA_prev_temp, dW_temp, db_temp = linear_activation_backward(grads["dA" + str(l + 2)], current_cache, "sigmoid")
        grads["dA" + str(l + 1)] = dA_prev_temp
        grads["dW" + str(l + 1)] = dW_temp
        grads["db" + str(l + 1)] = db_temp
 
    return grads

def update_parameters(parameters, grads, learning_rate):
    """
    Update parameters using gradient descent
    
    Arguments:
    parameters -- python dictionary containing your parameters 
    grads -- python dictionary containing your gradients, output of L_model_backward
    
    Returns:
    parameters -- python dictionary containing your updated parameters 
                  parameters["W" + str(l)] = ... 
                  parameters["b" + str(l)] = ...
    """
    
    L = len(parameters) // 2 # number of layers in the neural network
 
    # Update rule for each parameter. Use a for loop.
    for l in range(L):
        parameters["W" + str(l+1)] = parameters["W" + str(l+1)] - learning_rate * grads["dW" + str(l+1)]
        parameters["b" + str(l+1)] = parameters["b" + str(l+1)] - learning_rate * grads["db" + str(l+1)]
        
    return parameters

def L_layer_model(X, Y, layers_dims, learning_rate = 0.0075, num_iterations = 3000, print_cost=False, filename='plot.png'):#lr was 0.009
    """
    Implements a L-layer neural network: [LINEAR->RELU]*(L-1)->LINEAR->SIGMOID.
    
    Arguments:
    X -- data, numpy array of shape (number of examples, input number)
    Y -- true "label" vector (containing 0 if cat, 1 if non-cat), of shape (1, number of examples)
    layers_dims -- list containing the input size and each layer size, of length (number of layers + 1).
    learning_rate -- learning rate of the gradient descent update rule
    num_iterations -- number of iterations of the optimization loop
    print_cost -- if True, it prints the cost every 100 steps
    
    Returns:
    parameters -- parameters learnt by the model. They can then be used to predict.
    """
    np.random.seed(1)
    costs = [] # keep track of cost

    # Parameters initialization.
    parameters = initialize_parameters_deep(layers_dims)
    
    # Loop (gradient descent)
    for i in range(0, num_iterations):
        # print ('iteration:', i, '---------------------------')
        # Forward propagation: [LINEAR -> RELU]*(L-1) -> LINEAR -> SIGMOID.
        AL, caches = L_model_forward(X.T, parameters)

        # Compute cost.
        cost = compute_cost(AL, Y)
    
        # Backward propagation.
        grads = L_model_backward(AL, Y, caches)
 
        # Update parameters.
        parameters = update_parameters(parameters, grads, learning_rate)
                
        # Print the cost every 100 training example
        if print_cost and i % 100 == 0:
            print ("Cost after iteration %i: %f" %(i, cost))
            print (AL)
        if print_cost and i % 100 == 0:
            costs.append(cost)
            
    # plot the cost
    plt.plot(np.squeeze(costs))
    plt.ylabel('cost')
    plt.xlabel('iterations (per tens)')
    plt.title("Learning rate =" + str(learning_rate))
    # plt.show()
    plt.savefig(filename)
    
    return parameters

def getData(m):
    matrix = np.matrix(m).astype(int)
    labels = np.squeeze(np.asarray(matrix[:, -1]))
    dataset = matrix[:, 0:-1]
    return dataset, labels


def getData1():
    # in this case; num_features = num_statements
    # num_input = num_test_cases
    dataset = np.matrix([
        [1,1,1,1,0,1,0,0,1],
        [1,0,0,0,1,1,1,1,0],
        [0,0,0,0,0,1,1,0,0],
        [1,1,0,0,1,0,1,1,1],
        [1,1,1,0,1,1,1,1,1],
        [0,0,1,0,0,1,1,1,0],
        [1,1,1,1,0,1,0,1,1]
    ]).astype(int)
    # in labels, 0 means success, 1 means failure
    #labels = np.array([[0],[0],[0],[0],[0],[1],[1]])
    labels = np.array([0,0,0,0,0,1,1])
    # transform the labels to one-hot format
    labels_onehot = np.zeros((labels.shape[0], 2)).astype(int)
    labels_onehot[np.arange(len(labels)), labels.astype(int)] = 1
    # # divide the dataset into train and test datasets
    # train_dataset, test_dataset, \
    # train_labels, test_labels = train_test_split(
    #     dataset, labels, test_size = .1, random_state = 12)
    return dataset, labels

def getDataTest(dim):
    # estimate the suspiciousness of each statement
    test_susp_dataset = np.identity(dim)
    return test_susp_dataset

def predict(X, parameters):
    """
    This function is used to predict the results of a  L-layer neural network.
    
    Arguments:
    X -- data set of examples you would like to label
    parameters -- parameters of the trained model
    
    Returns:
    p -- predictions for the given dataset X
    """
    
    m = X.shape[1]
    n = len(parameters) // 2 # number of layers in the neural network
    # p = np.zeros((1,m))
    
    # Forward propagation
    probas, caches = L_model_forward(X, parameters)
 
    # # convert probas to 0/1 predictions
    # for i in range(0, probas.shape[1]):
    #     if probas[0,i] > 0.5:
    #         p[0,i] = 1
    #     else:
    #         p[0,i] = 0
    
    # print("Accuracy: "  + str(np.sum((p == y)/m)))
        
    return probas

def insertonSort(alist):
    index = [x for x in range(len(alist))]
    rank = [1 for x in range(len(alist))]
    for i in range(len(alist)):
        key = alist[i]
        val = index[i]
        j = i - 1 
        while j >= 0 and alist[j] > key:
            alist[j+1] = alist[j]
            index[j+1] = index[j]
            j -= 1
        alist[j+1] = key
        index[j+1] = val
    ranking = 1
    for i in range(len(alist)-1,0,-1):
        ranking += 1
        if alist[i] == alist[i-1]:
            rank[index[i-1]] = rank[index[i]]
        else:
            rank[index[i-1]] = ranking
    return rank,index

def train(train_dataset, train_labels):
    layers_dims = [train_dataset.shape[1],5,5,1]
    train_labels = np.array([train_labels])
    parameters = L_layer_model(train_dataset, train_labels, layers_dims, learning_rate = 0.3, num_iterations = 15000, print_cost = True, filename='case2.1.png')
    return parameters

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Run BPNN to locate fault against coverage XML files')
    parser.add_argument('source', type=str, help='name of the folder containing coverage .xml file')
    parser.add_argument('prog_name', type=str, help='name of program, i.e. \'sort\' for sort.py')

    args = parser.parse_args()

    matrix = get_tests_matrix(args.source, args.prog_name)

    train_dataset, train_labels = getData(matrix)
    
    # print(train_dataset)
    # print(train_labels)
    params = train(train_dataset, train_labels)
    # print(params)
    test_dataset = getDataTest(train_dataset.shape[1])
    result = predict(test_dataset, params)
    print(result)
    #sus = result
    # print(np.squeeze(np.asarray(result)))
    rank, index= insertonSort(np.squeeze(np.asarray(result)))
    # print ("sus: ",sus,"\n")
    # print ("sorted: ",result,"\n")
    # print ("index:", index,"\n")
    # print ("rank is :",rank,"\n")
    # print ("The most buggy statement is: Statement No.", rank.index(1)+1)
    #print(len(rank))
    for i in range(len(rank)-1,-1,-1):
        #print("i is:", i)
        print("Statement {:>2}: {:>4}".format(index[i]+1,rank[index[i]]))
import numpy as np
import time
import h5py
import matplotlib.pyplot as plt
import scipy
from sklearn.model_selection import train_test_split
from parse_coverages import get_tests_matrix
import argparse

# from PIL import Image
 
# %matplotlib inline
# plt.rcParams['figure.figsize'] = (5.0, 4.0) # set default size of plots
# plt.rcParams['image.interpolation'] = 'nearest'
# plt.rcParams['image.cmap'] = 'gray'
 
# %load_ext autoreload
# %autoreload 2
 
np.random.seed(1)

def sigmoid(Z):
    """
    Implements the sigmoid activation in numpy
    
    Arguments:
    Z -- numpy array of any shape
    
    Returns:
    A -- output of sigmoid(z), same shape as Z
    cache -- returns Z as well, useful during backpropagation
    """
    
    A = 1/(1+np.exp(-Z))
    cache = Z
    
    return A, cache

def sigmoid_backward(dA, cache):
    """
    Implement the backward propagation for a single SIGMOID unit.
 
    Arguments:
    dA -- post-activation gradient, of any shape
    cache -- 'Z' where we store for computing backward propagation efficiently
 
    Returns:
    dZ -- Gradient of the cost with respect to Z
    """
    Z = cache
    
    s = 1/(1+np.exp(-Z))
    dA = np.squeeze(np.asarray(dA))
    s = np.squeeze(np.asarray(s))
    dZ = dA * s * (1-s)

    if (Z.shape[0] == 1):
        dZ = dZ.reshape((1, dZ.shape[0]))
    
    assert (dZ.shape == Z.shape)
    
    return dZ

def relu(Z):
    """
    Implement the RELU function.
 
    Arguments:
    Z -- Output of the linear layer, of any shape
 
    Returns:
    A -- Post-activation parameter, of the same shape as Z
    cache -- a python dictionary containing "A" ; stored for computing the backward pass efficiently
    """
    
    A = np.maximum(0,Z)
    
    assert(A.shape == Z.shape)
    
    cache = Z 
    return A, cache

def relu_backward(dA, cache):
    """
    Implement the backward propagation for a single RELU unit.
 
    Arguments:
    dA -- post-activation gradient, of any shape
    cache -- 'Z' where we store for computing backward propagation efficiently
 
    Returns:
    dZ -- Gradient of the cost with respect to Z
    """
    
    Z = cache
    dZ = np.array(dA, copy=True) # just converting dz to a correct object.
    
    # When z <= 0, you should set dz to 0 as well. 
    dZ[Z <= 0] = 0
    
    assert (dZ.shape == Z.shape)
    
    return dZ

def initialize_parameters_deep(layer_dims):
    """
    Arguments:
    layer_dims -- python array (list) containing the dimensions of each layer in our network
    
    Returns:
    parameters -- python dictionary containing your parameters "W1", "b1", ..., "WL", "bL":
                    Wl -- weight matrix of shape (layer_dims[l], layer_dims[l-1])
                    bl -- bias vector of shape (layer_dims[l], 1)
    """
    
    np.random.seed(5)
    parameters = {}
    L = len(layer_dims)            # number of layers in the network
 
    for l in range(1, L):
        parameters['W' + str(l)] = np.random.randn(layer_dims[l], layer_dims[l-1]) * 0.01
        parameters['b' + str(l)] = np.zeros((layer_dims[l], 1))
        assert(parameters['W' + str(l)].shape == (layer_dims[l], layer_dims[l-1]))
        assert(parameters['b' + str(l)].shape == (layer_dims[l], 1))
    # print(parameters)
    return parameters

def linear_forward(A, W, b):
    """
    Implement the linear part of a layer's forward propagation.
 
    Arguments:
    A -- activations from previous layer (or input data): (size of previous layer, number of examples)
    W -- weights matrix: numpy array of shape (size of current layer, size of previous layer)
    b -- bias vector, numpy array of shape (size of the current layer, 1)
 
    Returns:
    Z -- the input of the activation function, also called pre-activation parameter 
    cache -- a python dictionary containing "A", "W" and "b" ; stored for computing the backward pass efficiently
    """
    # print ('linear_forward')
    # print ('W',W)
    # print ('A',A)
    # print ('b',b)
    Z = np.dot(W, A) + b
    # print ('Z',Z)

    assert(Z.shape == (W.shape[0], A.shape[1]))
    cache = (A, W, b)
    
    return Z, cache

def linear_activation_forward(A_prev, W, b, activation):
    """
    Implement the forward propagation for the LINEAR->ACTIVATION layer
 
    Arguments:
    A_prev -- activations from previous layer (or input data): (size of previous layer, number of examples)
    W -- weights matrix: numpy array of shape (size of current layer, size of previous layer)
    b -- bias vector, numpy array of shape (size of the current layer, 1)
    activation -- the activation to be used in this layer, stored as a text string: "sigmoid" or "relu"
 
    Returns:
    A -- the output of the activation function, also called the post-activation value 
    cache -- a python dictionary containing "linear_cache" and "activation_cache";
             stored for computing the backward pass efficiently
    """
    if activation == "sigmoid":
        # Inputs: "A_prev, W, b". Outputs: "A, activation_cache".
        Z, linear_cache = linear_forward(A_prev, W, b)
        A, activation_cache = sigmoid(Z)
    
    elif activation == "relu":
        # Inputs: "A_prev, W, b". Outputs: "A, activation_cache".
        Z, linear_cache = linear_forward(A_prev, W, b)
        A, activation_cache = relu(Z)
    
    assert (A.shape == (W.shape[0], A_prev.shape[1]))
    cache = (linear_cache, activation_cache)

    return A, cache

def L_model_forward(X, parameters):
    """
    Implement forward propagation for the [LINEAR->RELU]*(L-1)->LINEAR->SIGMOID computation
    
    Arguments:
    X -- data, numpy array of shape (input size, number of examples)
    parameters -- output of initialize_parameters_deep()
    
    Returns:
    AL -- last post-activation value
    caches -- list of caches containing:
                every cache of linear_relu_forward() (there are L-1 of them, indexed from 0 to L-2)
                the cache of linear_sigmoid_forward() (there is one, indexed L-1)
    """
    caches = []
    A = X
    L = len(parameters) // 2                  # number of layers in the neural network
    
    # Implement [LINEAR -> RELU]*(L-1). Add "cache" to the "caches" list.
    for l in range(1, L):
        A_prev = A 
        A, cache = linear_activation_forward(A_prev, parameters['W' + str(l)], parameters['b' + str(l)], "sigmoid")
        caches.append(cache)
    
    # Implement LINEAR -> SIGMOID. Add "cache" to the "caches" list.
    AL, cache = linear_activation_forward(A, parameters['W' + str(L)], parameters['b' + str(L)], "sigmoid")
    caches.append(cache)
    assert(AL.shape == (1,X.shape[1]))
    return AL, caches

def compute_cost(AL, Y):
    """
    Implement the cost function defined by equation (7).
 
    Arguments:
    AL -- probability vector corresponding to your label predictions, shape (1, number of examples)
    Y -- true "label" vector (for example: containing 0 if non-cat, 1 if cat), shape (1, number of examples)
 
    Returns:
    cost -- cross-entropy cost
    """
    
    m = Y.shape[1]
 
    # Compute loss from aL and y.
    cost = -np.sum(np.multiply(np.log(AL),Y) + np.multiply(np.log(1 - AL), 1 - Y)) / m
    
    cost = np.squeeze(cost)
    assert(cost.shape == ())
    
    return cost

def linear_backward(dZ, cache):
    """
    Implement the linear portion of backward propagation for a single layer (layer l)
 
    Arguments:
    dZ -- Gradient of the cost with respect to the linear output (of current layer l)
    cache -- tuple of values (A_prev, W, b) coming from the forward propagation in the current layer
 
    Returns:
    dA_prev -- Gradient of the cost with respect to the activation (of the previous layer l-1), same shape as A_prev
    dW -- Gradient of the cost with respect to W (current layer l), same shape as W
    db -- Gradient of the cost with respect to b (current layer l), same shape as b
    """
    A_prev, W, b = cache
    m = A_prev.shape[1]
 
    dW = np.dot(dZ, A_prev.T) / m
    db = np.sum(dZ, axis=1, keepdims=True) / m
    dA_prev = np.dot(W.T, dZ)
    
    assert (dA_prev.shape == A_prev.shape)
    assert (dW.shape == W.shape)
    assert (db.shape == b.shape)
    
    return dA_prev, dW, db

def linear_activation_backward(dA, cache, activation):
    """
    Implement the backward propagation for the LINEAR->ACTIVATION layer.
    
    Arguments:
    dA -- post-activation gradient for current layer l 
    cache -- tuple of values (linear_cache, activation_cache) we store for computing backward propagation efficiently
    activation -- the activation to be used in this layer, stored as a text string: "sigmoid" or "relu"
    
    Returns:
    dA_prev -- Gradient of the cost with respect to the activation (of the previous layer l-1), same shape as A_prev
    dW -- Gradient of the cost with respect to W (current layer l), same shape as W
    db -- Gradient of the cost with respect to b (current layer l), same shape as b
    """
    linear_cache, activation_cache = cache
    
    if activation == "relu":
        dZ = relu_backward(dA, activation_cache)
        dA_prev, dW, db = linear_backward(dZ, linear_cache)
        
    elif activation == "sigmoid":
        dZ = sigmoid_backward(dA, activation_cache)
        dA_prev, dW, db = linear_backward(dZ, linear_cache)
    
    return dA_prev, dW, db

def L_model_backward(AL, Y, caches):
    """
    Implement the backward propagation for the [LINEAR->RELU] * (L-1) -> LINEAR -> SIGMOID group
    
    Arguments:
    AL -- probability vector, output of the forward propagation (L_model_forward())
    Y -- true "label" vector (containing 0 if non-cat, 1 if cat)
    caches -- list of caches containing:
                every cache of linear_activation_forward() with "relu" (it's caches[l], for l in range(L-1) i.e l = 0...L-2)
                the cache of linear_activation_forward() with "sigmoid" (it's caches[L-1])
    
    Returns:
    grads -- A dictionary with the gradients
             grads["dA" + str(l)] = ... 
             grads["dW" + str(l)] = ...
             grads["db" + str(l)] = ... 
    """
    grads = {}
    L = len(caches) # the number of layers
    m = AL.shape[1]
    Y = Y.reshape(AL.shape) # after this line, Y is the same shape as AL
    
    # Initializing the backpropagation
    dAL = - (np.divide(Y, AL) - np.divide(1 - Y, 1 - AL))
    
    # Lth layer (SIGMOID -> LINEAR) gradients. Inputs: "AL, Y, caches". Outputs: "grads["dAL"], grads["dWL"], grads["dbL"]
    current_cache = caches[L-1]
    grads["dA" + str(L)], grads["dW" + str(L)], grads["db" + str(L)] = linear_activation_backward(dAL, current_cache, "sigmoid")
    
    for l in reversed(range(L-1)):
        # lth layer: (RELU -> LINEAR) gradients.
        # Inputs: "grads["dA" + str(l + 2)], caches". Outputs: "grads["dA" + str(l + 1)] , grads["dW" + str(l + 1)] , grads["db" + str(l + 1)] 
        current_cache = caches[l]
        dA_prev_temp, dW_temp, db_temp = linear_activation_backward(grads["dA" + str(l + 2)], current_cache, "sigmoid")
        grads["dA" + str(l + 1)] = dA_prev_temp
        grads["dW" + str(l + 1)] = dW_temp
        grads["db" + str(l + 1)] = db_temp
 
    return grads

def update_parameters(parameters, grads, learning_rate):
    """
    Update parameters using gradient descent
    
    Arguments:
    parameters -- python dictionary containing your parameters 
    grads -- python dictionary containing your gradients, output of L_model_backward
    
    Returns:
    parameters -- python dictionary containing your updated parameters 
                  parameters["W" + str(l)] = ... 
                  parameters["b" + str(l)] = ...
    """
    
    L = len(parameters) // 2 # number of layers in the neural network
 
    # Update rule for each parameter. Use a for loop.
    for l in range(L):
        parameters["W" + str(l+1)] = parameters["W" + str(l+1)] - learning_rate * grads["dW" + str(l+1)]
        parameters["b" + str(l+1)] = parameters["b" + str(l+1)] - learning_rate * grads["db" + str(l+1)]
        
    return parameters

def L_layer_model(X, Y, layers_dims, learning_rate = 0.0075, num_iterations = 3000, print_cost=False, filename='plot.png'):#lr was 0.009
    """
    Implements a L-layer neural network: [LINEAR->RELU]*(L-1)->LINEAR->SIGMOID.
    
    Arguments:
    X -- data, numpy array of shape (number of examples, input number)
    Y -- true "label" vector (containing 0 if cat, 1 if non-cat), of shape (1, number of examples)
    layers_dims -- list containing the input size and each layer size, of length (number of layers + 1).
    learning_rate -- learning rate of the gradient descent update rule
    num_iterations -- number of iterations of the optimization loop
    print_cost -- if True, it prints the cost every 100 steps
    
    Returns:
    parameters -- parameters learnt by the model. They can then be used to predict.
    """
    np.random.seed(1)
    costs = [] # keep track of cost

    # Parameters initialization.
    parameters = initialize_parameters_deep(layers_dims)
    
    # Loop (gradient descent)
    for i in range(0, num_iterations):
        # print ('iteration:', i, '---------------------------')
        # Forward propagation: [LINEAR -> RELU]*(L-1) -> LINEAR -> SIGMOID.
        AL, caches = L_model_forward(X.T, parameters)

        # Compute cost.
        cost = compute_cost(AL, Y)
    
        # Backward propagation.
        grads = L_model_backward(AL, Y, caches)
 
        # Update parameters.
        parameters = update_parameters(parameters, grads, learning_rate)
                
        # Print the cost every 100 training example
        if print_cost and i % 100 == 0:
            print ("Cost after iteration %i: %f" %(i, cost))
            print (AL)
        if print_cost and i % 100 == 0:
            costs.append(cost)
            
    # plot the cost
    plt.plot(np.squeeze(costs))
    plt.ylabel('cost')
    plt.xlabel('iterations (per tens)')
    plt.title("Learning rate =" + str(learning_rate))
    # plt.show()
    plt.savefig(filename)
    
    return parameters

def getData(m):
    matrix = np.matrix(m).astype(int)
    labels = np.squeeze(np.asarray(matrix[:, -1]))
    dataset = matrix[:, 0:-1]
    return dataset, labels


def getData1():
    # in this case; num_features = num_statements
    # num_input = num_test_cases
    dataset = np.matrix([
        [1,1,1,1,0,1,0,0,1],
        [1,0,0,0,1,1,1,1,0],
        [0,0,0,0,0,1,1,0,0],
        [1,1,0,0,1,0,1,1,1],
        [1,1,1,0,1,1,1,1,1],
        [0,0,1,0,0,1,1,1,0],
        [1,1,1,1,0,1,0,1,1]
    ]).astype(int)
    # in labels, 0 means success, 1 means failure
    #labels = np.array([[0],[0],[0],[0],[0],[1],[1]])
    labels = np.array([0,0,0,0,0,1,1])
    # transform the labels to one-hot format
    labels_onehot = np.zeros((labels.shape[0], 2)).astype(int)
    labels_onehot[np.arange(len(labels)), labels.astype(int)] = 1
    # # divide the dataset into train and test datasets
    # train_dataset, test_dataset, \
    # train_labels, test_labels = train_test_split(
    #     dataset, labels, test_size = .1, random_state = 12)
    return dataset, labels

def getDataTest(dim):
    # estimate the suspiciousness of each statement
    test_susp_dataset = np.identity(dim)
    return test_susp_dataset

def predict(X, parameters):
    """
    This function is used to predict the results of a  L-layer neural network.
    
    Arguments:
    X -- data set of examples you would like to label
    parameters -- parameters of the trained model
    
    Returns:
    p -- predictions for the given dataset X
    """
    
    m = X.shape[1]
    n = len(parameters) // 2 # number of layers in the neural network
    # p = np.zeros((1,m))
    
    # Forward propagation
    probas, caches = L_model_forward(X, parameters)
 
    # # convert probas to 0/1 predictions
    # for i in range(0, probas.shape[1]):
    #     if probas[0,i] > 0.5:
    #         p[0,i] = 1
    #     else:
    #         p[0,i] = 0
    
    # print("Accuracy: "  + str(np.sum((p == y)/m)))
        
    return probas

def insertonSort(alist):
    index = [x for x in range(len(alist))]
    rank = [1 for x in range(len(alist))]
    for i in range(len(alist)):
        key = alist[i]
        val = index[i]
        j = i - 1 
        while j >= 0 and alist[j] > key:
            alist[j+1] = alist[j]
            index[j+1] = index[j]
            j -= 1
        alist[j+1] = key
        index[j+1] = val
    ranking = 1
    for i in range(len(alist)-1,0,-1):
        ranking += 1
        if alist[i] == alist[i-1]:
            rank[index[i-1]] = rank[index[i]]
        else:
            rank[index[i-1]] = ranking
    return rank,index

def train(train_dataset, train_labels):
    layers_dims = [train_dataset.shape[1],5,5,1]
    train_labels = np.array([train_labels])
    parameters = L_layer_model(train_dataset, train_labels, layers_dims, learning_rate = 0.3, num_iterations = 15000, print_cost = True, filename='case2.1.png')
    return parameters

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Run BPNN to locate fault against coverage XML files')
    parser.add_argument('source', type=str, help='name of the folder containing coverage .xml file')
    parser.add_argument('prog_name', type=str, help='name of program, i.e. \'sort\' for sort.py')

    args = parser.parse_args()

    matrix = get_tests_matrix(args.source, args.prog_name)

    train_dataset, train_labels = getData(matrix)
    
    # print(train_dataset)
    # print(train_labels)
    params = train(train_dataset, train_labels)
    # print(params)
    test_dataset = getDataTest(train_dataset.shape[1])
    result = predict(test_dataset, params)
    print(result)
    #sus = result
    # print(np.squeeze(np.asarray(result)))
    rank, index= insertonSort(np.squeeze(np.asarray(result)))
    # print ("sus: ",sus,"\n")
    # print ("sorted: ",result,"\n")
    # print ("index:", index,"\n")
    # print ("rank is :",rank,"\n")
    # print ("The most buggy statement is: Statement No.", rank.index(1)+1)
    #print(len(rank))
    for i in range(len(rank)-1,-1,-1):
        #print("i is:", i)
        print("Statement {:>2}: {:>4}".format(index[i]+1,rank[index[i]]))
# from PIL import Image # %matplotlib inline # plt.rcParams['figure.figsize'] = (5.0, 4.0) # set default size of plots # plt.rcParams['image.interpolation'] = 'nearest' # plt.rcParams['image.cmap'] = 'gray' # %load_ext autoreload # %autoreload 2 Implements the sigmoid activation in numpy          Arguments:     Z -- numpy array of any shape          Returns:     A -- output of sigmoid(z), same shape as Z     cache -- returns Z as well, useful during backpropagation Implement the backward propagation for a single SIGMOID unit.       Arguments:     dA -- post-activation gradient, of any shape     cache -- 'Z' where we store for computing backward propagation efficiently       Returns:     dZ -- Gradient of the cost with respect to Z Implement the RELU function.       Arguments:     Z -- Output of the linear layer, of any shape       Returns:     A -- Post-activation parameter, of the same shape as Z     cache -- a python dictionary containing "A" ; stored for computing the backward pass efficiently Implement the backward propagation for a single RELU unit.       Arguments:     dA -- post-activation gradient, of any shape     cache -- 'Z' where we store for computing backward propagation efficiently       Returns:     dZ -- Gradient of the cost with respect to Z # just converting dz to a correct object. # When z <= 0, you should set dz to 0 as well. Arguments:     layer_dims -- python array (list) containing the dimensions of each layer in our network          Returns:     parameters -- python dictionary containing your parameters "W1", "b1", ..., "WL", "bL":                     Wl -- weight matrix of shape (layer_dims[l], layer_dims[l-1])                     bl -- bias vector of shape (layer_dims[l], 1) # number of layers in the network # print(parameters) Implement the linear part of a layer's forward propagation.       Arguments:     A -- activations from previous layer (or input data): (size of previous layer, number of examples)     W -- weights matrix: numpy array of shape (size of current layer, size of previous layer)     b -- bias vector, numpy array of shape (size of the current layer, 1)       Returns:     Z -- the input of the activation function, also called pre-activation parameter      cache -- a python dictionary containing "A", "W" and "b" ; stored for computing the backward pass efficiently # print ('linear_forward') # print ('W',W) # print ('A',A) # print ('b',b) # print ('Z',Z) Implement the forward propagation for the LINEAR->ACTIVATION layer       Arguments:     A_prev -- activations from previous layer (or input data): (size of previous layer, number of examples)     W -- weights matrix: numpy array of shape (size of current layer, size of previous layer)     b -- bias vector, numpy array of shape (size of the current layer, 1)     activation -- the activation to be used in this layer, stored as a text string: "sigmoid" or "relu"       Returns:     A -- the output of the activation function, also called the post-activation value      cache -- a python dictionary containing "linear_cache" and "activation_cache";              stored for computing the backward pass efficiently # Inputs: "A_prev, W, b". Outputs: "A, activation_cache". # Inputs: "A_prev, W, b". Outputs: "A, activation_cache". Implement forward propagation for the [LINEAR->RELU]*(L-1)->LINEAR->SIGMOID computation          Arguments:     X -- data, numpy array of shape (input size, number of examples)     parameters -- output of initialize_parameters_deep()          Returns:     AL -- last post-activation value     caches -- list of caches containing:                 every cache of linear_relu_forward() (there are L-1 of them, indexed from 0 to L-2)                 the cache of linear_sigmoid_forward() (there is one, indexed L-1) # number of layers in the neural network # Implement [LINEAR -> RELU]*(L-1). Add "cache" to the "caches" list. # Implement LINEAR -> SIGMOID. Add "cache" to the "caches" list. Implement the cost function defined by equation (7).       Arguments:     AL -- probability vector corresponding to your label predictions, shape (1, number of examples)     Y -- true "label" vector (for example: containing 0 if non-cat, 1 if cat), shape (1, number of examples)       Returns:     cost -- cross-entropy cost # Compute loss from aL and y. Implement the linear portion of backward propagation for a single layer (layer l)       Arguments:     dZ -- Gradient of the cost with respect to the linear output (of current layer l)     cache -- tuple of values (A_prev, W, b) coming from the forward propagation in the current layer       Returns:     dA_prev -- Gradient of the cost with respect to the activation (of the previous layer l-1), same shape as A_prev     dW -- Gradient of the cost with respect to W (current layer l), same shape as W     db -- Gradient of the cost with respect to b (current layer l), same shape as b Implement the backward propagation for the LINEAR->ACTIVATION layer.          Arguments:     dA -- post-activation gradient for current layer l      cache -- tuple of values (linear_cache, activation_cache) we store for computing backward propagation efficiently     activation -- the activation to be used in this layer, stored as a text string: "sigmoid" or "relu"          Returns:     dA_prev -- Gradient of the cost with respect to the activation (of the previous layer l-1), same shape as A_prev     dW -- Gradient of the cost with respect to W (current layer l), same shape as W     db -- Gradient of the cost with respect to b (current layer l), same shape as b Implement the backward propagation for the [LINEAR->RELU] * (L-1) -> LINEAR -> SIGMOID group          Arguments:     AL -- probability vector, output of the forward propagation (L_model_forward())     Y -- true "label" vector (containing 0 if non-cat, 1 if cat)     caches -- list of caches containing:                 every cache of linear_activation_forward() with "relu" (it's caches[l], for l in range(L-1) i.e l = 0...L-2)                 the cache of linear_activation_forward() with "sigmoid" (it's caches[L-1])          Returns:     grads -- A dictionary with the gradients              grads["dA" + str(l)] = ...               grads["dW" + str(l)] = ...              grads["db" + str(l)] = ... # the number of layers # after this line, Y is the same shape as AL # Initializing the backpropagation # Lth layer (SIGMOID -> LINEAR) gradients. Inputs: "AL, Y, caches". Outputs: "grads["dAL"], grads["dWL"], grads["dbL"] # lth layer: (RELU -> LINEAR) gradients. # Inputs: "grads["dA" + str(l + 2)], caches". Outputs: "grads["dA" + str(l + 1)] , grads["dW" + str(l + 1)] , grads["db" + str(l + 1)] Update parameters using gradient descent          Arguments:     parameters -- python dictionary containing your parameters      grads -- python dictionary containing your gradients, output of L_model_backward          Returns:     parameters -- python dictionary containing your updated parameters                    parameters["W" + str(l)] = ...                    parameters["b" + str(l)] = ... # number of layers in the neural network # Update rule for each parameter. Use a for loop. #lr was 0.009 Implements a L-layer neural network: [LINEAR->RELU]*(L-1)->LINEAR->SIGMOID.          Arguments:     X -- data, numpy array of shape (number of examples, input number)     Y -- true "label" vector (containing 0 if cat, 1 if non-cat), of shape (1, number of examples)     layers_dims -- list containing the input size and each layer size, of length (number of layers + 1).     learning_rate -- learning rate of the gradient descent update rule     num_iterations -- number of iterations of the optimization loop     print_cost -- if True, it prints the cost every 100 steps          Returns:     parameters -- parameters learnt by the model. They can then be used to predict. # keep track of cost # Parameters initialization. # Loop (gradient descent) # print ('iteration:', i, '---------------------------') # Forward propagation: [LINEAR -> RELU]*(L-1) -> LINEAR -> SIGMOID. # Compute cost. # Backward propagation. # Update parameters. # Print the cost every 100 training example # plot the cost # plt.show() # in this case; num_features = num_statements # num_input = num_test_cases # in labels, 0 means success, 1 means failure #labels = np.array([[0],[0],[0],[0],[0],[1],[1]]) # transform the labels to one-hot format # # divide the dataset into train and test datasets # train_dataset, test_dataset, \ # train_labels, test_labels = train_test_split( #     dataset, labels, test_size = .1, random_state = 12) # estimate the suspiciousness of each statement This function is used to predict the results of a  L-layer neural network.          Arguments:     X -- data set of examples you would like to label     parameters -- parameters of the trained model          Returns:     p -- predictions for the given dataset X # number of layers in the neural network # p = np.zeros((1,m)) # Forward propagation # # convert probas to 0/1 predictions # for i in range(0, probas.shape[1]): #     if probas[0,i] > 0.5: #         p[0,i] = 1 #     else: #         p[0,i] = 0 # print("Accuracy: "  + str(np.sum((p == y)/m))) # print(train_dataset) # print(train_labels) # print(params) #sus = result # print(np.squeeze(np.asarray(result))) # print ("sus: ",sus,"\n") # print ("sorted: ",result,"\n") # print ("index:", index,"\n") # print ("rank is :",rank,"\n") # print ("The most buggy statement is: Statement No.", rank.index(1)+1) #print(len(rank)) #print("i is:", i)