import logging
from . import epdconfig

# Display resolution
EPD_WIDTH       = 200
EPD_HEIGHT      = 200

logger = logging.getLogger(__name__)

class EPD:
    def __init__(self):
        self.reset_pin = epdconfig.RST_PIN
        self.dc_pin = epdconfig.DC_PIN
        self.busy_pin = epdconfig.BUSY_PIN
        self.cs_pin = epdconfig.CS_PIN
        self.width = EPD_WIDTH
        self.height = EPD_HEIGHT

    # Hardware reset
    def reset(self):
        epdconfig.digital_write(self.reset_pin, 1)
        epdconfig.delay_ms(200)
        epdconfig.digital_write(self.reset_pin, 0) # module reset
        epdconfig.delay_ms(5)
        epdconfig.digital_write(self.reset_pin, 1)
        epdconfig.delay_ms(200)

    def send_command(self, command):
        epdconfig.digital_write(self.dc_pin, 0)
        epdconfig.digital_write(self.cs_pin, 0)
        epdconfig.spi_writebyte([command])
        epdconfig.digital_write(self.cs_pin, 1)

    def send_data(self, data):
        epdconfig.digital_write(self.dc_pin, 1)
        epdconfig.digital_write(self.cs_pin, 0)
        epdconfig.spi_writebyte([data])
        epdconfig.digital_write(self.cs_pin, 1)

    # send a lot of data
    def send_data2(self, data):
        epdconfig.digital_write(self.dc_pin, 1)
        epdconfig.digital_write(self.cs_pin, 0)
        epdconfig.spi_writebyte2(data)
        epdconfig.digital_write(self.cs_pin, 1)

    def ReadBusy(self):
        logger.debug("e-Paper busy")
        while(epdconfig.digital_read(self.busy_pin) == 1):
            epdconfig.delay_ms(100)
        logger.debug("e-Paper busy release")

    def init(self):
        if (epdconfig.module_init() != 0):
            return -1
        # EPD hardware init start
        self.reset()

        self.ReadBusy()
        self.send_command(0x12)  #SWRESET
        self.ReadBusy()

        self.send_command(0x01) #Driver output control
        self.send_data(0xC7)
        self.send_data(0x00)
        self.send_data(0x01)

        self.send_command(0x11) #data entry mode
        self.send_data(0x01)

        self.send_command(0x44) #set Ram-X address start/end position
        self.send_data(0x00)
        self.send_data(0x18)    #0x18-->(24+1)*8=200

        self.send_command(0x45) #set Ram-Y address start/end position
        self.send_data(0xC7)    #0xC7-->(199+1)=200
        self.send_data(0x00)
        self.send_data(0x00)
        self.send_data(0x00)

        self.send_command(0x3C) #BorderWavefrom
        self.send_data(0x05)

        self.send_command(0x18) #Read built-in temperature sensor
        self.send_data(0x80)

        self.send_command(0x4E)   # set RAM x address count to 0
        self.send_data(0x00)
        self.send_command(0x4F)   # set RAM y address count to 0X199
        self.send_data(0xC7)
        self.send_data(0x00)
        self.ReadBusy()
        return 0

    def getbuffer(self, image):
        buf = [0xFF] * int(self.width * self.height / 8)
        # Set buffer to value of Python Imaging Library image.
        # Image must be in mode 1.
        image_monocolor = image.convert('1')
        imwidth, imheight = image_monocolor.size
        if imwidth != self.width or imheight != self.height:
            raise ValueError('Image must be same dimensions as display \
                ({0}x{1}).' .format(self.width, self.height))

        pixels = image_monocolor.load()
        for y in range(self.height):
            for x in range(self.width):
                # Set the bits for the column of pixels at the current position.
                if pixels[x, y] == 0:
                    buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8))
        return buf

    def display(self, blackimage, redimage):

        if self.width%8 == 0:
            linewidth = int(self.width/8)
        else:
            linewidth = int(self.width/8) + 1

        buf = [0x00] * self.height * linewidth

        # send black data
        if (blackimage != None):
            self.send_command(0x24) # DATA_START_TRANSMISSION_1
            self.send_data2(blackimage)

        # send red data
        if (redimage != None):
            self.send_command(0x26) # DATA_START_TRANSMISSION_2
            for i in range(0, int(self.width * self.height / 8)):
                buf[i] = ~redimage[i]
            self.send_data2(buf)

        self.send_command(0x22) # DISPLAY_REFRESH
        self.send_data(0xF7)
        self.send_command(0x20) # DISPLAY_REFRESH
        self.ReadBusy()

    def Clear(self):
        if self.width%8 == 0:
            linewidth = int(self.width/8)
        else:
            linewidth = int(self.width/8) + 1

        self.send_command(0x24) # DATA_START_TRANSMISSION_1
        self.send_data2([0xff] * int(self.height * linewidth))

        self.send_command(0x26) # DATA_START_TRANSMISSION_2
        self.send_data2([0x00] * int(self.height * linewidth))

        self.send_command(0x22) # DISPLAY_REFRESH
        self.send_data(0xF7)
        self.send_command(0x20) # DISPLAY_REFRESH
        self.ReadBusy()

    def sleep(self):
        self.send_command(0x10) #enter deep sleep
        self.send_data(0x01)

        epdconfig.delay_ms(2000)
        epdconfig.module_exit()

### END OF FILE ###


### IMPORTS ###
import pandas as pd
import numpy as np
import os
import pprint as pp
import matplotlib.pyplot as plt
import pickle

# Math imports
from scipy.stats import pearsonr, linregress
from sklearn.metrics.pairwise import cosine_distances

# Sklearn imports
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, roc_auc_score, r2_score

# Sklearn algorithms
from sklearn.neighbors import NearestNeighbors, KNeighborsRegressor, KNeighborsClassifier
from sklearn.ensemble import RandomForestRegressor,RandomForestClassifier, GradientBoostingRegressor
from sklearn.svm import SVR, SVC
from sklearn.gaussian_process import GaussianProcessRegressor

# Sklearn boundaries
from sklearn.covariance import EllipticEnvelope
from sklearn.ensemble import IsolationForest
from sklearn.svm import OneClassSVM
from sklearn.neighbors import LocalOutlierFactor

# From this project
from src.algorithm import Algorithm
from src.distance import Distance
from src.utils import drop_infs

######################
# DATASET FUNCTIONS
######################

class DataSet():

    def __init__(self, csv, size, **kwargs):

        # Read data and define data variables
        self.csv = csv
        self.dataset_name = csv.split('\\')[-1].split('_')[0]
        self.df = pd.read_csv(csv, header=0, index_col=0)
        self.df = drop_infs(self.df)
        if size:
            self.df = self.df.sample(size, **kwargs)
        self.X = self.df.iloc[:,:-1]
        self.y = self.df.iloc[:,-1]

        # Preprocessing
        self.var_filter_ = {'filtered': False, 'Value': None}
        self.corr_filter_ = {'filtered': False, 'Value': None}
        self.scaled_ = False

        # Train and test sets
        self.X_train = []
        self.X_test = []
        self.y_train = []
        self.y_test = []

        # Train and test numpy arrays
        self.X_train_np = []
        self.X_test_np = []
        self.y_train_np = []
        self.y_test_np = []

        self.function_list = [Distance.cosine_distance,
                Distance.euclidean,
                Distance.cityblock,
                Distance.minkowski,
                Distance.chebyshev,
                Distance.sorensen,
                Distance.gower,
                Distance.sorgel,
                Distance.kulczynski,
                Distance.canberra,
                Distance.lorentzian,
                Distance.czekanowski,
                Distance.ruzicka,
                Distance.tanimoto]

        self.boundary_list = ['one_class_svm',
                        'robust_covariance',
                        'isolation_forest',
                        'local_outlier_factor']

        # Neighbor indices and distances
        self.n_neighbors = None
        self.indices = {
            'X_test':{k.__name__: [] for k in self.function_list},
            'X_train': {k.__name__: [] for k in self.function_list}
            }
        self.distances = {
            'X_test': {k.__name__: [] for k in self.function_list},
            'X_train': {k.__name__: [] for k in self.function_list}
            }

        # Training distances
        self.mean_training_set_distances = {k.__name__: [] for k in self.function_list}

        self.score_dicts = {}

        # AD measures for the test set
        self.test_set_ad_measures = {
            'distance': {k.__name__: [] for k in self.function_list},
            'boundary': {k: [] for k in self.boundary_list}
        }

    def normalize_data(self):
        """ Normalizes data."""

        scaler = StandardScaler()
        scaled_X = scaler.fit_transform(self.X)
        scaled_df = pd.DataFrame(scaled_X, index = self.X.index, columns = self.X.columns)
        self.X = scaled_df
        self.scaled_ = True

        return None

    def apply_variance_filter(self, threshold_percentile):
        """ Filters the features in the dataset on a percentile of the total variance. Every feature
        below the variance threshold is dropped.

        threshold_percentile: Percentile value for the filter (i.e. 50 for 50%
        )
        """
        # Get absolute threshold value from percentile
        threshold = np.percentile(self.X.var(), threshold_percentile)

        # Get variance and assign to dataframe where feature variables are rows
        variance = self.X.var()
        df_var = pd.DataFrame(data = {'variance': variance}, index = self.X.columns)

        # Drop the low variance rows
        df_low_v_dropped = df_var[df_var['variance'] > threshold]

        # Filter the dataset's X dataframe by the selected feature variables
        self.X = self.X[df_low_v_dropped.index]

        # Assign dataset variables
        self.var_filter_ = {'filtered': True, 'Value': threshold}

        return None

    def apply_correlation_filter(self, threshold):
        """ Filters the feature in the dataset on an absolute correlation threshold. Any features which correlate
        to one another above the provided threshold are dropped.

        threshold: Absolute correlation threshold (i.e. 0.95)

        """

        # Create correlation matrix
        corr_matrix = self.X.corr().abs()

        # Select upper triangle of correlation matrix
        upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool))

        # Find features with correlation greater than 0.95
        to_drop = [column for column in upper.columns if any(upper[column] > threshold)]

        # Drop features
        self.X.drop(to_drop, axis=1, inplace=True)

        # Reassign dataset variables
        self.num_features = len(self.X.columns)
        self.features = self.X.columns
        self.corr_filter_ = {'filtered': True, 'Value': threshold}

        return None

    def split(self, **kwargs):
        """ Splits into train and test set."""

        X_train, X_test, y_train, y_test= train_test_split(self.X, self.y, **kwargs)

        self.X_train.append(X_train)
        self.X_test.append(X_test)
        self.y_train.append(y_train)
        self.y_test.append(y_test)

        self.X_train_np.append(X_train.to_numpy())
        self.X_test_np.append(X_test.to_numpy())
        self.y_train_np.append(y_train.to_numpy())
        self.y_test_np.append(y_test.to_numpy())

        return None

    def get_test_and_training_set_neighbors(self, n_neighbors, **kwargs):
        """ For every distance function,
            calculates the n nearest neighbors for every entry in the training and test sets,
            calculates the indices and distances for each neighbor,
            calculates the average distance from the n neighbors, and
            calculates the average n-neighbor distance over the entire training set.
        """

        # Store neighbors as class variable
        self.n_neighbors = n_neighbors

        # Loop through every distance function
        for function in self.function_list:

            # Loop through each training/test set
            for i in range(len(self.X_train)):
                neighbors = NearestNeighbors(n_neighbors=self.n_neighbors, metric = function, **kwargs)
                neighbors.fit(X=self.X_train_np[i])

                # Calculate for test set
                dists, indices = neighbors.kneighbors(X=self.X_test_np[i], n_neighbors=self.n_neighbors, return_distance=True)
                self.indices['X_test'][function.__name__].append(indices)
                self.distances['X_test'][function.__name__].append(dists)
                row_mean_dists = np.mean(dists, axis=1)
                self.test_set_ad_measures['distance'][function.__name__].append(row_mean_dists)

                # Calculate for training test
                training_dists, training_indices = neighbors.kneighbors(X=self.X_train_np[i], n_neighbors = self.n_neighbors, return_distance=True)
                self.indices['X_train'][function.__name__].append(training_indices)
                self.distances['X_train'][function.__name__].append(training_dists)
                row_mean_training_dists = np.mean(training_dists, axis=1)
                self.mean_training_set_distances[function.__name__].append(np.mean(row_mean_training_dists))

        return None

##############################
# AD MEASURES
##############################

    def calculate_boundary_one_class_svm(self, nu=0.5, **kwargs):
        """ Calculates distance to a boundary using one class svm."""

        for i in range(len(self.X_test)):

            one_class = OneClassSVM(nu=nu, **kwargs)
            one_class.fit(self.X_train_np[i])
            distances_to_boundary = one_class.decision_function(self.X_test_np[i])
            self.test_set_ad_measures['boundary']['one_class_svm'].append(distances_to_boundary)

        return None

    def calculate_boundary_robust_covariance(self, **kwargs):
        """ Calculates distance to a boundary using robust covariance."""

        for i in range(len(self.X_test)):

            rob_cov = EllipticEnvelope(**kwargs)
            rob_cov.fit(self.X_train_np[i])
            distances_to_boundary = rob_cov.decision_function(self.X_test_np[i])
            self.test_set_ad_measures['boundary']['robust_covariance'].append(distances_to_boundary)

        return None

    def calculate_boundary_isolation_forest(self, **kwargs):
        """ Calculates distance to a boundary using isolation forest."""

        for i in range(len(self.X_test)):

            forest = IsolationForest(**kwargs)
            forest.fit(self.X_train_np[i])
            distances_to_boundary = forest.decision_function(self.X_test_np[i])
            self.test_set_ad_measures['boundary']['isolation_forest'].append(distances_to_boundary)

        return None

    def calculate_boundary_local_outlier_factor(self, **kwargs):
        """ Calculates distance to a boundary using local outlier factor."""

        for i in range(len(self.X_test)):

            lof = LocalOutlierFactor(novelty = True, **kwargs)
            lof.fit(self.X_train_np[i])
            distances_to_boundary = lof.decision_function(self.X_test_np[i])
            self.test_set_ad_measures['boundary']['local_outlier_factor'].append(distances_to_boundary)

        return None

#########################
# Algorithm predictions
#########################
    def predict_algorithm(self, algorithm):
        """
        Makes predictions with an Algorithm class object and stores appropriate class variables.

        algorithm: Algorithm class object

        """

        # Loop through each training set
        for i in range(len(self.X_train)):

            # Fit algorithm and predict
            algorithm.estimator.fit(self.X_train[i], self.y_train[i])
            y_pred = algorithm.estimator.predict(self.X_test[i])

            # Store predictions
            algorithm.predictions.append(y_pred)

            # Calculate and store prediction error
            squared_error = (self.y_test[i] - y_pred)**2
            algorithm.squared_error.append(squared_error)
            absolute_error = abs(self.y_test[i] - y_pred)
            algorithm.absolute_error.append(absolute_error)

            # Get intrinsic measure
            algorithm.get_intrinsic(self.X_test[i])

        return None

    def predict_random_forest(self, **kwargs):
        """ Generates predictions using Random Forest, calculates the prediction error, and calculates
        the Pearson Correlation Coefficient between the prediction error and the AD measures."""

        self.score_dicts['RF'] = {
            'Algorithm': {
                'Estimators': [],
                'Predictions': [],
                'Squared_error': [],
                'Absolute_error': [],
                'Tree_predictions': []
            },
            'Intrinsic': {
                'arrays': [],
                'description': 'Standard deviations of the predictions of each tree in the random forest.'
            },
            'Results': {
                'distance': {k:[] for k in self.test_set_ad_measures['distance'].keys()},
                'boundary': {k:[] for k in self.test_set_ad_measures['boundary'].keys()}
            },
            'Ratios': {
                'distance': {k:[] for k in self.test_set_ad_measures['distance'].keys()},
                'boundary': {k:[] for k in self.test_set_ad_measures['boundary'].keys()}
            },
            'Differences': {
                'distance': {k:[] for k in self.test_set_ad_measures['distance'].keys()},
                'boundary': {k:[] for k in self.test_set_ad_measures['boundary'].keys()}
            }
        }

        self.score_dicts['RF']['Results']['Intrinsic'] = []

        # Loop through each training set
        for i in range(len(self.X_train)):

            # Fit random forest and create tree dictionary
            rf = RandomForestRegressor(**kwargs)
            rf.fit(self.X_train[i], self.y_train[i])
            self.score_dicts['RF']['Algorithm']['Estimators'].append(rf.get_params())
            self.score_dicts['RF']['Algorithm']['Tree_predictions'].append({})

            # Predit with each estimator (tree)
            for each_tree in range(rf.n_estimators):
                preds = rf.estimators_[each_tree].predict(self.X_test[i])
                self.score_dicts['RF']['Algorithm']['Tree_predictions'][i][f'tree_{each_tree}'] = preds

            # Predict with ensemble
            y_pred = rf.predict(self.X_test[i])
            self.score_dicts['RF']['Algorithm']['Predictions'].append(y_pred)

            # Calculate squared error for each prediction
            squared_error = (self.y_test[i] - y_pred)**2
            self.score_dicts['RF']['Algorithm']['Squared_error'].append(squared_error)
            absolute_error = abs(self.y_test[i] - y_pred)
            self.score_dicts['RF']['Algorithm']['Absolute_error'].append(absolute_error)

        return None

    def predict_gradient_boosted_trees(self, **kwargs):
        """ Generates predictions using gradient boosted trees and calculates prediction error."""

        # Define class information
        self.score_dicts['GBT'] = {
            'Algorithm': {
                'Estimators': [],
                'Predictions': [],
                'Tree_predictions': [],
                'Squared_error': [],
                'Absolute_error': []
                },
            'Intrinsic': {
                'arrays': [],
                'description': 'Standard deviations of the predictions of each tree in the boosted random forest.'
            },
            'Results':{
                'distance': {k:[] for k in self.test_set_ad_measures['distance'].keys()},
                'boundary': {k:[] for k in self.test_set_ad_measures['boundary'].keys()}
            },
            'Ratios': {
                'distance': {k:[] for k in self.test_set_ad_measures['distance'].keys()},
                'boundary': {k:[] for k in self.test_set_ad_measures['boundary'].keys()}
            },
            'Differences': {
                'distance': {k:[] for k in self.test_set_ad_measures['distance'].keys()},
                'boundary': {k:[] for k in self.test_set_ad_measures['boundary'].keys()}
            }
        }

        self.score_dicts['GBT']['Results']['Intrinsic'] = []

        # Loop through each training set
        for i in range(len(self.X_train)):

            # Fit knn and add params to class object
            gbt = GradientBoostingRegressor(**kwargs)
            gbt.fit(self.X_train[i], self.y_train[i])
            self.score_dicts['GBT']['Algorithm']['Estimators'].append(gbt.get_params())
            self.score_dicts['GBT']['Algorithm']['Tree_predictions'].append({})

            # Make and store predictions
            y_pred = gbt.predict(self.X_test[i])
            self.score_dicts['GBT']['Algorithm']['Predictions'].append(y_pred)

            # Calculate and store prediction error
            squared_error = (self.y_test[i] - y_pred)**2
            self.score_dicts['GBT']['Algorithm']['Squared_error'].append(squared_error)
            absolute_error = abs(self.y_test[i] - y_pred)
            self.score_dicts['GBT']['Algorithm']['Absolute_error'].append(absolute_error)

            # Predict with each estimator (tree)
            for i_tree,tree in enumerate(gbt.estimators_):
                preds = gbt.estimators_[i_tree][0].predict(self.X_test[i])
                self.score_dicts['GBT']['Algorithm']['Tree_predictions'][i][f'tree_{i_tree}'] = preds

        return None

    def predict_gaussian_process(self, **kwargs):
        """ Generates predictions with gaussian process and calculates prediction error."""

        # Define class information
        self.score_dicts['GP'] = {
            'Algorithm': {
                'Estimators': [],
                'Predictions': [],
                'Squared_error': [],
                'Absolute_error': []
                },
            'Intrinsic': {
                'arrays': [],
                'description': 'Standard deviations of prediction for each compound.'
            },
            'Results':{
                'distance': {k:[] for k in self.test_set_ad_measures['distance'].keys()},
                'boundary': {k:[] for k in self.test_set_ad_measures['boundary'].keys()}
            },
            'Ratios': {
                'distance': {k:[] for k in self.test_set_ad_measures['distance'].keys()},
                'boundary': {k:[] for k in self.test_set_ad_measures['boundary'].keys()}
            },
            'Differences': {
                'distance': {k:[] for k in self.test_set_ad_measures['distance'].keys()},
                'boundary': {k:[] for k in self.test_set_ad_measures['boundary'].keys()}
            }
        }

        self.score_dicts['GP']['Results']['Intrinsic'] = []

        # Loop through each training set
        for i in range(len(self.X_train)):

            # Fit knn and add params to class object
            gp = GaussianProcessRegressor(**kwargs)
            gp.fit(self.X_train[i], self.y_train[i])
            self.score_dicts['GP']['Algorithm']['Estimators'].append(gp.get_params())

            # Make and store predictions and stdevs of prediction
            y_pred, y_stds = gp.predict(X=self.X_test[i], return_std=True)
            self.score_dicts['GP']['Algorithm']['Predictions'].append(y_pred)
            self.score_dicts['GP']['Intrinsic']['arrays'].append(y_stds)

            # Calculate and store prediction error
            squared_error = (self.y_test[i] - y_pred)**2
            self.score_dicts['GP']['Algorithm']['Squared_error'].append(squared_error)
            absolute_error = abs(self.y_test[i] - y_pred)
            self.score_dicts['GP']['Algorithm']['Absolute_error'].append(absolute_error)

        return None

#####################
# GET RESULTS
#####################

    def calculate_global_stats_class(self, algorithm, print_res):
        """ Calculates correlation coefficient, slope, R2, and p values for each type of prediction error
        and applicability domain measure, for an Algorithm object.

        algorithm: Algorithm object which has stored prediction error results
        print_res (bool): Set to True to print the correlation results
        """

        # Loop through each test set
        for i in range(len(self.X_test)):

            # Loop through each error metric
            for error_metric in ['squared_error', 'absolute_error']:

                # Get prediction error
                pred_error = getattr(algorithm, error_metric)[i]

                # Loop through each measure type
                for ad_measure_type in ['boundary', 'distance', 'intrinsic']:

                    # Intrinsic
                    if ad_measure_type == 'intrinsic':

                        # Catch empty data
                        if not getattr(algorithm, 'intrinsic'):
                            result_dict = None

                        # If there is actually intrinsic data
                        else:
                            # Get correlations and linear fit
                            ad_measure_array = getattr(algorithm, 'intrinsic')[i]
                            corr, corr_p = pearsonr(ad_measure_array, pred_error)
                            lin_result = linregress(ad_measure_array, pred_error)

                            # Store results
                            result_dict = {
                                'Pearson_correlation': corr,
                                'Pearson_p_val': corr_p,
                                'Slope': lin_result.slope,
                                'Stderr': lin_result.stderr,
                                'p_val': lin_result.pvalue,
                                'R2': lin_result.rvalue**2
                            }
                            getattr(algorithm, 'Results')[error_metric][ad_measure_type].append(result_dict)

                        # Print
                        if print_res:
                                    printer = pp.PrettyPrinter(indent=4)
                                    print(f'METRIC: {error_metric} TYPE: {ad_measure_type}')
                                    printer.pprint(result_dict)
                                    print('~'*30)

                    # Distance and boundary
                    else:

                        # Loop through each measure
                        for key in self.test_set_ad_measures[ad_measure_type].keys():

                            # Catch empty data
                            if not self.test_set_ad_measures[ad_measure_type][key]:
                                result_dict = {}

                            else:
                                # Get array, correlations, and linear fit
                                ad_measure_array = self.test_set_ad_measures[ad_measure_type][key][i]
                                corr, corr_p = pearsonr(ad_measure_array, pred_error)
                                lin_result = linregress(ad_measure_array, pred_error)

                                # Store results
                                result_dict = {
                                    'Pearson_correlation': corr,
                                    'Pearson_p_val': corr_p,
                                    'Slope': lin_result.slope,
                                    'Stderr': lin_result.stderr,
                                    'p_val': lin_result.pvalue,
                                    'R2': lin_result.rvalue**2
                                }
                                getattr(algorithm, 'Results')[error_metric][ad_measure_type][key].append(result_dict)

                            # Print
                            if print_res:
                                printer = pp.PrettyPrinter(indent=4)
                                print(f'METRIC: {error_metric} TYPE: {ad_measure_type} MEASURE: {key}')
                                printer.pprint(result_dict)
                                print('~'*30)

        return None

    def calculate_ratios_class(self, algorithm, print_res):
        """ Calculates RMSE and R2 ratios inside and outside AD for a given algorithm class object.

        algorithm: Algorithm class object with predictions
        print_res (bool): Set to True to print the ratio dictionary

        """

        # Loop through test sets
        for i in range(len(self.X_test)):

            # Loop through measure types
            for ad_measure_type in ['distance', 'boundary']:

                # Loop through each measure
                for key in self.test_set_ad_measures[ad_measure_type].keys():

                    # Calculate appropriate thresholds
                    if ad_measure_type == 'distance':
                        training_distances = self.distances['X_train'][key][i]
                        training_average_d_neighbors = np.mean(training_distances, axis=1)
                        thresholds = [np.percentile(training_average_d_neighbors, x) for x in range(5,100,5)]
                    elif ad_measure_type == 'boundary':

                        # Catch empty list for a boundary measure
                        if not self.test_set_ad_measures[ad_measure_type][key]:
                            Z = None
                            thresholds = None
                        else:
                            Z = self.test_set_ad_measures[ad_measure_type][key][i]
                            thresholds = np.linspace(Z.min(), 0, 10)

                    # Catch empty array
                    if not self.test_set_ad_measures[ad_measure_type][key]:
                        ad_measure_array = None
                        result_dict = None
                        getattr(algorithm, 'Ratios')[ad_measure_type][key].append(ratio_dict)

                    # Finish loop
                    else:
                        ad_measure_array = self.test_set_ad_measures[ad_measure_type][key][i]

                        # Make empty ratio lists
                        rmse_ratios = []
                        r2_ratios = []

                        # Loop through each threshold
                        for thresh in thresholds:

                            # Get bool arrays for inside and outside
                            if ad_measure_type == 'distance':
                                bool_inside_AD = (ad_measure_array < thresh)
                                bool_outside_AD = (ad_measure_array > thresh)
                            elif ad_measure_type == 'boundary':
                                bool_inside_AD = (ad_measure_array > thresh)
                                bool_outside_AD = (ad_measure_array < thresh)

                            # Get test set true values for inside and outside
                            y_true_inside = self.y_test_np[i][bool_inside_AD]
                            y_true_outside = self.y_test_np[i][bool_outside_AD]

                            # Get test set predicted values for inside and outside
                            y_pred = np.array(getattr(algorithm, 'predictions')[i])
                            y_pred_inside = y_pred[bool_inside_AD]
                            y_pred_outside = y_pred[bool_outside_AD]

                            # Set ratios to np.nan if all compounds are inside or outside AD
                            if np.sum(bool_inside_AD) == 0 or np.sum(bool_outside_AD) == 0:
                                rmse_ratio = np.nan
                                rmse_ratios.append(rmse_ratio)
                                r2_ratio = np.nan
                                r2_ratios.append(r2_ratio)

                            # Proceed to calculation if there are compounds both inside and outside AD
                            else:
                                # Get RMSE inside and outside and append to ratio list
                                rmse_inside = mean_squared_error(y_true_inside, y_pred_inside, squared=False)
                                rmse_outside = mean_squared_error(y_true_outside, y_pred_outside, squared=False)
                                rmse_ratio = rmse_inside/rmse_outside
                                rmse_ratios.append(rmse_ratio)

                                # Get R2 inside and outside and append to ratio list
                                r2_inside = r2_score(y_true_inside, y_pred_inside)
                                r2_outside = r2_score(y_true_outside, y_pred_outside)
                                r2_ratio = r2_inside/r2_outside
                                r2_ratios.append(r2_ratio)

                        # Create ratio dictionary and append to class object
                        if ad_measure_type == 'distance':
                            threshold_percentiles = range(5,100,5)
                        elif ad_measure_type == 'boundary':
                            threshold_percentiles == None

                        ratio_dict = {
                            'thresholds': thresholds,
                            'threshold_percentiles': threshold_percentiles,
                            'rmse_ratios': rmse_ratios,
                            'r2_ratios': r2_ratios
                        }
                        getattr(algorithm, 'Ratios')[ad_measure_type][key].append(ratio_dict)

                        # Print if parameter set to print
                        if print_res:
                            print(f'TYPE: {ad_measure_type} MEASURE: {key}')
                            pprinter = pp.PrettyPrinter(indent=4)
                            pprinter.pprint(ratio_dict)
                            print('*'*30)

        return None

    def calculate_differences_class(self, algorithm, print_res):
        """ Calculates differences for R2 and RMSE inside and outside the applicability domain for every
        AD measure.

        algorithm: Algorithm class object with predictions
        print_res (bool): Set to True to print difference dictionary

        """

        for i in range(len(self.X_test)):

            for ad_measure_type in ['distance', 'boundary']:

                for key in self.test_set_ad_measures[ad_measure_type].keys():

                    if ad_measure_type == 'distance':
                        training_set_distances = self.distances['X_train'][key][i]
                        training_average_d_neighbors = np.mean(training_set_distances, axis=1)
                        thresholds = [np.percentile(training_average_d_neighbors, x) for x in range(5,100, 5)]
                    elif ad_measure_type == 'boundary':

                        # Catch empty list
                        if not self.test_set_ad_measures[ad_measure_type][key]:
                            Z = None
                            thresholds = None
                        else:
                            Z = self.test_set_ad_measures[ad_measure_type][key][i]
                            thresholds = np.linspace(Z.min(), 0, 10)

                    # Catch empty array
                    if not self.test_set_ad_measures[ad_measure_type][key]:
                        ad_measure_array = None
                        result_dict = None
                        getattr(algorithm, 'Differences')[ad_measure_type][key].append(difference_dict)

                    # Finish loop
                    else:
                        ad_measure_array = self.test_set_ad_measures[ad_measure_type][key][i]

                        # Make empty difference lists
                        rmse_differences = []
                        r2_differences = []

                        # Loop through each threshold
                        for thresh in thresholds:

                            # Get bool arrays for inside and outside
                            if ad_measure_type == 'distance':
                                bool_inside_AD = (ad_measure_array < thresh)
                                bool_outside_AD = (ad_measure_array > thresh)
                            elif ad_measure_type == 'boundary':
                                bool_inside_AD = (ad_measure_array > thresh)
                                bool_outside_AD = (ad_measure_array < thresh)

                            # Get test set true values for inside and outside
                            y_true_inside = self.y_test_np[i][bool_inside_AD]
                            y_true_outside = self.y_test_np[i][bool_outside_AD]

                            # Get test set predicted values for inside and outside
                            y_pred = np.array(getattr(algorithm, 'predictions')[i])
                            y_pred_inside = y_pred[bool_inside_AD]
                            y_pred_outside = y_pred[bool_outside_AD]

                            # Set ratios to np.nan if all compounds are inside or outside AD
                            if np.sum(bool_inside_AD) == 0 or np.sum(bool_outside_AD) == 0:
                                rmse_difference = np.nan
                                rmse_differences.append(rmse_difference)
                                r2_difference = np.nan
                                r2_differences.append(r2_difference)

                            # Proceed to calculation if there are compounds both inside and outside AD
                            else:
                                # Get RMSE inside and outside and append to ratio list
                                rmse_inside = mean_squared_error(y_true_inside, y_pred_inside, squared=False)
                                rmse_outside = mean_squared_error(y_true_outside, y_pred_outside, squared=False)
                                rmse_difference = rmse_outside - rmse_inside
                                rmse_differences.append(rmse_difference)

                                # Get R2 inside and outside and append to ratio list
                                r2_inside = r2_score(y_true_inside, y_pred_inside)
                                r2_outside = r2_score(y_true_outside, y_pred_outside)
                                r2_difference = r2_inside - r2_outside
                                r2_differences.append(r2_difference)

                        # Create ratio dictionary and append to class object
                        if ad_measure_type == 'distance':
                            threshold_percentiles = np.arange(5,100,5)
                        elif ad_measure_type == 'boundary':
                            threshold_percentiles = None
                        difference_dict = {
                            'thresholds': thresholds,
                            'threshold_percentiles': threshold_percentiles,
                            'rmse_differences': rmse_differences,
                            'r2_differences': r2_differences
                        }
                        getattr(algorithm, 'Differences')[ad_measure_type][key].append(difference_dict)

                        # Print if parameter set to print
                        if print_res:
                            print(f'TYPE: {ad_measure_type} MEASURE: {key}')
                            pprinter = pp.PrettyPrinter(indent=4)
                            pprinter.pprint(difference_dict)
                            print('+'*30)

        return None

    def plot_comparisons_class(self, algorithm, out_path):
        """ Plots differences and ratios of metrics for compounds inside and outside the applicability domain,
        for each AD measure, for a given algorithm.

        algorithm: Algorithm class object with predictions
        out_path: Absolute path to the results for a dataset
        """

        # Loop through all test sets
        for i in range(len(self.X_test)):

            # Loop through all measure types
            for ad_measure_type in ['distance', 'boundary']:

                # Loop through each measure
                for key in self.test_set_ad_measures[ad_measure_type].keys():

                    # Get ratios
                    ratio_dict =  getattr(algorithm, 'Ratios')[ad_measure_type][key][i]
                    rmse_ratio = ratio_dict['rmse_ratios']
                    r2_ratio = ratio_dict['r2_ratios']
                    rmse_ratio_label = 'RMSEin/RMSEout'
                    r2_ratio_label = 'R2in/R2out'

                    # Get differences
                    difference_dict = getattr(algorithm, 'Differences')[ad_measure_type][key][i]
                    rmse_difference = difference_dict['rmse_differences']
                    r2_difference = difference_dict['r2_differences']
                    rmse_difference_label = 'RMSEout-RMSEin'
                    r2_difference_label = 'R2in-R2out'

                    # Set xaxis
                    if ad_measure_type == 'distance':
                        ratio_xaxis = ratio_dict['threshold_percentiles']
                        ratio_xlabel = 'Training Set Distance Percentile'
                        difference_xaxis = difference_dict['threshold_percentiles']
                        difference_xlabel = 'Training Set Distance Percentile'
                    elif ad_measure_type == 'boundary':
                        ratio_xaxis = ratio_dict['thresholds']
                        ratio_xlabel = 'Thresholds'
                        difference_xaxis = difference_dict['thresholds']
                        difference_xlabel = 'Thresholds'

                    # Make Ratio figure with two different y-axes on same plot
                    fig, ax1 = plt.subplots()

                    # Plot Ratio R2 data
                    color = 'tab:red'
                    ax1.set_xlabel(ratio_xlabel)
                    ax1.set_ylabel(r2_ratio_label, color=color)
                    ax1.plot(ratio_xaxis, r2_ratio, color=color)
                    ax1.tick_params(axis='y', labelcolor=color)

                    # Set duplicate x axis
                    ax2 = ax1.twinx()

                    # Plot Ratio RMSE data
                    color = 'tab:blue'
                    ax2.set_ylabel(rmse_ratio_label, color=color)
                    ax2.plot(ratio_xaxis, rmse_ratio, color=color)
                    ax2.tick_params(axis='y', labelcolor=color)

                    # Set figure title and layout
                    fig.suptitle(f'{algorithm}, {key}')
                    fig.tight_layout()

                    # Save figure
                    alg_name = algorithm.estimator.__class__.__name__
                    if algorithm.recursive_features:
                        rfe = f'RFE_{algorithm.recursive_features}'
                    else:
                        rfe = 'NoRFE'
                    file_name = f'Ratios_{key}_test_set{i}.png'
                    file_path = os.path.join(out_path, alg_name, rfe,'Ratios', file_name)
                    plt.savefig(file_path)

                    # Make Difference figure
                    fig2, ax3 = plt.subplots()

                    # Plot Difference R2 data
                    color = 'tab:red'
                    ax3.set_xlabel(difference_xlabel)
                    ax3.set_ylabel(r2_difference_label, color=color)
                    ax3.plot(difference_xaxis, r2_difference, color=color)
                    ax3.tick_params(axis='y', labelcolor=color)

                    # Set duplicate x axis
                    ax4 = ax3.twinx()

                    # Plot Ratio RMSE data
                    color = 'tab:blue'
                    ax4.set_ylabel(rmse_difference_label, color=color)
                    ax4.plot(difference_xaxis, rmse_difference, color=color)
                    ax4.tick_params(axis='y', labelcolor=color)

                    # Set figure title and layout
                    fig.suptitle(f'{algorithm}, {key}')
                    fig.tight_layout()

                    # Save figure
                    file_name = f'Differences_{key}_test_set{i}.png'
                    file_path = os.path.join(out_path, algorithm, rfe, 'Differences', file_name)
                    plt.savefig(file_path)

        return None

    def plot_correlations_class(self, algorithm, out_path):
        """
        algorithm: Algorithm class object with predictions and correlations
        out_path: Absolute path to PNG folder
        """

        # Loop through each test set
        for i in range(len(self.X_test)):

            # Loop through each error metric
            for error_metric in ['squared_error', 'absolute_error']:

                # Get prediction error
                pred_error = getattr(algorithm, error_metric)[i]

                # Loop through each measure type
                for ad_measure_type in ['distance', 'boundary', 'intrinsic']:

                    if ad_measure_type == 'intrinsic':
                        ad_measure_array = algorithm['intrinsic'][i]
                        # Get result dictionary
                        result_dict = getattr(algorithm, ad_measure_type)[key][i]
                        pearson_corr = result_dict['Pearson_correlation']
                        pearson_p = result_dict['Pearson_p_val']
                        slope = result_dict['Slope']
                        stderr = result_dict['Stderr']
                        p_val = result_dict['p_val']
                        r2 = result_dict['R2']

                        label = f'Pearson R: {pearson_corr: .2g} p = {pearson_p: .2g}'\
                        + '\n' + f'Slope: {slope: .2g} +/- {stderr: 0.2g} p = {p_val: .2g}'\
                        + '\n' f'R2: {r2: .2g}'

                        # Get algorithm name and RFE
                        alg_name = algorithm.estimator.__clas__.__name__
                        if algorithm.recursive_features:
                            rfe = f'RFE_{algorithm.recursive_features}'
                        else:
                            rfe = 'NoRFE'

                        # Plot
                        f1 = plt.figure()
                        plt.plot(ad_measure_array, pred_error, 'ro', label=label)
                        plt.xlabel(key)
                        plt.ylabel(error_metric)
                        plt.title(alg_name)
                        plt.legend(loc="upper left")

                        # Save
                        file_name = f'{key}_test_set{i}.png'
                        file_path = os.path.join(out_path, alg_name, rfe, error_metric, 'Correlations', file_name)
                        plt.tight_layout()
                        plt.savefig(file_path)

                    else:
                        # Loop through each measure
                        for key in getattr(algorithm, 'Results')[ad_measure_type].keys():

                            # Get ad measure array
                            ad_measure_array = self.test_set_ad_measures[ad_measure_type][key][i]

                            # Get result dictionary
                            result_dict = getattr(algorithm, ad_measure_type)[key][i]
                            pearson_corr = result_dict['Pearson_correlation']
                            pearson_p = result_dict['Pearson_p_val']
                            slope = result_dict['Slope']
                            stderr = result_dict['Stderr']
                            p_val = result_dict['p_val']
                            r2 = result_dict['R2']

                            label = f'Pearson R: {pearson_corr: .2g} p = {pearson_p: .2g}'\
                            + '\n' + f'Slope: {slope: .2g} +/- {stderr: 0.2g} p = {p_val: .2g}'\
                            + '\n' f'R2: {r2: .2g}'

                            # Get algorithm name and RFE
                            alg_name = algorithm.estimator.__clas__.__name__
                            if algorithm.recursive_features:
                                rfe = f'RFE_{algorithm.recursive_features}'
                            else:
                                rfe = 'NoRFE'

                            # Plot
                            f1 = plt.figure()
                            plt.plot(ad_measure_array, pred_error, 'ro', label=label)
                            plt.xlabel(key)
                            plt.ylabel(error_metric)
                            plt.title(alg_name)
                            plt.legend(loc="upper left")

                            # Save
                            file_name = f'{key}_test_set{i}.png'
                            file_path = os.path.join(out_path, alg_name, rfe, error_metric, 'Correlations', file_name)
                            plt.tight_layout()
                            plt.savefig(file_path)

        return None

    def save_pkl(self, algorithm=None, out_path=None):
        """ Saves dataset and algorithm information in a PKL file.

        algorithm: Algorithm class object with predictions and correlation results
        out_path: Optional custom PKL file path
        """
        # Class dictionaries
        dataset_dict = self.__dict__.copy()
        algorithm_dict = algorithm.__dict__.copy()
        dict_list = [dataset_dict,algorithm_dict]

        # path variables
        alg_name = algorithm.estimator.__class__.__name__

        if algorithm.recursive_features:
            rfe = f'RFE_{algorithm.recursive_features}'
        else:
            rfe = 'NoRFE'

        if self.scaled_:
            scaled = self.scaled_
        else:
            scaled = 'None'

        if self.var_filter_['Value']:
            varfilt = self.var_filter_['Value']
        else:
            varfilt = 'None'

        if self.corr_filter_['Value']:
            corrfilt = self.corr_filter_['Value']
        else:
            corrfilt = 'None'

        dataset_name = self.dataset_name
        file_name = f'{dataset_name}_{alg_name}_{rfe}_Scaled{scaled}_Var{varfilt}_Corr{corrfilt}.pkl'

        if not out_path:
            pkl_folder = r'C:\Users\skolmar\PycharmProjects\Modeling\AD_Methods\PKL'
            out_path = os.path.join(pkl_folder, dataset_name, alg_name, file_name)
        else:
            out_path = os.path.join(out_path, dataset_name, alg_name)

        output = open(out_path, 'wb')
        pickle.dump(dict_list, output)
        output.close()

        return None

###################
# PKL METHOD
###################

def plot_comparisons_pkl(pkl_file, out_path):
        """ Plots differences and ratios of metrics for compounds inside and outside the applicability domain,
        for each AD measure, for a given algorithm.

        algorithm: Algorithm class object with predictions
        out_path: Absolute path to the results for a dataset
        """

        pkl_list = pickle.load(open(pkl_file, 'rb'))
        data_dict = pkl_list[0]
        alg_dict = pkl_list[1]

        # Loop through all test sets
        for i in range(len(data_dict['X_test'])):

            # Loop through all measure types
            for ad_measure_type in ['distance', 'boundary']:

                # Loop through each measure
                for key in data_dict['test_set_ad_measures'][ad_measure_type].keys():

                    # Get ratios
                    ratio_dict =  alg_dict['Ratios'][ad_measure_type][key][i]
                    rmse_ratio = ratio_dict['rmse_ratios']
                    r2_ratio = ratio_dict['r2_ratios']
                    rmse_ratio_label = 'RMSEin/RMSEout'
                    r2_ratio_label = 'R2in/R2out'

                    # Get differences
                    difference_dict = alg_dict['Differences'][ad_measure_type][key][i]
                    rmse_difference = difference_dict['rmse_differences']
                    r2_difference = difference_dict['r2_differences']
                    rmse_difference_label = 'RMSEout-RMSEin'
                    r2_difference_label = 'R2in-R2out'

                    # Set xaxis
                    if ad_measure_type == 'distance':
                        ratio_xaxis = ratio_dict['threshold_percentiles']
                        ratio_xlabel = 'Training Set Distance Percentile'
                        difference_xaxis = difference_dict['threshold_percentiles']
                        difference_xlabel = 'Training Set Distance Percentile'
                    elif ad_measure_type == 'boundary':
                        ratio_xaxis = ratio_dict['thresholds']
                        ratio_xlabel = 'Thresholds'
                        difference_xaxis = difference_dict['thresholds']
                        difference_xlabel = 'Thresholds'

                    # Make Ratio figure with two different y-axes on same plot
                    fig, ax1 = plt.subplots()

                    # Plot Ratio R2 data
                    color = 'tab:red'
                    ax1.set_xlabel(ratio_xlabel)
                    ax1.set_ylabel(r2_ratio_label, color=color)
                    ax1.plot(ratio_xaxis, r2_ratio, color=color)
                    ax1.tick_params(axis='y', labelcolor=color)

                    # Set duplicate x axis
                    ax2 = ax1.twinx()

                    # Plot Ratio RMSE data
                    color = 'tab:blue'
                    ax2.set_ylabel(rmse_ratio_label, color=color)
                    ax2.plot(ratio_xaxis, rmse_ratio, color=color)
                    ax2.tick_params(axis='y', labelcolor=color)

                    # Set figure title and layout
                    alg_name = alg_dict['estimator'].__class__.__name__
                    fig.suptitle(f'{alg_name}, {key}')
                    fig.tight_layout()

                    # Save figure
                    if alg_dict['recursive_features']:
                        rfe = f"RFE_{alg_dict['recursive_features']}"
                    else:
                        rfe = 'NoRFE'
                    file_name = f'Ratios_{key}_test_set{i}.png'
                    file_path = os.path.join(out_path, alg_name, rfe,'Ratios', file_name)
                    plt.savefig(file_path)

                    # Make Difference figure
                    fig2, ax3 = plt.subplots()

                    # Plot Difference R2 data
                    color = 'tab:red'
                    ax3.set_xlabel(difference_xlabel)
                    ax3.set_ylabel(r2_difference_label, color=color)
                    ax3.plot(difference_xaxis, r2_difference, color=color)
                    ax3.tick_params(axis='y', labelcolor=color)

                    # Set duplicate x axis
                    ax4 = ax3.twinx()

                    # Plot Ratio RMSE data
                    color = 'tab:blue'
                    ax4.set_ylabel(rmse_difference_label, color=color)
                    ax4.plot(difference_xaxis, rmse_difference, color=color)
                    ax4.tick_params(axis='y', labelcolor=color)

                    # Set figure title and layout
                    fig.suptitle(f'{alg_name}, {key}')
                    fig.tight_layout()

                    # Save figure
                    file_name = f'Differences_{key}_test_set{i}.png'
                    file_path = os.path.join(out_path, alg_name, rfe, 'Differences', file_name)
                    plt.savefig(file_path)

        return None

def pkl_plot_correlations(pkl_file, out_path):
        """
        algorithm: Algorithm class object with predictions and correlations
        out_path: Absolute path to PNG folder
        """

        pkl_list = pickle.load(open(pkl_file, 'rb'))
        data_dict = pkl_list[0]
        alg_dict = pkl_list[1]

        # Loop through each test set
        for i in range(len(data_dict['X_test'])):

            # Loop through each error metric
            for error_metric in ['squared_error', 'absolute_error']:

                # Get prediction error
                pred_error = alg_dict[error_metric][i]

                # Loop through each measure type
                for ad_measure_type in ['distance', 'boundary']:

                    # Loop through each measure
                    for key in alg_dict['Results'][error_metric][ad_measure_type].keys():

                        # Get ad measure array
                        ad_measure_array = data_dict['test_set_ad_measures'][ad_measure_type][key][i]

                        # Get result dictionary
                        result_dict = alg_dict['Results'][error_metric][ad_measure_type][key][i]
                        pearson_corr = result_dict['Pearson_correlation']
                        pearson_p = result_dict['Pearson_p_val']
                        slope = result_dict['Slope']
                        stderr = result_dict['Stderr']
                        p_val = result_dict['p_val']
                        r2 = result_dict['R2']

                        label = f'Pearson R: {pearson_corr: .2g} p = {pearson_p: .2g}'\
                        + '\n' + f'Slope: {slope: .2g} +/- {stderr: 0.2g} p = {p_val: .2g}'\
                        + '\n' f'R2: {r2: .2g}'

                        # Get algorithm name and RFE
                        alg_name = alg_dict['estimator'].__class__.__name__
                        if alg_dict['recursive_features']:
                            rfe = f"RFE_{alg_dict['recursive_features']}"
                        else:
                            rfe = 'NoRFE'

                        # Plot
                        f1 = plt.figure()
                        plt.plot(ad_measure_array, pred_error, 'ro', label=label)
                        plt.xlabel(key)
                        plt.ylabel(error_metric)
                        plt.title(alg_name)
                        plt.legend(loc="upper left")

                        # Save
                        file_name = f'{key}_test_set{i}.png'
                        file_path = os.path.join(out_path, alg_name, rfe, error_metric, 'Correlations', file_name)
                        plt.tight_layout()
                        plt.savefig(file_path)

        return None


# coding:utf-8
'''''
Created on 2018

@author: J
'''
import numpy as np
import createData_for_knn as cd
from sklearn.cross_validation import train_test_split
from sklearn import preprocessing
from sklearn import metrics
import matplotlib.pyplot as plt

np.random.seed(23)

# 定义双曲函数和他们的导数
def tanh(x):
    return np.tanh(x)

def tanh_deriv(x):
    return 1.0 - np.tanh(x) ** 2

def logistic(x):
    return 1 / (1 + np.exp(-x))

def logistic_derivative(x):
    return logistic(x) * (1 - logistic(x))

# 定义NeuralNetwork 神经网络算法
class NeuralNetwork:
    # 初始化，layes表示的是一个list，eg[10,10,3]表示第一层10个神经元，第二层10个神经元，第三层3个神经元
    def __init__(self, layers, activation='tanh'):
        """
        :param layers: A list containing the number of units in each layer.
        Should be at least two values
        :param activation: The activation function to be used. Can be
        "logistic" or "tanh"
        """
        if activation == 'logistic':
            self.activation = logistic
            self.activation_deriv = logistic_derivative
        elif activation == 'tanh':
            self.activation = tanh
            self.activation_deriv = tanh_deriv

        self.weights = []
        # 循环从1开始，相当于以第二层为基准，进行权重的初始化
        for i in range(1, len(layers) - 1):
            # 对当前神经节点的前驱赋值
            self.weights.append((2 * np.random.random((layers[i - 1] + 1, layers[i] + 1)) - 1) * 0.25)
            # 对当前神经节点的后继赋值
            self.weights.append((2 * np.random.random((layers[i] + 1, layers[i + 1])) - 1) * 0.25)

    # 训练函数   ，X矩阵，每行是一个实例 ，y是每个实例对应的结果，learning_rate 学习率，
    # epochs，表示抽样的方法对神经网络进行更新的最大次数
    def fit(self, X, y, learning_rate=0.2, epochs=10000):
        X = np.atleast_2d(X)  # 确定X至少是二维的数据
        temp = np.ones([X.shape[0], X.shape[1] + 1])  # 初始化矩阵
        temp[:, 0:-1] = X  # adding the bias unit to the input layer
        X = temp
        y = np.array(y)  # 把list转换成array的形式

        for k in range(epochs):
            # 随机选取一行，对神经网络进行更新
            i = np.random.randint(X.shape[0])
            a = [X[i]]

            # 完成所有正向的更新
            for l in range(len(self.weights)):
                a.append(self.activation(np.dot(a[l], self.weights[l])))
                #
            error = y[i] - a[-1]
            deltas = [error * self.activation_deriv(a[-1])]

            # 开始反向计算误差，更新权重
            for l in range(len(a) - 2, 0, -1):  # we need to begin at the second to last layer
                deltas.append(deltas[-1].dot(self.weights[l].T) * self.activation_deriv(a[l]))
            deltas.reverse()
            for i in range(len(self.weights)):
                layer = np.atleast_2d(a[i])
                delta = np.atleast_2d(deltas[i])
                self.weights[i] += learning_rate * layer.T.dot(delta)

    # 预测函数

    def predict(self, x):
        x = np.array(x)
        temp = np.ones(x.shape[0] + 1)
        temp[0:-1] = x
        a = temp
        for l in range(0, len(self.weights)):
            a = self.activation(np.dot(a, self.weights[l]))
        return a

def print_evaluate(test_labels,predict_label):
    print('confusion_matrix:')
    print(metrics.confusion_matrix(test_labels, predict_label))

    print('precision: ', end='')
    print(metrics.precision_score(test_labels, predict_label, average=None))

    print('recall:', end='')
    print(metrics.recall_score(test_labels, predict_label, average=None))

    print('accu:', end='')
    print(metrics.accuracy_score(test_labels, predict_label))

    print('classify_report:')
    print(metrics.classification_report(test_labels, predict_label))

def optLayer(layer):
    data = cd.createData(10000)
    features = data[:,0:-1]
    labels = data[:,-1]
    min_max_scaler = preprocessing.MinMaxScaler()
    labels = min_max_scaler.fit_transform(labels) #归一化

    train_features, test_features, train_labels, test_labels = train_test_split(
        features, labels, test_size=0.33, random_state=23323)

    nn = NeuralNetwork(layers=[35,layer,1])
    nn.fit(train_features,train_labels,0.1)
    predice_result_set = []
    for x in test_features:
        y_pre = nn.predict(x)
        predice_result_set.append(y_pre)

    test_labels = min_max_scaler.inverse_transform(test_labels)
    predice_result_set = min_max_scaler.inverse_transform(predice_result_set)#去归一化

    positive = 0
    for i in range(len(test_features)):
        # if test_labels[i] == [round(int(pr)) for pr in predice_result_set][i]:
        if abs(test_labels[i] - predice_result_set[i]) < 0.5:
            positive = positive +1
        # print('x_true:',end='')
        # print(test_labels[i],end='')
        # print(', y_pre:',end='')
        # print(predice_result_set[i])
    return positive/len(test_labels)
    # print('acc:'+str(positive/len(test_labels)) )
    # plt.plot(test_labels[:50])
    # plt.plot(predice_result_set[:50],'r')
    # plt.show()

# acc_set = []
# for i in range(10,50):
#     acc_set.append(optLayer(i))
# plt.plot(acc_set)
# plt.show()
print('accu:'+str(optLayer(24)))


from PIL import ImageFont, ImageDraw
from pyowm import OWM
from pyowm.utils import config
from pyowm.utils import timestamps
from datetime import datetime

from PIL import Image, ImageOps
import urllib.request

from config_loader import config

def make_weather_image():

    owm = OWM(config["OWM_key"])
    mgr = owm.weather_manager()

    observation = mgr.weather_at_coords(config["lat"], config["lon"])
    w = observation.weather

    weather_status = w.detailed_status
    weather_status = weather_status[0].upper() + weather_status[1:]
    weather_icon = w.weather_icon_name
    # 100 x 100 image
    weather_icon_url = f"https://openweathermap.org/img/wn/{weather_icon}@2x.png"

    temp = 'temp.png'
    urllib.request.urlretrieve(weather_icon_url, temp)

    t_max = round(w.temperature('fahrenheit')['temp_max'])
    t_min = round(w.temperature('fahrenheit')['temp_min'])
    t_cur = round(w.temperature('fahrenheit')['temp'])
    temp_message = f"Now: {t_cur} H:{t_max} L:{t_min}"
    icon = Image.open(temp)

    now = datetime.now()
    now = now.strftime("%Y-%m-%d - %A")

    image = Image.new('RGB', (400, 100))

    font = ImageFont.truetype(config["font_name"], 20)
    draw = ImageDraw.Draw(image)
    draw.rectangle([0, 0, 400, 100], fill="#ffffff")
    draw.rectangle([1, 1, 399, 99], fill="#000000")

    draw.text((105, 10), f"{now}\n{temp_message}\n{weather_status}", (255, 255, 255), font=font)
    image = ImageOps.invert(image)
    #image = ImageOps.posterize(image, 1)
    image.paste(icon)
    image.show()

    image.save(temp)

    #forecast = mgr.three_hour_forecast(config["location"])


from decimal import Decimal

from django.core.validators import MinValueValidator
from django.db import models
from django.utils.translation import gettext_lazy as _

from src.utils.models import ACMEModel

class User(ACMEModel):

    user_id = models.CharField(
        unique=True,
        max_length=17
    )

    pin = models.PositiveSmallIntegerField()

    def __str__(self):
        return _('User %(user_id)s') % {'user_id': self.user_id}

class Account(ACMEModel):

    user = models.OneToOneField(
        User,
        on_delete=models.PROTECT
    )

    balance = models.DecimalField(
        default=Decimal(0),
        max_digits=17,
        decimal_places=2,
        validators=[
            MinValueValidator(Decimal(0)),
        ]
    )

    def __str__(self):
        return _('User %(user_id)s account with balance: %(balance)s') % {
            'user_id': self.user.user_id,
            'balance': self.balance,
        }

class Upload(ACMEModel):

    file = models.FileField(
        upload_to='workflow/files/'
    )
    success = models.BooleanField(default=False)
    logs = models.JSONField(default=list)


import sys

sys.path.append(".")

from bisturi.field import Field, Data
from netaddr import EUI

class MACAddress(Field):
    '''MAC (Ethernet) address using the 'netaddr' lib.
      Two version, the common 48 bits (6 bytes) and the uncommon 64 bits.

      See https://pythonhosted.org/netaddr/api.html.'''
    def __init__(self, version=48, default=None):
        Field.__init__(self)
        self.version = version
        if default is None:
            default = EUI(0, version)
        self.default = default

    def init(self, packet, defaults):
        obj = defaults.get(self.field_name, self.default)
        setattr(packet, self.field_name, EUI(obj, self.version))

    def pack(self, pkt, fragments, **k):
        obj = getattr(pkt, self.field_name)
        fragments.append(obj.packed)
        return fragments

    def unpack(self, pkt, raw, offset=0, **k):
        obj = EUI(raw[offset:offset + 6].hex())
        setattr(pkt, self.field_name, obj)

        return offset + 6

if __name__ == '__main__':
    from bisturi.packet import Packet

    class TestPacket(Packet):
        src = MACAddress(default=EUI("ff:ff:00:00:00:11"))
        dst = MACAddress()

    p1 = TestPacket(dst=EUI("11:11:11:22:22:22"))
    assert p1.pack() == b'\xff\xff\x00\x00\x00\x11\x11\x11\x11\x22\x22\x22'

    p1.dst = EUI('33:33:33:33:33:33')
    assert p1.pack() == b'\xff\xff\x00\x00\x00\x11\x33\x33\x33\x33\x33\x33'

    p2 = TestPacket()
    assert p2.pack() == b'\xff\xff\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00'

    p4 = TestPacket.unpack((p2.pack()))
    assert p4.pack() == b'\xff\xff\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00'


import os
import sys

import mlflow
from mlflow.tracking import MlflowClient

HOME = 'customer-complaints-email-classification-case-study'
curr = os.path.dirname(__file__)
while True:
    if curr.endswith(HOME):
        break
    curr = os.path.dirname(curr)
sys.path.append(curr)

from utils import get_credential

class MlflowTrackingServer:

    def __init__(self, host=""):
        self.host = host
        mlflow.set_tracking_uri(f"http://{self.host}:5000")

    def createif_experiment(self, name):
        mlflow.set_experiment(name)

if __name__ == '__main__':

    host_file = os.path.join(curr, 'doc', 'mlflow.tracking_server.host.txt')

    tracking_server = MlflowTrackingServer(host=get_credential(host_file))

    tracking_server.createif_experiment("experiment-002")


#!/usr/bin/python
#-*- coding: utf-8 -*-

import pygame
import zrcommon
from Vector2D import Vector2D

class Obstacle:
    """Class that represents an obstacle."""
    def __init__(self, game_display, center, radius, color):
        """Constructor.

        Args:
            param1 (pygame.Surface): game display handler
            param2 ((int, int)): center of game world coordinates in form of a touple (x, y)
            param3 (int): obstacle's radius
            param4 ((int, int, int)): obstacle's color in form of a touple (r, g, b)
        """
        self.__screen = game_display
        self.center = Vector2D(center[0], center[1])
        self.radius = radius
        self.__color = color

    def draw(self):
        """Draws Obstacle."""
        pygame.draw.circle(self.__screen,
                           self.__color,
                           (self.center.x, self.center.y),
                           self.radius,
                           1
                          )

    def is_collided(self, ob):
        """Checks if Obstacle collides with given object.

        Args:
            param ((int, int, int)): object info used in collision detection in form of a touple (x, y, radius)

        Returns:
            True if collision occured. False otherwise.
        """
        return zrcommon.check_collision((self.center.x, self.center.y, self.radius), ob)

    def avoid_collision(self, ob):
        """Checks if Obstacle collides with given object.
        If yes, recalculate position of given object to avoid collision.

        Args:
            param ((int, int, int)): object info used in collision detection in form of a touple (x, y, radius)

        Returns:
            True if collision occured. False otherwise.
        """
        return zrcommon.avoid_collision((self.center.x, self.center.y, self.radius), ob)


#!/usr/bin/env python
# -*- encoding: utf-8 -*-
# Created on 2018-04-26 10:52:48
# Project: wechat

from pyspider.libs.base_handler import *
import pymongo

class Handler(BaseHandler):
    start_url = 'http://weixin.sogou.com/weixin?'
    query = {
        'query': 'python',
        'type': 2,
        'page': 1,
        'ie': 'utf8',
    }
    client = pymongo.MongoClient("localhost")
    db = client['wechat']
    crawl_config = {

    }

    @every(minutes=24 * 60)
    def on_start(self):
        self.crawl(Handler.start_url, callback=self.index_page, params=Handler.query, method="GET")

    @config(age=10 * 24 * 60 * 60)
    def index_page(self, response):
        for each in response.doc('.news-box .news-list li .txt-box h3 a').items():
            self.crawl(each.attr.href, callback=self.detail_page)
        if response.doc('.p-fy .np').attr.href:
            self.crawl(response.doc('.p-fy .np').attr.href, callback=self.index_page)

    @config(priority=2)
    def detail_page(self, response):
        author = response.doc('.rich_media_meta_link').text()
        wechat = response.doc('#js_profile_qrcode > div > p:nth-child(3) > span').text()
        content = response.doc('#js_content').text()
        return {
            "url": response.url,
            "title": response.doc('title').text(),
            'author': author,
            'wechat': wechat,
            'content': content,
        }

    def on_result(self, result):
        if result:
            self.save_to_mongo(result)

    def save_to_mongo(self, result):
        if self.db["articles"].insert(result):
            print("save to mongodb sucess", result)


import torch
import torch.nn as nn
from torch.utils.data import DataLoader
import albumentations as alb
import numpy as np

import json
import os

from data_load import DspitesColored, train_val_split
from models import AutoEncoderWithProjector
from metircs_losses import NT_Xent, metric_Recall_top_K
from utils import image_color_batch_transformation

def recall_validation_step(model, batch, augment_transform_list1, augment_transform_list2, device):
    augment_transform1 = np.random.choice(augment_transform_list1)
    augment_transform2 = np.random.choice(augment_transform_list2)
    augment_transform3 = np.random.choice(augment_transform_list1)
    augment_transform4 = np.random.choice(augment_transform_list2)
    batch1 = image_color_batch_transformation(batch, augment_transform1).to(device)
    batch2 = image_color_batch_transformation(batch, augment_transform2).to(device)
    batch3 = image_color_batch_transformation(batch, augment_transform3).to(device)
    batch4 = image_color_batch_transformation(batch, augment_transform4).to(device)
    embedding1 = model.encode(batch1)
    embedding2 = model.encode(batch2)
    embedding3 = model.encode(batch3)
    embedding4 = model.encode(batch4)
    batch_composed = torch.cat((embedding1, embedding2, embedding3, embedding4), dim=0)
    batch_composed = batch_composed.squeeze()

    y_label = torch.arange(0, batch.shape[0]).long()
    y_label = torch.cat((y_label, y_label, y_label, y_label), dim=0)
    y_label = y_label.to(batch.device)

    recall_k = metric_Recall_top_K(batch_composed.squeeze(), y_label, K=3, metric='cosine')
    return recall_k

def recall_validation(model, data_loader_val, augment_transform_list1, augment_transform_list2, device):
    recall_list = []
    recall_list10 = []
    for batch_i, batch in enumerate(data_loader_val):
        batch = batch['image']
        batch = batch.type(torch.FloatTensor)
        batch = batch.to(device)
        recall_k = recall_validation_step(model, batch, augment_transform_list1, augment_transform_list2, device)
        recall_list.append(recall_k[0])
        recall_list10.append(recall_k[1])
    recall = sum(recall_list) / len(recall_list)
    recall10 = sum(recall_list10) / len(recall_list10)
    return recall, recall10

def sim_clr_training_step(model, batch, transformation1, transformation2, loss_f, device, optimizer):
    augment_transform1 = np.random.choice(transformation1)
    augment_transform2 = np.random.choice(transformation2)
    augmented_batch1 = image_color_batch_transformation(batch, augment_transform1)
    augmented_batch1 = augmented_batch1.to(device)
    augmented_batch2 = image_color_batch_transformation(batch, augment_transform2)
    augmented_batch2 = augmented_batch2.to(device)

    embedding1 = model.encode(augmented_batch1)
    metric_space1 = model.projector(embedding1)
    embedding2 = model.encode(augmented_batch2)
    metric_space2 = model.projector(embedding2)

    loss = loss_f(metric_space1, metric_space2)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    return loss

def contrastive_learning(model, data_loader_train, conf, epoch_number):
    device = conf['train']['device']
    transform1 = [alb.GaussianBlur(p=1),
                  alb.GaussNoise(var_limit=(0.0, 0.07), mean=0.1, p=1),
                  alb.augmentations.transforms.VerticalFlip(p=1),
                  alb.OpticalDistortion(p=1, distort_limit=(-0.2, 0.2), shift_limit=(-0.2, 0.2)),
                  alb.ColorJitter(p=1),
                  alb.Rotate(p=1, limit=20)]

    transform2 = [alb.GridDistortion(p=1, num_steps=10, distort_limit=1.0),
                  alb.HorizontalFlip(p=1),
                  alb.RandomSizedCrop(p=1, min_max_height=[50, 64], height=64, width=64, w2h_ratio=1),
                  alb.ChannelShuffle(p=1),
                  alb.RandomFog(p=1),
                  alb.ToGray(p=1),
                  alb.GridDropout(p=1, ratio=0.4)
                  ]

    loss_f = NT_Xent(batch_size=conf['train']['batch_size'], temperature=1.0, device=conf['train']['device'])

    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    loss_list = []

    np.random.seed()
    for epoch in range(epoch_number):
        if epoch == 7000:
            for param in optimizer.param_groups:
                param['lr'] = 0.0005
                print('lr: ', param['lr'])
        for batch in data_loader_train:
            batch = batch['image']
            loss = sim_clr_training_step(model, batch, transform1, transform2, loss_f, device, optimizer)
            loss_list.append(loss.item())
        print('{0} loss: {1}'.format(epoch, sum(loss_list)/len(loss_list)))
        if epoch % 10 == 0:
            recall, _ = recall_validation(model, data_loader_train, transform1, transform2, device)
            print('recall: ', recall)

if __name__ == "__main__":
    with open("config.json") as json_file:
        conf = json.load(json_file)
    device = conf['train']['device']

    number_of_colors = 9
    print('number of colors:', number_of_colors)
    dataset_path = os.path.join(conf['data']['dataset_path'], conf['data']['dataset_file'])
    dspites_dataset_colored = DspitesColored(dataset_path, number_of_colors=number_of_colors)

    #dspites_dataset_colored = dspites_dataset_colored.clone_subset()
    print('dataset length', len(dspites_dataset_colored))

    random_state = np.random.randint(0, 1000000)
    print('random_state:', random_state)
    train_val = train_val_split(dspites_dataset_colored, fixed_size_train=1474, random_state=random_state)#val_split=0.998,)
    data_loader_train = DataLoader(train_val['train'], batch_size=conf['train']['batch_size'], shuffle=True,
                                   num_workers=2, drop_last=True)

    model = AutoEncoderWithProjector(in_channels=3, dec_channels=3, latent_size=conf['model']['latent_size'])
    #model.load_state_dict(torch.load('weights/colored1_metric_learning.pth'))
    model = model.to(device)

    contrastive_learning(model, data_loader_train, conf, 8000)
    torch.save(model.state_dict(), 'weights/colored9_2.pth')


import numpy as np
from scipy import stats as stats

'''
The subject specific analyses (correlational similarity) can be computed much
    faster if we analyze all components and all subjects at once using numpy
    arrays instead of for-looping over individual components and subjects.This
    makes the code more elaborate and complicated. This functionality has been
    but in this module, as opposed to in connsearcher.py.
'''

def corrsim_all_components(X_components: np.ndarray,
                           Y: np.ndarray,
                           get_similarity: bool = False,
                           subtract_mean: bool = True) -> tuple:
    '''
    Calculates correlational similarity for every component (X)
        with respect to labels Y

    X_components contains all data for all components and for both Y = 0 & Y = 1
        The Y vector is used to extract X[Y = 0] and X[Y = 1]

    After this, correlational similarity is calculated for each component

    Setting subtract_mean=True causes group-level effects (i.e., differences
        between the two conditions seen across all participants) to be regressed
        out of each subject's data. This is done on a component by component
        and edge-by-edge basis for every participant before calculation of
        correlational similarity.
    Setting subtract_mean=True ensures that any effects found via subject-
        specific ConnSearch indeed are unique to that specific subject and do
        not apply to everybody.
    The subtract_mean=True subtraction and code are somewhat obtuse. To
        regress out group-level effects on subject i, we must also omit
        subject i's contribution to the group-level effects. Doing this in an
        unbiased way requires the code below. For detFails, please see
        Supplemental Materials 1.4 of the paper.

    After calculating correlational similarity on a subject-by-subject and
        component-by-component basis, a t-test is performed across-subjects to
        assess whether the correlational similarity effect is significant.
        The results of the t-tests (t-values and p-values) are returned.

    :param X_components: 6D array if running ConnSearch, (component, subject,
                         session, example, ROI, ROI). Can be a 5D array
                         if doing wu_analysis=True.
    :param Y: 3D array of labels, (subject, session, example)
    :param get_similarity: If True, returns the raw correlational similarity
                           values instead of t-values and p-values. This =True
                           functionality was not used for the final manuscript.
    :param subtract_mean: If True, regresses out group-level effects before
                          calculating correlational similarity
    :return: Assuming get_similarity=False, returns a tuple of (t-values,
                p-values, and euclidean_adj_matrix). The t-values and p-values
                are lists wherin each element represent the statistics for
                a given component. The euclidean_adj_matrix is used for plotting
    '''

    component_size = X_components.shape[4]
    tril_idxs = np.tril_indices(component_size, k=-1)

    if len(X_components.shape) == 6:
        X0_flat = extract_cond_X(0, X_components, Y)[:, :, :, :,
                  tril_idxs[0], tril_idxs[1]]
        X1_flat = extract_cond_X(1, X_components, Y)[:, :, :, :,
                  tril_idxs[0], tril_idxs[1]]
    else:
        X0_flat = extract_cond_X(0,
                        np.expand_dims(X_components, 5), Y).squeeze(axis=-1)
        X1_flat = extract_cond_X(1,
                        np.expand_dims(X_components, 5), Y).squeeze(axis=-1)

    if subtract_mean:  # Subtract group-level effects following the equations
        # Provided in Supplemental Materials 1.3.
        X0_flat_M = X0_flat.mean(axis=(1, 2))
        X1_flat_M = X1_flat.mean(axis=(1, 2))
        X0_flat_subj_M = X0_flat.mean(axis=2)[:, :, np.newaxis]
        X1_flat_subj_M = X1_flat.mean(axis=2)[:, :, np.newaxis]

        X0_flat_M_prime = (X0_flat_M[:, np.newaxis, np.newaxis, :] -
                           X0_flat_subj_M / (X_components.shape[1] * 2)) * \
                          X_components.shape[1] * 2 / \
                          (X_components.shape[1] * 2 - 1)
        X1_flat_M_prime = (X1_flat_M[:, np.newaxis, np.newaxis, :] -
                           X1_flat_subj_M / (X_components.shape[1] * 2)) * \
                          X_components.shape[1] * 2 / \
                          (X_components.shape[1] * 2 - 1)

        X0_flat = X0_flat - X0_flat_M_prime
        X1_flat = X1_flat - X1_flat_M_prime

    euclidean_adj_matrix = get_euclidean_edge_distances(X0_flat, X1_flat,
                                                        X_components)
    corr_same_00_10 = corr_all(X0_flat[:, :, 0], X0_flat[:, :, 1], axis=3)
    corr_same_10_11 = corr_all(X1_flat[:, :, 0], X1_flat[:, :, 1], axis=3)
    corr_dif_00_11 = corr_all(X0_flat[:, :, 0], X1_flat[:, :, 1], axis=3)
    corr_dif_10_01 = corr_all(X1_flat[:, :, 0], X0_flat[:, :, 1], axis=3)

    similarity = corr_same_00_10 + corr_same_10_11 - \
                 corr_dif_00_11 - corr_dif_10_01
    similarity = np.squeeze(similarity)
    if get_similarity:
        return similarity, euclidean_adj_matrix

    M_similarity = np.mean(similarity, axis=1)
    SE_similarity = np.std(similarity, axis=1, ddof=1) / \
                    np.sqrt(similarity.shape[1])
    ts = M_similarity / SE_similarity
    ps = stats.t.sf(ts, similarity.shape[1] - 1)
    return ts, ps, euclidean_adj_matrix

def extract_cond_X(cond: int,
                   X_components: np.ndarray,
                   Y: np.ndarray):
    '''
    Extracts examples from X_components for a given condition (Y = cond)

    for X_components:
        axis=0 is component, axis=1 is subject, axis=2 is session,
        axis=3 is example, axes=4 & 5 are ROIs.
        See elaboration in the documentation for class ConnSearcher
    for Y: axis=0 is subject, axis=1 is session, axis=2 is example

    Assumes that every session only has a single example of Y = cond

    :param cond: value of Y to extract
    :param X_components: 6D numpy array, (component, subject, session, example,
                                          ROI, ROI)
    :param Y: 3D numpy array, (subject, session, example)
    :return: 6D numpy, with the same structure as X_components
    '''
    Y_bool = Y == cond
    # The code below basically takes Y_bool and repeats it across the three
    #   axes of X_components, which Y does not have
    # Y_bool_repeated is then used to extract X_components_cond
    Y_bool_repeated = np.repeat(np.repeat(np.repeat(
        Y_bool[np.newaxis, :, :, :], X_components.shape[0], axis=0)
              [:, :, :, :, np.newaxis], X_components.shape[4], axis=4)
              [:, :, :, :, :, np.newaxis], X_components.shape[5], axis=5)
    X_components_cond = np.where(Y_bool_repeated, X_components, np.nan)
    X_components_cond = np.nanmean(X_components_cond, axis=3, keepdims=True)
    # This code may seem a bit strange.
    #   However, I'm not sure whether there is a better way to do this. I can't
    #   find any dedicated numpy function for this nor a more elegant approach
    #   on searching StackOverflow.
    return X_components_cond

def corr_all(ar0: np.ndarray,
             ar1: np.ndarray,
             axis: int=1):
    '''
    For all i: correlates column i in 2D matrix ar0 x column i in 2D matrix ar1.
        Then, Fisher z-transforms the correlations

    This is equivalent to:
        corrXY = np.empty(ar.shape[1])
        for i in range(ar0.shape[1]):
            r, _ = stats.pearsonr(ar0[:, i], ar1[:, i])
            corrXY[i] = np.arctanh(r)
        return corrXY

    However, the code below is much faster than that above, as it calculates
        all the correlations at once via numpy arrays.

    Can be switched to correlating rows instead of columns by setting axis=0

    :param ar0: 2D array
    :param ar1: 2D array of same shape as ar0
    :param axis: axis to correlate along
    :return: 1D array of Fisher z-transformed correlations
    '''
    eXY = np.mean(ar0 * ar1, axis=axis)
    eXeY = ar0.mean(axis=axis) * ar1.mean(axis=axis)
    covXY = eXY - eXeY
    sdX = np.std(ar0, axis=axis)
    sdY = np.std(ar1, axis=axis)
    corrXY = covXY / (sdX * sdY)
    corrXY = np.arctanh(corrXY)  # Fisher z-transform
    return corrXY

def get_euclidean_edge_distances(X0_flat: np.ndarray,
                                 X1_flat: np.ndarray,
                                 X_components: np.ndarray):
    '''
    Calculates Euclidean edge distances between X0 and X1. Takes in data after
        the connectivity matrices have been flattened into vectors of features.
    It then converts the difference vectors back into matricies corresponding
        to the connectivity edges.
    These euclidean edge distances are used for plotting.

    :param X0_flat: 5D array, (component, subject, session, example, features)
    :param X1_flat: 5D array, (component, subject, session, example, features)
    :param X_components: Its shape is used to rebuild the matrices
    :return: 3D array, (component, ROI0, ROI1)
    '''

    euclidean = abs(X0_flat[:, :, 0] - X0_flat[:, :, 1]) + \
                abs(X1_flat[:, :, 0] - X1_flat[:, :, 1]) - \
                abs(X0_flat[:, :, 0] - X1_flat[:, :, 1]) - \
                abs(X1_flat[:, :, 0] - X0_flat[:, :, 1])

    euclidean = -euclidean
    if len(X_components.shape) == 5:  # For the Wu et al. (2021) analysis
        euclidean = np.mean(euclidean, axis=1)
        max_abs = np.max(abs(euclidean), axis=2)
        euclidean = np.squeeze(euclidean)
        max_abs = np.squeeze(max_abs)
        euclidean = euclidean.T / max_abs.T
        euclidean = euclidean.T
        return euclidean

    # Rebuild connectivity matrices
    euclidean_adj_matrix = np.zeros((euclidean.shape[0], euclidean.shape[1],
                                     X_components.shape[-2],
                                     X_components.shape[-1]))
    for component_i in range(euclidean.shape[0]):
        for subj_j in range(euclidean.shape[1]):
            euclidean_adj_matrix[component_i, subj_j] = \
                vector2matrix(euclidean[component_i, subj_j, 0, :])
    euclidean_adj_matrix = np.mean(euclidean_adj_matrix, axis=1)
    max_abs = np.max(abs(euclidean_adj_matrix), axis=(1, 2))
    euclidean_adj_matrix = euclidean_adj_matrix.T / max_abs.T  # Norm to [-1, 1]
    euclidean_adj_matrix = euclidean_adj_matrix.T
    return euclidean_adj_matrix

def vector2matrix(a: np.ndarray):
    '''
    Takes in a vector, e.g., shape = (120, ), then creates a matrix, e.g.,
        shape = (16, 16). Indices of the vector map to edges of matrix following
        the numpy procedures (see np.tri). The previous functions likewise
        used numpy to convert matrices to vectors (see np.tril_indices).
    :param a: 1D numpy array
    :return: 2D numpy array
    '''
    n = int(np.sqrt(len(a) * 2)) + 1
    mask = np.tri(n, dtype=bool, k=-1)  # or np.arange(n)[:,None] > np.arange(n)
    out = np.zeros((n, n), dtype=float)
    out[mask] = a
    out = out + out.T - np.diag(out.diagonal())  # copy bottom triangle to top
    return out


# -*- coding: utf-8 -*-
"""
    Double Deep Q-Learning Networks
"""

import numpy as np
import keras
import random
import os
import my_tools

from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras.optimizers import RMSprop
from keras.layers.advanced_activations import LeakyReLU

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
script_name = os.path.basename(__file__)
script_name = script_name.replace('.py', '')

# Possible actions
dense = [128, 256, 384, 512, 640, 768, 896, 1024]
dropouts = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]
# First action leaves the network intact
actions = [0]

actions.extend(dense)
actions.extend(dropouts)
# Create the log
logger = my_tools.ProgressLogger(script_name, actions)

max_depth = 4
start_depth = 1

max_memory = 2000
vals_memory = dict()
memory = []

gamma = 1
# depth+1 for the final state, len-1 because the first action
# is "stay"(no layers added)
states_no = (max_depth+1)*(len(actions)-1)
onehot_actions = keras.utils.to_categorical([x for x in range(len(actions)-1)])
states = np.zeros(states_no, dtype='int')

# Epsilon is bounded to [1.0,0.01]
epsilon = 1.0
epsilon_min = 0.01

epsilon_decay = 0.995

episodes = 200
baseline = 0.9
search_epochs = 3

def get_data():
    '''
        Get MNIST data
    '''
    num_classes = 10

    # the data, shuffled and split between train and test sets
    (x_train, y_train), (x_test, y_test) = mnist.load_data()
    x_train = x_train.reshape(60000, 784)
    x_test = x_test.reshape(10000, 784)
    x_train = x_train.astype('float32')
    x_test = x_test.astype('float32')
    x_train /= 255
    x_test /= 255

    print(x_train.shape[0], 'train samples')
    print(x_test.shape[0], 'test samples')

    # convert class vectors to binary class matrices
    y_train = keras.utils.to_categorical(y_train, num_classes)
    y_test = keras.utils.to_categorical(y_test, num_classes)
    return (x_train, y_train), (x_test, y_test)

def evaluate_model(model_in, x_train, y_train, x_test, y_test, epochs=search_epochs, verbose_in=0):
    '''
        Train and evaluate on test set
    '''
    batch_size = 128

    model_str = ''
    model = model_in
    # Add classification layer
    model.add(Dense(10, activation='softmax'))
    # Print the model's layers
    for layer in model.layers:
        model_str = model_str+str(layer.input_shape)
    print(model_str, end=' ')
    # Compile and train
    model.compile(loss='categorical_crossentropy',
                  optimizer=RMSprop(),
                  metrics=['accuracy'])

    history = model.fit(x_train, y_train,
                        batch_size=batch_size,
                        epochs=epochs,
                        verbose=verbose_in,
                        validation_data=(x_test, y_test))
    score = model.evaluate(x_test, y_test, verbose=0)
    # Remove classification layer
    model.pop()
    return score[1]

def generate_model():
    '''
        Generate random model
    '''

    # Avoid action 0
    action = np.random.randint(1, len(actions))
    model = Sequential()
    model.add(Dense(actions[action], activation='relu', input_shape=(784,)))

    return model, action

def expand_model(model, action):
    '''
        Act upon the architecture, adding a layer
    '''
    if model is None:
        model = Sequential()
        if actions[action] in dropouts:
            model.add(Dropout(actions[action], input_shape=(784,)))
        elif actions[action] in dense:
            model.add(
                Dense(actions[action], activation='relu', input_shape=(784,)))
        else:
            model = None
        return model

    # Terminal action, leaves the layers intact
    if actions[action] == 0:
        return model
    elif actions[action] in dense:
        model.add(Dense(actions[action], activation='relu'))
    else:
        model.add(Dropout(actions[action]))
    return model

def remember(states, action, acc, prev_acc, next_state):
    '''
        Reward is difference in accuracy
    '''
    reward = (acc-prev_acc)
    memory.append([states, action, reward, next_state])
    vals_memory[logger.repr_from_onehot(states)+','+str(action)] = acc
    if len(memory) >= max_memory:
        del memory[0]

def replay(ql, sz):
    '''
        Sample from memory and train the net
    '''
    global epsilon
    minibatch = random.sample(memory, sz)
    for state, action, reward, next_state in minibatch:
        state = state.reshape(1, states_no)
        next_state = next_state.reshape(1, states_no)
        target = reward + gamma*np.amax(qv.predict(next_state)[0])
        target_f = ql.predict(state)
        target_f[0][action] = target
        ql.fit(state, target_f, epochs=1, verbose=0)
    # Reduce epsilon
    if epsilon > epsilon_min:
        epsilon = epsilon*epsilon_decay
    update_q_target(ql, qv)

def train_q(ql):
    '''
        The RL
    '''
    (x_train, y_train), (x_test, y_test) = get_data()

    current_depth = start_depth

    for episode in range(episodes):
        if episode % 10 == 9:
            current_depth = min(current_depth+1, max_depth)
        prev_acc = baseline
        states = np.zeros(states_no, dtype='int')
        model = None

        for x in range(current_depth):
            print('Depth: '+str(x), end=' ')
            action = int(np.argmax(
                ql.predict(states.reshape(1, states_no))[0]
            ))
            if np.random.rand() <= epsilon:
                action = np.random.randint(len(actions))

            new_state = update_state(states, action)

            print('Action: ',action, end=' ')
            model = expand_model(model, action)

            if model is None:
                acc = baseline
                print('')
            elif logger.repr_from_onehot(states)+','+str(action) not in vals_memory:
                acc = evaluate_model(model, x_train, y_train, x_test, y_test)
                model.save_weights(
                    'weights_cache/'+logger.get_representation(states)+','+str(action))
                print(acc)
            else:
                acc = vals_memory[logger.repr_from_onehot(
                    states)+','+str(action)]
                model.load_weights(
                    'weights_cache/'+logger.get_representation(states)+','+str(action))
                print(str(acc)+'--MEMORY')

            remember(states, action, acc, prev_acc, new_state)
            prev_acc = acc
            states = new_state
            logger.update(states, acc, search_epochs)

        logger.log(states, acc, search_epochs, epsilon)
        replay(ql, min(len(memory), 256))

def update_state(state, action):
    '''
        Update the state, based on the action taken
    '''
    new_state = state.copy()
    # If we added a layer
    if actions[action] != 0:
        index = 0
        # Find the network's depth
        for depth in range(max_depth+1):
            start = depth*(len(actions)-1)
            actives = 0
            for i in range(len(actions)-1):
                actives += state[start+i]
            if actives == 0:
                index = start
                break
        # Set the next layer's state
        onehot_action = onehot_actions[action-1]
        for i in range(len(actions)-1):
            new_state[index+i] = onehot_action[i]
    return new_state

def clear_cache():
    '''
        Clear the model cahce file
    '''
    import os
    folder = 'weights_cache/'
    for the_file in os.listdir(folder):
        file_path = os.path.join(folder, the_file)
        try:
            if os.path.isfile(file_path):
                os.unlink(file_path)
        except Exception as e:
            print(e)

def evaluate_q(ql):
    '''
        Create the best learnt network and evaluate it
    '''
    (x_train, y_train), (x_test, y_test) = get_data()
    states = np.zeros(states_no, dtype='int')
    model = None
    model_str = ''
    for x in range(max_depth):
        action = int(np.argmax(
            ql.predict(states.reshape(1, states_no))[0]
        ))
        model = expand_model(model, action)
        model_str += str(actions[action])+'x'
        states = update_state(states, action)
        print(states)
    print(model_str)

    acc = evaluate_model(model, x_train, y_train, x_test,
                         y_test, epochs=20, verbose_in=1)
    print(acc)

def zero_model(ql):
    '''
        Make model have close to zero output
    '''
    states = np.zeros(states_no, dtype='int')
    y_train = []
    x_train = []
    samples = 10000
    for i in range(samples):
        x_train.append(states.copy())
        y_train.append(np.zeros(len(actions), dtype='int'))
        states[np.random.randint(states_no)] = np.abs(
            states[np.random.randint(states_no)]-1)
    x_train = np.array(x_train).reshape(samples, states_no)
    y_train = np.array(y_train).reshape(samples, len(actions))
    ql.fit(x_train, y_train, epochs=5, verbose=1)

def update_q_target(ql, qv):
    '''
        Copy weights from learner network to target value network
    '''
    ql.save_weights('ql_weights')
    qv.load_weights('ql_weights')

def get_q_net():
    '''
        Create the Q network
    '''
    model = Sequential()
    model.add(Dense(128, activation='linear', input_dim=states_no))
    model.add(LeakyReLU(alpha=.001))
    model.add(Dropout(0.1))
    model.add(Dense(len(actions), activation='tanh'))
    model.compile(loss='mean_squared_error', optimizer='RMSProp')
    model.summary()
    return model

def main():
    global qv
    np.random.seed(7)

    clear_cache()

    ql = get_q_net()
    qv = get_q_net()

    zero_model(ql)
    update_q_target(ql, qv)

    train_q(ql)
    print('Done')
    evaluate_q(ql)

if __name__ == "__main__":
    main()